@dbx-tools/projen 0.9.153 → 0.9.157
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 +44 -52
- package/index.ts +43 -21
- package/package.json +8 -8
- package/src/_task-command.ts +7 -7
- package/src/appkit-workspace.ts +278 -104
- package/src/barrels.ts +147 -259
- package/src/bun-app.ts +54 -10
- package/src/clean.ts +6 -6
- package/src/engine-root.ts +4 -4
- package/src/mixin.ts +0 -2
- package/src/module-exports.ts +4 -5
- package/src/npm-packaging.ts +128 -0
- package/src/npm-release.ts +200 -0
- package/src/packages.ts +11 -11
- package/src/project-js.ts +209 -103
- package/src/project-predicate.ts +5 -12
- package/src/project-py.ts +8 -6
- package/src/project.ts +7 -14
- package/src/publish.ts +3 -3
- package/src/python-node-bindings.ts +16 -12
- package/src/python-packaging.ts +177 -0
- package/src/release-context.ts +4 -0
- package/src/release-options.ts +2 -2
- package/src/release.ts +133 -13
- package/src/shared-core-usage.ts +17 -4
- package/src/tags.ts +10 -9
- package/src/watch.ts +7 -9
- package/src/workspace-lock.ts +2 -2
- package/src/workspace-manifest.ts +2 -2
- package/tasks/appkit-deploy.ts +35 -15
- package/tasks/barrels.ts +6 -9
- package/tasks/bump.ts +1 -1
- package/tasks/clean.ts +3 -3
- package/tasks/cli.ts +3 -3
- package/tasks/compile-workspace.ts +8 -8
- package/tasks/dev-watch.ts +11 -11
- package/tasks/local-publish.ts +6 -6
- package/tasks/publish-npm.ts +11 -317
- package/tasks/publish-python.ts +2 -180
- package/tasks/publish.ts +6 -11
- package/tasks/python-node-bindings.ts +2 -2
- package/tasks/python-registry.ts +6 -6
- package/tasks/python-sync.ts +2 -3
- package/tasks/release-version.ts +1 -1
- package/tasks/release.ts +11 -11
- package/tasks/sync.ts +2 -2
- package/tasks/test-workspace.ts +9 -9
- package/tasks/verify-publications.ts +273 -0
- package/tasks/version-check.ts +5 -5
- /package/{tasks → src}/python-release.ts +0 -0
package/tasks/release-version.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
/** Validate one stable release version against the canonical workspace policy. */
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import { projectUtils } from "@dbx-tools/core";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
|
|
7
7
|
import { TaskRootOptionSchema } from "./options.ts";
|
package/tasks/release.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/** Run the configured release transaction. */
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
5
|
+
import { projectUtils, type ProjectGhAccount, resolveProjectGhAccount } from "@dbx-tools/core";
|
|
6
|
+
import { log, toError, parse } from "@dbx-tools/shared-core";
|
|
7
7
|
import type { Command } from "commander";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { parsedTaskOptions, runTaskMain, taskCommand } from "./cli.ts";
|
|
@@ -67,7 +67,7 @@ const GithubReleaseRunViewSchema = z
|
|
|
67
67
|
|
|
68
68
|
/** Inputs required to locate and watch one tag-triggered release workflow. */
|
|
69
69
|
export interface WatchReleaseGithubActionsOptions {
|
|
70
|
-
readonly account:
|
|
70
|
+
readonly account: ProjectGhAccount;
|
|
71
71
|
readonly head: string;
|
|
72
72
|
readonly root: string;
|
|
73
73
|
readonly tag: string;
|
|
@@ -80,7 +80,7 @@ export type ReleaseGithubActionsWatcher = (
|
|
|
80
80
|
options: WatchReleaseGithubActionsOptions,
|
|
81
81
|
) => Promise<void>;
|
|
82
82
|
|
|
83
|
-
function githubRepository(account:
|
|
83
|
+
function githubRepository(account: ProjectGhAccount): string {
|
|
84
84
|
return account.host === "github.com"
|
|
85
85
|
? account.remote.repository
|
|
86
86
|
: `${account.host}/${account.remote.repository}`;
|
|
@@ -194,7 +194,7 @@ export async function watchReleaseGithubActions(
|
|
|
194
194
|
],
|
|
195
195
|
{ check: true, env: account.env, stderr: "inherit" },
|
|
196
196
|
);
|
|
197
|
-
const parsed = z.array(GithubReleaseRunSchema).safeParse(
|
|
197
|
+
const parsed = z.array(GithubReleaseRunSchema).safeParse(parse(output, []));
|
|
198
198
|
if (!parsed.success) throw new Error("GitHub returned invalid release workflow metadata");
|
|
199
199
|
run = parsed.data.find(
|
|
200
200
|
(candidate) => candidate.headSha === head && candidate.headBranch === tag,
|
|
@@ -234,7 +234,7 @@ export async function watchReleaseGithubActions(
|
|
|
234
234
|
],
|
|
235
235
|
{ check: true, env: account.env, stderr: "inherit" },
|
|
236
236
|
);
|
|
237
|
-
const parsed = GithubReleaseRunViewSchema.safeParse(
|
|
237
|
+
const parsed = GithubReleaseRunViewSchema.safeParse(parse(output));
|
|
238
238
|
if (!parsed.success) throw new Error("GitHub returned invalid release progress metadata");
|
|
239
239
|
reportGithubProgress(parsed.data, progress);
|
|
240
240
|
completed = parsed.data.status === "completed";
|
|
@@ -251,7 +251,7 @@ export async function watchReleaseGithubActions(
|
|
|
251
251
|
}
|
|
252
252
|
} catch (error) {
|
|
253
253
|
logger.warn("GitHub Actions release watch stopped; the remote run was not cancelled", {
|
|
254
|
-
error:
|
|
254
|
+
error: toError(error),
|
|
255
255
|
runId: run.databaseId,
|
|
256
256
|
url: run.url,
|
|
257
257
|
});
|
|
@@ -305,7 +305,7 @@ function pushAnnotatedReleaseTag(options: {
|
|
|
305
305
|
}): void {
|
|
306
306
|
const { annotation, branch, head, remote, root, tag } = options;
|
|
307
307
|
runTaskCommand(root, "git", ["tag", "--annotate", tag, "--message", annotation]);
|
|
308
|
-
const githubAccount =
|
|
308
|
+
const githubAccount = resolveProjectGhAccount(root, { remote });
|
|
309
309
|
if (!githubAccount) {
|
|
310
310
|
logger.warn(
|
|
311
311
|
"no authenticated GitHub account can access the release repository; using git push",
|
|
@@ -382,7 +382,7 @@ function pushAnnotatedReleaseTag(options: {
|
|
|
382
382
|
} catch (error) {
|
|
383
383
|
if (remoteRefCreated) throw error;
|
|
384
384
|
logger.warn("GitHub API tag creation failed; using git push", {
|
|
385
|
-
error:
|
|
385
|
+
error: toError(error),
|
|
386
386
|
repository: githubAccount.remote.repository,
|
|
387
387
|
});
|
|
388
388
|
runTaskCommand(root, "git", ["push", remote, `refs/tags/${tag}`]);
|
|
@@ -480,7 +480,7 @@ export async function runRelease(
|
|
|
480
480
|
/** After local release actions, wait for and stream the matching GitHub Actions run. */
|
|
481
481
|
readonly watchGithubActions?: boolean;
|
|
482
482
|
readonly githubActionsWatcher?: ReleaseGithubActionsWatcher;
|
|
483
|
-
readonly resolveGithubAccount?: typeof
|
|
483
|
+
readonly resolveGithubAccount?: typeof resolveProjectGhAccount;
|
|
484
484
|
},
|
|
485
485
|
): Promise<string> {
|
|
486
486
|
const { branch, prefix, remote, root } = options;
|
|
@@ -606,7 +606,7 @@ export async function runRelease(
|
|
|
606
606
|
}
|
|
607
607
|
|
|
608
608
|
if (shouldWatchReleaseGithubActions(options, selection)) {
|
|
609
|
-
const account = (options.resolveGithubAccount ??
|
|
609
|
+
const account = (options.resolveGithubAccount ?? resolveProjectGhAccount)(root, {
|
|
610
610
|
remote,
|
|
611
611
|
});
|
|
612
612
|
if (account) {
|
package/tasks/sync.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
/** Synthesize once or supervise Projen, barrels, and registered focused watchers. */
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import { log, toError } from "@dbx-tools/shared-core";
|
|
5
5
|
import concurrently from "concurrently";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
|
|
@@ -84,7 +84,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
84
84
|
} catch (err) {
|
|
85
85
|
// A tree that doesn't synth is precisely when the watcher is most useful: the edit
|
|
86
86
|
// that repairs it is the one the projenrc watcher is sitting there waiting for.
|
|
87
|
-
logger.error("initial sync failed - watching anyway", { error:
|
|
87
|
+
logger.error("initial sync failed - watching anyway", { error: toError(err) });
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const watchers = [
|
package/tasks/test-workspace.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import
|
|
4
|
-
import { findFilesSync } from "@dbx-tools/path
|
|
5
|
-
import { log,
|
|
3
|
+
import { spawnSync, spawn } from "@dbx-tools/core";
|
|
4
|
+
import { findFilesSync } from "@dbx-tools/path";
|
|
5
|
+
import { log, sequence } from "@dbx-tools/shared-core";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { runTaskMain, taskCommand, taskOptions, taskPositionals, taskRoot } from "./cli.ts";
|
|
8
8
|
import { TaskRootOptionSchema } from "./options.ts";
|
|
@@ -137,7 +137,7 @@ function changedFiles(root: string, base?: string): string[] {
|
|
|
137
137
|
const candidates = base ? [base] : ["origin/main", "main", "HEAD~1"];
|
|
138
138
|
let committed: string[] = [];
|
|
139
139
|
for (const candidate of candidates) {
|
|
140
|
-
const result =
|
|
140
|
+
const result = spawnSync("git", ["diff", "--name-only", `${candidate}...HEAD`], {
|
|
141
141
|
cwd: root,
|
|
142
142
|
stdin: "ignore",
|
|
143
143
|
stdout: "capture",
|
|
@@ -148,14 +148,14 @@ function changedFiles(root: string, base?: string): string[] {
|
|
|
148
148
|
break;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
const working =
|
|
151
|
+
const working = spawnSync("git", ["diff", "--name-only", "HEAD"], {
|
|
152
152
|
cwd: root,
|
|
153
153
|
stdin: "ignore",
|
|
154
154
|
stdout: "capture",
|
|
155
155
|
stderr: "ignore",
|
|
156
156
|
check: true,
|
|
157
157
|
}).stdoutLines;
|
|
158
|
-
return [...
|
|
158
|
+
return [...sequence(committed, working).filter(Boolean).distinct()].sort();
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
function selectedTests(
|
|
@@ -203,13 +203,13 @@ function runnerGroup(root: string, path: string): string {
|
|
|
203
203
|
if (relativePath.startsWith("packages/js/shared/")) return "browser-shared";
|
|
204
204
|
if (
|
|
205
205
|
relativePath.startsWith("packages/js/ui/") ||
|
|
206
|
-
relativePath.startsWith("packages/
|
|
206
|
+
relativePath.startsWith("packages/demo/client/")
|
|
207
207
|
) {
|
|
208
208
|
return "react-ui";
|
|
209
209
|
}
|
|
210
210
|
if (relativePath.startsWith("projen/")) return "projen";
|
|
211
211
|
if (relativePath.startsWith("docs/scripts/")) return "docs";
|
|
212
|
-
if (relativePath.startsWith("packages/
|
|
212
|
+
if (relativePath.startsWith("packages/demo/")) return "demo";
|
|
213
213
|
return "node";
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -228,7 +228,7 @@ async function runTests(root: string, files: readonly string[]): Promise<void> {
|
|
|
228
228
|
const started = performance.now();
|
|
229
229
|
const jobs = [...groups].map(async ([name, paths]) => {
|
|
230
230
|
const groupStarted = performance.now();
|
|
231
|
-
const result = await
|
|
231
|
+
const result = await spawn("bun", ["test", ...paths], {
|
|
232
232
|
cwd: root,
|
|
233
233
|
stdin: "ignore",
|
|
234
234
|
stdout: "inherit",
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Wait for one release batch to become available from public package registries. */
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { log, poll, mapConcurrent, toError } from "@dbx-tools/shared-core";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
|
|
9
|
+
import {
|
|
10
|
+
npmReleaseMatches,
|
|
11
|
+
publishedNpmRelease,
|
|
12
|
+
readNpmArchiveIdentity,
|
|
13
|
+
} from "../src/npm-release.ts";
|
|
14
|
+
import { PUBLIC_NPM_REGISTRY_URL, PUBLIC_PYPI_REGISTRY_URL } from "../src/release-context.ts";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
17
|
+
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
18
|
+
const DEFAULT_TIMEOUT_SECONDS = 10 * 60;
|
|
19
|
+
const logger = log.logger("projen:verify-publications");
|
|
20
|
+
|
|
21
|
+
const PypiReleaseSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
urls: z
|
|
24
|
+
.array(
|
|
25
|
+
z
|
|
26
|
+
.object({
|
|
27
|
+
filename: z.string().describe("Published distribution filename"),
|
|
28
|
+
digests: z
|
|
29
|
+
.object({
|
|
30
|
+
sha256: z.string().describe("Published distribution SHA-256 digest"),
|
|
31
|
+
})
|
|
32
|
+
.describe("Published distribution digests"),
|
|
33
|
+
})
|
|
34
|
+
.describe("One published Python distribution"),
|
|
35
|
+
)
|
|
36
|
+
.describe("Published Python distributions"),
|
|
37
|
+
})
|
|
38
|
+
.describe("PyPI release metadata");
|
|
39
|
+
|
|
40
|
+
interface PypiReleaseIdentity {
|
|
41
|
+
readonly files: ReadonlyMap<string, string>;
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly version: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One public artifact group checked during the shared availability barrier. */
|
|
47
|
+
export interface PublicationProbe {
|
|
48
|
+
/** Human-readable package identity used in progress and timeout messages. */
|
|
49
|
+
readonly label: string;
|
|
50
|
+
/** Return true only when the exact expected public artifact is available. */
|
|
51
|
+
available(signal: AbortSignal): boolean | PromiseLike<boolean>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Timing and concurrency controls for the shared publication barrier. */
|
|
55
|
+
export interface PublicationWaitOptions {
|
|
56
|
+
readonly concurrency?: number;
|
|
57
|
+
readonly intervalMs?: number;
|
|
58
|
+
readonly timeoutMs?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Poll every unresolved publication concurrently and stop after one shared timeout. */
|
|
62
|
+
export async function waitForPublications(
|
|
63
|
+
probes: readonly PublicationProbe[],
|
|
64
|
+
options: PublicationWaitOptions = {},
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
if (probes.length === 0) throw new Error("No public publications were provided");
|
|
67
|
+
const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
68
|
+
const errors = new Map<string, Error>();
|
|
69
|
+
let pending = [...probes];
|
|
70
|
+
let previousCount: number | undefined;
|
|
71
|
+
try {
|
|
72
|
+
for await (const unavailable of poll<PublicationProbe[]>(
|
|
73
|
+
async ({ signal }) => {
|
|
74
|
+
const results = await mapConcurrent(
|
|
75
|
+
pending,
|
|
76
|
+
async (probe) => {
|
|
77
|
+
try {
|
|
78
|
+
const available = await probe.available(signal);
|
|
79
|
+
if (available) errors.delete(probe.label);
|
|
80
|
+
return { available, probe };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
errors.set(probe.label, toError(error));
|
|
83
|
+
return { available: false, probe };
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{ concurrency },
|
|
87
|
+
);
|
|
88
|
+
return results.filter(({ available }) => !available).map(({ probe }) => probe);
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
intervalMs: options.intervalMs ?? DEFAULT_INTERVAL_SECONDS * 1_000,
|
|
92
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_SECONDS * 1_000,
|
|
93
|
+
predicate: (unavailable) => unavailable.length > 0,
|
|
94
|
+
},
|
|
95
|
+
)) {
|
|
96
|
+
pending = unavailable;
|
|
97
|
+
if (pending.length > 0 && pending.length !== previousCount) {
|
|
98
|
+
logger.info(
|
|
99
|
+
`waiting for ${pending.length} public publication${pending.length === 1 ? "" : "s"}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
previousCount = pending.length;
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (pending.length === 0) throw error;
|
|
106
|
+
const details = pending
|
|
107
|
+
.map((probe) => {
|
|
108
|
+
const failure = errors.get(probe.label);
|
|
109
|
+
return failure ? `${probe.label}: ${failure.message}` : probe.label;
|
|
110
|
+
})
|
|
111
|
+
.join("\n");
|
|
112
|
+
throw new Error(`Publications did not become available:\n${details}`, { cause: error });
|
|
113
|
+
}
|
|
114
|
+
logger.info(`verified ${probes.length} public publication${probes.length === 1 ? "" : "s"}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function npmPublicationProbes(directory: string, version: string): PublicationProbe[] {
|
|
118
|
+
return readdirSync(directory)
|
|
119
|
+
.filter((file) => file.endsWith(".tgz"))
|
|
120
|
+
.sort()
|
|
121
|
+
.map((file) => {
|
|
122
|
+
const local = readNpmArchiveIdentity(join(directory, file));
|
|
123
|
+
if (local.version !== version) {
|
|
124
|
+
throw new Error(`${local.name} archive carries ${local.version}, expected ${version}`);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
label: `${local.name}@${local.version}`,
|
|
128
|
+
available: async (signal: AbortSignal) =>
|
|
129
|
+
npmReleaseMatches(
|
|
130
|
+
local,
|
|
131
|
+
await publishedNpmRelease(local.name, local.version, PUBLIC_NPM_REGISTRY_URL, signal),
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function distributionFiles(directory: string): string[] {
|
|
138
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
139
|
+
const path = join(directory, entry.name);
|
|
140
|
+
if (entry.isDirectory()) return distributionFiles(path);
|
|
141
|
+
return entry.name.endsWith(".whl") || entry.name.endsWith(".tar.gz") ? [path] : [];
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function pythonDistributionName(file: string, version: string): string {
|
|
146
|
+
const name = basename(file);
|
|
147
|
+
const sourceSuffix = `-${version}.tar.gz`;
|
|
148
|
+
if (name.endsWith(sourceSuffix)) {
|
|
149
|
+
return name.slice(0, -sourceSuffix.length).replaceAll("_", "-").toLowerCase();
|
|
150
|
+
}
|
|
151
|
+
if (name.endsWith(".whl")) {
|
|
152
|
+
const marker = `-${version}-`;
|
|
153
|
+
const markerIndex = name.lastIndexOf(marker);
|
|
154
|
+
if (markerIndex > 0) {
|
|
155
|
+
return name.slice(0, markerIndex).replaceAll("_", "-").toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`Python distribution ${name} does not carry release version ${version}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function pypiReleaseIdentities(directory: string, version: string): PypiReleaseIdentity[] {
|
|
162
|
+
const releases = new Map<string, Map<string, string>>();
|
|
163
|
+
for (const path of distributionFiles(directory)) {
|
|
164
|
+
const name = pythonDistributionName(path, version);
|
|
165
|
+
const files = releases.get(name) ?? new Map<string, string>();
|
|
166
|
+
const filename = basename(path);
|
|
167
|
+
files.set(filename, createHash("sha256").update(readFileSync(path)).digest("hex"));
|
|
168
|
+
releases.set(name, files);
|
|
169
|
+
}
|
|
170
|
+
return [...releases]
|
|
171
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
172
|
+
.map(([name, files]) => ({ files, name, version }));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function pypiPublicationProbes(directory: string, version: string): PublicationProbe[] {
|
|
176
|
+
return pypiReleaseIdentities(directory, version).map((local) => ({
|
|
177
|
+
label: `${local.name}==${local.version}`,
|
|
178
|
+
available: async (signal: AbortSignal) => {
|
|
179
|
+
const response = await fetch(
|
|
180
|
+
`${PUBLIC_PYPI_REGISTRY_URL}/${encodeURIComponent(local.name)}/${encodeURIComponent(local.version)}/json`,
|
|
181
|
+
{
|
|
182
|
+
headers: { accept: "application/json" },
|
|
183
|
+
signal,
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
if (response.status === 404) return false;
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`PyPI lookup failed for ${local.name}==${local.version}: ${response.status}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const published = PypiReleaseSchema.parse(await response.json());
|
|
193
|
+
const publishedFiles = new Map(
|
|
194
|
+
published.urls.map(({ filename, digests }) => [filename, digests.sha256]),
|
|
195
|
+
);
|
|
196
|
+
for (const [filename, digest] of local.files) {
|
|
197
|
+
const publishedDigest = publishedFiles.get(filename);
|
|
198
|
+
if (!publishedDigest) return false;
|
|
199
|
+
if (publishedDigest !== digest) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`PyPI content does not match ${local.name}==${local.version} ${filename}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
},
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const VerifyPublicationsOptionsSchema = z
|
|
211
|
+
.object({
|
|
212
|
+
version: z.string().trim().min(1).describe("Expected public release version"),
|
|
213
|
+
npmDirectory: z
|
|
214
|
+
.string()
|
|
215
|
+
.trim()
|
|
216
|
+
.min(1)
|
|
217
|
+
.optional()
|
|
218
|
+
.describe("Directory containing released npm archives"),
|
|
219
|
+
pypiDirectory: z
|
|
220
|
+
.string()
|
|
221
|
+
.trim()
|
|
222
|
+
.min(1)
|
|
223
|
+
.optional()
|
|
224
|
+
.describe("Directory containing released Python distributions"),
|
|
225
|
+
concurrency: z.coerce
|
|
226
|
+
.number()
|
|
227
|
+
.int()
|
|
228
|
+
.positive()
|
|
229
|
+
.default(DEFAULT_CONCURRENCY)
|
|
230
|
+
.describe("Maximum concurrent public registry checks"),
|
|
231
|
+
intervalSeconds: z.coerce
|
|
232
|
+
.number()
|
|
233
|
+
.positive()
|
|
234
|
+
.default(DEFAULT_INTERVAL_SECONDS)
|
|
235
|
+
.describe("Delay between shared publication checks"),
|
|
236
|
+
timeoutSeconds: z.coerce
|
|
237
|
+
.number()
|
|
238
|
+
.positive()
|
|
239
|
+
.default(DEFAULT_TIMEOUT_SECONDS)
|
|
240
|
+
.describe("Maximum time to wait for the complete publication batch"),
|
|
241
|
+
})
|
|
242
|
+
.refine((options) => options.npmDirectory || options.pypiDirectory, {
|
|
243
|
+
message: "At least one publication directory is required",
|
|
244
|
+
})
|
|
245
|
+
.describe("Options for the public package availability barrier");
|
|
246
|
+
|
|
247
|
+
/** Parse expected release artifacts and wait for their public registry records. */
|
|
248
|
+
export async function main(args: readonly string[] = process.argv.slice(2)): Promise<void> {
|
|
249
|
+
const options = await taskOptions(
|
|
250
|
+
taskCommand(
|
|
251
|
+
import.meta.url,
|
|
252
|
+
"Wait for released packages to become publicly available",
|
|
253
|
+
VerifyPublicationsOptionsSchema,
|
|
254
|
+
),
|
|
255
|
+
VerifyPublicationsOptionsSchema,
|
|
256
|
+
args,
|
|
257
|
+
);
|
|
258
|
+
const probes = [
|
|
259
|
+
...(options.npmDirectory
|
|
260
|
+
? npmPublicationProbes(resolve(options.npmDirectory), options.version)
|
|
261
|
+
: []),
|
|
262
|
+
...(options.pypiDirectory
|
|
263
|
+
? pypiPublicationProbes(resolve(options.pypiDirectory), options.version)
|
|
264
|
+
: []),
|
|
265
|
+
];
|
|
266
|
+
await waitForPublications(probes, {
|
|
267
|
+
concurrency: options.concurrency,
|
|
268
|
+
intervalMs: options.intervalSeconds * 1_000,
|
|
269
|
+
timeoutMs: options.timeoutSeconds * 1_000,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await runTaskMain(import.meta, main);
|
package/tasks/version-check.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/** Verify that every committed package version matches the root VERSION file. */
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { projectUtils } from "@dbx-tools/core";
|
|
6
|
+
import { findFiles } from "@dbx-tools/path";
|
|
7
|
+
import { log, parseRecord } from "@dbx-tools/shared-core";
|
|
8
8
|
import { parse } from "smol-toml";
|
|
9
9
|
import { runTaskMain } from "./cli.ts";
|
|
10
10
|
import { recordedPackages } from "../src/packages.ts";
|
|
@@ -13,7 +13,7 @@ import { readWorkspaceVersion } from "../src/workspace-version.ts";
|
|
|
13
13
|
const logger = log.logger("projen:version-check");
|
|
14
14
|
|
|
15
15
|
function manifestVersion(path: string): string | undefined {
|
|
16
|
-
const manifest =
|
|
16
|
+
const manifest = parseRecord(readFileSync(path, "utf8"));
|
|
17
17
|
return typeof manifest?.version === "string" ? manifest.version : undefined;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -37,7 +37,7 @@ export async function main(): Promise<void> {
|
|
|
37
37
|
check(`${pkg.path}/package.json`, manifestVersion(join(pkg.dir, "package.json")));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
for await (const path of
|
|
40
|
+
for await (const path of findFiles("**/pyproject.toml", { cwd: root })) {
|
|
41
41
|
check(path, pythonVersion(join(root, path)));
|
|
42
42
|
}
|
|
43
43
|
if (mismatches.length > 0) {
|
|
File without changes
|