@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 +21 -0
- package/dist/docker/index.d.ts +15 -0
- package/dist/docker/index.js +32 -0
- package/dist/gh/client.d.ts +11 -0
- package/dist/gh/client.js +33 -0
- package/dist/gh/index.d.ts +1 -0
- package/dist/gh/index.js +1 -0
- package/dist/gh/output/index.d.ts +15 -0
- package/dist/gh/output/index.js +31 -0
- package/dist/gh/pr/index.d.ts +38 -0
- package/dist/gh/pr/index.js +101 -0
- package/dist/gh/pr/schema.d.ts +8 -0
- package/dist/gh/pr/schema.js +29 -0
- package/dist/gh/tag/index.d.ts +23 -0
- package/dist/gh/tag/index.js +118 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/internal/create-tag/index.d.ts +1 -0
- package/dist/internal/create-tag/index.js +33 -0
- package/dist/internal/upload-npm/index.d.ts +1 -0
- package/dist/internal/upload-npm/index.js +26 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.js +0 -0
- package/dist/utils/env.d.ts +20 -0
- package/dist/utils/env.js +67 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/short.d.ts +6 -0
- package/dist/utils/short.js +10 -0
- package/package.json +51 -0
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";
|
package/dist/gh/index.js
ADDED
|
@@ -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,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
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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
|
+
});
|
package/dist/types.d.ts
ADDED
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
|
+
}
|
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
|
+
}
|