@dbx-tools/projen 0.9.38 → 0.9.40
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 +72 -5
- package/index.ts +5 -2
- package/package.json +4 -4
- package/src/project-js.ts +3 -0
- package/src/project-py.ts +39 -17
- package/src/release-context.ts +2 -33
- package/src/release-options.ts +82 -0
- package/src/release.ts +113 -58
- package/tasks/lib/publish-python.ts +16 -2
- package/tasks/lib/release.ts +97 -31
- package/tasks/release-options.ts +32 -0
- package/tasks/release.ts +1 -1
package/README.md
CHANGED
|
@@ -64,6 +64,8 @@ Repository policy stays in the consuming `.projenrc.ts`:
|
|
|
64
64
|
preparation. Omit it when the workspace has no standard Python packages.
|
|
65
65
|
- `releaseValidationTasks` names repository tasks that must pass in tag CI
|
|
66
66
|
before release artifacts are built and published.
|
|
67
|
+
- `releaseSetupSteps` installs any extra validation/build prerequisites in the
|
|
68
|
+
shared release job before the validation tasks run.
|
|
67
69
|
- `pullRequestTitlePolicy` configures semantic title types and scope policy.
|
|
68
70
|
Omit it or pass `false` to disable the title job.
|
|
69
71
|
- `workflowCacheIgnorePaths` excludes generated output trees that may contain
|
|
@@ -380,17 +382,45 @@ list, so a new package is covered without a re-synth. Work from the root:
|
|
|
380
382
|
| `bun run version:check` | verify every generated version against `VERSION` |
|
|
381
383
|
| `bun run release` | run the configured release transaction |
|
|
382
384
|
|
|
383
|
-
Run
|
|
385
|
+
Run `bun run release` from any branch. Pending changes are committed and
|
|
384
386
|
the current branch is pushed first. A non-`main` branch then fast-forwards
|
|
385
387
|
`main`; the command fails without creating a merge commit if either branch has
|
|
386
388
|
diverged. From `main`, it calls the existing `bump` task, commits the generated
|
|
387
389
|
version changes, pushes `main`, and pushes the matching annotated `vX.Y.Z` tag.
|
|
388
390
|
Pass `--no-bump` only to release an existing synchronized local bump. The tag
|
|
389
|
-
workflow verifies that the tag points at the exact `origin/main` commit
|
|
390
|
-
|
|
391
|
-
|
|
391
|
+
workflow verifies that the tag points at the exact `origin/main` commit. One
|
|
392
|
+
`build-release` job restores the Bun cache and installs workspace dependencies,
|
|
393
|
+
runs validation, packs the npm archives, builds all Python distributions, and
|
|
394
|
+
builds the documentation site. Install validation prerequisites through
|
|
395
|
+
`releaseSetupSteps`; `releaseDocs.prepareSteps` runs after validation.
|
|
396
|
+
|
|
397
|
+
Publication jobs download the resulting artifacts rather than rebuilding the
|
|
398
|
+
workspace. Each Python package retains its own publishing environment and waits
|
|
399
|
+
for its package dependencies to publish. The npm job uses the bundled archive
|
|
400
|
+
publisher without a checkout or workspace install. Pages deploys the already
|
|
401
|
+
built site after registry publication succeeds. Local npm and Python registry publication remains available
|
|
392
402
|
through the direct local publication tooling.
|
|
393
403
|
|
|
404
|
+
The default release still publishes npm, Python, and docs, then publishes to
|
|
405
|
+
configured local registries automatically. Select a scope or disable individual
|
|
406
|
+
steps for one run without changing the Projen definition:
|
|
407
|
+
|
|
408
|
+
```sh
|
|
409
|
+
bun run release --publish pypi --no-docs
|
|
410
|
+
bun run release --no-npm --no-local-publish
|
|
411
|
+
bun run release --publish local --install never
|
|
412
|
+
bun run release --local-registry false --local-pypi auto
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`--install auto` keeps Projen's normal local dependency-install behavior;
|
|
416
|
+
`always` performs a frozen install first, and `never` uses dependencies already
|
|
417
|
+
installed while still regenerating versioned sources. `--no-validation` skips
|
|
418
|
+
optional task checks locally and in CI, not version or immutable-source checks.
|
|
419
|
+
`--no-docs` skips building and deploying the site, not package README validation.
|
|
420
|
+
Scopes select artifacts, not the normal commit and tag-push transaction.
|
|
421
|
+
The annotated tag records CI step selections, so publishing a selected scope does
|
|
422
|
+
not change the next release's defaults. Local registry URLs stay local.
|
|
423
|
+
|
|
394
424
|
Members intentionally keep only the tasks that something OTHER than a human
|
|
395
425
|
invokes, so there is no second place to run the same thing:
|
|
396
426
|
|
|
@@ -402,7 +432,7 @@ invokes, so there is no second place to run the same thing:
|
|
|
402
432
|
edit/compile loop to one package.
|
|
403
433
|
- `build` / `package` - a complete compile/test/pack lifecycle when invoked in
|
|
404
434
|
one package. Release preparation validates through the root's filtered tasks,
|
|
405
|
-
and publication
|
|
435
|
+
and publication uploads archives that were each packed and
|
|
406
436
|
validated once with lifecycle scripts disabled. The package phase also packs
|
|
407
437
|
with `--ignore-scripts` because its build already compiled; `prepack` remains
|
|
408
438
|
available for a standalone publish that did not run `build` first.
|
|
@@ -421,3 +451,40 @@ from the root instead.
|
|
|
421
451
|
Projen-owned and reproduce it exactly across JavaScript and Python packages.
|
|
422
452
|
Release jobs publish the generated workspace inventory in dependency-safe
|
|
423
453
|
workflow order without introducing another version owner.
|
|
454
|
+
|
|
455
|
+
<!-- cli-reference:start -->
|
|
456
|
+
|
|
457
|
+
## Command Reference
|
|
458
|
+
|
|
459
|
+
### `release`
|
|
460
|
+
|
|
461
|
+
```text
|
|
462
|
+
Usage: release [options]
|
|
463
|
+
|
|
464
|
+
Prepare an annotated release and select its build and publication steps
|
|
465
|
+
|
|
466
|
+
Options:
|
|
467
|
+
--root <path> repository root
|
|
468
|
+
--branch <name> release branch (default: "main")
|
|
469
|
+
--prefix <prefix> release tag prefix (default: "v")
|
|
470
|
+
--remote <name> git remote (default: "origin")
|
|
471
|
+
--python-root <path> Python package root for local publish (default: "packages/py")
|
|
472
|
+
--validate <task> task to run before pushing (default: [])
|
|
473
|
+
--no-bump use an existing synchronized local version bump
|
|
474
|
+
--publish <target> publication scope (choices: "auto", "npm", "pypi", "local",
|
|
475
|
+
"none", default: "auto")
|
|
476
|
+
--install <mode> local workspace dependency installation (choices: "auto",
|
|
477
|
+
"always", "never", default: "auto")
|
|
478
|
+
--no-npm skip npm build and publication, including local npm publication
|
|
479
|
+
--no-pypi skip Python build and publication, including local Python
|
|
480
|
+
publication
|
|
481
|
+
--docs build and deploy docs for a selected scope
|
|
482
|
+
--no-docs skip documentation build and deployment
|
|
483
|
+
--no-validation skip optional release validation tasks; version/source checks
|
|
484
|
+
remain mandatory
|
|
485
|
+
--no-local-publish skip publishing to configured local registries
|
|
486
|
+
--local-registry <auto|false|url> local npm registry selection (default: "auto")
|
|
487
|
+
--local-pypi <auto|false|url> local devpi registry selection (default: "auto")
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
<!-- cli-reference:end -->
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
|
|
6
|
-
export const PACKAGE_VERSION = "0.9.
|
|
6
|
+
export const PACKAGE_VERSION = "0.9.40";
|
|
7
7
|
export * as barrels from "./src/barrels.ts";
|
|
8
8
|
export * as bunApp from "./src/bun-app.ts";
|
|
9
9
|
export * as bunWorkflow from "./src/bun-workflow.ts";
|
|
@@ -27,6 +27,7 @@ export * as pythonNodeBindings from "./src/python-node-bindings.ts";
|
|
|
27
27
|
export * as pythonNodeBundle from "./src/python-node-bundle.ts";
|
|
28
28
|
export * as release from "./src/release.ts";
|
|
29
29
|
export * as releaseContext from "./src/release-context.ts";
|
|
30
|
+
export * as releaseOptions from "./src/release-options.ts";
|
|
30
31
|
export * as scaffold from "./src/scaffold.ts";
|
|
31
32
|
export * as tags from "./src/tags.ts";
|
|
32
33
|
export * as tsconfig from "./src/tsconfig.ts";
|
|
@@ -60,7 +61,9 @@ export { PythonNodeBundle } from "./src/python-node-bundle.ts";
|
|
|
60
61
|
export type { PythonNodeFunctionOverride, PythonNodeBindingsOptions, PythonNodeBundleOptions } from "./src/python-node-bundle.ts";
|
|
61
62
|
export { DBXToolsRelease } from "./src/release.ts";
|
|
62
63
|
export type { ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
|
|
63
|
-
export {
|
|
64
|
+
export { RELEASE_VERSION } from "./src/release-context.ts";
|
|
65
|
+
export { RELEASE_PUBLISH_TARGETS, RELEASE_INSTALL_MODES } from "./src/release-options.ts";
|
|
66
|
+
export type { ReleasePublishTarget, ReleaseInstallMode, ReleaseStepSelection, ReleaseSelectionOptions } from "./src/release-options.ts";
|
|
64
67
|
export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
|
|
65
68
|
export type { PackageTag } from "./src/tags.ts";
|
|
66
69
|
export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
|
package/package.json
CHANGED
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@clack/prompts": "^1.7.0",
|
|
35
|
-
"@dbx-tools/core": "^0.9.
|
|
36
|
-
"@dbx-tools/path": "^0.9.
|
|
37
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
35
|
+
"@dbx-tools/core": "^0.9.40",
|
|
36
|
+
"@dbx-tools/path": "^0.9.40",
|
|
37
|
+
"@dbx-tools/shared-core": "^0.9.40",
|
|
38
38
|
"commander": "^15.0.0",
|
|
39
39
|
"concurrently": "^10.0.3",
|
|
40
40
|
"constructs": "^10.6.0",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"main": "index.ts",
|
|
58
58
|
"license": "Apache-2.0",
|
|
59
|
-
"version": "0.9.
|
|
59
|
+
"version": "0.9.40",
|
|
60
60
|
"types": "index.ts",
|
|
61
61
|
"type": "module",
|
|
62
62
|
"exports": {
|
package/src/project-js.ts
CHANGED
|
@@ -573,6 +573,8 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
|
|
|
573
573
|
readonly releasePythonRoot?: string;
|
|
574
574
|
/** Repository task names run during local release preparation before publication. */
|
|
575
575
|
readonly releaseValidationTasks?: readonly string[];
|
|
576
|
+
/** Release build prerequisites installed before repository validation tasks. */
|
|
577
|
+
readonly releaseSetupSteps?: readonly JobStep[];
|
|
576
578
|
/** Set to `false` to omit normal npm workspace publication. */
|
|
577
579
|
readonly nodeRelease?: boolean;
|
|
578
580
|
/** Unified dbx-tools release workflow, or no release surface. Defaults to `dbx-tools`. */
|
|
@@ -1365,6 +1367,7 @@ function initProject(
|
|
|
1365
1367
|
docs: options.releaseDocs,
|
|
1366
1368
|
pythonRoot: options.releasePythonRoot,
|
|
1367
1369
|
validationTasks: options.releaseValidationTasks,
|
|
1370
|
+
setupSteps: options.releaseSetupSteps,
|
|
1368
1371
|
});
|
|
1369
1372
|
}
|
|
1370
1373
|
}
|
package/src/project-py.ts
CHANGED
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
3
3
|
import { stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
|
|
4
4
|
import { Component, License, TextFile, type Project, javascript, python } from "projen";
|
|
5
|
-
import { JobPermission } from "projen/lib/github/workflows-model";
|
|
6
|
-
import { BUN_VERSION } from "./bun-workflow.ts";
|
|
5
|
+
import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
|
|
7
6
|
import { DBX_TOOLS_LICENSE, projectRepositoryUrl } from "./project-js.ts";
|
|
8
7
|
import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
|
|
9
8
|
import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
|
|
10
9
|
import { PythonNodeBundle, type PythonNodeBindingsOptions } from "./python-node-bundle.ts";
|
|
11
|
-
import { RELEASE_VERSION, releaseSourceSteps } from "./release-context.ts";
|
|
12
10
|
import {
|
|
13
11
|
refreshReleaseDocsDependencies,
|
|
14
12
|
releaseCondition,
|
|
13
|
+
releaseBuildSteps,
|
|
15
14
|
releaseTagPattern,
|
|
16
15
|
tryReleaseWorkflow,
|
|
17
16
|
uvSetupStep,
|
|
@@ -401,16 +400,45 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
401
400
|
if (!workflow) {
|
|
402
401
|
throw new Error("Python release requires the root dbx-tools release mode");
|
|
403
402
|
}
|
|
403
|
+
const build = workflow.getJob("build-release") as Job | undefined;
|
|
404
|
+
if (!build) throw new Error("Python release requires the shared release build job");
|
|
405
|
+
const pythonSteps: JobStep[] = [
|
|
406
|
+
uvSetupStep(),
|
|
407
|
+
{
|
|
408
|
+
name: "Build Python distributions",
|
|
409
|
+
env: { RELEASE_VERSION: "${{ steps.release.outputs.release_version }}" },
|
|
410
|
+
run: [
|
|
411
|
+
'bun node_modules/@dbx-tools/projen/tasks/publish-python.ts "$RELEASE_VERSION"',
|
|
412
|
+
`--root ${JSON.stringify(this.repository.root)}`,
|
|
413
|
+
`--package ${allPublications.map((publication) => JSON.stringify(publication.directory)).join(" ")}`,
|
|
414
|
+
"--output .release/python --package-directories",
|
|
415
|
+
].join(" \\\n "),
|
|
416
|
+
},
|
|
417
|
+
...allPublications.map((publication) => ({
|
|
418
|
+
name: `Upload ${publication.distribution} distributions`,
|
|
419
|
+
uses: "actions/upload-artifact@v4",
|
|
420
|
+
with: {
|
|
421
|
+
name: `release-python-${publication.directory}`,
|
|
422
|
+
path: `.release/python/${publication.directory}`,
|
|
423
|
+
"if-no-files-found": "error",
|
|
424
|
+
},
|
|
425
|
+
})),
|
|
426
|
+
];
|
|
427
|
+
workflow.updateJob("build-release", {
|
|
428
|
+
...build,
|
|
429
|
+
steps: [...build.steps, ...releaseBuildSteps("pypi", pythonSteps)],
|
|
430
|
+
});
|
|
404
431
|
for (const publication of allPublications) {
|
|
405
432
|
const dependencyJobs = (publication.dependencies ?? []).map(
|
|
406
433
|
(dependency) => `publish-pypi-${dependency}`,
|
|
407
434
|
);
|
|
408
435
|
workflow.addJob(`publish-pypi-${publication.directory}`, {
|
|
409
436
|
if: releaseCondition([
|
|
410
|
-
"needs.
|
|
437
|
+
"needs.build-release.result == 'success'",
|
|
438
|
+
"needs.build-release.outputs.pypi == 'true'",
|
|
411
439
|
...dependencyJobs.map((job) => `needs['${job}'].result == 'success'`),
|
|
412
440
|
]),
|
|
413
|
-
needs: ["
|
|
441
|
+
needs: ["build-release", ...dependencyJobs],
|
|
414
442
|
environment: {
|
|
415
443
|
name: publication.environment,
|
|
416
444
|
url:
|
|
@@ -420,20 +448,14 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
420
448
|
runsOn: ["ubuntu-latest"],
|
|
421
449
|
permissions: { idToken: JobPermission.WRITE },
|
|
422
450
|
timeoutMinutes: 10,
|
|
423
|
-
env: { BUN_VERSION },
|
|
424
451
|
steps: [
|
|
425
|
-
...releaseSourceSteps(),
|
|
426
|
-
...project.renderWorkflowSetup({ mutable: false }),
|
|
427
|
-
uvSetupStep(),
|
|
428
452
|
{
|
|
429
|
-
name: `
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
`--output dist/${publication.directory}`,
|
|
436
|
-
].join(" \\\n "),
|
|
453
|
+
name: `Download ${publication.distribution} distributions`,
|
|
454
|
+
uses: "actions/download-artifact@v4",
|
|
455
|
+
with: {
|
|
456
|
+
name: `release-python-${publication.directory}`,
|
|
457
|
+
path: `dist/${publication.directory}`,
|
|
458
|
+
},
|
|
437
459
|
},
|
|
438
460
|
{
|
|
439
461
|
name: `Publish ${publication.distribution} to PyPI`,
|
package/src/release-context.ts
CHANGED
|
@@ -1,34 +1,3 @@
|
|
|
1
1
|
/** Verified release context shared by every job in the unified workflow. */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export const RELEASE_TAG = "${{ needs.verify-context.outputs.release_tag }}";
|
|
6
|
-
export const RELEASE_SHA = "${{ needs.verify-context.outputs.expected_sha }}";
|
|
7
|
-
export const RELEASE_VERSION = "${{ needs.verify-context.outputs.release_version }}";
|
|
8
|
-
|
|
9
|
-
/** Check out and verify the immutable commit selected by the release tag. */
|
|
10
|
-
export function releaseSourceSteps(): readonly JobStep[] {
|
|
11
|
-
return [
|
|
12
|
-
github.WorkflowSteps.checkout({
|
|
13
|
-
name: "Checkout release commit",
|
|
14
|
-
with: {
|
|
15
|
-
ref: RELEASE_SHA,
|
|
16
|
-
fetchDepth: 1,
|
|
17
|
-
},
|
|
18
|
-
}),
|
|
19
|
-
{
|
|
20
|
-
name: "Verify release source",
|
|
21
|
-
shell: "bash",
|
|
22
|
-
env: {
|
|
23
|
-
RELEASE_TAG,
|
|
24
|
-
EXPECTED_SHA: RELEASE_SHA,
|
|
25
|
-
},
|
|
26
|
-
run: [
|
|
27
|
-
'git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"',
|
|
28
|
-
'test "$(git cat-file -t "$RELEASE_TAG")" = "tag"',
|
|
29
|
-
'test "$(git rev-parse "$RELEASE_TAG^{commit}")" = "$EXPECTED_SHA"',
|
|
30
|
-
'test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"',
|
|
31
|
-
].join("\n"),
|
|
32
|
-
},
|
|
33
|
-
];
|
|
34
|
-
}
|
|
2
|
+
/** Version verified by the shared build that produced every publication artifact. */
|
|
3
|
+
export const RELEASE_VERSION = "${{ needs.build-release.outputs.release_version }}";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-run release policy shared by the Projen task and tag-triggered workflow.
|
|
3
|
+
*
|
|
4
|
+
* This module owns defaults and the annotated-tag contract. Reuse these values
|
|
5
|
+
* when adding release steps so local flags and CI cannot drift. Registry URLs
|
|
6
|
+
* and local installation choices never belong in the public tag annotation.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { json } from "@dbx-tools/shared-core";
|
|
11
|
+
|
|
12
|
+
/** Publication scopes accepted by the release task; auto preserves normal publication. */
|
|
13
|
+
export const RELEASE_PUBLISH_TARGETS = ["auto", "npm", "pypi", "local", "none"] as const;
|
|
14
|
+
|
|
15
|
+
/** One publication scope accepted by the release task. */
|
|
16
|
+
export type ReleasePublishTarget = (typeof RELEASE_PUBLISH_TARGETS)[number];
|
|
17
|
+
|
|
18
|
+
/** Native Projen dependency-install modes available during local release preparation. */
|
|
19
|
+
export const RELEASE_INSTALL_MODES = ["auto", "always", "never"] as const;
|
|
20
|
+
|
|
21
|
+
/** Local dependency-install behavior; auto retains Projen's normal trigger policy. */
|
|
22
|
+
export type ReleaseInstallMode = (typeof RELEASE_INSTALL_MODES)[number];
|
|
23
|
+
|
|
24
|
+
/** CI steps selected by an immutable release annotation. */
|
|
25
|
+
export interface ReleaseStepSelection {
|
|
26
|
+
readonly npm: boolean;
|
|
27
|
+
readonly pypi: boolean;
|
|
28
|
+
readonly docs: boolean;
|
|
29
|
+
readonly validation: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Flags consumed by the shared release-selection policy. */
|
|
33
|
+
export interface ReleaseSelectionOptions {
|
|
34
|
+
readonly publish?: ReleasePublishTarget;
|
|
35
|
+
readonly npm?: boolean;
|
|
36
|
+
readonly pypi?: boolean;
|
|
37
|
+
readonly docs?: boolean;
|
|
38
|
+
readonly validation?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ANNOTATION_MARKER = "dbx-tools-release:";
|
|
42
|
+
|
|
43
|
+
/** Derive public build/publication steps without mutating repository configuration. */
|
|
44
|
+
export function releaseStepSelection(options: ReleaseSelectionOptions = {}): ReleaseStepSelection {
|
|
45
|
+
const target = options.publish ?? "auto";
|
|
46
|
+
return {
|
|
47
|
+
npm: (target === "auto" || target === "npm") && options.npm !== false,
|
|
48
|
+
pypi: (target === "auto" || target === "pypi") && options.pypi !== false,
|
|
49
|
+
docs: options.docs ?? (target !== "local" && target !== "none"),
|
|
50
|
+
validation: options.validation ?? true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Record non-default CI selections alongside the existing annotated release tag. */
|
|
55
|
+
export function releaseTagAnnotation(tag: string, selection: ReleaseStepSelection): string {
|
|
56
|
+
if (selection.npm && selection.pypi && selection.docs && selection.validation) return tag;
|
|
57
|
+
return `${tag}\n\n${ANNOTATION_MARKER} ${JSON.stringify(selection)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read the release selection, rejecting malformed or ambiguous annotated policy. */
|
|
61
|
+
export function parseReleaseTagAnnotation(annotation: string): ReleaseStepSelection {
|
|
62
|
+
const selections = annotation.split("\n").filter((line) => line.startsWith(ANNOTATION_MARKER));
|
|
63
|
+
if (selections.length === 0) return releaseStepSelection();
|
|
64
|
+
if (selections.length !== 1) throw new Error("Release tag contains multiple step selections");
|
|
65
|
+
const value = json.parseRecord(selections[0]!.slice(ANNOTATION_MARKER.length));
|
|
66
|
+
if (
|
|
67
|
+
!value ||
|
|
68
|
+
Object.keys(value).length !== 4 ||
|
|
69
|
+
typeof value.npm !== "boolean" ||
|
|
70
|
+
typeof value.pypi !== "boolean" ||
|
|
71
|
+
typeof value.docs !== "boolean" ||
|
|
72
|
+
typeof value.validation !== "boolean"
|
|
73
|
+
) {
|
|
74
|
+
throw new Error("Invalid release tag step selection");
|
|
75
|
+
}
|
|
76
|
+
return { npm: value.npm, pypi: value.pypi, docs: value.docs, validation: value.validation };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Whether the selected release should inspect and publish configured local registries. */
|
|
80
|
+
export function releasePublishesLocally(target: ReleasePublishTarget = "auto"): boolean {
|
|
81
|
+
return target === "auto" || target === "local";
|
|
82
|
+
}
|
package/src/release.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { GithubWorkflow } from "projen/lib/github";
|
|
|
5
5
|
import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
|
|
6
6
|
import { BUN_VERSION } from "./bun-workflow.ts";
|
|
7
7
|
import { projectReleaseBranch, taskCommand, type DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
8
|
-
import { RELEASE_VERSION
|
|
8
|
+
import { RELEASE_VERSION } from "./release-context.ts";
|
|
9
|
+
import type { ReleaseStepSelection } from "./release-options.ts";
|
|
9
10
|
|
|
10
11
|
const NODE_VERSION = "24";
|
|
11
12
|
const NPM_VERSION = "11.4.2";
|
|
@@ -17,7 +18,7 @@ const releaseWorkflows = new WeakMap<DBXToolsJavaScriptProject, GithubWorkflow>(
|
|
|
17
18
|
export interface ReleaseDocsOptions {
|
|
18
19
|
readonly siteUrl: string;
|
|
19
20
|
readonly base?: string;
|
|
20
|
-
/**
|
|
21
|
+
/** Documentation generation steps run in the shared release build after validation. */
|
|
21
22
|
readonly prepareSteps: readonly JobStep[];
|
|
22
23
|
/** Repository-defined documentation build and validation steps. */
|
|
23
24
|
readonly buildSteps: readonly JobStep[];
|
|
@@ -37,6 +38,8 @@ export interface DBXToolsReleaseOptions {
|
|
|
37
38
|
readonly pythonRoot?: string;
|
|
38
39
|
/** Repository task names run after VERSION verification and before publication. */
|
|
39
40
|
readonly validationTasks?: readonly string[];
|
|
41
|
+
/** Repository prerequisites installed before release validation and artifact builds. */
|
|
42
|
+
readonly setupSteps?: readonly JobStep[];
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/** Locate the unified workflow when release generation is enabled. */
|
|
@@ -44,7 +47,7 @@ export function tryReleaseWorkflow(project: DBXToolsJavaScriptProject): GithubWo
|
|
|
44
47
|
return releaseWorkflows.get(project);
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
/** Tag pattern
|
|
50
|
+
/** Tag pattern accepted by the unified release workflow. */
|
|
48
51
|
export function releaseTagPattern(project: DBXToolsJavaScriptProject): string {
|
|
49
52
|
const prefix = releaseTagPrefixes.get(project);
|
|
50
53
|
if (!prefix) throw new Error("Release workflow is not configured");
|
|
@@ -57,11 +60,25 @@ export function releaseCondition(prerequisites: readonly string[] = []): string
|
|
|
57
60
|
return `\${{ always() && ${prerequisites.map((condition) => `(${condition})`).join(" && ")} }}`;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
/**
|
|
61
|
-
export function
|
|
63
|
+
/** Gate shared-build steps using the same per-run flag, preserving their existing conditions. */
|
|
64
|
+
export function releaseBuildSteps(
|
|
65
|
+
flag: keyof ReleaseStepSelection,
|
|
66
|
+
steps: readonly JobStep[],
|
|
67
|
+
): JobStep[] {
|
|
68
|
+
return steps.map((step) => ({
|
|
69
|
+
...step,
|
|
70
|
+
if: `\${{ success() && steps.release.outputs.${flag} == 'true'${step.if ? ` && (${step.if.replace(/^\s*\$\{\{\s*|\s*\}\}\s*$/g, "")})` : ""} }}`,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Artifact-only publication runtime and npm authentication; no workspace installation. */
|
|
75
|
+
export function nodeReleaseSetupSteps(): readonly JobStep[] {
|
|
62
76
|
return [
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
{
|
|
78
|
+
name: "Setup Bun",
|
|
79
|
+
uses: "oven-sh/setup-bun@v2",
|
|
80
|
+
with: { "bun-version": BUN_VERSION },
|
|
81
|
+
},
|
|
65
82
|
{
|
|
66
83
|
name: "Setup Node.js",
|
|
67
84
|
uses: "actions/setup-node@v6",
|
|
@@ -88,13 +105,23 @@ export function uvSetupStep(): JobStep {
|
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
function refreshDocsRegistryDependencies(workflow: GithubWorkflow): void {
|
|
91
|
-
const docs = workflow.getJob("
|
|
108
|
+
const docs = workflow.getJob("deploy-docs") as Job | undefined;
|
|
92
109
|
if (!docs) return;
|
|
93
110
|
const registryJobs = Object.keys(workflow.jobs).filter(
|
|
94
111
|
(name) => name === "publish-node" || name.startsWith("publish-pypi-"),
|
|
95
112
|
);
|
|
96
113
|
if (registryJobs.length > 0) {
|
|
97
|
-
workflow.updateJob("
|
|
114
|
+
workflow.updateJob("deploy-docs", {
|
|
115
|
+
...docs,
|
|
116
|
+
needs: ["build-release", ...registryJobs],
|
|
117
|
+
if: releaseCondition([
|
|
118
|
+
"needs['build-release'].result == 'success'",
|
|
119
|
+
"needs['build-release'].outputs.docs == 'true'",
|
|
120
|
+
...registryJobs.map(
|
|
121
|
+
(name) => `needs['${name}'].result == 'success' || needs['${name}'].result == 'skipped'`,
|
|
122
|
+
),
|
|
123
|
+
]),
|
|
124
|
+
});
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
127
|
|
|
@@ -106,7 +133,7 @@ export function refreshReleaseDocsDependencies(project: DBXToolsJavaScriptProjec
|
|
|
106
133
|
}
|
|
107
134
|
}
|
|
108
135
|
|
|
109
|
-
function
|
|
136
|
+
function releaseBuildJob(
|
|
110
137
|
project: DBXToolsJavaScriptProject,
|
|
111
138
|
tagPrefix: string,
|
|
112
139
|
releaseBranch: string,
|
|
@@ -115,12 +142,21 @@ function verifyContextJob(
|
|
|
115
142
|
return {
|
|
116
143
|
runsOn: ["ubuntu-latest"],
|
|
117
144
|
permissions: { contents: JobPermission.READ },
|
|
118
|
-
timeoutMinutes:
|
|
119
|
-
env: {
|
|
145
|
+
timeoutMinutes: 60,
|
|
146
|
+
env: {
|
|
147
|
+
BUN_VERSION,
|
|
148
|
+
CI: "true",
|
|
149
|
+
...(options.docs
|
|
150
|
+
? { DOCS_SITE_URL: options.docs.siteUrl, DOCS_BASE: options.docs.base ?? "/" }
|
|
151
|
+
: {}),
|
|
152
|
+
},
|
|
120
153
|
outputs: {
|
|
121
154
|
release_tag: { stepId: "release", outputName: "release_tag" },
|
|
122
155
|
expected_sha: { stepId: "release", outputName: "expected_sha" },
|
|
123
156
|
release_version: { stepId: "release", outputName: "release_version" },
|
|
157
|
+
npm: { stepId: "release", outputName: "npm" },
|
|
158
|
+
pypi: { stepId: "release", outputName: "pypi" },
|
|
159
|
+
docs: { stepId: "release", outputName: "docs" },
|
|
124
160
|
},
|
|
125
161
|
steps: [
|
|
126
162
|
github.WorkflowSteps.checkout({
|
|
@@ -155,28 +191,70 @@ function verifyContextJob(
|
|
|
155
191
|
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
|
156
192
|
echo "expected_sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
|
|
157
193
|
echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
|
|
194
|
+
bun node_modules/@dbx-tools/projen/tasks/release-options.ts --tag "$RELEASE_TAG" --output "$GITHUB_OUTPUT"
|
|
158
195
|
`
|
|
159
196
|
// ============================================================================
|
|
160
197
|
),
|
|
161
198
|
},
|
|
162
|
-
...(options.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
199
|
+
...(options.setupSteps ?? []),
|
|
200
|
+
...releaseBuildSteps(
|
|
201
|
+
"validation",
|
|
202
|
+
(options.validationTasks ?? []).map((task) => ({
|
|
203
|
+
name: `Validate ${task}`,
|
|
204
|
+
run: `bun run ${task}`,
|
|
205
|
+
})),
|
|
206
|
+
),
|
|
207
|
+
...(options.nodeRelease === false
|
|
208
|
+
? []
|
|
209
|
+
: releaseBuildSteps("npm", [
|
|
210
|
+
{
|
|
211
|
+
name: "Build npm archives",
|
|
212
|
+
env: { RELEASE_VERSION: "${{ steps.release.outputs.release_version }}" },
|
|
213
|
+
run: [
|
|
214
|
+
"bun run compile",
|
|
215
|
+
'bun node_modules/@dbx-tools/projen/tasks/publish.ts "$RELEASE_VERSION" --skip-compile --output .release/npm',
|
|
216
|
+
"bun build node_modules/@dbx-tools/projen/tasks/publish-npm.ts --target=bun --outfile=.release/npm/publish-npm.mjs",
|
|
217
|
+
].join("\n"),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "Upload npm archives",
|
|
221
|
+
uses: "actions/upload-artifact@v4",
|
|
222
|
+
with: { name: "release-npm", path: ".release/npm", "if-no-files-found": "error" },
|
|
223
|
+
},
|
|
224
|
+
])),
|
|
225
|
+
...(options.docs
|
|
226
|
+
? releaseBuildSteps("docs", [
|
|
227
|
+
...options.docs.prepareSteps,
|
|
228
|
+
...options.docs.buildSteps,
|
|
229
|
+
{
|
|
230
|
+
name: "Upload Pages artifact",
|
|
231
|
+
uses: "actions/upload-pages-artifact@v4",
|
|
232
|
+
with: { path: options.docs.artifactPath },
|
|
233
|
+
},
|
|
234
|
+
])
|
|
235
|
+
: []),
|
|
166
236
|
],
|
|
167
237
|
};
|
|
168
238
|
}
|
|
169
239
|
|
|
170
|
-
function nodePublishJob(
|
|
240
|
+
function nodePublishJob(): Job {
|
|
171
241
|
return {
|
|
172
|
-
if: releaseCondition(
|
|
173
|
-
|
|
242
|
+
if: releaseCondition([
|
|
243
|
+
"needs['build-release'].result == 'success'",
|
|
244
|
+
"needs['build-release'].outputs.npm == 'true'",
|
|
245
|
+
]),
|
|
246
|
+
needs: ["build-release"],
|
|
174
247
|
runsOn: ["ubuntu-latest"],
|
|
175
248
|
permissions: { contents: JobPermission.READ, idToken: JobPermission.WRITE },
|
|
176
249
|
timeoutMinutes: 30,
|
|
177
250
|
env: { BUN_VERSION, CI: "true" },
|
|
178
251
|
steps: [
|
|
179
|
-
|
|
252
|
+
{
|
|
253
|
+
name: "Download npm archives",
|
|
254
|
+
uses: "actions/download-artifact@v4",
|
|
255
|
+
with: { name: "release-npm", path: ".release/npm" },
|
|
256
|
+
},
|
|
257
|
+
...nodeReleaseSetupSteps(),
|
|
180
258
|
{
|
|
181
259
|
name: "Publish npm workspace",
|
|
182
260
|
env: {
|
|
@@ -184,47 +262,19 @@ function nodePublishJob(project: DBXToolsJavaScriptProject): Job {
|
|
|
184
262
|
NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}",
|
|
185
263
|
NPM_CONFIG_PROVENANCE: "true",
|
|
186
264
|
},
|
|
187
|
-
run: 'bun
|
|
265
|
+
run: 'bun .release/npm/publish-npm.mjs --directory .release/npm --version "$RELEASE_VERSION"',
|
|
188
266
|
},
|
|
189
267
|
],
|
|
190
268
|
};
|
|
191
269
|
}
|
|
192
270
|
|
|
193
|
-
function addDocsJobs(
|
|
194
|
-
workflow: GithubWorkflow,
|
|
195
|
-
project: DBXToolsJavaScriptProject,
|
|
196
|
-
options: ReleaseDocsOptions,
|
|
197
|
-
): void {
|
|
198
|
-
workflow.addJob("build-docs", {
|
|
199
|
-
if: releaseCondition(),
|
|
200
|
-
needs: ["verify-context"],
|
|
201
|
-
runsOn: ["ubuntu-latest"],
|
|
202
|
-
permissions: {
|
|
203
|
-
contents: JobPermission.READ,
|
|
204
|
-
pages: JobPermission.WRITE,
|
|
205
|
-
idToken: JobPermission.WRITE,
|
|
206
|
-
},
|
|
207
|
-
timeoutMinutes: 30,
|
|
208
|
-
env: {
|
|
209
|
-
BUN_VERSION,
|
|
210
|
-
DOCS_SITE_URL: options.siteUrl,
|
|
211
|
-
DOCS_BASE: options.base ?? "/",
|
|
212
|
-
},
|
|
213
|
-
steps: [
|
|
214
|
-
...releaseSourceSteps(),
|
|
215
|
-
...project.renderWorkflowSetup({ mutable: false }),
|
|
216
|
-
...options.prepareSteps,
|
|
217
|
-
...options.buildSteps,
|
|
218
|
-
{
|
|
219
|
-
name: "Upload Pages artifact",
|
|
220
|
-
uses: "actions/upload-pages-artifact@v4",
|
|
221
|
-
with: { path: options.artifactPath },
|
|
222
|
-
},
|
|
223
|
-
],
|
|
224
|
-
});
|
|
271
|
+
function addDocsJobs(workflow: GithubWorkflow): void {
|
|
225
272
|
workflow.addJob("deploy-docs", {
|
|
226
|
-
if: releaseCondition(
|
|
227
|
-
|
|
273
|
+
if: releaseCondition([
|
|
274
|
+
"needs['build-release'].result == 'success'",
|
|
275
|
+
"needs['build-release'].outputs.docs == 'true'",
|
|
276
|
+
]),
|
|
277
|
+
needs: ["build-release"],
|
|
228
278
|
environment: {
|
|
229
279
|
name: "github-pages",
|
|
230
280
|
url: "${{ steps.deployment.outputs.page_url }}",
|
|
@@ -247,6 +297,11 @@ function addDocsJobs(
|
|
|
247
297
|
export class DBXToolsRelease extends Component {
|
|
248
298
|
constructor(project: DBXToolsJavaScriptProject, options: DBXToolsReleaseOptions = {}) {
|
|
249
299
|
super(project);
|
|
300
|
+
project.addGitIgnore(".release/");
|
|
301
|
+
const installCondition =
|
|
302
|
+
'node -e "process.exit(process.env.DBX_TOOLS_RELEASE_INSTALL === \'never\' ? 1 : 0)"';
|
|
303
|
+
project.package.installTask.addCondition(installCondition);
|
|
304
|
+
project.package.installCiTask.addCondition(installCondition);
|
|
250
305
|
const tagPrefix = options.tagPrefix ?? "v";
|
|
251
306
|
const releaseBranch = projectReleaseBranch(project);
|
|
252
307
|
releaseTagPrefixes.set(project, tagPrefix);
|
|
@@ -290,12 +345,12 @@ export class DBXToolsRelease extends Component {
|
|
|
290
345
|
push: { tags: [`${tagPrefix}*`] },
|
|
291
346
|
});
|
|
292
347
|
workflow.file?.addOverride("permissions.contents", "read");
|
|
293
|
-
workflow.addJob("
|
|
348
|
+
workflow.addJob("build-release", releaseBuildJob(project, tagPrefix, releaseBranch, options));
|
|
294
349
|
if (options.nodeRelease !== false) {
|
|
295
|
-
workflow.addJob("publish-node", nodePublishJob(
|
|
350
|
+
workflow.addJob("publish-node", nodePublishJob());
|
|
296
351
|
}
|
|
297
352
|
if (options.docs) {
|
|
298
|
-
addDocsJobs(workflow
|
|
353
|
+
addDocsJobs(workflow);
|
|
299
354
|
}
|
|
300
355
|
refreshReleaseDocsDependencies(project);
|
|
301
356
|
}
|
|
@@ -94,6 +94,8 @@ export function buildPythonProjects(options: {
|
|
|
94
94
|
readonly allowEmpty?: boolean;
|
|
95
95
|
readonly output: string;
|
|
96
96
|
readonly packages?: readonly string[];
|
|
97
|
+
/** Place each package's artifacts in its directory beneath output. */
|
|
98
|
+
readonly packageDirectories?: boolean;
|
|
97
99
|
readonly root: string;
|
|
98
100
|
readonly version: string;
|
|
99
101
|
}): void {
|
|
@@ -136,9 +138,15 @@ export function buildPythonProjects(options: {
|
|
|
136
138
|
version: options.version,
|
|
137
139
|
}),
|
|
138
140
|
);
|
|
139
|
-
|
|
141
|
+
const packageOutput = options.packageDirectories ? join(output, project.directory) : output;
|
|
142
|
+
runTaskCommand(root, "uv", ["build", "--out-dir", packageOutput, packageRoot]);
|
|
143
|
+
if (pythonDistributionPaths(packageOutput).length === 0) {
|
|
144
|
+
throw new Error(`No Python distributions found for ${project.name}`);
|
|
145
|
+
}
|
|
140
146
|
}
|
|
141
|
-
const distributions =
|
|
147
|
+
const distributions = options.packageDirectories
|
|
148
|
+
? projects.flatMap((project) => pythonDistributionPaths(join(output, project.directory)))
|
|
149
|
+
: pythonDistributionPaths(output);
|
|
142
150
|
if (distributions.length === 0) {
|
|
143
151
|
throw new Error(`No Python distributions found in ${output}`);
|
|
144
152
|
}
|
|
@@ -157,6 +165,10 @@ export async function main(): Promise<void> {
|
|
|
157
165
|
.option("--root <path>", "Python workspace package root", "packages/py")
|
|
158
166
|
.option("--package <directory...>", "Build only selected package directories")
|
|
159
167
|
.option("--output <path>", "Build distributions into a directory without publishing")
|
|
168
|
+
.option(
|
|
169
|
+
"--package-directories",
|
|
170
|
+
"Keep each package's distributions in its own output directory",
|
|
171
|
+
)
|
|
160
172
|
.option("--dry-run", "build and inspect distributions without uploading")
|
|
161
173
|
.action(
|
|
162
174
|
(
|
|
@@ -165,6 +177,7 @@ export async function main(): Promise<void> {
|
|
|
165
177
|
dryRun?: boolean;
|
|
166
178
|
indexUrl?: string;
|
|
167
179
|
package?: string[];
|
|
180
|
+
packageDirectories?: boolean;
|
|
168
181
|
publishUrl?: string;
|
|
169
182
|
root: string;
|
|
170
183
|
output?: string;
|
|
@@ -174,6 +187,7 @@ export async function main(): Promise<void> {
|
|
|
174
187
|
buildPythonProjects({
|
|
175
188
|
output: options.output,
|
|
176
189
|
packages: options.package,
|
|
190
|
+
packageDirectories: options.packageDirectories,
|
|
177
191
|
root: options.root,
|
|
178
192
|
version,
|
|
179
193
|
});
|
package/tasks/lib/release.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/** Run the configured release transaction. */
|
|
3
3
|
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
4
4
|
import { log } from "@dbx-tools/shared-core";
|
|
5
|
-
import { Command } from "commander";
|
|
5
|
+
import { Command, Option } from "commander";
|
|
6
6
|
import { publishLocalRelease } from "./local-publish.ts";
|
|
7
7
|
import { assertReleaseVersion } from "./release-version.ts";
|
|
8
8
|
import {
|
|
@@ -11,6 +11,15 @@ import {
|
|
|
11
11
|
taskCommandSucceeds,
|
|
12
12
|
} from "../../src/_task-command.ts";
|
|
13
13
|
import { readWorkspaceVersion } from "../../src/workspace-version.ts";
|
|
14
|
+
import {
|
|
15
|
+
RELEASE_INSTALL_MODES,
|
|
16
|
+
RELEASE_PUBLISH_TARGETS,
|
|
17
|
+
releasePublishesLocally,
|
|
18
|
+
releaseStepSelection,
|
|
19
|
+
releaseTagAnnotation,
|
|
20
|
+
type ReleaseInstallMode,
|
|
21
|
+
type ReleaseSelectionOptions,
|
|
22
|
+
} from "../../src/release-options.ts";
|
|
14
23
|
|
|
15
24
|
const logger = log.logger("projen:release");
|
|
16
25
|
|
|
@@ -87,23 +96,34 @@ function prepareReleaseBranch(options: {
|
|
|
87
96
|
runTaskCommand(root, "git", ["push", remote, `HEAD:${branch}`]);
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
export async function runRelease(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
export async function runRelease(
|
|
100
|
+
options: ReleaseSelectionOptions & {
|
|
101
|
+
readonly root: string;
|
|
102
|
+
readonly branch: string;
|
|
103
|
+
readonly bump?: boolean;
|
|
104
|
+
readonly prefix: string;
|
|
105
|
+
readonly remote: string;
|
|
106
|
+
readonly localPublish?: boolean;
|
|
107
|
+
readonly localRegistry?: string;
|
|
108
|
+
readonly localPypi?: string;
|
|
109
|
+
readonly install?: ReleaseInstallMode;
|
|
110
|
+
readonly pythonRoot?: string;
|
|
111
|
+
readonly validationTasks?: readonly string[];
|
|
112
|
+
},
|
|
113
|
+
): Promise<string> {
|
|
100
114
|
const { branch, prefix, remote, root } = options;
|
|
115
|
+
const selection = releaseStepSelection(options);
|
|
116
|
+
const install = options.install ?? "auto";
|
|
117
|
+
|
|
118
|
+
if (install === "always") runTaskCommand(root, "bun", ["install", "--frozen-lockfile"]);
|
|
101
119
|
|
|
102
120
|
prepareReleaseBranch({ branch, remote, root });
|
|
103
121
|
|
|
104
122
|
runTaskCommand(root, "git", ["fetch", remote, branch, "--tags"]);
|
|
105
123
|
if (options.bump ?? true) {
|
|
106
|
-
runTaskCommand(root, "bun", ["run", "bump"]
|
|
124
|
+
runTaskCommand(root, "bun", ["run", "bump"], {
|
|
125
|
+
env: { ...process.env, DBX_TOOLS_RELEASE_INSTALL: install === "auto" ? "auto" : "never" },
|
|
126
|
+
});
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
const version = readWorkspaceVersion(root);
|
|
@@ -115,7 +135,7 @@ export async function runRelease(options: {
|
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
runTaskCommand(root, "bun", ["run", "version:check"]);
|
|
118
|
-
for (const task of options.validationTasks ?? []) {
|
|
138
|
+
for (const task of selection.validation ? (options.validationTasks ?? []) : []) {
|
|
119
139
|
runTaskCommand(root, "bun", ["run", task]);
|
|
120
140
|
}
|
|
121
141
|
const status = captureTaskCommand(
|
|
@@ -145,7 +165,13 @@ export async function runRelease(options: {
|
|
|
145
165
|
);
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
runTaskCommand(root, "git", [
|
|
168
|
+
runTaskCommand(root, "git", [
|
|
169
|
+
"tag",
|
|
170
|
+
"--annotate",
|
|
171
|
+
tag,
|
|
172
|
+
"--message",
|
|
173
|
+
releaseTagAnnotation(tag, selection),
|
|
174
|
+
]);
|
|
149
175
|
try {
|
|
150
176
|
runTaskCommand(root, "git", ["push", "--atomic", remote, `HEAD:${branch}`, `refs/tags/${tag}`]);
|
|
151
177
|
} catch (error) {
|
|
@@ -157,10 +183,10 @@ export async function runRelease(options: {
|
|
|
157
183
|
// Restore local deploys: when a non-standard (loopback) npm or uv registry is
|
|
158
184
|
// configured, publish the freshly tagged version to it directly. Detection and
|
|
159
185
|
// publishing are owned by local-publish.ts; this no-ops on standard registries.
|
|
160
|
-
if (options.localPublish ?? true) {
|
|
186
|
+
if ((options.localPublish ?? true) && releasePublishesLocally(options.publish)) {
|
|
161
187
|
await publishLocalRelease({
|
|
162
|
-
localPypi: "auto",
|
|
163
|
-
localRegistry: "auto",
|
|
188
|
+
localPypi: options.pypi === false ? "false" : (options.localPypi ?? "auto"),
|
|
189
|
+
localRegistry: options.npm === false ? "false" : (options.localRegistry ?? "auto"),
|
|
164
190
|
pythonRoot: options.pythonRoot ?? "packages/py",
|
|
165
191
|
root,
|
|
166
192
|
version,
|
|
@@ -170,8 +196,11 @@ export async function runRelease(options: {
|
|
|
170
196
|
return tag;
|
|
171
197
|
}
|
|
172
198
|
|
|
173
|
-
|
|
174
|
-
|
|
199
|
+
/** Build the release task's native parser without executing git or registry operations. */
|
|
200
|
+
export function createReleaseCommand(): Command {
|
|
201
|
+
return new Command()
|
|
202
|
+
.name("release")
|
|
203
|
+
.description("Prepare an annotated release and select its build and publication steps")
|
|
175
204
|
.option("--root <path>", "repository root")
|
|
176
205
|
.option("--branch <name>", "release branch", "main")
|
|
177
206
|
.option("--prefix <prefix>", "release tag prefix", "v")
|
|
@@ -184,18 +213,43 @@ export async function main(): Promise<void> {
|
|
|
184
213
|
[],
|
|
185
214
|
)
|
|
186
215
|
.option("--no-bump", "use an existing synchronized local version bump")
|
|
216
|
+
.addOption(
|
|
217
|
+
new Option("--publish <target>", "publication scope")
|
|
218
|
+
.choices([...RELEASE_PUBLISH_TARGETS])
|
|
219
|
+
.default("auto"),
|
|
220
|
+
)
|
|
221
|
+
.addOption(
|
|
222
|
+
new Option("--install <mode>", "local workspace dependency installation")
|
|
223
|
+
.choices([...RELEASE_INSTALL_MODES])
|
|
224
|
+
.default("auto"),
|
|
225
|
+
)
|
|
226
|
+
.option("--no-npm", "skip npm build and publication, including local npm publication")
|
|
227
|
+
.option("--no-pypi", "skip Python build and publication, including local Python publication")
|
|
228
|
+
.option("--docs", "build and deploy docs for a selected scope")
|
|
229
|
+
.option("--no-docs", "skip documentation build and deployment")
|
|
230
|
+
.option(
|
|
231
|
+
"--no-validation",
|
|
232
|
+
"skip optional release validation tasks; version/source checks remain mandatory",
|
|
233
|
+
)
|
|
187
234
|
.option("--no-local-publish", "skip publishing to configured local registries")
|
|
235
|
+
.option("--local-registry <auto|false|url>", "local npm registry selection", "auto")
|
|
236
|
+
.option("--local-pypi <auto|false|url>", "local devpi registry selection", "auto")
|
|
188
237
|
.action(
|
|
189
|
-
async (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
async (
|
|
239
|
+
options: ReleaseSelectionOptions & {
|
|
240
|
+
root?: string;
|
|
241
|
+
branch: string;
|
|
242
|
+
bump: boolean;
|
|
243
|
+
prefix: string;
|
|
244
|
+
remote: string;
|
|
245
|
+
pythonRoot: string;
|
|
246
|
+
localPublish: boolean;
|
|
247
|
+
localRegistry: string;
|
|
248
|
+
localPypi: string;
|
|
249
|
+
install: ReleaseInstallMode;
|
|
250
|
+
validate: string[];
|
|
251
|
+
},
|
|
252
|
+
) => {
|
|
199
253
|
await runRelease({
|
|
200
254
|
root: options.root ?? projectUtils.root() ?? process.cwd(),
|
|
201
255
|
branch: options.branch,
|
|
@@ -204,9 +258,21 @@ export async function main(): Promise<void> {
|
|
|
204
258
|
remote: options.remote,
|
|
205
259
|
pythonRoot: options.pythonRoot,
|
|
206
260
|
localPublish: options.localPublish,
|
|
261
|
+
localRegistry: options.localRegistry,
|
|
262
|
+
localPypi: options.localPypi,
|
|
263
|
+
install: options.install,
|
|
264
|
+
publish: options.publish,
|
|
265
|
+
npm: options.npm,
|
|
266
|
+
pypi: options.pypi,
|
|
267
|
+
docs: options.docs,
|
|
268
|
+
validation: options.validation,
|
|
207
269
|
validationTasks: options.validate,
|
|
208
270
|
});
|
|
209
271
|
},
|
|
210
|
-
)
|
|
211
|
-
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Execute the native release task parser. */
|
|
276
|
+
export async function main(): Promise<void> {
|
|
277
|
+
await createReleaseCommand().parseAsync();
|
|
212
278
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Export the current annotated release tag's step selection to GitHub Actions. */
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { captureTaskCommand } from "../src/_task-command.ts";
|
|
6
|
+
import { parseReleaseTagAnnotation } from "../src/release-options.ts";
|
|
7
|
+
|
|
8
|
+
/** Verify and export a tag policy using the shared annotated-tag contract. */
|
|
9
|
+
export function main(args: string[] = process.argv.slice(2)): void {
|
|
10
|
+
const { values } = parseArgs({
|
|
11
|
+
args,
|
|
12
|
+
options: { tag: { type: "string" }, output: { type: "string" } },
|
|
13
|
+
strict: true,
|
|
14
|
+
});
|
|
15
|
+
if (!values.tag || !values.output) throw new Error("--tag and --output are required");
|
|
16
|
+
const annotation = captureTaskCommand(
|
|
17
|
+
process.cwd(),
|
|
18
|
+
"git",
|
|
19
|
+
["for-each-ref", "--format=%(contents)", `refs/tags/${values.tag}`],
|
|
20
|
+
{ check: true },
|
|
21
|
+
);
|
|
22
|
+
if (!annotation) throw new Error(`Release tag ${values.tag} has no annotation`);
|
|
23
|
+
const selection = parseReleaseTagAnnotation(annotation);
|
|
24
|
+
appendFileSync(
|
|
25
|
+
values.output,
|
|
26
|
+
Object.entries(selection)
|
|
27
|
+
.map(([name, enabled]) => `${name}=${enabled}\n`)
|
|
28
|
+
.join(""),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (import.meta.main) main();
|
package/tasks/release.ts
CHANGED