@dbx-tools/projen 0.6.194 → 0.6.196
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 +58 -42
- package/index.ts +2 -2
- package/package.json +4 -4
- package/src/pnpm-workspace.ts +4 -2
- package/src/project-js.ts +43 -12
- package/src/project-py.ts +7 -6
- package/src/project-rs.ts +99 -130
- package/src/release.ts +72 -22
- package/src/workspace-version.ts +57 -7
- package/tasks/bump.ts +19 -444
- package/tasks/clean.ts +1 -1
- package/tasks/local-publish.ts +166 -0
- package/tasks/publish-uniffi-local.ts +30 -8
- package/tasks/publish.ts +22 -59
- package/tasks/release-pr.ts +259 -0
- package/tasks/version-check.ts +85 -0
package/src/project-rs.ts
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { project as coreProject } from "@dbx-tools/core";
|
|
5
|
+
import { exec, project as coreProject } from "@dbx-tools/core";
|
|
6
6
|
import { string } from "@dbx-tools/shared-core";
|
|
7
|
-
import { Project, TextFile, javascript } from "projen";
|
|
8
|
-
import { GithubWorkflow } from "projen/lib/github";
|
|
7
|
+
import { Component, Project, TextFile, javascript } from "projen";
|
|
9
8
|
import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
|
|
10
9
|
import { BUN_VERSION } from "./bun-workflow.ts";
|
|
11
10
|
import { DBXToolsTypeScriptProject, projectRepositoryUrl } from "./project-js.ts";
|
|
@@ -84,7 +83,7 @@ export interface DBXToolsRustWorkspaceOptions {
|
|
|
84
83
|
readonly pythonRoot?: string;
|
|
85
84
|
readonly pythonModulePrefix?: string;
|
|
86
85
|
readonly private?: boolean;
|
|
87
|
-
/** Generate
|
|
86
|
+
/** Generate release-branch-driven cross-platform UniFFI package releases. */
|
|
88
87
|
readonly release?: boolean;
|
|
89
88
|
/** Native release targets; defaults to the maintained GitHub-hosted matrix. */
|
|
90
89
|
readonly releaseTargets?: readonly UniFFIReleaseTarget[];
|
|
@@ -174,30 +173,35 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
|
|
|
174
173
|
},
|
|
175
174
|
] as const;
|
|
176
175
|
|
|
176
|
+
const UBRN_VERSION = "0.31.0-5";
|
|
177
|
+
const RELEASE_PLATFORMS_ENV = "DBX_TOOLS_RELEASE_PLATFORMS";
|
|
177
178
|
const RUST_CACHE_ENV = {
|
|
178
179
|
CARGO_INCREMENTAL: "0",
|
|
179
180
|
CARGO_TERM_COLOR: "always",
|
|
180
181
|
} as const;
|
|
181
|
-
const UBRN_VERSION = "0.31.0-5";
|
|
182
|
-
const RELEASE_PLATFORMS_ENV = "DBX_TOOLS_RELEASE_PLATFORMS";
|
|
183
182
|
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
function cargoCacheKeyStep(): Record<string, unknown> {
|
|
184
|
+
return {
|
|
185
|
+
name: "Resolve Cargo dependency cache key",
|
|
186
|
+
id: "cargo_cache_key",
|
|
187
|
+
run: "node .projen/cargo-cache-key.mjs",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function rustCacheSteps(sharedKey: string): readonly Record<string, unknown>[] {
|
|
189
192
|
return [
|
|
190
193
|
{
|
|
191
|
-
name: "Cache Cargo registry and
|
|
192
|
-
id:
|
|
194
|
+
name: "Cache Cargo registry and dependencies",
|
|
195
|
+
id: "cargo_cache",
|
|
193
196
|
uses: "Swatinem/rust-cache@v2.9.2",
|
|
194
197
|
with: {
|
|
195
198
|
"cache-targets": true,
|
|
196
199
|
"cache-workspace-crates": false,
|
|
197
200
|
"add-job-id-key": false,
|
|
198
|
-
"add-rust-environment-hash-key":
|
|
201
|
+
"add-rust-environment-hash-key": false,
|
|
202
|
+
key: "${{ steps.cargo_cache_key.outputs.key }}",
|
|
199
203
|
"shared-key": sharedKey,
|
|
200
|
-
"save-if":
|
|
204
|
+
"save-if": "${{ github.event_name == 'push' }}",
|
|
201
205
|
},
|
|
202
206
|
},
|
|
203
207
|
];
|
|
@@ -273,6 +277,79 @@ function uniffiReleaseTaskSource(): string {
|
|
|
273
277
|
return readFileSync(source, "utf8");
|
|
274
278
|
}
|
|
275
279
|
|
|
280
|
+
function cargoCacheKeySource(): string {
|
|
281
|
+
return `#!/usr/bin/env node
|
|
282
|
+
import { createHash } from "node:crypto";
|
|
283
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
284
|
+
import { spawnSync } from "node:child_process";
|
|
285
|
+
|
|
286
|
+
function command(command, args) {
|
|
287
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
288
|
+
if (result.error) throw result.error;
|
|
289
|
+
if (result.status !== 0) throw new Error(\`\${command} exited with \${result.status}\`);
|
|
290
|
+
return result.stdout;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const hasLock = existsSync("Cargo.lock");
|
|
294
|
+
const metadata = JSON.parse(
|
|
295
|
+
command("cargo", [
|
|
296
|
+
"metadata",
|
|
297
|
+
...(hasLock ? ["--locked"] : []),
|
|
298
|
+
"--format-version",
|
|
299
|
+
"1",
|
|
300
|
+
"--no-deps",
|
|
301
|
+
]),
|
|
302
|
+
);
|
|
303
|
+
const workspaceNames = new Set(metadata.packages.map((pkg) => pkg.name));
|
|
304
|
+
const dependencyLock = hasLock
|
|
305
|
+
? readFileSync("Cargo.lock", "utf8")
|
|
306
|
+
.split("[[package]]")
|
|
307
|
+
.slice(1)
|
|
308
|
+
.filter((block) => {
|
|
309
|
+
const name = /^\\s*name = "([^"]+)"/m.exec(block)?.[1];
|
|
310
|
+
return name && !workspaceNames.has(name);
|
|
311
|
+
})
|
|
312
|
+
.map((block) => \`[[package]]\${block}\`)
|
|
313
|
+
.join("")
|
|
314
|
+
: "";
|
|
315
|
+
const manifests = [
|
|
316
|
+
metadata.workspace_root + "/Cargo.toml",
|
|
317
|
+
...metadata.packages.map((pkg) => pkg.manifest_path),
|
|
318
|
+
]
|
|
319
|
+
.map((path) =>
|
|
320
|
+
readFileSync(path, "utf8").replace(/^version = "[0-9]+\\.[0-9]+\\.[0-9]+"\\s*$/gm, ""),
|
|
321
|
+
)
|
|
322
|
+
.join("\\n");
|
|
323
|
+
const config = existsSync(".cargo/config.toml")
|
|
324
|
+
? readFileSync(".cargo/config.toml", "utf8")
|
|
325
|
+
: "";
|
|
326
|
+
const key = createHash("sha256")
|
|
327
|
+
.update(dependencyLock)
|
|
328
|
+
.update(manifests)
|
|
329
|
+
.update(config)
|
|
330
|
+
.update(command("rustc", ["-vV"]))
|
|
331
|
+
.digest("hex");
|
|
332
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
333
|
+
appendFileSync(process.env.GITHUB_OUTPUT, \`key=\${key}\\n\`);
|
|
334
|
+
} else {
|
|
335
|
+
process.stdout.write(\`\${key}\\n\`);
|
|
336
|
+
}
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Keep tracked workspace package versions in Cargo.lock aligned with VERSION. */
|
|
341
|
+
class RustWorkspaceVersionLock extends Component {
|
|
342
|
+
public override postSynthesize(): void {
|
|
343
|
+
exec.spawnSync("cargo", ["metadata", "--format-version", "1", "--no-deps"], {
|
|
344
|
+
cwd: this.project.outdir,
|
|
345
|
+
stdout: "ignore",
|
|
346
|
+
stderr: "inherit",
|
|
347
|
+
stdin: "ignore",
|
|
348
|
+
check: true,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
276
353
|
/** Persisted mapping consumed by the focused Rust source watcher. */
|
|
277
354
|
export interface RustBindingMapping {
|
|
278
355
|
readonly crate: string;
|
|
@@ -829,125 +906,24 @@ export class DBXToolsRustWorkspace {
|
|
|
829
906
|
.trimEnd()
|
|
830
907
|
.split("\n"),
|
|
831
908
|
});
|
|
909
|
+
new RustWorkspaceVersionLock(project);
|
|
832
910
|
project.addTask("rs:format", { exec: "cargo fmt --all" });
|
|
833
911
|
project.addTask("rs:lint", { exec: "cargo clippy --workspace --all-targets --all-features" });
|
|
834
912
|
project.addTask("rs:test", { exec: "cargo test --workspace" });
|
|
835
913
|
project.addTask("rs:build", { exec: "cargo build --workspace" });
|
|
836
|
-
|
|
914
|
+
project.addTask("rs:bindings", {
|
|
837
915
|
exec: "bun node_modules/@dbx-tools/projen/tasks/rust.ts",
|
|
838
916
|
description: "Generate language bindings for UniFFI-enabled Rust crates",
|
|
839
917
|
});
|
|
840
|
-
if (this.bindingMappings.some((binding) => binding.node)) {
|
|
841
|
-
project.tasks.tryFind("pre-compile")?.spawn(bindingsTask);
|
|
842
|
-
}
|
|
843
918
|
project.removeTask("rs:bindings:demo");
|
|
844
919
|
if (releaseEnabled) {
|
|
845
|
-
|
|
920
|
+
new TextFile(project, ".projen/cargo-cache-key.mjs", {
|
|
921
|
+
lines: cargoCacheKeySource().trimEnd().split("\n"),
|
|
922
|
+
});
|
|
846
923
|
this.addReleaseWorkflow(project, options, nativeTargets);
|
|
847
924
|
}
|
|
848
925
|
}
|
|
849
926
|
|
|
850
|
-
private addCacheWorkflow(
|
|
851
|
-
project: javascript.NodeProject,
|
|
852
|
-
options: DBXToolsRustWorkspaceOptions,
|
|
853
|
-
targets: readonly UniFFIReleaseTarget[],
|
|
854
|
-
): void {
|
|
855
|
-
if (!project.github || !isDBXToolsJavaScriptProject()(project) || targets.length === 0) return;
|
|
856
|
-
const releaseRustVersion = options.releaseRustVersion ?? "stable";
|
|
857
|
-
const usePreinstalledWindowsRust = releaseRustVersion === "stable";
|
|
858
|
-
const usesCargoLock = existsSync(join(project.outdir, "Cargo.lock"));
|
|
859
|
-
const hasReleaseExclusions = this.packages.some(
|
|
860
|
-
(pkg) => pkg.packageOptions.releaseExcludeOs?.length,
|
|
861
|
-
);
|
|
862
|
-
const matrix = targets.map((target) => ({
|
|
863
|
-
...target,
|
|
864
|
-
...(hasReleaseExclusions
|
|
865
|
-
? {
|
|
866
|
-
cargoExcludes: this.packages
|
|
867
|
-
.filter((pkg) => pkg.packageOptions.releaseExcludeOs?.includes(target.os))
|
|
868
|
-
.map((pkg) => `--exclude ${pkg.crateName}`)
|
|
869
|
-
.join(" "),
|
|
870
|
-
}
|
|
871
|
-
: {}),
|
|
872
|
-
}));
|
|
873
|
-
const workflow = new GithubWorkflow(project.github, "rust-cache", {
|
|
874
|
-
fileName: "rust-cache.yml",
|
|
875
|
-
limitConcurrency: true,
|
|
876
|
-
concurrencyOptions: { group: "rust-cache", cancelInProgress: true },
|
|
877
|
-
});
|
|
878
|
-
workflow.on({
|
|
879
|
-
push: {
|
|
880
|
-
branches: ["main"],
|
|
881
|
-
paths: [
|
|
882
|
-
".cargo/**",
|
|
883
|
-
".github/workflows/rust-cache.yml",
|
|
884
|
-
"Cargo.lock",
|
|
885
|
-
"Cargo.toml",
|
|
886
|
-
"packages/rs/**",
|
|
887
|
-
],
|
|
888
|
-
},
|
|
889
|
-
workflowDispatch: {},
|
|
890
|
-
});
|
|
891
|
-
workflow.addJob("prime", {
|
|
892
|
-
name: "${{ matrix.node }}",
|
|
893
|
-
runsOn: ["${{ matrix.runner }}"],
|
|
894
|
-
permissions: { contents: JobPermission.READ },
|
|
895
|
-
env: RUST_CACHE_ENV,
|
|
896
|
-
strategy: {
|
|
897
|
-
failFast: false,
|
|
898
|
-
matrix: { include: matrix },
|
|
899
|
-
},
|
|
900
|
-
steps: [
|
|
901
|
-
{ name: "Checkout", uses: "actions/checkout@v6" },
|
|
902
|
-
{
|
|
903
|
-
name: "Setup Rust",
|
|
904
|
-
...(usePreinstalledWindowsRust ? { if: "${{ matrix.os != 'win32' }}" } : {}),
|
|
905
|
-
uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
|
|
906
|
-
with: { targets: "${{ matrix.cargo }}" },
|
|
907
|
-
},
|
|
908
|
-
...(usePreinstalledWindowsRust
|
|
909
|
-
? [
|
|
910
|
-
{
|
|
911
|
-
name: "Verify preinstalled Windows Rust",
|
|
912
|
-
if: "${{ matrix.os == 'win32' }}",
|
|
913
|
-
shell: "bash",
|
|
914
|
-
run: [
|
|
915
|
-
"rustc --version --verbose",
|
|
916
|
-
"cargo --version",
|
|
917
|
-
'rustup target list --installed | grep -Fx "${{ matrix.cargo }}"',
|
|
918
|
-
'test -f "$(rustc --print sysroot)/lib/rustlib/${{ matrix.cargo }}/bin/rust-lld.exe"',
|
|
919
|
-
].join("\n"),
|
|
920
|
-
},
|
|
921
|
-
]
|
|
922
|
-
: []),
|
|
923
|
-
...rustCacheSteps(`release-\${{ matrix.cargo }}-rust-${releaseRustVersion}`),
|
|
924
|
-
{
|
|
925
|
-
name: "Install Linux native dependencies",
|
|
926
|
-
if: "${{ matrix.os == 'linux' }}",
|
|
927
|
-
run: [
|
|
928
|
-
"sudo rm -f /etc/apt/sources.list.d/google-chrome.list",
|
|
929
|
-
"sudo apt-get update",
|
|
930
|
-
"sudo apt-get install --yes libdbus-1-dev pkg-config",
|
|
931
|
-
].join("\n"),
|
|
932
|
-
},
|
|
933
|
-
{
|
|
934
|
-
name: "Prime Cargo target cache",
|
|
935
|
-
shell: "bash",
|
|
936
|
-
env: {
|
|
937
|
-
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER:
|
|
938
|
-
"${{ matrix.os == 'win32' && 'rust-lld' || '' }}",
|
|
939
|
-
},
|
|
940
|
-
run: timedBash(
|
|
941
|
-
"rust_cache",
|
|
942
|
-
`cargo build --release --workspace${usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"${
|
|
943
|
-
hasReleaseExclusions ? " ${{ matrix.cargoExcludes }}" : ""
|
|
944
|
-
}`,
|
|
945
|
-
),
|
|
946
|
-
},
|
|
947
|
-
],
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
|
|
951
927
|
private addReleaseWorkflow(
|
|
952
928
|
project: javascript.NodeProject,
|
|
953
929
|
options: DBXToolsRustWorkspaceOptions,
|
|
@@ -1142,15 +1118,8 @@ export class DBXToolsRustWorkspace {
|
|
|
1142
1118
|
},
|
|
1143
1119
|
]
|
|
1144
1120
|
: []),
|
|
1145
|
-
|
|
1146
|
-
{
|
|
1147
|
-
name: "Log cache configuration",
|
|
1148
|
-
shell: "bash",
|
|
1149
|
-
run: [
|
|
1150
|
-
`echo "cargo_cache_namespace=release-\${{ matrix.cargo }}-rust-${releaseRustVersion}"`,
|
|
1151
|
-
'echo "cargo_cache_hit=${{ steps.cargo_cache.outputs.cache-hit }}"',
|
|
1152
|
-
].join("\n"),
|
|
1153
|
-
},
|
|
1121
|
+
cargoCacheKeyStep(),
|
|
1122
|
+
...rustCacheSteps(`release-\${{ matrix.cargo }}-rust-${releaseRustVersion}`),
|
|
1154
1123
|
{
|
|
1155
1124
|
name: "Install Linux native dependencies",
|
|
1156
1125
|
if: "${{ matrix.os == 'linux' }}",
|
package/src/release.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/** Unified
|
|
1
|
+
/** Unified default-branch release workflow generation. */
|
|
2
2
|
import { Component } from "projen";
|
|
3
3
|
import { GithubWorkflow } from "projen/lib/github";
|
|
4
4
|
import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
|
|
5
5
|
import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
|
|
6
|
-
import type
|
|
6
|
+
import { projectReleaseBranch, type DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
7
7
|
import { applyTasks, taskScript } from "./project.ts";
|
|
8
8
|
import { RELEASE_VERSION, releaseSourceSteps } from "./release-dispatch.ts";
|
|
9
9
|
|
|
@@ -44,14 +44,14 @@ export function hasNodeRelease(project: DBXToolsJavaScriptProject): boolean {
|
|
|
44
44
|
return nodeReleaseProjects.has(project);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** Tag pattern
|
|
47
|
+
/** Tag pattern created by release jobs and accepted by manual recovery. */
|
|
48
48
|
export function releaseTagPattern(project: DBXToolsJavaScriptProject): string {
|
|
49
49
|
const prefix = releaseTagPrefixes.get(project);
|
|
50
50
|
if (!prefix) throw new Error("Release workflow is not configured");
|
|
51
51
|
return `${prefix}*`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/** Run a release stage on
|
|
54
|
+
/** Run a release stage on default-branch pushes or when selected for manual recovery. */
|
|
55
55
|
export function releaseStageCondition(stage: Exclude<ReleaseStage, "all">): string {
|
|
56
56
|
return `\${{ github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == '${stage}' }}`;
|
|
57
57
|
}
|
|
@@ -121,10 +121,10 @@ export function npmPublishEnvironment(): Record<string, string> {
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function verifyContextJob(tagPrefix: string): Job {
|
|
124
|
+
function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
125
125
|
return {
|
|
126
126
|
runsOn: ["ubuntu-latest"],
|
|
127
|
-
permissions: { actions: JobPermission.READ, contents: JobPermission.
|
|
127
|
+
permissions: { actions: JobPermission.READ, contents: JobPermission.WRITE },
|
|
128
128
|
outputs: {
|
|
129
129
|
release_tag: { stepId: "release", outputName: "release_tag" },
|
|
130
130
|
expected_sha: { stepId: "release", outputName: "expected_sha" },
|
|
@@ -132,11 +132,11 @@ function verifyContextJob(tagPrefix: string): Job {
|
|
|
132
132
|
},
|
|
133
133
|
steps: [
|
|
134
134
|
{
|
|
135
|
-
name: "Checkout release
|
|
135
|
+
name: "Checkout release source",
|
|
136
136
|
uses: "actions/checkout@v6",
|
|
137
137
|
with: {
|
|
138
|
-
ref: "${{ github.event_name == 'push' && github.
|
|
139
|
-
"fetch-depth":
|
|
138
|
+
ref: "${{ github.event_name == 'push' && github.sha || inputs.expected_sha }}",
|
|
139
|
+
"fetch-depth": 2,
|
|
140
140
|
},
|
|
141
141
|
},
|
|
142
142
|
{
|
|
@@ -145,18 +145,50 @@ function verifyContextJob(tagPrefix: string): Job {
|
|
|
145
145
|
shell: "bash",
|
|
146
146
|
env: {
|
|
147
147
|
RELEASE_TAG:
|
|
148
|
-
"${{ github.event_name == '
|
|
148
|
+
"${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || '' }}",
|
|
149
149
|
EXPECTED_SHA:
|
|
150
150
|
"${{ github.event_name == 'workflow_dispatch' && inputs.expected_sha || '' }}",
|
|
151
151
|
DRY_RUN: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}",
|
|
152
152
|
},
|
|
153
153
|
run: [
|
|
154
|
-
|
|
155
|
-
'
|
|
156
|
-
|
|
157
|
-
'RELEASE_SHA="$
|
|
158
|
-
'test "$(git rev-parse HEAD)" = "$RELEASE_SHA"',
|
|
159
|
-
|
|
154
|
+
'if [ "$GITHUB_EVENT_NAME" = "push" ]; then',
|
|
155
|
+
' test "$GITHUB_REF_TYPE" = "branch"',
|
|
156
|
+
` test "$GITHUB_REF_NAME" = "${releaseBranch}"`,
|
|
157
|
+
' RELEASE_SHA="$GITHUB_SHA"',
|
|
158
|
+
' test "$(git rev-parse HEAD)" = "$RELEASE_SHA"',
|
|
159
|
+
" RELEASE_VERSION=\"$(tr -d '\\r\\n' < VERSION)\"",
|
|
160
|
+
' [[ "$RELEASE_VERSION" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+$ ]]',
|
|
161
|
+
' PREVIOUS_VERSION="$(git show "$RELEASE_SHA^:VERSION" | tr -d \'\\r\\n\')"',
|
|
162
|
+
' test "$PREVIOUS_VERSION" != "$RELEASE_VERSION"',
|
|
163
|
+
` RELEASE_TAG="${tagPrefix}$RELEASE_VERSION"`,
|
|
164
|
+
' if git ls-remote --exit-code --tags origin "refs/tags/$RELEASE_TAG" >/dev/null 2>&1; then',
|
|
165
|
+
' git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"',
|
|
166
|
+
' test "$(git cat-file -t "$RELEASE_TAG")" = "tag"',
|
|
167
|
+
' test "$(git rev-parse "$RELEASE_TAG^{commit}")" = "$RELEASE_SHA"',
|
|
168
|
+
" else",
|
|
169
|
+
" git fetch --force --tags origin",
|
|
170
|
+
' LATEST_VERSION=""',
|
|
171
|
+
` for TAG in $(git tag --sort=-version:refname --list '${tagPrefix}*'); do`,
|
|
172
|
+
` CANDIDATE="\${TAG#${tagPrefix}}"`,
|
|
173
|
+
' if [[ "$CANDIDATE" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then',
|
|
174
|
+
' LATEST_VERSION="$CANDIDATE"',
|
|
175
|
+
" break",
|
|
176
|
+
" fi",
|
|
177
|
+
" done",
|
|
178
|
+
' if [ -n "$LATEST_VERSION" ]; then',
|
|
179
|
+
' node -e \'const a=process.argv[1].split(".").map(Number); const b=process.argv[2].split(".").map(Number); if (!(b[0]>a[0] || b[0]===a[0] && (b[1]>a[1] || b[1]===a[1] && b[2]>a[2]))) process.exit(1)\' "$LATEST_VERSION" "$RELEASE_VERSION"',
|
|
180
|
+
" fi",
|
|
181
|
+
' git config user.name "github-actions[bot]"',
|
|
182
|
+
' git config user.email "41898282+github-actions[bot]@users.noreply.github.com"',
|
|
183
|
+
' git tag -a "$RELEASE_TAG" "$RELEASE_SHA" -m "$RELEASE_TAG"',
|
|
184
|
+
' git push origin "refs/tags/$RELEASE_TAG"',
|
|
185
|
+
" fi",
|
|
186
|
+
"else",
|
|
187
|
+
` case "$RELEASE_TAG" in ${tagPrefix}*) ;; *) exit 1 ;; esac`,
|
|
188
|
+
' git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"',
|
|
189
|
+
' test "$(git cat-file -t "$RELEASE_TAG")" = "tag"',
|
|
190
|
+
' RELEASE_SHA="$(git rev-parse "$RELEASE_TAG^{commit}")"',
|
|
191
|
+
' test "$(git rev-parse HEAD)" = "$RELEASE_SHA"',
|
|
160
192
|
' test "$GITHUB_REF_TYPE" = "tag"',
|
|
161
193
|
' test "$GITHUB_REF_NAME" = "$RELEASE_TAG"',
|
|
162
194
|
' test "$RELEASE_SHA" = "$EXPECTED_SHA"',
|
|
@@ -289,31 +321,49 @@ function addDocsJobs(
|
|
|
289
321
|
});
|
|
290
322
|
}
|
|
291
323
|
|
|
292
|
-
/** Owns the single release workflow and
|
|
324
|
+
/** Owns the single release workflow and local release preparation tasks. */
|
|
293
325
|
export class DBXToolsRelease extends Component {
|
|
294
326
|
constructor(project: DBXToolsJavaScriptProject, options: DBXToolsReleaseOptions = {}) {
|
|
295
327
|
super(project);
|
|
296
328
|
const tagPrefix = options.tagPrefix ?? "v";
|
|
329
|
+
const releaseBranch = projectReleaseBranch(project);
|
|
297
330
|
releaseTagPrefixes.set(project, tagPrefix);
|
|
298
331
|
if (options.nodeRelease !== false) nodeReleaseProjects.add(project);
|
|
299
332
|
applyTasks(project, {
|
|
300
333
|
bump: {
|
|
301
334
|
exec: taskScript(project, "bump.ts", `--prefix ${tagPrefix}`),
|
|
302
335
|
receiveArgs: true,
|
|
303
|
-
description: "
|
|
336
|
+
description: "Increment VERSION and synchronize generated workspace versions",
|
|
337
|
+
},
|
|
338
|
+
"version:check": {
|
|
339
|
+
exec: taskScript(project, "version-check.ts"),
|
|
340
|
+
description: "Verify every package and generated barrel matches VERSION",
|
|
304
341
|
},
|
|
342
|
+
...(project.github
|
|
343
|
+
? {
|
|
344
|
+
release: {
|
|
345
|
+
exec: taskScript(
|
|
346
|
+
project,
|
|
347
|
+
"release-pr.ts",
|
|
348
|
+
`--prefix ${tagPrefix} --base ${releaseBranch}`,
|
|
349
|
+
),
|
|
350
|
+
receiveArgs: true,
|
|
351
|
+
description: "Prepare, validate, locally publish, and open a reviewed release PR",
|
|
352
|
+
},
|
|
353
|
+
}
|
|
354
|
+
: {}),
|
|
305
355
|
});
|
|
306
356
|
if (!project.github) return;
|
|
307
357
|
|
|
308
358
|
const workflow = new GithubWorkflow(project.github, "release", {
|
|
309
359
|
fileName: "release.yml",
|
|
310
360
|
limitConcurrency: true,
|
|
311
|
-
concurrencyOptions: { group: "release", cancelInProgress:
|
|
361
|
+
concurrencyOptions: { group: "release", cancelInProgress: false },
|
|
312
362
|
});
|
|
313
363
|
workflow.runName =
|
|
314
|
-
"release ${{ github.event_name == 'push' && github.
|
|
364
|
+
"release ${{ github.event_name == 'push' && github.sha || inputs.release_tag }}";
|
|
315
365
|
workflow.on({
|
|
316
|
-
push: {
|
|
366
|
+
push: { branches: [releaseBranch], paths: ["VERSION"] },
|
|
317
367
|
workflowDispatch: {
|
|
318
368
|
inputs: {
|
|
319
369
|
release_tag: {
|
|
@@ -350,7 +400,7 @@ export class DBXToolsRelease extends Component {
|
|
|
350
400
|
});
|
|
351
401
|
workflow.file?.addOverride("permissions.contents", "read");
|
|
352
402
|
workflow.file?.addOverride("on.workflow_dispatch.inputs.dry_run.default", true);
|
|
353
|
-
workflow.addJob("verify-context", verifyContextJob(tagPrefix));
|
|
403
|
+
workflow.addJob("verify-context", verifyContextJob(tagPrefix, releaseBranch));
|
|
354
404
|
if (options.nodeRelease !== false) {
|
|
355
405
|
workflow.addJob("publish-node", nodePublishJob(project));
|
|
356
406
|
}
|
package/src/workspace-version.ts
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
* DEFAULT_VERSION} when it is absent on a fresh tree); it never rewrites it, so an
|
|
9
9
|
* ordinary `bunx projen` cannot move a package version up or down.
|
|
10
10
|
*
|
|
11
|
-
* Only two callers change the number: `bump`
|
|
12
|
-
* one-time bootstrap of a workspace that has no
|
|
13
|
-
* base from
|
|
14
|
-
* cut elsewhere is respected, and fall back
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* Only two callers change the number: the pure `bump` task used while preparing
|
|
12
|
+
* a reviewed release PR, and the one-time bootstrap of a workspace that has no
|
|
13
|
+
* `VERSION` yet. Both resolve the base from remote git tags first ({@link
|
|
14
|
+
* resolveRemoteVersion}) so a release cut elsewhere is respected, and fall back
|
|
15
|
+
* to the local file (or {@link DEFAULT_VERSION}) when the remote is unreachable
|
|
16
|
+
* or has no tags. The remote is consulted only on those paths.
|
|
17
17
|
*/
|
|
18
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { exec } from "@dbx-tools/core";
|
|
21
21
|
|
|
@@ -30,6 +30,9 @@ const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
|
30
30
|
/** A parsed `[major, minor, patch]` tuple. */
|
|
31
31
|
export type Semver = [number, number, number];
|
|
32
32
|
|
|
33
|
+
/** Supported semantic release increments. */
|
|
34
|
+
export type VersionLevel = "patch" | "minor" | "major";
|
|
35
|
+
|
|
33
36
|
/** Parse `x.y.z` (ignoring any leading `v`/prefix), or `undefined` when it does not match. */
|
|
34
37
|
export function parseSemver(raw: string): Semver | undefined {
|
|
35
38
|
const m = /(\d+)\.(\d+)\.(\d+)/.exec(raw.trim());
|
|
@@ -41,6 +44,13 @@ export function compareSemver(a: Semver, b: Semver): number {
|
|
|
41
44
|
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/** Increment a semantic version tuple by the requested release level. */
|
|
48
|
+
export function incrementSemver(version: Semver, level: VersionLevel): Semver {
|
|
49
|
+
if (level === "major") return [version[0] + 1, 0, 0];
|
|
50
|
+
if (level === "minor") return [version[0], version[1] + 1, 0];
|
|
51
|
+
return [version[0], version[1], version[2] + 1];
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
/** Absolute path to the `VERSION` file for a workspace root. */
|
|
45
55
|
export function versionPath(root: string): string {
|
|
46
56
|
return join(root, VERSION_FILE);
|
|
@@ -70,6 +80,30 @@ export function writeWorkspaceVersion(root: string, version: string): void {
|
|
|
70
80
|
writeFileSync(versionPath(root), `${version}\n`);
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Synchronize one existing workspace manifest with the authoritative version.
|
|
85
|
+
*
|
|
86
|
+
* This covers workspace members that synthesize themselves and therefore are
|
|
87
|
+
* not child projects of the root. Missing manifests are ignored so a freshly
|
|
88
|
+
* bootstrapped extra member can create its package metadata independently.
|
|
89
|
+
*/
|
|
90
|
+
export function syncWorkspaceManifestVersion(manifestPath: string, version: string): boolean {
|
|
91
|
+
if (!existsSync(manifestPath)) return false;
|
|
92
|
+
const content = readFileSync(manifestPath, "utf8");
|
|
93
|
+
const manifest = JSON.parse(content) as Record<string, unknown>;
|
|
94
|
+
if (manifest.version === version) return false;
|
|
95
|
+
|
|
96
|
+
manifest.version = version;
|
|
97
|
+
const { mode } = statSync(manifestPath);
|
|
98
|
+
chmodSync(manifestPath, mode | 0o200);
|
|
99
|
+
try {
|
|
100
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
101
|
+
} finally {
|
|
102
|
+
chmodSync(manifestPath, mode);
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
73
107
|
/** Run git in `cwd`, capturing stdout and swallowing failure (offline, no repo). */
|
|
74
108
|
function gitCapture(cwd: string, args: string[]): string {
|
|
75
109
|
try {
|
|
@@ -138,6 +172,22 @@ export function resolveBaseVersion(
|
|
|
138
172
|
return { version: readWorkspaceVersion(root), source: "local" };
|
|
139
173
|
}
|
|
140
174
|
|
|
175
|
+
/** Resolve the next release version without mutating the workspace. */
|
|
176
|
+
export function resolveNextVersion(
|
|
177
|
+
root: string,
|
|
178
|
+
prefixes: readonly string[],
|
|
179
|
+
level: VersionLevel,
|
|
180
|
+
options: { fetch?: boolean } = {},
|
|
181
|
+
): { base: string; version: string; source: "remote" | "local" } {
|
|
182
|
+
const base = resolveBaseVersion(root, prefixes, options);
|
|
183
|
+
const parsed = parseSemver(base.version) ?? [0, 0, 1];
|
|
184
|
+
return {
|
|
185
|
+
base: parsed.join("."),
|
|
186
|
+
version: incrementSemver(parsed, level).join("."),
|
|
187
|
+
source: base.source,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
141
191
|
/**
|
|
142
192
|
* Create the `VERSION` file when it does not yet exist, seeding it from the remote
|
|
143
193
|
* tags (or {@link DEFAULT_VERSION} when the remote is unreachable / has no tag). An
|