@tahminator/pipeline 1.0.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ <h1>
2
+ <a href="https://www.npmjs.com/package/@tahminator/pipeline">
3
+ <code>@tahminator/pipeline</code>
4
+ </a>
5
+ </h1>
6
+
7
+ A collection of Bun shell scripts that can be re-used in various CICD pipelines.
8
+
9
+ ## Setup
10
+
11
+ To install dependencies:
12
+
13
+ ```bash
14
+ bun install
15
+ ```
16
+
17
+ To run:
18
+
19
+ ```bash
20
+ bun run src/index.ts
21
+ ```
@@ -0,0 +1,15 @@
1
+ export declare class DockerClient {
2
+ private readonly username;
3
+ private readonly pat;
4
+ private constructor();
5
+ static create(username: string, pat: string): Promise<DockerClient>;
6
+ /**
7
+ * Will lookup the Docker Hub image of `originalTag` & add `newGithubTags` to it.
8
+ */
9
+ promoteDockerImage({ originalTag, newGithubTags, repository, }: {
10
+ originalTag: string;
11
+ newGithubTags: string[];
12
+ repository: string;
13
+ }): Promise<void>;
14
+ cleanup(): Promise<void>;
15
+ }
@@ -0,0 +1,32 @@
1
+ import { $ } from "bun";
2
+ export class DockerClient {
3
+ username;
4
+ pat;
5
+ constructor(username, pat) {
6
+ this.username = username;
7
+ this.pat = pat;
8
+ }
9
+ static async create(username, pat) {
10
+ const client = new DockerClient(username, pat);
11
+ await $ `echo ${pat} | docker login -u ${username} --password-stdin`;
12
+ return client;
13
+ }
14
+ /**
15
+ * Will lookup the Docker Hub image of `originalTag` & add `newGithubTags` to it.
16
+ */
17
+ async promoteDockerImage({ originalTag, newGithubTags, repository, }) {
18
+ const fullRepo = `${this.username}/${repository}`;
19
+ const oldImage = `${fullRepo}:${originalTag}`;
20
+ await $ `docker pull ${oldImage}`;
21
+ for (const tag of newGithubTags) {
22
+ const newImage = `${fullRepo}:${tag}`;
23
+ console.log(`Promoting to ${newImage}...`);
24
+ await $ `docker tag ${oldImage} ${newImage}`;
25
+ await $ `docker push ${newImage}`;
26
+ }
27
+ console.log(`Promoted ${originalTag} to: ${newGithubTags.join(", ")}`);
28
+ }
29
+ async cleanup() {
30
+ await $ `docker logout`;
31
+ }
32
+ }
@@ -0,0 +1,11 @@
1
+ export declare class GitHubClient {
2
+ private readonly client;
3
+ private readonly isExplicitToken;
4
+ private readonly tagManager;
5
+ private readonly outputManager;
6
+ private readonly prManager;
7
+ constructor(ghToken?: string);
8
+ createTag(...args: Parameters<typeof this.tagManager.createTag>): Promise<void>;
9
+ outputToGithubOutput(...args: Parameters<typeof this.outputManager.outputToGithubOutput>): Promise<void>;
10
+ updateK8sTagWithPR(...args: Parameters<typeof this.prManager.updateK8sTagWithPR>): Promise<void>;
11
+ }
@@ -0,0 +1,33 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ import { GitHubOutputManager } from "./output";
3
+ import { GitHubPRManager } from "./pr";
4
+ import { GitHubTagManager } from "./tag";
5
+ export class GitHubClient {
6
+ client;
7
+ isExplicitToken;
8
+ tagManager;
9
+ outputManager;
10
+ prManager;
11
+ constructor(ghToken) {
12
+ this.isExplicitToken = !!ghToken;
13
+ const token = ghToken ?? process.env.GH_TOKEN;
14
+ if (!token) {
15
+ throw new Error("No GitHub token has been provided & GH_TOKEN cannot be found in environment");
16
+ }
17
+ this.client = new Octokit({
18
+ auth: token,
19
+ });
20
+ this.tagManager = new GitHubTagManager(this.client, this.isExplicitToken);
21
+ this.outputManager = new GitHubOutputManager(this.client);
22
+ this.prManager = new GitHubPRManager(this.client);
23
+ }
24
+ createTag(...args) {
25
+ return this.tagManager.createTag(...args);
26
+ }
27
+ outputToGithubOutput(...args) {
28
+ return this.outputManager.outputToGithubOutput(...args);
29
+ }
30
+ updateK8sTagWithPR(...args) {
31
+ return this.prManager.updateK8sTagWithPR(...args);
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ export * from "./client";
@@ -0,0 +1 @@
1
+ export * from "./client";
@@ -0,0 +1,15 @@
1
+ import type { Octokit } from "@octokit/rest";
2
+ export declare class GitHubOutputManager {
3
+ private readonly client;
4
+ constructor(client: Octokit);
5
+ /**
6
+ * Write an output back to Github Actions in order to re-use / pass
7
+ * data between steps, jobs, etc.
8
+ *
9
+ * @see documentation for passing outputs between jobs [here](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/pass-job-outputs)
10
+ */
11
+ outputToGithubOutput({ overrideGithubOutputFile, ctx, }: {
12
+ overrideGithubOutputFile?: string;
13
+ ctx: Record<string, unknown>;
14
+ }): Promise<void>;
15
+ }
@@ -0,0 +1,31 @@
1
+ export class GitHubOutputManager {
2
+ client;
3
+ constructor(client) {
4
+ this.client = client;
5
+ }
6
+ /**
7
+ * Write an output back to Github Actions in order to re-use / pass
8
+ * data between steps, jobs, etc.
9
+ *
10
+ * @see documentation for passing outputs between jobs [here](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/pass-job-outputs)
11
+ */
12
+ async outputToGithubOutput({ overrideGithubOutputFile, ctx, }) {
13
+ const githubOutputEnv = (() => {
14
+ const v = process.env.GITHUB_OUTPUT;
15
+ return v;
16
+ })();
17
+ const githubOutput = overrideGithubOutputFile ?? githubOutputEnv;
18
+ if (!githubOutput) {
19
+ throw new Error("Failed to find GITHUB_OUTPUT from environment and no explicit github output file override defined");
20
+ }
21
+ console.log("Outputting GitHub output...");
22
+ const w = Bun.file(githubOutput).writer();
23
+ for (const [k, v] of Object.entries(ctx)) {
24
+ console.log(`Piping ${k}`);
25
+ await w.write(`${k}<<EOF\n${JSON.stringify(v)}\nEOF\n`);
26
+ }
27
+ await w.flush();
28
+ await w.end();
29
+ console.log("GitHub output complete");
30
+ }
31
+ }
@@ -0,0 +1,38 @@
1
+ import type { Octokit } from "@octokit/rest";
2
+ import type { Environment, OwnerString, RepoString } from "../../types";
3
+ export declare class GitHubPRManager {
4
+ private readonly client;
5
+ constructor(client: Octokit);
6
+ /**
7
+ *
8
+ * update k8s manifest repo with new tag version.
9
+ *
10
+ * @note `kustomizationFile` must look like this:
11
+ *
12
+ * ```yaml
13
+ * apiVersion: kustomize.config.k8s.io/v1beta1
14
+ * kind: Kustomization
15
+ * resources:
16
+ * - deployment.yaml
17
+ * - secrets.yaml
18
+ * - service.yaml
19
+ * - monitor.yaml
20
+ * commonLabels:
21
+ * app: instalock-web
22
+ * environment: production
23
+ * # This part specifically
24
+ * images:
25
+ * - name: tahminator/instalock-web
26
+ * newTag: a70ee0e
27
+ * ```
28
+ */
29
+ updateK8sTagWithPR({ client, newTag, imageName, kustomizationFilePath, environment, originRepo, manifestRepo, }: {
30
+ client: Octokit;
31
+ newTag: string;
32
+ imageName: string;
33
+ kustomizationFilePath: string;
34
+ environment: Environment;
35
+ originRepo: [OwnerString, RepoString];
36
+ manifestRepo: [OwnerString, RepoString];
37
+ }): Promise<void>;
38
+ }
@@ -0,0 +1,101 @@
1
+ import yaml from "yaml";
2
+ import { Utils } from "../../utils";
3
+ import { kustomizeSchema } from "./schema";
4
+ export class GitHubPRManager {
5
+ client;
6
+ constructor(client) {
7
+ this.client = client;
8
+ }
9
+ /**
10
+ *
11
+ * update k8s manifest repo with new tag version.
12
+ *
13
+ * @note `kustomizationFile` must look like this:
14
+ *
15
+ * ```yaml
16
+ * apiVersion: kustomize.config.k8s.io/v1beta1
17
+ * kind: Kustomization
18
+ * resources:
19
+ * - deployment.yaml
20
+ * - secrets.yaml
21
+ * - service.yaml
22
+ * - monitor.yaml
23
+ * commonLabels:
24
+ * app: instalock-web
25
+ * environment: production
26
+ * # This part specifically
27
+ * images:
28
+ * - name: tahminator/instalock-web
29
+ * newTag: a70ee0e
30
+ * ```
31
+ */
32
+ async updateK8sTagWithPR({ client, newTag, imageName, kustomizationFilePath, environment, originRepo, manifestRepo, }) {
33
+ const [manifestOwner, manifestRepository] = manifestRepo;
34
+ const [originOwner, originRepository] = originRepo;
35
+ const newBranchName = `${imageName}-${newTag}-${Utils.generateShortId()}`;
36
+ const { data: repo } = await client.rest.repos.get({
37
+ owner: manifestOwner,
38
+ repo: manifestRepository,
39
+ });
40
+ const baseBranch = repo.default_branch;
41
+ const { data: ref } = await client.rest.git.getRef({
42
+ owner: manifestOwner,
43
+ repo: manifestRepository,
44
+ ref: `heads/${baseBranch}`,
45
+ });
46
+ await client.rest.git.createRef({
47
+ owner: manifestOwner,
48
+ repo: manifestRepository,
49
+ ref: `refs/heads/${newBranchName}`,
50
+ sha: ref.object.sha,
51
+ });
52
+ const { data: file } = await client.rest.repos.getContent({
53
+ owner: manifestOwner,
54
+ repo: manifestRepository,
55
+ path: kustomizationFilePath,
56
+ ref: newBranchName,
57
+ });
58
+ if (Array.isArray(file))
59
+ throw new Error("Unexpected file shape found");
60
+ if (!file)
61
+ throw new Error("Kustomization file not found");
62
+ if (file.type !== "file")
63
+ throw new Error("Unexpected file type found");
64
+ if (!file.content)
65
+ throw new Error("Kustomization file is empty");
66
+ const currentYaml = Buffer.from(file.content ?? "", "base64").toString();
67
+ const doc = yaml.parseDocument(currentYaml);
68
+ const yamlObj = kustomizeSchema.parse(doc.toJS());
69
+ const targetImage = yamlObj.images?.find((img) => img.name === imageName);
70
+ if (!targetImage) {
71
+ console.debug(yamlObj);
72
+ throw new Error("Target image could not be found.");
73
+ }
74
+ targetImage.newTag = newTag;
75
+ doc.set("images", yamlObj.images);
76
+ const updatedYaml = doc.toString();
77
+ await client.rest.repos.createOrUpdateFileContents({
78
+ owner: manifestOwner,
79
+ repo: manifestRepository,
80
+ path: kustomizationFilePath,
81
+ message: `deploy: update ${imageName} to ${newTag}`,
82
+ content: Buffer.from(updatedYaml).toString("base64"),
83
+ sha: file.sha,
84
+ branch: newBranchName,
85
+ });
86
+ const { data: pr } = await client.rest.pulls.create({
87
+ owner: manifestOwner,
88
+ repo: manifestRepository,
89
+ title: `Deploying ${newTag} for ${imageName} in ${environment}`,
90
+ head: newBranchName,
91
+ base: baseBranch,
92
+ body: `Automated image tag change to ${newTag} for ${imageName} in ${environment} triggered by [${originOwner}/${originRepository}](https://github.com/${originOwner}/${originRepository}).`,
93
+ });
94
+ await client.rest.pulls.merge({
95
+ owner: manifestOwner,
96
+ repo: manifestRepository,
97
+ pull_number: pr.number,
98
+ merge_method: "squash",
99
+ });
100
+ }
101
+ }
@@ -0,0 +1,8 @@
1
+ import z from "zod";
2
+ export declare const kustomizeSchema: z.ZodObject<{
3
+ kind: z.ZodLiteral<"Kustomization">;
4
+ images: z.ZodOptional<z.ZodArray<z.ZodObject<{
5
+ name: z.ZodString;
6
+ newTag: z.ZodString;
7
+ }, z.core.$strip>>>;
8
+ }, z.core.$strip>;
@@ -0,0 +1,29 @@
1
+ import z from "zod";
2
+ /*
3
+ * schema should match the values we need from the below snippet:
4
+ *
5
+ * ```yaml
6
+ * apiVersion: kustomize.config.k8s.io/v1beta1
7
+ * kind: Kustomization
8
+ * resources:
9
+ * - deployment.yaml
10
+ * - secrets.yaml
11
+ * - service.yaml
12
+ * - monitor.yaml
13
+ * commonLabels:
14
+ * app: instalock-web
15
+ * environment: production
16
+ * images:
17
+ * - name: tahminator/instalock-web
18
+ * newTag: a70ee0e
19
+ * ```
20
+ */
21
+ export const kustomizeSchema = z.object({
22
+ kind: z.literal("Kustomization"),
23
+ images: z
24
+ .array(z.object({
25
+ name: z.string(),
26
+ newTag: z.string(),
27
+ }))
28
+ .optional(),
29
+ });
@@ -0,0 +1,23 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ import semver from "semver";
3
+ import type { OwnerString, RepoString } from "../../types";
4
+ export declare class GitHubTagManager {
5
+ private readonly client;
6
+ readonly isExplicitToken: boolean;
7
+ private static readonly BASE_VERSION;
8
+ constructor(client: Octokit, isExplicitToken: boolean);
9
+ /**
10
+ * Utilizes the GitHub API to create a new tag version in the given repository.
11
+ *
12
+ * @note You **must** pass in a GitHub token because the regular Github bot token
13
+ * cannot trigger actions (due to fear of recursion). You must either provide a GitHub App token or
14
+ * a GitHub PAT.
15
+ */
16
+ createTag({ repositoryOverride, releaseType, onPreTagCreate, }: {
17
+ repositoryOverride?: [OwnerString, RepoString];
18
+ releaseType?: semver.ReleaseType;
19
+ onPreTagCreate?: (tag: string) => Promise<void>;
20
+ }): Promise<void>;
21
+ private parseRepository;
22
+ private createBlob;
23
+ }
@@ -0,0 +1,118 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ import { $ } from "bun";
3
+ import semver from "semver";
4
+ export class GitHubTagManager {
5
+ client;
6
+ isExplicitToken;
7
+ static BASE_VERSION = "1.0.0";
8
+ constructor(client, isExplicitToken) {
9
+ this.client = client;
10
+ this.isExplicitToken = isExplicitToken;
11
+ if (!isExplicitToken) {
12
+ throw new Error("You must pass in an explicit GitHub token for this operation. You may either use a PAT or a GitHub App Token");
13
+ }
14
+ }
15
+ /**
16
+ * Utilizes the GitHub API to create a new tag version in the given repository.
17
+ *
18
+ * @note You **must** pass in a GitHub token because the regular Github bot token
19
+ * cannot trigger actions (due to fear of recursion). You must either provide a GitHub App token or
20
+ * a GitHub PAT.
21
+ */
22
+ async createTag({ repositoryOverride, releaseType, onPreTagCreate, }) {
23
+ const repositoryEnv = (() => {
24
+ const v = process.env.GITHUB_REPOSITORY;
25
+ return v;
26
+ })();
27
+ const [owner, repo] = this.parseRepository(repositoryOverride, repositoryEnv)();
28
+ const { data: tags } = await this.client.rest.repos.listTags({
29
+ owner,
30
+ repo,
31
+ });
32
+ const lastTag = tags
33
+ .map((t) => t.name)
34
+ .filter((v) => semver.valid(v))
35
+ .sort(semver.rcompare)[0];
36
+ const nextTag = lastTag ?
37
+ semver.inc(lastTag, releaseType ?? "patch")
38
+ : GitHubTagManager.BASE_VERSION;
39
+ if (!nextTag) {
40
+ throw new Error("Could not increment version");
41
+ }
42
+ const { data: repository } = await this.client.rest.repos.get({
43
+ owner,
44
+ repo,
45
+ });
46
+ const { data: branch } = await this.client.rest.repos.getBranch({
47
+ owner,
48
+ repo,
49
+ branch: repository.default_branch,
50
+ });
51
+ if (onPreTagCreate) {
52
+ await onPreTagCreate?.(nextTag);
53
+ }
54
+ const { stdout } = await $ `git status --porcelain`.quiet();
55
+ const changedFiles = stdout
56
+ .toString()
57
+ .split("\n")
58
+ .map((line) => line.slice(3).trim())
59
+ .filter(Boolean);
60
+ const sha = await this.createBlob({
61
+ owner,
62
+ repo,
63
+ changedFiles,
64
+ baseSha: branch.commit.commit.tree.sha,
65
+ });
66
+ const { data: commit } = await this.client.rest.git.createCommit({
67
+ owner,
68
+ repo,
69
+ message: `${nextTag}`,
70
+ tree: sha,
71
+ parents: [branch.commit.sha],
72
+ });
73
+ await this.client.rest.git.createRef({
74
+ owner,
75
+ repo,
76
+ ref: `refs/tags/${nextTag}`,
77
+ sha: commit.sha,
78
+ });
79
+ }
80
+ parseRepository(repositoryOverride, repositoryEnv) {
81
+ return () => {
82
+ if (repositoryOverride) {
83
+ return [repositoryOverride[0], repositoryOverride[1]];
84
+ }
85
+ if (repositoryEnv) {
86
+ return repositoryEnv.split("/");
87
+ }
88
+ throw new Error("GITHUB_REPOSITORY not found in environment and no explicit github repository override defined");
89
+ };
90
+ }
91
+ async createBlob({ owner, repo, baseSha, changedFiles, }) {
92
+ if (!changedFiles.length) {
93
+ return baseSha;
94
+ }
95
+ const treeEntries = await Promise.all(changedFiles.map(async (filePath) => {
96
+ const content = await Bun.file(filePath).text();
97
+ const { data: blob } = await this.client.rest.git.createBlob({
98
+ owner,
99
+ repo,
100
+ content,
101
+ encoding: "utf-8",
102
+ });
103
+ return {
104
+ path: filePath,
105
+ mode: "100644",
106
+ type: "blob",
107
+ sha: blob.sha,
108
+ };
109
+ }));
110
+ const { data: newTree } = await this.client.rest.git.createTree({
111
+ owner,
112
+ repo,
113
+ base_tree: baseSha,
114
+ tree: treeEntries,
115
+ });
116
+ return newTree.sha;
117
+ }
118
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./gh";
2
+ export * from "./docker";
3
+ export * from "./types";
4
+ export * from "./utils";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./gh";
2
+ export * from "./docker";
3
+ export * from "./types";
4
+ export * from "./utils";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { GitHubClient } from "../../gh";
2
+ import { getEnvVariables } from "../../utils/env";
3
+ async function main() {
4
+ const { githubPat } = parseCiEnv(await getEnvVariables(["ci"]));
5
+ const ghClient = new GitHubClient(githubPat);
6
+ await ghClient.createTag({
7
+ onPreTagCreate: async (tag) => {
8
+ const file = Bun.file("./package.json");
9
+ const pkg = await file.json();
10
+ pkg.version = tag;
11
+ await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n");
12
+ console.log(`Successfully updated version in package.json to ${tag}`);
13
+ },
14
+ });
15
+ }
16
+ function parseCiEnv(ciEnv) {
17
+ const githubPat = (() => {
18
+ const v = ciEnv["GITHUB_PAT"];
19
+ if (!v) {
20
+ throw new Error("Missing GITHUB_PAT from .env.ci");
21
+ }
22
+ return v;
23
+ })();
24
+ return { githubPat };
25
+ }
26
+ main()
27
+ .then(() => {
28
+ process.exit(0);
29
+ })
30
+ .catch((e) => {
31
+ console.error(e);
32
+ process.exit(1);
33
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { $ } from "bun";
2
+ import { getEnvVariables } from "../../utils/env";
3
+ async function main() {
4
+ const { npmToken } = parseCiEnv(await getEnvVariables(["ci"]));
5
+ await $ `npm config set //registry.npmjs.org/:_authToken=${npmToken}`;
6
+ await $ `npm publish --access public`;
7
+ console.log("Package has been successfully published");
8
+ }
9
+ function parseCiEnv(ciEnv) {
10
+ const npmToken = (() => {
11
+ const v = ciEnv["NPM_TOKEN"];
12
+ if (!v) {
13
+ throw new Error("Missing NPM_TOKEN from .env.ci");
14
+ }
15
+ return v;
16
+ })();
17
+ return { npmToken };
18
+ }
19
+ main()
20
+ .then(() => {
21
+ process.exit(0);
22
+ })
23
+ .catch((e) => {
24
+ console.error(e);
25
+ process.exit(1);
26
+ });
@@ -0,0 +1,3 @@
1
+ export type Environment = "staging" | "production";
2
+ export type OwnerString = string;
3
+ export type RepoString = string;
package/dist/types.js ADDED
File without changes
@@ -0,0 +1,20 @@
1
+ type Opts = {
2
+ baseDir?: string;
3
+ mask_PLZ_DO_NOT_TURN_OFF_UNLESS_YOU_KNOW_WHAT_UR_DOING?: boolean;
4
+ };
5
+ /**
6
+ * Load environment variables from encrypted `.env.*` files. This method will call `git-crypt unlock` for you if necessary.
7
+ *
8
+ * @param environments - List of environment files to load.
9
+ *
10
+ * @param opts - Optional overrides
11
+ * @param opts.baseDir - Directory of environment variable. Defaults to the root directory / ""
12
+ * @param opts.mask_PLZ_DO_NOT_TURN_OFF_UNLESS_YOU_KNOW_WHAT_UR_DOING - Should variables be masked. Defaults to `true`. __NOTE: This will only work in a GitHub Action runner.__
13
+ *
14
+ * @returns a map of the loaded environments as a key and value inside of a map.
15
+ *
16
+ * @note _Please note that duplicate environment variables will be overwritten, so
17
+ * the order in which you define `environments` does matter._
18
+ */
19
+ export declare function getEnvVariables(environments: string[], opts?: Opts): Promise<Record<string, string>>;
20
+ export {};
@@ -0,0 +1,67 @@
1
+ import { $ } from "bun";
2
+ // TODO: replace with a more robust solution
3
+ let isGitCryptUnlocked = false;
4
+ /**
5
+ * Load environment variables from encrypted `.env.*` files. This method will call `git-crypt unlock` for you if necessary.
6
+ *
7
+ * @param environments - List of environment files to load.
8
+ *
9
+ * @param opts - Optional overrides
10
+ * @param opts.baseDir - Directory of environment variable. Defaults to the root directory / ""
11
+ * @param opts.mask_PLZ_DO_NOT_TURN_OFF_UNLESS_YOU_KNOW_WHAT_UR_DOING - Should variables be masked. Defaults to `true`. __NOTE: This will only work in a GitHub Action runner.__
12
+ *
13
+ * @returns a map of the loaded environments as a key and value inside of a map.
14
+ *
15
+ * @note _Please note that duplicate environment variables will be overwritten, so
16
+ * the order in which you define `environments` does matter._
17
+ */
18
+ export async function getEnvVariables(environments, opts) {
19
+ const { baseDir = "", mask_PLZ_DO_NOT_TURN_OFF_UNLESS_YOU_KNOW_WHAT_UR_DOING = true, } = opts ?? {};
20
+ if (!isGitCryptUnlocked) {
21
+ await $ `git-crypt unlock`.nothrow();
22
+ isGitCryptUnlocked = true;
23
+ }
24
+ const loaded = new Map();
25
+ for (const env of environments) {
26
+ const path = baseDir ? `${baseDir}/.env.${env}` : `.env.${env}`;
27
+ const envFile = Bun.file(path);
28
+ if (await envFile.exists()) {
29
+ console.log(`Loading ${envFile.name}`);
30
+ const content = await envFile.text();
31
+ const lines = content.split("\n").filter((s) => s.length > 0);
32
+ for (const line of lines) {
33
+ const trimmed = line.trim();
34
+ if (!trimmed || trimmed.startsWith("#"))
35
+ continue;
36
+ const match = (() => {
37
+ const [key, ...rest] = trimmed.split("=");
38
+ return [key, rest.join("=")];
39
+ })();
40
+ if (match.length === 2) {
41
+ const [key, value] = match;
42
+ const cleanKey = key.trim();
43
+ let cleanValue = value.trim();
44
+ if ((cleanValue.startsWith('"') && cleanValue.endsWith('"')) ||
45
+ (cleanValue.startsWith("'") && cleanValue.endsWith("'"))) {
46
+ cleanValue = cleanValue.slice(1, -1);
47
+ }
48
+ loaded.set(cleanKey, cleanValue);
49
+ }
50
+ }
51
+ }
52
+ else {
53
+ console.warn(`Warning: ${envFile.name} not found`);
54
+ }
55
+ }
56
+ if (mask_PLZ_DO_NOT_TURN_OFF_UNLESS_YOU_KNOW_WHAT_UR_DOING) {
57
+ for (const [varName, value] of loaded.entries()) {
58
+ if (value === "true" || value === "false" || value === "") {
59
+ console.log(`Not masking ${varName}: true/false/empty value`);
60
+ continue;
61
+ }
62
+ console.log(`Masking ${varName}`);
63
+ console.log(`::add-mask::${value}`);
64
+ }
65
+ }
66
+ return Object.fromEntries(loaded);
67
+ }
@@ -0,0 +1,6 @@
1
+ import { getEnvVariables } from "./env";
2
+ import { generateShortId } from "./short";
3
+ export declare class Utils {
4
+ static getEnvVariables(...args: Parameters<typeof getEnvVariables>): Promise<Record<string, string>>;
5
+ static generateShortId(...args: Parameters<typeof generateShortId>): string;
6
+ }
@@ -0,0 +1,10 @@
1
+ import { getEnvVariables } from "./env";
2
+ import { generateShortId } from "./short";
3
+ export class Utils {
4
+ static getEnvVariables(...args) {
5
+ return getEnvVariables(...args);
6
+ }
7
+ static generateShortId(...args) {
8
+ return generateShortId(...args);
9
+ }
10
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Generate a short ID (default len = 7).
3
+ *
4
+ * @note THIS DOES NOT GUARANTEE EXTREMELY LOW COLLISIONS.
5
+ */
6
+ export declare function generateShortId(len?: number): string;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Generate a short ID (default len = 7).
3
+ *
4
+ * @note THIS DOES NOT GUARANTEE EXTREMELY LOW COLLISIONS.
5
+ */
6
+ export function generateShortId(len = 7) {
7
+ return Math.random()
8
+ .toString(36)
9
+ .substring(2, 2 + len);
10
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@tahminator/pipeline",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "bun run typecheck && bun run fmt && bun run lint",
20
+ "fmt": "bun run prettier",
21
+ "lint": "bun run eslint",
22
+ "eslint": "eslint .",
23
+ "eslint:fix": "eslint . --fix",
24
+ "prettier": "prettier --check .",
25
+ "prettier:fix": "prettier --write .",
26
+ "vt": "vitest run",
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "prepublishOnly": "bun run build"
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/js": "^10.0.1",
32
+ "@types/bun": "latest",
33
+ "eslint": "^10.0.3",
34
+ "eslint-plugin-perfectionist": "^5.7.0",
35
+ "globals": "^17.4.0",
36
+ "jiti": "^2.6.1",
37
+ "prettier": "^3.8.1",
38
+ "typescript-eslint": "^8.57.1",
39
+ "vitest": "^4.1.0"
40
+ },
41
+ "peerDependencies": {
42
+ "typescript": "^5"
43
+ },
44
+ "dependencies": {
45
+ "@octokit/rest": "^22.0.1",
46
+ "@types/semver": "^7.7.1",
47
+ "semver": "^7.7.4",
48
+ "yaml": "^2.8.2",
49
+ "zod": "^4.3.6"
50
+ }
51
+ }