@intentius/chant 0.124.35 → 0.124.36
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/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +5 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
- package/dist/lifecycle/__fixtures__/publish-fakes.js +15 -1
- package/dist/lifecycle/publish-declaration.d.ts +2 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
- package/dist/lifecycle/publish-declaration.js +6 -0
- package/dist/lifecycle/publish-npm.d.ts +4 -0
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +44 -1
- package/dist/lifecycle/release-publish.d.ts +4 -0
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/wave-runtime.mjs +4 -4
- package/package.json +1 -1
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +4 -1
- package/src/lifecycle/__fixtures__/publish-fakes.ts +15 -1
- package/src/lifecycle/publish-declaration.ts +6 -0
- package/src/lifecycle/publish-npm.ts +45 -1
- package/src/lifecycle/publish-targets.test.ts +65 -2
- package/src/lifecycle/release-publish.ts +4 -0
package/package.json
CHANGED
|
@@ -160,6 +160,9 @@ describe("chant release publish with targets", () => {
|
|
|
160
160
|
["publish", join(dir, "packages/cli"), "--tag", "next", "--access", "public", "--provenance"],
|
|
161
161
|
["dist-tag", "add", "@acme/core@1.0.0", "latest"],
|
|
162
162
|
["dist-tag", "add", "@acme/cli@1.0.0", "latest"],
|
|
163
|
+
// #4244: the reveal waits until the registry serves each version it published.
|
|
164
|
+
["view", "@acme/core@1.0.0", "version"],
|
|
165
|
+
["view", "@acme/cli@1.0.0", "version"],
|
|
163
166
|
// #4049: in CI, what was published is installed in an empty directory and its signatures and attestations audited.
|
|
164
167
|
["install", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
165
168
|
["audit", "signatures"],
|
|
@@ -212,7 +215,7 @@ describe("chant release publish with targets", () => {
|
|
|
212
215
|
const moved = await chant(dir, ["release", "publish", "v1", "--yes"], { ...path, ...secrets, ...oidc });
|
|
213
216
|
expect(moved.code).toBe(1);
|
|
214
217
|
expect(moved.stderr).toMatch(/\[release-head-mismatch\]/);
|
|
215
|
-
expect(calls()).toHaveLength(
|
|
218
|
+
expect(calls()).toHaveLength(10);
|
|
216
219
|
},
|
|
217
220
|
240_000,
|
|
218
221
|
);
|
|
@@ -155,6 +155,11 @@ export function seedFakeNpmRegistry(log: string, registry: FakeNpmRegistry): voi
|
|
|
155
155
|
* its registry at that version, and installs nothing.
|
|
156
156
|
* - `audit signatures` (#4049) prints what npm prints for verified
|
|
157
157
|
* signatures and attestations, and exits 1 when `$FAKE_NPM_AUDIT_FAIL` is set.
|
|
158
|
+
* - `$FAKE_NPM_LAG` (#4244), `<name>=<n>[,<name>=<n>]`: after a publish of
|
|
159
|
+
* that name, the next `n` views of the version find nothing, as a registry
|
|
160
|
+
* that accepted a publish but does not serve it yet.
|
|
161
|
+
* - `$FAKE_NPM_INSTALL_ETARGET` (#4244), `<n>`: the first `n` installs fail
|
|
162
|
+
* with ETARGET.
|
|
158
163
|
*/
|
|
159
164
|
export function writeFakeNpm(binDir: string, log: string): string {
|
|
160
165
|
mkdirSync(binDir, { recursive: true });
|
|
@@ -186,6 +191,10 @@ if (args[0] === "publish") {
|
|
|
186
191
|
const p = reg.packages[name] || (reg.packages[name] = { versions: [], distTags: {} });
|
|
187
192
|
if (p.versions.includes(version)) { console.error("npm error 403 You cannot publish over the previously published versions: " + version); process.exit(1); }
|
|
188
193
|
p.versions.push(version);
|
|
194
|
+
for (const pair of (process.env.FAKE_NPM_LAG || "").split(",").filter(Boolean)) {
|
|
195
|
+
const eq = pair.lastIndexOf("=");
|
|
196
|
+
if (pair.slice(0, eq) === name) (p.lag || (p.lag = {}))[version] = Number(pair.slice(eq + 1));
|
|
197
|
+
}
|
|
189
198
|
p.distTags[flag("--tag") || "latest"] = version;
|
|
190
199
|
if (!p.distTags.latest) p.distTags.latest = version;
|
|
191
200
|
save();
|
|
@@ -194,7 +203,8 @@ if (args[0] === "publish") {
|
|
|
194
203
|
const [name, version] = split(args[1]);
|
|
195
204
|
const p = reg.packages[name];
|
|
196
205
|
if (!p) { console.error("npm error 404 Not Found - " + name); process.exit(1); }
|
|
197
|
-
if (p.
|
|
206
|
+
if (p.lag && p.lag[version] > 0) { p.lag[version] -= 1; save(); }
|
|
207
|
+
else if (p.versions.includes(version)) console.log(version);
|
|
198
208
|
} else if (args[0] === "dist-tag" && args[1] === "add") {
|
|
199
209
|
const [name, version] = split(args[2]);
|
|
200
210
|
if (process.env.FAKE_NPM_FAIL_TAG && name.includes(process.env.FAKE_NPM_FAIL_TAG)) { console.error("npm error 403 Forbidden - no permission to change dist-tags"); process.exit(1); }
|
|
@@ -204,6 +214,10 @@ if (args[0] === "publish") {
|
|
|
204
214
|
save();
|
|
205
215
|
console.log("+" + args[3] + ": " + args[2]);
|
|
206
216
|
} else if (args[0] === "install") {
|
|
217
|
+
if (Number(process.env.FAKE_NPM_INSTALL_ETARGET || 0) > (reg.installs || 0)) {
|
|
218
|
+
reg.installs = (reg.installs || 0) + 1; save();
|
|
219
|
+
console.error("npm error code ETARGET"); console.error("npm error notarget No matching version found for a package"); process.exit(1);
|
|
220
|
+
}
|
|
207
221
|
const deps = JSON.parse(fs.readFileSync("package.json", "utf-8")).dependencies || {};
|
|
208
222
|
for (const [name, version] of Object.entries(deps)) {
|
|
209
223
|
const p = reg.packages[name];
|
|
@@ -60,6 +60,12 @@ export const NpmPublishSchema = z
|
|
|
60
60
|
* token rather than failing the release. Unset or empty, there is no retry.
|
|
61
61
|
*/
|
|
62
62
|
tokenEnv: envName.optional(),
|
|
63
|
+
/**
|
|
64
|
+
* Seconds the reveal waits for the registry to serve every version it
|
|
65
|
+
* published before installing them to audit (#4244). A registry accepts a
|
|
66
|
+
* publish before it serves it. Default 600.
|
|
67
|
+
*/
|
|
68
|
+
registryWait: z.number().int().min(0).optional(),
|
|
63
69
|
})
|
|
64
70
|
.strict();
|
|
65
71
|
|
|
@@ -160,8 +160,17 @@ function run(bin: string, args: string[], ctx: PublishContext, cwd: string = ctx
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** Seconds the reveal waits for the registry to serve what it published, by default (#4244). */
|
|
164
|
+
export const NPM_REGISTRY_WAIT_SECONDS = 600;
|
|
165
|
+
/** Between polls of the registry while it waits. */
|
|
166
|
+
export const NPM_REGISTRY_POLL_MS = 10_000;
|
|
167
|
+
/** npm's errors for a version the registry does not serve yet. */
|
|
168
|
+
const NPM_NOT_SERVED = /\b(ETARGET|ERESOLVE|notarget)\b|No matching version found/;
|
|
169
|
+
|
|
163
170
|
export function npmTarget(decl: NpmPublishDeclaration, deps: PublishDeps): PublishTarget {
|
|
164
171
|
const bin = deps.npm ?? "npm";
|
|
172
|
+
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
173
|
+
const now = deps.now ?? Date.now;
|
|
165
174
|
const registry = decl.registry ?? "the registry npm's config names";
|
|
166
175
|
const shown = (tag: string) => `${registry}, dist-tag ${tag}`;
|
|
167
176
|
const revealTag = (ctx: PublishContext) => ctx.tag ?? "latest";
|
|
@@ -216,15 +225,50 @@ export function npmTarget(decl: NpmPublishDeclaration, deps: PublishDeps): Publi
|
|
|
216
225
|
* `npm audit signatures` there (#4049), which checks each package's
|
|
217
226
|
* registry signature and provenance attestation.
|
|
218
227
|
*/
|
|
228
|
+
/**
|
|
229
|
+
* Wait until the registry serves every version (#4244): npm accepts a
|
|
230
|
+
* publish minutes before `npm view` and `npm install` find it, so an
|
|
231
|
+
* install straight after fails with ETARGET or ERESOLVE.
|
|
232
|
+
*/
|
|
233
|
+
const served = async (ctx: PublishContext, pkgs: NpmPackage[]): Promise<void> => {
|
|
234
|
+
const wait = (decl.registryWait ?? NPM_REGISTRY_WAIT_SECONDS) * 1000;
|
|
235
|
+
const start = now();
|
|
236
|
+
let missing = pkgs;
|
|
237
|
+
for (;;) {
|
|
238
|
+
const still: NpmPackage[] = [];
|
|
239
|
+
for (const pkg of missing) if (!(await onRegistry(ctx, pkg))) still.push(pkg);
|
|
240
|
+
missing = still;
|
|
241
|
+
if (missing.length === 0) return;
|
|
242
|
+
const names = missing.map((p) => `${p.name}@${p.version}`).join(", ");
|
|
243
|
+
if (now() - start >= wait) {
|
|
244
|
+
throw new PublishTargetError(
|
|
245
|
+
"publish-npm-not-served",
|
|
246
|
+
`the registry did not serve ${names} within ${Math.round(wait / 1000)}s of the publish, so nothing was audited`,
|
|
247
|
+
"Each package was published. Once npm serves them, run the publish again: it publishes nothing twice and audits again. `registryWait` sets the wait.",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
ctx.log(`waiting for the registry to serve ${names}`);
|
|
251
|
+
await sleep(NPM_REGISTRY_POLL_MS);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
219
254
|
const audit = async (ctx: PublishContext, pkgs: NpmPackage[]): Promise<void> => {
|
|
255
|
+
await served(ctx, pkgs);
|
|
220
256
|
const dir = mkdtempSync(join(tmpdir(), "chant-npm-audit-"));
|
|
221
257
|
try {
|
|
222
258
|
const dependencies = Object.fromEntries(pkgs.map((p) => [p.name, p.version]));
|
|
223
259
|
writeFileSync(join(dir, "package.json"), `${JSON.stringify({ name: "chant-publish-audit", version: "0.0.0", private: true, dependencies }, null, 2)}\n`);
|
|
224
260
|
for (const args of [npmAuditInstallArgs(decl), npmAuditSignaturesArgs(decl)]) {
|
|
225
261
|
ctx.log(`npm ${args.join(" ")}`);
|
|
226
|
-
|
|
262
|
+
let result = await npm(ctx, args, dir);
|
|
227
263
|
for (const line of result.output) ctx.log(` ${line}`);
|
|
264
|
+
// `npm view` can answer before every mirror `npm install` reads does: wait once more, then install again.
|
|
265
|
+
if (result.code !== 0 && args[0] === "install" && result.output.some((l) => NPM_NOT_SERVED.test(l))) {
|
|
266
|
+
ctx.log("the registry does not serve every version to an install yet; waiting, then installing again");
|
|
267
|
+
await sleep(NPM_REGISTRY_POLL_MS);
|
|
268
|
+
await served(ctx, pkgs);
|
|
269
|
+
result = await npm(ctx, args, dir);
|
|
270
|
+
for (const line of result.output) ctx.log(` ${line}`);
|
|
271
|
+
}
|
|
228
272
|
if (result.code !== 0) {
|
|
229
273
|
throw new PublishTargetError(
|
|
230
274
|
"publish-npm-audit-failed",
|
|
@@ -136,6 +136,10 @@ describe("npm", () => {
|
|
|
136
136
|
["dist-tag", "add", "@acme/core@1.4.0", "latest"],
|
|
137
137
|
["dist-tag", "add", "@acme/cli@1.4.0", "latest"],
|
|
138
138
|
["dist-tag", "add", "@acme/kit@1.4.0", "latest"],
|
|
139
|
+
// #4244: it first asks the registry for each version it published.
|
|
140
|
+
["view", "@acme/core@1.4.0", "version"],
|
|
141
|
+
["view", "@acme/cli@1.4.0", "version"],
|
|
142
|
+
["view", "@acme/kit@1.4.0", "version"],
|
|
139
143
|
// #4049: with an OIDC token, what was published is installed in an empty directory and its signatures audited.
|
|
140
144
|
["install", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
141
145
|
["audit", "signatures"],
|
|
@@ -179,8 +183,8 @@ describe("npm", () => {
|
|
|
179
183
|
["publish", join(dir, "packages/core"), "--tag", "latest", "--access", "public", "--provenance"],
|
|
180
184
|
["publish", join(dir, "kit.tgz"), "--tag", "latest", "--access", "public", "--provenance"],
|
|
181
185
|
]);
|
|
182
|
-
// The reveal only audits.
|
|
183
|
-
expect(calls(log).slice(before).map((c) => c.args[0])).toEqual(["install", "audit"]);
|
|
186
|
+
// The reveal only audits, once the registry serves both (#4244).
|
|
187
|
+
expect(calls(log).slice(before).map((c) => c.args[0])).toEqual(["view", "view", "install", "audit"]);
|
|
184
188
|
expect(done.items.map((i) => i.destination)).toEqual(Array(2).fill("the registry npm's config names, dist-tag latest"));
|
|
185
189
|
expect(readFakeNpmRegistry(log).packages["@acme/core"]!.distTags.next).toBeUndefined();
|
|
186
190
|
});
|
|
@@ -481,6 +485,65 @@ describe("SBOMs and provenance (#4049)", () => {
|
|
|
481
485
|
expect(readFileSync(log, "utf-8")).toContain(`"args":["audit","signatures"]`);
|
|
482
486
|
});
|
|
483
487
|
|
|
488
|
+
describe("npm: the reveal waits for the registry to serve what it published (#4244)", () => {
|
|
489
|
+
function lagged(env: Record<string, string>, registryWait?: number) {
|
|
490
|
+
const dir = temp();
|
|
491
|
+
writePackage(join(dir, "core"), "@acme/core", "1.0.0");
|
|
492
|
+
writePackage(join(dir, "cli"), "@acme/cli", "1.0.0");
|
|
493
|
+
const log = join(dir, "npm.log");
|
|
494
|
+
const npm = writeFakeNpm(join(dir, "bin"), log);
|
|
495
|
+
let clock = 0;
|
|
496
|
+
const sleeps: number[] = [];
|
|
497
|
+
const deps = { npm, now: () => clock, sleep: async (ms: number) => { sleeps.push(ms); clock += ms; } };
|
|
498
|
+
const ctx = context(dir, { env: { PATH: process.env.PATH, ...env }, provenance: OIDC });
|
|
499
|
+
const d = decl({ type: "npm", packages: ["core", "cli"], stage: false, ...(registryWait !== undefined ? { registryWait } : {}) });
|
|
500
|
+
return { log, deps, ctx, d, sleeps };
|
|
501
|
+
}
|
|
502
|
+
const installs = (log: string) => readFileSync(log, "utf-8").split("\n").filter((l) => l.includes(`"args":["install"`)).length;
|
|
503
|
+
|
|
504
|
+
test("every version served at once: no wait, one install", async () => {
|
|
505
|
+
const { log, deps, ctx, d, sleeps } = lagged({});
|
|
506
|
+
const [target] = await createPublishTargets([d], deps);
|
|
507
|
+
await target!.reveal(ctx, await target!.stage(ctx));
|
|
508
|
+
expect(sleeps).toEqual([]);
|
|
509
|
+
expect(installs(log)).toBe(1);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("one version served late: it waits, names it, then installs once", async () => {
|
|
513
|
+
const { log, deps, ctx, d, sleeps } = lagged({ FAKE_NPM_LAG: "@acme/cli=2" });
|
|
514
|
+
const [target] = await createPublishTargets([d], deps);
|
|
515
|
+
await target!.reveal(ctx, await target!.stage(ctx));
|
|
516
|
+
expect(sleeps).toEqual([10_000, 10_000]);
|
|
517
|
+
expect(ctx.lines).toContain("waiting for the registry to serve @acme/cli@1.0.0");
|
|
518
|
+
expect(installs(log)).toBe(1);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("a version never served: the wait ends with an error naming it, before any install", async () => {
|
|
522
|
+
const { log, deps, ctx, d } = lagged({ FAKE_NPM_LAG: "@acme/core=1000" }, 30);
|
|
523
|
+
const [target] = await createPublishTargets([d], deps);
|
|
524
|
+
const err = await refused(target!.reveal(ctx, await target!.stage(ctx)));
|
|
525
|
+
expect(err.code).toBe("publish-npm-not-served");
|
|
526
|
+
expect(err.message).toBe("the registry did not serve @acme/core@1.0.0 within 30s of the publish, so nothing was audited");
|
|
527
|
+
expect(installs(log)).toBe(0);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("an install that fails with ETARGET waits and installs once more", async () => {
|
|
531
|
+
const { log, deps, ctx, d, sleeps } = lagged({ FAKE_NPM_INSTALL_ETARGET: "1" });
|
|
532
|
+
const [target] = await createPublishTargets([d], deps);
|
|
533
|
+
await target!.reveal(ctx, await target!.stage(ctx));
|
|
534
|
+
expect(installs(log)).toBe(2);
|
|
535
|
+
expect(sleeps).toEqual([10_000]);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("a second ETARGET is the audit's failure, not a third install", async () => {
|
|
539
|
+
const { log, deps, ctx, d } = lagged({ FAKE_NPM_INSTALL_ETARGET: "2" });
|
|
540
|
+
const [target] = await createPublishTargets([d], deps);
|
|
541
|
+
const err = await refused(target!.reveal(ctx, await target!.stage(ctx)));
|
|
542
|
+
expect(err.code).toBe("publish-npm-audit-failed");
|
|
543
|
+
expect(installs(log)).toBe(2);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
484
547
|
test("OCI: the provenance statement carries the cut's signature, says a workstation's provenance is the signature, and is the same bytes every run; a declared SBOM is pushed as it is", async () => {
|
|
485
548
|
const reg = await startFakeRegistry({ username: "robot", password: "pw-attach", token: "tok-attach" });
|
|
486
549
|
try {
|
|
@@ -129,6 +129,10 @@ export interface PublishDeps {
|
|
|
129
129
|
fetch?: PublishFetch;
|
|
130
130
|
/** The npm executable. Default: `npm` on `PATH`. */
|
|
131
131
|
npm?: string;
|
|
132
|
+
/** How a target waits between polls of a registry (#4244). Default: a timer. */
|
|
133
|
+
sleep?: (ms: number) => Promise<void>;
|
|
134
|
+
/** The clock a target's wait reads (#4244). Default: `Date.now`. */
|
|
135
|
+
now?: () => number;
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
/** The subset of the WHATWG `fetch` the targets use. */
|