@tahminator/pipeline 1.0.58 → 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 +123 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/postgres/client.d.ts +11 -0
- package/dist/postgres/client.js +75 -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/package.json +1 -1
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/
|
|
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
|
-
//
|
|
162
|
-
// with `
|
|
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: {
|
|
185
|
-
|
|
186
|
-
|
|
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.
|
|
239
|
-
|
|
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
|
```
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -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
|
+
}
|
|
File without changes
|
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.59-beta.2d7a142f",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "git+https://github.com/tahminator/pipeline.git"
|
|
9
9
|
},
|