@tahminator/pipeline
v1.0.59
Published
A collection of Bun shell scripts that can be re-used in various CICD pipelines.
Readme
A collection of APIs built around Bun Shell that can be re-used in various CICD pipelines.
Examples
tahminator/instalock-web/.github/workflows, a monorepo with over 30k tracked users & 650 active in production.tahminator/sapling/.github/workflows, an Express library that makes backend development easier & less painful (used ininstalock-web)tahminator/pipeline/src/internal, which is used to help build, package & test this library
Setup
To install dependencies:
bun installTo run:
# note not all dependencies may run directly in a local environment
bun run src/index.ts[!WARNING] This repository is iterating quickly & as such may have rough edges. I will always be happy to respond to & fix any issues anyone may have :)
Available Clients
import {
DockerClient,
EnvClient,
GitHubClient,
NPMClient,
PulumiClient,
SonarScannerClient,
Utils,
VersioningClient,
} from "@tahminator/pipeline";[!NOTE] While there is documentation below, each API should be relatively well commented and be more up to date than what is seen below.
GitHubClient
Interface with GitHub / GitHub Actions API
This client has two auth strategies: createWithGithubAppToken() and createWithDefaultCiToken()
NOTE: If you want to automate pull requests, tags, or anything else that may trigger GitHub Actions, you must use createWithGithubAppToken(); there is no exception
It is strongly recommended that you basically always use createWithGithubAppToken()
const client = await GitHubClient.createWithGithubAppToken({
appId: process.env.GH_APP_ID!,
privateKey: await Utils.decodeBase64EncodedString(
process.env.GH_PRIVATE_KEY_B64!,
),
installationId: process.env.GH_INSTALLATION_ID!,
});
// output to env.GITHUB_OUTPUT to re-use outputs across steps, jobs, outputs, etc.
// Hover over type in IDE to see more details
await client.outputToGithubOutput({
ctx: {
imageTag: "1.2.3",
deploy: { env: "production", healthy: true },
},
});
// updates kubernetes manifest repo. Hover over type of function and each key in argument in IDE to see more details
await client.updateK8sTagWithPR({
newTag: "1.2.3",
imageName: "tahminator/my-service",
kustomizationFilePath: "apps/prod/kustomization.yaml",
environment: "production",
originRepo: ["tahminator", "pipeline"],
manifestRepo: ["tahminator", "infra"],
});
await client.sendPrMessage({
owner: "tahminator",
repository: "pipeline",
prId: 123,
message: "Deployed and healthy.",
});
// use VersioningClient to compute your next semver tag.
// look for `VersioningClient` section for how to configure / use
const versioning = new VersioningClient(client, VersionUpdatingStrategy.JSTS);
const nextTag =
// requires gh app
await client.createTag({
nextTag: await versioning.next("1.5.0", {
repositoryOverride: ["tahminator", "my-service"],
}),
repositoryOverride: ["tahminator", "my-service"],
onPreTagCreate: async (tag) => {
// update versions across your repo before tagging (strategy dependent)
await versioning.update(tag);
// you can write you own logic here if you would like
},
});// client cannot do most automations without getting skipped by CICD, but it can do many read operations and all CI operations just fine
// uses GH_TOKEN from env
const client = await GitHubClient.createWithDefaultCiToken();
await client.outputToGithubOutput({
ctx: { imageTag: "1.2.3" },
});DockerClient
Interface with Docker to build & deploy images
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management
await using client = await DockerClient.create(
process.env.DOCKER_USERNAME!,
process.env.DOCKER_PAT!,
);
// `buildImage` will build AND deploy
await client.buildImage({
dockerFileLocation: "./Dockerfile",
dockerRepository: "my-service",
tags: ["latest", "sha-abc123"], // define as many as you would like
platforms: ["linux/amd64", "linux/arm64"], // build for multiple platforms
buildArgs: { NODE_ENV: "production" }, // pass in build args
});
await client.buildImage({
dockerFileLocation: "./Dockerfile",
dockerRepository: "my-service",
tags: ["local-dev"],
shouldUpload: false, // dry run, do not actually upload
});
// find tag by given name,
await client.promoteDockerImage({
repository: "my-service",
originalTag: "sha-abc123",
newGithubTags: ["staging", "1.2.3"],
});NPMClient
Interface with NPM registry in order to publish packages to npmjs.com
// uses trusted publishing, token based uploads are not supported
const npm = await NPMClient.create();
await npm.publish();
// dry run
await npm.publish(true);
// publish to the beta dist-tag instead of latest
// validate versions with `Utils.SemVer.validate(...)`
// and update versions with `VersioningClient.update(...)` if desired
await npm.publish(false, true);SonarScannerClient
Interface with SonarQube and upload test coverage for basically any language / framework that is supported by Sonar Scanner.
import { $ } from "bun";
const backendClient = new SonarScannerClient({
auth: {
token: process.env.SONAR_TOKEN!,
},
run: {
runTestsCmd: $`./mvnw clean verify -Dspring.profiles.active=ci`,
},
scan: {
projectKey: "my-org_my-java-service",
organization: "my-org",
sourceCodeDir: "src/main/java",
additionalArgs: {
// all args are automatically wrapped in `-Dsonar.${key}=${value}`
"java.binaries": "target/classes",
"coverage.jacoco.xmlReportPaths": "target/site/jacoco/jacoco.xml",
},
},
});
await backendClient.runTests();
await backendClient.uploadTestCoverage();
const frontendClient = new SonarScannerClient({
auth: {
token: process.env.SONAR_TOKEN!,
},
run: {
runTestsCmd: $`pnpm run --dir=js test`,
},
scan: {
projectKey: "my-org_my-ts-service",
organization: "my-org",
sourceCodeDir: "js/src",
additionalArgs: {
"javascript.lcov.reportPaths": "js/coverage/lcov.info",
},
},
});
await frontendClient.runTests();
await frontendClient.uploadTestCoverage();Utils
Assorted helper class used by clients and useful as an exported class to pipelines.
const githubPrivateKey = await Utils.decodeBase64EncodedString(
process.env.GH_PRIVATE_KEY_B64!,
);
const shortId = Utils.generateShortId();
if (await Utils.isCmdAvailable("gh")) {
console.log(Utils.Colors.green(`gh is installed (${shortId})`));
}
if (!Utils.SemVer.validate("1.2.3")) throw new Error("invalid version");EnvClient
Load environment variables from encrypted files and automatically mask them in GitHub Actions logs.
Supports:
EnvClientStrategy.SOPS(YAML files decrypted viasops)EnvClientStrategy.GIT_CRYPT(.envstyle files viagit-crypt unlock)
const envClient = EnvClient.create(EnvClientStrategy.SOPS);
// YAML only
const env = await envClient.readFromEnv("production.yaml", {
baseDir: "apps/backend",
});
// `env` is a Record<string, string>
console.log(env.DATABASE_URL);// will automatically decrypt files via `git-crypt unlock`
const envClient = EnvClient.create(EnvClientStrategy.GIT_CRYPT);
const env = await envClient.readFromEnv(".env.ci");PulumiClient
Interface with Pulumi Automation API (local workspace strategy).
const client = await PulumiClient.create({
strategy: PulumiClientStrategy.AZURE,
stackName: "production",
workDir: "./infra",
envs: {
PULUMI_BACKEND_URL: process.env.PULUMI_BACKEND_URL!,
ARM_CLIENT_ID: process.env.ARM_CLIENT_ID,
ARM_CLIENT_SECRET: process.env.ARM_CLIENT_SECRET,
ARM_TENANT_ID: process.env.ARM_TENANT_ID,
ARM_SUBSCRIPTION_ID: process.env.ARM_SUBSCRIPTION_ID,
},
});
const preview = await client.preview({
diff: true,
rewriteStdoutToDiffFriendly: true,
});
console.log(preview.stdout);
console.log(PulumiClient.parseChangeSumaryToPrettyTable(preview.changeSummary));VersioningClient
Utilities to compute the next semver tag from GitHub and update versions in codebases.
const gh = await GitHubClient.createWithDefaultCiToken();
const versioning = new VersioningClient(gh, VersionUpdatingStrategy.JSTS);
// if your repo has no tags yet, this will return `1.0.0`
// if `baseVersion` is provided, its patch number must be `0` (e.g. `1.5.0`)
const next = await versioning.next("1.5.0");
// update versions across your repo (strategy dependent)
await versioning.update(next);// beta tags
const gh = await GitHubClient.createWithDefaultCiToken();
const versioning = new VersioningClient(gh, VersionUpdatingStrategy.JSTS);
const sha = process.env.GITHUB_SHA!;
const shortSha = sha.slice(0, 8);
const beta = await versioning.nextBeta(shortSha);
if (!Utils.SemVer.validate(beta)) throw new Error("invalid beta version");
await versioning.update(beta);