@dbx-tools/projen 0.9.18 → 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 +19 -19
- package/index.ts +1 -1
- package/package.json +4 -4
- package/src/_rust-release-workflow.ts +25 -202
- package/src/project-py.ts +60 -71
- package/src/project-rs.ts +1 -24
- package/src/release.ts +85 -142
- package/tasks/publish-npm.ts +63 -11
- package/tasks/publish-python.ts +101 -26
- package/tasks/publish.ts +22 -3
- package/tasks/release-candidate.ts +383 -0
- package/tasks/release-manifest.ts +273 -0
- package/tasks/release-pr.ts +68 -151
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,48 +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
|
-
}): readonly JobStep[] {
|
|
122
|
-
const shared = {
|
|
123
|
-
pattern: options.pattern,
|
|
124
|
-
path: options.path,
|
|
125
|
-
"merge-multiple": true,
|
|
126
|
-
};
|
|
127
|
-
return [
|
|
128
|
-
{
|
|
129
|
-
name: options.currentName,
|
|
130
|
-
if: `\${{ ${options.condition ? `${options.condition} && ` : ""}inputs.source_run_id == '' }}`,
|
|
131
|
-
uses: "actions/download-artifact@v8",
|
|
132
|
-
with: shared,
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
name: options.recoveredName,
|
|
136
|
-
if: `\${{ ${options.condition ? `${options.condition} && ` : ""}inputs.source_run_id != '' }}`,
|
|
137
|
-
uses: "actions/download-artifact@v8",
|
|
138
|
-
with: {
|
|
139
|
-
...shared,
|
|
140
|
-
"run-id": "${{ inputs.source_run_id }}",
|
|
141
|
-
"github-token": "${{ github.token }}",
|
|
142
|
-
repository: "${{ github.repository }}",
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
];
|
|
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}'))) }}`;
|
|
146
132
|
}
|
|
147
133
|
|
|
148
134
|
/** Shared Bun, Node, cache, and install setup for Node release jobs. */
|
|
@@ -153,7 +139,11 @@ export function nodeReleaseSetupSteps(project: DBXToolsJavaScriptProject): reado
|
|
|
153
139
|
{
|
|
154
140
|
name: "Setup Node.js",
|
|
155
141
|
uses: "actions/setup-node@v6",
|
|
156
|
-
with: {
|
|
142
|
+
with: {
|
|
143
|
+
"node-version": NODE_VERSION,
|
|
144
|
+
"registry-url": NPM_REGISTRY_URL,
|
|
145
|
+
"package-manager-cache": false,
|
|
146
|
+
},
|
|
157
147
|
},
|
|
158
148
|
{ name: "Install", run: "bun install" },
|
|
159
149
|
bunCacheSaveStep(),
|
|
@@ -164,9 +154,7 @@ export function nodeReleaseSetupSteps(project: DBXToolsJavaScriptProject): reado
|
|
|
164
154
|
export function npmPublishEnvironment(): Record<string, string> {
|
|
165
155
|
return {
|
|
166
156
|
NPM_CONFIG_PROVENANCE:
|
|
167
|
-
"${{ (github.event_name == '
|
|
168
|
-
NPM_CONFIG_TOKEN: "${{ secrets.NPM_TOKEN }}",
|
|
169
|
-
NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}",
|
|
157
|
+
"${{ (github.event_name == 'release' || inputs.dry_run != true) && 'true' || 'false' }}",
|
|
170
158
|
DRY_RUN:
|
|
171
159
|
"${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && '--dry-run' || '' }}",
|
|
172
160
|
};
|
|
@@ -175,19 +163,18 @@ export function npmPublishEnvironment(): Record<string, string> {
|
|
|
175
163
|
function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
176
164
|
return {
|
|
177
165
|
runsOn: ["ubuntu-latest"],
|
|
178
|
-
permissions: {
|
|
166
|
+
permissions: { contents: JobPermission.READ },
|
|
179
167
|
outputs: {
|
|
180
168
|
release_tag: { stepId: "release", outputName: "release_tag" },
|
|
181
169
|
expected_sha: { stepId: "release", outputName: "expected_sha" },
|
|
182
170
|
release_version: { stepId: "release", outputName: "release_version" },
|
|
183
|
-
build_mode: { stepId: "release", outputName: "build_mode" },
|
|
184
171
|
},
|
|
185
172
|
steps: [
|
|
186
173
|
{
|
|
187
174
|
name: "Checkout release source",
|
|
188
175
|
uses: "actions/checkout@v6",
|
|
189
176
|
with: {
|
|
190
|
-
ref: "${{ github.event_name == '
|
|
177
|
+
ref: "${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}",
|
|
191
178
|
"fetch-depth": 0,
|
|
192
179
|
},
|
|
193
180
|
},
|
|
@@ -203,90 +190,39 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
203
190
|
shell: "bash",
|
|
204
191
|
env: {
|
|
205
192
|
RELEASE_TAG:
|
|
206
|
-
"${{ github.event_name == '
|
|
207
|
-
EXPECTED_SHA:
|
|
208
|
-
"${{ github.event_name == 'workflow_dispatch' && inputs.expected_sha || '' }}",
|
|
209
|
-
DRY_RUN: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}",
|
|
193
|
+
"${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}",
|
|
210
194
|
GH_TOKEN: "${{ github.token }}",
|
|
211
195
|
},
|
|
212
196
|
// prettier-ignore
|
|
213
197
|
run: stringUtils.dedent(
|
|
214
198
|
// ============================================================================
|
|
215
199
|
/*bash*/`
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
git tag -a "$RELEASE_TAG" "$RELEASE_SHA" -m "$RELEASE_TAG"
|
|
236
|
-
git push origin "refs/tags/$RELEASE_TAG"
|
|
237
|
-
fi
|
|
238
|
-
else
|
|
239
|
-
case "$RELEASE_TAG" in ${tagPrefix}*) ;; *) exit 1 ;; esac
|
|
240
|
-
RELEASE_VERSION="\${RELEASE_TAG#${tagPrefix}}"
|
|
241
|
-
bun node_modules/@dbx-tools/projen/tasks/release-version.ts --version "$RELEASE_VERSION"
|
|
242
|
-
git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
|
|
243
|
-
test "$(git cat-file -t "$RELEASE_TAG")" = "tag"
|
|
244
|
-
RELEASE_SHA="$(git rev-parse "$RELEASE_TAG^{commit}")"
|
|
245
|
-
test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
|
|
246
|
-
test "$RELEASE_SHA" = "$EXPECTED_SHA"
|
|
247
|
-
if [ -n "\${{ inputs.source_run_id }}" ]; then
|
|
248
|
-
case "\${{ inputs.stage }}" in node|python) ;; *) exit 1 ;; esac
|
|
249
|
-
case "\${{ inputs.source_run_id }}" in *[!0-9]*|"") exit 1 ;; esac
|
|
250
|
-
fi
|
|
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
|
|
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"
|
|
257
219
|
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
|
258
220
|
echo "expected_sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
|
|
259
221
|
echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
|
|
260
|
-
echo "build_mode=$BUILD_MODE" >> "$GITHUB_OUTPUT"
|
|
261
222
|
`
|
|
262
223
|
// ============================================================================
|
|
263
224
|
),
|
|
264
225
|
},
|
|
265
|
-
{
|
|
266
|
-
name: "Verify source artifact run",
|
|
267
|
-
if: "${{ inputs.source_run_id != '' }}",
|
|
268
|
-
uses: "actions/github-script@v8",
|
|
269
|
-
env: {
|
|
270
|
-
EXPECTED_SHA: "${{ steps.release.outputs.expected_sha }}",
|
|
271
|
-
SOURCE_RUN_ID: "${{ inputs.source_run_id }}",
|
|
272
|
-
},
|
|
273
|
-
with: {
|
|
274
|
-
// prettier-ignore
|
|
275
|
-
script: stringUtils.dedent(
|
|
276
|
-
// ============================================================================
|
|
277
|
-
/*js*/`
|
|
278
|
-
const run = await github.rest.actions.getWorkflowRun({
|
|
279
|
-
owner: context.repo.owner,
|
|
280
|
-
repo: context.repo.repo,
|
|
281
|
-
run_id: Number(process.env.SOURCE_RUN_ID),
|
|
282
|
-
});
|
|
283
|
-
if (run.data.path !== ".github/workflows/release.yml") core.setFailed("Source run is not release.yml");
|
|
284
|
-
if (run.data.head_sha !== process.env.EXPECTED_SHA) core.setFailed("Source run commit does not match the release tag");
|
|
285
|
-
`
|
|
286
|
-
// ============================================================================
|
|
287
|
-
),
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
226
|
],
|
|
291
227
|
};
|
|
292
228
|
}
|
|
@@ -302,13 +238,27 @@ function nodePublishJob(project: DBXToolsJavaScriptProject): Job {
|
|
|
302
238
|
steps: [
|
|
303
239
|
...nodeReleaseSetupSteps(project),
|
|
304
240
|
{
|
|
305
|
-
name: "
|
|
306
|
-
env: {
|
|
241
|
+
name: "Download approved npm archives",
|
|
242
|
+
env: { GH_TOKEN: "${{ github.token }}", RELEASE_SHA, RELEASE_TAG, RELEASE_VERSION },
|
|
243
|
+
shell: "bash",
|
|
307
244
|
run: [
|
|
308
|
-
"
|
|
309
|
-
|
|
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",
|
|
310
255
|
].join("\n"),
|
|
311
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
|
+
},
|
|
312
262
|
],
|
|
313
263
|
};
|
|
314
264
|
}
|
|
@@ -1028,12 +978,12 @@ export class DBXToolsRelease extends Component {
|
|
|
1028
978
|
),
|
|
1029
979
|
receiveArgs: true,
|
|
1030
980
|
description:
|
|
1031
|
-
"Prepare, validate, locally publish, and
|
|
981
|
+
"Prepare, validate, locally publish, and upload a draft release candidate",
|
|
1032
982
|
},
|
|
1033
983
|
"release:assets": {
|
|
1034
|
-
exec: taskScript(project, "release-
|
|
984
|
+
exec: taskScript(project, "release-candidate.ts"),
|
|
1035
985
|
receiveArgs: true,
|
|
1036
|
-
description: "
|
|
986
|
+
description: "Rebuild and upload a complete draft release candidate",
|
|
1037
987
|
},
|
|
1038
988
|
}
|
|
1039
989
|
: {}),
|
|
@@ -1043,14 +993,18 @@ export class DBXToolsRelease extends Component {
|
|
|
1043
993
|
const workflow = new GithubWorkflow(project.github, "release", {
|
|
1044
994
|
fileName: "release.yml",
|
|
1045
995
|
limitConcurrency: true,
|
|
1046
|
-
concurrencyOptions: {
|
|
996
|
+
concurrencyOptions: {
|
|
997
|
+
group:
|
|
998
|
+
"release-${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}",
|
|
999
|
+
cancelInProgress: false,
|
|
1000
|
+
},
|
|
1047
1001
|
});
|
|
1048
1002
|
const version = readWorkspaceVersion(project.outdir);
|
|
1049
1003
|
workflow.runName =
|
|
1050
1004
|
`release ${version} ` +
|
|
1051
|
-
"${{ github.event_name == '
|
|
1005
|
+
"${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}";
|
|
1052
1006
|
workflow.on({
|
|
1053
|
-
|
|
1007
|
+
release: { types: ["published"] },
|
|
1054
1008
|
workflowDispatch: {
|
|
1055
1009
|
inputs: {
|
|
1056
1010
|
release_tag: {
|
|
@@ -1058,24 +1012,13 @@ export class DBXToolsRelease extends Component {
|
|
|
1058
1012
|
type: "string",
|
|
1059
1013
|
required: true,
|
|
1060
1014
|
},
|
|
1061
|
-
expected_sha: {
|
|
1062
|
-
description: "Commit the release tag must reference",
|
|
1063
|
-
type: "string",
|
|
1064
|
-
required: true,
|
|
1065
|
-
},
|
|
1066
1015
|
stage: {
|
|
1067
|
-
description: "
|
|
1016
|
+
description: "Published release stage to validate or recover",
|
|
1068
1017
|
type: "choice",
|
|
1069
|
-
options: ["all", "node", "python", "docs"],
|
|
1018
|
+
options: ["all", "node", "python", "cargo", "docs"],
|
|
1070
1019
|
default: "all",
|
|
1071
1020
|
required: true,
|
|
1072
1021
|
},
|
|
1073
|
-
source_run_id: {
|
|
1074
|
-
description: "Earlier release workflow run containing Rust artifacts",
|
|
1075
|
-
type: "string",
|
|
1076
|
-
default: "",
|
|
1077
|
-
required: false,
|
|
1078
|
-
},
|
|
1079
1022
|
dry_run: {
|
|
1080
1023
|
description: "Build and validate without publishing",
|
|
1081
1024
|
type: "boolean",
|
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) {
|
package/tasks/publish-python.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
chmodSync,
|
|
4
4
|
existsSync,
|
|
5
|
+
mkdirSync,
|
|
5
6
|
mkdtempSync,
|
|
6
7
|
readFileSync,
|
|
7
8
|
readdirSync,
|
|
@@ -29,10 +30,19 @@ interface PythonProjectFile {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export interface StampPythonProjectsOptions {
|
|
33
|
+
readonly excludeUniFFI?: boolean;
|
|
32
34
|
readonly rewriteDependencies?: boolean;
|
|
33
35
|
readonly versions?: ReadonlyMap<string, string>;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/** Python wheel and source archives accepted by Twine and package indexes. */
|
|
39
|
+
export function pythonDistributionPaths(directory: string): string[] {
|
|
40
|
+
return readdirSync(directory)
|
|
41
|
+
.filter((file) => file.endsWith(".whl") || file.endsWith(".tar.gz"))
|
|
42
|
+
.sort()
|
|
43
|
+
.map((file) => join(directory, file));
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
export interface RestorePythonProjects {
|
|
37
47
|
(): void;
|
|
38
48
|
readonly paths: readonly string[];
|
|
@@ -63,7 +73,10 @@ export function stampPythonProjects(
|
|
|
63
73
|
};
|
|
64
74
|
});
|
|
65
75
|
const projects = allProjects.filter(
|
|
66
|
-
(project) =>
|
|
76
|
+
(project) =>
|
|
77
|
+
!project.private &&
|
|
78
|
+
(!options.excludeUniFFI || !project.uniffi) &&
|
|
79
|
+
(!options.versions || options.versions.has(project.directory)),
|
|
67
80
|
);
|
|
68
81
|
if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
|
|
69
82
|
|
|
@@ -110,12 +123,65 @@ export function publishPythonProjects(options: {
|
|
|
110
123
|
readonly version: string;
|
|
111
124
|
readonly plan?: ReleasePlan;
|
|
112
125
|
}): void {
|
|
113
|
-
const root = resolve(options.root);
|
|
114
126
|
const output = mkdtempSync(join(tmpdir(), "projen-python-publish-"));
|
|
127
|
+
try {
|
|
128
|
+
buildPythonProjects({ ...options, output });
|
|
129
|
+
exec.spawnSync(
|
|
130
|
+
"uvx",
|
|
131
|
+
[
|
|
132
|
+
"--from",
|
|
133
|
+
"devpi-client",
|
|
134
|
+
"devpi",
|
|
135
|
+
"upload",
|
|
136
|
+
"--index",
|
|
137
|
+
options.publishUrl,
|
|
138
|
+
"--from-dir",
|
|
139
|
+
...(options.dryRun ? ["--dry-run"] : []),
|
|
140
|
+
output,
|
|
141
|
+
],
|
|
142
|
+
{
|
|
143
|
+
cwd: process.cwd(),
|
|
144
|
+
env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl },
|
|
145
|
+
stdout: "inherit",
|
|
146
|
+
stderr: "inherit",
|
|
147
|
+
stdin: "ignore",
|
|
148
|
+
check: true,
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
} finally {
|
|
152
|
+
rmSync(output, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Build the exact Python release distributions without publishing them. */
|
|
157
|
+
export function buildPythonProjects(options: {
|
|
158
|
+
readonly allowEmpty?: boolean;
|
|
159
|
+
readonly excludeUniFFI?: boolean;
|
|
160
|
+
readonly output: string;
|
|
161
|
+
readonly root: string;
|
|
162
|
+
readonly version: string;
|
|
163
|
+
readonly plan?: ReleasePlan;
|
|
164
|
+
}): void {
|
|
165
|
+
const root = resolve(options.root);
|
|
166
|
+
const output = resolve(options.output);
|
|
167
|
+
rmSync(output, { recursive: true, force: true });
|
|
168
|
+
const standardProjects = options.excludeUniFFI
|
|
169
|
+
? readdirSync(root, { withFileTypes: true })
|
|
170
|
+
.filter((entry) => entry.isDirectory())
|
|
171
|
+
.map((entry) => resolve(root, entry.name, "pyproject.toml"))
|
|
172
|
+
.filter(existsSync)
|
|
173
|
+
.map((path) => pythonProjectInfo(readFileSync(path, "utf8"), { parse, stringify }))
|
|
174
|
+
.filter((project) => !project.private && !project.uniffi)
|
|
175
|
+
: undefined;
|
|
176
|
+
if (standardProjects?.length === 0 && options.allowEmpty) {
|
|
177
|
+
mkdirSync(output, { recursive: true });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
115
180
|
const planned = new Map(
|
|
116
181
|
options.plan?.pythonPackages.map((pkg) => [pkg.path.replace(/^.*\//, ""), pkg.version]) ?? [],
|
|
117
182
|
);
|
|
118
183
|
const stamp = stampPythonProjects(root, options.version, {
|
|
184
|
+
excludeUniFFI: options.excludeUniFFI,
|
|
119
185
|
...(options.plan ? { versions: planned } : {}),
|
|
120
186
|
});
|
|
121
187
|
try {
|
|
@@ -130,6 +196,16 @@ export function publishPythonProjects(options: {
|
|
|
130
196
|
check: true,
|
|
131
197
|
});
|
|
132
198
|
}
|
|
199
|
+
} else if (options.excludeUniFFI) {
|
|
200
|
+
for (const project of standardProjects ?? []) {
|
|
201
|
+
exec.spawnSync("uv", ["build", "--package", project.name, "--out-dir", output], {
|
|
202
|
+
cwd: process.cwd(),
|
|
203
|
+
stdout: "inherit",
|
|
204
|
+
stderr: "inherit",
|
|
205
|
+
stdin: "ignore",
|
|
206
|
+
check: true,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
133
209
|
} else {
|
|
134
210
|
exec.spawnSync("uv", ["build", "--all-packages", "--out-dir", output], {
|
|
135
211
|
cwd: process.cwd(),
|
|
@@ -139,31 +215,19 @@ export function publishPythonProjects(options: {
|
|
|
139
215
|
check: true,
|
|
140
216
|
});
|
|
141
217
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
output,
|
|
154
|
-
],
|
|
155
|
-
{
|
|
156
|
-
cwd: process.cwd(),
|
|
157
|
-
env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl },
|
|
158
|
-
stdout: "inherit",
|
|
159
|
-
stderr: "inherit",
|
|
160
|
-
stdin: "ignore",
|
|
161
|
-
check: true,
|
|
162
|
-
},
|
|
163
|
-
);
|
|
218
|
+
const distributions = pythonDistributionPaths(output);
|
|
219
|
+
if (distributions.length === 0) {
|
|
220
|
+
throw new Error(`No Python distributions found in ${output}`);
|
|
221
|
+
}
|
|
222
|
+
exec.spawnSync("uvx", ["twine", "check", ...distributions], {
|
|
223
|
+
cwd: process.cwd(),
|
|
224
|
+
stdout: "inherit",
|
|
225
|
+
stderr: "inherit",
|
|
226
|
+
stdin: "ignore",
|
|
227
|
+
check: true,
|
|
228
|
+
});
|
|
164
229
|
} finally {
|
|
165
230
|
stamp();
|
|
166
|
-
rmSync(output, { recursive: true, force: true });
|
|
167
231
|
}
|
|
168
232
|
}
|
|
169
233
|
|
|
@@ -175,6 +239,7 @@ if (import.meta.main) {
|
|
|
175
239
|
.requiredOption("--publish-url <url>", "devpi writable index URL")
|
|
176
240
|
.option("--root <path>", "Python workspace package root", "packages/py")
|
|
177
241
|
.option("--plan <path>", "affected release plan")
|
|
242
|
+
.option("--output <path>", "Build distributions into a directory without publishing")
|
|
178
243
|
.option("--dry-run", "build and inspect distributions without uploading")
|
|
179
244
|
.action(
|
|
180
245
|
(
|
|
@@ -185,6 +250,7 @@ if (import.meta.main) {
|
|
|
185
250
|
publishUrl: string;
|
|
186
251
|
root: string;
|
|
187
252
|
plan?: string;
|
|
253
|
+
output?: string;
|
|
188
254
|
},
|
|
189
255
|
) => {
|
|
190
256
|
const plan = options.plan
|
|
@@ -192,7 +258,16 @@ if (import.meta.main) {
|
|
|
192
258
|
: undefined;
|
|
193
259
|
const fallbackVersion = version ?? plan?.pythonPackages[0]?.version;
|
|
194
260
|
if (!fallbackVersion) throw new Error("Python publication requires a version or plan");
|
|
195
|
-
|
|
261
|
+
if (options.output) {
|
|
262
|
+
buildPythonProjects({
|
|
263
|
+
output: options.output,
|
|
264
|
+
root: options.root,
|
|
265
|
+
version: fallbackVersion,
|
|
266
|
+
plan,
|
|
267
|
+
});
|
|
268
|
+
} else {
|
|
269
|
+
publishPythonProjects({ ...options, version: fallbackVersion, plan });
|
|
270
|
+
}
|
|
196
271
|
},
|
|
197
272
|
);
|
|
198
273
|
await program.parseAsync();
|