@tahminator/pipeline 1.0.78 → 1.0.79-beta.cf2df8b7

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
@@ -185,7 +185,7 @@ await client.promoteDockerImage({
185
185
 
186
186
  ### `ProtobufCompilerClient`
187
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.
188
+ Generate protobuf messages and gRPC client/server stubs with the [Buf CLI](https://buf.build/docs/installation/) and local protoc plugins. Java, Rust, and Go targets can be published to Artifact Keeper with Maven/Gradle, Cargo, or Go module uploads. The client checks for required local tools and throws when one is missing.
189
189
 
190
190
  ```ts
191
191
  const protobuf = new ProtobufCompilerClient();
@@ -210,6 +210,54 @@ await protobuf.compile({
210
210
  });
211
211
  ```
212
212
 
213
+ Configure generated-code packages in each `.proto` file, not in `targetLanguages`. The client does not enable Buf managed mode; Buf's generators read these file options directly:
214
+
215
+ ```proto
216
+ option go_package = "example.com/proto/helloworld/v1;helloworldv1";
217
+ option java_package = "org.myorg.helloworld.v1";
218
+ option java_multiple_files = true;
219
+ ```
220
+
221
+ `go_package` is required for Go generation with this client. Java package options are recommended; `java_outer_classname` is optional. Rust generation needs no language-specific file options. CI target options such as Java `groupId`/`artifactId` and Rust `crateName` are publishing metadata, not generated-code package names. Go's former unused `modulePath` option has been removed.
222
+
223
+ In CI, enable the setup action's protobuf dependency installer so generation uses local tools and does not send source code to BSR remote plugins:
224
+
225
+ ```yaml
226
+ - uses: tahminator/pipeline/actions/setup@<version>
227
+ with:
228
+ INSTALL_PROTO_DEPENDENCIES: "true"
229
+ ```
230
+
231
+ Every target enables both message and service generation by default:
232
+
233
+ | Target | Generated service API | Published runtime dependencies |
234
+ | ----------------------- | -------------------------------------------------------------------- | --------------------------------------------------- |
235
+ | Java (Maven and Gradle) | `<Service>Grpc.<Service>ImplBase` and client stubs | `protobuf-java`, `grpc-protobuf`, `grpc-stub` |
236
+ | Go | `<Service>Server`, `Unimplemented<Service>Server`, `<Service>Client` | Resolved by `go mod tidy` into the generated module |
237
+ | Rust | Tonic `<service>_server` traits and `<service>_client` clients | `prost`, `prost-types`, `tonic`, `tonic-prost` |
238
+
239
+ Applications implement the generated service interfaces and supply server startup, endpoints, authentication, and transport configuration. Java applications must add a transport implementation such as `io.grpc:grpc-netty-shaded:1.75.0`; the generated SDK does not choose one. Gradle publishes public runtime types as `api` dependencies so they are visible on consumers' compile classpaths.
240
+
241
+ Imported schemas are generated alongside the selected input, except standard well-known types, which use the runtime libraries. This produces self-contained schema output rather than relying on separately published schema packages. Go publishing still requires those package paths to share a valid module root.
242
+
243
+ Rust generation uses local Prost and Tonic protoc plugins, paired with the `0.14` runtime series. `prostVersion` overrides must remain within `0.14`. The published crate preserves proto package hierarchy (for example, `example.v1` becomes `example::v1`) and includes Tonic stubs in the same module as their messages. This replaces the previous flattened Rust module names.
244
+
245
+ For Go publishing, only the release version is needed:
246
+
247
+ ```ts
248
+ targetLanguages: {
249
+ [ProtobufTargetLanguage.GO]: {
250
+ version: "1.0.0",
251
+ },
252
+ },
253
+ ```
254
+
255
+ The publisher generates `go.mod` using the deepest shared directory of the generated Go packages as the module root. For a single package, that package's import path becomes the module path. It never adds, strips, or rewrites version suffixes. Proto authors own package layout; invalid Go module/version combinations fail. In particular, Go does not allow a module path ending in `/v1`. Adding packages outside the existing shared directory changes the inferred module root, so keep the package layout stable across releases.
256
+
257
+ Go publishing requires `go` and `zip` on PATH. It stages generated packages without changing the original output, runs `go mod init` and `go mod tidy` to generate dependency metadata, and uploads `.mod` and `.zip` artifacts to the hosted `go` repository at `<backend.url>/go/go`. It does not lint, build, or test generated code. Dependency resolution may require network access. Versions may include a leading `v`; v2+ modules require the corresponding major-version module path suffix.
258
+
259
+ Java generation is pinned to the compiler release matching `protobufJavaVersion` (default runtime `4.32.1`, Buf Java plugin `v32.1`). The same runtime version is used in both Maven and Gradle metadata. Overrides must be exact stable versions, not ranges, and the corresponding plugin must exist in the Buf registry.
260
+
213
261
  ### `NPMClient`
214
262
 
215
263
  Interface with NPM registry in order to publish packages to `npmjs.com`
@@ -1,22 +1,15 @@
1
1
  import type { IProtobufCompilerBackendStrategy, ProtobufCompilerBackendPublishArgs } from "./types";
2
2
  import { type ProtobufArtifactKeeperBackend } from "../types";
3
+ /**
4
+ * Select the publisher for one target language.
5
+ *
6
+ * ```text
7
+ * The language publishers contain the package-manager rules.
8
+ * This class only routes the publish request.
9
+ * ```
10
+ */
3
11
  export declare class ArtifactKeeperProtobufCompilerBackend implements IProtobufCompilerBackendStrategy {
4
12
  private readonly config;
5
13
  constructor(config: ProtobufArtifactKeeperBackend);
6
14
  publish({ generatedDirectory, targetLanguage, options, }: ProtobufCompilerBackendPublishArgs): Promise<void>;
7
- private publishJava;
8
- private publishMaven;
9
- private publishGradle;
10
- private publishRust;
11
- private moveJavaSources;
12
- private findRustFiles;
13
- private createMavenPom;
14
- private createMavenSettings;
15
- private createGradleBuild;
16
- private createCargoToml;
17
- private createRustLibrary;
18
- private escapeXml;
19
- private get artifactKeeperUrl();
20
- private get mavenRepositoryUrl();
21
- private get cargoIndexUrl();
22
15
  }
@@ -1,10 +1,15 @@
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
1
  import { ProtobufTargetLanguage, } from "../types";
6
- const DEFAULT_PROTOBUF_JAVA_VERSION = "4.32.1";
7
- const DEFAULT_PROST_VERSION = "0.14";
2
+ import { ArtifactKeeperGoPublisher } from "./publisher/go";
3
+ import { ArtifactKeeperJavaPublisher } from "./publisher/java";
4
+ import { ArtifactKeeperRustPublisher } from "./publisher/rust/index";
5
+ /**
6
+ * Select the publisher for one target language.
7
+ *
8
+ * ```text
9
+ * The language publishers contain the package-manager rules.
10
+ * This class only routes the publish request.
11
+ * ```
12
+ */
8
13
  export class ArtifactKeeperProtobufCompilerBackend {
9
14
  config;
10
15
  constructor(config) {
@@ -13,221 +18,23 @@ export class ArtifactKeeperProtobufCompilerBackend {
13
18
  async publish({ generatedDirectory, targetLanguage, options, }) {
14
19
  switch (targetLanguage) {
15
20
  case ProtobufTargetLanguage.JAVA:
16
- await this.publishJava({
21
+ await new ArtifactKeeperJavaPublisher(this.config).publish({
17
22
  generatedDirectory,
18
23
  options: options,
19
24
  });
20
25
  return;
21
26
  case ProtobufTargetLanguage.RUST:
22
- await this.publishRust({
27
+ await new ArtifactKeeperRustPublisher(this.config).publish({
23
28
  generatedDirectory,
24
29
  options: options,
25
30
  });
26
31
  return;
27
32
  case ProtobufTargetLanguage.GO:
28
- throw new Error("Artifact Keeper Go module publishing requires its upload workflow; only Go module consumption has been configured.");
29
- }
30
- }
31
- async publishJava({ generatedDirectory, options, }) {
32
- await this.moveJavaSources(generatedDirectory);
33
- if (options.buildTool === "gradle") {
34
- await this.publishGradle({ generatedDirectory, options });
35
- return;
36
- }
37
- await this.publishMaven({ generatedDirectory, options });
38
- }
39
- async publishMaven({ generatedDirectory, options, }) {
40
- await writeFile(path.join(generatedDirectory, "pom.xml"), this.createMavenPom(options));
41
- const settingsDirectory = await mkdtemp(path.join(tmpdir(), "artifact-keeper-maven-"));
42
- const settingsPath = path.join(settingsDirectory, "settings.xml");
43
- await writeFile(settingsPath, this.createMavenSettings());
44
- try {
45
- await $ `mvn --settings ${settingsPath} deploy`.cwd(generatedDirectory);
46
- }
47
- finally {
48
- await rm(settingsDirectory, { force: true, recursive: true });
49
- }
50
- }
51
- async publishGradle({ generatedDirectory, options, }) {
52
- await Promise.all([
53
- writeFile(path.join(generatedDirectory, "build.gradle"), this.createGradleBuild(options)),
54
- writeFile(path.join(generatedDirectory, "settings.gradle"), `rootProject.name = "${options.artifactId}"\n`),
55
- ]);
56
- await $ `gradle -PmavenUsername=${this.config.username} -PmavenPassword=${this.config.token} publish`.cwd(generatedDirectory);
57
- }
58
- async publishRust({ generatedDirectory, options, }) {
59
- const generatedFiles = await this.findRustFiles(generatedDirectory);
60
- await mkdir(path.join(generatedDirectory, "src"), { recursive: true });
61
- await Promise.all([
62
- writeFile(path.join(generatedDirectory, "Cargo.toml"), this.createCargoToml(options)),
63
- writeFile(path.join(generatedDirectory, "src", "lib.rs"), this.createRustLibrary(generatedFiles)),
64
- ]);
65
- await $ `cargo publish --registry cargo --token ${this.config.token} --config ${`registries.cargo.index=${this.cargoIndexUrl}`}`.cwd(generatedDirectory);
66
- }
67
- async moveJavaSources(generatedDirectory) {
68
- const sourceDirectory = path.join(generatedDirectory, "src", "main", "java");
69
- await mkdir(sourceDirectory, { recursive: true });
70
- for (const entry of await readdir(generatedDirectory, {
71
- withFileTypes: true,
72
- })) {
73
- if (entry.name === "src" ||
74
- entry.name === "pom.xml" ||
75
- entry.name === "build.gradle" ||
76
- entry.name === "settings.gradle") {
77
- continue;
78
- }
79
- await rename(path.join(generatedDirectory, entry.name), path.join(sourceDirectory, entry.name));
80
- }
81
- }
82
- async findRustFiles(directory, baseDirectory = directory) {
83
- const files = [];
84
- for (const entry of await readdir(directory, { withFileTypes: true })) {
85
- const filePath = path.join(directory, entry.name);
86
- if (entry.isDirectory()) {
87
- if (entry.name === "src")
88
- continue;
89
- files.push(...(await this.findRustFiles(filePath, baseDirectory)));
90
- continue;
91
- }
92
- if (entry.isFile() && entry.name.endsWith(".rs")) {
93
- files.push(path.relative(baseDirectory, filePath).split(path.sep).join("/"));
94
- }
95
- }
96
- return files;
97
- }
98
- createMavenPom(options) {
99
- const protobufVersion = options.protobufJavaVersion ?? DEFAULT_PROTOBUF_JAVA_VERSION;
100
- return `<?xml version="1.0" encoding="UTF-8"?>
101
- <project xmlns="http://maven.apache.org/POM/4.0.0"
102
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
103
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
104
- <modelVersion>4.0.0</modelVersion>
105
- <groupId>${this.escapeXml(options.groupId)}</groupId>
106
- <artifactId>${this.escapeXml(options.artifactId)}</artifactId>
107
- <version>${this.escapeXml(options.version)}</version>
108
- <properties>
109
- <maven.compiler.release>17</maven.compiler.release>
110
- </properties>
111
- <dependencies>
112
- <dependency>
113
- <groupId>com.google.protobuf</groupId>
114
- <artifactId>protobuf-java</artifactId>
115
- <version>${this.escapeXml(protobufVersion)}</version>
116
- </dependency>
117
- </dependencies>
118
- <distributionManagement>
119
- <repository>
120
- <id>maven</id>
121
- <url>${this.escapeXml(this.mavenRepositoryUrl)}</url>
122
- </repository>
123
- </distributionManagement>
124
- </project>
125
- `;
126
- }
127
- createMavenSettings() {
128
- return `<settings>
129
- <servers>
130
- <server>
131
- <id>maven</id>
132
- <username>${this.escapeXml(this.config.username)}</username>
133
- <password>${this.escapeXml(this.config.token)}</password>
134
- </server>
135
- </servers>
136
- </settings>
137
- `;
138
- }
139
- createGradleBuild(options) {
140
- const protobufVersion = options.protobufJavaVersion ?? DEFAULT_PROTOBUF_JAVA_VERSION;
141
- return `plugins {
142
- id "java"
143
- id "maven-publish"
144
- }
145
-
146
- group = "${options.groupId}"
147
- version = "${options.version}"
148
-
149
- repositories {
150
- mavenCentral()
151
- }
152
-
153
- dependencies {
154
- implementation "com.google.protobuf:protobuf-java:${protobufVersion}"
155
- }
156
-
157
- java {
158
- toolchain {
159
- languageVersion = JavaLanguageVersion.of(17)
160
- }
161
- }
162
-
163
- publishing {
164
- publications {
165
- mavenJava(MavenPublication) {
166
- from components.java
167
- artifactId = "${options.artifactId}"
168
- }
169
- }
170
- repositories {
171
- maven {
172
- url = uri("${this.mavenRepositoryUrl}")
173
- credentials {
174
- username = project.findProperty("mavenUsername")
175
- password = project.findProperty("mavenPassword")
176
- }
33
+ await new ArtifactKeeperGoPublisher(this.config).publish({
34
+ generatedDirectory,
35
+ options: options,
36
+ });
37
+ return;
177
38
  }
178
39
  }
179
40
  }
180
- `;
181
- }
182
- createCargoToml(options) {
183
- const prostVersion = options.prostVersion ?? DEFAULT_PROST_VERSION;
184
- return `[package]
185
- name = "${options.crateName}"
186
- version = "${options.version}"
187
- edition = "2021"
188
-
189
- [dependencies]
190
- prost = "${prostVersion}"
191
- `;
192
- }
193
- createRustLibrary(generatedFiles) {
194
- return generatedFiles
195
- .map((relativePath) => {
196
- const moduleName = relativePath
197
- .replace(/\.rs$/, "")
198
- .replace(/[^a-zA-Z0-9_]/g, "_");
199
- return `pub mod ${moduleName} {
200
- include!(concat!(env!("CARGO_MANIFEST_DIR"), "/${relativePath}"));
201
- }
202
- `;
203
- })
204
- .join("\n");
205
- }
206
- escapeXml(value) {
207
- return value.replace(/[<>&'"]/g, (character) => {
208
- switch (character) {
209
- case "<":
210
- return "&lt;";
211
- case ">":
212
- return "&gt;";
213
- case "&":
214
- return "&amp;";
215
- case "'":
216
- return "&apos;";
217
- case '"':
218
- return "&quot;";
219
- default:
220
- return character;
221
- }
222
- });
223
- }
224
- get artifactKeeperUrl() {
225
- return this.config.url.replace(/\/$/, "");
226
- }
227
- get mavenRepositoryUrl() {
228
- return `${this.artifactKeeperUrl}/maven/maven/`;
229
- }
230
- get cargoIndexUrl() {
231
- return `${this.artifactKeeperUrl}/cargo/cargo/index`;
232
- }
233
- }
@@ -0,0 +1,33 @@
1
+ import type { ProtobufArtifactKeeperBackend, ProtobufGoTargetLanguageOptions } from "../../types";
2
+ /**
3
+ * Publish generated Go code as a hosted Go proxy module.
4
+ *
5
+ * ```text
6
+ * Buf writes files under the go_package import path.
7
+ * Artifact Keeper expects Go proxy files:
8
+ * <module>/@v/<version>.mod
9
+ * <module>/@v/<version>.zip
10
+ *
11
+ * This publisher creates those files and uploads them.
12
+ * ```
13
+ */
14
+ export declare class ArtifactKeeperGoPublisher {
15
+ private readonly config;
16
+ constructor(config: ProtobufArtifactKeeperBackend);
17
+ publish({ generatedDirectory, options, }: {
18
+ generatedDirectory: string;
19
+ options: ProtobufGoTargetLanguageOptions;
20
+ }): Promise<void>;
21
+ private normalizeVersion;
22
+ private requireToolchains;
23
+ private inferModulePath;
24
+ private findPackageDirectories;
25
+ private validateModuleVersion;
26
+ private stageModule;
27
+ private packageModule;
28
+ private uploadArtifact;
29
+ private archiveRoot;
30
+ private escapeProxyPath;
31
+ private get repositoryUrl();
32
+ private get shell();
33
+ }
@@ -0,0 +1,186 @@
1
+ import { $ } from "bun";
2
+ import { cp, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import semver from "semver";
6
+ import { isCmdAvailable } from "../../../utils/cmd";
7
+ /**
8
+ * Publish generated Go code as a hosted Go proxy module.
9
+ *
10
+ * ```text
11
+ * Buf writes files under the go_package import path.
12
+ * Artifact Keeper expects Go proxy files:
13
+ * <module>/@v/<version>.mod
14
+ * <module>/@v/<version>.zip
15
+ *
16
+ * This publisher creates those files and uploads them.
17
+ * ```
18
+ */
19
+ export class ArtifactKeeperGoPublisher {
20
+ config;
21
+ constructor(config) {
22
+ this.config = config;
23
+ }
24
+ async publish({ generatedDirectory, options, }) {
25
+ /*
26
+ * Go module publishing is not the same as Maven or Cargo.
27
+ * We must infer the module path and upload proxy artifacts.
28
+ */
29
+ const version = this.normalizeVersion(options.version);
30
+ await this.requireToolchains();
31
+ const modulePath = await this.inferModulePath(generatedDirectory);
32
+ this.validateModuleVersion(modulePath, version);
33
+ const module = { modulePath, version };
34
+ const stagingDirectory = await mkdtemp(path.join(tmpdir(), "protobuf-go-"));
35
+ try {
36
+ const moduleDirectory = await this.stageModule(module, generatedDirectory, stagingDirectory);
37
+ const artifacts = await this.packageModule(module, moduleDirectory, stagingDirectory);
38
+ for (const artifact of artifacts) {
39
+ await this.uploadArtifact(module, artifact);
40
+ }
41
+ }
42
+ finally {
43
+ await rm(stagingDirectory, { force: true, recursive: true });
44
+ }
45
+ }
46
+ normalizeVersion(value) {
47
+ /*
48
+ * Go proxy versions must start with v.
49
+ * Build metadata is not safe for this upload path.
50
+ */
51
+ const version = `v${value.replace(/^v/, "")}`;
52
+ if (!semver.valid(version) || version.includes("+")) {
53
+ throw new Error("Go publishing requires a semantic version without build metadata.");
54
+ }
55
+ return version;
56
+ }
57
+ async requireToolchains() {
58
+ for (const command of ["go", "zip"]) {
59
+ if (!(await isCmdAvailable(command))) {
60
+ throw new Error(`Go publishing requires ${command} on PATH.`);
61
+ }
62
+ }
63
+ }
64
+ async inferModulePath(generatedDirectory) {
65
+ /*
66
+ * Buf uses paths=import by default.
67
+ * Thus the directory tree contains the full go_package path.
68
+ * The shared prefix of all Go packages is the module path.
69
+ */
70
+ const packages = await this.findPackageDirectories(generatedDirectory);
71
+ const prefix = packages[0]?.split("/") ?? [];
72
+ for (const directory of packages.slice(1)) {
73
+ const segments = directory.split("/");
74
+ while (prefix.some((segment, index) => segment !== segments[index])) {
75
+ prefix.pop();
76
+ }
77
+ }
78
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
79
+ if (prefix.length < 2 || !prefix[0].includes(".")) {
80
+ throw new Error("Generated go_package paths must share a module prefix containing a domain and path.");
81
+ }
82
+ return prefix.join("/");
83
+ }
84
+ async findPackageDirectories(directory, relativeDirectory = "") {
85
+ const entries = await readdir(directory, { withFileTypes: true });
86
+ const packages = [];
87
+ if (entries.some((entry) => entry.isFile() && entry.name.endsWith(".go"))) {
88
+ packages.push(relativeDirectory);
89
+ }
90
+ for (const entry of entries) {
91
+ if (entry.isDirectory()) {
92
+ packages.push(...(await this.findPackageDirectories(path.join(directory, entry.name), relativeDirectory ?
93
+ `${relativeDirectory}/${entry.name}`
94
+ : entry.name)));
95
+ }
96
+ }
97
+ return packages;
98
+ }
99
+ validateModuleVersion(modulePath, version) {
100
+ /*
101
+ * Go requires the major version to match the module path.
102
+ * v2 and later must use a /vN suffix, unless it is gopkg.in.
103
+ */
104
+ const major = semver.major(version);
105
+ const pathMajor = modulePath.match(/\/v([0-9]+)$/)?.[1] ??
106
+ (modulePath.startsWith("gopkg.in/") ?
107
+ modulePath.match(/\.v([0-9]+)$/)?.[1]
108
+ : undefined);
109
+ if ((pathMajor &&
110
+ !modulePath.startsWith("gopkg.in/") &&
111
+ Number(pathMajor) < 2) ||
112
+ (pathMajor && Number(pathMajor) !== major) ||
113
+ (!pathMajor && major >= 2)) {
114
+ throw new Error(`Go module path ${modulePath} does not match version ${version}.`);
115
+ }
116
+ }
117
+ async stageModule(module, generatedDirectory, stagingDirectory) {
118
+ /*
119
+ * Buf's default paths=import layout includes the full go_package path.
120
+ * Strip only the module prefix.
121
+ * Do not modify the original generated output.
122
+ */
123
+ const sourceDirectory = path.join(generatedDirectory, module.modulePath);
124
+ const moduleDirectory = path.join(stagingDirectory, this.archiveRoot(module));
125
+ await mkdir(path.dirname(moduleDirectory), { recursive: true });
126
+ await cp(sourceDirectory, moduleDirectory, { recursive: true });
127
+ await this.shell `go mod init ${module.modulePath}`.cwd(moduleDirectory);
128
+ return moduleDirectory;
129
+ }
130
+ async packageModule(module, moduleDirectory, stagingDirectory) {
131
+ /*
132
+ * Resolve dependency metadata.
133
+ * Do not lint or test generated code here.
134
+ */
135
+ await this.shell `go mod tidy`.cwd(moduleDirectory);
136
+ const archivePath = path.join(stagingDirectory, "module.zip");
137
+ const archiveRoot = this.archiveRoot(module);
138
+ await this
139
+ .shell `zip -q -r ${archivePath} ${archiveRoot} -i ${`${archiveRoot}/*`}`.cwd(stagingDirectory);
140
+ return [
141
+ {
142
+ extension: "mod",
143
+ filePath: path.join(moduleDirectory, "go.mod"),
144
+ contentType: "text/plain",
145
+ },
146
+ {
147
+ extension: "zip",
148
+ filePath: archivePath,
149
+ contentType: "application/zip",
150
+ },
151
+ ];
152
+ }
153
+ async uploadArtifact(module, artifact) {
154
+ const url = `${this.repositoryUrl}/${this.escapeProxyPath(module.modulePath)}/@v/${this.escapeProxyPath(module.version)}.${artifact.extension}`;
155
+ const credentials = Buffer.from(`${this.config.username}:${this.config.token}`).toString("base64");
156
+ const response = await fetch(url, {
157
+ method: "PUT",
158
+ headers: {
159
+ Authorization: `Basic ${credentials}`,
160
+ "Content-Type": artifact.contentType,
161
+ },
162
+ body: Bun.file(artifact.filePath),
163
+ redirect: "error",
164
+ });
165
+ await response.body?.cancel();
166
+ if (!response.ok) {
167
+ throw new Error(`Go ${artifact.extension} upload failed: HTTP ${response.status}`);
168
+ }
169
+ }
170
+ archiveRoot(module) {
171
+ return `${module.modulePath}@${module.version}`;
172
+ }
173
+ escapeProxyPath(value) {
174
+ /*
175
+ * Go proxy URLs escape uppercase letters.
176
+ * Zip archive entries do not use this escape.
177
+ */
178
+ return value.replace(/[A-Z]/g, (letter) => `!${letter.toLowerCase()}`);
179
+ }
180
+ get repositoryUrl() {
181
+ return `${this.config.url.replace(/\/$/, "")}/go/go`;
182
+ }
183
+ get shell() {
184
+ return $.env({ ...process.env, GOWORK: "off" });
185
+ }
186
+ }
@@ -0,0 +1,27 @@
1
+ import type { ProtobufArtifactKeeperBackend, ProtobufJavaTargetLanguageOptions } from "../../types";
2
+ /**
3
+ * Publish generated Java code to the Maven repository in Artifact Keeper.
4
+ *
5
+ * ```text
6
+ * Buf only generates Java source files.
7
+ * Maven and Gradle need a project layout and build files.
8
+ * This publisher creates that project wrapper and then deploys it.
9
+ * ```
10
+ */
11
+ export declare class ArtifactKeeperJavaPublisher {
12
+ private readonly config;
13
+ static JAVA_LANGUAGE_VERSION: string;
14
+ constructor(config: ProtobufArtifactKeeperBackend);
15
+ publish({ generatedDirectory, options, }: {
16
+ generatedDirectory: string;
17
+ options: ProtobufJavaTargetLanguageOptions;
18
+ }): Promise<void>;
19
+ private publishMaven;
20
+ private publishGradle;
21
+ private moveSources;
22
+ createMavenPom(options: ProtobufJavaTargetLanguageOptions): string;
23
+ private createMavenSettings;
24
+ createGradleBuild(options: ProtobufJavaTargetLanguageOptions): string;
25
+ private escapeXml;
26
+ private get repositoryUrl();
27
+ }
@@ -0,0 +1,216 @@
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 { DEFAULT_PROTOBUF_JAVA_VERSION, GRPC_JAVA_VERSION, } from "../../versions";
6
+ /**
7
+ * Publish generated Java code to the Maven repository in Artifact Keeper.
8
+ *
9
+ * ```text
10
+ * Buf only generates Java source files.
11
+ * Maven and Gradle need a project layout and build files.
12
+ * This publisher creates that project wrapper and then deploys it.
13
+ * ```
14
+ */
15
+ export class ArtifactKeeperJavaPublisher {
16
+ config;
17
+ static JAVA_LANGUAGE_VERSION = "17";
18
+ constructor(config) {
19
+ this.config = config;
20
+ }
21
+ async publish({ generatedDirectory, options, }) {
22
+ /*
23
+ * Buf writes packages at the output root.
24
+ * Java build tools expect sources in src/main/java.
25
+ */
26
+ await this.moveSources(generatedDirectory);
27
+ switch (options.buildTool) {
28
+ case "gradle":
29
+ return await this.publishGradle({ generatedDirectory, options });
30
+ case "maven":
31
+ return await this.publishMaven({ generatedDirectory, options });
32
+ }
33
+ }
34
+ async publishMaven({ generatedDirectory, options, }) {
35
+ await writeFile(path.join(generatedDirectory, "pom.xml"), this.createMavenPom(options));
36
+ /*
37
+ * Write credentials to a temporary Maven settings file.
38
+ * Do not put secrets into the generated project directory.
39
+ */
40
+ const settingsDirectory = await mkdtemp(path.join(tmpdir(), "artifact-keeper-maven-"));
41
+ const settingsPath = path.join(settingsDirectory, "settings.xml");
42
+ await writeFile(settingsPath, this.createMavenSettings());
43
+ try {
44
+ await $ `mvn --settings ${settingsPath} deploy`.cwd(generatedDirectory);
45
+ }
46
+ finally {
47
+ await rm(settingsDirectory, { force: true, recursive: true });
48
+ }
49
+ }
50
+ async publishGradle({ generatedDirectory, options, }) {
51
+ /*
52
+ * Gradle needs a build file and a settings file.
53
+ * Credentials are passed as project properties at publish time.
54
+ */
55
+ await Promise.all([
56
+ writeFile(path.join(generatedDirectory, "build.gradle"), this.createGradleBuild(options)),
57
+ writeFile(path.join(generatedDirectory, "settings.gradle"), `rootProject.name = "${options.artifactId}"\n`),
58
+ ]);
59
+ await $ `gradle -PmavenUsername=${this.config.username} -PmavenPassword=${this.config.token} publish`.cwd(generatedDirectory);
60
+ }
61
+ async moveSources(generatedDirectory) {
62
+ /*
63
+ * Move only generated package directories.
64
+ * Leave build files and an existing src directory in place.
65
+ */
66
+ const sourceDirectory = path.join(generatedDirectory, "src", "main", "java");
67
+ await mkdir(sourceDirectory, { recursive: true });
68
+ for (const entry of await readdir(generatedDirectory, {
69
+ withFileTypes: true,
70
+ })) {
71
+ if (entry.name === "src" ||
72
+ entry.name === "pom.xml" ||
73
+ entry.name === "build.gradle" ||
74
+ entry.name === "settings.gradle") {
75
+ continue;
76
+ }
77
+ await rename(path.join(generatedDirectory, entry.name), path.join(sourceDirectory, entry.name));
78
+ }
79
+ }
80
+ /*
81
+ * Keep this public for tests.
82
+ * It is generated here because Buf does not write Maven project files.
83
+ */
84
+ createMavenPom(options) {
85
+ const protobufVersion = options.protobufJavaVersion ?? DEFAULT_PROTOBUF_JAVA_VERSION;
86
+ return `<?xml version="1.0" encoding="UTF-8"?>
87
+ <project xmlns="http://maven.apache.org/POM/4.0.0"
88
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
89
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
90
+ <modelVersion>4.0.0</modelVersion>
91
+ <groupId>${this.escapeXml(options.groupId)}</groupId>
92
+ <artifactId>${this.escapeXml(options.artifactId)}</artifactId>
93
+ <version>${this.escapeXml(options.version)}</version>
94
+ <properties>
95
+ <maven.compiler.release>17</maven.compiler.release>
96
+ </properties>
97
+ <dependencies>
98
+ <dependency>
99
+ <groupId>com.google.protobuf</groupId>
100
+ <artifactId>protobuf-java</artifactId>
101
+ <version>${this.escapeXml(protobufVersion)}</version>
102
+ </dependency>
103
+ <dependency>
104
+ <groupId>io.grpc</groupId>
105
+ <artifactId>grpc-protobuf</artifactId>
106
+ <version>${GRPC_JAVA_VERSION}</version>
107
+ </dependency>
108
+ <dependency>
109
+ <groupId>io.grpc</groupId>
110
+ <artifactId>grpc-stub</artifactId>
111
+ <version>${GRPC_JAVA_VERSION}</version>
112
+ </dependency>
113
+ <dependency>
114
+ <groupId>javax.annotation</groupId>
115
+ <artifactId>javax.annotation-api</artifactId>
116
+ <version>1.3.2</version>
117
+ <scope>provided</scope>
118
+ </dependency>
119
+ </dependencies>
120
+ <distributionManagement>
121
+ <repository>
122
+ <id>maven</id>
123
+ <url>${this.escapeXml(this.repositoryUrl)}</url>
124
+ </repository>
125
+ </distributionManagement>
126
+ </project>
127
+ `;
128
+ }
129
+ createMavenSettings() {
130
+ return `<settings>
131
+ <servers>
132
+ <server>
133
+ <id>maven</id>
134
+ <username>${this.escapeXml(this.config.username)}</username>
135
+ <password>${this.escapeXml(this.config.token)}</password>
136
+ </server>
137
+ </servers>
138
+ </settings>
139
+ `;
140
+ }
141
+ /*
142
+ * Keep this public for tests.
143
+ * It is generated here because Buf does not write Gradle project files.
144
+ */
145
+ createGradleBuild(options) {
146
+ const protobufVersion = options.protobufJavaVersion ?? DEFAULT_PROTOBUF_JAVA_VERSION;
147
+ return `plugins {
148
+ id "java-library"
149
+ id "maven-publish"
150
+ }
151
+
152
+ group = "${options.groupId}"
153
+ version = "${options.version}"
154
+
155
+ repositories {
156
+ mavenCentral()
157
+ }
158
+
159
+ dependencies {
160
+ api "com.google.protobuf:protobuf-java:${protobufVersion}"
161
+ api "io.grpc:grpc-protobuf:${GRPC_JAVA_VERSION}"
162
+ api "io.grpc:grpc-stub:${GRPC_JAVA_VERSION}"
163
+ compileOnly "javax.annotation:javax.annotation-api:1.3.2"
164
+ }
165
+
166
+ java {
167
+ toolchain {
168
+ languageVersion = JavaLanguageVersion.of(${ArtifactKeeperJavaPublisher.JAVA_LANGUAGE_VERSION})
169
+ }
170
+ }
171
+
172
+ publishing {
173
+ publications {
174
+ mavenJava(MavenPublication) {
175
+ from components.java
176
+ artifactId = "${options.artifactId}"
177
+ }
178
+ }
179
+ repositories {
180
+ maven {
181
+ url = uri("${this.repositoryUrl}")
182
+ credentials {
183
+ username = project.findProperty("mavenUsername")
184
+ password = project.findProperty("mavenPassword")
185
+ }
186
+ }
187
+ }
188
+ }
189
+ `;
190
+ }
191
+ escapeXml(value) {
192
+ /*
193
+ * User-provided package data goes into XML.
194
+ * Escape it before Maven reads the file.
195
+ */
196
+ return value.replace(/[<>&'"]/g, (character) => {
197
+ switch (character) {
198
+ case "<":
199
+ return "&lt;";
200
+ case ">":
201
+ return "&gt;";
202
+ case "&":
203
+ return "&amp;";
204
+ case "'":
205
+ return "&apos;";
206
+ case '"':
207
+ return "&quot;";
208
+ default:
209
+ return character;
210
+ }
211
+ });
212
+ }
213
+ get repositoryUrl() {
214
+ return `${this.config.url.replace(/\/$/, "")}/maven/maven/`;
215
+ }
216
+ }
@@ -0,0 +1,21 @@
1
+ import type { ProtobufArtifactKeeperBackend, ProtobufRustTargetLanguageOptions } from "../../../types";
2
+ /**
3
+ * Publish generated Rust code as a Cargo crate.
4
+ *
5
+ * ```text
6
+ * Buf only generates .rs files.
7
+ * Cargo needs Cargo.toml and src/lib.rs.
8
+ * This publisher adds those files before cargo publish runs.
9
+ * ```
10
+ */
11
+ export declare class ArtifactKeeperRustPublisher {
12
+ private readonly config;
13
+ constructor(config: ProtobufArtifactKeeperBackend);
14
+ publish({ generatedDirectory, options, }: {
15
+ generatedDirectory: string;
16
+ options: ProtobufRustTargetLanguageOptions;
17
+ }): Promise<void>;
18
+ private findRustFiles;
19
+ createCargoToml(options: ProtobufRustTargetLanguageOptions): string;
20
+ private get indexUrl();
21
+ }
@@ -0,0 +1,84 @@
1
+ import { $ } from "bun";
2
+ import { mkdir, readdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { DEFAULT_PROST_VERSION, TONIC_VERSION } from "../../../versions";
5
+ import { createRustLibrary } from "./library";
6
+ /**
7
+ * Publish generated Rust code as a Cargo crate.
8
+ *
9
+ * ```text
10
+ * Buf only generates .rs files.
11
+ * Cargo needs Cargo.toml and src/lib.rs.
12
+ * This publisher adds those files before cargo publish runs.
13
+ * ```
14
+ */
15
+ export class ArtifactKeeperRustPublisher {
16
+ config;
17
+ constructor(config) {
18
+ this.config = config;
19
+ }
20
+ async publish({ generatedDirectory, options, }) {
21
+ const generatedFiles = await this.findRustFiles(generatedDirectory);
22
+ /*
23
+ * Add the two files that make the generated code a crate.
24
+ * Cargo.toml gives package data and dependencies.
25
+ * src/lib.rs exposes the generated modules to users.
26
+ */
27
+ await mkdir(path.join(generatedDirectory, "src"), { recursive: true });
28
+ await Promise.all([
29
+ writeFile(path.join(generatedDirectory, "Cargo.toml"), this.createCargoToml(options)),
30
+ writeFile(path.join(generatedDirectory, "src", "lib.rs"), createRustLibrary(generatedFiles)),
31
+ ]);
32
+ /*
33
+ * Artifact Keeper exposes Cargo as a sparse registry.
34
+ * Cargo reads these CARGO_REGISTRIES_* variables for this publish.
35
+ */
36
+ await $.env({
37
+ ...process.env,
38
+ CARGO_REGISTRIES_CARGO_INDEX: this.indexUrl,
39
+ CARGO_REGISTRIES_CARGO_TOKEN: this.config.token,
40
+ CARGO_REGISTRIES_CARGO_CREDENTIAL_PROVIDER: "cargo:token",
41
+ }) `cargo publish --registry cargo`.cwd(generatedDirectory);
42
+ }
43
+ /*
44
+ * Find generated Rust files before we add src/lib.rs.
45
+ * Do not scan src, or lib.rs can include itself on a re-run.
46
+ */
47
+ async findRustFiles(directory, baseDirectory = directory) {
48
+ const files = [];
49
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
50
+ const filePath = path.join(directory, entry.name);
51
+ if (entry.isDirectory()) {
52
+ if (entry.name === "src")
53
+ continue;
54
+ files.push(...(await this.findRustFiles(filePath, baseDirectory)));
55
+ continue;
56
+ }
57
+ if (entry.isFile() && entry.name.endsWith(".rs")) {
58
+ files.push(path.relative(baseDirectory, filePath).split(path.sep).join("/"));
59
+ }
60
+ }
61
+ return files;
62
+ }
63
+ /*
64
+ * Keep this public for tests.
65
+ * It is generated here because Buf does not write Cargo manifests.
66
+ */
67
+ createCargoToml(options) {
68
+ const prostVersion = options.prostVersion ?? DEFAULT_PROST_VERSION;
69
+ return `[package]
70
+ name = "${options.crateName}"
71
+ version = "${options.version}"
72
+ edition = "2021"
73
+
74
+ [dependencies]
75
+ prost = "${prostVersion}"
76
+ prost-types = "${prostVersion}"
77
+ tonic = "${TONIC_VERSION}"
78
+ tonic-prost = "${TONIC_VERSION}"
79
+ `;
80
+ }
81
+ get indexUrl() {
82
+ return `sparse+${this.config.url.replace(/\/$/, "")}/cargo/cargo/`;
83
+ }
84
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Create the Cargo library entry file for generated Rust code.
3
+ *
4
+ * ```text
5
+ * Buf writes one Rust file for each proto package.
6
+ * Example:
7
+ * example.v1.rs
8
+ * example.v1.tonic.rs
9
+ *
10
+ * Cargo does not load these files by itself.
11
+ * A crate must have src/lib.rs.
12
+ * This function writes the content for that file.
13
+ *
14
+ * The module path must match the proto package.
15
+ * example.v1 becomes example::v1.
16
+ * The Prost messages and Tonic services go in the same module.
17
+ * ```
18
+ */
19
+ export declare function createRustLibrary(generatedFiles: string[]): string;
@@ -0,0 +1,65 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Create the Cargo library entry file for generated Rust code.
4
+ *
5
+ * ```text
6
+ * Buf writes one Rust file for each proto package.
7
+ * Example:
8
+ * example.v1.rs
9
+ * example.v1.tonic.rs
10
+ *
11
+ * Cargo does not load these files by itself.
12
+ * A crate must have src/lib.rs.
13
+ * This function writes the content for that file.
14
+ *
15
+ * The module path must match the proto package.
16
+ * example.v1 becomes example::v1.
17
+ * The Prost messages and Tonic services go in the same module.
18
+ * ```
19
+ */
20
+ export function createRustLibrary(generatedFiles) {
21
+ const root = { files: [], children: new Map() };
22
+ for (const file of [...generatedFiles].sort()) {
23
+ const packageName = path.posix
24
+ .basename(file)
25
+ .replace(/(?:\.tonic)?\.rs$/, "");
26
+ let node = root;
27
+ for (const segment of packageName === "_" ? [] : packageName.split(".")) {
28
+ /*
29
+ * Rust module names must be valid identifiers.
30
+ * We fail here instead of publishing a crate that cannot compile.
31
+ */
32
+ if (!/^(?:r#)?[a-zA-Z_][a-zA-Z0-9_]*$/.test(segment) ||
33
+ ["self", "Self", "super", "crate"].includes(segment)) {
34
+ throw new Error(`Cannot represent Rust package module ${segment} from ${file}.`);
35
+ }
36
+ /*
37
+ * Use a raw identifier for each module name.
38
+ * This makes names such as type or match safe in Rust.
39
+ */
40
+ const name = segment.startsWith("r#") ? segment : `r#${segment}`;
41
+ let child = node.children.get(name);
42
+ if (!child) {
43
+ child = { files: [], children: new Map() };
44
+ node.children.set(name, child);
45
+ }
46
+ node = child;
47
+ }
48
+ node.files.push(file);
49
+ }
50
+ function render(node, indent = "") {
51
+ /*
52
+ * Include files from the crate root.
53
+ * The generated files stay outside src.
54
+ * CARGO_MANIFEST_DIR gives Cargo the crate root path.
55
+ */
56
+ const includes = node.files
57
+ .map((file) => `${indent}include!(concat!(env!("CARGO_MANIFEST_DIR"), ${JSON.stringify(`/${file}`)}));\n`)
58
+ .join("");
59
+ const children = [...node.children]
60
+ .map(([name, child]) => `${indent}pub mod ${name} {\n${render(child, `${indent} `)}${indent}}\n`)
61
+ .join("");
62
+ return includes + children;
63
+ }
64
+ return render(root);
65
+ }
@@ -1,10 +1,18 @@
1
- import { type ProtobufCompilerCompileArgs, type ProtobufCompilerCompileResult } from "./types";
1
+ import { type IProtobufTargetLanguageStrategy } from "./strategy";
2
+ import { type ProtobufCompilerCompileArgs, type ProtobufCompilerCompileResult, type ProtobufTargetLanguageOptions, ProtobufTargetLanguage } from "./types";
3
+ type CompilationTarget = {
4
+ language: ProtobufTargetLanguage;
5
+ options: ProtobufTargetLanguageOptions;
6
+ strategy: IProtobufTargetLanguageStrategy;
7
+ };
2
8
  export declare class ProtobufCompilerClient {
3
9
  compile({ backend, bufToken, targetLanguages, protoFilesLocation, outputDirectory, }: ProtobufCompilerCompileArgs): Promise<ProtobufCompilerCompileResult>;
4
- installToolchains(): Promise<void>;
10
+ installToolchains(targets?: CompilationTarget[]): Promise<void>;
11
+ private requireToolchains;
5
12
  private generateWithBuf;
6
13
  private getCompilationTargets;
7
14
  private getConfiguredTargetLanguages;
8
15
  private getBufGenerateInput;
9
16
  private findBufModuleDirectory;
10
17
  }
18
+ export {};
@@ -8,16 +8,16 @@ import { createProtobufCompilerBackendStrategy } from "./backend";
8
8
  import { GoProtobufTargetLanguageStrategy, JavaProtobufTargetLanguageStrategy, RustProtobufTargetLanguageStrategy, } from "./strategy";
9
9
  import { ProtobufTargetLanguage, } from "./types";
10
10
  const TARGET_LANGUAGE_STRATEGY_FACTORIES = {
11
- [ProtobufTargetLanguage.JAVA]: () => new JavaProtobufTargetLanguageStrategy(),
11
+ [ProtobufTargetLanguage.JAVA]: (targets) => new JavaProtobufTargetLanguageStrategy(targets[ProtobufTargetLanguage.JAVA]?.protobufJavaVersion),
12
12
  [ProtobufTargetLanguage.GO]: () => new GoProtobufTargetLanguageStrategy(),
13
- [ProtobufTargetLanguage.RUST]: () => new RustProtobufTargetLanguageStrategy(),
13
+ [ProtobufTargetLanguage.RUST]: (targets) => new RustProtobufTargetLanguageStrategy(targets[ProtobufTargetLanguage.RUST]?.prostVersion),
14
14
  };
15
15
  export class ProtobufCompilerClient {
16
16
  async compile({ backend, bufToken, targetLanguages, protoFilesLocation, outputDirectory, }) {
17
- await this.installToolchains();
17
+ const targets = this.getCompilationTargets(targetLanguages);
18
+ await this.requireToolchains(targets);
18
19
  const outputDirectoryPath = path.resolve(outputDirectory ??
19
20
  (await mkdtemp(path.join(tmpdir(), "protobuf-compiler-"))));
20
- const targets = this.getCompilationTargets(targetLanguages);
21
21
  const compilerBackend = backend ? createProtobufCompilerBackendStrategy(backend) : undefined;
22
22
  await mkdir(outputDirectoryPath, { recursive: true });
23
23
  await this.generateWithBuf({
@@ -45,10 +45,24 @@ export class ProtobufCompilerClient {
45
45
  }
46
46
  return { generatedDirectories, outputDirectory: outputDirectoryPath };
47
47
  }
48
- async installToolchains() {
49
- if (!(await isCmdAvailable("buf"))) {
50
- console.log("Buf is missing, installing globally via NPM...");
51
- await $ `npm i -g @bufbuild/buf`;
48
+ async installToolchains(targets = []) {
49
+ await this.requireToolchains(targets);
50
+ }
51
+ async requireToolchains(targets) {
52
+ /*
53
+ * Use local protoc plugins, not Buf remote plugins.
54
+ * This keeps proto source code off BSR servers.
55
+ * The setup action installs these tools in CI.
56
+ */
57
+ const requiredCommands = [
58
+ "buf",
59
+ ...new Set(targets.flatMap(({ strategy }) => strategy.requiredCommands ?? [])),
60
+ ];
61
+ for (const command of requiredCommands) {
62
+ if (await isCmdAvailable(command))
63
+ continue;
64
+ throw new Error(`${command} is required for local protobuf generation. ` +
65
+ "Enable INSTALL_PROTO_DEPENDENCIES in the setup action, or install the tool on PATH.");
52
66
  }
53
67
  }
54
68
  async generateWithBuf({ bufToken, protoFilesLocation, outputDirectory, targetStrategies, }) {
@@ -56,12 +70,17 @@ export class ProtobufCompilerClient {
56
70
  const templatePath = path.join(templateDirectory, "buf.gen.yaml");
57
71
  const { input, pathFilter } = await this.getBufGenerateInput(protoFilesLocation);
58
72
  const pathFlag = pathFilter ? ["--path", pathFilter] : [];
73
+ /*
74
+ * Write a temporary Buf template.
75
+ * This lets callers choose languages without a checked-in buf.gen.yaml.
76
+ */
59
77
  await writeFile(templatePath, yaml.stringify({
60
78
  version: "v2",
61
- plugins: targetStrategies.map((strategy) => ({
79
+ plugins: targetStrategies.flatMap((strategy) => strategy.plugins.map((plugin) => ({
80
+ ...plugin,
62
81
  out: strategy.outputDirectoryName,
63
- remote: strategy.remotePlugin,
64
- })),
82
+ include_imports: true,
83
+ }))),
65
84
  }));
66
85
  try {
67
86
  const command = bufToken ?
@@ -80,13 +99,18 @@ export class ProtobufCompilerClient {
80
99
  return this.getConfiguredTargetLanguages(targetLanguages).map(([language, options]) => ({
81
100
  language,
82
101
  options,
83
- strategy: TARGET_LANGUAGE_STRATEGY_FACTORIES[language](),
102
+ strategy: TARGET_LANGUAGE_STRATEGY_FACTORIES[language](targetLanguages),
84
103
  }));
85
104
  }
86
105
  getConfiguredTargetLanguages(targetLanguages) {
87
106
  return Object.entries(targetLanguages).flatMap(([language, options]) => options ? [[Number(language), options]] : []);
88
107
  }
89
108
  async getBufGenerateInput(protoFilesLocation) {
109
+ /*
110
+ * If the input is inside a Buf module, run Buf at the module root.
111
+ * Then add --path for the selected file.
112
+ * This lets imports resolve correctly.
113
+ */
90
114
  const resolvedLocation = path.resolve(protoFilesLocation);
91
115
  const stats = await Bun.file(resolvedLocation).stat();
92
116
  if (stats.isFile() && !resolvedLocation.endsWith(".proto")) {
@@ -1,5 +1,8 @@
1
1
  import type { IProtobufTargetLanguageStrategy } from "./types";
2
2
  export declare class GoProtobufTargetLanguageStrategy implements IProtobufTargetLanguageStrategy {
3
3
  readonly outputDirectoryName = "go";
4
- readonly remotePlugin = "buf.build/protocolbuffers/go";
4
+ readonly requiredCommands: string[];
5
+ readonly plugins: {
6
+ local: string;
7
+ }[];
5
8
  }
@@ -1,4 +1,8 @@
1
1
  export class GoProtobufTargetLanguageStrategy {
2
2
  outputDirectoryName = "go";
3
- remotePlugin = "buf.build/protocolbuffers/go";
3
+ requiredCommands = ["protoc-gen-go", "protoc-gen-go-grpc"];
4
+ plugins = [
5
+ { local: "protoc-gen-go" },
6
+ { local: "protoc-gen-go-grpc" },
7
+ ];
4
8
  }
@@ -1,5 +1,13 @@
1
1
  import type { IProtobufTargetLanguageStrategy } from "./types";
2
2
  export declare class JavaProtobufTargetLanguageStrategy implements IProtobufTargetLanguageStrategy {
3
3
  readonly outputDirectoryName = "java";
4
- readonly remotePlugin = "buf.build/protocolbuffers/java";
4
+ readonly requiredCommands: string[];
5
+ readonly plugins: ({
6
+ protoc_builtin: string;
7
+ local?: undefined;
8
+ } | {
9
+ local: string;
10
+ protoc_builtin?: undefined;
11
+ })[];
12
+ constructor(runtimeVersion?: string);
5
13
  }
@@ -1,4 +1,19 @@
1
+ import semver from "semver";
2
+ import { DEFAULT_PROTOBUF_JAVA_VERSION } from "../versions";
1
3
  export class JavaProtobufTargetLanguageStrategy {
2
4
  outputDirectoryName = "java";
3
- remotePlugin = "buf.build/protocolbuffers/java";
5
+ requiredCommands = ["protoc-gen-grpc-java"];
6
+ plugins = [
7
+ { protoc_builtin: "java" },
8
+ { local: "protoc-gen-grpc-java" },
9
+ ];
10
+ constructor(runtimeVersion = DEFAULT_PROTOBUF_JAVA_VERSION) {
11
+ const version = semver.parse(runtimeVersion);
12
+ if (!version ||
13
+ ![3, 4].includes(version.major) ||
14
+ version.prerelease.length > 0 ||
15
+ version.build.length > 0) {
16
+ throw new Error("protobufJavaVersion must be an exact stable version.");
17
+ }
18
+ }
4
19
  }
@@ -1,5 +1,13 @@
1
1
  import type { IProtobufTargetLanguageStrategy } from "./types";
2
2
  export declare class RustProtobufTargetLanguageStrategy implements IProtobufTargetLanguageStrategy {
3
3
  readonly outputDirectoryName = "rust";
4
- readonly remotePlugin = "buf.build/community/neoeinstein-prost";
4
+ readonly requiredCommands: string[];
5
+ readonly plugins: ({
6
+ local: string;
7
+ opt?: undefined;
8
+ } | {
9
+ local: string;
10
+ opt: string[];
11
+ })[];
12
+ constructor(prostVersion?: string);
5
13
  }
@@ -1,4 +1,23 @@
1
+ import semver from "semver";
2
+ import { DEFAULT_PROST_VERSION } from "../versions";
1
3
  export class RustProtobufTargetLanguageStrategy {
2
4
  outputDirectoryName = "rust";
3
- remotePlugin = "buf.build/community/neoeinstein-prost";
5
+ requiredCommands = ["protoc-gen-prost", "protoc-gen-tonic"];
6
+ plugins = [
7
+ { local: "protoc-gen-prost" },
8
+ {
9
+ local: "protoc-gen-tonic",
10
+ /*
11
+ * Do not let Tonic include Prost files.
12
+ * src/lib.rs includes messages and services in one module.
13
+ */
14
+ opt: ["no_include=true"],
15
+ },
16
+ ];
17
+ constructor(prostVersion = DEFAULT_PROST_VERSION) {
18
+ if (!semver.validRange(prostVersion) ||
19
+ !semver.subset(prostVersion, ">=0.14.0 <0.15.0")) {
20
+ throw new Error("The pinned Rust gRPC generators require prostVersion in the 0.14 release series.");
21
+ }
22
+ }
4
23
  }
@@ -1,4 +1,15 @@
1
+ export type ProtobufGeneratorPlugin = {
2
+ readonly remote: string;
3
+ readonly opt?: string[];
4
+ } | {
5
+ readonly local: string;
6
+ readonly opt?: string[];
7
+ } | {
8
+ readonly protoc_builtin: string;
9
+ readonly opt?: string[];
10
+ };
1
11
  export interface IProtobufTargetLanguageStrategy {
2
12
  readonly outputDirectoryName: string;
3
- readonly remotePlugin: string;
13
+ readonly plugins: ProtobufGeneratorPlugin[];
14
+ readonly requiredCommands?: readonly string[];
4
15
  }
@@ -17,19 +17,22 @@ export interface ProtobufArtifactKeeperBackend {
17
17
  }
18
18
  export type ProtobufCompilerBackendConfig = ProtobufArtifactKeeperBackend;
19
19
  export interface ProtobufJavaTargetLanguageOptions {
20
+ /** Maven/Gradle publishing group; Java packages are configured in .proto files. */
20
21
  groupId: string;
21
22
  artifactId: string;
22
23
  version: string;
23
24
  buildTool?: "maven" | "gradle";
25
+ /** Exact stable Java runtime version; also pins the matching Buf generator. Defaults to 4.32.1. */
24
26
  protobufJavaVersion?: string;
25
27
  }
26
28
  export interface ProtobufRustTargetLanguageOptions {
27
29
  crateName: string;
28
30
  version: string;
31
+ /** Prost runtime constraint within 0.14; matched to the pinned Prost/Tonic generators. */
29
32
  prostVersion?: string;
30
33
  }
34
+ /** Go import paths are configured with go_package in .proto files. */
31
35
  export interface ProtobufGoTargetLanguageOptions {
32
- modulePath: string;
33
36
  version: string;
34
37
  }
35
38
  export type ProtobufTargetLanguageOptions = ProtobufGoTargetLanguageOptions | ProtobufJavaTargetLanguageOptions | ProtobufRustTargetLanguageOptions;
@@ -42,7 +45,7 @@ export interface ProtobufCompilerCompileArgs {
42
45
  /** All targets are published through this one backend. */
43
46
  backend?: ProtobufCompilerBackendConfig;
44
47
  sourceLanguage: ProtobufSourceLanguage;
45
- /** Optional BSR API token used for Buf remote plugins. */
48
+ /** Optional Buf token for private module inputs. Code generation uses local plugins. */
46
49
  bufToken?: string;
47
50
  targetLanguages: ProtobufTargetLanguages;
48
51
  /** Directory containing proto files, or one specific `.proto` file. */
@@ -0,0 +1,4 @@
1
+ export declare const DEFAULT_PROTOBUF_JAVA_VERSION = "4.32.1";
2
+ export declare const GRPC_JAVA_VERSION = "1.75.0";
3
+ export declare const DEFAULT_PROST_VERSION = "0.14.1";
4
+ export declare const TONIC_VERSION = "0.14.1";
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_PROTOBUF_JAVA_VERSION = "4.32.1";
2
+ export const GRPC_JAVA_VERSION = "1.75.0";
3
+ export const DEFAULT_PROST_VERSION = "0.14.1";
4
+ export const TONIC_VERSION = "0.14.1";
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.78",
6
+ "version": "1.0.79-beta.cf2df8b7",
7
7
  "repository": {
8
8
  "url": "git+https://github.com/tahminator/pipeline.git"
9
9
  },