@dbx-tools/projen 0.9.5 → 0.9.7
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 +69 -17
- package/index.ts +7 -5
- package/package.json +9 -6
- package/src/_rust-project.ts +356 -0
- package/src/_rust-release-workflow.ts +954 -0
- package/src/_rust-release.ts +605 -0
- package/src/_task-command.ts +89 -0
- package/src/barrels.ts +12 -11
- package/src/bun-app.ts +96 -85
- package/src/bun-workflow.ts +60 -52
- package/src/engine-root.ts +5 -5
- package/src/openapi.ts +16 -9
- package/src/packages.ts +9 -9
- package/src/pnpm-workspace.ts +1 -1
- package/src/project-js.ts +19 -25
- package/src/project-py.ts +28 -23
- package/src/project-rs.ts +100 -1098
- package/src/projen-version.ts +9 -0
- package/src/publish.ts +3 -4
- package/src/release-catalog.ts +1 -1
- package/src/release.ts +212 -177
- package/src/scaffold.ts +1 -1
- package/src/uniffi.ts +7 -0
- package/src/watch.ts +2 -2
- package/src/workspace-version.ts +20 -15
- package/tasks/barrels.ts +4 -4
- package/tasks/build-rust-release.ts +68 -0
- package/tasks/bump.ts +3 -2
- package/tasks/clean.ts +3 -3
- package/tasks/local-publish.ts +21 -3
- package/tasks/openapi.ts +2 -2
- package/tasks/publish-npm.ts +12 -6
- package/tasks/publish-python.ts +2 -2
- package/tasks/publish-uniffi-local.ts +11 -15
- package/tasks/publish.ts +88 -88
- package/tasks/python-registry.ts +1 -1
- package/tasks/release-bootstrap.ts +2 -2
- package/tasks/release-plan.ts +1 -1
- package/tasks/release-please.ts +1 -1
- package/tasks/release-pr.ts +60 -115
- package/tasks/release-request.ts +8 -29
- package/tasks/release-summary-units.ts +2 -9
- package/tasks/release-summary.ts +35 -24
- package/tasks/release-version.ts +44 -0
- package/tasks/rust-release.d.ts +41 -0
- package/tasks/rust-release.mjs +1717 -0
- package/tasks/sync.ts +3 -3
- package/tasks/uniffi-python.js +1 -1
- package/tasks/uniffi.ts +7 -1
- package/tasks/version-check.ts +6 -5
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
/** Rust release planning and generated GitHub workflow jobs. */
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { stringUtils } from "@dbx-tools/shared-core";
|
|
7
|
+
import { TextFile, javascript } from "projen";
|
|
8
|
+
import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
|
|
9
|
+
import type { RustProject } from "./_rust-project.ts";
|
|
10
|
+
import { BUN_VERSION } from "./bun-workflow.ts";
|
|
11
|
+
import type { DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
12
|
+
import type {
|
|
13
|
+
DBXToolsRustWorkspaceOptions,
|
|
14
|
+
RustBindingMapping,
|
|
15
|
+
RustReleaseOs,
|
|
16
|
+
UniFFIReleaseTarget,
|
|
17
|
+
} from "./project-rs.ts";
|
|
18
|
+
import { defaultReleaseUnitId } from "./release-catalog.ts";
|
|
19
|
+
import {
|
|
20
|
+
RELEASE_SHA,
|
|
21
|
+
RELEASE_SUMMARY_FILE,
|
|
22
|
+
RELEASE_TAG,
|
|
23
|
+
RELEASE_VERSION,
|
|
24
|
+
releaseSourceSteps,
|
|
25
|
+
} from "./release-dispatch.ts";
|
|
26
|
+
import {
|
|
27
|
+
independentReleaseSetupSteps,
|
|
28
|
+
npmPublishEnvironment,
|
|
29
|
+
nodeReleaseSetupSteps,
|
|
30
|
+
releaseArtifactSteps,
|
|
31
|
+
releaseStageCondition,
|
|
32
|
+
} from "./release.ts";
|
|
33
|
+
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
|
|
36
|
+
type RustPackageDependencyResolver = (pkg: RustProject) => RustProject[];
|
|
37
|
+
|
|
38
|
+
const RUST_CACHE_ENV = {
|
|
39
|
+
CARGO_INCREMENTAL: "0",
|
|
40
|
+
CARGO_TERM_COLOR: "always",
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
function rustCacheSteps(sharedKey: string): readonly JobStep[] {
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
name: "Cache Cargo registry and dependencies",
|
|
47
|
+
id: "cargo_cache",
|
|
48
|
+
uses: "Swatinem/rust-cache@v2.9.2",
|
|
49
|
+
with: {
|
|
50
|
+
"cache-targets": true,
|
|
51
|
+
"cache-workspace-crates": true,
|
|
52
|
+
"add-job-id-key": false,
|
|
53
|
+
"add-rust-environment-hash-key": true,
|
|
54
|
+
"shared-key": sharedKey,
|
|
55
|
+
"save-if": "${{ github.event_name == 'push' }}",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function timedBash(phase: string, command: string): string {
|
|
62
|
+
return [
|
|
63
|
+
"SECONDS=0",
|
|
64
|
+
`trap 'status=$?; echo "phase=${phase} duration_seconds=$SECONDS status=$status"; exit "$status"' EXIT`,
|
|
65
|
+
command,
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function releaseBinaryAssetName(
|
|
70
|
+
binaryName: string,
|
|
71
|
+
nodeTarget: string,
|
|
72
|
+
os: RustReleaseOs | "darwin" | "linux" | "win32",
|
|
73
|
+
): string {
|
|
74
|
+
const extension = os === "win32" ? "zip" : "tar.gz";
|
|
75
|
+
return `${binaryName}-${nodeTarget}.${extension}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function taskSource(name: string): string {
|
|
79
|
+
const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
|
80
|
+
const candidates = [
|
|
81
|
+
resolve(sourceDirectory, `../tasks/${name}`),
|
|
82
|
+
resolve(sourceDirectory, `../../tasks/${name}`),
|
|
83
|
+
];
|
|
84
|
+
const source = candidates.find(existsSync);
|
|
85
|
+
if (!source) throw new Error(`Could not locate tasks/${name}`);
|
|
86
|
+
return readFileSync(source, "utf8");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function smolTomlSource(name: "dist/index.cjs" | "LICENSE"): string {
|
|
90
|
+
const entry = require.resolve("smol-toml");
|
|
91
|
+
return readFileSync(
|
|
92
|
+
resolve(dirname(entry), name === "LICENSE" ? "../LICENSE" : "index.cjs"),
|
|
93
|
+
"utf8",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function orderRustBindings(bindings: readonly RustBindingMapping[]): RustBindingMapping[] {
|
|
98
|
+
const ordered: RustBindingMapping[] = [];
|
|
99
|
+
const visiting = new Set<string>();
|
|
100
|
+
const completed = new Set<string>();
|
|
101
|
+
const visit = (binding: RustBindingMapping): void => {
|
|
102
|
+
if (completed.has(binding.crate)) return;
|
|
103
|
+
if (visiting.has(binding.crate)) {
|
|
104
|
+
throw new Error(`Cyclic Rust binding dependency: ${binding.crate}`);
|
|
105
|
+
}
|
|
106
|
+
visiting.add(binding.crate);
|
|
107
|
+
for (const name of binding.dependencies ?? []) {
|
|
108
|
+
const dependency = bindings.find((candidate) => candidate.crate === name);
|
|
109
|
+
if (!dependency) throw new Error(`Missing Rust binding dependency: ${name}`);
|
|
110
|
+
visit(dependency);
|
|
111
|
+
}
|
|
112
|
+
visiting.delete(binding.crate);
|
|
113
|
+
completed.add(binding.crate);
|
|
114
|
+
ordered.push(binding);
|
|
115
|
+
};
|
|
116
|
+
for (const binding of bindings) visit(binding);
|
|
117
|
+
return ordered;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface RustReleaseBinding extends RustBindingMapping {
|
|
121
|
+
readonly node: string;
|
|
122
|
+
readonly python: string;
|
|
123
|
+
readonly nodePackage: string;
|
|
124
|
+
readonly pythonPackage: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface RustReleaseBinaryPlan {
|
|
128
|
+
readonly crate: string;
|
|
129
|
+
readonly binary: string;
|
|
130
|
+
readonly excludedOs: readonly RustReleaseOs[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type RustReleaseTargetPlan = Readonly<Record<string, string | boolean | number>>;
|
|
134
|
+
|
|
135
|
+
export interface RustReleasePlan {
|
|
136
|
+
readonly releaseRustVersion: string;
|
|
137
|
+
readonly releaseTask: string;
|
|
138
|
+
readonly releaseHelper: string;
|
|
139
|
+
readonly rustRoot: string;
|
|
140
|
+
readonly bindings: readonly RustReleaseBinding[];
|
|
141
|
+
readonly nodeBindings: readonly RustReleaseBinding[];
|
|
142
|
+
readonly releaseBinaries: readonly RustReleaseBinaryPlan[];
|
|
143
|
+
readonly publicCrates: readonly string[];
|
|
144
|
+
readonly targets: readonly RustReleaseTargetPlan[];
|
|
145
|
+
readonly hasReleaseExclusions: boolean;
|
|
146
|
+
readonly hasPythonBindings: boolean;
|
|
147
|
+
readonly usesCargoLock: boolean;
|
|
148
|
+
readonly usePreinstalledWindowsRust: boolean;
|
|
149
|
+
readonly hasTargetOutputs: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function planRustRelease(
|
|
153
|
+
project: javascript.NodeProject,
|
|
154
|
+
options: DBXToolsRustWorkspaceOptions,
|
|
155
|
+
targets: readonly UniFFIReleaseTarget[],
|
|
156
|
+
packages: readonly RustProject[],
|
|
157
|
+
bindingMappings: readonly RustBindingMapping[],
|
|
158
|
+
packageDependencies: RustPackageDependencyResolver,
|
|
159
|
+
): RustReleasePlan {
|
|
160
|
+
const releaseRustVersion = options.releaseRustVersion ?? "stable";
|
|
161
|
+
const bindings = bindingMappings.map((binding) => ({
|
|
162
|
+
...binding,
|
|
163
|
+
node: binding.node ?? "",
|
|
164
|
+
python: binding.python ?? "",
|
|
165
|
+
nodePackage: binding.nodePackage ?? "",
|
|
166
|
+
pythonPackage: binding.pythonPackage ?? "",
|
|
167
|
+
}));
|
|
168
|
+
const releaseBinaries = packages
|
|
169
|
+
.filter((pkg) => pkg.packageOptions.release)
|
|
170
|
+
.map((pkg) => ({
|
|
171
|
+
crate: pkg.crateName,
|
|
172
|
+
binary: pkg.packageOptions.binaryName ?? pkg.crateName,
|
|
173
|
+
excludedOs: pkg.packageOptions.releaseExcludeOs ?? [],
|
|
174
|
+
}));
|
|
175
|
+
const publicCrates = orderRustBindings(
|
|
176
|
+
packages
|
|
177
|
+
.filter((pkg) => !pkg.packageOptions.private)
|
|
178
|
+
.map((pkg) => ({
|
|
179
|
+
crate: pkg.crateName,
|
|
180
|
+
rust: pkg.outdir,
|
|
181
|
+
dependencies: packageDependencies(pkg).map((dependency) => dependency.crateName),
|
|
182
|
+
})),
|
|
183
|
+
).map((pkg) => pkg.crate);
|
|
184
|
+
const hasReleaseExclusions = packages.some((pkg) => pkg.packageOptions.releaseExcludeOs?.length);
|
|
185
|
+
const plannedTargets: RustReleaseTargetPlan[] = targets.map((target) => {
|
|
186
|
+
const cargoExcludes = packages
|
|
187
|
+
.filter((pkg) => pkg.packageOptions.releaseExcludeOs?.includes(target.os))
|
|
188
|
+
.map((pkg) => `--exclude ${pkg.crateName}`)
|
|
189
|
+
.join(" ");
|
|
190
|
+
return {
|
|
191
|
+
runner: target.runner,
|
|
192
|
+
cargo: target.cargo,
|
|
193
|
+
node: target.node,
|
|
194
|
+
python: target.python,
|
|
195
|
+
os: target.os,
|
|
196
|
+
cpu: target.cpu,
|
|
197
|
+
...(target.libc ? { libc: target.libc } : {}),
|
|
198
|
+
...(hasReleaseExclusions ? { cargoExcludes } : {}),
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
const hasTargetOutputs =
|
|
202
|
+
bindings.length > 0 || releaseBinaries.length > 0 || publicCrates.length > 0;
|
|
203
|
+
if (hasTargetOutputs && plannedTargets.length === 0) {
|
|
204
|
+
throw new Error("Rust release requires at least one target");
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
releaseRustVersion,
|
|
208
|
+
releaseTask: ".projen/uniffi-release.mjs",
|
|
209
|
+
releaseHelper: ".projen/rust-release.mjs",
|
|
210
|
+
rustRoot: options.root ?? "packages/rs",
|
|
211
|
+
bindings,
|
|
212
|
+
nodeBindings: bindings.filter((binding) => Boolean(binding.node && binding.nodePackage)),
|
|
213
|
+
releaseBinaries,
|
|
214
|
+
publicCrates,
|
|
215
|
+
targets: plannedTargets,
|
|
216
|
+
hasReleaseExclusions,
|
|
217
|
+
hasPythonBindings: bindings.some((binding) => binding.python),
|
|
218
|
+
usesCargoLock: existsSync(join(project.outdir, "Cargo.lock")),
|
|
219
|
+
usePreinstalledWindowsRust: releaseRustVersion === "stable",
|
|
220
|
+
hasTargetOutputs,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function configureRustReleaseTask(
|
|
225
|
+
project: javascript.NodeProject,
|
|
226
|
+
plan: RustReleasePlan,
|
|
227
|
+
): void {
|
|
228
|
+
const bindingSupportFiles = [
|
|
229
|
+
plan.releaseTask,
|
|
230
|
+
".projen/uniffi-python.js",
|
|
231
|
+
".projen/smol-toml.cjs",
|
|
232
|
+
".projen/smol-toml.LICENSE",
|
|
233
|
+
] as const;
|
|
234
|
+
new TextFile(project, plan.releaseHelper, {
|
|
235
|
+
lines: taskSource("rust-release.mjs").trimEnd().split("\n"),
|
|
236
|
+
});
|
|
237
|
+
if (plan.bindings.length) {
|
|
238
|
+
new TextFile(project, plan.releaseTask, {
|
|
239
|
+
lines: taskSource("uniffi-release.mjs").trimEnd().split("\n"),
|
|
240
|
+
});
|
|
241
|
+
new TextFile(project, bindingSupportFiles[1], {
|
|
242
|
+
lines: taskSource("uniffi-python.js").trimEnd().split("\n"),
|
|
243
|
+
});
|
|
244
|
+
new TextFile(project, bindingSupportFiles[2], {
|
|
245
|
+
lines: smolTomlSource("dist/index.cjs").trimEnd().split("\n"),
|
|
246
|
+
});
|
|
247
|
+
new TextFile(project, bindingSupportFiles[3], {
|
|
248
|
+
lines: smolTomlSource("LICENSE").trimEnd().split("\n"),
|
|
249
|
+
});
|
|
250
|
+
} else {
|
|
251
|
+
for (const path of bindingSupportFiles) project.tryRemoveFile(path);
|
|
252
|
+
}
|
|
253
|
+
project.addTask("rs:release-fingerprint", {
|
|
254
|
+
description: "Write the version-independent Rust release build manifest",
|
|
255
|
+
exec: [
|
|
256
|
+
`node ${plan.releaseHelper} fingerprint`,
|
|
257
|
+
"--root .",
|
|
258
|
+
...plan.targets.map(
|
|
259
|
+
(target) =>
|
|
260
|
+
`--target ${JSON.stringify(`${String(target.cargo)}|${String(target.cargoExcludes ?? "")}`)}`,
|
|
261
|
+
),
|
|
262
|
+
`--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
|
|
263
|
+
`--source ${JSON.stringify(plan.rustRoot)}`,
|
|
264
|
+
"--portable",
|
|
265
|
+
].join(" "),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function rustBindingCommands(plan: RustReleasePlan, independent: boolean): string[] {
|
|
270
|
+
return plan.bindings.map((binding) => {
|
|
271
|
+
const unit = defaultReleaseUnitId("rust", binding.crate);
|
|
272
|
+
const command = [
|
|
273
|
+
`node ${plan.releaseTask} build`,
|
|
274
|
+
`--crate "${binding.crate}"`,
|
|
275
|
+
`--node "${binding.node}"`,
|
|
276
|
+
`--python "${binding.python}"`,
|
|
277
|
+
`--node-package "${binding.nodePackage}"`,
|
|
278
|
+
`--python-package "${binding.pythonPackage}"`,
|
|
279
|
+
`--python-module "${binding.pythonModule}"`,
|
|
280
|
+
'--cargo-target "${{ matrix.cargo }}"',
|
|
281
|
+
'--node-triple "${{ matrix.node }}"',
|
|
282
|
+
'--python-tag "${{ matrix.python }}"',
|
|
283
|
+
'--os "${{ matrix.os }}"',
|
|
284
|
+
'--cpu "${{ matrix.cpu }}"',
|
|
285
|
+
'--libc "${{ matrix.libc }}"',
|
|
286
|
+
independent
|
|
287
|
+
? `--version "$(jq -r --arg unit "${unit}" '.units[] | select(.id == $unit) | .newVersion' dist/release-plan.json)"`
|
|
288
|
+
: '--version "$VERSION"',
|
|
289
|
+
`--output "dist/release/${binding.crate}/\${{ matrix.node }}"`,
|
|
290
|
+
"--skip-build",
|
|
291
|
+
].join(" \\\n ");
|
|
292
|
+
return independent
|
|
293
|
+
? [
|
|
294
|
+
`if jq -e --arg unit "${unit}" '.units[] | select(.id == $unit)' dist/release-plan.json >/dev/null; then`,
|
|
295
|
+
` ${command.replaceAll("\n", "\n ")}`,
|
|
296
|
+
"fi",
|
|
297
|
+
].join("\n")
|
|
298
|
+
: command;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function rustReleaseBinaryCondition(excludedOs: readonly RustReleaseOs[]): string | undefined {
|
|
303
|
+
return excludedOs.length
|
|
304
|
+
? `\${{ ${excludedOs.map((os) => `matrix.os != '${os}'`).join(" && ")} }}`
|
|
305
|
+
: undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function rustBinaryCommands(plan: RustReleasePlan, independent: boolean): string[] {
|
|
309
|
+
return plan.releaseBinaries.flatMap((pkg) => {
|
|
310
|
+
const unit = defaultReleaseUnitId("rust", pkg.crate);
|
|
311
|
+
const windowsAsset = releaseBinaryAssetName(pkg.binary, "${{ matrix.node }}", "win32");
|
|
312
|
+
const unixAsset = releaseBinaryAssetName(pkg.binary, "${{ matrix.node }}", "linux");
|
|
313
|
+
const commands = [
|
|
314
|
+
`mkdir -p "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
|
|
315
|
+
`SOURCE="target/\${{ matrix.cargo }}/release/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
|
|
316
|
+
`DESTINATION="dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage/${pkg.binary}\${{ matrix.os == 'win32' && '.exe' || '' }}"`,
|
|
317
|
+
'cp "$SOURCE" "$DESTINATION"',
|
|
318
|
+
'if [ "${{ matrix.os }}" = "win32" ]; then',
|
|
319
|
+
` 7z a "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${windowsAsset}" "$DESTINATION"`,
|
|
320
|
+
"else",
|
|
321
|
+
` tar -C "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage" -czf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/${unixAsset}" "${pkg.binary}"`,
|
|
322
|
+
"fi",
|
|
323
|
+
`rm -rf "dist/release/${pkg.crate}/\${{ matrix.node }}/binary/stage"`,
|
|
324
|
+
];
|
|
325
|
+
const excludedCondition = pkg.excludedOs
|
|
326
|
+
.map((os) => `[ "\${{ matrix.os }}" != "${os}" ]`)
|
|
327
|
+
.join(" && ");
|
|
328
|
+
const platformCommands = excludedCondition
|
|
329
|
+
? [`if ${excludedCondition}; then`, ...commands.map((command) => ` ${command}`), "fi"]
|
|
330
|
+
: commands;
|
|
331
|
+
return independent
|
|
332
|
+
? [
|
|
333
|
+
`if jq -e --arg unit "${unit}" '.units[] | select(.id == $unit)' dist/release-plan.json >/dev/null; then`,
|
|
334
|
+
...platformCommands.map((command) => ` ${command}`),
|
|
335
|
+
"fi",
|
|
336
|
+
]
|
|
337
|
+
: platformCommands;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function rustArtifactSteps(plan: RustReleasePlan): JobStep[] {
|
|
342
|
+
return [
|
|
343
|
+
...plan.bindings.flatMap((binding) => [
|
|
344
|
+
...(binding.node
|
|
345
|
+
? [
|
|
346
|
+
{
|
|
347
|
+
name: `Upload ${binding.crate} native npm package`,
|
|
348
|
+
uses: "actions/upload-artifact@v7",
|
|
349
|
+
with: {
|
|
350
|
+
name: `${binding.crate}-\${{ matrix.node }}-npm`,
|
|
351
|
+
path: `dist/release/${binding.crate}/\${{ matrix.node }}/npm/*.tgz`,
|
|
352
|
+
"retention-days": 7,
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
]
|
|
356
|
+
: []),
|
|
357
|
+
...(binding.python
|
|
358
|
+
? [
|
|
359
|
+
{
|
|
360
|
+
name: `Upload ${binding.crate} Python wheel`,
|
|
361
|
+
uses: "actions/upload-artifact@v7",
|
|
362
|
+
with: {
|
|
363
|
+
name: `${binding.pythonPackage}--\${{ matrix.python }}--python-wheel`,
|
|
364
|
+
path: `dist/release/${binding.crate}/\${{ matrix.node }}/python/*.whl`,
|
|
365
|
+
"retention-days": 7,
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
]
|
|
369
|
+
: []),
|
|
370
|
+
]),
|
|
371
|
+
...plan.releaseBinaries.map((pkg) => ({
|
|
372
|
+
name: `Upload ${pkg.crate} release binary`,
|
|
373
|
+
uses: "actions/upload-artifact@v7",
|
|
374
|
+
...(rustReleaseBinaryCondition(pkg.excludedOs)
|
|
375
|
+
? { if: rustReleaseBinaryCondition(pkg.excludedOs) }
|
|
376
|
+
: {}),
|
|
377
|
+
with: {
|
|
378
|
+
name: `${pkg.crate}-\${{ matrix.node }}-binary`,
|
|
379
|
+
path: `dist/release/${pkg.crate}/\${{ matrix.node }}/binary/*`,
|
|
380
|
+
"retention-days": 7,
|
|
381
|
+
},
|
|
382
|
+
})),
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly JobStep[]): Job {
|
|
387
|
+
const bindingCommands = rustBindingCommands(plan, Boolean(independentSetup));
|
|
388
|
+
const binaryCommands = rustBinaryCommands(plan, Boolean(independentSetup));
|
|
389
|
+
return {
|
|
390
|
+
if: independentSetup
|
|
391
|
+
? "${{ needs.release-plan.outputs.rust_targets != '[]' && (github.event_name == 'push' || inputs.stage != 'docs') }}"
|
|
392
|
+
: "${{ github.event_name == 'push' || inputs.stage == 'all' }}",
|
|
393
|
+
name: "${{ matrix.node }}",
|
|
394
|
+
needs: [independentSetup ? "release-plan" : "verify-context"],
|
|
395
|
+
runsOn: ["${{ matrix.runner }}"],
|
|
396
|
+
permissions: { contents: JobPermission.READ },
|
|
397
|
+
env: {
|
|
398
|
+
...RUST_CACHE_ENV,
|
|
399
|
+
...(independentSetup ? { BUN_VERSION } : {}),
|
|
400
|
+
},
|
|
401
|
+
strategy: {
|
|
402
|
+
failFast: false,
|
|
403
|
+
matrix: {
|
|
404
|
+
include: independentSetup
|
|
405
|
+
? ("${{ fromJSON(needs.release-plan.outputs.rust_targets) }}" as never)
|
|
406
|
+
: [...plan.targets],
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
steps: [
|
|
410
|
+
...(independentSetup ?? releaseSourceSteps()),
|
|
411
|
+
...(plan.hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
|
|
412
|
+
{
|
|
413
|
+
name: "Setup Rust",
|
|
414
|
+
...(plan.usePreinstalledWindowsRust ? { if: "${{ matrix.os != 'win32' }}" } : {}),
|
|
415
|
+
uses: `dtolnay/rust-toolchain@${plan.releaseRustVersion}`,
|
|
416
|
+
with: { targets: "${{ matrix.cargo }}" },
|
|
417
|
+
},
|
|
418
|
+
...(plan.usePreinstalledWindowsRust
|
|
419
|
+
? [
|
|
420
|
+
{
|
|
421
|
+
name: "Verify preinstalled Windows Rust",
|
|
422
|
+
if: "${{ matrix.os == 'win32' }}",
|
|
423
|
+
shell: "bash",
|
|
424
|
+
run: [
|
|
425
|
+
"rustc --version --verbose",
|
|
426
|
+
"cargo --version",
|
|
427
|
+
'rustup target list --installed | grep -Fx "${{ matrix.cargo }}"',
|
|
428
|
+
'test -f "$(rustc --print sysroot)/lib/rustlib/${{ matrix.cargo }}/bin/rust-lld.exe"',
|
|
429
|
+
].join("\n"),
|
|
430
|
+
},
|
|
431
|
+
]
|
|
432
|
+
: []),
|
|
433
|
+
{
|
|
434
|
+
name: "Install LLVM release tools",
|
|
435
|
+
shell: "bash",
|
|
436
|
+
run: "rustup component add llvm-tools-preview",
|
|
437
|
+
},
|
|
438
|
+
...rustCacheSteps(`release-\${{ matrix.cargo }}-rust-${plan.releaseRustVersion}`),
|
|
439
|
+
{
|
|
440
|
+
name: "Install Linux native dependencies",
|
|
441
|
+
if: "${{ matrix.os == 'linux' }}",
|
|
442
|
+
run: [
|
|
443
|
+
"sudo rm -f /etc/apt/sources.list.d/google-chrome.list",
|
|
444
|
+
"sudo apt-get update",
|
|
445
|
+
"sudo apt-get install --yes libdbus-1-dev pkg-config",
|
|
446
|
+
].join("\n"),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "Verify Rust build fingerprint",
|
|
450
|
+
if: independentSetup
|
|
451
|
+
? undefined
|
|
452
|
+
: "${{ github.event_name == 'push' || inputs.stage == 'all' }}",
|
|
453
|
+
...(!independentSetup ? { id: "rust-fingerprint" } : {}),
|
|
454
|
+
shell: "bash",
|
|
455
|
+
run: independentSetup
|
|
456
|
+
? [
|
|
457
|
+
`node ${plan.releaseHelper} fingerprint --check --root .`,
|
|
458
|
+
'--target "${{ matrix.cargo }}|${{ matrix.cargoExcludes }}"',
|
|
459
|
+
`--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
|
|
460
|
+
`--source ${JSON.stringify(plan.rustRoot)}`,
|
|
461
|
+
"--portable",
|
|
462
|
+
].join(" ")
|
|
463
|
+
: [
|
|
464
|
+
'RUNTIME_MANIFEST="dist/rust-raw/rust-build-${{ matrix.node }}.json"',
|
|
465
|
+
'mkdir -p "$(dirname "$RUNTIME_MANIFEST")"',
|
|
466
|
+
[
|
|
467
|
+
`node ${plan.releaseHelper} fingerprint --root .`,
|
|
468
|
+
'--output "$RUNTIME_MANIFEST"',
|
|
469
|
+
'--target "${{ matrix.cargo }}|${{ matrix.cargoExcludes }}"',
|
|
470
|
+
`--toolchain ${JSON.stringify(plan.releaseRustVersion)}`,
|
|
471
|
+
`--source ${JSON.stringify(plan.rustRoot)}`,
|
|
472
|
+
].join(" "),
|
|
473
|
+
'test "$(jq -r .rustSourceHash "$RUNTIME_MANIFEST")" = "$(jq -r .rustSourceHash .release/rust-build.json)"',
|
|
474
|
+
'KEY="$(jq -r --arg target "${{ matrix.cargo }}" \'.targets[$target] // empty\' "$RUNTIME_MANIFEST")"',
|
|
475
|
+
'test -n "$KEY"',
|
|
476
|
+
'echo "key=$KEY" >> "$GITHUB_OUTPUT"',
|
|
477
|
+
].join("\n"),
|
|
478
|
+
},
|
|
479
|
+
...(!independentSetup
|
|
480
|
+
? [
|
|
481
|
+
{
|
|
482
|
+
name: "Reuse matching raw Rust outputs",
|
|
483
|
+
id: "raw-native",
|
|
484
|
+
shell: "bash",
|
|
485
|
+
env: { GH_TOKEN: "${{ github.token }}" },
|
|
486
|
+
// prettier-ignore
|
|
487
|
+
run: stringUtils.dedent(
|
|
488
|
+
// ============================================================================
|
|
489
|
+
/*bash*/`
|
|
490
|
+
KEY="\${{ steps.rust-fingerprint.outputs.key }}"
|
|
491
|
+
test -n "$KEY"
|
|
492
|
+
ASSET="rust-raw-\${{ matrix.node }}-$KEY.tar.gz"
|
|
493
|
+
mkdir -p dist/rust-raw
|
|
494
|
+
MATCH="$(gh api --paginate "repos/\${{ github.repository }}/releases?per_page=100" --jq '.[] | select(.draft == false) | . as $release | .assets[] | select(.name == "'"$ASSET"'") | [$release.tag_name, .url] | @tsv' | head -n 1)"
|
|
495
|
+
if [ -n "$MATCH" ]; then
|
|
496
|
+
TAG="\${MATCH%%$'\\t'*}"
|
|
497
|
+
URL="\${MATCH#*$'\\t'}"
|
|
498
|
+
CHECKSUM_URL="$(gh api "repos/\${{ github.repository }}/releases/tags/$TAG" --jq '.assets[] | select(.name == "'"$ASSET.sha256"'") | .url')"
|
|
499
|
+
rm -rf "target/\${{ matrix.cargo }}/release"
|
|
500
|
+
if test -n "$CHECKSUM_URL" && gh api "$URL" -H "Accept: application/octet-stream" > "dist/rust-raw/$ASSET" && gh api "$CHECKSUM_URL" -H "Accept: application/octet-stream" > "dist/rust-raw/$ASSET.sha256" && (cd dist/rust-raw && if command -v sha256sum >/dev/null 2>&1; then sha256sum --check "$ASSET.sha256"; else shasum -a 256 --check "$ASSET.sha256"; fi) && tar -xzf "dist/rust-raw/$ASSET"; then
|
|
501
|
+
echo "hit=true" >> "$GITHUB_OUTPUT"
|
|
502
|
+
else
|
|
503
|
+
rm -rf "target/\${{ matrix.cargo }}/release" "dist/rust-raw/$ASSET" "dist/rust-raw/$ASSET.sha256"
|
|
504
|
+
echo "::warning::matching raw Rust bundle failed validation; rebuilding"
|
|
505
|
+
echo "hit=false" >> "$GITHUB_OUTPUT"
|
|
506
|
+
fi
|
|
507
|
+
else
|
|
508
|
+
echo "hit=false" >> "$GITHUB_OUTPUT"
|
|
509
|
+
fi
|
|
510
|
+
echo "asset=$ASSET" >> "$GITHUB_OUTPUT"
|
|
511
|
+
`
|
|
512
|
+
// ============================================================================
|
|
513
|
+
),
|
|
514
|
+
},
|
|
515
|
+
]
|
|
516
|
+
: []),
|
|
517
|
+
{
|
|
518
|
+
name: "Build Rust outputs",
|
|
519
|
+
...(independentSetup ? {} : { if: "${{ steps.raw-native.outputs.hit != 'true' }}" }),
|
|
520
|
+
shell: "bash",
|
|
521
|
+
env: {
|
|
522
|
+
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER:
|
|
523
|
+
"${{ matrix.os == 'win32' && 'rust-lld' || '' }}",
|
|
524
|
+
},
|
|
525
|
+
run: timedBash(
|
|
526
|
+
"rust_workspace",
|
|
527
|
+
independentSetup
|
|
528
|
+
? [
|
|
529
|
+
"mapfile -t PACKAGES < <(jq -r '.[]' <<<'${{ toJSON(matrix.packages) }}')",
|
|
530
|
+
"PACKAGE_ARGS=()",
|
|
531
|
+
'for PACKAGE in "${PACKAGES[@]}"; do PACKAGE_ARGS+=(--package "$PACKAGE"); done',
|
|
532
|
+
`cargo build --release --timings "\${PACKAGE_ARGS[@]}"${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"`,
|
|
533
|
+
].join("\n")
|
|
534
|
+
: `cargo build --release --timings --workspace${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"${
|
|
535
|
+
plan.hasReleaseExclusions ? " ${{ matrix.cargoExcludes }}" : ""
|
|
536
|
+
}`,
|
|
537
|
+
),
|
|
538
|
+
},
|
|
539
|
+
...(!independentSetup
|
|
540
|
+
? [
|
|
541
|
+
{
|
|
542
|
+
name: "Upload Cargo timings",
|
|
543
|
+
if: "${{ steps.raw-native.outputs.hit != 'true' }}",
|
|
544
|
+
uses: "actions/upload-artifact@v7",
|
|
545
|
+
with: {
|
|
546
|
+
name: "rust-${{ matrix.node }}-cargo-timings",
|
|
547
|
+
path: "target/cargo-timings/*",
|
|
548
|
+
"retention-days": 14,
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
]
|
|
552
|
+
: []),
|
|
553
|
+
...(!independentSetup
|
|
554
|
+
? [
|
|
555
|
+
{
|
|
556
|
+
name: "Archive raw Rust outputs",
|
|
557
|
+
if: "${{ steps.raw-native.outputs.hit != 'true' }}",
|
|
558
|
+
shell: "bash",
|
|
559
|
+
run: [
|
|
560
|
+
"mkdir -p dist/rust-raw",
|
|
561
|
+
'tar -czf "dist/rust-raw/${{ steps.raw-native.outputs.asset }}" "target/${{ matrix.cargo }}/release"',
|
|
562
|
+
'(cd dist/rust-raw && if command -v sha256sum >/dev/null 2>&1; then sha256sum "${{ steps.raw-native.outputs.asset }}"; else shasum -a 256 "${{ steps.raw-native.outputs.asset }}"; fi > "${{ steps.raw-native.outputs.asset }}.sha256")',
|
|
563
|
+
].join("\n"),
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: "Stamp native release version",
|
|
567
|
+
shell: "bash",
|
|
568
|
+
env: { VERSION: RELEASE_VERSION },
|
|
569
|
+
run: `node ${plan.releaseHelper} stamp-tree --root "target/\${{ matrix.cargo }}/release" --version "$VERSION"`,
|
|
570
|
+
},
|
|
571
|
+
]
|
|
572
|
+
: []),
|
|
573
|
+
...(bindingCommands.length
|
|
574
|
+
? [
|
|
575
|
+
{
|
|
576
|
+
name: "Package UniFFI outputs",
|
|
577
|
+
shell: "bash",
|
|
578
|
+
...(independentSetup ? {} : { env: { VERSION: RELEASE_VERSION } }),
|
|
579
|
+
run: timedBash("uniffi_packaging", bindingCommands.join("\n")),
|
|
580
|
+
},
|
|
581
|
+
]
|
|
582
|
+
: []),
|
|
583
|
+
...(binaryCommands.length
|
|
584
|
+
? [
|
|
585
|
+
{
|
|
586
|
+
name: "Package release binaries",
|
|
587
|
+
shell: "bash",
|
|
588
|
+
run: timedBash("binary_packaging", binaryCommands.join("\n")),
|
|
589
|
+
},
|
|
590
|
+
]
|
|
591
|
+
: []),
|
|
592
|
+
...(!independentSetup
|
|
593
|
+
? [
|
|
594
|
+
{
|
|
595
|
+
name: "Upload reusable raw Rust outputs",
|
|
596
|
+
uses: "actions/upload-artifact@v7",
|
|
597
|
+
with: {
|
|
598
|
+
name: "rust-${{ matrix.node }}-raw",
|
|
599
|
+
path: "dist/rust-raw/*",
|
|
600
|
+
"retention-days": 7,
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
]
|
|
604
|
+
: []),
|
|
605
|
+
...rustArtifactSteps(plan),
|
|
606
|
+
],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function rustCargoPublishJob(plan: RustReleasePlan, local: boolean): Job {
|
|
611
|
+
const registry = local ? '"${{ vars.LOCAL_CARGO_REGISTRY }}"' : "crates-io";
|
|
612
|
+
return {
|
|
613
|
+
if: local
|
|
614
|
+
? "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}"
|
|
615
|
+
: "${{ github.event_name == 'push' }}",
|
|
616
|
+
needs: ["verify-context", "rust-build"],
|
|
617
|
+
runsOn: [local ? "self-hosted" : "ubuntu-latest"],
|
|
618
|
+
permissions: { contents: JobPermission.READ },
|
|
619
|
+
steps: [
|
|
620
|
+
...releaseSourceSteps(),
|
|
621
|
+
{
|
|
622
|
+
name: "Setup Rust",
|
|
623
|
+
uses: `dtolnay/rust-toolchain@${plan.releaseRustVersion}`,
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: local ? "Publish Cargo crates locally" : "Publish public crates",
|
|
627
|
+
env: {
|
|
628
|
+
CARGO_REGISTRY_TOKEN: local
|
|
629
|
+
? "${{ secrets.LOCAL_CARGO_TOKEN }}"
|
|
630
|
+
: "${{ secrets.CARGO_REGISTRY_TOKEN }}",
|
|
631
|
+
},
|
|
632
|
+
run: plan.publicCrates
|
|
633
|
+
.map((crate) => `cargo publish --package "${crate}" --registry ${registry} --no-verify`)
|
|
634
|
+
.join("\n"),
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export function independentRustCargoPublishJob(
|
|
641
|
+
project: DBXToolsJavaScriptProject,
|
|
642
|
+
plan: RustReleasePlan,
|
|
643
|
+
): Job {
|
|
644
|
+
const commands = plan.publicCrates.map((crate) =>
|
|
645
|
+
[
|
|
646
|
+
`VERSION="$(jq -r --arg package "${crate}" '.rustPackages[] | select(.identity == $package) | .version' dist/release-plan.json)"`,
|
|
647
|
+
'if [ -n "$VERSION" ]; then',
|
|
648
|
+
` if cargo info "${crate}@$VERSION" >/dev/null 2>&1; then`,
|
|
649
|
+
` echo "skip published ${crate}@$VERSION"`,
|
|
650
|
+
" else",
|
|
651
|
+
` cargo publish --package "${crate}" --registry crates-io --no-verify`,
|
|
652
|
+
" fi",
|
|
653
|
+
"fi",
|
|
654
|
+
].join("\n"),
|
|
655
|
+
);
|
|
656
|
+
return {
|
|
657
|
+
if: plan.hasTargetOutputs
|
|
658
|
+
? "${{ always() && needs.release-plan.outputs.rust == 'true' && needs.rust-build.result != 'failure' && needs.rust-build.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'rust') }}"
|
|
659
|
+
: "${{ needs.release-plan.outputs.rust == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'rust') }}",
|
|
660
|
+
needs: ["release-plan", ...(plan.hasTargetOutputs ? ["rust-build"] : [])],
|
|
661
|
+
runsOn: ["ubuntu-latest"],
|
|
662
|
+
permissions: { contents: JobPermission.READ },
|
|
663
|
+
env: { BUN_VERSION },
|
|
664
|
+
steps: [
|
|
665
|
+
...independentReleaseSetupSteps(project),
|
|
666
|
+
{
|
|
667
|
+
name: "Setup Rust",
|
|
668
|
+
uses: `dtolnay/rust-toolchain@${plan.releaseRustVersion}`,
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: "Publish affected Cargo crates",
|
|
672
|
+
env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" },
|
|
673
|
+
run: commands.join("\n"),
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export function independentRustGitHubReleaseJob(
|
|
680
|
+
project: DBXToolsJavaScriptProject,
|
|
681
|
+
plan: RustReleasePlan,
|
|
682
|
+
): Job {
|
|
683
|
+
return {
|
|
684
|
+
if: "${{ needs.release-plan.outputs.github == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'github') }}",
|
|
685
|
+
needs: ["release-plan", "rust-build"],
|
|
686
|
+
runsOn: ["ubuntu-latest"],
|
|
687
|
+
permissions: { contents: JobPermission.WRITE },
|
|
688
|
+
env: { BUN_VERSION },
|
|
689
|
+
steps: [
|
|
690
|
+
...independentReleaseSetupSteps(project),
|
|
691
|
+
{
|
|
692
|
+
name: "Download release binaries",
|
|
693
|
+
uses: "actions/download-artifact@v8",
|
|
694
|
+
with: {
|
|
695
|
+
pattern: "*-binary",
|
|
696
|
+
path: "dist/rust-release",
|
|
697
|
+
"merge-multiple": true,
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
name: "Download reusable raw Rust outputs",
|
|
702
|
+
uses: "actions/download-artifact@v8",
|
|
703
|
+
with: {
|
|
704
|
+
pattern: "*-raw",
|
|
705
|
+
path: "dist/rust-raw",
|
|
706
|
+
"merge-multiple": true,
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
name: "Upload affected GitHub release assets",
|
|
711
|
+
env: { GH_TOKEN: "${{ github.token }}" },
|
|
712
|
+
shell: "bash",
|
|
713
|
+
run: plan.releaseBinaries
|
|
714
|
+
.map((binary) => {
|
|
715
|
+
const unit = defaultReleaseUnitId("rust", binary.crate);
|
|
716
|
+
return [
|
|
717
|
+
`VERSION="$(jq -r --arg unit "${unit}" '.units[] | select(.id == $unit) | .newVersion' dist/release-plan.json)"`,
|
|
718
|
+
'if [ -n "$VERSION" ]; then',
|
|
719
|
+
` gh release upload "${unit}-v$VERSION" dist/rust-release/${binary.binary}-* --clobber`,
|
|
720
|
+
"fi",
|
|
721
|
+
].join("\n");
|
|
722
|
+
})
|
|
723
|
+
.join("\n"),
|
|
724
|
+
},
|
|
725
|
+
],
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function rustGitHubReleaseJob(): Job {
|
|
730
|
+
return {
|
|
731
|
+
if: "${{ github.event_name == 'push' }}",
|
|
732
|
+
needs: ["verify-context", "rust-build"],
|
|
733
|
+
runsOn: ["ubuntu-latest"],
|
|
734
|
+
permissions: { contents: JobPermission.WRITE },
|
|
735
|
+
steps: [
|
|
736
|
+
...releaseSourceSteps(),
|
|
737
|
+
{
|
|
738
|
+
name: "Download release binaries",
|
|
739
|
+
uses: "actions/download-artifact@v8",
|
|
740
|
+
with: {
|
|
741
|
+
pattern: "*-binary",
|
|
742
|
+
path: "dist/rust-release",
|
|
743
|
+
"merge-multiple": true,
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "Download reusable raw Rust outputs",
|
|
748
|
+
uses: "actions/download-artifact@v8",
|
|
749
|
+
with: {
|
|
750
|
+
pattern: "*-raw",
|
|
751
|
+
path: "dist/rust-raw",
|
|
752
|
+
"merge-multiple": true,
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: "Publish GitHub release assets",
|
|
757
|
+
uses: "softprops/action-gh-release@v2",
|
|
758
|
+
with: {
|
|
759
|
+
files: ["dist/rust-release/*", "dist/rust-raw/*"].join("\n"),
|
|
760
|
+
body_path: RELEASE_SUMMARY_FILE,
|
|
761
|
+
generate_release_notes: true,
|
|
762
|
+
tag_name: RELEASE_TAG,
|
|
763
|
+
target_commitish: RELEASE_SHA,
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
name: "Delete superseded raw Rust release assets",
|
|
768
|
+
uses: "actions/github-script@v8",
|
|
769
|
+
env: { CURRENT_RELEASE_TAG: RELEASE_TAG },
|
|
770
|
+
with: {
|
|
771
|
+
// prettier-ignore
|
|
772
|
+
script: stringUtils.dedent(
|
|
773
|
+
// ============================================================================
|
|
774
|
+
/*js*/`
|
|
775
|
+
const currentTag = process.env.CURRENT_RELEASE_TAG;
|
|
776
|
+
if (!currentTag) throw new Error("CURRENT_RELEASE_TAG is required");
|
|
777
|
+
const parseVersion = (tag) => {
|
|
778
|
+
const match = /^v(\\d+)\\.(\\d+)\\.(\\d+)$/.exec(tag);
|
|
779
|
+
return match ? match.slice(1).map(Number) : undefined;
|
|
780
|
+
};
|
|
781
|
+
const compareVersions = (left, right) => {
|
|
782
|
+
for (let index = 0; index < 3; index += 1) {
|
|
783
|
+
if (left[index] !== right[index]) return left[index] - right[index];
|
|
784
|
+
}
|
|
785
|
+
return 0;
|
|
786
|
+
};
|
|
787
|
+
const currentVersion = parseVersion(currentTag);
|
|
788
|
+
if (!currentVersion) throw new Error(\`Unsupported release tag: \${currentTag}\`);
|
|
789
|
+
const releases = await github.paginate(github.rest.repos.listReleases, {
|
|
790
|
+
...context.repo,
|
|
791
|
+
per_page: 100,
|
|
792
|
+
});
|
|
793
|
+
const assets = releases.flatMap((release) => {
|
|
794
|
+
const version = parseVersion(release.tag_name);
|
|
795
|
+
if (release.draft || !version || compareVersions(version, currentVersion) >= 0) {
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
return release.assets.filter((asset) => /^rust-(?:raw|build)-/.test(asset.name));
|
|
799
|
+
});
|
|
800
|
+
for (const asset of assets) {
|
|
801
|
+
core.info(\`Deleting \${asset.name} (\${asset.id})\`);
|
|
802
|
+
await github.rest.repos.deleteReleaseAsset({
|
|
803
|
+
...context.repo,
|
|
804
|
+
asset_id: asset.id,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
core.info(\`Deleted \${assets.length} superseded raw Rust release assets\`);
|
|
808
|
+
`
|
|
809
|
+
// ============================================================================
|
|
810
|
+
),
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
],
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export function independentRustNativeNpmPublishJob(project: DBXToolsJavaScriptProject): Job {
|
|
818
|
+
return {
|
|
819
|
+
if: "${{ needs.release-plan.outputs.node == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
|
|
820
|
+
needs: ["release-plan", "rust-build"],
|
|
821
|
+
runsOn: ["ubuntu-latest"],
|
|
822
|
+
permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
|
|
823
|
+
timeoutMinutes: 15,
|
|
824
|
+
env: { BUN_VERSION, CI: "true" },
|
|
825
|
+
steps: [
|
|
826
|
+
...independentReleaseSetupSteps(project),
|
|
827
|
+
{
|
|
828
|
+
name: "Download native npm packages",
|
|
829
|
+
uses: "actions/download-artifact@v8",
|
|
830
|
+
with: {
|
|
831
|
+
pattern: "*-npm",
|
|
832
|
+
path: "dist/uniffi/native",
|
|
833
|
+
"merge-multiple": true,
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: "Publish affected native npm packages",
|
|
838
|
+
env: npmPublishEnvironment(),
|
|
839
|
+
run: "bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory dist/uniffi/native",
|
|
840
|
+
},
|
|
841
|
+
],
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export function independentRustNodeFacadePublishJob(
|
|
846
|
+
project: DBXToolsJavaScriptProject,
|
|
847
|
+
bindings: readonly RustBindingMapping[],
|
|
848
|
+
): Job {
|
|
849
|
+
return {
|
|
850
|
+
if: "${{ needs.release-plan.outputs.node == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
|
|
851
|
+
needs: ["release-plan", "publish-node", "publish-native-npm"],
|
|
852
|
+
runsOn: ["ubuntu-latest"],
|
|
853
|
+
permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
|
|
854
|
+
timeoutMinutes: 30,
|
|
855
|
+
env: { BUN_VERSION, CI: "true" },
|
|
856
|
+
steps: [
|
|
857
|
+
...independentReleaseSetupSteps(project),
|
|
858
|
+
{
|
|
859
|
+
name: "Build and publish affected UniFFI npm facades",
|
|
860
|
+
env: npmPublishEnvironment(),
|
|
861
|
+
shell: "bash",
|
|
862
|
+
run: bindings
|
|
863
|
+
.flatMap((binding) => {
|
|
864
|
+
if (!binding.node || !binding.nodePackage) return [];
|
|
865
|
+
const unit = defaultReleaseUnitId("rust", binding.crate);
|
|
866
|
+
const output = `dist/uniffi/facades/${binding.crate}`;
|
|
867
|
+
return [
|
|
868
|
+
`VERSION="$(jq -r --arg unit "${unit}" '.units[] | select(.id == $unit) | .newVersion' dist/release-plan.json)"`,
|
|
869
|
+
'if [ -n "$VERSION" ]; then',
|
|
870
|
+
` node .projen/uniffi-release.mjs facade --node "${binding.node}" --node-package "${binding.nodePackage}" --node-triple "linux-x64-gnu" --version "$VERSION" --output "${output}"`,
|
|
871
|
+
` bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory "${output}/npm-facade" --version "$VERSION"`,
|
|
872
|
+
"fi",
|
|
873
|
+
];
|
|
874
|
+
})
|
|
875
|
+
.join("\n"),
|
|
876
|
+
},
|
|
877
|
+
],
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export function rustNativeNpmPublishJob(project: DBXToolsJavaScriptProject): Job {
|
|
882
|
+
return {
|
|
883
|
+
if: "${{ always() && needs.verify-context.result == 'success' && needs.rust-build.result != 'failure' && needs.rust-build.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'node') }}",
|
|
884
|
+
needs: ["verify-context", "rust-build"],
|
|
885
|
+
runsOn: ["ubuntu-latest"],
|
|
886
|
+
permissions: {
|
|
887
|
+
actions: JobPermission.READ,
|
|
888
|
+
contents: JobPermission.READ,
|
|
889
|
+
idToken: JobPermission.WRITE,
|
|
890
|
+
},
|
|
891
|
+
timeoutMinutes: 15,
|
|
892
|
+
env: { BUN_VERSION, CI: "true" },
|
|
893
|
+
steps: [
|
|
894
|
+
...nodeReleaseSetupSteps(project),
|
|
895
|
+
...releaseArtifactSteps({
|
|
896
|
+
currentName: "Download native npm packages",
|
|
897
|
+
recoveredName: "Download recovered native npm packages",
|
|
898
|
+
pattern: "*-npm",
|
|
899
|
+
path: "dist/uniffi/native",
|
|
900
|
+
}),
|
|
901
|
+
{
|
|
902
|
+
name: "Publish native npm packages",
|
|
903
|
+
env: { RELEASE_VERSION, ...npmPublishEnvironment() },
|
|
904
|
+
run: 'bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory dist/uniffi/native --version "$RELEASE_VERSION" $DRY_RUN',
|
|
905
|
+
},
|
|
906
|
+
],
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export function rustNodeFacadePublishJob(
|
|
911
|
+
project: DBXToolsJavaScriptProject,
|
|
912
|
+
bindings: readonly RustReleaseBinding[],
|
|
913
|
+
): Job {
|
|
914
|
+
return {
|
|
915
|
+
if: releaseStageCondition("node"),
|
|
916
|
+
needs: ["verify-context", "publish-node"],
|
|
917
|
+
runsOn: ["ubuntu-latest"],
|
|
918
|
+
permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
|
|
919
|
+
timeoutMinutes: 30,
|
|
920
|
+
env: { BUN_VERSION, CI: "true" },
|
|
921
|
+
steps: [
|
|
922
|
+
...nodeReleaseSetupSteps(project),
|
|
923
|
+
{
|
|
924
|
+
name: "Build and publish UniFFI npm facades",
|
|
925
|
+
env: { RELEASE_VERSION, ...npmPublishEnvironment() },
|
|
926
|
+
run: bindings
|
|
927
|
+
.flatMap((binding) => {
|
|
928
|
+
const output = `dist/uniffi/facades/${binding.crate}`;
|
|
929
|
+
return [
|
|
930
|
+
`node .projen/uniffi-release.mjs facade --node "${binding.node}" --node-package "${binding.nodePackage}" --node-triple "linux-x64-gnu" --version "$RELEASE_VERSION" --output "${output}"`,
|
|
931
|
+
`bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory "${output}/npm-facade" --version "$RELEASE_VERSION" $DRY_RUN`,
|
|
932
|
+
];
|
|
933
|
+
})
|
|
934
|
+
.join("\n"),
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
name: "Smoke test published UniFFI npm facades",
|
|
938
|
+
if: "${{ github.event_name == 'push' && vars.UNIFFI_FACADE_SMOKE == 'true' }}",
|
|
939
|
+
continueOnError: true,
|
|
940
|
+
env: { RELEASE_VERSION },
|
|
941
|
+
run: [
|
|
942
|
+
'SMOKE_DIR="$(mktemp -d)"',
|
|
943
|
+
"trap 'rm -rf \"$SMOKE_DIR\"' EXIT",
|
|
944
|
+
'cd "$SMOKE_DIR"',
|
|
945
|
+
"npm init --yes >/dev/null",
|
|
946
|
+
...bindings.flatMap((binding) => [
|
|
947
|
+
`npm install --ignore-scripts --no-audit --no-fund --package-lock=false "${binding.nodePackage}@$RELEASE_VERSION"`,
|
|
948
|
+
`node -e 'import("${binding.nodePackage}")'`,
|
|
949
|
+
]),
|
|
950
|
+
].join("\n"),
|
|
951
|
+
},
|
|
952
|
+
],
|
|
953
|
+
};
|
|
954
|
+
}
|