@tahminator/pipeline 1.0.59-beta.4cc29c8c → 1.0.59-beta.7832b36f

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
@@ -42,9 +42,30 @@ import {
42
42
  SonarScannerClient,
43
43
  Utils,
44
44
  VersioningClient,
45
+ LocalPostgresClient,
46
+ LocalRedisClient,
45
47
  } from "@tahminator/pipeline";
46
48
  ```
47
49
 
50
+ Jump to client documentation
51
+
52
+ <!-- toc -->
53
+
54
+ - [`GitHubClient`](#githubclient)
55
+ - [`DockerClient`](#dockerclient)
56
+ - [`NPMClient`](#npmclient)
57
+ - [`SonarScannerClient`](#sonarscannerclient)
58
+ - [`Utils`](#utils)
59
+ - [`EnvClient`](#envclient)
60
+ - [`PulumiClient`](#pulumiclient)
61
+ - [`VersioningClient`](#versioningclient)
62
+ - [`LocalPostgresClient`](#localpostgresclient)
63
+ - [`LocalRedisClient`](#localredisclient)
64
+
65
+ <!-- tocstop -->
66
+
67
+ <!-- if toc does not update automatically, `markdown-toc -i README.md` -->
68
+
48
69
  > [!NOTE]
49
70
  > While there is documentation below, each API should be relatively well commented and be more up to date than what is seen below.
50
71
 
@@ -328,3 +349,65 @@ if (!Utils.SemVer.validate(beta)) throw new Error("invalid beta version");
328
349
 
329
350
  await versioning.update(beta);
330
351
  ```
