@dbx-tools/projen 0.9.158 → 0.9.161
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 +14 -8
- package/index.ts +8 -5
- package/package.json +6 -6
- package/src/npm-release.ts +111 -3
- package/src/project-js.ts +9 -2
- package/src/release-context.ts +4 -0
- package/src/release.ts +3 -2
- package/src/workspace-lock.ts +28 -6
- package/src/workspace-vendor.ts +103 -0
- package/tasks/appkit-deploy.ts +8 -0
- package/tasks/publish-npm.ts +9 -16
- package/tasks/publish.ts +16 -25
- package/tasks/release.ts +6 -1
- package/tasks/verify-publications.ts +8 -3
package/README.md
CHANGED
|
@@ -340,9 +340,12 @@ to `appkit:deploy` unless the repository already owns `deploy`. The AppKit task
|
|
|
340
340
|
runs `databricks apps deploy --skip-validation` with all forwarded arguments
|
|
341
341
|
from an isolated source snapshot, so the Databricks CLI never runs npm locally.
|
|
342
342
|
Because the remote builder selects npm when no portable pnpm lockfile is
|
|
343
|
-
uploaded, the snapshot replaces `catalog:`
|
|
344
|
-
|
|
345
|
-
|
|
343
|
+
uploaded, the snapshot replaces `catalog:` protocols with concrete versions.
|
|
344
|
+
When the application uses `workspace:` dependencies, the task packs their
|
|
345
|
+
transitive runtime workspace closure and links those archives from the staged
|
|
346
|
+
application, so deployment does not wait for a matching public release. Every
|
|
347
|
+
source manifest stays stable through upload and the generated working tree is
|
|
348
|
+
never modified. The task passes an explicit profile,
|
|
346
349
|
using the supplied flag or resolving one through `@dbx-tools/auth`, and fails
|
|
347
350
|
before staging when neither profile nor complete environment credentials are
|
|
348
351
|
available. It validates the bundle strictly, removes only stale direct-engine
|
|
@@ -574,9 +577,10 @@ the current branch is pushed first. A non-`main` branch then fast-forwards
|
|
|
574
577
|
diverged. From `main`, it calls the existing `bump` task, commits the generated
|
|
575
578
|
version changes, pushes `main`, and pushes the matching annotated `vX.Y.Z` tag.
|
|
576
579
|
Pass `--no-bump` only to release an existing synchronized local bump. The tag
|
|
577
|
-
workflow verifies that the tag points at the exact `origin/main` commit.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
+
workflow verifies that the tag points at the exact `origin/main` commit. A newer
|
|
581
|
+
tag cancels the previous release run, including leftover publish, wait, and docs
|
|
582
|
+
jobs. The local task completes local publication and demo deployment first, then
|
|
583
|
+
waits for this workflow by default. It prints the initial job and step state, then only
|
|
580
584
|
append-only status changes, without annotations. Successful runs omit full logs;
|
|
581
585
|
unsuccessful runs print failed-step logs only. Local-only releases and
|
|
582
586
|
repositories without a resolved authenticated GitHub account skip the watcher.
|
|
@@ -590,8 +594,10 @@ site. Install validation prerequisites through `releaseSetupSteps`.
|
|
|
590
594
|
Publication jobs download the resulting artifacts rather than rebuilding the
|
|
591
595
|
workspace. Each Python package retains its own publishing environment and waits
|
|
592
596
|
for its package dependencies to publish. The npm job uses the bundled archive
|
|
593
|
-
publisher without a checkout or workspace install.
|
|
594
|
-
|
|
597
|
+
publisher without a checkout or workspace install. If npm reports that a version
|
|
598
|
+
is already staged, that publisher waits for the public artifact instead of
|
|
599
|
+
publishing the version again. After all selected public publishers finish, one
|
|
600
|
+
verifier downloads the complete release artifact set and
|
|
595
601
|
polls only unresolved packages concurrently. It validates npm tarball contents
|
|
596
602
|
and PyPI distribution hashes before Pages deploys the already built site. Local
|
|
597
603
|
npm and Python registry publication remains available through the direct local
|
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.161";
|
|
7
7
|
export * as appkitWorkspace from "./src/appkit-workspace.ts";
|
|
8
8
|
export * as barrels from "./src/barrels.ts";
|
|
9
9
|
export * as bunApp from "./src/bun-app.ts";
|
|
@@ -41,6 +41,7 @@ export * as vscode from "./src/vscode.ts";
|
|
|
41
41
|
export * as watch from "./src/watch.ts";
|
|
42
42
|
export * as workspaceLock from "./src/workspace-lock.ts";
|
|
43
43
|
export * as workspaceManifest from "./src/workspace-manifest.ts";
|
|
44
|
+
export * as workspaceVendor from "./src/workspace-vendor.ts";
|
|
44
45
|
export * as workspaceVersion from "./src/workspace-version.ts";
|
|
45
46
|
export { isAppKitApplication, AppKitWorkspace } from "./src/appkit-workspace.ts";
|
|
46
47
|
export type { AppKitWorkspaceMode, AppKitBundleOptions, AppKitWorkspaceOptions } from "./src/appkit-workspace.ts";
|
|
@@ -61,8 +62,8 @@ export { moduleStatements, moduleExports, publicFunctionExports, publicNamespace
|
|
|
61
62
|
export type { ModuleStatement, ModuleExport, PublicFunctionExport } from "./src/module-exports.ts";
|
|
62
63
|
export { applyPublishConfig, packNpmPackage } from "./src/npm-packaging.ts";
|
|
63
64
|
export type { NpmArchiveManifestTransform } from "./src/npm-packaging.ts";
|
|
64
|
-
export { npmReleaseMatches, publishedNpmRelease, readNpmArchiveManifest, readNpmArchiveIdentity, npmArchiveContentDigest } from "./src/npm-release.ts";
|
|
65
|
-
export type { NpmReleaseIdentity, NpmArchiveManifest } from "./src/npm-release.ts";
|
|
65
|
+
export { npmReleaseMatches, publishedNpmRelease, isNpmStagedVersionConflict, waitForMatchingNpmRelease, publishNpmArchive, readNpmArchiveManifest, readNpmArchiveIdentity, npmArchiveContentDigest } from "./src/npm-release.ts";
|
|
66
|
+
export type { NpmReleaseIdentity, NpmArchiveManifest, NpmPublishOutcome } from "./src/npm-release.ts";
|
|
66
67
|
export { resolveRepoRoot, repoRoot, DEFAULT_PACKAGE_ROOTS, projectName, toPosix, isGeneratedFile, isModuleFile, DiscoveredPackage, scanPackages, readPackageManifest, readDbxToolsConfig, syncResynthPaths, syncWatchTasks, recordedPackages, workspaceDependencyDirectories, recordedRoots } from "./src/packages.ts";
|
|
67
68
|
export type { RecordedPackage } from "./src/packages.ts";
|
|
68
69
|
export { PnpmWorkspaceState } from "./src/pnpm-workspace.ts";
|
|
@@ -84,7 +85,7 @@ export { PYTHON_NODE_RUNTIME_DISTRIBUTION, PYTHON_NODE_RUNTIME_MODULE, PYTHON_NO
|
|
|
84
85
|
export type { PythonNodeRuntimeOptions } from "./src/python-node-runtime.ts";
|
|
85
86
|
export { pythonDistributionPaths, publishPythonProjects, buildPythonProjects } from "./src/python-packaging.ts";
|
|
86
87
|
export { pythonProjectInfo, preparePythonProjectForPublication } from "./src/python-release.ts";
|
|
87
|
-
export { RELEASE_VERSION, RELEASE_WORKFLOW_FILE, PUBLIC_NPM_REGISTRY_URL, PUBLIC_PYPI_REGISTRY_URL } from "./src/release-context.ts";
|
|
88
|
+
export { RELEASE_VERSION, RELEASE_WORKFLOW_FILE, PUBLIC_NPM_REGISTRY_URL, PUBLIC_PYPI_REGISTRY_URL, PUBLICATION_WAIT_TIMEOUT_SECONDS, PUBLICATION_WAIT_INTERVAL_SECONDS } from "./src/release-context.ts";
|
|
88
89
|
export { RELEASE_PUBLISH_TARGETS, RELEASE_INSTALL_MODES, ReleaseStepSelectionSchema, releaseStepSelection, releaseTagAnnotation, parseReleaseTagAnnotation, releasePublishesLocally } from "./src/release-options.ts";
|
|
89
90
|
export type { ReleasePublishTarget, ReleaseInstallMode, ReleaseStepSelection, ReleaseSelectionOptions } from "./src/release-options.ts";
|
|
90
91
|
export { tryReleaseWorkflow, releaseTagPattern, releaseCondition, releaseBuildSteps, nodeReleaseSetupSteps, uvSetupStep, refreshReleaseDocsDependencies, refreshPublicationVerification, DBXToolsRelease } from "./src/release.ts";
|
|
@@ -100,7 +101,9 @@ export { configureVsCode } from "./src/vscode.ts";
|
|
|
100
101
|
export { watchRoots, watchLoop } from "./src/watch.ts";
|
|
101
102
|
export type { IgnoreGroupOptions, WatchLoopOptions } from "./src/watch.ts";
|
|
102
103
|
export { withWorkspaceMutationLock } from "./src/workspace-lock.ts";
|
|
103
|
-
export type { WorkspaceMutationLockOptions } from "./src/workspace-lock.ts";
|
|
104
|
+
export type { WorkspaceMutationLockTimer, WorkspaceMutationLockScheduler, WorkspaceMutationLockOptions } from "./src/workspace-lock.ts";
|
|
104
105
|
export { materializeWorkspaceManifest, withMaterializedWorkspaceManifests, withStagedWorkspaceManifests } from "./src/workspace-manifest.ts";
|
|
106
|
+
export { vendorWorkspaceDependencies } from "./src/workspace-vendor.ts";
|
|
107
|
+
export type { VendoredWorkspaceDependencies } from "./src/workspace-vendor.ts";
|
|
105
108
|
export { VERSION_FILE, DEFAULT_VERSION, parseSemver, compareSemver, incrementSemver, versionPath, readWorkspaceVersion, writeWorkspaceVersion, syncWorkspaceManifestVersion, resolveNextVersion } from "./src/workspace-version.ts";
|
|
106
109
|
export type { Semver, VersionLevel } from "./src/workspace-version.ts";
|
package/package.json
CHANGED
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^1.7.0",
|
|
33
|
-
"@dbx-tools/auth": "^0.9.
|
|
34
|
-
"@dbx-tools/cli-args": "^0.9.
|
|
35
|
-
"@dbx-tools/core": "^0.9.
|
|
36
|
-
"@dbx-tools/path": "^0.9.
|
|
37
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
33
|
+
"@dbx-tools/auth": "^0.9.161",
|
|
34
|
+
"@dbx-tools/cli-args": "^0.9.161",
|
|
35
|
+
"@dbx-tools/core": "^0.9.161",
|
|
36
|
+
"@dbx-tools/path": "^0.9.161",
|
|
37
|
+
"@dbx-tools/shared-core": "^0.9.161",
|
|
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.161",
|
|
60
60
|
"types": "index.ts",
|
|
61
61
|
"type": "module",
|
|
62
62
|
"exports": {
|
package/src/npm-release.ts
CHANGED
|
@@ -11,8 +11,15 @@ import {
|
|
|
11
11
|
} from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join, relative } from "node:path";
|
|
14
|
-
import { spawnSync } from "@dbx-tools/core";
|
|
15
|
-
import {
|
|
14
|
+
import { spawn, spawnSync } from "@dbx-tools/core";
|
|
15
|
+
import { log, poll, toError } from "@dbx-tools/shared-core";
|
|
16
|
+
import {
|
|
17
|
+
PUBLIC_NPM_REGISTRY_URL,
|
|
18
|
+
PUBLICATION_WAIT_INTERVAL_SECONDS,
|
|
19
|
+
PUBLICATION_WAIT_TIMEOUT_SECONDS,
|
|
20
|
+
} from "./release-context.ts";
|
|
21
|
+
|
|
22
|
+
const logger = log.logger("projen:npm-release");
|
|
16
23
|
|
|
17
24
|
/** Registry and archive identity used to verify idempotent npm publication. */
|
|
18
25
|
export interface NpmReleaseIdentity {
|
|
@@ -72,6 +79,7 @@ export async function publishedNpmRelease(
|
|
|
72
79
|
version: string,
|
|
73
80
|
registry = process.env.NPM_CONFIG_REGISTRY ?? PUBLIC_NPM_REGISTRY_URL,
|
|
74
81
|
signal?: AbortSignal,
|
|
82
|
+
options: { readonly content?: boolean } = {},
|
|
75
83
|
): Promise<NpmReleaseIdentity | undefined> {
|
|
76
84
|
const response = await fetch(registryUrl(registry, name, version), {
|
|
77
85
|
headers: { accept: "application/json" },
|
|
@@ -91,7 +99,7 @@ export async function publishedNpmRelease(
|
|
|
91
99
|
throw new Error(`npm registry returned an incomplete identity for ${name}@${version}`);
|
|
92
100
|
}
|
|
93
101
|
let contentDigest: string | undefined;
|
|
94
|
-
if (metadata.dist?.tarball) {
|
|
102
|
+
if (options.content !== false && metadata.dist?.tarball) {
|
|
95
103
|
const archive = await fetch(metadata.dist.tarball, { signal });
|
|
96
104
|
if (!archive.ok) {
|
|
97
105
|
throw new Error(`npm tarball lookup failed for ${name}@${version}: ${archive.status}`);
|
|
@@ -114,6 +122,106 @@ export async function publishedNpmRelease(
|
|
|
114
122
|
};
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
/** Whether npm refused a retry because it already accepted this version for staging. */
|
|
126
|
+
export function isNpmStagedVersionConflict(error: unknown): boolean {
|
|
127
|
+
const message = toError(error).message;
|
|
128
|
+
return /409\b/.test(message) && /previously staged version/i.test(message);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Wait until the public registry serves the exact local npm archive. */
|
|
132
|
+
export async function waitForMatchingNpmRelease(
|
|
133
|
+
local: NpmReleaseIdentity,
|
|
134
|
+
registry?: string,
|
|
135
|
+
options: { readonly intervalMs?: number; readonly timeoutMs?: number } = {},
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
const selectedRegistry = registry ?? process.env.NPM_CONFIG_REGISTRY ?? PUBLIC_NPM_REGISTRY_URL;
|
|
138
|
+
try {
|
|
139
|
+
for await (const _available of poll(
|
|
140
|
+
async ({ signal }) => {
|
|
141
|
+
const published = await publishedNpmRelease(
|
|
142
|
+
local.name,
|
|
143
|
+
local.version,
|
|
144
|
+
selectedRegistry,
|
|
145
|
+
signal,
|
|
146
|
+
{ content: false },
|
|
147
|
+
);
|
|
148
|
+
if (!published) return false;
|
|
149
|
+
return npmReleaseMatches(
|
|
150
|
+
local,
|
|
151
|
+
await publishedNpmRelease(local.name, local.version, selectedRegistry, signal),
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
intervalMs: options.intervalMs ?? PUBLICATION_WAIT_INTERVAL_SECONDS * 1_000,
|
|
156
|
+
timeoutMs: options.timeoutMs ?? PUBLICATION_WAIT_TIMEOUT_SECONDS * 1_000,
|
|
157
|
+
predicate: (available) => !available,
|
|
158
|
+
},
|
|
159
|
+
)) {
|
|
160
|
+
void _available;
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Staged npm version ${local.name}@${local.version} did not become available`,
|
|
165
|
+
{ cause: error },
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Outcome of one idempotent npm archive publication. */
|
|
171
|
+
export type NpmPublishOutcome = "published" | "skipped" | "staged";
|
|
172
|
+
|
|
173
|
+
/** Publish one archive, or wait when npm has already staged that exact version. */
|
|
174
|
+
export async function publishNpmArchive(options: {
|
|
175
|
+
readonly archive: string;
|
|
176
|
+
readonly identity: NpmReleaseIdentity;
|
|
177
|
+
readonly cwd?: string;
|
|
178
|
+
readonly dryRun?: boolean;
|
|
179
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
180
|
+
readonly intervalMs?: number;
|
|
181
|
+
readonly registry?: string;
|
|
182
|
+
readonly timeoutMs?: number;
|
|
183
|
+
}): Promise<NpmPublishOutcome> {
|
|
184
|
+
const { archive, identity } = options;
|
|
185
|
+
if (!options.dryRun) {
|
|
186
|
+
const published = await publishedNpmRelease(identity.name, identity.version, options.registry);
|
|
187
|
+
if (npmReleaseMatches(identity, published)) return "skipped";
|
|
188
|
+
}
|
|
189
|
+
const result = await spawn(
|
|
190
|
+
"npm",
|
|
191
|
+
[
|
|
192
|
+
"publish",
|
|
193
|
+
archive,
|
|
194
|
+
"--ignore-scripts",
|
|
195
|
+
...(identity.access ? ["--access", identity.access] : []),
|
|
196
|
+
...(options.registry ? ["--registry", options.registry] : []),
|
|
197
|
+
...(options.dryRun ? ["--dry-run"] : []),
|
|
198
|
+
],
|
|
199
|
+
{
|
|
200
|
+
cwd: options.cwd ?? process.cwd(),
|
|
201
|
+
env: options.env,
|
|
202
|
+
stdin: "ignore",
|
|
203
|
+
stdout: "inherit",
|
|
204
|
+
stderr: "capture",
|
|
205
|
+
check: false,
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
if (result.stderr) process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
|
|
209
|
+
if (result.exitCode === 0) return "published";
|
|
210
|
+
const error = new Error(
|
|
211
|
+
`npm publish failed (exit ${result.exitCode})${result.stderr ? `: ${result.stderr}` : ""}`,
|
|
212
|
+
);
|
|
213
|
+
if (options.dryRun) throw error;
|
|
214
|
+
const published = await publishedNpmRelease(identity.name, identity.version, options.registry);
|
|
215
|
+
if (npmReleaseMatches(identity, published)) return "skipped";
|
|
216
|
+
if (!isNpmStagedVersionConflict(error)) throw error;
|
|
217
|
+
logger.info(`waiting for staged ${identity.name}@${identity.version}`);
|
|
218
|
+
await waitForMatchingNpmRelease(identity, options.registry, {
|
|
219
|
+
intervalMs: options.intervalMs,
|
|
220
|
+
timeoutMs: options.timeoutMs,
|
|
221
|
+
});
|
|
222
|
+
return "staged";
|
|
223
|
+
}
|
|
224
|
+
|
|
117
225
|
/** Read one npm package manifest directly from its archive. */
|
|
118
226
|
export function readNpmArchiveManifest(path: string): NpmArchiveManifest {
|
|
119
227
|
const result = spawnSync("tar", ["-xOf", path, "package/package.json"], {
|
package/src/project-js.ts
CHANGED
|
@@ -1397,7 +1397,11 @@ function initProject(
|
|
|
1397
1397
|
// repairs the worktree it is meant to validate; `eslint:fix` is the explicit
|
|
1398
1398
|
// local mutation path.
|
|
1399
1399
|
const eslint = new javascript.Eslint(project, {
|
|
1400
|
-
dirs: [
|
|
1400
|
+
dirs: [
|
|
1401
|
+
...roots,
|
|
1402
|
+
...project.extraWorkspaceMembers,
|
|
1403
|
+
...(configuredAppRoot ? [configuredAppRoot] : []),
|
|
1404
|
+
],
|
|
1401
1405
|
fileExtensions: [".ts", ".tsx"],
|
|
1402
1406
|
projectService: true,
|
|
1403
1407
|
prettier: Boolean(project.prettier),
|
|
@@ -1605,9 +1609,12 @@ function preSynthesizeProject(project: javascript.NodeProject): void {
|
|
|
1605
1609
|
const subtree = project.node.findAll().filter(Project.isProject);
|
|
1606
1610
|
if (project.prettier) {
|
|
1607
1611
|
const ignorePatterns = new Set<string>();
|
|
1612
|
+
const root = resolve(project.outdir);
|
|
1608
1613
|
for (const p of subtree) {
|
|
1614
|
+
const prefix = toPosix(relative(root, p.outdir));
|
|
1609
1615
|
p.files.forEach((file) => {
|
|
1610
|
-
if (file.readonly)
|
|
1616
|
+
if (!file.readonly) return;
|
|
1617
|
+
ignorePatterns.add(prefix ? `${prefix}/${file.path}` : file.path);
|
|
1611
1618
|
});
|
|
1612
1619
|
}
|
|
1613
1620
|
ignorePatterns.forEach((pattern) => project.prettier!.addIgnorePattern(pattern));
|
package/src/release-context.ts
CHANGED
|
@@ -7,3 +7,7 @@ export const RELEASE_WORKFLOW_FILE = "release.yml";
|
|
|
7
7
|
export const PUBLIC_NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
8
8
|
/** Public PyPI JSON API queried after Python publication completes. */
|
|
9
9
|
export const PUBLIC_PYPI_REGISTRY_URL = "https://pypi.org/pypi";
|
|
10
|
+
/** Shared wait for every unresolved public npm and PyPI artifact. */
|
|
11
|
+
export const PUBLICATION_WAIT_TIMEOUT_SECONDS = 20 * 60;
|
|
12
|
+
/** Delay between public registry probes while a release is still invisible. */
|
|
13
|
+
export const PUBLICATION_WAIT_INTERVAL_SECONDS = 5;
|
package/src/release.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { BUN_VERSION } from "./bun-workflow.ts";
|
|
|
18
18
|
import { projectReleaseBranch, taskCommand, type DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
19
19
|
import {
|
|
20
20
|
PUBLIC_NPM_REGISTRY_URL,
|
|
21
|
+
PUBLICATION_WAIT_TIMEOUT_SECONDS,
|
|
21
22
|
RELEASE_VERSION,
|
|
22
23
|
RELEASE_WORKFLOW_FILE,
|
|
23
24
|
} from "./release-context.ts";
|
|
@@ -73,7 +74,7 @@ export function releaseTagPattern(project: DBXToolsJavaScriptProject): string {
|
|
|
73
74
|
/** Run a release job after all explicit prerequisite results succeed. */
|
|
74
75
|
export function releaseCondition(prerequisites: readonly string[] = []): string {
|
|
75
76
|
if (prerequisites.length === 0) return "${{ success() }}";
|
|
76
|
-
return `\${{ always() && ${prerequisites.map((condition) => `(${condition})`).join(" && ")} }}`;
|
|
77
|
+
return `\${{ always() && !cancelled() && ${prerequisites.map((condition) => `(${condition})`).join(" && ")} }}`;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/** Gate shared-build steps using the same per-run flag, preserving their existing conditions. */
|
|
@@ -171,7 +172,7 @@ export function refreshPublicationVerification(project: DBXToolsJavaScriptProjec
|
|
|
171
172
|
needs: ["build-release", ...registryJobs],
|
|
172
173
|
runsOn: ["ubuntu-latest"],
|
|
173
174
|
permissions: { contents: JobPermission.READ },
|
|
174
|
-
timeoutMinutes:
|
|
175
|
+
timeoutMinutes: Math.ceil(PUBLICATION_WAIT_TIMEOUT_SECONDS / 60) + 5,
|
|
175
176
|
steps: [
|
|
176
177
|
{
|
|
177
178
|
name: "Download publication verifier",
|
package/src/workspace-lock.ts
CHANGED
|
@@ -7,6 +7,26 @@ const MUTATION_LOCK_SCOPE = "dbx-tools-workspace-mutation";
|
|
|
7
7
|
const WAIT_LOG_INTERVAL_MS = 5_000;
|
|
8
8
|
const logger = log.logger("projen:workspace-lock");
|
|
9
9
|
|
|
10
|
+
/** Cancelable recurring task returned by a workspace-lock scheduler. */
|
|
11
|
+
export interface WorkspaceMutationLockTimer {
|
|
12
|
+
cancel(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Clock and recurring scheduler used by workspace-lock wait logging. */
|
|
16
|
+
export interface WorkspaceMutationLockScheduler {
|
|
17
|
+
now(): number;
|
|
18
|
+
repeat(callback: () => void, intervalMs: number): WorkspaceMutationLockTimer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const _systemScheduler: WorkspaceMutationLockScheduler = {
|
|
22
|
+
now: Date.now,
|
|
23
|
+
repeat(callback, intervalMs) {
|
|
24
|
+
const timer = setInterval(callback, intervalMs);
|
|
25
|
+
timer.unref?.();
|
|
26
|
+
return { cancel: () => clearInterval(timer) };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
10
30
|
/** Optional preflight used before and after acquiring the workspace mutation lock. */
|
|
11
31
|
export interface WorkspaceMutationLockOptions {
|
|
12
32
|
/**
|
|
@@ -15,6 +35,8 @@ export interface WorkspaceMutationLockOptions {
|
|
|
15
35
|
* without taking the critical section only to return.
|
|
16
36
|
*/
|
|
17
37
|
check?: () => boolean | Promise<boolean>;
|
|
38
|
+
/** Scheduler override for deterministic wait-logging tests. */
|
|
39
|
+
scheduler?: WorkspaceMutationLockScheduler;
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
/**
|
|
@@ -31,10 +53,11 @@ export async function withWorkspaceMutationLock<T>(
|
|
|
31
53
|
): Promise<T | undefined> {
|
|
32
54
|
const repository = resolve(root);
|
|
33
55
|
const check = options?.check;
|
|
56
|
+
const scheduler = options?.scheduler ?? _systemScheduler;
|
|
34
57
|
if (check && !(await check())) {
|
|
35
58
|
return undefined;
|
|
36
59
|
}
|
|
37
|
-
let waitLogTimer:
|
|
60
|
+
let waitLogTimer: WorkspaceMutationLockTimer | undefined;
|
|
38
61
|
try {
|
|
39
62
|
return await withFileLock(
|
|
40
63
|
[MUTATION_LOCK_SCOPE, repository],
|
|
@@ -48,20 +71,19 @@ export async function withWorkspaceMutationLock<T>(
|
|
|
48
71
|
backends: process.platform === "win32" ? ["file"] : ["flock", "file"],
|
|
49
72
|
onWait: () => {
|
|
50
73
|
if (waitLogTimer) return;
|
|
51
|
-
const startedAt =
|
|
52
|
-
waitLogTimer =
|
|
74
|
+
const startedAt = scheduler.now();
|
|
75
|
+
waitLogTimer = scheduler.repeat(
|
|
53
76
|
() =>
|
|
54
77
|
logger.info("waiting for workspace mutation lock", {
|
|
55
78
|
repository,
|
|
56
|
-
elapsedMs:
|
|
79
|
+
elapsedMs: scheduler.now() - startedAt,
|
|
57
80
|
}),
|
|
58
81
|
WAIT_LOG_INTERVAL_MS,
|
|
59
82
|
);
|
|
60
|
-
waitLogTimer.unref?.();
|
|
61
83
|
},
|
|
62
84
|
},
|
|
63
85
|
);
|
|
64
86
|
} finally {
|
|
65
|
-
|
|
87
|
+
waitLogTimer?.cancel();
|
|
66
88
|
}
|
|
67
89
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Workspace package vendoring for isolated AppKit deployment snapshots. */
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { isRecord } from "@dbx-tools/shared-core";
|
|
5
|
+
import { applyPublishConfig, packNpmPackage } from "./npm-packaging.ts";
|
|
6
|
+
import { readPackageManifest, workspaceDependencyDirectories } from "./packages.ts";
|
|
7
|
+
|
|
8
|
+
const RUNTIME_DEPENDENCY_FIELDS = [
|
|
9
|
+
"dependencies",
|
|
10
|
+
"optionalDependencies",
|
|
11
|
+
"peerDependencies",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
/** Result of staging a workspace package dependency closure. */
|
|
15
|
+
export interface VendoredWorkspaceDependencies {
|
|
16
|
+
readonly archives: readonly string[];
|
|
17
|
+
readonly packages: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Whether a runtime manifest field contains a workspace protocol dependency. */
|
|
21
|
+
function _workspaceDependencyNames(manifest: Readonly<Record<string, unknown>>): string[] {
|
|
22
|
+
return RUNTIME_DEPENDENCY_FIELDS.flatMap((field) => {
|
|
23
|
+
const dependencies = manifest[field];
|
|
24
|
+
if (!isRecord(dependencies)) return [];
|
|
25
|
+
return Object.entries(dependencies).flatMap(([name, value]) =>
|
|
26
|
+
typeof value === "string" && value.startsWith("workspace:") ? [name] : [],
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Ensure generated archives survive the staged application's ignore rules. */
|
|
32
|
+
function _includeVendorArchives(application: string): void {
|
|
33
|
+
const gitignore = join(application, ".gitignore");
|
|
34
|
+
const before = existsSync(gitignore) ? readFileSync(gitignore, "utf8") : "";
|
|
35
|
+
const lines = ["!/vendor/", "!/vendor/npm/", "!/vendor/npm/*.tgz"];
|
|
36
|
+
const missing = lines.filter((line) => !before.split(/\r?\n/).includes(line));
|
|
37
|
+
if (missing.length === 0) return;
|
|
38
|
+
if (existsSync(gitignore)) chmodSync(gitignore, statSync(gitignore).mode | 0o200);
|
|
39
|
+
writeFileSync(gitignore, `${before.replace(/\s*$/, "")}\n${missing.join("\n")}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pack the transitive runtime workspace closure and link every archive directly
|
|
44
|
+
* from the staged application manifest. Returns without writing when the source
|
|
45
|
+
* application has no workspace protocol dependencies.
|
|
46
|
+
*/
|
|
47
|
+
export function vendorWorkspaceDependencies(
|
|
48
|
+
sourceRoot: string,
|
|
49
|
+
stageRoot: string,
|
|
50
|
+
applicationPath: string,
|
|
51
|
+
): VendoredWorkspaceDependencies {
|
|
52
|
+
const sourceApplication = resolve(sourceRoot, applicationPath);
|
|
53
|
+
const sourceManifest = readPackageManifest(sourceApplication);
|
|
54
|
+
if (!sourceManifest || typeof sourceManifest.name !== "string") {
|
|
55
|
+
throw new Error(`Application package manifest is missing: ${sourceApplication}`);
|
|
56
|
+
}
|
|
57
|
+
const directWorkspaceDependencies = _workspaceDependencyNames(sourceManifest);
|
|
58
|
+
if (directWorkspaceDependencies.length === 0) return { archives: [], packages: [] };
|
|
59
|
+
|
|
60
|
+
const selected = workspaceDependencyDirectories(sourceManifest.name, sourceRoot)
|
|
61
|
+
.filter((directory) => resolve(directory) !== sourceApplication)
|
|
62
|
+
.map((directory) => {
|
|
63
|
+
const manifest = readPackageManifest(directory);
|
|
64
|
+
if (!manifest || typeof manifest.name !== "string") {
|
|
65
|
+
throw new Error(`Workspace package manifest is missing: ${directory}`);
|
|
66
|
+
}
|
|
67
|
+
return { directory, name: manifest.name };
|
|
68
|
+
});
|
|
69
|
+
const selectedNames = new Set(selected.map(({ name }) => name));
|
|
70
|
+
const unresolved = directWorkspaceDependencies.filter((name) => !selectedNames.has(name));
|
|
71
|
+
if (unresolved.length > 0) {
|
|
72
|
+
throw new Error(`Workspace dependencies are not recorded: ${unresolved.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const stagedApplication = resolve(stageRoot, applicationPath);
|
|
76
|
+
const archiveDirectory = join(stagedApplication, "vendor", "npm");
|
|
77
|
+
mkdirSync(archiveDirectory, { recursive: true });
|
|
78
|
+
const links = Object.fromEntries(
|
|
79
|
+
selected.map(({ directory, name }) => {
|
|
80
|
+
const archive = packNpmPackage(
|
|
81
|
+
directory,
|
|
82
|
+
archiveDirectory,
|
|
83
|
+
process.env.PATH,
|
|
84
|
+
applyPublishConfig,
|
|
85
|
+
);
|
|
86
|
+
return [name, `file:./vendor/npm/${basename(archive)}`];
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
const stagedManifestPath = join(stagedApplication, "package.json");
|
|
90
|
+
const stagedManifest = JSON.parse(readFileSync(stagedManifestPath, "utf8")) as Record<
|
|
91
|
+
string,
|
|
92
|
+
unknown
|
|
93
|
+
>;
|
|
94
|
+
const dependencies = isRecord(stagedManifest.dependencies) ? stagedManifest.dependencies : {};
|
|
95
|
+
stagedManifest.dependencies = { ...dependencies, ...links };
|
|
96
|
+
chmodSync(stagedManifestPath, statSync(stagedManifestPath).mode | 0o200);
|
|
97
|
+
writeFileSync(stagedManifestPath, `${JSON.stringify(stagedManifest, null, 2)}\n`);
|
|
98
|
+
_includeVendorArchives(stagedApplication);
|
|
99
|
+
return {
|
|
100
|
+
archives: Object.values(links),
|
|
101
|
+
packages: selected.map(({ name }) => name),
|
|
102
|
+
};
|
|
103
|
+
}
|
package/tasks/appkit-deploy.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { log, toError } from "@dbx-tools/shared-core";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
|
|
11
11
|
import { withStagedWorkspaceManifests } from "../src/workspace-manifest.ts";
|
|
12
|
+
import { vendorWorkspaceDependencies } from "../src/workspace-vendor.ts";
|
|
12
13
|
|
|
13
14
|
const logger = log.logger("projen:appkit-deploy");
|
|
14
15
|
|
|
@@ -146,6 +147,13 @@ export async function deployApp(
|
|
|
146
147
|
}));
|
|
147
148
|
await withStagedWorkspaceManifests(root, async (stage) => {
|
|
148
149
|
const stagedApplication = resolve(stage, options.directory);
|
|
150
|
+
const vendored = vendorWorkspaceDependencies(root, stage, applicationPath);
|
|
151
|
+
if (vendored.packages.length > 0) {
|
|
152
|
+
logger.info("Vendored workspace packages for deployment", {
|
|
153
|
+
count: vendored.packages.length,
|
|
154
|
+
packages: vendored.packages,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
149
157
|
cpSync(clientDist, join(stagedApplication, "client", "dist"), { recursive: true });
|
|
150
158
|
await reconcileStagedBundleState(stagedApplication, contextArgs, options.target, dependencies);
|
|
151
159
|
const result = await run(deployArgs, stagedApplication);
|
package/tasks/publish-npm.ts
CHANGED
|
@@ -6,10 +6,8 @@ import { log, mapConcurrent } from "@dbx-tools/shared-core";
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
|
|
8
8
|
import { TaskConcurrencyOptionSchema, TaskDryRunOptionSchema } from "./options.ts";
|
|
9
|
-
import { runTaskCommandAsync } from "../src/_task-command.ts";
|
|
10
9
|
import {
|
|
11
|
-
|
|
12
|
-
publishedNpmRelease,
|
|
10
|
+
publishNpmArchive,
|
|
13
11
|
readNpmArchiveIdentity,
|
|
14
12
|
readNpmArchiveManifest,
|
|
15
13
|
} from "../src/npm-release.ts";
|
|
@@ -84,20 +82,15 @@ export async function publishNpmArchives(options: {
|
|
|
84
82
|
`npm archive ${archive} carries ${local.version}, expected ${options.version}`,
|
|
85
83
|
);
|
|
86
84
|
}
|
|
87
|
-
|
|
88
|
-
const published = await publishedNpmRelease(local.name, local.version, options.registry);
|
|
89
|
-
if (npmReleaseMatches(local, published)) {
|
|
90
|
-
logger.info(`skip published ${local.name}@${local.version}`);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
await runTaskCommandAsync(process.cwd(), "npm", [
|
|
95
|
-
"publish",
|
|
85
|
+
const outcome = await publishNpmArchive({
|
|
96
86
|
archive,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
identity: local,
|
|
88
|
+
dryRun: options.dryRun,
|
|
89
|
+
registry: options.registry,
|
|
90
|
+
});
|
|
91
|
+
if (outcome === "skipped") {
|
|
92
|
+
logger.info(`skip published ${local.name}@${local.version}`);
|
|
93
|
+
}
|
|
101
94
|
},
|
|
102
95
|
{
|
|
103
96
|
concurrency: options.concurrency ?? DEFAULT_PUBLISH_CONCURRENCY,
|
package/tasks/publish.ts
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
* standalone publishes.
|
|
33
33
|
* - **release recovery** compares each packed archive's integrity and
|
|
34
34
|
* repository identity with registry metadata. A matching immutable version
|
|
35
|
-
* is skipped
|
|
35
|
+
* is skipped. A 409 for a previously staged version waits for that exact
|
|
36
|
+
* public artifact instead of publishing again. Any other mismatch fails.
|
|
36
37
|
*
|
|
37
38
|
* `--dry-run` forwards to `npm publish`: it packs + validates
|
|
38
39
|
* but uploads nothing. `--registry` targets a non-default registry (a local
|
|
@@ -56,13 +57,9 @@ import {
|
|
|
56
57
|
TaskOutputOptionSchema,
|
|
57
58
|
TaskRootOptionSchema,
|
|
58
59
|
} from "./options.ts";
|
|
59
|
-
import { runTaskCommand
|
|
60
|
+
import { runTaskCommand } from "../src/_task-command.ts";
|
|
60
61
|
import { applyPublishConfig, packNpmPackage } from "../src/npm-packaging.ts";
|
|
61
|
-
import {
|
|
62
|
-
npmReleaseMatches,
|
|
63
|
-
publishedNpmRelease,
|
|
64
|
-
readNpmArchiveIdentity,
|
|
65
|
-
} from "../src/npm-release.ts";
|
|
62
|
+
import { publishNpmArchive, readNpmArchiveIdentity } from "../src/npm-release.ts";
|
|
66
63
|
import { recordedPackages } from "../src/packages.ts";
|
|
67
64
|
|
|
68
65
|
const logger = log.logger("projen:publish");
|
|
@@ -153,11 +150,6 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
|
|
153
150
|
}
|
|
154
151
|
logger.info(`validated ${members.length} selected member manifests`);
|
|
155
152
|
|
|
156
|
-
const publishArgs = [
|
|
157
|
-
"--ignore-scripts",
|
|
158
|
-
...(registry ? ["--registry", registry] : []),
|
|
159
|
-
...(dryRun ? ["--dry-run"] : []),
|
|
160
|
-
];
|
|
161
153
|
const publishable: Array<{
|
|
162
154
|
dir: string;
|
|
163
155
|
name: string;
|
|
@@ -253,20 +245,19 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
|
|
253
245
|
logger.info(`packed ${name} @ ${packageVersion}`);
|
|
254
246
|
return;
|
|
255
247
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
248
|
+
const outcome = await publishNpmArchive({
|
|
249
|
+
archive,
|
|
250
|
+
identity: local,
|
|
251
|
+
cwd: dir,
|
|
252
|
+
dryRun,
|
|
253
|
+
env: { ...process.env, PATH: path },
|
|
254
|
+
registry,
|
|
255
|
+
});
|
|
256
|
+
if (outcome === "skipped") {
|
|
257
|
+
logger.info(`skip published ${name} @ ${packageVersion}`);
|
|
258
|
+
return;
|
|
262
259
|
}
|
|
263
|
-
logger.info(`${dryRun ? "dry-run
|
|
264
|
-
await runTaskCommandAsync(
|
|
265
|
-
dir,
|
|
266
|
-
"npm",
|
|
267
|
-
["publish", ...(access ? ["--access", access] : []), ...publishArgs, archive],
|
|
268
|
-
{ env: { ...process.env, PATH: path } },
|
|
269
|
-
);
|
|
260
|
+
logger.info(`${dryRun ? "dry-run published" : "published"} ${name} @ ${packageVersion}`);
|
|
270
261
|
} finally {
|
|
271
262
|
rmSync(packed, { recursive: true, force: true });
|
|
272
263
|
}
|
package/tasks/release.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/** Run the configured release transaction. */
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { serializeArgs } from "@dbx-tools/cli-args";
|
|
5
6
|
import { projectUtils, type ProjectGhAccount, resolveProjectGhAccount } from "@dbx-tools/core";
|
|
6
7
|
import { log, toError, parse } from "@dbx-tools/shared-core";
|
|
7
8
|
import type { Command } from "commander";
|
|
@@ -598,10 +599,14 @@ export async function runRelease(
|
|
|
598
599
|
}
|
|
599
600
|
|
|
600
601
|
if (options.demoDeploy) {
|
|
602
|
+
const demoArgs = serializeArgs({
|
|
603
|
+
...(selection.validation ? {} : { validation: false }),
|
|
604
|
+
...(options.demoProfile ? { profile: options.demoProfile } : {}),
|
|
605
|
+
});
|
|
601
606
|
runTaskCommand(root, "bun", [
|
|
602
607
|
"run",
|
|
603
608
|
"demo:deploy",
|
|
604
|
-
...(
|
|
609
|
+
...(demoArgs.length > 0 ? ["--", ...demoArgs] : []),
|
|
605
610
|
]);
|
|
606
611
|
}
|
|
607
612
|
|
|
@@ -11,11 +11,16 @@ import {
|
|
|
11
11
|
publishedNpmRelease,
|
|
12
12
|
readNpmArchiveIdentity,
|
|
13
13
|
} from "../src/npm-release.ts";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
PUBLIC_NPM_REGISTRY_URL,
|
|
16
|
+
PUBLIC_PYPI_REGISTRY_URL,
|
|
17
|
+
PUBLICATION_WAIT_INTERVAL_SECONDS,
|
|
18
|
+
PUBLICATION_WAIT_TIMEOUT_SECONDS,
|
|
19
|
+
} from "../src/release-context.ts";
|
|
15
20
|
|
|
16
21
|
const DEFAULT_CONCURRENCY = 8;
|
|
17
|
-
const DEFAULT_INTERVAL_SECONDS =
|
|
18
|
-
const DEFAULT_TIMEOUT_SECONDS =
|
|
22
|
+
const DEFAULT_INTERVAL_SECONDS = PUBLICATION_WAIT_INTERVAL_SECONDS;
|
|
23
|
+
const DEFAULT_TIMEOUT_SECONDS = PUBLICATION_WAIT_TIMEOUT_SECONDS;
|
|
19
24
|
const logger = log.logger("projen:verify-publications");
|
|
20
25
|
|
|
21
26
|
const PypiReleaseSchema = z
|