@tahminator/pipeline 1.0.57 → 1.0.59-beta.2d7a142f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ A collection of APIs built around Bun Shell that can be re-used in various CICD
9
9
  ## Examples
10
10
 
11
11
  - [`tahminator/instalock-web/.github/workflows`](https://github.com/tahminator/instalock-web/blob/main/.github), a monorepo with over 30k tracked users & 650 active in production.
12
- - [`tahminator/sapling/.github/workflows`](https://github.com/tahminator/instalock-web/blob/main/.github), an Express library that makes backend development easier & less painful (used in `instalock-web`)
12
+ - [`tahminator/sapling/.github/workflows`](https://github.com/tahminator/sapling/blob/main/.github), an Express library that makes backend development easier & less painful (used in `instalock-web`)
13
13
  - [`tahminator/pipeline/src/internal`](https://github.com/tahminator/pipeline/blob/main/src/internal), which is used to help build, package & test this library
14
14
 
15
15
  ## Setup
@@ -35,10 +35,13 @@ bun run src/index.ts
35
35
  ```ts
36
36
  import {
37
37
  DockerClient,
38
+ EnvClient,
38
39
  GitHubClient,
39
40
  NPMClient,
41
+ PulumiClient,
40
42
  SonarScannerClient,
41
43
  Utils,
44
+ VersioningClient,
42
45
  } from "@tahminator/pipeline";
43
46
  ```
44
47
 
@@ -64,22 +67,6 @@ const client = await GitHubClient.createWithGithubAppToken({
64
67
  installationId: process.env.GH_INSTALLATION_ID!,
65
68
  });
66
69
 
67
- // requires gh app
68
- await client.createTag({
69
- releaseType: "patch", // default is patch
70
- // can also be automatically be inferred from env.GITHUB_REPOSITORY which is automatically injected in Actions
71
- repositoryOverride: ["tahminator", "my-service"],
72
- });
73
-
74
- await client.createTag({
75
- releaseType: "minor",
76
- onPreTagCreate: async (tag) => {
77
- // will set `version` key for all `package.json` excluding `node_modules/`
78
- await Utils.updateAllPackageJsonsWithVersion(tag);
79
- // you can write you own logic here if you would like
80
- },
81
- });
82
-
83
70
  // output to env.GITHUB_OUTPUT to re-use outputs across steps, jobs, outputs, etc.
84
71
  // Hover over type in IDE to see more details
85
72
  await client.outputToGithubOutput({
@@ -98,10 +85,36 @@ await client.updateK8sTagWithPR({
98
85
  originRepo: ["tahminator", "pipeline"],
99
86
  manifestRepo: ["tahminator", "infra"],
100
87
  });
88
+
89
+ await client.sendPrMessage({
90
+ owner: "tahminator",
91
+ repository: "pipeline",
92
+ prId: 123,
93
+ message: "Deployed and healthy.",
94
+ });
95
+
96
+ // use VersioningClient to compute your next semver tag.
97
+ // look for `VersioningClient` section for how to configure / use
98
+ const versioning = new VersioningClient(client, VersionUpdatingStrategy.JSTS);
99
+
100
+ const nextTag =
101
+ // requires gh app
102
+ await client.createTag({
103
+ nextTag: await versioning.next("1.5.0", {
104
+ repositoryOverride: ["tahminator", "my-service"],
105
+ }),
106
+ repositoryOverride: ["tahminator", "my-service"],
107
+ onPreTagCreate: async (tag) => {
108
+ // update versions across your repo before tagging (strategy dependent)
109
+ await versioning.update(tag);
110
+ // you can write you own logic here if you would like
111
+ },
112
+ });
101
113
  ```
102
114
 
103
115
  ```ts
104
116
  // client cannot do most automations without getting skipped by CICD, but it can do many read operations and all CI operations just fine
117
+ // uses GH_TOKEN from env
105
118
  const client = await GitHubClient.createWithDefaultCiToken();
106
119
 
107
120
  await client.outputToGithubOutput({
@@ -158,8 +171,8 @@ await npm.publish();
158
171
  await npm.publish(true);
159
172
 
160
173
  // publish to the beta dist-tag instead of latest
161
- // just like before, you should use `Utils.updateAllPackageJsonsWithVersion`
162
- // with `Utils.SemVar.validate(betaTag)` to set a valid version
174
+ // validate versions with `Utils.SemVer.validate(...)`
175
+ // and update versions with `VersioningClient.update(...)` if desired
163
176
  await npm.publish(false, true);
164
177
  ```
165
178
 
@@ -181,9 +194,10 @@ const backendClient = new SonarScannerClient({
181
194
  projectKey: "my-org_my-java-service",
182
195
  organization: "my-org",
183
196
  sourceCodeDir: "src/main/java",
184
- additionalArgs: { // all args are automatically wrapped in `-Dsonar.${key}=${value}`
185
- java.binaries: "target/classes",
186
- coverage.jacoco.xmlReportPaths: "target/site/jacoco/jacoco.xml",
197
+ additionalArgs: {
198
+ // all args are automatically wrapped in `-Dsonar.${key}=${value}`
199
+ "java.binaries": "target/classes",
200
+ "coverage.jacoco.xmlReportPaths": "target/site/jacoco/jacoco.xml",
187
201
  },
188
202
  },
189
203
  });
@@ -203,7 +217,7 @@ const frontendClient = new SonarScannerClient({
203
217
  organization: "my-org",
204
218
  sourceCodeDir: "js/src",
205
219
  additionalArgs: {
206
- javascript.lcov.reportPaths: "js/coverage/lcov.info",
220
+ "javascript.lcov.reportPaths": "js/coverage/lcov.info",
207
221
  },
208
222
  },
209
223
  });
@@ -221,21 +235,96 @@ const githubPrivateKey = await Utils.decodeBase64EncodedString(
221
235
  process.env.GH_PRIVATE_KEY_B64!,
222
236
  );
223
237
 
224
- // will read from git-crypt encrypted variable so long as git-crypt & gpg are setup
225
- // will only consume in memory
226
- const env = await Utils.getEnvVariables(["shared", "production"], {
227
- baseDir: "apps/backend",
228
- });
229
-
230
238
  const shortId = Utils.generateShortId();
231
239
 
232
- await Utils.updateAllPackageJsonsWithVersion("1.2.3");
233
-
234
240
  if (await Utils.isCmdAvailable("gh")) {
235
241
  console.log(Utils.Colors.green(`gh is installed (${shortId})`));
236
242
  }
237
243
 
238
- if (Utils.Log.isDebug) {
239
- console.log(env.DATABASE_URL);
240
- }
244
+ if (!Utils.SemVer.validate("1.2.3")) throw new Error("invalid version");
245
+ ```
246
+
247
+ ### `EnvClient`
248
+
249
+ Load environment variables from encrypted files and automatically mask them in GitHub Actions logs.
250
+
251
+ Supports:
252
+
253
+ - `EnvClientStrategy.SOPS` (YAML files decrypted via `sops`)
254
+ - `EnvClientStrategy.GIT_CRYPT` (`.env` style files via `git-crypt unlock`)
255
+
256
+ ```ts
257
+ const envClient = EnvClient.create(EnvClientStrategy.SOPS);
258
+
259
+ // YAML only
260
+ const env = await envClient.readFromEnv("production.yaml", {
261
+ baseDir: "apps/backend",
262
+ });
263
+
264
+ // `env` is a Record<string, string>
265
+ console.log(env.DATABASE_URL);
266
+ ```
267
+
268
+ ```ts
269
+ // will automatically decrypt files via `git-crypt unlock`
270
+ const envClient = EnvClient.create(EnvClientStrategy.GIT_CRYPT);
271
+ const env = await envClient.readFromEnv(".env.ci");
272
+ ```
273
+
274
+ ### `PulumiClient`
275
+
276
+ Interface with Pulumi Automation API (local workspace strategy).
277
+
278
+ ```ts
279
+ const client = await PulumiClient.create({
280
+ strategy: PulumiClientStrategy.AZURE,
281
+ stackName: "production",
282
+ workDir: "./infra",
283
+ envs: {
284
+ PULUMI_BACKEND_URL: process.env.PULUMI_BACKEND_URL!,
285
+ ARM_CLIENT_ID: process.env.ARM_CLIENT_ID,
286
+ ARM_CLIENT_SECRET: process.env.ARM_CLIENT_SECRET,
287
+ ARM_TENANT_ID: process.env.ARM_TENANT_ID,
288
+ ARM_SUBSCRIPTION_ID: process.env.ARM_SUBSCRIPTION_ID,
289
+ },
290
+ });
291
+
292
+ const preview = await client.preview({
293
+ diff: true,
294
+ rewriteStdoutToDiffFriendly: true,
295
+ });
296
+
297
+ console.log(preview.stdout);
298
+ console.log(PulumiClient.parseChangeSumaryToPrettyTable(preview.changeSummary));
299
+ ```
300
+
301
+ ### `VersioningClient`
302
+
303
+ Utilities to compute the next semver tag from GitHub and update versions in codebases.
304
+
305
+ ```ts
306
+ const gh = await GitHubClient.createWithDefaultCiToken();
307
+
308
+ const versioning = new VersioningClient(gh, VersionUpdatingStrategy.JSTS);
309
+
310
+ // if your repo has no tags yet, this will return `1.0.0`
311
+ // if `baseVersion` is provided, its patch number must be `0` (e.g. `1.5.0`)
312
+ const next = await versioning.next("1.5.0");
313
+
314
+ // update versions across your repo (strategy dependent)
315
+ await versioning.update(next);
316
+ ```
317
+
318
+ ```ts
319
+ // beta tags
320
+ const gh = await GitHubClient.createWithDefaultCiToken();
321
+ const versioning = new VersioningClient(gh, VersionUpdatingStrategy.JSTS);
322
+
323
+ const sha = process.env.GITHUB_SHA!;
324
+ const shortSha = sha.slice(0, 8);
325
+
326
+ const beta = await versioning.nextBeta(shortSha);
327
+ if (!Utils.SemVer.validate(beta)) throw new Error("invalid beta version");
328
+
329
+ await versioning.update(beta);
241
330
  ```
@@ -4,7 +4,6 @@ import { GitHubTagManager } from "./tag";
4
4
  export declare class GitHubClient {
5
5
  private readonly client;
6
6
  private readonly isExplicitToken;
7
- static readonly BASE_VERSION = "1.0.0";
8
7
  private readonly tagManager;
9
8
  private readonly outputManager;
10
9
  private readonly prManager;
package/dist/gh/client.js CHANGED
@@ -6,7 +6,6 @@ import { GitHubTagManager } from "./tag";
6
6
  export class GitHubClient {
7
7
  client;
8
8
  isExplicitToken;
9
- static BASE_VERSION = GitHubTagManager.BASE_VERSION;
10
9
  tagManager;
11
10
  outputManager;
12
11
  prManager;
@@ -1,10 +1,8 @@
1
1
  import { Octokit } from "@octokit/rest";
2
- import semver from "semver";
3
2
  import type { OwnerString, RepoString } from "../../types";
4
3
  export declare class GitHubTagManager {
5
4
  private readonly client;
6
5
  private readonly isExplicitToken;
7
- static readonly BASE_VERSION = "1.0.0";
8
6
  constructor(client: Octokit, isExplicitToken: boolean);
9
7
  private checkToken;
10
8
  /**
@@ -19,10 +17,12 @@ export declare class GitHubTagManager {
19
17
  * @note You **must** pass in a GitHub token because the regular Github bot token
20
18
  * cannot trigger actions (due to fear of recursion). You must either provide a GitHub App token or
21
19
  * a GitHub PAT.
20
+ *
21
+ * @note you should use `VersioningClient` to generate `nextTag`
22
22
  */
23
- createTag({ repositoryOverride, releaseType, onPreTagCreate, }: {
23
+ createTag({ nextTag, repositoryOverride, onPreTagCreate, }: {
24
+ nextTag: string;
24
25
  repositoryOverride?: [OwnerString, RepoString];
25
- releaseType?: semver.ReleaseType;
26
26
  onPreTagCreate?: (tag: string) => Promise<void>;
27
27
  }): Promise<void>;
28
28
  private parseRepository;
@@ -4,7 +4,6 @@ import semver from "semver";
4
4
  export class GitHubTagManager {
5
5
  client;
6
6
  isExplicitToken;
7
- static BASE_VERSION = "1.0.0";
8
7
  constructor(client, isExplicitToken) {
9
8
  this.client = client;
10
9
  this.isExplicitToken = isExplicitToken;
@@ -37,19 +36,12 @@ export class GitHubTagManager {
37
36
  * @note You **must** pass in a GitHub token because the regular Github bot token
38
37
  * cannot trigger actions (due to fear of recursion). You must either provide a GitHub App token or
39
38
  * a GitHub PAT.
39
+ *
40
+ * @note you should use `VersioningClient` to generate `nextTag`
40
41
  */
41
- async createTag({ repositoryOverride, releaseType, onPreTagCreate, }) {
42
+ async createTag({ nextTag, repositoryOverride, onPreTagCreate, }) {
42
43
  this.checkToken();
43
44
  const [owner, repo] = this.parseRepository(repositoryOverride);
44
- const lastTag = await this.getLatestTag({
45
- repositoryOverride: [owner, repo],
46
- });
47
- const nextTag = lastTag ?
48
- semver.inc(lastTag, releaseType ?? "patch")
49
- : GitHubTagManager.BASE_VERSION;
50
- if (!nextTag) {
51
- throw new Error("Could not increment version");
52
- }
53
45
  const { data: repository } = await this.client.rest.repos.get({
54
46
  owner,
55
47
  repo,
package/dist/index.d.ts CHANGED
@@ -5,5 +5,6 @@ export * from "./sonar";
5
5
  export * from "./pulumi";
6
6
  export * from "./types";
7
7
  export * from "./env";
8
+ export * from "./postgres";
8
9
  export * from "./utils";
9
10
  export * from "./versioning";
package/dist/index.js CHANGED
@@ -5,5 +5,6 @@ export * from "./sonar";
5
5
  export * from "./pulumi";
6
6
  export * from "./types";
7
7
  export * from "./env";
8
+ export * from "./postgres";
8
9
  export * from "./utils";
9
10
  export * from "./versioning";
@@ -1,6 +1,7 @@
1
1
  import { EnvClient, EnvClientStrategy } from "../../env";
2
2
  import { GitHubClient } from "../../gh";
3
3
  import { Utils } from "../../utils";
4
+ import { VersioningClient, VersionUpdatingStrategy } from "../../versioning";
4
5
  async function main() {
5
6
  const envClient = EnvClient.create(EnvClientStrategy.GIT_CRYPT);
6
7
  const { githubAppAppId, githubAppInstallationId, githubAppPrivateKeyB64 } = parseCiEnv(await envClient.readFromEnv(".env.ci"));
@@ -9,7 +10,10 @@ async function main() {
9
10
  installationId: githubAppInstallationId,
10
11
  privateKey: await Utils.decodeBase64EncodedString(githubAppPrivateKeyB64),
11
12
  });
13
+ const versioningClient = new VersioningClient(ghClient, VersionUpdatingStrategy.JSTS);
14
+ const rootPkgJson = await Bun.file("./package.json").json();
12
15
  await ghClient.createTag({
16
+ nextTag: await versioningClient.next(rootPkgJson.version),
13
17
  onPreTagCreate: async (tag) => {
14
18
  const file = Bun.file("./package.json");
15
19
  const pkg = await file.json();
@@ -28,8 +28,7 @@ async function main() {
28
28
  const npmClient = await NPMClient.create();
29
29
  const versioningClient = new VersioningClient(ghClient, VersionUpdatingStrategy.JSTS);
30
30
  const shortSha = await getShortSha(sha);
31
- const lastTag = (await ghClient.getLatestTag()) ?? GitHubClient.BASE_VERSION;
32
- const betaVersion = `${lastTag}-beta.${shortSha}`;
31
+ const betaVersion = await versioningClient.nextBeta(shortSha);
33
32
  if (!Utils.SemVer.validate(betaVersion)) {
34
33
  throw new Error(`Generated invalid beta version: ${betaVersion}`);
35
34
  }
@@ -0,0 +1,11 @@
1
+ import type { PostgresInternalState, PostgresState } from "./types";
2
+ export declare class LocalPostgresClient {
3
+ private readonly iState;
4
+ private constructor();
5
+ static create(state: PostgresState): Promise<LocalPostgresClient>;
6
+ private static launch;
7
+ private static waitUntilReady;
8
+ get state(): PostgresInternalState;
9
+ [Symbol.asyncDispose](): Promise<void>;
10
+ cleanup(): Promise<void>;
11
+ }
@@ -0,0 +1,75 @@
1
+ import { $, randomUUIDv7 } from "bun";
2
+ import { Utils } from "../utils";
3
+ export class LocalPostgresClient {
4
+ iState;
5
+ constructor(iState) {
6
+ this.iState = iState;
7
+ }
8
+ static async create(state) {
9
+ const iState = {
10
+ ...state,
11
+ user: "postgres",
12
+ password: "postgres",
13
+ port: 5432,
14
+ host: "127.0.0.1",
15
+ dockerName: `local-db-${randomUUIDv7()}`,
16
+ };
17
+ const hostPort = await this.launch(iState);
18
+ iState.port = hostPort;
19
+ await this.waitUntilReady(iState);
20
+ return new this(iState);
21
+ }
22
+ static async launch(iState) {
23
+ await $ `docker run -d \
24
+ --name ${iState.dockerName} \
25
+ -e POSTGRES_USER=${iState.user} \
26
+ -e POSTGRES_PASSWORD=${iState.password} \
27
+ -e POSTGRES_DB=${iState.database} \
28
+ -p 5432 \
29
+ mirror.gcr.io/library/postgres:16-alpine`;
30
+ const raw = (await $ `docker port ${iState.dockerName} 5432/tcp`.text()).trim();
31
+ const match = raw.match(/:(\d+)/);
32
+ if (!match) {
33
+ throw new Error(`Could not parse host port from: ${raw}`);
34
+ }
35
+ return Number(match[1]);
36
+ }
37
+ static async waitUntilReady(iState) {
38
+ console.log(`Waiting for ${iState.dockerName} to become ready.`);
39
+ const attempts = 30;
40
+ for (let i = 1; i <= attempts; i++) {
41
+ const check = await $ `docker exec ${iState.dockerName} pg_isready -U ${iState.user}`
42
+ .quiet()
43
+ .nothrow();
44
+ if (check.exitCode === 0) {
45
+ console.log(`${iState.dockerName} is ready`);
46
+ return;
47
+ }
48
+ console.log(`Waiting for ${iState.dockerName}... (${i}/${attempts})`);
49
+ await Bun.sleep(2000);
50
+ }
51
+ const msg = `${iState.dockerName} failed to launch`;
52
+ console.error(msg);
53
+ throw new Error(msg);
54
+ }
55
+ get state() {
56
+ return this.iState;
57
+ }
58
+ async [Symbol.asyncDispose]() {
59
+ await this.cleanup();
60
+ }
61
+ async cleanup() {
62
+ console.log(`Stopping and removing ${this.iState.dockerName} container...`);
63
+ if (Utils.Log.isDebug) {
64
+ console.log(Utils.Colors.brightMagenta("=== DB LOGS ==="));
65
+ const logs = await $ `docker logs ${this.iState.dockerName}`.text();
66
+ logs
67
+ .split("\n")
68
+ .filter((s) => s.length > 0)
69
+ .forEach((line) => console.log(Utils.Colors.brightMagenta(line)));
70
+ console.log(Utils.Colors.brightMagenta("=== DB LOGS END ==="));
71
+ }
72
+ await $ `docker stop ${this.iState.dockerName}`.quiet().nothrow();
73
+ await $ `docker rm ${this.iState.dockerName}`.quiet().nothrow();
74
+ }
75
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./client";
2
+ export * from "./types";
@@ -0,0 +1,2 @@
1
+ export * from "./client";
2
+ export * from "./types";
@@ -0,0 +1,10 @@
1
+ export interface PostgresState {
2
+ database: string;
3
+ }
4
+ export interface PostgresInternalState extends PostgresState {
5
+ port: number;
6
+ host: string;
7
+ user: string;
8
+ password: string;
9
+ dockerName: string;
10
+ }
File without changes
@@ -23,4 +23,12 @@ export declare class VersioningClient implements IVersioningClient {
23
23
  * `baseVersion` must set patch number to `0`. (e.g. `1.0.0`, `1.5.0`, `2.0.0`, etc.)
24
24
  */
25
25
  next(baseVersion?: string, ...opts: Parameters<GitHubClient["getLatestTag"]>): Promise<string>;
26
+ /**
27
+ * generate next beta version tag using a `sha`.
28
+ *
29
+ * simply finds latest version from github and generates `{version}-beta.{sha}` to it.
30
+ *
31
+ * for example, `1.3.2-beta.58cf28bd`
32
+ */
33
+ nextBeta(sha: string, ...opts: Parameters<GitHubClient["getLatestTag"]>): Promise<string>;
26
34
  }
@@ -67,4 +67,19 @@ export class VersioningClient {
67
67
  }
68
68
  return baseVersionSemver.toString();
69
69
  }
70
+ /**
71
+ * generate next beta version tag using a `sha`.
72
+ *
73
+ * simply finds latest version from github and generates `{version}-beta.{sha}` to it.
74
+ *
75
+ * for example, `1.3.2-beta.58cf28bd`
76
+ */
77
+ async nextBeta(sha, ...opts) {
78
+ const latestTag = await this.githubClient.getLatestTag(...opts);
79
+ if (!latestTag) {
80
+ throw new Error("You must upload a tag atleast once before generating a beta tag.");
81
+ }
82
+ const latest = this.parseOrThrow("latest tag from github", latestTag);
83
+ return `${latest.toString()}-beta.${sha}`;
84
+ }
70
85
  }
@@ -21,6 +21,16 @@ export interface IVersioningClient extends IVersionUpdatingClient {
21
21
  * `baseVersion` must set patch number to `0`. (e.g. `1.0.0`, `1.5.0`, `2.0.0`, etc.)
22
22
  */
23
23
  next(baseVersion?: string): Promise<string>;
24
+ /**
25
+ * generate next beta version tag.
26
+ *
27
+ * simply finds latest version from github and generates `{version}-beta.{sha}` to it.
28
+ *
29
+ * @note you may pass in the first 8 characters of `sha` if you choose.
30
+ *
31
+ * for example, `1.3.2-beta.58cf28bd`
32
+ */
33
+ nextBeta(sha: string): Promise<string>;
24
34
  }
25
35
  export interface IJavascriptPackageJsonVersionUpdatingClient extends IVersionUpdatingClient {
26
36
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "type": "module",
4
4
  "author": "Tahmid Ahmed",
5
5
  "description": "A collection of Bun shell scripts that can be re-used in various CICD pipelines.",
6
- "version": "1.0.57",
6
+ "version": "1.0.59-beta.2d7a142f",
7
7
  "repository": {
8
8
  "url": "git+https://github.com/tahminator/pipeline.git"
9
9
  },