@gjsify/cli 0.45.0 → 0.47.0
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/cli.gjs.mjs +144 -135
- package/lib/commands/onboard.js +11 -0
- package/lib/commands/publish.d.ts +41 -0
- package/lib/commands/publish.js +222 -2
- package/lib/commands/ship.js +89 -76
- package/lib/commands/upgrade.d.ts +23 -0
- package/lib/commands/upgrade.js +95 -12
- package/lib/templates/install.mjs.tmpl +2 -2
- package/lib/types/config-data.d.ts +64 -0
- package/lib/utils/gi-search-path.js +11 -3
- package/lib/utils/publish-readback.d.ts +132 -0
- package/lib/utils/publish-readback.js +273 -0
- package/lib/utils/ship/discover.js +85 -0
- package/lib/utils/ship/formats.d.ts +23 -0
- package/lib/utils/ship/formats.js +44 -0
- package/lib/utils/ship/launcher.js +57 -0
- package/lib/utils/ship/layout.d.ts +64 -2
- package/lib/utils/ship/layout.js +169 -12
- package/lib/utils/ship/msi.js +24 -7
- package/lib/utils/ship/payload.js +22 -3
- package/lib/utils/ship/pe-launcher.d.ts +29 -0
- package/lib/utils/ship/pe-launcher.js +663 -0
- package/lib/utils/ship/plan.js +35 -0
- package/lib/utils/ship/plist.d.ts +41 -1
- package/lib/utils/ship/plist.js +59 -1
- package/lib/utils/ship/rpm.js +7 -0
- package/lib/utils/ship/settings.d.ts +76 -5
- package/lib/utils/ship/settings.js +89 -8
- package/lib/utils/ship/share-dirs.d.ts +30 -0
- package/lib/utils/ship/share-dirs.js +30 -0
- package/lib/utils/ship/signing.d.ts +122 -6
- package/lib/utils/ship/signing.js +256 -67
- package/lib/utils/ship/stage-writer.js +20 -1
- package/lib/utils/ship/types.d.ts +34 -2
- package/lib/utils/system-gi.d.ts +20 -5
- package/lib/utils/system-gi.js +36 -13
- package/package.json +19 -19
package/lib/commands/onboard.js
CHANGED
|
@@ -660,6 +660,17 @@ function describePublishFailure(pub) {
|
|
|
660
660
|
return 'OIDC/token auth failed';
|
|
661
661
|
case 'diagnostic':
|
|
662
662
|
return `HTTP 404 (${pub.diag.reason})`;
|
|
663
|
+
case 'publish-unconfirmed':
|
|
664
|
+
// npm claimed the version and the registry does not serve it. Counted
|
|
665
|
+
// as a FAILURE here on purpose: onboard's next step is `configureTrust`
|
|
666
|
+
// on a package it just decided exists. `onboard` passes `tolerate:
|
|
667
|
+
// true`, so `already-published` is the claim it meets on a RE-RUN —
|
|
668
|
+
// and #1407 is exactly a re-run that counted a half-stored publish
|
|
669
|
+
// as done.
|
|
670
|
+
return (`npm ${pub.claim === 'already-published' ? 'reported it already published' : 'accepted the upload'} ` +
|
|
671
|
+
`(HTTP ${pub.putStatus}) but ${pub.name}@${pub.version} did not ` +
|
|
672
|
+
`resolve on ${pub.registry} after ${pub.readback.attempts} probe(s) over ` +
|
|
673
|
+
`${(pub.readback.elapsedMs / 1000).toFixed(1)}s`);
|
|
663
674
|
case 'error':
|
|
664
675
|
return `HTTP ${pub.status} ${pub.statusText}`;
|
|
665
676
|
default:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
2
|
import { type Diagnose404Result } from '../utils/publish-diagnose.js';
|
|
3
|
+
import { type ReadbackResult } from '../utils/publish-readback.js';
|
|
3
4
|
import { OtpProvider } from '../utils/npm-otp.js';
|
|
4
5
|
interface PublishOptions {
|
|
5
6
|
path?: string;
|
|
@@ -8,6 +9,8 @@ interface PublishOptions {
|
|
|
8
9
|
otp?: string;
|
|
9
10
|
'tolerate-republish'?: boolean;
|
|
10
11
|
'tolerate-untrusted-new'?: boolean;
|
|
12
|
+
'verify-timeout'?: number;
|
|
13
|
+
'verify-defer'?: boolean;
|
|
11
14
|
provenance?: boolean;
|
|
12
15
|
'dry-run'?: boolean;
|
|
13
16
|
json?: boolean;
|
|
@@ -37,6 +40,13 @@ export interface PublishWorkspaceInput {
|
|
|
37
40
|
otpProvider: OtpProvider;
|
|
38
41
|
/** Send the cached code on the first PUT (publish `--otp`). */
|
|
39
42
|
seedOtpFirst: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Polling budget for the post-PUT read-back, in ms. Default
|
|
45
|
+
* {@link DEFAULT_VERIFY_BUDGET_MS}; `0` skips the read-back entirely and the
|
|
46
|
+
* returned `published` outcome then carries no `readback` — an UNVERIFIED
|
|
47
|
+
* success, which is the pre-v0.46.0 behaviour and why this defaults on.
|
|
48
|
+
*/
|
|
49
|
+
verifyBudgetMs?: number;
|
|
40
50
|
}
|
|
41
51
|
/** Discriminated result of {@link publishWorkspace}; the caller owns presentation. */
|
|
42
52
|
export type PublishOutcome = {
|
|
@@ -49,12 +59,43 @@ export type PublishOutcome = {
|
|
|
49
59
|
integrity: string;
|
|
50
60
|
tag: string;
|
|
51
61
|
registry: string;
|
|
62
|
+
/** The read-back that CONFIRMED it. `undefined` only when disabled. */
|
|
63
|
+
readback?: ReadbackResult;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* npm returned 2xx and the registry does not serve the version. Its own
|
|
67
|
+
* outcome on purpose: not `published` (nothing is installable), not
|
|
68
|
+
* `republish-tolerated` (npm never claimed it existed), not `error` (the
|
|
69
|
+
* HTTP exchange succeeded). The v0.46.0 incident had no name at all — it
|
|
70
|
+
* came out as `+ name@version`, exit 0.
|
|
71
|
+
*/
|
|
72
|
+
| {
|
|
73
|
+
ok: false;
|
|
74
|
+
action: 'publish-unconfirmed';
|
|
75
|
+
/**
|
|
76
|
+
* What npm claimed before the read-back disagreed: a 2xx (`accepted`)
|
|
77
|
+
* or a tolerated 409 (`already-published`). It selects the remediation
|
|
78
|
+
* sentence, which is the OPPOSITE one for a conflict — re-running
|
|
79
|
+
* answers the same 409.
|
|
80
|
+
*/
|
|
81
|
+
claim: 'accepted' | 'already-published';
|
|
82
|
+
name: string;
|
|
83
|
+
version: string;
|
|
84
|
+
registry: string;
|
|
85
|
+
/** The PUT that was accepted. */
|
|
86
|
+
putStatus: number;
|
|
87
|
+
putStatusText: string;
|
|
88
|
+
putUrl: string;
|
|
89
|
+
payloadBytes: number;
|
|
90
|
+
readback: ReadbackResult;
|
|
52
91
|
} | {
|
|
53
92
|
ok: true;
|
|
54
93
|
action: 'republish-tolerated';
|
|
55
94
|
name: string;
|
|
56
95
|
version: string;
|
|
57
96
|
status: number;
|
|
97
|
+
/** The read-back that CONFIRMED the conflicting version. */
|
|
98
|
+
readback?: ReadbackResult;
|
|
58
99
|
} | {
|
|
59
100
|
ok: true;
|
|
60
101
|
action: 'skipped-untrusted-new';
|
package/lib/commands/publish.js
CHANGED
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
// a sigstore signer that gjsify doesn't ship; we record the flag in the
|
|
20
20
|
// publish manifest but don't sign yet. Surface a warning so callers know.
|
|
21
21
|
// - --dry-run: pack only, no PUT.
|
|
22
|
+
// - --verify-timeout / --verify-defer: the post-PUT READ-BACK. npm's 2xx is an
|
|
23
|
+
// accepted write, not a durable one, so a successful PUT is confirmed by
|
|
24
|
+
// asking the registry for that exact version before `+ name@version` is
|
|
25
|
+
// printed. The incident, the measured lag distribution and the choice of
|
|
26
|
+
// window live in `utils/publish-readback.ts`; WHERE the read-back runs and
|
|
27
|
+
// what it costs is argued at its call site below.
|
|
22
28
|
//
|
|
23
29
|
// The request body matches npm's "publish" payload shape:
|
|
24
30
|
// {
|
|
@@ -41,6 +47,7 @@ import { buildPublishHeaders, escapePackageName } from '../utils/publish-headers
|
|
|
41
47
|
import { packWorkspace } from './pack.js';
|
|
42
48
|
import { getNpmTrustedToken, hasGithubOidcEnv, OidcExchangeError, OidcUnavailableError } from '../utils/npm-oidc.js';
|
|
43
49
|
import { diagnose404, is404DiagnosticCandidate } from '../utils/publish-diagnose.js';
|
|
50
|
+
import { DEFAULT_VERIFY_BUDGET_MS, formatUnconfirmedPublish, verifyPublishedVersion, } from '../utils/publish-readback.js';
|
|
44
51
|
import { loadNpmrc } from '../utils/load-npmrc.js';
|
|
45
52
|
import { OtpProvider, withOtpRetry, isOtpChallenge } from '../utils/npm-otp.js';
|
|
46
53
|
import { promptLine } from '../utils/prompt.js';
|
|
@@ -71,6 +78,16 @@ export const publishCommand = {
|
|
|
71
78
|
description: 'Skip (exit 0) when OIDC token exchange returns `package not found` AND no fallback token is configured — i.e. a never-before-published `@scope/<name>` whose Trusted Publisher entry hasn\'t been set up on npmjs.com yet. Without this flag, one un-bootstrapped new package breaks the entire serialized `gjsify foreach publish` loop. Pair with `--tolerate-republish` in CI release workflows so a fresh-merged package gracefully skips its first CI publish, leaving the manual-bootstrap step to a maintainer (see AGENTS.md "New @gjsify/* package: first-publish + Trusted Publisher bootstrap").',
|
|
72
79
|
type: 'boolean',
|
|
73
80
|
default: false,
|
|
81
|
+
})
|
|
82
|
+
.option('verify-timeout', {
|
|
83
|
+
description: 'Seconds to keep asking the registry for the just-published version before giving up. npm answers 2xx to a write it has only ACCEPTED: in the v0.46.0 release 19 of 199 packages were recorded 56-252s after their 2xx and one was never recorded at all, so a shorter window turns normal queueing into a false red. `0` disables the read-back — for a registry with no packument read path.',
|
|
84
|
+
type: 'number',
|
|
85
|
+
default: DEFAULT_VERIFY_BUDGET_MS / 1000,
|
|
86
|
+
})
|
|
87
|
+
.option('verify-defer', {
|
|
88
|
+
description: 'Report an unconfirmed read-back and exit 0 instead of 1. ONLY for a caller that re-checks the same set afterwards — `gjsify run npm:publish` passes it because `scripts/verify-published-closure.mjs` sweeps the whole roster at the end of release.yml. Everywhere else an unconfirmed publish must fail the job that made it.',
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
default: false,
|
|
74
91
|
})
|
|
75
92
|
.option('provenance', {
|
|
76
93
|
description: "Pass-through flag — recorded in the payload but no signing happens (gjsify doesn't ship a sigstore signer yet).",
|
|
@@ -107,6 +124,17 @@ export const publishCommand = {
|
|
|
107
124
|
const otp = args.otp;
|
|
108
125
|
const tolerate = args['tolerate-republish'] === true;
|
|
109
126
|
const tolerateUntrustedNew = args['tolerate-untrusted-new'] === true;
|
|
127
|
+
const verifySeconds = args['verify-timeout'] ?? DEFAULT_VERIFY_BUDGET_MS / 1000;
|
|
128
|
+
// A non-number reaches here as NaN (yargs coerces `--verify-timeout abc`
|
|
129
|
+
// silently), and NaN would fall through every `> 0` test and turn the
|
|
130
|
+
// read-back OFF without saying so — the failure mode this whole change
|
|
131
|
+
// exists to remove. Refuse it instead.
|
|
132
|
+
if (!Number.isFinite(verifySeconds) || verifySeconds < 0) {
|
|
133
|
+
console.error(`gjsify publish: --verify-timeout must be a non-negative number of seconds, got ${JSON.stringify(args['verify-timeout'])}`);
|
|
134
|
+
return process.exit(2);
|
|
135
|
+
}
|
|
136
|
+
const verifyBudgetMs = Math.round(verifySeconds * 1000);
|
|
137
|
+
const deferUnconfirmed = args['verify-defer'] === true;
|
|
110
138
|
const provenance = args.provenance === true;
|
|
111
139
|
const dryRun = args['dry-run'] === true;
|
|
112
140
|
const checkTrustedOnly = args['check-trusted'] === true;
|
|
@@ -219,8 +247,9 @@ export const publishCommand = {
|
|
|
219
247
|
json: args.json === true,
|
|
220
248
|
otpProvider,
|
|
221
249
|
seedOtpFirst: otp !== undefined,
|
|
250
|
+
verifyBudgetMs,
|
|
222
251
|
});
|
|
223
|
-
reportPublishOutcome(outcome, args.json === true);
|
|
252
|
+
reportPublishOutcome(outcome, args.json === true, deferUnconfirmed);
|
|
224
253
|
},
|
|
225
254
|
};
|
|
226
255
|
/**
|
|
@@ -329,12 +358,128 @@ export async function publishWorkspace(input) {
|
|
|
329
358
|
return fetch(url, { method: 'PUT', headers: reqHeaders, body: bodyStr });
|
|
330
359
|
};
|
|
331
360
|
const res = await withOtpRetry(doPut, otpProvider, { seedFirstAttempt: input.seedOtpFirst });
|
|
361
|
+
if (verbose) {
|
|
362
|
+
// The debug output used to print the request and then the `+` success
|
|
363
|
+
// line, so a 200 and a 201 were indistinguishable and no body was ever
|
|
364
|
+
// recorded — which is why reconstructing the v0.46.0 incident needed the
|
|
365
|
+
// registry rather than the log it already had. Print what npm answered.
|
|
366
|
+
// `clone()` because the error path below reads `res.text()` itself.
|
|
367
|
+
console.error(` response: ${res.status} ${res.statusText}`);
|
|
368
|
+
const peek = await res
|
|
369
|
+
.clone()
|
|
370
|
+
.text()
|
|
371
|
+
.catch(() => '');
|
|
372
|
+
if (peek)
|
|
373
|
+
console.error(` body: ${peek.slice(0, 300)}`);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Ask the registry for what we just PUT. `undefined` when the read-back is
|
|
377
|
+
* off (`--verify-timeout 0`).
|
|
378
|
+
*
|
|
379
|
+
* The PUT's own `authorization` goes with it: a registry that requires a
|
|
380
|
+
* credential to READ answers 401/403 to an anonymous packument GET, which is
|
|
381
|
+
* an `error` probe, so an intact publish to GitHub Packages or an
|
|
382
|
+
* authenticated Verdaccio would spend the whole budget and then report
|
|
383
|
+
* `publish-unconfirmed`. Same origin, same credential, one request later.
|
|
384
|
+
*/
|
|
385
|
+
const readBack = async () => {
|
|
386
|
+
const budgetMs = input.verifyBudgetMs ?? DEFAULT_VERIFY_BUDGET_MS;
|
|
387
|
+
if (budgetMs <= 0)
|
|
388
|
+
return undefined;
|
|
389
|
+
return verifyPublishedVersion({
|
|
390
|
+
registry: registryClean,
|
|
391
|
+
name: packed.name,
|
|
392
|
+
version: packed.version,
|
|
393
|
+
budgetMs,
|
|
394
|
+
authorization: headers['authorization'],
|
|
395
|
+
log: verbose ? (m) => console.error(m) : undefined,
|
|
396
|
+
});
|
|
397
|
+
};
|
|
332
398
|
// A surviving OTP challenge means the provider gave up (no code entered /
|
|
333
399
|
// non-interactive). Surface it so the caller can point at --otp.
|
|
334
400
|
if (await isOtpChallenge(res)) {
|
|
335
401
|
return { ok: false, action: 'otp-required', name: packed.name, version: packed.version, status: res.status };
|
|
336
402
|
}
|
|
337
403
|
if (res.ok) {
|
|
404
|
+
// WHERE THE READ-BACK BELONGS — per package, here, immediately.
|
|
405
|
+
//
|
|
406
|
+
// This path runs ~209 times per release (199 in the serial
|
|
407
|
+
// `foreach --exec` sweep behind `npm:publish`, plus the per-bundle
|
|
408
|
+
// single-package jobs), so its cost is measured rather than assumed:
|
|
409
|
+
//
|
|
410
|
+
// CONFIRMED ON THE FIRST PROBE — 180 of 199 packages in v0.46.0. One
|
|
411
|
+
// abbreviated-packument GET, 0.59 s against registry.npmjs.org, so
|
|
412
|
+
// ~2 min across the whole sweep against a publish job that ran 33 min.
|
|
413
|
+
//
|
|
414
|
+
// NOT CONFIRMED YET — the other 19. Here the read-back waits out
|
|
415
|
+
// npm's own write queue, and those residual lags SUM TO 2554 s: a
|
|
416
|
+
// sweep that waited them all out with the 300 s default would have
|
|
417
|
+
// added ~43 min to that 33 min job. That single number decides the
|
|
418
|
+
// shape below.
|
|
419
|
+
//
|
|
420
|
+
// A BATCHED END-OF-SWEEP CHECK CANNOT LIVE IN THIS FILE. `npm:publish`
|
|
421
|
+
// is `gjsify foreach --exec -- gjsify publish`, one CHILD PROCESS per
|
|
422
|
+
// package: there is no in-process hook after the last one to collect a
|
|
423
|
+
// pending set in. So the batched half already sits where it can —
|
|
424
|
+
// `scripts/verify-published-closure.mjs`, its own job at the end of
|
|
425
|
+
// `release.yml`, rounds and retries over the whole roster. It is what
|
|
426
|
+
// named the missing package in the incident.
|
|
427
|
+
//
|
|
428
|
+
// AND A FATAL READ-BACK INSIDE THAT SWEEP WOULD ABORT THE RELEASE, which
|
|
429
|
+
// is the argument the 2554 s only quantifies. `foreach --exec` THROWS on
|
|
430
|
+
// a non-zero child (`foreach.ts`: `exited with code ${result.code}`),
|
|
431
|
+
// and pack time has already rewritten every `workspace:*` to the exact
|
|
432
|
+
// released version — so one lagging package would stop the sweep
|
|
433
|
+
// mid-roster and leave a PARTIAL publish, the state
|
|
434
|
+
// `verify-published-closure.mjs` exists because it is worse than no
|
|
435
|
+
// publish at all. A deferral here is not leniency; it is the only shape
|
|
436
|
+
// that does not trade this incident for that one.
|
|
437
|
+
//
|
|
438
|
+
// Hence BOTH, split along that cost. The read-back runs per package
|
|
439
|
+
// everywhere and is FATAL by default — that is what turns the
|
|
440
|
+
// incident's green single-package job red, for up to 300 s of
|
|
441
|
+
// patience on a job that otherwise ran 59 s. The
|
|
442
|
+
// 199-package sweep passes `--verify-defer --verify-timeout 5`: it
|
|
443
|
+
// still CONFIRMS nine packages in ten and still names the suspect
|
|
444
|
+
// against the package that PUT it, but the minutes-long tail is left to
|
|
445
|
+
// the closure job that re-asks the same question over the same set —
|
|
446
|
+
// which had to be MADE able to answer it (`dist.tarball`, and a
|
|
447
|
+
// propagation retry that never fired for a roster-only name).
|
|
448
|
+
//
|
|
449
|
+
// What the deferred annotation is NOT is the ledger. At a 5 s budget
|
|
450
|
+
// every one of a release's ~19 lagging packages emits one, and GitHub
|
|
451
|
+
// renders only the first few annotations per step (documented as 10 per
|
|
452
|
+
// level; not verified from a run here). The closure job is the fatal
|
|
453
|
+
// half; the annotation points a human at the name.
|
|
454
|
+
const readback = await readBack();
|
|
455
|
+
if (readback?.confirmed && verbose) {
|
|
456
|
+
// The CONFIRMED half of the read-back is printed too, and it is not
|
|
457
|
+
// decoration: it is the only client-side measurement of when a
|
|
458
|
+
// version became RESOLVABLE, as against `time[version]`, which is
|
|
459
|
+
// when the registry recorded the write. The two are assumed equal by
|
|
460
|
+
// the 4.2-minute figure and the 300 s window, and nothing had ever
|
|
461
|
+
// measured the gap. The pre-registered comparison — and what each
|
|
462
|
+
// outcome would change — is in #1509's description; this line, under
|
|
463
|
+
// GJSIFY_PUBLISH_DEBUG (which release.yml sets), is its input. Note
|
|
464
|
+
// the value is quantised by the backoff grid, so it is the UPPER end
|
|
465
|
+
// of a bracket whose lower end is the previous probe.
|
|
466
|
+
console.error(` read-back: confirmed after ${readback.attempts} probe(s) in ${readback.elapsedMs} ms`);
|
|
467
|
+
}
|
|
468
|
+
if (readback && !readback.confirmed) {
|
|
469
|
+
return {
|
|
470
|
+
ok: false,
|
|
471
|
+
action: 'publish-unconfirmed',
|
|
472
|
+
claim: 'accepted',
|
|
473
|
+
name: packed.name,
|
|
474
|
+
version: packed.version,
|
|
475
|
+
registry: registryClean,
|
|
476
|
+
putStatus: res.status,
|
|
477
|
+
putStatusText: res.statusText,
|
|
478
|
+
putUrl: url,
|
|
479
|
+
payloadBytes: bodyStr.length,
|
|
480
|
+
readback,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
338
483
|
return {
|
|
339
484
|
ok: true,
|
|
340
485
|
action: 'published',
|
|
@@ -345,18 +490,50 @@ export async function publishWorkspace(input) {
|
|
|
345
490
|
integrity: packed.integrity,
|
|
346
491
|
tag,
|
|
347
492
|
registry: registryClean,
|
|
493
|
+
readback,
|
|
348
494
|
};
|
|
349
495
|
}
|
|
350
496
|
const text = await res.text().catch(() => '<no body>');
|
|
351
497
|
// "version already published" — 409 Conflict, or 403 + "previously published".
|
|
498
|
+
//
|
|
499
|
+
// READ BACK HERE TOO, and the reason it was first written the other way is
|
|
500
|
+
// worth keeping: a 2xx is npm accepting a write it has not necessarily
|
|
501
|
+
// durably applied, whereas a 409 is npm ASSERTING the version exists and
|
|
502
|
+
// refusing to overwrite it — sounding like a positive statement about the
|
|
503
|
+
// registry's own state rather than a promise about a queued one. The
|
|
504
|
+
// incident's own recovery says otherwise. Attempt 3 re-PUT
|
|
505
|
+
// @gjsify/node-runtime-darwin-arm64 and was answered `409 already
|
|
506
|
+
// published` at 09:48:49.19, while that packument's `time["0.46.0"]` is
|
|
507
|
+
// 09:49:07.419 — 18 s LATER — and the closure job had found the name absent
|
|
508
|
+
// at 09:44:28. So the assertion can precede the version being served, and
|
|
509
|
+
// this is the path a `publish-unconfirmed` re-run always lands on: exempting
|
|
510
|
+
// it would leave the documented remediation reporting a success nothing
|
|
511
|
+
// checked. Cost is one GET, on a path that only runs on a re-publish.
|
|
352
512
|
const isRepublishConflict = res.status === 409 || (res.status === 403 && /previously published/i.test(text));
|
|
353
513
|
if (isRepublishConflict && tolerate) {
|
|
514
|
+
const readback = await readBack();
|
|
515
|
+
if (readback && !readback.confirmed) {
|
|
516
|
+
return {
|
|
517
|
+
ok: false,
|
|
518
|
+
action: 'publish-unconfirmed',
|
|
519
|
+
claim: 'already-published',
|
|
520
|
+
name: packed.name,
|
|
521
|
+
version: packed.version,
|
|
522
|
+
registry: registryClean,
|
|
523
|
+
putStatus: res.status,
|
|
524
|
+
putStatusText: res.statusText,
|
|
525
|
+
putUrl: url,
|
|
526
|
+
payloadBytes: bodyStr.length,
|
|
527
|
+
readback,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
354
530
|
return {
|
|
355
531
|
ok: true,
|
|
356
532
|
action: 'republish-tolerated',
|
|
357
533
|
name: packed.name,
|
|
358
534
|
version: packed.version,
|
|
359
535
|
status: res.status,
|
|
536
|
+
readback,
|
|
360
537
|
};
|
|
361
538
|
}
|
|
362
539
|
// 404 diagnostic (token-auth): disambiguate dead-token vs missing-package.
|
|
@@ -384,8 +561,13 @@ export async function publishWorkspace(input) {
|
|
|
384
561
|
/**
|
|
385
562
|
* Present a {@link PublishOutcome} on stdout/stderr and exit non-zero on
|
|
386
563
|
* failure — the single-command `gjsify publish` behaviour, reproduced exactly.
|
|
564
|
+
*
|
|
565
|
+
* `deferUnconfirmed` (`--verify-defer`) affects exactly one outcome: it downgrades
|
|
566
|
+
* `publish-unconfirmed` from exit 1 to a printed warning, for a caller that
|
|
567
|
+
* re-checks the same set afterwards. It is policy, so it lives here and not in
|
|
568
|
+
* {@link publishWorkspace}, which stays a pure fact-finder.
|
|
387
569
|
*/
|
|
388
|
-
function reportPublishOutcome(outcome, asJson) {
|
|
570
|
+
function reportPublishOutcome(outcome, asJson, deferUnconfirmed = false) {
|
|
389
571
|
switch (outcome.action) {
|
|
390
572
|
case 'published': {
|
|
391
573
|
const out = {
|
|
@@ -404,6 +586,44 @@ function reportPublishOutcome(outcome, asJson) {
|
|
|
404
586
|
process.stdout.write(`+ ${outcome.name}@${outcome.version}\n`);
|
|
405
587
|
return;
|
|
406
588
|
}
|
|
589
|
+
case 'publish-unconfirmed': {
|
|
590
|
+
const message = formatUnconfirmedPublish(outcome);
|
|
591
|
+
if (asJson) {
|
|
592
|
+
process.stdout.write(`${JSON.stringify({
|
|
593
|
+
ok: false,
|
|
594
|
+
action: 'publish-unconfirmed',
|
|
595
|
+
name: outcome.name,
|
|
596
|
+
version: outcome.version,
|
|
597
|
+
registry: outcome.registry,
|
|
598
|
+
claim: outcome.claim,
|
|
599
|
+
putStatus: outcome.putStatus,
|
|
600
|
+
probes: outcome.readback.attempts,
|
|
601
|
+
elapsedMs: outcome.readback.elapsedMs,
|
|
602
|
+
readbackUrl: outcome.readback.url,
|
|
603
|
+
readbackState: outcome.readback.last.state,
|
|
604
|
+
deferred: deferUnconfirmed,
|
|
605
|
+
}, null, 2)}\n`);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
process.stderr.write(`${message}\n`);
|
|
609
|
+
}
|
|
610
|
+
// Under `--verify-defer` the multi-line message is one entry in a
|
|
611
|
+
// serial log carrying a line per package, i.e. exactly the channel
|
|
612
|
+
// the incident's `+` line hid in. An annotation renders beside the
|
|
613
|
+
// job's own check and needs no writable file, the same reason
|
|
614
|
+
// `verify-published-closure.mjs` emits its notes as `::warning::`.
|
|
615
|
+
// On STDERR, because `--json` owns stdout.
|
|
616
|
+
if (deferUnconfirmed) {
|
|
617
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
618
|
+
const claimed = outcome.claim === 'already-published' ? 'was reported already published' : 'was accepted';
|
|
619
|
+
process.stderr.write(`::warning title=Publish unconfirmed::${outcome.name}@${outcome.version} ${claimed} ` +
|
|
620
|
+
`(HTTP ${outcome.putStatus}) but did not resolve on ${outcome.registry} within ` +
|
|
621
|
+
`${(outcome.readback.elapsedMs / 1000).toFixed(1)}s — the release-closure job must confirm it\n`);
|
|
622
|
+
}
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
return process.exit(1);
|
|
626
|
+
}
|
|
407
627
|
case 'republish-tolerated': {
|
|
408
628
|
const out = {
|
|
409
629
|
ok: true,
|