@tahminator/pipeline 1.0.59 → 1.0.60
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 +83 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internal/upload-npm/beta/index.js +1 -1
- package/dist/postgres/client.d.ts +37 -0
- package/dist/postgres/client.js +107 -0
- package/dist/postgres/index.d.ts +2 -0
- package/dist/postgres/index.js +2 -0
- package/dist/postgres/types.d.ts +10 -0
- package/dist/postgres/types.js +0 -0
- package/dist/redis/client.d.ts +37 -0
- package/dist/redis/client.js +104 -0
- package/dist/redis/index.d.ts +2 -0
- package/dist/redis/index.js +2 -0
- package/dist/redis/types.d.ts +8 -0
- package/dist/redis/types.js +0 -0
- package/dist/utils/client.d.ts +2 -0
- package/dist/utils/client.js +4 -0
- package/dist/utils/wait.d.ts +5 -0
- package/dist/utils/wait.js +9 -0
- package/package.json +1 -1
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
|
+
```
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ async function main() {
|
|
|
42
42
|
message: `
|
|
43
43
|
## Test Version Uploaded
|
|
44
44
|
|
|
45
|
-
Uploaded
|
|
45
|
+
Uploaded \`@tahminator/pipeline@${betaVersion}\` to NPM. View version on NPM registry [here](https://www.npmjs.com/package/@tahminator/pipeline/v/${betaVersion}).
|
|
46
46
|
`,
|
|
47
47
|
});
|
|
48
48
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PostgresInternalState, PostgresState } from "./types";
|
|
2
|
+
export declare class LocalPostgresClient {
|
|
3
|
+
private readonly iState;
|
|
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
|
+
*/
|
|
31
|
+
static create(state: PostgresState): Promise<LocalPostgresClient>;
|
|
32
|
+
private static launch;
|
|
33
|
+
private static waitUntilReady;
|
|
34
|
+
get state(): PostgresInternalState;
|
|
35
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
36
|
+
cleanup(): Promise<void>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
/**
|
|
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
|
+
*/
|
|
34
|
+
static async create(state) {
|
|
35
|
+
const iState = {
|
|
36
|
+
...state,
|
|
37
|
+
user: "postgres",
|
|
38
|
+
password: "postgres",
|
|
39
|
+
port: 5432,
|
|
40
|
+
host: "127.0.0.1",
|
|
41
|
+
dockerName: `local-db-${randomUUIDv7()}`,
|
|
42
|
+
};
|
|
43
|
+
const hostPort = await this.launch(iState);
|
|
44
|
+
iState.port = hostPort;
|
|
45
|
+
await this.waitUntilReady(iState);
|
|
46
|
+
return new this(iState);
|
|
47
|
+
}
|
|
48
|
+
static async launch(iState) {
|
|
49
|
+
await $ `docker run -d \
|
|
50
|
+
--name ${iState.dockerName} \
|
|
51
|
+
-e POSTGRES_USER=${iState.user} \
|
|
52
|
+
-e POSTGRES_PASSWORD=${iState.password} \
|
|
53
|
+
-e POSTGRES_DB=${iState.database} \
|
|
54
|
+
-p 5432 \
|
|
55
|
+
mirror.gcr.io/library/postgres:16-alpine`;
|
|
56
|
+
const raw = (await $ `docker port ${iState.dockerName} 5432/tcp`.text()).trim();
|
|
57
|
+
const match = raw.match(/:(\d+)/);
|
|
58
|
+
if (!match) {
|
|
59
|
+
throw new Error(`Could not parse host port from: ${raw}`);
|
|
60
|
+
}
|
|
61
|
+
return Number(match[1]);
|
|
62
|
+
}
|
|
63
|
+
static async waitUntilReady(iState) {
|
|
64
|
+
console.log(`Waiting for ${iState.dockerName} to become ready.`);
|
|
65
|
+
const attempts = 30;
|
|
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);
|
|
84
|
+
}
|
|
85
|
+
console.log(`${iState.dockerName} is ready`);
|
|
86
|
+
}
|
|
87
|
+
get state() {
|
|
88
|
+
return this.iState;
|
|
89
|
+
}
|
|
90
|
+
async [Symbol.asyncDispose]() {
|
|
91
|
+
await this.cleanup();
|
|
92
|
+
}
|
|
93
|
+
async cleanup() {
|
|
94
|
+
console.log(`Stopping and removing ${this.iState.dockerName} container...`);
|
|
95
|
+
if (Utils.Log.isDebug) {
|
|
96
|
+
console.log(Utils.Colors.brightMagenta(`=== ${this.iState.dockerName} LOGS ===`));
|
|
97
|
+
const logs = await $ `docker logs ${this.iState.dockerName}`.text();
|
|
98
|
+
logs
|
|
99
|
+
.split("\n")
|
|
100
|
+
.filter((s) => s.length > 0)
|
|
101
|
+
.forEach((line) => console.log(Utils.Colors.brightMagenta(line)));
|
|
102
|
+
console.log(Utils.Colors.brightMagenta(`=== ${this.iState.dockerName} LOGS END ===`));
|
|
103
|
+
}
|
|
104
|
+
await $ `docker stop ${this.iState.dockerName}`.quiet().nothrow();
|
|
105
|
+
await $ `docker rm ${this.iState.dockerName}`.quiet().nothrow();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RedisInternalState, RedisState } from "./types";
|
|
2
|
+
export declare class LocalRedisClient {
|
|
3
|
+
private readonly iState;
|
|
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
|
+
*/
|
|
31
|
+
static create(state?: RedisState): Promise<LocalRedisClient>;
|
|
32
|
+
private static launch;
|
|
33
|
+
private static waitUntilReady;
|
|
34
|
+
get state(): RedisInternalState;
|
|
35
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
36
|
+
cleanup(): Promise<void>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { $, randomUUIDv7 } from "bun";
|
|
2
|
+
import { Utils } from "../utils";
|
|
3
|
+
export class LocalRedisClient {
|
|
4
|
+
iState;
|
|
5
|
+
constructor(iState) {
|
|
6
|
+
this.iState = iState;
|
|
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
|
+
*/
|
|
34
|
+
static async create(state = {}) {
|
|
35
|
+
const iState = {
|
|
36
|
+
...state,
|
|
37
|
+
password: "redis",
|
|
38
|
+
port: 6379,
|
|
39
|
+
host: "127.0.0.1",
|
|
40
|
+
dockerName: `local-redis-${randomUUIDv7()}`,
|
|
41
|
+
};
|
|
42
|
+
const hostPort = await this.launch(iState);
|
|
43
|
+
iState.port = hostPort;
|
|
44
|
+
await this.waitUntilReady(iState);
|
|
45
|
+
return new this(iState);
|
|
46
|
+
}
|
|
47
|
+
static async launch(iState) {
|
|
48
|
+
await $ `docker run -d \
|
|
49
|
+
--name ${iState.dockerName} \
|
|
50
|
+
-p 6379 \
|
|
51
|
+
mirror.gcr.io/library/redis:7-alpine \
|
|
52
|
+
redis-server --requirepass ${iState.password}`;
|
|
53
|
+
const raw = (await $ `docker port ${iState.dockerName} 6379/tcp`.text()).trim();
|
|
54
|
+
const match = raw.match(/:(\d+)/);
|
|
55
|
+
if (!match) {
|
|
56
|
+
throw new Error(`Could not parse host port from: ${raw}`);
|
|
57
|
+
}
|
|
58
|
+
return Number(match[1]);
|
|
59
|
+
}
|
|
60
|
+
static async waitUntilReady(iState) {
|
|
61
|
+
console.log(`Waiting for ${iState.dockerName} to become ready.`);
|
|
62
|
+
const attempts = 30;
|
|
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);
|
|
81
|
+
}
|
|
82
|
+
console.log(`${iState.dockerName} is ready`);
|
|
83
|
+
}
|
|
84
|
+
get state() {
|
|
85
|
+
return this.iState;
|
|
86
|
+
}
|
|
87
|
+
async [Symbol.asyncDispose]() {
|
|
88
|
+
await this.cleanup();
|
|
89
|
+
}
|
|
90
|
+
async cleanup() {
|
|
91
|
+
console.log(`Stopping and removing ${this.iState.dockerName} container...`);
|
|
92
|
+
if (Utils.Log.isDebug) {
|
|
93
|
+
console.log(Utils.Colors.brightMagenta(`=== ${this.iState.dockerName} LOGS ===`));
|
|
94
|
+
const logs = await $ `docker logs ${this.iState.dockerName}`.text();
|
|
95
|
+
logs
|
|
96
|
+
.split("\n")
|
|
97
|
+
.filter((s) => s.length > 0)
|
|
98
|
+
.forEach((line) => console.log(Utils.Colors.brightMagenta(line)));
|
|
99
|
+
console.log(Utils.Colors.brightMagenta(`=== ${this.iState.dockerName} LOGS END ===`));
|
|
100
|
+
}
|
|
101
|
+
await $ `docker stop ${this.iState.dockerName}`.quiet().nothrow();
|
|
102
|
+
await $ `docker rm ${this.iState.dockerName}`.quiet().nothrow();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
File without changes
|
package/dist/utils/client.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/utils/client.js
CHANGED
|
@@ -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
|
}
|
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.
|
|
6
|
+
"version": "1.0.60",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "git+https://github.com/tahminator/pipeline.git"
|
|
9
9
|
},
|