@tahminator/pipeline 1.0.75 → 1.0.76-beta.0f9c2cb9

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
@@ -38,6 +38,10 @@ import {
38
38
  EnvClient,
39
39
  GitHubClient,
40
40
  NPMClient,
41
+ ProtobufCompilerBackend,
42
+ ProtobufCompilerClient,
43
+ ProtobufSourceLanguage,
44
+ ProtobufTargetLanguage,
41
45
  PulumiClient,
42
46
  SonarScannerClient,
43
47
  Utils,
@@ -53,6 +57,7 @@ Jump to client documentation
53
57
 
54
58
  - [`GitHubClient`](#githubclient)
55
59
  - [`DockerClient`](#dockerclient)
60
+ - [`ProtobufCompilerClient`](#protobufcompilerclient)
56
61
  - [`NPMClient`](#npmclient)
57
62
  - [`SonarScannerClient`](#sonarscannerclient)
58
63
  - [`Utils`](#utils)
@@ -178,6 +183,32 @@ await client.promoteDockerImage({
178
183
  });
179
184
  ```
180
185
 
186
+ ### `ProtobufCompilerClient`
187
+
188
+ Generate protobuf clients with the [Buf CLI](https://buf.build/docs/installation/) and its remote plugins. Java and Rust targets can be published to Artifact Keeper with Maven, Gradle, or Cargo. The client installs Buf globally with npm when necessary.
189
+
190
+ ```ts
191
+ const protobuf = new ProtobufCompilerClient();
192
+
193
+ await protobuf.compile({
194
+ backend: {
195
+ type: ProtobufCompilerBackend.ARTIFACT_KEEPER,
196
+ token: process.env.ARTIFACT_KEEPER_TOKEN!,
197
+ username: process.env.ARTIFACT_KEEPER_USERNAME!,
198
+ },
199
+ sourceLanguage: ProtobufSourceLanguage.RUST,
200
+ bufToken: process.env.BUF_TOKEN,
201
+ targetLanguages: {
202
+ [ProtobufTargetLanguage.JAVA]: {
203
+ artifactId: "hello-world-grpc-service",
204
+ groupId: "org.my-org",
205
+ version: "1.0.0",
206
+ },
207
+ },
208
+ protoFilesLocation: "./proto",
209
+ });
210
+ ```
211
+
181
212
  ### `NPMClient`
182
213
 
183
214
  Interface with NPM registry in order to publish packages to `npmjs.com`
@@ -88,10 +88,10 @@ export declare class GitHubClient {
88
88
  */
89
89
  mergePr(...args: Parameters<GitHubPRManager["mergePr"]>): Promise<void>;
90
90
  /**
91
- * Create or update a check run (the "status checks" shown on a PR/commit), dispatched by
92
- * `action`.
91
+ * Create, update, or get a check run (the "status checks" shown on a PR/commit), dispatched
92
+ * by `args.action`.
93
93
  */
94
- writeStatusCheck(...args: Parameters<GitHubPRManager["writeStatusCheck"]>): Promise<{
94
+ statusCheck(...args: Parameters<GitHubPRManager["statusCheck"]>): Promise<{
95
95
  id: number;
96
96
  head_sha: string;
97
97
  node_id: string;
@@ -117,7 +117,14 @@ export declare class GitHubClient {
117
117
  app: import("@octokit/openapi-types").components["schemas"]["nullable-integration"];
118
118
  pull_requests: import("@octokit/openapi-types").components["schemas"]["pull-request-minimal"][];
119
119
  deployment?: import("@octokit/openapi-types").components["schemas"]["deployment-simple"];
120
- }>;
120
+ } | null>;
121
+ /**
122
+ * Uploads a local directory into a repository path using the authenticated GitHub API.
123
+ * Existing files under `directory` that are not present locally are deleted.
124
+ *
125
+ * **PLEASE DO NOT USE THIS FUNCTION UNLESS YOU KNOW WHAT YOU ARE DOING.**
126
+ */
127
+ DANGEROUS__uploadDirectoryContents(...args: Parameters<GitHubPRManager["uploadDirectoryContents"]>): Promise<void>;
121
128
  /**
122
129
  * Confirms whether the given user is an active member of a team within an org.
123
130
  *
package/dist/gh/client.js CHANGED
@@ -120,11 +120,20 @@ export class GitHubClient {
120
120
  return this.prManager.mergePr(...args);
121
121
  }
122
122
  /**
123
- * Create or update a check run (the "status checks" shown on a PR/commit), dispatched by
124
- * `action`.
123
+ * Create, update, or get a check run (the "status checks" shown on a PR/commit), dispatched
124
+ * by `args.action`.
125
125
  */
126
- writeStatusCheck(...args) {
127
- return this.prManager.writeStatusCheck(...args);
126
+ statusCheck(...args) {
127
+ return this.prManager.statusCheck(...args);
128
+ }
129
+ /**
130
+ * Uploads a local directory into a repository path using the authenticated GitHub API.
131
+ * Existing files under `directory` that are not present locally are deleted.
132
+ *
133
+ * **PLEASE DO NOT USE THIS FUNCTION UNLESS YOU KNOW WHAT YOU ARE DOING.**
134
+ */
135
+ DANGEROUS__uploadDirectoryContents(...args) {
136
+ return this.prManager.uploadDirectoryContents(...args);
128
137
  }
129
138
  /**
130
139
  * Confirms whether the given user is an active member of a team within an org.
@@ -7,7 +7,15 @@ type CheckRunOutput = {
7
7
  summary: string;
8
8
  text?: string;
9
9
  };
10
- type WriteStatusCheckArgs = {
10
+ type UploadDirectoryContentsArgs = {
11
+ owner: string;
12
+ repository: string;
13
+ sourceDirectory: string;
14
+ directory?: string;
15
+ message?: string;
16
+ branch?: string;
17
+ };
18
+ type StatusCheckArgs = {
11
19
  action: "create";
12
20
  owner: string;
13
21
  repository: string;
@@ -29,10 +37,20 @@ type WriteStatusCheckArgs = {
29
37
  conclusion?: CheckRunConclusion;
30
38
  output?: CheckRunOutput;
31
39
  detailsUrl?: string;
40
+ } | {
41
+ action: "get";
42
+ owner: string;
43
+ repository: string;
44
+ /**
45
+ * commit sha, branch name, or tag name
46
+ */
47
+ ref: string;
48
+ name: string;
32
49
  };
33
50
  export declare class GitHubPRManager {
34
51
  private readonly client;
35
52
  constructor(client: Octokit);
53
+ uploadDirectoryContents({ owner, repository, sourceDirectory, directory, message, branch, }: UploadDirectoryContentsArgs): Promise<void>;
36
54
  updateK8sTagWithPR({ newTag, imageName, kustomizationFilePath, environment, originRepo, manifestRepo, }: {
37
55
  newTag: string;
38
56
  imageName: string;
@@ -47,7 +65,7 @@ export declare class GitHubPRManager {
47
65
  repository: string;
48
66
  mergeMethod?: "merge" | "squash" | "rebase";
49
67
  }): Promise<void>;
50
- writeStatusCheck(args: WriteStatusCheckArgs): Promise<{
68
+ statusCheck(args: StatusCheckArgs): Promise<{
51
69
  id: number;
52
70
  head_sha: string;
53
71
  node_id: string;
@@ -73,12 +91,16 @@ export declare class GitHubPRManager {
73
91
  app: import("@octokit/openapi-types").components["schemas"]["nullable-integration"];
74
92
  pull_requests: import("@octokit/openapi-types").components["schemas"]["pull-request-minimal"][];
75
93
  deployment?: import("@octokit/openapi-types").components["schemas"]["deployment-simple"];
76
- }>;
94
+ } | null>;
77
95
  sendPrMessage({ prId, owner, repository, message, }: {
78
96
  prId: number;
79
97
  owner: string;
80
98
  repository: string;
81
99
  message: string;
82
100
  }): Promise<void>;
101
+ private createDirectoryTree;
102
+ private createDirectoryBlobEntries;
103
+ private getDirectoryPathPrefix;
104
+ private collectDirectoryFiles;
83
105
  }
84
106
  export {};
@@ -1,3 +1,5 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
1
3
  import yaml from "yaml";
2
4
  import { Utils } from "../../utils";
3
5
  import { kustomizeSchema } from "./schema";
@@ -6,6 +8,47 @@ export class GitHubPRManager {
6
8
  constructor(client) {
7
9
  this.client = client;
8
10
  }
11
+ async uploadDirectoryContents({ owner, repository, sourceDirectory, directory = "", message = "chore: update generated files", branch, }) {
12
+ const { data: repo } = await this.client.rest.repos.get({
13
+ owner,
14
+ repo: repository,
15
+ });
16
+ const targetBranch = branch ?? repo.default_branch;
17
+ const { data: ref } = await this.client.rest.git.getRef({
18
+ owner,
19
+ repo: repository,
20
+ ref: `heads/${targetBranch}`,
21
+ });
22
+ const { data: baseCommit } = await this.client.rest.git.getCommit({
23
+ owner,
24
+ repo: repository,
25
+ commit_sha: ref.object.sha,
26
+ });
27
+ const treeSha = await this.createDirectoryTree({
28
+ directory,
29
+ owner,
30
+ repository,
31
+ sourceDirectory,
32
+ baseTreeSha: baseCommit.tree.sha,
33
+ });
34
+ if (treeSha === baseCommit.tree.sha) {
35
+ console.log("No directory changes to upload.");
36
+ return;
37
+ }
38
+ const { data: commit } = await this.client.rest.git.createCommit({
39
+ owner,
40
+ repo: repository,
41
+ message,
42
+ tree: treeSha,
43
+ parents: [ref.object.sha],
44
+ });
45
+ await this.client.rest.git.updateRef({
46
+ owner,
47
+ repo: repository,
48
+ ref: `heads/${targetBranch}`,
49
+ sha: commit.sha,
50
+ });
51
+ }
9
52
  async updateK8sTagWithPR({ newTag, imageName, kustomizationFilePath, environment, originRepo, manifestRepo, }) {
10
53
  const [manifestOwner, manifestRepository] = manifestRepo;
11
54
  const [originOwner, originRepository] = originRepo;
@@ -82,7 +125,7 @@ export class GitHubPRManager {
82
125
  merge_method: mergeMethod,
83
126
  });
84
127
  }
85
- async writeStatusCheck(args) {
128
+ async statusCheck(args) {
86
129
  switch (args.action) {
87
130
  case "create": {
88
131
  const { owner, repository, sha, name, status, conclusion, output, detailsUrl, } = args;
@@ -111,6 +154,16 @@ export class GitHubPRManager {
111
154
  });
112
155
  return data;
113
156
  }
157
+ case "get": {
158
+ const { owner, repository, ref, name } = args;
159
+ const { data } = await this.client.rest.checks.listForRef({
160
+ owner,
161
+ repo: repository,
162
+ ref,
163
+ check_name: name,
164
+ });
165
+ return data.check_runs[0] ?? null;
166
+ }
114
167
  }
115
168
  }
116
169
  async sendPrMessage({ prId, owner, repository, message, }) {
@@ -121,4 +174,84 @@ export class GitHubPRManager {
121
174
  body: message,
122
175
  });
123
176
  }
177
+ async createDirectoryTree({ owner, repository, sourceDirectory, directory, baseTreeSha, }) {
178
+ const pathPrefix = this.getDirectoryPathPrefix(directory);
179
+ const files = await this.collectDirectoryFiles(sourceDirectory);
180
+ const localPaths = new Set(files.map(({ relativePath }) => `${pathPrefix}${relativePath}`));
181
+ const { data: existingTree } = await this.client.rest.git.getTree({
182
+ owner,
183
+ repo: repository,
184
+ tree_sha: baseTreeSha,
185
+ recursive: "true",
186
+ });
187
+ const deletionEntries = existingTree.tree.flatMap((entry) => {
188
+ if (entry.type !== "blob" ||
189
+ !entry.path?.startsWith(pathPrefix) ||
190
+ localPaths.has(entry.path)) {
191
+ return [];
192
+ }
193
+ return [
194
+ {
195
+ path: entry.path,
196
+ mode: "100644",
197
+ type: "blob",
198
+ sha: null,
199
+ },
200
+ ];
201
+ });
202
+ const blobEntries = await this.createDirectoryBlobEntries({
203
+ files,
204
+ owner,
205
+ repository,
206
+ pathPrefix,
207
+ });
208
+ const { data: tree } = await this.client.rest.git.createTree({
209
+ owner,
210
+ repo: repository,
211
+ base_tree: baseTreeSha,
212
+ tree: [...deletionEntries, ...blobEntries],
213
+ });
214
+ return tree.sha;
215
+ }
216
+ async createDirectoryBlobEntries({ owner, repository, files, pathPrefix, }) {
217
+ return Promise.all(files.map(async ({ absolutePath, relativePath }) => {
218
+ const content = await readFile(absolutePath);
219
+ const { data: blob } = await this.client.rest.git.createBlob({
220
+ owner,
221
+ repo: repository,
222
+ content: content.toString("base64"),
223
+ encoding: "base64",
224
+ });
225
+ return {
226
+ path: `${pathPrefix}${relativePath}`,
227
+ mode: "100644",
228
+ type: "blob",
229
+ sha: blob.sha,
230
+ };
231
+ }));
232
+ }
233
+ getDirectoryPathPrefix(directory) {
234
+ const normalizedDirectory = directory.replace(/^\/+|\/+$/g, "");
235
+ return normalizedDirectory ? `${normalizedDirectory}/` : "";
236
+ }
237
+ async collectDirectoryFiles(directory, baseDirectory = directory) {
238
+ const files = [];
239
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
240
+ const absolutePath = path.join(directory, entry.name);
241
+ if (entry.isDirectory()) {
242
+ files.push(...(await this.collectDirectoryFiles(absolutePath, baseDirectory)));
243
+ continue;
244
+ }
245
+ if (entry.isFile()) {
246
+ files.push({
247
+ absolutePath,
248
+ relativePath: path
249
+ .relative(baseDirectory, absolutePath)
250
+ .split(path.sep)
251
+ .join("/"),
252
+ });
253
+ }
254
+ }
255
+ return files;
256
+ }
124
257
  }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./docker";
3
3
  export * from "./npm";
4
4
  export * from "./sonar";
5
5
  export * from "./pulumi";
6
+ export * from "./protobuf";
6
7
  export * from "./types";
7
8
  export * from "./env";
8
9
  export * from "./postgres";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./docker";
3
3
  export * from "./npm";
4
4
  export * from "./sonar";
5
5
  export * from "./pulumi";
6
+ export * from "./protobuf";
6
7
  export * from "./types";
7
8
  export * from "./env";
8
9
  export * from "./postgres";
@@ -1,14 +1,11 @@
1
- import { EnvClient, EnvClientStrategy } from "../../env";
2
1
  import { GitHubClient } from "../../gh";
3
- import { Utils } from "../../utils";
4
2
  import { VersioningClient, VersionUpdatingStrategy } from "../../versioning";
5
3
  async function main() {
6
- const envClient = EnvClient.create(EnvClientStrategy.GIT_CRYPT);
7
- const { githubAppAppId, githubAppInstallationId, githubAppPrivateKeyB64 } = parseCiEnv(await envClient.readFromEnv(".env.ci"));
4
+ const { githubAppAppId, githubAppInstallationId, githubAppPrivateKey } = parseCiEnv(process.env);
8
5
  const ghClient = await GitHubClient.createWithGithubAppToken({
9
6
  appId: githubAppAppId,
10
7
  installationId: githubAppInstallationId,
11
- privateKey: await Utils.decodeBase64EncodedString(githubAppPrivateKeyB64),
8
+ privateKey: githubAppPrivateKey,
12
9
  });
13
10
  const versioningClient = new VersioningClient(ghClient, VersionUpdatingStrategy.JSTS);
14
11
  const rootPkgJson = await Bun.file("./package.json").json();
@@ -25,27 +22,27 @@ async function main() {
25
22
  }
26
23
  function parseCiEnv(ciEnv) {
27
24
  const githubAppAppId = (() => {
28
- const v = ciEnv["GITHUB_APP_APP_ID"];
25
+ const v = ciEnv["_GITHUB_APP_APP_ID"];
29
26
  if (!v) {
30
- throw new Error("Missing GITHUB_APP_APP_ID from .env.ci");
27
+ throw new Error("Missing _GITHUB_APP_APP_ID from env");
31
28
  }
32
29
  return v;
33
30
  })();
34
31
  const githubAppInstallationId = (() => {
35
- const v = ciEnv["GITHUB_APP_INSTALLATION_ID"];
32
+ const v = ciEnv["_GITHUB_APP_INSTALLATION_ID"];
36
33
  if (!v) {
37
- throw new Error("Missing GITHUB_APP_INSTALLATION_ID from .env.ci");
34
+ throw new Error("Missing _GITHUB_APP_INSTALLATION_ID from env");
38
35
  }
39
36
  return v;
40
37
  })();
41
- const githubAppPrivateKeyB64 = (() => {
42
- const v = ciEnv["GITHUB_APP_PRIVATE_KEY_B64"];
38
+ const githubAppPrivateKey = (() => {
39
+ const v = ciEnv["_GITHUB_APP_PEM_CONTENT"];
43
40
  if (!v) {
44
- throw new Error("Missing GITHUB_APP_PRIVATE_KEY_B64 from .env.ci");
41
+ throw new Error("Missing _GITHUB_APP_PEM_CONTENT from env");
45
42
  }
46
43
  return v;
47
44
  })();
48
- return { githubAppAppId, githubAppInstallationId, githubAppPrivateKeyB64 };
45
+ return { githubAppAppId, githubAppInstallationId, githubAppPrivateKey };
49
46
  }
50
47
  main()
51
48
  .then(() => {
@@ -1,6 +1,5 @@
1
1
  import yargs from "yargs";
2
2
  import { hideBin } from "yargs/helpers";
3
- import { EnvClient, EnvClientStrategy } from "../../../env";
4
3
  import { GitHubClient } from "../../../gh";
5
4
  import { NPMClient } from "../../../npm";
6
5
  import { Utils } from "../../../utils";
@@ -18,12 +17,11 @@ const { sha, prId } = await yargs(hideBin(process.argv))
18
17
  .strict()
19
18
  .parse();
20
19
  async function main() {
21
- const envClient = EnvClient.create(EnvClientStrategy.GIT_CRYPT);
22
- const { githubAppAppId, githubAppInstallationId, githubAppPrivateKeyB64 } = parseCiEnv(await envClient.readFromEnv(".env.ci"));
20
+ const { githubAppAppId, githubAppInstallationId, githubAppPrivateKey } = parseCiEnv(process.env);
23
21
  const ghClient = await GitHubClient.createWithGithubAppToken({
24
22
  appId: githubAppAppId,
25
23
  installationId: githubAppInstallationId,
26
- privateKey: await Utils.decodeBase64EncodedString(githubAppPrivateKeyB64),
24
+ privateKey: githubAppPrivateKey,
27
25
  });
28
26
  const npmClient = await NPMClient.create();
29
27
  const versioningClient = new VersioningClient(ghClient, VersionUpdatingStrategy.JSTS);
@@ -55,27 +53,27 @@ async function getShortSha(sha) {
55
53
  }
56
54
  function parseCiEnv(ciEnv) {
57
55
  const githubAppAppId = (() => {
58
- const v = ciEnv["GITHUB_APP_APP_ID"];
56
+ const v = ciEnv["_GITHUB_APP_APP_ID"];
59
57
  if (!v) {
60
- throw new Error("Missing GITHUB_APP_APP_ID from .env.ci");
58
+ throw new Error("Missing _GITHUB_APP_APP_ID from env");
61
59
  }
62
60
  return v;
63
61
  })();
64
62
  const githubAppInstallationId = (() => {
65
- const v = ciEnv["GITHUB_APP_INSTALLATION_ID"];
63
+ const v = ciEnv["_GITHUB_APP_INSTALLATION_ID"];
66
64
  if (!v) {
67
- throw new Error("Missing GITHUB_APP_INSTALLATION_ID from .env.ci");
65
+ throw new Error("Missing _GITHUB_APP_INSTALLATION_ID from env");
68
66
  }
69
67
  return v;
70
68
  })();
71
- const githubAppPrivateKeyB64 = (() => {
72
- const v = ciEnv["GITHUB_APP_PRIVATE_KEY_B64"];
69
+ const githubAppPrivateKey = (() => {
70
+ const v = ciEnv["_GITHUB_APP_PEM_CONTENT"];
73
71
  if (!v) {
74
- throw new Error("Missing GITHUB_APP_PRIVATE_KEY_B64 from .env.ci");
72
+ throw new Error("Missing _GITHUB_APP_PEM_CONTENT from env");
75
73
  }
76
74
  return v;
77
75
  })();
78
- return { githubAppAppId, githubAppInstallationId, githubAppPrivateKeyB64 };
76
+ return { githubAppAppId, githubAppInstallationId, githubAppPrivateKey };
79
77
  }
80
78
  main()
81
79
  .then(() => {
@@ -0,0 +1,25 @@
1
+ import { type ProtobufArtifactKeeperBackend, type ProtobufTargetLanguageOptions, ProtobufTargetLanguage } from "../types";
2
+ export declare class ArtifactKeeperProtobufCompilerBackend {
3
+ private readonly config;
4
+ constructor(config: ProtobufArtifactKeeperBackend);
5
+ publish({ generatedDirectory, targetLanguage, options, }: {
6
+ generatedDirectory: string;
7
+ targetLanguage: ProtobufTargetLanguage;
8
+ options: ProtobufTargetLanguageOptions;
9
+ }): Promise<void>;
10
+ private publishJava;
11
+ private publishMaven;
12
+ private publishGradle;
13
+ private publishRust;
14
+ private moveJavaSources;
15
+ private findRustFiles;
16
+ private createMavenPom;
17
+ private createMavenSettings;
18
+ private createGradleBuild;
19
+ private createCargoToml;
20
+ private createRustLibrary;
21
+ private escapeXml;
22
+ private get artifactKeeperUrl();
23
+ private get mavenRepositoryUrl();
24
+ private get cargoIndexUrl();
25
+ }
@@ -0,0 +1,234 @@
1
+ import { $ } from "bun";
2
+ import { mkdir, mkdtemp, readdir, rename, rm, writeFile, } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { ProtobufTargetLanguage, } from "../types";
6
+ const DEFAULT_ARTIFACT_KEEPER_URL = "https://pkg.vpn.patinanetwork.org";
7
+ const DEFAULT_PROTOBUF_JAVA_VERSION = "4.32.1";
8
+ const DEFAULT_PROST_VERSION = "0.14";
9
+ export class ArtifactKeeperProtobufCompilerBackend {
10
+ config;
11
+ constructor(config) {
12
+ this.config = config;
13
+ }
14
+ async publish({ generatedDirectory, targetLanguage, options, }) {
15
+ switch (targetLanguage) {
16
+ case ProtobufTargetLanguage.JAVA:
17
+ await this.publishJava({
18
+ generatedDirectory,
19
+ options: options,
20
+ });
21
+ return;
22
+ case ProtobufTargetLanguage.RUST:
23
+ await this.publishRust({
24
+ generatedDirectory,
25
+ options: options,
26
+ });
27
+ return;
28
+ case ProtobufTargetLanguage.GO:
29
+ throw new Error("Artifact Keeper Go module publishing requires its upload workflow; only Go module consumption has been configured.");
30
+ }
31
+ }
32
+ async publishJava({ generatedDirectory, options, }) {
33
+ await this.moveJavaSources(generatedDirectory);
34
+ if (options.buildTool === "gradle") {
35
+ await this.publishGradle({ generatedDirectory, options });
36
+ return;
37
+ }
38
+ await this.publishMaven({ generatedDirectory, options });
39
+ }
40
+ async publishMaven({ generatedDirectory, options, }) {
41
+ await writeFile(path.join(generatedDirectory, "pom.xml"), this.createMavenPom(options));
42
+ const settingsDirectory = await mkdtemp(path.join(tmpdir(), "artifact-keeper-maven-"));
43
+ const settingsPath = path.join(settingsDirectory, "settings.xml");
44
+ await writeFile(settingsPath, this.createMavenSettings());
45
+ try {
46
+ await $ `mvn --settings ${settingsPath} deploy`.cwd(generatedDirectory);
47
+ }
48
+ finally {
49
+ await rm(settingsDirectory, { force: true, recursive: true });
50
+ }
51
+ }
52
+ async publishGradle({ generatedDirectory, options, }) {
53
+ await Promise.all([
54
+ writeFile(path.join(generatedDirectory, "build.gradle"), this.createGradleBuild(options)),
55
+ writeFile(path.join(generatedDirectory, "settings.gradle"), `rootProject.name = "${options.artifactId}"\n`),
56
+ ]);
57
+ await $ `gradle -PmavenUsername=${this.config.username} -PmavenPassword=${this.config.token} publish`.cwd(generatedDirectory);
58
+ }
59
+ async publishRust({ generatedDirectory, options, }) {
60
+ const generatedFiles = await this.findRustFiles(generatedDirectory);
61
+ await mkdir(path.join(generatedDirectory, "src"), { recursive: true });
62
+ await Promise.all([
63
+ writeFile(path.join(generatedDirectory, "Cargo.toml"), this.createCargoToml(options)),
64
+ writeFile(path.join(generatedDirectory, "src", "lib.rs"), this.createRustLibrary(generatedFiles)),
65
+ ]);
66
+ await $ `cargo publish --registry cargo --token ${this.config.token} --config ${`registries.cargo.index=${this.cargoIndexUrl}`}`.cwd(generatedDirectory);
67
+ }
68
+ async moveJavaSources(generatedDirectory) {
69
+ const sourceDirectory = path.join(generatedDirectory, "src", "main", "java");
70
+ await mkdir(sourceDirectory, { recursive: true });
71
+ for (const entry of await readdir(generatedDirectory, {
72
+ withFileTypes: true,
73
+ })) {
74
+ if (entry.name === "src" ||
75
+ entry.name === "pom.xml" ||
76
+ entry.name === "build.gradle" ||
77
+ entry.name === "settings.gradle") {
78
+ continue;
79
+ }
80
+ await rename(path.join(generatedDirectory, entry.name), path.join(sourceDirectory, entry.name));
81
+ }
82
+ }
83
+ async findRustFiles(directory, baseDirectory = directory) {
84
+ const files = [];
85
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
86
+ const filePath = path.join(directory, entry.name);
87
+ if (entry.isDirectory()) {
88
+ if (entry.name === "src")
89
+ continue;
90
+ files.push(...(await this.findRustFiles(filePath, baseDirectory)));
91
+ continue;
92
+ }
93
+ if (entry.isFile() && entry.name.endsWith(".rs")) {
94
+ files.push(path.relative(baseDirectory, filePath).split(path.sep).join("/"));
95
+ }
96
+ }
97
+ return files;
98
+ }
99
+ createMavenPom(options) {
100
+ const protobufVersion = options.protobufJavaVersion ?? DEFAULT_PROTOBUF_JAVA_VERSION;
101
+ return `<?xml version="1.0" encoding="UTF-8"?>
102
+ <project xmlns="http://maven.apache.org/POM/4.0.0"
103
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
104
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
105
+ <modelVersion>4.0.0</modelVersion>
106
+ <groupId>${this.escapeXml(options.groupId)}</groupId>
107
+ <artifactId>${this.escapeXml(options.artifactId)}</artifactId>
108
+ <version>${this.escapeXml(options.version)}</version>
109
+ <properties>
110
+ <maven.compiler.release>17</maven.compiler.release>
111
+ </properties>
112
+ <dependencies>
113
+ <dependency>
114
+ <groupId>com.google.protobuf</groupId>
115
+ <artifactId>protobuf-java</artifactId>
116
+ <version>${this.escapeXml(protobufVersion)}</version>
117
+ </dependency>
118
+ </dependencies>
119
+ <distributionManagement>
120
+ <repository>
121
+ <id>maven</id>
122
+ <url>${this.escapeXml(this.mavenRepositoryUrl)}</url>
123
+ </repository>
124
+ </distributionManagement>
125
+ </project>
126
+ `;
127
+ }
128
+ createMavenSettings() {
129
+ return `<settings>
130
+ <servers>
131
+ <server>
132
+ <id>maven</id>
133
+ <username>${this.escapeXml(this.config.username)}</username>
134
+ <password>${this.escapeXml(this.config.token)}</password>
135
+ </server>
136
+ </servers>
137
+ </settings>
138
+ `;
139
+ }
140
+ createGradleBuild(options) {
141
+ const protobufVersion = options.protobufJavaVersion ?? DEFAULT_PROTOBUF_JAVA_VERSION;
142
+ return `plugins {
143
+ id "java"
144
+ id "maven-publish"
145
+ }
146
+
147
+ group = "${options.groupId}"
148
+ version = "${options.version}"
149
+
150
+ repositories {
151
+ mavenCentral()
152
+ }
153
+
154
+ dependencies {
155
+ implementation "com.google.protobuf:protobuf-java:${protobufVersion}"
156
+ }
157
+
158
+ java {
159
+ toolchain {
160
+ languageVersion = JavaLanguageVersion.of(17)
161
+ }
162
+ }
163
+
164
+ publishing {
165
+ publications {
166
+ mavenJava(MavenPublication) {
167
+ from components.java
168
+ artifactId = "${options.artifactId}"
169
+ }
170
+ }
171
+ repositories {
172
+ maven {
173
+ url = uri("${this.mavenRepositoryUrl}")
174
+ credentials {
175
+ username = project.findProperty("mavenUsername")
176
+ password = project.findProperty("mavenPassword")
177
+ }
178
+ }
179
+ }
180
+ }
181
+ `;
182
+ }
183
+ createCargoToml(options) {
184
+ const prostVersion = options.prostVersion ?? DEFAULT_PROST_VERSION;
185
+ return `[package]
186
+ name = "${options.crateName}"
187
+ version = "${options.version}"
188
+ edition = "2021"
189
+
190
+ [dependencies]
191
+ prost = "${prostVersion}"
192
+ `;
193
+ }
194
+ createRustLibrary(generatedFiles) {
195
+ return generatedFiles
196
+ .map((relativePath) => {
197
+ const moduleName = relativePath
198
+ .replace(/\.rs$/, "")
199
+ .replace(/[^a-zA-Z0-9_]/g, "_");
200
+ return `pub mod ${moduleName} {
201
+ include!(concat!(env!("CARGO_MANIFEST_DIR"), "/${relativePath}"));
202
+ }
203
+ `;
204
+ })
205
+ .join("\n");
206
+ }
207
+ escapeXml(value) {
208
+ return value.replace(/[<>&'"]/g, (character) => {
209
+ switch (character) {
210
+ case "<":
211
+ return "&lt;";
212
+ case ">":
213
+ return "&gt;";
214
+ case "&":
215
+ return "&amp;";
216
+ case "'":
217
+ return "&apos;";
218
+ case '"':
219
+ return "&quot;";
220
+ default:
221
+ return character;
222
+ }
223
+ });
224
+ }
225
+ get artifactKeeperUrl() {
226
+ return (this.config.url ?? DEFAULT_ARTIFACT_KEEPER_URL).replace(/\/$/, "");
227
+ }
228
+ get mavenRepositoryUrl() {
229
+ return `${this.artifactKeeperUrl}/maven/maven/`;
230
+ }
231
+ get cargoIndexUrl() {
232
+ return `${this.artifactKeeperUrl}/cargo/cargo/index`;
233
+ }
234
+ }
@@ -0,0 +1 @@
1
+ export * from "./artifact-keeper";
@@ -0,0 +1 @@
1
+ export * from "./artifact-keeper";
@@ -0,0 +1,11 @@
1
+ import { type ProtobufCompilerCompileArgs, type ProtobufCompilerCompileResult } from "./types";
2
+ export declare class ProtobufCompilerClient {
3
+ compile({ backend, bufToken, targetLanguages, protoFilesLocation, outputDirectory, }: ProtobufCompilerCompileArgs): Promise<ProtobufCompilerCompileResult>;
4
+ installToolchains(): Promise<void>;
5
+ private generateWithBuf;
6
+ private getCompilationTargets;
7
+ private getConfiguredTargetLanguages;
8
+ private getTargetLanguageStrategy;
9
+ private getBufGenerateInput;
10
+ private findBufModuleDirectory;
11
+ }
@@ -0,0 +1,126 @@
1
+ import { $ } from "bun";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import yaml from "yaml";
6
+ import { isCmdAvailable } from "../utils/cmd";
7
+ import { ArtifactKeeperProtobufCompilerBackend } from "./backend";
8
+ import { GoProtobufTargetLanguageStrategy, JavaProtobufTargetLanguageStrategy, RustProtobufTargetLanguageStrategy, } from "./strategy";
9
+ import { ProtobufTargetLanguage, } from "./types";
10
+ export class ProtobufCompilerClient {
11
+ async compile({ backend, bufToken, targetLanguages, protoFilesLocation, outputDirectory, }) {
12
+ await this.installToolchains();
13
+ const outputDirectoryPath = path.resolve(outputDirectory ??
14
+ (await mkdtemp(path.join(tmpdir(), "protobuf-compiler-"))));
15
+ const targets = this.getCompilationTargets(targetLanguages);
16
+ const artifactKeeper = backend ? new ArtifactKeeperProtobufCompilerBackend(backend) : undefined;
17
+ await mkdir(outputDirectoryPath, { recursive: true });
18
+ await this.generateWithBuf({
19
+ bufToken,
20
+ outputDirectory: outputDirectoryPath,
21
+ protoFilesLocation,
22
+ targetStrategies: targets.map(({ strategy }) => strategy),
23
+ });
24
+ const generatedDirectories = {};
25
+ for (const { language, strategy } of targets) {
26
+ generatedDirectories[language] = path.join(outputDirectoryPath, strategy.outputDirectoryName);
27
+ }
28
+ if (artifactKeeper) {
29
+ for (const { language, options } of targets) {
30
+ const generatedDirectory = generatedDirectories[language];
31
+ if (!generatedDirectory) {
32
+ throw new Error("Generated code directory was not created.");
33
+ }
34
+ await artifactKeeper.publish({
35
+ generatedDirectory,
36
+ options,
37
+ targetLanguage: language,
38
+ });
39
+ }
40
+ }
41
+ return { generatedDirectories, outputDirectory: outputDirectoryPath };
42
+ }
43
+ async installToolchains() {
44
+ if (!(await isCmdAvailable("buf"))) {
45
+ console.log("Buf is missing, installing globally via NPM...");
46
+ await $ `npm i -g @bufbuild/buf`;
47
+ }
48
+ }
49
+ async generateWithBuf({ bufToken, protoFilesLocation, outputDirectory, targetStrategies, }) {
50
+ const templateDirectory = await mkdtemp(path.join(tmpdir(), "protobuf-buf-template-"));
51
+ const templatePath = path.join(templateDirectory, "buf.gen.yaml");
52
+ const { input, pathFilter } = await this.getBufGenerateInput(protoFilesLocation);
53
+ const pathFlag = pathFilter ? ["--path", pathFilter] : [];
54
+ await writeFile(templatePath, yaml.stringify({
55
+ version: "v2",
56
+ plugins: targetStrategies.map((strategy) => ({
57
+ out: strategy.outputDirectoryName,
58
+ remote: strategy.remotePlugin,
59
+ })),
60
+ }));
61
+ try {
62
+ const command = bufToken ?
63
+ $.env({
64
+ ...process.env,
65
+ BUF_TOKEN: bufToken,
66
+ }) `buf generate ${input} --template ${templatePath} ${pathFlag}`.cwd(outputDirectory)
67
+ : $ `buf generate ${input} --template ${templatePath} ${pathFlag}`.cwd(outputDirectory);
68
+ await command;
69
+ }
70
+ finally {
71
+ await rm(templateDirectory, { force: true, recursive: true });
72
+ }
73
+ }
74
+ getCompilationTargets(targetLanguages) {
75
+ return this.getConfiguredTargetLanguages(targetLanguages).map(([language, options]) => ({
76
+ language,
77
+ options,
78
+ strategy: this.getTargetLanguageStrategy(language),
79
+ }));
80
+ }
81
+ getConfiguredTargetLanguages(targetLanguages) {
82
+ const targets = [];
83
+ for (const [language, options] of Object.entries(targetLanguages)) {
84
+ if (!options)
85
+ continue;
86
+ targets.push([Number(language), options]);
87
+ }
88
+ return targets;
89
+ }
90
+ getTargetLanguageStrategy(targetLanguage) {
91
+ switch (targetLanguage) {
92
+ case ProtobufTargetLanguage.JAVA:
93
+ return new JavaProtobufTargetLanguageStrategy();
94
+ case ProtobufTargetLanguage.GO:
95
+ return new GoProtobufTargetLanguageStrategy();
96
+ case ProtobufTargetLanguage.RUST:
97
+ return new RustProtobufTargetLanguageStrategy();
98
+ }
99
+ }
100
+ async getBufGenerateInput(protoFilesLocation) {
101
+ const resolvedLocation = path.resolve(protoFilesLocation);
102
+ const stats = await Bun.file(resolvedLocation).stat();
103
+ if (stats.isFile() && !resolvedLocation.endsWith(".proto")) {
104
+ throw new Error(`${protoFilesLocation} is not a .proto file.`);
105
+ }
106
+ const sourceDirectory = stats.isFile() ? path.dirname(resolvedLocation) : resolvedLocation;
107
+ const moduleDirectory = (await this.findBufModuleDirectory(sourceDirectory)) ?? sourceDirectory;
108
+ return {
109
+ input: moduleDirectory,
110
+ pathFilter: moduleDirectory === resolvedLocation ? undefined : resolvedLocation,
111
+ };
112
+ }
113
+ async findBufModuleDirectory(directory) {
114
+ let currentDirectory = path.resolve(directory);
115
+ while (true) {
116
+ if (await Bun.file(path.join(currentDirectory, "buf.yaml")).exists()) {
117
+ return currentDirectory;
118
+ }
119
+ const parentDirectory = path.dirname(currentDirectory);
120
+ if (parentDirectory === currentDirectory) {
121
+ return null;
122
+ }
123
+ currentDirectory = parentDirectory;
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./client";
2
+ export * from "./types";
@@ -0,0 +1,2 @@
1
+ export * from "./client";
2
+ export * from "./types";
@@ -0,0 +1,5 @@
1
+ import type { IProtobufTargetLanguageStrategy } from "./types";
2
+ export declare class GoProtobufTargetLanguageStrategy implements IProtobufTargetLanguageStrategy {
3
+ readonly outputDirectoryName = "go";
4
+ readonly remotePlugin = "buf.build/protocolbuffers/go";
5
+ }
@@ -0,0 +1,4 @@
1
+ export class GoProtobufTargetLanguageStrategy {
2
+ outputDirectoryName = "go";
3
+ remotePlugin = "buf.build/protocolbuffers/go";
4
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./go";
2
+ export * from "./java";
3
+ export * from "./rust";
4
+ export * from "./types";
@@ -0,0 +1,4 @@
1
+ export * from "./go";
2
+ export * from "./java";
3
+ export * from "./rust";
4
+ export * from "./types";
@@ -0,0 +1,5 @@
1
+ import type { IProtobufTargetLanguageStrategy } from "./types";
2
+ export declare class JavaProtobufTargetLanguageStrategy implements IProtobufTargetLanguageStrategy {
3
+ readonly outputDirectoryName = "java";
4
+ readonly remotePlugin = "buf.build/protocolbuffers/java";
5
+ }
@@ -0,0 +1,4 @@
1
+ export class JavaProtobufTargetLanguageStrategy {
2
+ outputDirectoryName = "java";
3
+ remotePlugin = "buf.build/protocolbuffers/java";
4
+ }
@@ -0,0 +1,5 @@
1
+ import type { IProtobufTargetLanguageStrategy } from "./types";
2
+ export declare class RustProtobufTargetLanguageStrategy implements IProtobufTargetLanguageStrategy {
3
+ readonly outputDirectoryName = "rust";
4
+ readonly remotePlugin = "buf.build/community/neoeinstein-prost";
5
+ }
@@ -0,0 +1,4 @@
1
+ export class RustProtobufTargetLanguageStrategy {
2
+ outputDirectoryName = "rust";
3
+ remotePlugin = "buf.build/community/neoeinstein-prost";
4
+ }
@@ -0,0 +1,4 @@
1
+ export interface IProtobufTargetLanguageStrategy {
2
+ readonly outputDirectoryName: string;
3
+ readonly remotePlugin: string;
4
+ }
File without changes
@@ -0,0 +1,56 @@
1
+ export declare enum ProtobufCompilerBackend {
2
+ ARTIFACT_KEEPER = 0
3
+ }
4
+ export declare enum ProtobufSourceLanguage {
5
+ RUST = 0
6
+ }
7
+ export declare enum ProtobufTargetLanguage {
8
+ JAVA = 0,
9
+ RUST = 1,
10
+ GO = 2
11
+ }
12
+ export interface ProtobufArtifactKeeperBackend {
13
+ type: ProtobufCompilerBackend.ARTIFACT_KEEPER;
14
+ /** Defaults to `https://pkg.vpn.patinanetwork.org`. */
15
+ url?: string;
16
+ username: string;
17
+ token: string;
18
+ }
19
+ export interface ProtobufJavaTargetLanguageOptions {
20
+ groupId: string;
21
+ artifactId: string;
22
+ version: string;
23
+ buildTool?: "maven" | "gradle";
24
+ protobufJavaVersion?: string;
25
+ }
26
+ export interface ProtobufRustTargetLanguageOptions {
27
+ crateName: string;
28
+ version: string;
29
+ prostVersion?: string;
30
+ }
31
+ export interface ProtobufGoTargetLanguageOptions {
32
+ modulePath: string;
33
+ version: string;
34
+ }
35
+ export type ProtobufTargetLanguageOptions = ProtobufGoTargetLanguageOptions | ProtobufJavaTargetLanguageOptions | ProtobufRustTargetLanguageOptions;
36
+ export type ProtobufTargetLanguages = {
37
+ [ProtobufTargetLanguage.JAVA]?: ProtobufJavaTargetLanguageOptions;
38
+ [ProtobufTargetLanguage.RUST]?: ProtobufRustTargetLanguageOptions;
39
+ [ProtobufTargetLanguage.GO]?: ProtobufGoTargetLanguageOptions;
40
+ };
41
+ export interface ProtobufCompilerCompileArgs {
42
+ /** All targets are published through this one backend. */
43
+ backend?: ProtobufArtifactKeeperBackend;
44
+ sourceLanguage: ProtobufSourceLanguage;
45
+ /** Optional BSR API token used for Buf remote plugins. */
46
+ bufToken?: string;
47
+ targetLanguages: ProtobufTargetLanguages;
48
+ /** Directory containing proto files, or one specific `.proto` file. */
49
+ protoFilesLocation: string;
50
+ /** Defaults to a temporary directory. */
51
+ outputDirectory?: string;
52
+ }
53
+ export interface ProtobufCompilerCompileResult {
54
+ outputDirectory: string;
55
+ generatedDirectories: Partial<Record<ProtobufTargetLanguage, string>>;
56
+ }
@@ -0,0 +1,14 @@
1
+ export var ProtobufCompilerBackend;
2
+ (function (ProtobufCompilerBackend) {
3
+ ProtobufCompilerBackend[ProtobufCompilerBackend["ARTIFACT_KEEPER"] = 0] = "ARTIFACT_KEEPER";
4
+ })(ProtobufCompilerBackend || (ProtobufCompilerBackend = {}));
5
+ export var ProtobufSourceLanguage;
6
+ (function (ProtobufSourceLanguage) {
7
+ ProtobufSourceLanguage[ProtobufSourceLanguage["RUST"] = 0] = "RUST";
8
+ })(ProtobufSourceLanguage || (ProtobufSourceLanguage = {}));
9
+ export var ProtobufTargetLanguage;
10
+ (function (ProtobufTargetLanguage) {
11
+ ProtobufTargetLanguage[ProtobufTargetLanguage["JAVA"] = 0] = "JAVA";
12
+ ProtobufTargetLanguage[ProtobufTargetLanguage["RUST"] = 1] = "RUST";
13
+ ProtobufTargetLanguage[ProtobufTargetLanguage["GO"] = 2] = "GO";
14
+ })(ProtobufTargetLanguage || (ProtobufTargetLanguage = {}));
package/dist/utils/cmd.js CHANGED
@@ -3,7 +3,7 @@ import { fromPromise } from "neverthrow";
3
3
  const GENERIC_ERROR = 1;
4
4
  const SUCCESS = 0;
5
5
  export async function isCmdAvailable(cmd) {
6
- return ((await fromPromise($ `which -v ${cmd}`.quiet(), (e) => e)
6
+ return ((await fromPromise($ `which ${cmd}`.quiet(), (e) => e)
7
7
  .map((s) => s.exitCode)
8
8
  .unwrapOr(GENERIC_ERROR)) === SUCCESS);
9
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.75",
6
+ "version": "1.0.76-beta.0f9c2cb9",
7
7
  "repository": {
8
8
  "url": "git+https://github.com/tahminator/pipeline.git"
9
9
  },
@@ -21,14 +21,13 @@
21
21
  },
22
22
  "scripts": {
23
23
  "typecheck": "tsc --noEmit",
24
- "test": "bun run typecheck && bun run fmt && bun run lint",
24
+ "test": "bun run typecheck && bun run fmt && bun run lint && bun test",
25
25
  "fmt": "bun run prettier",
26
26
  "lint": "bun run eslint",
27
27
  "eslint": "eslint .",
28
28
  "eslint:fix": "eslint . --fix",
29
29
  "prettier": "prettier --check .",
30
30
  "prettier:fix": "prettier --write .",
31
- "vt": "vitest run",
32
31
  "build": "tsc -p tsconfig.build.json"
33
32
  },
34
33
  "devDependencies": {