@dbx-tools/projen 0.9.12 → 0.9.18
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 +33 -25
- package/index.ts +5 -4
- package/package.json +4 -5
- package/src/_release-platform.ts +10 -0
- package/src/_rust-project.ts +40 -13
- package/src/_rust-release-workflow.ts +213 -344
- package/src/openapi.ts +191 -79
- package/src/project-js.ts +8 -13
- package/src/project-py.ts +22 -3
- package/src/project-rs.ts +50 -6
- package/src/release-github.ts +23 -0
- package/src/release-plan.ts +48 -15
- package/src/release.ts +17 -3
- package/src/watch.ts +19 -2
- package/src/workspace-lock.ts +32 -6
- package/tasks/barrels.ts +47 -30
- package/tasks/compile-workspace.ts +121 -0
- package/tasks/openapi.ts +15 -11
- package/tasks/release-assets.ts +476 -0
- package/tasks/release-pr.ts +123 -14
- package/tasks/rust.ts +62 -33
- package/tasks/uniffi-python.js +24 -12
- package/tasks/uniffi-release.mjs +40 -3
- package/tasks/uniffi.ts +2 -0
- package/src/_rust-release.ts +0 -605
- package/tasks/build-rust-release.ts +0 -68
- package/tasks/rust-release.d.ts +0 -41
- package/tasks/rust-release.mjs +0 -1717
package/src/openapi.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenAPI generator
|
|
2
|
+
* OpenAPI generator for tsoa controllers and code-first Rust producers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* TypeScript packages are discovered from tsoa imports. Rust producers are
|
|
5
|
+
* recorded by `DBXToolsRustWorkspace`. Both feed the same optimized generated
|
|
6
|
+
* package:
|
|
7
7
|
*
|
|
8
8
|
* - `openapi.json` - the OpenAPI 3 spec (tsoa `generateSpec`, then Speakeasy
|
|
9
9
|
* optimization to extract duplicate inline schemas into components).
|
|
@@ -33,11 +33,11 @@ import {
|
|
|
33
33
|
writeFileSync,
|
|
34
34
|
} from "node:fs";
|
|
35
35
|
import { createRequire } from "node:module";
|
|
36
|
-
import { join } from "node:path";
|
|
36
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
37
37
|
import { promisify } from "node:util";
|
|
38
38
|
import * as bin from "@dbx-tools/core/bin";
|
|
39
39
|
import { find } from "@dbx-tools/path";
|
|
40
|
-
import { log, stringUtils } from "@dbx-tools/shared-core";
|
|
40
|
+
import { log, object, stringUtils } from "@dbx-tools/shared-core";
|
|
41
41
|
import type * as ts from "typescript";
|
|
42
42
|
import { lazyRequire } from "./_lazy-require.ts";
|
|
43
43
|
import { makeReadonly, makeWritable, stampGenerated } from "./generated.ts";
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
toPosix,
|
|
49
49
|
recordedPackages,
|
|
50
50
|
} from "./packages.ts";
|
|
51
|
+
import type { RustOpenApiMapping, RustWorkspaceMapping } from "./project-rs.ts";
|
|
51
52
|
import type { ReleaseUnitGraph } from "./release-catalog.ts";
|
|
52
53
|
import { readWorkspaceVersion } from "./workspace-version.ts";
|
|
53
54
|
|
|
@@ -62,45 +63,95 @@ const SPEAKEASY_OPENAPI_RELEASE_URL = `https://github.com/speakeasy-api/openapi/
|
|
|
62
63
|
const execFileAsync = promisify(execFile);
|
|
63
64
|
|
|
64
65
|
// prettier-ignore
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
const CLIENT_SRC =
|
|
67
|
+
stringUtils.dedent(
|
|
68
|
+
// ============================================================================
|
|
69
|
+
/*ts*/`
|
|
70
|
+
import createClient, { type ClientOptions } from "openapi-fetch";
|
|
71
|
+
import type { paths } from "./schema";
|
|
72
|
+
|
|
73
|
+
/** Create a typed OpenAPI client (openapi-fetch); safe to use in the browser. */
|
|
74
|
+
export function createApiClient(options?: ClientOptions) {
|
|
75
|
+
return createClient<paths>(options);
|
|
76
|
+
}
|
|
77
|
+
`
|
|
78
|
+
// ============================================================================
|
|
79
|
+
) + "\n";
|
|
79
80
|
|
|
80
81
|
/** True if any module file in `<pkg>/src` matches {@link TSOA_IMPORT}. */
|
|
81
82
|
function hasTsoaControllers(pkg: Pick<RecordedPackage, "dir">): boolean {
|
|
82
|
-
const srcDir =
|
|
83
|
-
return
|
|
83
|
+
const srcDir = tsoaSource(pkg);
|
|
84
|
+
return object
|
|
85
|
+
.sequence(find.findFiles("**/*", { cwd: srcDir }))
|
|
84
86
|
.filter(isModuleFile)
|
|
85
|
-
.some((
|
|
87
|
+
.some((file) => TSOA_IMPORT.test(readFileSync(join(srcDir, file), "utf8")));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function tsoaSource(pkg: Pick<RecordedPackage, "dir">): string {
|
|
91
|
+
return join(pkg.dir, "src");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isTsoaCandidate(pkg: RecordedPackage): boolean {
|
|
95
|
+
return pkg.tags.includes("server") || pkg.tags.includes("node");
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
/** `server`/`node` packages (never the generated `openapi` tag) with a tsoa import. */
|
|
89
|
-
function controllerPackages(): RecordedPackage
|
|
90
|
-
return recordedPackages().filter(
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
function controllerPackages(): object.Sequence<RecordedPackage> {
|
|
100
|
+
return object.sequence(recordedPackages()).filter(isTsoaCandidate).filter(hasTsoaControllers);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function rustOpenapiMappings(): RustOpenApiMapping[] {
|
|
104
|
+
const manifest = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as unknown;
|
|
105
|
+
if (!object.isRecord(manifest)) return [];
|
|
106
|
+
const config = manifest.dbxToolsConfig;
|
|
107
|
+
if (!object.isRecord(config) || !object.isRecord(config.rust)) return [];
|
|
108
|
+
const mapping = config.rust as unknown as Partial<RustWorkspaceMapping>;
|
|
109
|
+
return Array.isArray(mapping.openapi) ? [...mapping.openapi] : [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function inside(path: string, directory: string): boolean {
|
|
113
|
+
const child = relative(resolve(directory), resolve(path));
|
|
114
|
+
return child === "" || (!child.startsWith("..") && !isAbsolute(child));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether one changed source belongs to any producing package directory. */
|
|
118
|
+
export function isOpenapiProducerSource(path: string, producers: readonly string[]): boolean {
|
|
119
|
+
const absolute = resolve(path);
|
|
120
|
+
return object.sequence(producers).some((producer) => inside(absolute, producer));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function rustOpenapiSources(mapping: RustOpenApiMapping): string[] {
|
|
124
|
+
return [resolve(repoRoot, mapping.rust, "src"), resolve(repoRoot, mapping.rust, "Cargo.toml")];
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
/**
|
|
96
|
-
export function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
/** Source roots consumed by TypeScript and Rust OpenAPI producers. */
|
|
128
|
+
export function openapiWatchRoots(): string[] {
|
|
129
|
+
return [
|
|
130
|
+
...controllerPackages()
|
|
131
|
+
.map((pkg) => resolve(tsoaSource(pkg)))
|
|
132
|
+
.join(object.sequence(rustOpenapiMappings()).flatMap(rustOpenapiSources))
|
|
133
|
+
.distinct(),
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Whether one changed source belongs to an OpenAPI-producing package. */
|
|
138
|
+
export function isOpenapiSource(path: string): boolean {
|
|
139
|
+
const absolute = resolve(path);
|
|
140
|
+
const segments = toPosix(relative(repoRoot, absolute)).split("/");
|
|
141
|
+
if (segments.includes(OPENAPI_TAG)) return false;
|
|
142
|
+
if (
|
|
143
|
+
object
|
|
144
|
+
.sequence(rustOpenapiMappings())
|
|
145
|
+
.flatMap(rustOpenapiSources)
|
|
146
|
+
.some((source) => inside(absolute, source))
|
|
147
|
+
) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return object
|
|
151
|
+
.sequence(recordedPackages())
|
|
152
|
+
.filter(isTsoaCandidate)
|
|
153
|
+
.filter((pkg) => inside(absolute, tsoaSource(pkg)))
|
|
154
|
+
.some(hasTsoaControllers);
|
|
104
155
|
}
|
|
105
156
|
|
|
106
157
|
/** GitHub release asset name for Speakeasy's OpenAPI binary. */
|
|
@@ -145,6 +196,59 @@ export async function optimizeOpenapiSpec(specPath: string, executable?: string)
|
|
|
145
196
|
await execFileAsync(openapi, ["spec", "optimize", specPath, "--write", "--non-interactive"]);
|
|
146
197
|
}
|
|
147
198
|
|
|
199
|
+
/** Cargo arguments that export one Rust producer's aide document. */
|
|
200
|
+
export function rustOpenapiArgs(
|
|
201
|
+
mapping: RustOpenApiMapping,
|
|
202
|
+
output: string,
|
|
203
|
+
targetDir = join(repoRoot, "target/openapi"),
|
|
204
|
+
): string[] {
|
|
205
|
+
return [
|
|
206
|
+
"run",
|
|
207
|
+
"--quiet",
|
|
208
|
+
"--target-dir",
|
|
209
|
+
targetDir,
|
|
210
|
+
"--package",
|
|
211
|
+
mapping.crate,
|
|
212
|
+
...(mapping.noDefaultFeatures ? ["--no-default-features"] : []),
|
|
213
|
+
...(mapping.features.length ? ["--features", mapping.features.join(",")] : []),
|
|
214
|
+
...(mapping.binary ? ["--bin", mapping.binary] : []),
|
|
215
|
+
"--",
|
|
216
|
+
"--generate-spec",
|
|
217
|
+
output,
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
type OpenApiTypeTools = {
|
|
222
|
+
openapiTS: typeof import("openapi-typescript").default;
|
|
223
|
+
astToString: typeof import("openapi-typescript").astToString;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
async function writeClientPackage(
|
|
227
|
+
outDir: string,
|
|
228
|
+
source: string,
|
|
229
|
+
tools: OpenApiTypeTools,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
const srcDir = join(outDir, "src");
|
|
232
|
+
mkdirSync(srcDir, { recursive: true });
|
|
233
|
+
const specPath = join(outDir, "openapi.json");
|
|
234
|
+
const spec = JSON.parse(readFileSync(specPath, "utf8"));
|
|
235
|
+
const schemaPath = join(srcDir, "schema.ts");
|
|
236
|
+
makeWritable(schemaPath);
|
|
237
|
+
writeFileSync(schemaPath, tools.astToString(await tools.openapiTS(spec)));
|
|
238
|
+
stampGenerated(schemaPath, {
|
|
239
|
+
tool: "projen openapi (Speakeasy + openapi-typescript)",
|
|
240
|
+
source,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const clientPath = join(srcDir, "client.ts");
|
|
244
|
+
makeWritable(clientPath);
|
|
245
|
+
writeFileSync(clientPath, CLIENT_SRC);
|
|
246
|
+
stampGenerated(clientPath, {
|
|
247
|
+
tool: "projen openapi (openapi-fetch)",
|
|
248
|
+
source: "./schema",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
148
252
|
/**
|
|
149
253
|
* Regenerate the `openapi` packages from every server/node package with a tsoa
|
|
150
254
|
* import. Returns the package dirs it wrote so the caller can rebuild their barrels.
|
|
@@ -152,35 +256,43 @@ export async function optimizeOpenapiSpec(specPath: string, executable?: string)
|
|
|
152
256
|
* members in `pnpm-workspace.yaml`.
|
|
153
257
|
*/
|
|
154
258
|
export async function generateOpenapi(): Promise<string[]> {
|
|
155
|
-
const pkgs = controllerPackages();
|
|
259
|
+
const pkgs = controllerPackages().toArray();
|
|
260
|
+
const rustMappings = rustOpenapiMappings();
|
|
156
261
|
// Same reasoning as codegen's empty case: a workspace with no tsoa controllers
|
|
157
262
|
// is not a condition worth a line on every synth.
|
|
158
|
-
if (pkgs.length === 0) {
|
|
159
|
-
logger.debug("no
|
|
263
|
+
if (pkgs.length === 0 && rustMappings.length === 0) {
|
|
264
|
+
logger.debug("no TypeScript or Rust OpenAPI producers found");
|
|
160
265
|
return [];
|
|
161
266
|
}
|
|
162
267
|
|
|
163
268
|
// Lazy, resilient loads: tsoa + typescript are CJS (require), openapi-typescript
|
|
164
269
|
// is ESM (dynamic import).
|
|
165
|
-
const require = createRequire(import.meta.url);
|
|
166
|
-
const { generateSpec } = lazyRequire<typeof import("tsoa")>(
|
|
167
|
-
require,
|
|
168
|
-
"tsoa",
|
|
169
|
-
"openapi generation",
|
|
170
|
-
);
|
|
171
|
-
const tsRuntime = lazyRequire<typeof ts>(require, "typescript", "openapi generation");
|
|
172
270
|
const { default: openapiTS, astToString } = await import("openapi-typescript");
|
|
271
|
+
const tools = { openapiTS, astToString };
|
|
173
272
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
273
|
+
const typescript =
|
|
274
|
+
pkgs.length > 0
|
|
275
|
+
? (() => {
|
|
276
|
+
const require = createRequire(import.meta.url);
|
|
277
|
+
const { generateSpec } = lazyRequire<typeof import("tsoa")>(
|
|
278
|
+
require,
|
|
279
|
+
"tsoa",
|
|
280
|
+
"openapi generation",
|
|
281
|
+
);
|
|
282
|
+
const runtime = lazyRequire<typeof ts>(require, "typescript", "openapi generation");
|
|
283
|
+
return {
|
|
284
|
+
generateSpec,
|
|
285
|
+
compilerOptions: {
|
|
286
|
+
experimentalDecorators: true,
|
|
287
|
+
target: runtime.ScriptTarget.ES2022,
|
|
288
|
+
module: runtime.ModuleKind.ESNext,
|
|
289
|
+
moduleResolution: runtime.ModuleResolutionKind.Bundler,
|
|
290
|
+
esModuleInterop: true,
|
|
291
|
+
skipLibCheck: true,
|
|
292
|
+
} satisfies ts.CompilerOptions,
|
|
293
|
+
};
|
|
294
|
+
})()
|
|
295
|
+
: undefined;
|
|
184
296
|
|
|
185
297
|
const releaseGraphPath = join(repoRoot, ".projen/release-units.json");
|
|
186
298
|
const releaseGraph = existsSync(releaseGraphPath)
|
|
@@ -196,8 +308,7 @@ export async function generateOpenapi(): Promise<string[]> {
|
|
|
196
308
|
// its npm name - `p.name` is the (possibly-overridden) manifest name.
|
|
197
309
|
const leaf = p.relPath.split("/").pop() ?? p.relPath;
|
|
198
310
|
const outDir = join(repoRoot, p.root, OPENAPI_TAG, leaf);
|
|
199
|
-
|
|
200
|
-
mkdirSync(srcDir, { recursive: true });
|
|
311
|
+
mkdirSync(outDir, { recursive: true });
|
|
201
312
|
|
|
202
313
|
// 1) tsoa writes a temporary openapi.json, Speakeasy optimizes it there, then
|
|
203
314
|
// the complete spec moves into place so readers never observe an intermediate file.
|
|
@@ -205,7 +316,7 @@ export async function generateOpenapi(): Promise<string[]> {
|
|
|
205
316
|
const tempDir = mkdtempSync(join(outDir, ".openapi-"));
|
|
206
317
|
const tempSpecPath = join(tempDir, "openapi.json");
|
|
207
318
|
try {
|
|
208
|
-
await generateSpec(
|
|
319
|
+
await typescript!.generateSpec(
|
|
209
320
|
{
|
|
210
321
|
entryFile: "",
|
|
211
322
|
noImplicitAdditionalProperties: "throw-on-extras",
|
|
@@ -216,7 +327,7 @@ export async function generateOpenapi(): Promise<string[]> {
|
|
|
216
327
|
name: `${p.relPath} API`,
|
|
217
328
|
version: specVersion,
|
|
218
329
|
},
|
|
219
|
-
compilerOptions,
|
|
330
|
+
typescript!.compilerOptions,
|
|
220
331
|
);
|
|
221
332
|
await optimizeOpenapiSpec(tempSpecPath);
|
|
222
333
|
makeWritable(specPath);
|
|
@@ -225,28 +336,29 @@ export async function generateOpenapi(): Promise<string[]> {
|
|
|
225
336
|
rmSync(tempDir, { recursive: true, force: true });
|
|
226
337
|
}
|
|
227
338
|
makeReadonly(specPath);
|
|
228
|
-
|
|
229
|
-
// 2) src/schema.ts: types generated from the spec (openapi-typescript).
|
|
230
|
-
const spec = JSON.parse(readFileSync(specPath, "utf8"));
|
|
231
|
-
const schemaPath = join(srcDir, "schema.ts");
|
|
232
|
-
makeWritable(schemaPath);
|
|
233
|
-
writeFileSync(schemaPath, astToString(await openapiTS(spec)));
|
|
234
|
-
stampGenerated(schemaPath, {
|
|
235
|
-
tool: "projen openapi (tsoa + Speakeasy + openapi-typescript)",
|
|
236
|
-
source: `the tsoa controllers in ${p.relPath}`,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// 3) src/client.ts: a typed openapi-fetch client over those types.
|
|
240
|
-
const clientPath = join(srcDir, "client.ts");
|
|
241
|
-
makeWritable(clientPath);
|
|
242
|
-
writeFileSync(clientPath, CLIENT_SRC);
|
|
243
|
-
stampGenerated(clientPath, {
|
|
244
|
-
tool: "projen openapi (openapi-fetch)",
|
|
245
|
-
source: "./schema",
|
|
246
|
-
});
|
|
339
|
+
await writeClientPackage(outDir, `the tsoa controllers in ${p.relPath}`, tools);
|
|
247
340
|
|
|
248
341
|
written.push(outDir);
|
|
249
342
|
logger.success(`openapi/${leaf} (from ${p.relPath})`);
|
|
250
343
|
}
|
|
344
|
+
for (const mapping of rustMappings) {
|
|
345
|
+
const outDir = resolve(repoRoot, mapping.output);
|
|
346
|
+
mkdirSync(outDir, { recursive: true });
|
|
347
|
+
const specPath = join(outDir, "openapi.json");
|
|
348
|
+
const tempDir = mkdtempSync(join(outDir, ".openapi-"));
|
|
349
|
+
const tempSpecPath = join(tempDir, "openapi.json");
|
|
350
|
+
try {
|
|
351
|
+
await execFileAsync("cargo", rustOpenapiArgs(mapping, tempSpecPath), { cwd: repoRoot });
|
|
352
|
+
await optimizeOpenapiSpec(tempSpecPath);
|
|
353
|
+
makeWritable(specPath);
|
|
354
|
+
renameSync(tempSpecPath, specPath);
|
|
355
|
+
} finally {
|
|
356
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
357
|
+
}
|
|
358
|
+
makeReadonly(specPath);
|
|
359
|
+
await writeClientPackage(outDir, `the aide routes in ${mapping.rust}`, tools);
|
|
360
|
+
written.push(outDir);
|
|
361
|
+
logger.success(`${mapping.output} (from ${mapping.rust})`);
|
|
362
|
+
}
|
|
251
363
|
return written;
|
|
252
364
|
}
|
package/src/project-js.ts
CHANGED
|
@@ -1064,18 +1064,14 @@ class GeneratedSource extends Component {
|
|
|
1064
1064
|
*
|
|
1065
1065
|
* projen gives a monorepo root empty `compile`/`test` tasks - a child's tasks
|
|
1066
1066
|
* are the child's business - so `bun run build` at the root type-checked nothing
|
|
1067
|
-
* and ran no package tests.
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* skipped instead of needing a guard, and the filter reads the workspace from
|
|
1072
|
-
* `package.json` - so it stays correct when a package is added without a
|
|
1073
|
-
* re-synth. A non-zero member exit still fails the run.
|
|
1067
|
+
* and ran no package tests. Compilation groups ordinary `tsc --build` members
|
|
1068
|
+
* into a few TypeScript processes and runs custom compile tasks alongside them. Tests
|
|
1069
|
+
* still use Bun's filtered workspace fan-out. Both read the current workspace
|
|
1070
|
+
* list from `package.json`, so a newly added member needs no re-synth.
|
|
1074
1071
|
*
|
|
1075
|
-
* `*` matches every workspace MEMBER and never the root itself, so the
|
|
1072
|
+
* `*` matches every workspace MEMBER and never the root itself, so the test
|
|
1076
1073
|
* task delegating to it cannot recurse. Members declared outside the scanned
|
|
1077
|
-
* package roots (`extraWorkspaceMembers`) are
|
|
1078
|
-
* covered by the same filter.
|
|
1074
|
+
* package roots (`extraWorkspaceMembers`) are covered by both tasks.
|
|
1079
1075
|
*/
|
|
1080
1076
|
class WorkspaceValidationTasks extends Component {
|
|
1081
1077
|
private configured = false;
|
|
@@ -1087,9 +1083,8 @@ class WorkspaceValidationTasks extends Component {
|
|
|
1087
1083
|
if (project.subprojects.length === 0 && project.extraWorkspaceMembers.length === 0) {
|
|
1088
1084
|
return;
|
|
1089
1085
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
}
|
|
1086
|
+
project.compileTask.exec(taskScript(project, "compile-workspace.ts"));
|
|
1087
|
+
project.testTask.exec("bun run --filter '*' test");
|
|
1093
1088
|
}
|
|
1094
1089
|
}
|
|
1095
1090
|
|
package/src/project-py.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workf
|
|
|
7
7
|
import { DBX_TOOLS_LICENSE, projectReleaseBranch, projectRepositoryUrl } from "./project-js.ts";
|
|
8
8
|
import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
|
|
9
9
|
import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
|
|
10
|
-
import { RELEASE_VERSION, releaseSourceSteps } from "./release-dispatch.ts";
|
|
10
|
+
import { RELEASE_TAG, RELEASE_VERSION, releaseSourceSteps } from "./release-dispatch.ts";
|
|
11
11
|
import {
|
|
12
12
|
independentReleaseSetupSteps,
|
|
13
13
|
registerIndependentPublicationJob,
|
|
@@ -546,9 +546,9 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
546
546
|
}
|
|
547
547
|
workflow.addJob("build-python", {
|
|
548
548
|
if: usesRustArtifacts
|
|
549
|
-
? "${{ always() && needs.verify-context.result == 'success' && needs.rust-
|
|
549
|
+
? "${{ always() && needs.verify-context.result == 'success' && needs.rust-assets.result == 'success' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}"
|
|
550
550
|
: releaseStageCondition("python"),
|
|
551
|
-
needs: ["verify-context", ...(usesRustArtifacts ? ["rust-
|
|
551
|
+
needs: ["verify-context", ...(usesRustArtifacts ? ["rust-assets"] : [])],
|
|
552
552
|
runsOn: ["ubuntu-latest"],
|
|
553
553
|
permissions: { actions: JobPermission.READ, contents: JobPermission.READ },
|
|
554
554
|
timeoutMinutes: 20,
|
|
@@ -567,8 +567,27 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
567
567
|
recoveredName: `Download recovered ${publication.distribution} native wheels`,
|
|
568
568
|
pattern: `${publication.distribution}--*--python-wheel`,
|
|
569
569
|
path: `dist/${publication.directory}`,
|
|
570
|
+
condition: "needs.verify-context.outputs.build_mode == 'remote'",
|
|
570
571
|
}),
|
|
571
572
|
),
|
|
573
|
+
...(uniffiPublications.length
|
|
574
|
+
? [
|
|
575
|
+
{
|
|
576
|
+
name: "Download locally built native wheels",
|
|
577
|
+
if: "${{ needs.verify-context.outputs.build_mode == 'local' }}",
|
|
578
|
+
env: { GH_TOKEN: "${{ github.token }}", RELEASE_TAG },
|
|
579
|
+
shell: "bash",
|
|
580
|
+
run: [
|
|
581
|
+
"mkdir -p dist/local-wheels",
|
|
582
|
+
'gh release download "$RELEASE_TAG" --pattern "*.whl" --dir dist/local-wheels',
|
|
583
|
+
...uniffiPublications.flatMap((publication) => [
|
|
584
|
+
`mkdir -p dist/${publication.directory}`,
|
|
585
|
+
`cp dist/local-wheels/${publication.distribution.replaceAll("-", "_")}-*.whl dist/${publication.directory}/`,
|
|
586
|
+
]),
|
|
587
|
+
].join("\n"),
|
|
588
|
+
},
|
|
589
|
+
]
|
|
590
|
+
: []),
|
|
572
591
|
{
|
|
573
592
|
name: "Stamp workspace versions",
|
|
574
593
|
env: { VERSION: RELEASE_VERSION },
|
package/src/project-rs.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Filesystem-discovered Rust workspaces and UniFFI binding package wiring. */
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
5
5
|
import { stringUtils } from "@dbx-tools/shared-core";
|
|
6
6
|
import { Component, Project, TextFile, TomlFile, javascript } from "projen";
|
|
7
|
+
import { releaseBinaryAssetName } from "./_release-platform.ts";
|
|
7
8
|
import {
|
|
8
9
|
RustProject,
|
|
9
10
|
type CargoExampleOptions as RustCargoExampleOptions,
|
|
@@ -15,8 +16,10 @@ import {
|
|
|
15
16
|
type DBXToolsRustProjectOptions as RustProjectOptions,
|
|
16
17
|
type RustCrateOptions as RustCrateConfiguration,
|
|
17
18
|
type RustCliOptions as RustCliConfiguration,
|
|
19
|
+
type RustOpenApiOptions as RustOpenApiConfiguration,
|
|
18
20
|
} from "./_rust-project.ts";
|
|
19
21
|
import {
|
|
22
|
+
rustAssetsJob,
|
|
20
23
|
configureRustReleaseTask,
|
|
21
24
|
independentRustCargoPublishJob,
|
|
22
25
|
independentRustGitHubReleaseJob,
|
|
@@ -24,7 +27,6 @@ import {
|
|
|
24
27
|
independentRustNodeFacadePublishJob,
|
|
25
28
|
orderRustBindings,
|
|
26
29
|
planRustRelease,
|
|
27
|
-
releaseBinaryAssetName,
|
|
28
30
|
rustBuildJob,
|
|
29
31
|
rustCargoPublishJob,
|
|
30
32
|
rustGitHubReleaseJob,
|
|
@@ -57,6 +59,7 @@ export type CargoExampleOptions = RustCargoExampleOptions;
|
|
|
57
59
|
export type DBXToolsRustProjectOptions = RustProjectOptions;
|
|
58
60
|
export type RustCrateOptions = RustCrateConfiguration;
|
|
59
61
|
export type RustCliOptions = RustCliConfiguration;
|
|
62
|
+
export type RustOpenApiOptions = RustOpenApiConfiguration;
|
|
60
63
|
|
|
61
64
|
/** Public Rust project facade; implementation lives apart from workspace/release coordination. */
|
|
62
65
|
export class DBXToolsRustProject extends RustProject {
|
|
@@ -116,6 +119,7 @@ export interface UniFFIReleaseTarget {
|
|
|
116
119
|
readonly os: RustReleaseOs;
|
|
117
120
|
readonly cpu: RustReleaseCpu;
|
|
118
121
|
readonly libc?: "glibc";
|
|
122
|
+
readonly glibcVersion?: string;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
/** Native targets built on matching GitHub-hosted runners. */
|
|
@@ -128,6 +132,7 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
|
|
|
128
132
|
os: RustReleaseOs.LINUX,
|
|
129
133
|
cpu: RustReleaseCpu.X64,
|
|
130
134
|
libc: "glibc",
|
|
135
|
+
glibcVersion: "2.35",
|
|
131
136
|
},
|
|
132
137
|
{
|
|
133
138
|
runner: "ubuntu-24.04-arm",
|
|
@@ -137,6 +142,7 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
|
|
|
137
142
|
os: RustReleaseOs.LINUX,
|
|
138
143
|
cpu: RustReleaseCpu.ARM64,
|
|
139
144
|
libc: "glibc",
|
|
145
|
+
glibcVersion: "2.39",
|
|
140
146
|
},
|
|
141
147
|
{
|
|
142
148
|
runner: "macos-15-intel",
|
|
@@ -269,12 +275,23 @@ export interface RustBindingMapping {
|
|
|
269
275
|
readonly dependencies?: readonly string[];
|
|
270
276
|
}
|
|
271
277
|
|
|
278
|
+
/** Persisted Rust OpenAPI producer consumed by the shared OpenAPI task. */
|
|
279
|
+
export interface RustOpenApiMapping {
|
|
280
|
+
readonly crate: string;
|
|
281
|
+
readonly rust: string;
|
|
282
|
+
readonly output: string;
|
|
283
|
+
readonly binary?: string;
|
|
284
|
+
readonly features: readonly string[];
|
|
285
|
+
readonly noDefaultFeatures: boolean;
|
|
286
|
+
}
|
|
287
|
+
|
|
272
288
|
/** Persisted Rust workspace state consumed by `sync --watch`. */
|
|
273
289
|
export interface RustWorkspaceMapping {
|
|
274
290
|
readonly root: string;
|
|
275
291
|
readonly crates: readonly string[];
|
|
276
292
|
readonly bindings: readonly RustBindingMapping[];
|
|
277
293
|
readonly binaries: readonly RustReleaseBinaryMapping[];
|
|
294
|
+
readonly openapi: readonly RustOpenApiMapping[];
|
|
278
295
|
}
|
|
279
296
|
|
|
280
297
|
/** One platform archive published for a Rust CLI binary. */
|
|
@@ -296,6 +313,10 @@ export interface RustReleaseBinaryMapping {
|
|
|
296
313
|
readonly tagPrefix: string;
|
|
297
314
|
readonly tag: string;
|
|
298
315
|
readonly repository: string;
|
|
316
|
+
/** Cargo crate published or mirrored for `cargo install` when GitHub has no archive. */
|
|
317
|
+
readonly crateName: string;
|
|
318
|
+
/** Feature flags passed to `cargo install --features` for this binary target. */
|
|
319
|
+
readonly cargoFeatures: readonly string[];
|
|
299
320
|
readonly assets: readonly RustReleaseBinaryAssetMapping[];
|
|
300
321
|
}
|
|
301
322
|
|
|
@@ -392,6 +413,7 @@ function planRustReleaseBinaries(
|
|
|
392
413
|
release: pkg.packageOptions.release,
|
|
393
414
|
excludedOs: pkg.packageOptions.releaseExcludeOs,
|
|
394
415
|
cli: pkg.packageOptions.cli,
|
|
416
|
+
requiredFeatures: undefined,
|
|
395
417
|
},
|
|
396
418
|
]
|
|
397
419
|
: []),
|
|
@@ -404,6 +426,7 @@ function planRustReleaseBinaries(
|
|
|
404
426
|
release: binary.release,
|
|
405
427
|
excludedOs: binary.releaseExcludeOs,
|
|
406
428
|
cli: binary.cli!,
|
|
429
|
+
requiredFeatures: binary.requiredFeatures,
|
|
407
430
|
})),
|
|
408
431
|
];
|
|
409
432
|
return configuredBinaries.map((binary) => {
|
|
@@ -450,6 +473,8 @@ function planRustReleaseBinaries(
|
|
|
450
473
|
tagPrefix,
|
|
451
474
|
tag: `${tagPrefix}${identity.version}`,
|
|
452
475
|
repository: resolved.repository,
|
|
476
|
+
crateName: pkg.crateName,
|
|
477
|
+
cargoFeatures: [...(binary.requiredFeatures ?? [])],
|
|
453
478
|
assets: resolved.nativeTargets
|
|
454
479
|
.filter((target) => !excludedOs.has(target.os))
|
|
455
480
|
.map((target) => ({
|
|
@@ -770,11 +795,26 @@ function createRustWorkspaceMapping(
|
|
|
770
795
|
binaries: readonly RustReleaseBinaryMapping[],
|
|
771
796
|
resolved: ResolvedRustWorkspaceOptions,
|
|
772
797
|
): RustWorkspaceMapping {
|
|
798
|
+
const openapiRoot = join(dirname(resolved.nodeRoot), "openapi");
|
|
773
799
|
return {
|
|
774
800
|
root: resolved.root,
|
|
775
801
|
crates: packages.map((pkg) => `${resolved.root}/${pkg.packageOptions.directory}`),
|
|
776
802
|
bindings,
|
|
777
803
|
binaries,
|
|
804
|
+
openapi: packages.flatMap((pkg) => {
|
|
805
|
+
if (!pkg.packageOptions.openapi) return [];
|
|
806
|
+
const options = pkg.packageOptions.openapi === true ? {} : pkg.packageOptions.openapi;
|
|
807
|
+
return [
|
|
808
|
+
{
|
|
809
|
+
crate: pkg.crateName,
|
|
810
|
+
rust: `${resolved.root}/${pkg.packageOptions.directory}`,
|
|
811
|
+
output: toPosix(join(openapiRoot, pkg.packageOptions.directory)),
|
|
812
|
+
...(options.binary ? { binary: options.binary } : {}),
|
|
813
|
+
features: [...(options.features ?? [])],
|
|
814
|
+
noDefaultFeatures: options.noDefaultFeatures ?? false,
|
|
815
|
+
},
|
|
816
|
+
];
|
|
817
|
+
}),
|
|
778
818
|
};
|
|
779
819
|
}
|
|
780
820
|
|
|
@@ -963,6 +1003,9 @@ export class DBXToolsRustWorkspace extends Component {
|
|
|
963
1003
|
name: binary.binaryName,
|
|
964
1004
|
generated: true,
|
|
965
1005
|
data: {
|
|
1006
|
+
binary: binary.binaryName,
|
|
1007
|
+
crate: binary.crateName,
|
|
1008
|
+
features: binary.cargoFeatures,
|
|
966
1009
|
targets: resolved.nativeTargets.filter((target) =>
|
|
967
1010
|
binary.assets.some((asset) => asset.os === target.os && asset.cpu === target.cpu),
|
|
968
1011
|
),
|
|
@@ -1059,7 +1102,7 @@ export class DBXToolsRustWorkspace extends Component {
|
|
|
1059
1102
|
}
|
|
1060
1103
|
configureRustReleaseTask(project, plan);
|
|
1061
1104
|
if (project.releaseCatalog.mode === "independent") {
|
|
1062
|
-
if (plan.
|
|
1105
|
+
if (plan.targets.length) {
|
|
1063
1106
|
workflow.addJob("rust-build", rustBuildJob(plan, independentReleaseSetupSteps(project)));
|
|
1064
1107
|
}
|
|
1065
1108
|
if (plan.publicCrates.length) {
|
|
@@ -1081,15 +1124,16 @@ export class DBXToolsRustWorkspace extends Component {
|
|
|
1081
1124
|
}
|
|
1082
1125
|
return;
|
|
1083
1126
|
}
|
|
1084
|
-
if (plan.
|
|
1127
|
+
if (plan.targets.length) {
|
|
1085
1128
|
workflow.addJob("rust-build", rustBuildJob(plan));
|
|
1129
|
+
workflow.addJob("rust-assets", rustAssetsJob());
|
|
1086
1130
|
}
|
|
1087
1131
|
if (plan.publicCrates.length) {
|
|
1088
1132
|
workflow.addJob("publish-cargo", rustCargoPublishJob(plan, false));
|
|
1089
1133
|
workflow.addJob("publish-local-cargo", rustCargoPublishJob(plan, true));
|
|
1090
1134
|
}
|
|
1091
|
-
if (plan.
|
|
1092
|
-
workflow.addJob("publish-github-release", rustGitHubReleaseJob());
|
|
1135
|
+
if (plan.hasTargetOutputs) {
|
|
1136
|
+
workflow.addJob("publish-github-release", rustGitHubReleaseJob(plan));
|
|
1093
1137
|
}
|
|
1094
1138
|
if (plan.nodeBindings.length && hasNodeRelease(project)) {
|
|
1095
1139
|
workflow.addJob("publish-native-npm", rustNativeNpmPublishJob(project));
|
package/src/release-github.ts
CHANGED
|
@@ -28,6 +28,29 @@ export interface GithubAuthenticatedAccount {
|
|
|
28
28
|
scopes: string[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Repository capabilities returned for one authenticated actor. */
|
|
32
|
+
export interface GithubRepositoryPermissions {
|
|
33
|
+
readonly push: boolean;
|
|
34
|
+
readonly maintain: boolean;
|
|
35
|
+
readonly admin: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Read normalized repository permissions from a GitHub repository response. */
|
|
39
|
+
export function githubRepositoryPermissions(value: unknown): GithubRepositoryPermissions {
|
|
40
|
+
const repository = object.isRecord(value) ? value : undefined;
|
|
41
|
+
const permissions = object.isRecord(repository?.permissions) ? repository.permissions : undefined;
|
|
42
|
+
return {
|
|
43
|
+
push: permissions?.push === true,
|
|
44
|
+
maintain: permissions?.maintain === true,
|
|
45
|
+
admin: permissions?.admin === true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether the actor can manage releases rather than only push source. */
|
|
50
|
+
export function githubRepositoryCanManage(permissions: GithubRepositoryPermissions): boolean {
|
|
51
|
+
return permissions.maintain || permissions.admin;
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
/** Parse successful GitHub CLI accounts with the active account first. */
|
|
32
55
|
export function githubAuthenticatedAccounts(
|
|
33
56
|
statusOutput: string,
|