@dbx-tools/projen 0.9.17 → 0.9.19
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 +39 -36
- 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 +117 -528
- package/src/project-js.ts +8 -13
- package/src/project-py.ts +62 -54
- package/src/project-rs.ts +7 -37
- package/src/release-github.ts +23 -0
- package/src/release-plan.ts +32 -40
- package/src/release.ts +91 -135
- package/tasks/compile-workspace.ts +121 -0
- package/tasks/publish-npm.ts +63 -11
- package/tasks/publish-python.ts +101 -26
- package/tasks/publish.ts +22 -3
- package/tasks/release-assets.ts +476 -0
- package/tasks/release-candidate.ts +383 -0
- package/tasks/release-manifest.ts +273 -0
- package/tasks/release-plan.ts +1 -2
- package/tasks/release-pr.ts +86 -60
- 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/release.ts
CHANGED
|
@@ -7,13 +7,15 @@ import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workf
|
|
|
7
7
|
import { projectReleaseBranch, type DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
8
8
|
import { applyTasks, taskScript } from "./project.ts";
|
|
9
9
|
import {
|
|
10
|
+
RELEASE_SHA,
|
|
11
|
+
RELEASE_TAG,
|
|
10
12
|
RELEASE_VERSION,
|
|
11
13
|
releaseSourceSteps,
|
|
12
14
|
type ReleaseSummaryProviderName,
|
|
13
15
|
} from "./release-dispatch.ts";
|
|
14
16
|
import { readWorkspaceVersion } from "./workspace-version.ts";
|
|
15
17
|
|
|
16
|
-
const NODE_VERSION = "
|
|
18
|
+
const NODE_VERSION = "24";
|
|
17
19
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
18
20
|
const nodeReleaseProjects = new WeakSet<DBXToolsJavaScriptProject>();
|
|
19
21
|
const releaseTagPrefixes = new WeakMap<DBXToolsJavaScriptProject, string>();
|
|
@@ -21,7 +23,7 @@ const releaseWorkflows = new WeakMap<DBXToolsJavaScriptProject, GithubWorkflow>(
|
|
|
21
23
|
const independentPublicationJobs = new WeakMap<GithubWorkflow, Set<string>>();
|
|
22
24
|
|
|
23
25
|
/** Independently recoverable portions of the release workflow. */
|
|
24
|
-
export type ReleaseStage = "all" | "node" | "python" | "docs";
|
|
26
|
+
export type ReleaseStage = "all" | "node" | "python" | "cargo" | "docs";
|
|
25
27
|
|
|
26
28
|
/** GitHub Pages configuration included in the unified release workflow. */
|
|
27
29
|
export interface ReleaseDocsOptions {
|
|
@@ -101,47 +103,32 @@ export function releaseTagPattern(project: DBXToolsJavaScriptProject): string {
|
|
|
101
103
|
return `${prefix}*`;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
/** Run a release stage on
|
|
105
|
-
export function releaseStageCondition(
|
|
106
|
-
|
|
106
|
+
/** Run a release stage on promotion or when selected for manual recovery. */
|
|
107
|
+
export function releaseStageCondition(
|
|
108
|
+
stage: Exclude<ReleaseStage, "all">,
|
|
109
|
+
prerequisites: readonly string[] = [],
|
|
110
|
+
): string {
|
|
111
|
+
if (prerequisites.length === 0) {
|
|
112
|
+
return `\${{ github.event_name == 'release' || inputs.stage == 'all' || inputs.stage == '${stage}' }}`;
|
|
113
|
+
}
|
|
114
|
+
const prefix = prerequisites.length
|
|
115
|
+
? `always() && ${prerequisites.map((condition) => `(${condition})`).join(" && ")} && `
|
|
116
|
+
: "";
|
|
117
|
+
return `\${{ ${prefix}(github.event_name == 'release' || inputs.stage == 'all' || inputs.stage == '${stage}') }}`;
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
/** Publish a selected stage unless a manual run remains in dry-run mode. */
|
|
110
|
-
export function releasePublishCondition(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
const shared = {
|
|
122
|
-
pattern: options.pattern,
|
|
123
|
-
path: options.path,
|
|
124
|
-
"merge-multiple": true,
|
|
125
|
-
};
|
|
126
|
-
return [
|
|
127
|
-
{
|
|
128
|
-
name: options.currentName,
|
|
129
|
-
if: "${{ inputs.source_run_id == '' }}",
|
|
130
|
-
uses: "actions/download-artifact@v8",
|
|
131
|
-
with: shared,
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: options.recoveredName,
|
|
135
|
-
if: "${{ inputs.source_run_id != '' }}",
|
|
136
|
-
uses: "actions/download-artifact@v8",
|
|
137
|
-
with: {
|
|
138
|
-
...shared,
|
|
139
|
-
"run-id": "${{ inputs.source_run_id }}",
|
|
140
|
-
"github-token": "${{ github.token }}",
|
|
141
|
-
repository: "${{ github.repository }}",
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
];
|
|
121
|
+
export function releasePublishCondition(
|
|
122
|
+
stage: Exclude<ReleaseStage, "all">,
|
|
123
|
+
prerequisites: readonly string[] = [],
|
|
124
|
+
): string {
|
|
125
|
+
if (prerequisites.length === 0) {
|
|
126
|
+
return `\${{ github.event_name == 'release' || (inputs.dry_run != true && (inputs.stage == 'all' || inputs.stage == '${stage}')) }}`;
|
|
127
|
+
}
|
|
128
|
+
const prefix = prerequisites.length
|
|
129
|
+
? `always() && ${prerequisites.map((condition) => `(${condition})`).join(" && ")} && `
|
|
130
|
+
: "";
|
|
131
|
+
return `\${{ ${prefix}(github.event_name == 'release' || (inputs.dry_run != true && (inputs.stage == 'all' || inputs.stage == '${stage}'))) }}`;
|
|
145
132
|
}
|
|
146
133
|
|
|
147
134
|
/** Shared Bun, Node, cache, and install setup for Node release jobs. */
|
|
@@ -152,7 +139,11 @@ export function nodeReleaseSetupSteps(project: DBXToolsJavaScriptProject): reado
|
|
|
152
139
|
{
|
|
153
140
|
name: "Setup Node.js",
|
|
154
141
|
uses: "actions/setup-node@v6",
|
|
155
|
-
with: {
|
|
142
|
+
with: {
|
|
143
|
+
"node-version": NODE_VERSION,
|
|
144
|
+
"registry-url": NPM_REGISTRY_URL,
|
|
145
|
+
"package-manager-cache": false,
|
|
146
|
+
},
|
|
156
147
|
},
|
|
157
148
|
{ name: "Install", run: "bun install" },
|
|
158
149
|
bunCacheSaveStep(),
|
|
@@ -163,9 +154,7 @@ export function nodeReleaseSetupSteps(project: DBXToolsJavaScriptProject): reado
|
|
|
163
154
|
export function npmPublishEnvironment(): Record<string, string> {
|
|
164
155
|
return {
|
|
165
156
|
NPM_CONFIG_PROVENANCE:
|
|
166
|
-
"${{ (github.event_name == '
|
|
167
|
-
NPM_CONFIG_TOKEN: "${{ secrets.NPM_TOKEN }}",
|
|
168
|
-
NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}",
|
|
157
|
+
"${{ (github.event_name == 'release' || inputs.dry_run != true) && 'true' || 'false' }}",
|
|
169
158
|
DRY_RUN:
|
|
170
159
|
"${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && '--dry-run' || '' }}",
|
|
171
160
|
};
|
|
@@ -174,7 +163,7 @@ export function npmPublishEnvironment(): Record<string, string> {
|
|
|
174
163
|
function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
175
164
|
return {
|
|
176
165
|
runsOn: ["ubuntu-latest"],
|
|
177
|
-
permissions: {
|
|
166
|
+
permissions: { contents: JobPermission.READ },
|
|
178
167
|
outputs: {
|
|
179
168
|
release_tag: { stepId: "release", outputName: "release_tag" },
|
|
180
169
|
expected_sha: { stepId: "release", outputName: "expected_sha" },
|
|
@@ -185,8 +174,8 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
185
174
|
name: "Checkout release source",
|
|
186
175
|
uses: "actions/checkout@v6",
|
|
187
176
|
with: {
|
|
188
|
-
ref: "${{ github.event_name == '
|
|
189
|
-
"fetch-depth":
|
|
177
|
+
ref: "${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}",
|
|
178
|
+
"fetch-depth": 0,
|
|
190
179
|
},
|
|
191
180
|
},
|
|
192
181
|
{
|
|
@@ -201,51 +190,32 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
201
190
|
shell: "bash",
|
|
202
191
|
env: {
|
|
203
192
|
RELEASE_TAG:
|
|
204
|
-
"${{ github.event_name == '
|
|
205
|
-
|
|
206
|
-
"${{ github.event_name == 'workflow_dispatch' && inputs.expected_sha || '' }}",
|
|
207
|
-
DRY_RUN: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}",
|
|
193
|
+
"${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}",
|
|
194
|
+
GH_TOKEN: "${{ github.token }}",
|
|
208
195
|
},
|
|
209
196
|
// prettier-ignore
|
|
210
197
|
run: stringUtils.dedent(
|
|
211
198
|
// ============================================================================
|
|
212
199
|
/*bash*/`
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
git tag -a "$RELEASE_TAG" "$RELEASE_SHA" -m "$RELEASE_TAG"
|
|
233
|
-
git push origin "refs/tags/$RELEASE_TAG"
|
|
234
|
-
fi
|
|
235
|
-
else
|
|
236
|
-
case "$RELEASE_TAG" in ${tagPrefix}*) ;; *) exit 1 ;; esac
|
|
237
|
-
RELEASE_VERSION="\${RELEASE_TAG#${tagPrefix}}"
|
|
238
|
-
bun node_modules/@dbx-tools/projen/tasks/release-version.ts --version "$RELEASE_VERSION"
|
|
239
|
-
git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
|
|
240
|
-
test "$(git cat-file -t "$RELEASE_TAG")" = "tag"
|
|
241
|
-
RELEASE_SHA="$(git rev-parse "$RELEASE_TAG^{commit}")"
|
|
242
|
-
test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
|
|
243
|
-
test "$RELEASE_SHA" = "$EXPECTED_SHA"
|
|
244
|
-
if [ -n "\${{ inputs.source_run_id }}" ]; then
|
|
245
|
-
case "\${{ inputs.stage }}" in node|python) ;; *) exit 1 ;; esac
|
|
246
|
-
case "\${{ inputs.source_run_id }}" in *[!0-9]*|"") exit 1 ;; esac
|
|
247
|
-
fi
|
|
248
|
-
fi
|
|
200
|
+
case "$RELEASE_TAG" in ${tagPrefix}*) ;; *) exit 1 ;; esac
|
|
201
|
+
RELEASE_VERSION="\${RELEASE_TAG#${tagPrefix}}"
|
|
202
|
+
bun node_modules/@dbx-tools/projen/tasks/release-version.ts --version "$RELEASE_VERSION"
|
|
203
|
+
git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
|
|
204
|
+
git fetch --force origin "+refs/heads/${releaseBranch}:refs/remotes/origin/${releaseBranch}"
|
|
205
|
+
test "$(git cat-file -t "$RELEASE_TAG")" = "tag"
|
|
206
|
+
RELEASE_SHA="$(git rev-parse "$RELEASE_TAG^{commit}")"
|
|
207
|
+
test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
|
|
208
|
+
git merge-base --is-ancestor "$RELEASE_SHA" "origin/${releaseBranch}"
|
|
209
|
+
test "$(tr -d '\\r\\n' < VERSION)" = "$RELEASE_VERSION"
|
|
210
|
+
test "$(gh release view "$RELEASE_TAG" --json isDraft --jq .isDraft)" = "false"
|
|
211
|
+
rm -rf dist/release-candidate
|
|
212
|
+
mkdir -p dist/release-candidate
|
|
213
|
+
gh release download "$RELEASE_TAG" --dir dist/release-candidate
|
|
214
|
+
bun node_modules/@dbx-tools/projen/tasks/release-manifest.ts verify \
|
|
215
|
+
--directory dist/release-candidate \
|
|
216
|
+
--tag "$RELEASE_TAG" \
|
|
217
|
+
--sha "$RELEASE_SHA" \
|
|
218
|
+
--version "$RELEASE_VERSION"
|
|
249
219
|
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
|
250
220
|
echo "expected_sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
|
|
251
221
|
echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
|
|
@@ -253,31 +223,6 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
253
223
|
// ============================================================================
|
|
254
224
|
),
|
|
255
225
|
},
|
|
256
|
-
{
|
|
257
|
-
name: "Verify source artifact run",
|
|
258
|
-
if: "${{ inputs.source_run_id != '' }}",
|
|
259
|
-
uses: "actions/github-script@v8",
|
|
260
|
-
env: {
|
|
261
|
-
EXPECTED_SHA: "${{ steps.release.outputs.expected_sha }}",
|
|
262
|
-
SOURCE_RUN_ID: "${{ inputs.source_run_id }}",
|
|
263
|
-
},
|
|
264
|
-
with: {
|
|
265
|
-
// prettier-ignore
|
|
266
|
-
script: stringUtils.dedent(
|
|
267
|
-
// ============================================================================
|
|
268
|
-
/*js*/`
|
|
269
|
-
const run = await github.rest.actions.getWorkflowRun({
|
|
270
|
-
owner: context.repo.owner,
|
|
271
|
-
repo: context.repo.repo,
|
|
272
|
-
run_id: Number(process.env.SOURCE_RUN_ID),
|
|
273
|
-
});
|
|
274
|
-
if (run.data.path !== ".github/workflows/release.yml") core.setFailed("Source run is not release.yml");
|
|
275
|
-
if (run.data.head_sha !== process.env.EXPECTED_SHA) core.setFailed("Source run commit does not match the release tag");
|
|
276
|
-
`
|
|
277
|
-
// ============================================================================
|
|
278
|
-
),
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
226
|
],
|
|
282
227
|
};
|
|
283
228
|
}
|
|
@@ -293,13 +238,27 @@ function nodePublishJob(project: DBXToolsJavaScriptProject): Job {
|
|
|
293
238
|
steps: [
|
|
294
239
|
...nodeReleaseSetupSteps(project),
|
|
295
240
|
{
|
|
296
|
-
name: "
|
|
297
|
-
env: {
|
|
241
|
+
name: "Download approved npm archives",
|
|
242
|
+
env: { GH_TOKEN: "${{ github.token }}", RELEASE_SHA, RELEASE_TAG, RELEASE_VERSION },
|
|
243
|
+
shell: "bash",
|
|
298
244
|
run: [
|
|
299
|
-
"
|
|
300
|
-
|
|
245
|
+
"rm -rf dist/release-download dist/npm-release",
|
|
246
|
+
"mkdir -p dist/release-download",
|
|
247
|
+
'gh release download "$RELEASE_TAG" --pattern release-manifest.json --pattern SHA256SUMS --pattern "*.tgz" --dir dist/release-download',
|
|
248
|
+
"bun node_modules/@dbx-tools/projen/tasks/release-manifest.ts verify \\",
|
|
249
|
+
" --directory dist/release-download \\",
|
|
250
|
+
' --tag "$RELEASE_TAG" \\',
|
|
251
|
+
' --sha "$RELEASE_SHA" \\',
|
|
252
|
+
' --version "$RELEASE_VERSION" \\',
|
|
253
|
+
" --kind npm \\",
|
|
254
|
+
" --output dist/npm-release",
|
|
301
255
|
].join("\n"),
|
|
302
256
|
},
|
|
257
|
+
{
|
|
258
|
+
name: "Publish approved npm archives",
|
|
259
|
+
env: { RELEASE_VERSION, ...npmPublishEnvironment() },
|
|
260
|
+
run: 'bun node_modules/@dbx-tools/projen/tasks/publish-npm.ts --directory dist/npm-release --version "$RELEASE_VERSION" $DRY_RUN',
|
|
261
|
+
},
|
|
303
262
|
],
|
|
304
263
|
};
|
|
305
264
|
}
|
|
@@ -591,8 +550,7 @@ function independentReleasePlanJob(project: DBXToolsJavaScriptProject): Job {
|
|
|
591
550
|
python_packages: { stepId: "plan", outputName: "python_packages" },
|
|
592
551
|
rust_packages: { stepId: "plan", outputName: "rust_packages" },
|
|
593
552
|
artifacts: { stepId: "plan", outputName: "artifacts" },
|
|
594
|
-
|
|
595
|
-
rust_binary_targets: { stepId: "plan", outputName: "rust_binary_targets" },
|
|
553
|
+
rust_targets: { stepId: "plan", outputName: "rust_targets" },
|
|
596
554
|
},
|
|
597
555
|
steps: [
|
|
598
556
|
{
|
|
@@ -1020,7 +978,12 @@ export class DBXToolsRelease extends Component {
|
|
|
1020
978
|
),
|
|
1021
979
|
receiveArgs: true,
|
|
1022
980
|
description:
|
|
1023
|
-
"Prepare, validate, locally publish, and
|
|
981
|
+
"Prepare, validate, locally publish, and upload a draft release candidate",
|
|
982
|
+
},
|
|
983
|
+
"release:assets": {
|
|
984
|
+
exec: taskScript(project, "release-candidate.ts"),
|
|
985
|
+
receiveArgs: true,
|
|
986
|
+
description: "Rebuild and upload a complete draft release candidate",
|
|
1024
987
|
},
|
|
1025
988
|
}
|
|
1026
989
|
: {}),
|
|
@@ -1030,14 +993,18 @@ export class DBXToolsRelease extends Component {
|
|
|
1030
993
|
const workflow = new GithubWorkflow(project.github, "release", {
|
|
1031
994
|
fileName: "release.yml",
|
|
1032
995
|
limitConcurrency: true,
|
|
1033
|
-
concurrencyOptions: {
|
|
996
|
+
concurrencyOptions: {
|
|
997
|
+
group:
|
|
998
|
+
"release-${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}",
|
|
999
|
+
cancelInProgress: false,
|
|
1000
|
+
},
|
|
1034
1001
|
});
|
|
1035
1002
|
const version = readWorkspaceVersion(project.outdir);
|
|
1036
1003
|
workflow.runName =
|
|
1037
1004
|
`release ${version} ` +
|
|
1038
|
-
"${{ github.event_name == '
|
|
1005
|
+
"${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}";
|
|
1039
1006
|
workflow.on({
|
|
1040
|
-
|
|
1007
|
+
release: { types: ["published"] },
|
|
1041
1008
|
workflowDispatch: {
|
|
1042
1009
|
inputs: {
|
|
1043
1010
|
release_tag: {
|
|
@@ -1045,24 +1012,13 @@ export class DBXToolsRelease extends Component {
|
|
|
1045
1012
|
type: "string",
|
|
1046
1013
|
required: true,
|
|
1047
1014
|
},
|
|
1048
|
-
expected_sha: {
|
|
1049
|
-
description: "Commit the release tag must reference",
|
|
1050
|
-
type: "string",
|
|
1051
|
-
required: true,
|
|
1052
|
-
},
|
|
1053
1015
|
stage: {
|
|
1054
|
-
description: "
|
|
1016
|
+
description: "Published release stage to validate or recover",
|
|
1055
1017
|
type: "choice",
|
|
1056
|
-
options: ["all", "node", "python", "docs"],
|
|
1018
|
+
options: ["all", "node", "python", "cargo", "docs"],
|
|
1057
1019
|
default: "all",
|
|
1058
1020
|
required: true,
|
|
1059
1021
|
},
|
|
1060
|
-
source_run_id: {
|
|
1061
|
-
description: "Earlier release workflow run containing Rust artifacts",
|
|
1062
|
-
type: "string",
|
|
1063
|
-
default: "",
|
|
1064
|
-
required: false,
|
|
1065
|
-
},
|
|
1066
1022
|
dry_run: {
|
|
1067
1023
|
description: "Build and validate without publishing",
|
|
1068
1024
|
type: "boolean",
|
|
@@ -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();
|
package/tasks/publish-npm.ts
CHANGED
|
@@ -28,6 +28,15 @@ export interface NpmReleaseIdentity {
|
|
|
28
28
|
readonly version: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
interface NpmArchiveManifest {
|
|
32
|
+
readonly dependencies?: Readonly<Record<string, string>>;
|
|
33
|
+
readonly optionalDependencies?: Readonly<Record<string, string>>;
|
|
34
|
+
readonly name?: string;
|
|
35
|
+
readonly publishConfig?: { readonly access?: unknown };
|
|
36
|
+
readonly repository?: unknown;
|
|
37
|
+
readonly version?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
function normalizedRepository(value: unknown): string | undefined {
|
|
32
41
|
const url =
|
|
33
42
|
typeof value === "string"
|
|
@@ -120,7 +129,7 @@ export async function publishedNpmRelease(
|
|
|
120
129
|
};
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
|
|
132
|
+
function readNpmArchiveManifest(path: string): NpmArchiveManifest {
|
|
124
133
|
const result = exec.spawnSync("tar", ["-xOf", path, "package/package.json"], {
|
|
125
134
|
cwd: process.cwd(),
|
|
126
135
|
stdout: "capture",
|
|
@@ -131,12 +140,11 @@ export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
|
|
|
131
140
|
if (result.exitCode !== 0 || !result.stdout) {
|
|
132
141
|
throw new Error(`Cannot read npm package manifest from ${path}: ${result.stderr}`);
|
|
133
142
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
};
|
|
143
|
+
return JSON.parse(result.stdout) as NpmArchiveManifest;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
|
|
147
|
+
const manifest = readNpmArchiveManifest(path);
|
|
140
148
|
if (!manifest.name || !manifest.version) {
|
|
141
149
|
throw new Error(`npm archive has no package name or version: ${path}`);
|
|
142
150
|
}
|
|
@@ -154,6 +162,48 @@ export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
|
|
|
154
162
|
};
|
|
155
163
|
}
|
|
156
164
|
|
|
165
|
+
function orderNpmArchives(archives: readonly string[]): string[] {
|
|
166
|
+
const byName = new Map(
|
|
167
|
+
archives.map((archive) => {
|
|
168
|
+
const manifest = readNpmArchiveManifest(archive);
|
|
169
|
+
if (!manifest.name) throw new Error(`npm archive has no package name: ${archive}`);
|
|
170
|
+
return [manifest.name, { archive, manifest }] as const;
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
if (byName.size !== archives.length) throw new Error("Release contains duplicate npm packages");
|
|
174
|
+
const remaining = new Map(
|
|
175
|
+
[...byName].map(([name, value]) => [
|
|
176
|
+
name,
|
|
177
|
+
new Set(
|
|
178
|
+
[
|
|
179
|
+
...Object.keys(value.manifest.dependencies ?? {}),
|
|
180
|
+
...Object.keys(value.manifest.optionalDependencies ?? {}),
|
|
181
|
+
].filter((dependency) => byName.has(dependency)),
|
|
182
|
+
),
|
|
183
|
+
]),
|
|
184
|
+
);
|
|
185
|
+
const ordered: string[] = [];
|
|
186
|
+
while (remaining.size > 0) {
|
|
187
|
+
const ready = [...remaining]
|
|
188
|
+
.filter(([, dependencies]) => dependencies.size === 0)
|
|
189
|
+
.map(([name]) => name)
|
|
190
|
+
.sort();
|
|
191
|
+
if (ready.length === 0) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Cyclic npm release dependencies: ${[...remaining.keys()].sort().join(", ")}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
for (const name of ready) {
|
|
197
|
+
ordered.push(byName.get(name)!.archive);
|
|
198
|
+
remaining.delete(name);
|
|
199
|
+
}
|
|
200
|
+
for (const dependencies of remaining.values()) {
|
|
201
|
+
for (const name of ready) dependencies.delete(name);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return ordered;
|
|
205
|
+
}
|
|
206
|
+
|
|
157
207
|
/** Hash paths, executable bits, symlink targets, and bytes while ignoring tar metadata. */
|
|
158
208
|
export function npmArchiveContentDigest(path: string): string {
|
|
159
209
|
const temp = mkdtempSync(join(tmpdir(), "projen-npm-content-"));
|
|
@@ -219,10 +269,12 @@ export async function publishNpmArchives(options: {
|
|
|
219
269
|
readonly version?: string;
|
|
220
270
|
}): Promise<void> {
|
|
221
271
|
const directory = resolve(options.directory);
|
|
222
|
-
const archives =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
272
|
+
const archives = orderNpmArchives(
|
|
273
|
+
readdirSync(directory)
|
|
274
|
+
.filter((file) => file.endsWith(".tgz"))
|
|
275
|
+
.sort()
|
|
276
|
+
.map((file) => join(directory, file)),
|
|
277
|
+
);
|
|
226
278
|
if (archives.length === 0) throw new Error(`No npm archives found in ${directory}`);
|
|
227
279
|
|
|
228
280
|
for (const archive of archives) {
|