352
+
353
+ ### `LocalPostgresClient`
354
+
355
+ Spin up a disposable local Postgres instance via Docker. Useful for running migrations or acceptance tests against a real database in CI / local dev.
356
+
357
+ Each call to `create()` gets a unique container name and a Docker-assigned host port, so multiple instances can coexist without conflict.
358
+
359
+ ```ts
360
+ import { $ } from "bun";
361
+
362
+ async function main() {
363
+ await using pgClient = await LocalPostgresClient.create({
364
+ database: "instalock-server-acceptance",
365
+ });
366
+
367
+ // get credentials
368
+ const { database, host, port, password, user } = pgClient.state;
369
+
370
+ await $.env({
371
+ ...process.env,
372
+ DB_HOST: host,
373
+ DB_PORT: String(port),
374
+ DB_NAME: database,
375
+ DB_USERNAME: user,
376
+ DB_PASSWORD: password,
377
+ })`just migrate`;
378
+
379
+ // `pgClient` will automatically be cleaned up when `main` returns.
380
+ // Set DEBUG=true in the env to dump container logs on cleanup.
381
+ }
382
+
383
+ await main();
384
+ ```
385
+
386
+ ### `LocalRedisClient`
387
+
388
+ Spin up a disposable local Redis instance via Docker.
389
+
390
+ Each call to `create()` gets a unique container name and a Docker-assigned host port, so multiple instances can coexist without conflict.
391
+
392
+ ```ts
393
+ import { $ } from "bun";
394
+
395
+ async function main() {
396
+ await using redisClient = await LocalRedisClient.create();
397
+
398
+ // get credentials
399
+ const { port, password, host } = redisClient.state;
400
+
401
+ await $.env({
402
+ ...process.env,
403
+ REDIS_HOST: host,
404
+ REDIS_PORT: String(port),
405
+ REDIS_PASSWORD: password,
406
+ })`./gradlew bootRun`;
407
+
408
+ // `redisClient` will automatically be cleaned up when `main` returns.
409
+ // Set DEBUG=true in the env to dump container logs on cleanup.
410
+ }
411
+
412
+ await main();
413
+ ```
@@ -2,6 +2,32 @@ import type { PostgresInternalState, PostgresState } from "./types";
2
2
  export declare class LocalPostgresClient {
3
3
  private readonly iState;
4
4
  private constructor();
5
+ /**
6
+ * Spin up a local Postgres instance via Docker.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ async function main() {
11
+ await using pgClient = await LocalPostgresClient.create({
12
+ database: "instalock-server-acceptance",
13
+ });
14
+
15
+ // get credentials
16
+ const { database, host, port, password, user } = pgClient.state;
17
+
18
+ $.env({
19
+ ...process.env,
20
+ DB_HOST: host,
21
+ DB_PORT: String(port),
22
+ DB_NAME: database,
23
+ DB_USERNAME: user,
24
+ DB_PASSWORD: password,
25
+ })`just migrate`
26
+
27
+ // `pgClient` will be automatically cleaned up at the end of the scope.
28
+ }
29
+ * ```
30
+ */
5
31
  static create(state: PostgresState): Promise<LocalPostgresClient>;
6
32
  private static launch;
7
33
  private static waitUntilReady;
@@ -5,6 +5,32 @@ export class LocalPostgresClient {
5
5
  constructor(iState) {
6
6
  this.iState = iState;
7
7
  }
8
+ /**
9
+ * Spin up a local Postgres instance via Docker.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ async function main() {
14
+ await using pgClient = await LocalPostgresClient.create({
15
+ database: "instalock-server-acceptance",
16
+ });
17
+
18
+ // get credentials
19
+ const { database, host, port, password, user } = pgClient.state;
20
+
21
+ $.env({
22
+ ...process.env,
23
+ DB_HOST: host,
24
+ DB_PORT: String(port),
25
+ DB_NAME: database,
26
+ DB_USERNAME: user,
27
+ DB_PASSWORD: password,
28
+ })`just migrate`
29
+
30
+ // `pgClient` will be automatically cleaned up at the end of the scope.
31
+ }
32
+ * ```
33
+ */
8
34
  static async create(state) {
9
35
  const iState = {
10
36
  ...state,
@@ -37,20 +63,26 @@ export class LocalPostgresClient {
37
63
  static async waitUntilReady(iState) {
38
64
  console.log(`Waiting for ${iState.dockerName} to become ready.`);
39
65
  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);
66
+ const ready = await Utils.waitUntil({
67
+ attempts,
68
+ intervalMs: 2000,
69
+ predicate: async (attempt) => {
70
+ const check = await $ `docker exec ${iState.dockerName} pg_isready -U ${iState.user}`
71
+ .quiet()
72
+ .nothrow();
73
+ if (check.exitCode === 0) {
74
+ return true;
75
+ }
76
+ console.log(`Waiting for ${iState.dockerName}... (${attempt}/${attempts})`);
77
+ return false;
78
+ },
79
+ });
80
+ if (!ready) {
81
+ const msg = `${iState.dockerName} failed to launch`;
82
+ console.error(msg);
83
+ throw new Error(msg);
50
84
  }
51
- const msg = `${iState.dockerName} failed to launch`;
52
- console.error(msg);
53
- throw new Error(msg);
85
+ console.log(`${iState.dockerName} is ready`);
54
86
  }
55
87
  get state() {
56
88
  return this.iState;
@@ -61,13 +93,13 @@ export class LocalPostgresClient {
61
93
  async cleanup() {
62
94
  console.log(`Stopping and removing ${this.iState.dockerName} container...`);
63
95
  if (Utils.Log.isDebug) {
64
- console.log(Utils.Colors.brightMagenta("=== DB LOGS ==="));
96
+ console.log(Utils.Colors.brightMagenta(`=== ${this.iState.dockerName} LOGS ===`));
65
97
  const logs = await $ `docker logs ${this.iState.dockerName}`.text();
66
98
  logs
67
99
  .split("\n")
68
100
  .filter((s) => s.length > 0)
69
101
  .forEach((line) => console.log(Utils.Colors.brightMagenta(line)));
70
- console.log(Utils.Colors.brightMagenta("=== DB LOGS END ==="));
102
+ console.log(Utils.Colors.brightMagenta(`=== ${this.iState.dockerName} LOGS END ===`));
71
103
  }
72
104
  await $ `docker stop ${this.iState.dockerName}`.quiet().nothrow();
73
105
  await $ `docker rm ${this.iState.dockerName}`.quiet().nothrow();
@@ -2,6 +2,32 @@ import type { RedisInternalState, RedisState } from "./types";
2
2
  export declare class LocalRedisClient {
3
3
  private readonly iState;
4
4
  private constructor();
5
+ /**
6
+ * Spin up a local Redis instance via Docker.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ async function main() {
11
+ await using redisClient = await LocalRedisClient.create();
12
+
13
+ // get credentials
14
+ const {
15
+ port,
16
+ password,
17
+ host,
18
+ } = redisClient.state;
19
+
20
+ $.env({
21
+ ...process.env,
22
+ REDISHOST: host,
23
+ REDIS_PORT: String(port),
24
+ REDIS_PASSWORD: password,
25
+ })`./gradlew bootRun`
26
+
27
+ // `redisClient` will be automatically cleaned up at the end of the scope.
28
+ }
29
+ * ```
30
+ */
5
31
  static create(state?: RedisState): Promise<LocalRedisClient>;
6
32
  private static launch;
7
33
  private static waitUntilReady;
@@ -5,6 +5,32 @@ export class LocalRedisClient {
5
5
  constructor(iState) {
6
6
  this.iState = iState;
7
7
  }
8
+ /**
9
+ * Spin up a local Redis instance via Docker.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ async function main() {
14
+ await using redisClient = await LocalRedisClient.create();
15
+
16
+ // get credentials
17
+ const {
18
+ port,
19
+ password,
20
+ host,
21
+ } = redisClient.state;
22
+
23
+ $.env({
24
+ ...process.env,
25
+ REDISHOST: host,
26
+ REDIS_PORT: String(port),
27
+ REDIS_PASSWORD: password,
28
+ })`./gradlew bootRun`
29
+
30
+ // `redisClient` will be automatically cleaned up at the end of the scope.
31
+ }
32
+ * ```
33
+ */
8
34
  static async create(state = {}) {
9
35
  const iState = {
10
36
  ...state,
@@ -34,20 +60,26 @@ export class LocalRedisClient {
34
60
  static async waitUntilReady(iState) {
35
61
  console.log(`Waiting for ${iState.dockerName} to become ready.`);
36
62
  const attempts = 30;
37
- for (let i = 1; i <= attempts; i++) {
38
- const check = await $ `docker exec ${iState.dockerName} redis-cli -a ${iState.password} ping`
39
- .quiet()
40
- .nothrow();
41
- if (check.exitCode === 0) {
42
- console.log(`${iState.dockerName} is ready`);
43
- return;
44
- }
45
- console.log(`Waiting for ${iState.dockerName}... (${i}/${attempts})`);
46
- await Bun.sleep(2000);
63
+ const ready = await Utils.waitUntil({
64
+ attempts,
65
+ intervalMs: 2000,
66
+ predicate: async (attempt) => {
67
+ const check = await $ `docker exec ${iState.dockerName} redis-cli -a ${iState.password} ping`
68
+ .quiet()
69
+ .nothrow();
70
+ if (check.exitCode === 0) {
71
+ return true;
72
+ }
73
+ console.log(`Waiting for ${iState.dockerName}... (${attempt}/${attempts})`);
74
+ return false;
75
+ },
76
+ });
77
+ if (!ready) {
78
+ const msg = `${iState.dockerName} failed to launch`;
79
+ console.error(msg);
80
+ throw new Error(msg);
47
81
  }
48
- const msg = `${iState.dockerName} failed to launch`;
49
- console.error(msg);
50
- throw new Error(msg);
82
+ console.log(`${iState.dockerName} is ready`);
51
83
  }
52
84
  get state() {
53
85
  return this.iState;
@@ -58,13 +90,13 @@ export class LocalRedisClient {
58
90
  async cleanup() {
59
91
  console.log(`Stopping and removing ${this.iState.dockerName} container...`);
60
92
  if (Utils.Log.isDebug) {
61
- console.log(Utils.Colors.brightMagenta("=== REDIS LOGS ==="));
93
+ console.log(Utils.Colors.brightMagenta(`=== ${this.iState.dockerName} LOGS ===`));
62
94
  const logs = await $ `docker logs ${this.iState.dockerName}`.text();
63
95
  logs
64
96
  .split("\n")
65
97
  .filter((s) => s.length > 0)
66
98
  .forEach((line) => console.log(Utils.Colors.brightMagenta(line)));
67
- console.log(Utils.Colors.brightMagenta("=== REDIS LOGS END ==="));
99
+ console.log(Utils.Colors.brightMagenta(`=== ${this.iState.dockerName} LOGS END ===`));
68
100
  }
69
101
  await $ `docker stop ${this.iState.dockerName}`.quiet().nothrow();
70
102
  await $ `docker rm ${this.iState.dockerName}`.quiet().nothrow();
@@ -4,6 +4,7 @@ import { Colors } from "./colors";
4
4
  import { Log } from "./log";
5
5
  import { SemVer } from "./semver";
6
6
  import { generateShortId } from "./short";
7
+ import { waitUntil } from "./wait";
7
8
  export declare class Utils {
8
9
  static Colors: typeof Colors;
9
10
  static Log: typeof Log;
@@ -11,4 +12,5 @@ export declare class Utils {
11
12
  static generateShortId(...args: Parameters<typeof generateShortId>): string;
12
13
  static isCmdAvailable(...args: Parameters<typeof isCmdAvailable>): Promise<boolean>;
13
14
  static decodeBase64EncodedString(...args: Parameters<typeof decodeBase64EncodedString>): Promise<string>;
15
+ static waitUntil(...args: Parameters<typeof waitUntil>): Promise<boolean>;
14
16
  }
@@ -4,6 +4,7 @@ import { Colors } from "./colors";
4
4
  import { Log } from "./log";
5
5
  import { SemVer } from "./semver";
6
6
  import { generateShortId } from "./short";
7
+ import { waitUntil } from "./wait";
7
8
  export class Utils {
8
9
  // hoist
9
10
  static Colors = Colors;
@@ -18,4 +19,7 @@ export class Utils {
18
19
  static async decodeBase64EncodedString(...args) {
19
20
  return decodeBase64EncodedString(...args);
20
21
  }
22
+ static async waitUntil(...args) {
23
+ return waitUntil(...args);
24
+ }
21
25
  }
@@ -0,0 +1,5 @@
1
+ export declare function waitUntil({ predicate, attempts, intervalMs, }: {
2
+ predicate: (attempt: number) => boolean | Promise<boolean>;
3
+ attempts?: number;
4
+ intervalMs?: number;
5
+ }): Promise<boolean>;
@@ -0,0 +1,9 @@
1
+ export async function waitUntil({ predicate, attempts = 30, intervalMs = 2000, }) {
2
+ for (let i = 1; i <= attempts; i++) {
3
+ if (await predicate(i)) {
4
+ return true;
5
+ }
6
+ await Bun.sleep(intervalMs);
7
+ }
8
+ return false;
9
+ }
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.59-beta.4cc29c8c",
6
+ "version": "1.0.59-beta.7832b36f",
7
7
  "repository": {
8
8
  "url": "git+https://github.com/tahminator/pipeline.git"
9
9
  },