@dbx-tools/projen 0.9.17 → 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 +29 -26
- package/index.ts +2 -2
- package/package.json +4 -5
- package/src/_release-platform.ts +10 -0
- package/src/_rust-project.ts +3 -0
- package/src/_rust-release-workflow.ts +158 -392
- package/src/project-js.ts +8 -13
- package/src/project-py.ts +24 -5
- package/src/project-rs.ts +11 -18
- package/src/release-github.ts +23 -0
- package/src/release-plan.ts +32 -40
- package/src/release.ts +18 -5
- package/tasks/compile-workspace.ts +121 -0
- package/tasks/release-assets.ts +476 -0
- package/tasks/release-plan.ts +1 -2
- package/tasks/release-pr.ts +122 -13
- package/tasks/uniffi-python.js +24 -12
- package/tasks/uniffi-release.mjs +35 -2
- package/src/_rust-release.ts +0 -633
- package/tasks/build-rust-release.ts +0 -68
- package/tasks/rust-release.d.ts +0 -45
- package/tasks/rust-release.mjs +0 -1726
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,
|
|
@@ -465,9 +465,9 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
465
465
|
if (project.releaseCatalog.mode === "independent") {
|
|
466
466
|
workflow.addJob("build-python", {
|
|
467
467
|
if: usesRustArtifacts
|
|
468
|
-
? "${{ always() && needs.release-plan.outputs.python == 'true' && needs.rust-
|
|
468
|
+
? "${{ always() && needs.release-plan.outputs.python == 'true' && needs.rust-build.result != 'failure' && needs.rust-build.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}"
|
|
469
469
|
: "${{ needs.release-plan.outputs.python == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}",
|
|
470
|
-
needs: ["release-plan", ...(usesRustArtifacts ? ["rust-
|
|
470
|
+
needs: ["release-plan", ...(usesRustArtifacts ? ["rust-build"] : [])],
|
|
471
471
|
runsOn: ["ubuntu-latest"],
|
|
472
472
|
permissions: { actions: JobPermission.READ, contents: JobPermission.READ },
|
|
473
473
|
timeoutMinutes: 20,
|
|
@@ -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
|
@@ -4,6 +4,7 @@ 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,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type RustOpenApiOptions as RustOpenApiConfiguration,
|
|
19
20
|
} from "./_rust-project.ts";
|
|
20
21
|
import {
|
|
22
|
+
rustAssetsJob,
|
|
21
23
|
configureRustReleaseTask,
|
|
22
24
|
independentRustCargoPublishJob,
|
|
23
25
|
independentRustGitHubReleaseJob,
|
|
@@ -25,7 +27,6 @@ import {
|
|
|
25
27
|
independentRustNodeFacadePublishJob,
|
|
26
28
|
orderRustBindings,
|
|
27
29
|
planRustRelease,
|
|
28
|
-
releaseBinaryAssetName,
|
|
29
30
|
rustBuildJob,
|
|
30
31
|
rustCargoPublishJob,
|
|
31
32
|
rustGitHubReleaseJob,
|
|
@@ -118,6 +119,7 @@ export interface UniFFIReleaseTarget {
|
|
|
118
119
|
readonly os: RustReleaseOs;
|
|
119
120
|
readonly cpu: RustReleaseCpu;
|
|
120
121
|
readonly libc?: "glibc";
|
|
122
|
+
readonly glibcVersion?: string;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
/** Native targets built on matching GitHub-hosted runners. */
|
|
@@ -130,6 +132,7 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
|
|
|
130
132
|
os: RustReleaseOs.LINUX,
|
|
131
133
|
cpu: RustReleaseCpu.X64,
|
|
132
134
|
libc: "glibc",
|
|
135
|
+
glibcVersion: "2.35",
|
|
133
136
|
},
|
|
134
137
|
{
|
|
135
138
|
runner: "ubuntu-24.04-arm",
|
|
@@ -139,6 +142,7 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
|
|
|
139
142
|
os: RustReleaseOs.LINUX,
|
|
140
143
|
cpu: RustReleaseCpu.ARM64,
|
|
141
144
|
libc: "glibc",
|
|
145
|
+
glibcVersion: "2.39",
|
|
142
146
|
},
|
|
143
147
|
{
|
|
144
148
|
runner: "macos-15-intel",
|
|
@@ -1098,17 +1102,8 @@ export class DBXToolsRustWorkspace extends Component {
|
|
|
1098
1102
|
}
|
|
1099
1103
|
configureRustReleaseTask(project, plan);
|
|
1100
1104
|
if (project.releaseCatalog.mode === "independent") {
|
|
1101
|
-
if (plan.
|
|
1102
|
-
workflow.addJob(
|
|
1103
|
-
"rust-uniffi",
|
|
1104
|
-
rustBuildJob(plan, "uniffi", independentReleaseSetupSteps(project)),
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
if (plan.binaryTargets.length) {
|
|
1108
|
-
workflow.addJob(
|
|
1109
|
-
"rust-binaries",
|
|
1110
|
-
rustBuildJob(plan, "binaries", independentReleaseSetupSteps(project)),
|
|
1111
|
-
);
|
|
1105
|
+
if (plan.targets.length) {
|
|
1106
|
+
workflow.addJob("rust-build", rustBuildJob(plan, independentReleaseSetupSteps(project)));
|
|
1112
1107
|
}
|
|
1113
1108
|
if (plan.publicCrates.length) {
|
|
1114
1109
|
workflow.addJob("publish-cargo", independentRustCargoPublishJob(project, plan));
|
|
@@ -1129,17 +1124,15 @@ export class DBXToolsRustWorkspace extends Component {
|
|
|
1129
1124
|
}
|
|
1130
1125
|
return;
|
|
1131
1126
|
}
|
|
1132
|
-
if (plan.
|
|
1133
|
-
workflow.addJob("rust-
|
|
1134
|
-
|
|
1135
|
-
if (plan.binaryTargets.length) {
|
|
1136
|
-
workflow.addJob("rust-binaries", rustBuildJob(plan, "binaries"));
|
|
1127
|
+
if (plan.targets.length) {
|
|
1128
|
+
workflow.addJob("rust-build", rustBuildJob(plan));
|
|
1129
|
+
workflow.addJob("rust-assets", rustAssetsJob());
|
|
1137
1130
|
}
|
|
1138
1131
|
if (plan.publicCrates.length) {
|
|
1139
1132
|
workflow.addJob("publish-cargo", rustCargoPublishJob(plan, false));
|
|
1140
1133
|
workflow.addJob("publish-local-cargo", rustCargoPublishJob(plan, true));
|
|
1141
1134
|
}
|
|
1142
|
-
if (plan.
|
|
1135
|
+
if (plan.hasTargetOutputs) {
|
|
1143
1136
|
workflow.addJob("publish-github-release", rustGitHubReleaseJob(plan));
|
|
1144
1137
|
}
|
|
1145
1138
|
if (plan.nodeBindings.length && hasNodeRelease(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,
|
package/src/release-plan.ts
CHANGED
|
@@ -47,10 +47,10 @@ export interface PlannedRustTarget {
|
|
|
47
47
|
readonly runner: string;
|
|
48
48
|
readonly python: string;
|
|
49
49
|
readonly libc: string;
|
|
50
|
+
readonly glibcVersion: string;
|
|
50
51
|
readonly packages: readonly string[];
|
|
51
52
|
readonly binaries: readonly string[];
|
|
52
53
|
readonly features: readonly string[];
|
|
53
|
-
readonly fingerprintConfig: string;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** Complete plan persisted as a workflow artifact and used for recovery. */
|
|
@@ -62,8 +62,7 @@ export interface ReleasePlan {
|
|
|
62
62
|
readonly pythonPackages: readonly PlannedPackage[];
|
|
63
63
|
readonly rustPackages: readonly PlannedPackage[];
|
|
64
64
|
readonly artifacts: readonly PlannedArtifact[];
|
|
65
|
-
readonly
|
|
66
|
-
readonly rustBinaryTargets: readonly PlannedRustTarget[];
|
|
65
|
+
readonly rustTargets: readonly PlannedRustTarget[];
|
|
67
66
|
readonly stages: {
|
|
68
67
|
readonly rust: boolean;
|
|
69
68
|
readonly python: boolean;
|
|
@@ -133,53 +132,52 @@ export function buildReleasePlan(
|
|
|
133
132
|
]);
|
|
134
133
|
}
|
|
135
134
|
}
|
|
136
|
-
const
|
|
137
|
-
uniffi: new Map<string, PlannedRustTarget>(),
|
|
138
|
-
binaries: new Map<string, PlannedRustTarget>(),
|
|
139
|
-
};
|
|
135
|
+
const rustTargets = new Map<string, PlannedRustTarget>();
|
|
140
136
|
for (const artifact of artifacts) {
|
|
141
137
|
const targets = artifact.data?.targets;
|
|
142
138
|
if (!Array.isArray(targets)) continue;
|
|
143
|
-
const
|
|
139
|
+
const binary = artifact.kind === "github-binary";
|
|
144
140
|
for (const value of targets) {
|
|
145
141
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
146
142
|
const target = value as Record<string, unknown>;
|
|
147
143
|
const required = ["os", "cpu", "node", "cargo", "runner", "python"] as const;
|
|
148
144
|
if (!required.every((field) => typeof target[field] === "string")) continue;
|
|
149
145
|
const key = String(target.node);
|
|
150
|
-
const
|
|
151
|
-
const currentTarget = targetsByKey.get(key);
|
|
146
|
+
const currentTarget = rustTargets.get(key);
|
|
152
147
|
const packages = [
|
|
153
148
|
...new Set([
|
|
154
149
|
...(currentTarget?.packages ?? []),
|
|
155
150
|
...(rustPackagesByUnit.get(artifact.unit) ?? []),
|
|
156
151
|
]),
|
|
157
152
|
].sort();
|
|
158
|
-
const binaries =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
...
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
: [];
|
|
153
|
+
const binaries = binary
|
|
154
|
+
? [
|
|
155
|
+
...new Set([
|
|
156
|
+
...(currentTarget?.binaries ?? []),
|
|
157
|
+
...(typeof artifact.data?.binary === "string" ? [artifact.data.binary] : []),
|
|
158
|
+
]),
|
|
159
|
+
].sort()
|
|
160
|
+
: (currentTarget?.binaries ?? []);
|
|
167
161
|
const configuredFeatures = Array.isArray(artifact.data?.features)
|
|
168
162
|
? artifact.data.features.filter((feature): feature is string => typeof feature === "string")
|
|
169
163
|
: [];
|
|
170
164
|
const crate = typeof artifact.data?.crate === "string" ? artifact.data.crate : undefined;
|
|
171
|
-
const features =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
...
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
165
|
+
const features = binary
|
|
166
|
+
? [
|
|
167
|
+
...new Set([
|
|
168
|
+
...(currentTarget?.features ?? []),
|
|
169
|
+
...(crate
|
|
170
|
+
? configuredFeatures.map((feature) => `${crate}/${feature}`)
|
|
171
|
+
: configuredFeatures),
|
|
172
|
+
]),
|
|
173
|
+
].sort()
|
|
174
|
+
: [
|
|
175
|
+
...new Set([
|
|
176
|
+
...(currentTarget?.features ?? []),
|
|
177
|
+
...packages.map((pkg) => `${pkg}/uniffi-bindgen`),
|
|
178
|
+
]),
|
|
179
|
+
].sort();
|
|
180
|
+
rustTargets.set(key, {
|
|
183
181
|
os: String(target.os),
|
|
184
182
|
cpu: String(target.cpu),
|
|
185
183
|
node: String(target.node),
|
|
@@ -187,19 +185,14 @@ export function buildReleasePlan(
|
|
|
187
185
|
runner: String(target.runner),
|
|
188
186
|
python: String(target.python),
|
|
189
187
|
libc: typeof target.libc === "string" ? target.libc : "",
|
|
188
|
+
glibcVersion: typeof target.glibcVersion === "string" ? target.glibcVersion : "",
|
|
190
189
|
packages,
|
|
191
190
|
binaries,
|
|
192
191
|
features,
|
|
193
|
-
fingerprintConfig: JSON.stringify({ packages, binaries, features }),
|
|
194
192
|
});
|
|
195
193
|
}
|
|
196
194
|
}
|
|
197
|
-
const
|
|
198
|
-
a.node.localeCompare(b.node),
|
|
199
|
-
);
|
|
200
|
-
const rustBinaryTargets = [...rustTargetsByKind.binaries.values()].sort((a, b) =>
|
|
201
|
-
a.node.localeCompare(b.node),
|
|
202
|
-
);
|
|
195
|
+
const plannedRustTargets = [...rustTargets.values()].sort((a, b) => a.node.localeCompare(b.node));
|
|
203
196
|
const publishBatches = current.publishBatches
|
|
204
197
|
.map((batch) => batch.filter((unit) => selected.has(unit)))
|
|
205
198
|
.filter((batch) => batch.length > 0);
|
|
@@ -226,8 +219,7 @@ export function buildReleasePlan(
|
|
|
226
219
|
pythonPackages,
|
|
227
220
|
rustPackages,
|
|
228
221
|
artifacts,
|
|
229
|
-
|
|
230
|
-
rustBinaryTargets,
|
|
222
|
+
rustTargets: plannedRustTargets,
|
|
231
223
|
stages,
|
|
232
224
|
omittedStages,
|
|
233
225
|
};
|
package/src/release.ts
CHANGED
|
@@ -117,6 +117,7 @@ export function releaseArtifactSteps(options: {
|
|
|
117
117
|
readonly recoveredName: string;
|
|
118
118
|
readonly pattern: string;
|
|
119
119
|
readonly path: string;
|
|
120
|
+
readonly condition?: string;
|
|
120
121
|
}): readonly JobStep[] {
|
|
121
122
|
const shared = {
|
|
122
123
|
pattern: options.pattern,
|
|
@@ -126,13 +127,13 @@ export function releaseArtifactSteps(options: {
|
|
|
126
127
|
return [
|
|
127
128
|
{
|
|
128
129
|
name: options.currentName,
|
|
129
|
-
if:
|
|
130
|
+
if: `\${{ ${options.condition ? `${options.condition} && ` : ""}inputs.source_run_id == '' }}`,
|
|
130
131
|
uses: "actions/download-artifact@v8",
|
|
131
132
|
with: shared,
|
|
132
133
|
},
|
|
133
134
|
{
|
|
134
135
|
name: options.recoveredName,
|
|
135
|
-
if:
|
|
136
|
+
if: `\${{ ${options.condition ? `${options.condition} && ` : ""}inputs.source_run_id != '' }}`,
|
|
136
137
|
uses: "actions/download-artifact@v8",
|
|
137
138
|
with: {
|
|
138
139
|
...shared,
|
|
@@ -179,6 +180,7 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
179
180
|
release_tag: { stepId: "release", outputName: "release_tag" },
|
|
180
181
|
expected_sha: { stepId: "release", outputName: "expected_sha" },
|
|
181
182
|
release_version: { stepId: "release", outputName: "release_version" },
|
|
183
|
+
build_mode: { stepId: "release", outputName: "build_mode" },
|
|
182
184
|
},
|
|
183
185
|
steps: [
|
|
184
186
|
{
|
|
@@ -186,7 +188,7 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
186
188
|
uses: "actions/checkout@v6",
|
|
187
189
|
with: {
|
|
188
190
|
ref: "${{ github.event_name == 'push' && github.sha || inputs.expected_sha }}",
|
|
189
|
-
"fetch-depth":
|
|
191
|
+
"fetch-depth": 0,
|
|
190
192
|
},
|
|
191
193
|
},
|
|
192
194
|
{
|
|
@@ -205,6 +207,7 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
205
207
|
EXPECTED_SHA:
|
|
206
208
|
"${{ github.event_name == 'workflow_dispatch' && inputs.expected_sha || '' }}",
|
|
207
209
|
DRY_RUN: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}",
|
|
210
|
+
GH_TOKEN: "${{ github.token }}",
|
|
208
211
|
},
|
|
209
212
|
// prettier-ignore
|
|
210
213
|
run: stringUtils.dedent(
|
|
@@ -246,9 +249,15 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
246
249
|
case "\${{ inputs.source_run_id }}" in *[!0-9]*|"") exit 1 ;; esac
|
|
247
250
|
fi
|
|
248
251
|
fi
|
|
252
|
+
BUILD_MODE="$(git log -1 --format=%B -- VERSION | sed -n 's/^Release-Build: //p' | tail -1)"
|
|
253
|
+
case "$BUILD_MODE" in local|remote) ;; *) BUILD_MODE="remote" ;; esac
|
|
254
|
+
if [ "$BUILD_MODE" = "local" ] && ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
|
|
255
|
+
gh release create "$RELEASE_TAG" --draft --title "$RELEASE_TAG" --target "$RELEASE_SHA"
|
|
256
|
+
fi
|
|
249
257
|
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
|
250
258
|
echo "expected_sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
|
|
251
259
|
echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
|
|
260
|
+
echo "build_mode=$BUILD_MODE" >> "$GITHUB_OUTPUT"
|
|
252
261
|
`
|
|
253
262
|
// ============================================================================
|
|
254
263
|
),
|
|
@@ -591,8 +600,7 @@ function independentReleasePlanJob(project: DBXToolsJavaScriptProject): Job {
|
|
|
591
600
|
python_packages: { stepId: "plan", outputName: "python_packages" },
|
|
592
601
|
rust_packages: { stepId: "plan", outputName: "rust_packages" },
|
|
593
602
|
artifacts: { stepId: "plan", outputName: "artifacts" },
|
|
594
|
-
|
|
595
|
-
rust_binary_targets: { stepId: "plan", outputName: "rust_binary_targets" },
|
|
603
|
+
rust_targets: { stepId: "plan", outputName: "rust_targets" },
|
|
596
604
|
},
|
|
597
605
|
steps: [
|
|
598
606
|
{
|
|
@@ -1022,6 +1030,11 @@ export class DBXToolsRelease extends Component {
|
|
|
1022
1030
|
description:
|
|
1023
1031
|
"Prepare, validate, locally publish, and automatically merge a release PR",
|
|
1024
1032
|
},
|
|
1033
|
+
"release:assets": {
|
|
1034
|
+
exec: taskScript(project, "release-assets.ts"),
|
|
1035
|
+
receiveArgs: true,
|
|
1036
|
+
description: "Build and upload native assets for an existing GitHub Release",
|
|
1037
|
+
},
|
|
1025
1038
|
}
|
|
1026
1039
|
: {}),
|
|
1027
1040
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Compile source-first workspace packages in a few TypeScript processes. */
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { availableParallelism } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
7
|
+
|
|
8
|
+
interface PackageManifest {
|
|
9
|
+
readonly name?: string;
|
|
10
|
+
readonly scripts?: Record<string, string>;
|
|
11
|
+
readonly workspaces?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface TaskManifest {
|
|
15
|
+
readonly tasks?: {
|
|
16
|
+
readonly compile?: {
|
|
17
|
+
readonly steps?: Array<{ readonly execArgs?: string[] }>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CompilePlan {
|
|
23
|
+
readonly typescriptConfigs: string[];
|
|
24
|
+
readonly customPackages: Array<{ readonly name: string; readonly directory: string }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJson<T>(path: string): T {
|
|
28
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function plainTypeScriptBuild(steps: unknown): boolean {
|
|
32
|
+
if (!Array.isArray(steps) || steps.length !== 1) return false;
|
|
33
|
+
const step = steps[0] as { execArgs?: string[] };
|
|
34
|
+
if (!step || typeof step !== "object") return false;
|
|
35
|
+
return (
|
|
36
|
+
Object.keys(step).length === 1 &&
|
|
37
|
+
step.execArgs?.length === 2 &&
|
|
38
|
+
step.execArgs[0] === "tsc" &&
|
|
39
|
+
step.execArgs[1] === "--build"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Read the root's concrete workspace paths on every run, including newly added members. */
|
|
44
|
+
export function compilePlan(root: string): CompilePlan {
|
|
45
|
+
const workspace = readJson<PackageManifest>(join(root, "package.json"));
|
|
46
|
+
const typescriptConfigs: string[] = [];
|
|
47
|
+
const customPackages: CompilePlan["customPackages"] = [];
|
|
48
|
+
for (const member of workspace.workspaces ?? []) {
|
|
49
|
+
const directory = resolve(root, member);
|
|
50
|
+
const manifest = readJson<PackageManifest>(join(directory, "package.json"));
|
|
51
|
+
const script = manifest.scripts?.compile;
|
|
52
|
+
if (!script) continue;
|
|
53
|
+
const taskPath = join(directory, ".projen", "tasks.json");
|
|
54
|
+
const steps = existsSync(taskPath)
|
|
55
|
+
? readJson<TaskManifest>(taskPath).tasks?.compile?.steps
|
|
56
|
+
: undefined;
|
|
57
|
+
if (
|
|
58
|
+
script === "projen compile" &&
|
|
59
|
+
!manifest.scripts?.precompile &&
|
|
60
|
+
!manifest.scripts?.postcompile &&
|
|
61
|
+
plainTypeScriptBuild(steps)
|
|
62
|
+
) {
|
|
63
|
+
typescriptConfigs.push(join(directory, "tsconfig.json"));
|
|
64
|
+
} else {
|
|
65
|
+
customPackages.push({ name: manifest.name ?? member, directory });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { typescriptConfigs, customPackages };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const emoji = /[\p{Extended_Pictographic}\uFE0F\u200D]/gu;
|
|
72
|
+
|
|
73
|
+
async function main(): Promise<void> {
|
|
74
|
+
const root = process.cwd();
|
|
75
|
+
const plan = compilePlan(root);
|
|
76
|
+
const jobs: Array<{ name: string; result: Promise<exec.ExecResult> }> = [];
|
|
77
|
+
if (plan.typescriptConfigs.length > 0) {
|
|
78
|
+
const batchCount = Math.min(4, availableParallelism(), plan.typescriptConfigs.length);
|
|
79
|
+
const batches = Array.from({ length: batchCount }, () => [] as string[]);
|
|
80
|
+
plan.typescriptConfigs.forEach((config, index) => batches[index % batchCount]!.push(config));
|
|
81
|
+
console.log(
|
|
82
|
+
`Type-checking ${plan.typescriptConfigs.length} workspaces in ${batchCount} batches`,
|
|
83
|
+
);
|
|
84
|
+
for (const [index, batch] of batches.entries()) {
|
|
85
|
+
jobs.push({
|
|
86
|
+
name: `TypeScript batch ${index + 1}`,
|
|
87
|
+
result: exec.spawn("bunx", ["tsc", "--build", ...batch], {
|
|
88
|
+
cwd: root,
|
|
89
|
+
stdin: "ignore",
|
|
90
|
+
stdout: "inherit",
|
|
91
|
+
stderr: "inherit",
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const pkg of plan.customPackages) {
|
|
97
|
+
jobs.push({
|
|
98
|
+
name: pkg.name,
|
|
99
|
+
result: exec.spawn("bun", ["run", "compile"], {
|
|
100
|
+
cwd: pkg.directory,
|
|
101
|
+
stdin: "ignore",
|
|
102
|
+
stdout: (line) => console.log(`${pkg.name}: ${line.replace(emoji, "")}`),
|
|
103
|
+
stderr: (line) => console.error(`${pkg.name}: ${line.replace(emoji, "")}`),
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const results = await Promise.allSettled(jobs.map((job) => job.result));
|
|
108
|
+
let failed = false;
|
|
109
|
+
for (const [index, result] of results.entries()) {
|
|
110
|
+
if (result.status === "rejected") {
|
|
111
|
+
console.error(`${jobs[index]!.name} compile failed:`, result.reason);
|
|
112
|
+
failed = true;
|
|
113
|
+
} else if (result.value.exitCode !== 0) {
|
|
114
|
+
console.error(`${jobs[index]!.name} compile exited with ${result.value.exitCode}`);
|
|
115
|
+
failed = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (failed) process.exitCode = 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (import.meta.main) await main();
|