@gjsify/cli 0.43.0 → 0.45.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 +453 -425
- package/lib/actions/build.js +2 -1
- package/lib/commands/affected-classify.js +7 -5
- package/lib/commands/build.js +6 -0
- package/lib/commands/flatpak/ci.js +10 -2
- package/lib/commands/flatpak/init.js +15 -16
- package/lib/commands/flatpak/utils.d.ts +0 -26
- package/lib/commands/flatpak/utils.js +4 -29
- package/lib/commands/gettext.d.ts +2 -1
- package/lib/commands/gettext.js +105 -47
- package/lib/commands/onboard.d.ts +7 -0
- package/lib/commands/onboard.js +349 -96
- package/lib/commands/ship.d.ts +3 -0
- package/lib/commands/ship.js +911 -70
- package/lib/commands/trust.d.ts +6 -0
- package/lib/commands/trust.js +1 -1
- package/lib/config.js +25 -0
- package/lib/types/cli-build-options.d.ts +11 -0
- package/lib/types/config-data.d.ts +137 -3
- package/lib/utils/flatpak-runtime.d.ts +41 -0
- package/lib/utils/flatpak-runtime.js +54 -0
- package/lib/utils/msgfmt-merge.d.ts +35 -0
- package/lib/utils/msgfmt-merge.js +102 -0
- package/lib/utils/npm-otp.d.ts +27 -2
- package/lib/utils/npm-otp.js +82 -7
- package/lib/utils/onboard-discovery.d.ts +68 -0
- package/lib/utils/onboard-discovery.js +178 -0
- package/lib/utils/onboard-probe.d.ts +118 -1
- package/lib/utils/onboard-probe.js +153 -4
- package/lib/utils/prompt-output.d.ts +12 -0
- package/lib/utils/prompt-output.js +54 -0
- package/lib/utils/prompt.js +22 -2
- package/lib/utils/publishable-packages.d.ts +1 -1
- package/lib/utils/publishable-packages.js +1 -0
- package/lib/utils/resolve-npm-package.d.ts +56 -0
- package/lib/utils/resolve-npm-package.js +69 -0
- package/lib/utils/ship/app-runtime.d.ts +214 -0
- package/lib/utils/ship/app-runtime.js +429 -0
- package/lib/utils/ship/depends.d.ts +54 -7
- package/lib/utils/ship/depends.js +120 -6
- package/lib/utils/ship/discover.js +18 -1
- package/lib/utils/ship/dmg.d.ts +71 -0
- package/lib/utils/ship/dmg.js +181 -0
- package/lib/utils/ship/flatpak-config.d.ts +66 -0
- package/lib/utils/ship/flatpak-config.js +132 -0
- package/lib/utils/ship/flatpak.d.ts +45 -0
- package/lib/utils/ship/flatpak.js +148 -0
- package/lib/utils/ship/formats.d.ts +116 -3
- package/lib/utils/ship/formats.js +869 -11
- package/lib/utils/ship/launcher.d.ts +13 -3
- package/lib/utils/ship/launcher.js +362 -17
- package/lib/utils/ship/layout.d.ts +268 -0
- package/lib/utils/ship/layout.js +351 -0
- package/lib/utils/ship/localize-metadata.d.ts +22 -0
- package/lib/utils/ship/localize-metadata.js +126 -0
- package/lib/utils/ship/mime.js +1 -9
- package/lib/utils/ship/msi.d.ts +126 -0
- package/lib/utils/ship/msi.js +417 -0
- package/lib/utils/ship/node-runtime.d.ts +71 -0
- package/lib/utils/ship/node-runtime.js +122 -0
- package/lib/utils/ship/payload.d.ts +156 -5
- package/lib/utils/ship/payload.js +580 -14
- package/lib/utils/ship/plan.d.ts +21 -0
- package/lib/utils/ship/plan.js +20 -7
- package/lib/utils/ship/plist.d.ts +58 -0
- package/lib/utils/ship/plist.js +198 -0
- package/lib/utils/ship/rpm.js +9 -3
- package/lib/utils/ship/schemas.d.ts +61 -0
- package/lib/utils/ship/schemas.js +118 -0
- package/lib/utils/ship/scripts.js +7 -3
- package/lib/utils/ship/settings.d.ts +19 -1
- package/lib/utils/ship/settings.js +19 -1
- package/lib/utils/ship/share-dirs.d.ts +23 -0
- package/lib/utils/ship/share-dirs.js +46 -0
- package/lib/utils/ship/signing.d.ts +195 -0
- package/lib/utils/ship/signing.js +317 -0
- package/lib/utils/ship/stage-manifest.d.ts +50 -13
- package/lib/utils/ship/stage-manifest.js +137 -14
- package/lib/utils/ship/stage-writer.d.ts +48 -0
- package/lib/utils/ship/stage-writer.js +71 -1
- package/lib/utils/ship/types.d.ts +264 -16
- package/lib/utils/ship/xml.d.ts +7 -0
- package/lib/utils/ship/xml.js +25 -0
- package/lib/utils/ship/zip.d.ts +69 -0
- package/lib/utils/ship/zip.js +245 -0
- package/lib/utils/trust-registry.js +5 -0
- package/package.json +19 -19
package/lib/commands/onboard.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
// `gjsify onboard [--
|
|
1
|
+
// `gjsify onboard [--packages <glob>] [--include/--exclude <glob>] [--dry-run] …`
|
|
2
2
|
//
|
|
3
|
-
// One command that ensures EVERY publishable
|
|
4
|
-
// PUBLISHED on npm AND has a Trusted Publisher configured for
|
|
5
|
-
//
|
|
3
|
+
// One command that ensures EVERY publishable package in a monorepo is both
|
|
4
|
+
// PUBLISHED on npm AND has a Trusted Publisher configured for that repo's
|
|
5
|
+
// release workflow. It does the minimum work — publishing/trusting only what's
|
|
6
6
|
// missing — and folds the whole manual bootstrap sequence documented in
|
|
7
|
-
//
|
|
8
|
-
// bootstrap") into a single idempotent sweep:
|
|
7
|
+
// docs/publishing.md into a single idempotent sweep:
|
|
9
8
|
//
|
|
10
9
|
// 1. Auth gate — `whoami` first. If the token is live, proceed without asking
|
|
11
10
|
// for credentials; only when it is dead/missing do we run the `gjsify
|
|
12
11
|
// login` flow (unless `--yes` on a non-TTY, which fails clearly).
|
|
13
|
-
// 2. Enumerate publishable
|
|
12
|
+
// 2. Enumerate the publishable packages (non-private), from the root
|
|
13
|
+
// manifest's `workspaces` globs and/or `--packages` directory globs, then
|
|
14
|
+
// filter by `--include`/`--exclude`. Every source is reported with its
|
|
15
|
+
// count — see utils/onboard-discovery.ts for why that is load-bearing.
|
|
14
16
|
// 3. Determine each package's state (bounded concurrency) by reading npm's
|
|
15
17
|
// Trusted-Publisher config through the SAME requester path `gjsify trust`
|
|
16
18
|
// uses: unpublished (404) / published-but-untrusted / published-and-trusted.
|
|
@@ -22,25 +24,38 @@
|
|
|
22
24
|
// code is tried before prompting, so a whole sweep of N packages typically
|
|
23
25
|
// needs the user to type an OTP only once (see utils/npm-otp.ts).
|
|
24
26
|
//
|
|
27
|
+
// NOTHING here is specific to this repo. `--packages '*'` over a root that has
|
|
28
|
+
// no package.json at all is the shape `gjsify/types` needs (703 `@girs/*`
|
|
29
|
+
// directories, no workspace manifest), and it is the same code path that sweeps
|
|
30
|
+
// this monorepo's workspaces.
|
|
31
|
+
//
|
|
25
32
|
// Re-running when everything is already published + trusted does nothing and
|
|
26
33
|
// exits 0. `--dry-run` reports the plan and changes nothing.
|
|
27
34
|
import { spawnSync } from 'node:child_process';
|
|
28
35
|
import { DEFAULT_REGISTRY, whoami } from '@gjsify/npm-registry';
|
|
29
|
-
import {
|
|
30
|
-
import { mergePublishables } from '../utils/publishable-packages.js';
|
|
36
|
+
import { filterWorkspaces } from '@gjsify/workspace';
|
|
31
37
|
import { hasAnyCredential, loadNpmrc } from '../utils/load-npmrc.js';
|
|
32
38
|
import { OtpProvider } from '../utils/npm-otp.js';
|
|
39
|
+
import { writeAroundPrompt } from '../utils/prompt-output.js';
|
|
33
40
|
import { publishWorkspace } from './publish.js';
|
|
34
41
|
import { createTrustRequester } from './trust.js';
|
|
35
42
|
import { runLogin, LoginError } from './login.js';
|
|
36
43
|
import { detectPackageManager } from './workspace.js';
|
|
37
44
|
import { spawnToCompletion } from '../utils/spawn.js';
|
|
38
|
-
import {
|
|
45
|
+
import { assertEveryPatternMatches, assertRepositoryAgreement, collectOnboardPackages, describeSources, resolveRepoRoot, } from '../utils/onboard-discovery.js';
|
|
39
46
|
import { githubTrustBody, normalizeWorkflowFile, parseRepoFromGitRemote, validateRepository, } from '../utils/trust-registry.js';
|
|
40
|
-
import { DEFAULT_PROBE_CONCURRENCY, probeAllTrustStates } from '../utils/onboard-probe.js';
|
|
47
|
+
import { DEFAULT_PROBE_CONCURRENCY, mapWithConcurrency, probeAllTrustStates, RateLimitGate, requestWaitingOutRateLimit, } from '../utils/onboard-probe.js';
|
|
48
|
+
/**
|
|
49
|
+
* How many Trusted-Publisher writes run in parallel by default.
|
|
50
|
+
*
|
|
51
|
+
* Deliberately the same small number as the read probe: one token, one registry,
|
|
52
|
+
* and a 429 costs a backoff. The win is not really throughput — it is that four
|
|
53
|
+
* writes per npm 2FA window means a quarter as many codes typed by hand.
|
|
54
|
+
*/
|
|
55
|
+
const DEFAULT_WRITE_CONCURRENCY = 4;
|
|
41
56
|
export const onboardCommand = {
|
|
42
57
|
command: 'onboard',
|
|
43
|
-
description: 'Ensure every publishable
|
|
58
|
+
description: 'Ensure every publishable package in a monorepo is both published on npm and has a Trusted Publisher configured for its release workflow — publishing/trusting only what is missing, one shared OTP across the whole sweep. Works on npm/yarn workspaces and, via --packages, on a monorepo with no workspace manifest at all. Idempotent.',
|
|
44
59
|
builder: (yargs) => yargs
|
|
45
60
|
.option('repository', {
|
|
46
61
|
description: 'GitHub repo as `owner/repo`. Default: inferred from the `origin` git remote.',
|
|
@@ -62,11 +77,48 @@ export const onboardCommand = {
|
|
|
62
77
|
.option('otp', {
|
|
63
78
|
description: 'npm 2FA one-time code, used as the initial shared code. Prompted on demand (once) if omitted and a challenge occurs.',
|
|
64
79
|
type: 'string',
|
|
80
|
+
})
|
|
81
|
+
.option('packages', {
|
|
82
|
+
description: 'Directory glob naming package folders, resolved against the repo root (repeatable). For a monorepo that is not an npm/yarn workspace — `--packages "*"` in a repo whose 703 package directories sit at the top level. Merged with the root manifest\'s own `workspaces` globs when it has any. A pattern matching NO directory is a hard error.',
|
|
83
|
+
type: 'string',
|
|
84
|
+
array: true,
|
|
85
|
+
})
|
|
86
|
+
.option('include', {
|
|
87
|
+
description: 'Glob pattern to include packages by NAME (repeatable). Applied after discovery.',
|
|
88
|
+
type: 'string',
|
|
89
|
+
array: true,
|
|
90
|
+
})
|
|
91
|
+
.option('exclude', {
|
|
92
|
+
description: 'Glob pattern to exclude packages by NAME (repeatable). Applied after discovery.',
|
|
93
|
+
type: 'string',
|
|
94
|
+
array: true,
|
|
95
|
+
})
|
|
96
|
+
.option('access', {
|
|
97
|
+
description: 'npm access for a FIRST publish — `public` or `restricted`. Only ever applied to a package this sweep publishes; an already-published package keeps whatever access it has.',
|
|
98
|
+
type: 'string',
|
|
99
|
+
choices: ['public', 'restricted'],
|
|
100
|
+
default: 'public',
|
|
101
|
+
})
|
|
102
|
+
.option('build', {
|
|
103
|
+
description: "Run each to-be-published package's `build` script first. `--no-build` skips it, for a repo whose packages are generated artifacts with nothing to build.",
|
|
104
|
+
type: 'boolean',
|
|
105
|
+
default: true,
|
|
106
|
+
})
|
|
107
|
+
.option('verbose', {
|
|
108
|
+
description: 'List every package in the plan table, including the ones already published + trusted (by default only the rows needing work are listed; the counts always cover all of them).',
|
|
109
|
+
type: 'boolean',
|
|
110
|
+
alias: 'v',
|
|
111
|
+
default: false,
|
|
65
112
|
})
|
|
66
113
|
.option('concurrency', {
|
|
67
114
|
description: 'How many packages to probe (read state) in parallel (kept small so a single token does not burst npm; the first read is always serial to prompt for the shared OTP once).',
|
|
68
115
|
type: 'number',
|
|
69
116
|
default: DEFAULT_PROBE_CONCURRENCY,
|
|
117
|
+
})
|
|
118
|
+
.option('write-concurrency', {
|
|
119
|
+
description: "How many Trusted-Publisher writes to issue in parallel. It buys FEWER 2FA PROMPTS more than it buys speed: an npm code lives about 30 seconds, so a serial sweep crosses a code boundary every ~38 packages. Raising it does not beat npm's rate limit — a 429 anywhere now pauses the whole sweep, so it self-paces down to whatever the registry will serve. Publishes are unaffected: they stay serial, because publish ORDER is a correctness property.",
|
|
120
|
+
type: 'number',
|
|
121
|
+
default: DEFAULT_WRITE_CONCURRENCY,
|
|
70
122
|
})
|
|
71
123
|
.option('dry-run', {
|
|
72
124
|
description: 'Report the plan (what would be published / trusted) without changing anything.',
|
|
@@ -93,8 +145,11 @@ export const onboardCommand = {
|
|
|
93
145
|
// route progress to stderr.
|
|
94
146
|
if (asJson)
|
|
95
147
|
process.stderr.write(`${msg}\n`);
|
|
148
|
+
// Through the prompt arbiter: a concurrent worker's notice must not
|
|
149
|
+
// land inside the OTP the user is typing. Held, not dropped — those
|
|
150
|
+
// notices are what explain why the sweep is slow.
|
|
96
151
|
else
|
|
97
|
-
|
|
152
|
+
writeAroundPrompt(`${msg}\n`);
|
|
98
153
|
};
|
|
99
154
|
let npmrc = await loadNpmrc(cwd);
|
|
100
155
|
const defaultRegistry = args.registry ?? process.env.npm_config_registry ?? npmrc.registry ?? DEFAULT_REGISTRY;
|
|
@@ -140,23 +195,66 @@ export const onboardCommand = {
|
|
|
140
195
|
}
|
|
141
196
|
const workflow = normalizeWorkflowFile(args.workflow);
|
|
142
197
|
const environment = args.environment;
|
|
143
|
-
// 2. Enumerate publishable
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
198
|
+
// 2. Enumerate the publishable packages (non-private), from every source
|
|
199
|
+
// that applies to this repo, then filter by name.
|
|
200
|
+
const root = resolveRepoRoot(cwd);
|
|
201
|
+
let all;
|
|
202
|
+
let sources;
|
|
203
|
+
try {
|
|
204
|
+
if (args.packages && args.packages.length > 0)
|
|
205
|
+
assertEveryPatternMatches(root, args.packages);
|
|
206
|
+
const found = collectOnboardPackages(root, cwd, { patterns: args.packages });
|
|
207
|
+
all = found.packages;
|
|
208
|
+
sources = describeSources(found.sources);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
212
|
+
if (asJson)
|
|
213
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: 'discovery', message: msg })}\n`);
|
|
214
|
+
else
|
|
215
|
+
console.error(msg);
|
|
216
|
+
return process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const selected = filterWorkspaces(all, {
|
|
219
|
+
noPrivate: true,
|
|
220
|
+
include: args.include,
|
|
221
|
+
exclude: args.exclude,
|
|
222
|
+
}).filter((ws) => ws.name);
|
|
151
223
|
if (selected.length === 0) {
|
|
152
|
-
const msg =
|
|
224
|
+
const msg = `gjsify onboard: no publishable packages found under ${root} ` +
|
|
225
|
+
`(discovered ${all.length}${sources ? ` — ${sources}` : ''}). ` +
|
|
226
|
+
(all.length > 0
|
|
227
|
+
? 'Every discovered package is private or was filtered out by --include/--exclude.'
|
|
228
|
+
: 'Pass --packages with a directory glob if this repo has no workspace manifest.');
|
|
153
229
|
if (asJson)
|
|
154
230
|
process.stdout.write(`${JSON.stringify({ ok: false, error: 'no-packages', message: msg })}\n`);
|
|
155
231
|
else
|
|
156
232
|
console.error(msg);
|
|
157
233
|
return process.exit(1);
|
|
158
234
|
}
|
|
159
|
-
|
|
235
|
+
// Every selected package must AGREE that it lives here. A workspace of
|
|
236
|
+
// this repo is not the same claim as a package published from it —
|
|
237
|
+
// `gjsify/ts-for-gir` has ~703 generated `@girs/*` workspaces that
|
|
238
|
+
// publish from `gjsify/types`, and trusting those for the wrong
|
|
239
|
+
// workflow breaks exactly the release this sweep exists to protect.
|
|
240
|
+
try {
|
|
241
|
+
assertRepositoryAgreement(selected, repository);
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
245
|
+
if (asJson) {
|
|
246
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: 'foreign-repository', message: msg })}\n`);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
console.error(msg);
|
|
250
|
+
}
|
|
251
|
+
return process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
// The blast radius of a sweep that writes to npm is exactly this list,
|
|
254
|
+
// so say where it came from — a total alone cannot distinguish the right
|
|
255
|
+
// set from a plausible wrong one.
|
|
256
|
+
log(`gjsify onboard: root=${root} | ${sources}`);
|
|
257
|
+
log(`gjsify onboard: ${selected.length} of ${all.length} package(s) selected | repo=${repository} workflow=${workflow}`);
|
|
160
258
|
if (dryRun)
|
|
161
259
|
log('(dry-run — nothing will be published or configured)');
|
|
162
260
|
log();
|
|
@@ -169,6 +267,25 @@ export const onboardCommand = {
|
|
|
169
267
|
registry: defaultRegistry,
|
|
170
268
|
});
|
|
171
269
|
const trustRequest = createTrustRequester({ npmrc, otpProvider });
|
|
270
|
+
// ONE cool-down for the whole sweep: a 429 on any request holds back
|
|
271
|
+
// every other one. Retrying a throttled request in isolation is not
|
|
272
|
+
// enough — the rest of the sweep keeps provoking the very limit the
|
|
273
|
+
// retry is waiting out, which is how a 703-package run spent its retry
|
|
274
|
+
// budget and reported `trust failed (HTTP 429)`.
|
|
275
|
+
const gate = new RateLimitGate();
|
|
276
|
+
let rateLimitReported = false;
|
|
277
|
+
const onRateLimit = (info) => {
|
|
278
|
+
if (rateLimitReported)
|
|
279
|
+
return;
|
|
280
|
+
rateLimitReported = true;
|
|
281
|
+
// Report the FIRST one only. It answers the question that matters —
|
|
282
|
+
// does npm tell us how fast we may go? — and repeating it per request
|
|
283
|
+
// would bury the sweep's own output.
|
|
284
|
+
log(` npm rate limit hit. ${info.headers}. Waiting ${Math.round(info.waitMs / 1000)}s ` +
|
|
285
|
+
`(${info.fromRetryAfter ? "npm's Retry-After" : 'our backoff — npm advised nothing'}), ` +
|
|
286
|
+
'and holding the whole sweep back while it lasts.');
|
|
287
|
+
};
|
|
288
|
+
const pacing = { gate, onRateLimit };
|
|
172
289
|
// 3. Determine per-package state. Reads run through the SAME
|
|
173
290
|
// `TrustRequester` path `gjsify trust` uses (identical endpoint + auth +
|
|
174
291
|
// OTP handling) — so a 2FA-gated trust-state read is answered with the
|
|
@@ -176,21 +293,69 @@ export const onboardCommand = {
|
|
|
176
293
|
// (task #60). The first read is serial (prompts for the shared OTP once,
|
|
177
294
|
// before the burst); the rest run at a SMALL concurrency cap so a single
|
|
178
295
|
// token never faces a 127-wide parallel read.
|
|
296
|
+
// Progress, because this phase is otherwise SILENT for minutes. It is
|
|
297
|
+
// paced by count rather than by time so the interval is reproducible
|
|
298
|
+
// between runs, and it carries the running TALLY rather than a bare
|
|
299
|
+
// counter: "600/703 — 590 to trust" says both that the sweep is alive
|
|
300
|
+
// and what it is about to do, which is what a reader is deciding on.
|
|
301
|
+
const progressEvery = Math.max(1, Math.min(50, Math.ceil(selected.length / 10)));
|
|
302
|
+
let toTrustSoFar = 0;
|
|
303
|
+
let doneSoFar = 0;
|
|
304
|
+
let blockedSoFar = 0;
|
|
305
|
+
if (selected.length > progressEvery)
|
|
306
|
+
log(`Reading trust state for ${selected.length} package(s)…`);
|
|
179
307
|
const plans = await probeAllTrustStates(trustRequest, selected, Math.max(1, args.concurrency), {
|
|
180
308
|
registryOverride: args.registry,
|
|
181
309
|
npmrc,
|
|
182
310
|
repository,
|
|
183
311
|
workflow,
|
|
312
|
+
}, {
|
|
313
|
+
...pacing,
|
|
314
|
+
onProgress: (n, total, plan) => {
|
|
315
|
+
if (plan.action === 'trust' || plan.action === 'publish-and-trust')
|
|
316
|
+
toTrustSoFar++;
|
|
317
|
+
else if (plan.action === 'skip')
|
|
318
|
+
doneSoFar++;
|
|
319
|
+
else
|
|
320
|
+
blockedSoFar++;
|
|
321
|
+
// Always report the LAST one, so the phase visibly ends.
|
|
322
|
+
if (n % progressEvery !== 0 && n !== total)
|
|
323
|
+
return;
|
|
324
|
+
if (total <= progressEvery)
|
|
325
|
+
return;
|
|
326
|
+
const parts = [`${toTrustSoFar} to do`, `${doneSoFar} already done`];
|
|
327
|
+
if (blockedSoFar > 0)
|
|
328
|
+
parts.push(`${blockedSoFar} unreadable`);
|
|
329
|
+
log(` read ${n}/${total} — ${parts.join(', ')}`);
|
|
330
|
+
},
|
|
184
331
|
});
|
|
185
332
|
// Summary table (published✓ / trusted✓ vs work to do).
|
|
186
333
|
const toPublish = plans.filter((p) => p.action === 'publish-and-trust');
|
|
187
334
|
const toTrust = plans.filter((p) => p.action === 'trust');
|
|
188
335
|
const done = plans.filter((p) => p.action === 'skip');
|
|
189
336
|
const blocked = plans.filter((p) => p.action === 'blocked');
|
|
190
|
-
|
|
337
|
+
// A 703-package sweep prints 703 rows of "nothing to do" and buries the
|
|
338
|
+
// handful that matter. Rows needing WORK are always listed; the finished
|
|
339
|
+
// ones only under --verbose. The counts below cover all of them either
|
|
340
|
+
// way — a row can be omitted from the listing, never from the tally.
|
|
341
|
+
for (const p of plans) {
|
|
342
|
+
if (p.action === 'skip' && args.verbose !== true)
|
|
343
|
+
continue;
|
|
191
344
|
log(` ${statusCell(p)} ${p.ws.name}`);
|
|
345
|
+
}
|
|
346
|
+
if (!args.verbose && done.length > 0)
|
|
347
|
+
log(` (${done.length} already published + trusted — pass -v to list)`);
|
|
192
348
|
log();
|
|
193
349
|
log(`Plan: ${done.length} already done, ${toPublish.length} to publish+trust, ${toTrust.length} to trust, ${blocked.length} unreadable.`);
|
|
350
|
+
// Name the cause when the unreadable ones are throttling rather than a
|
|
351
|
+
// property of those packages. It lands on the TAIL of an alphabetical
|
|
352
|
+
// sweep, which is exactly where it looks like "these packages are
|
|
353
|
+
// special" instead of "we asked too fast".
|
|
354
|
+
const throttled = blocked.filter((p) => p.httpStatus === 429).length;
|
|
355
|
+
if (throttled > 0) {
|
|
356
|
+
log(` ${throttled} of those are npm rate limits (HTTP 429) that outlasted the backoff — ` +
|
|
357
|
+
'lower --concurrency and re-run; the sweep is idempotent and picks up where it left off.');
|
|
358
|
+
}
|
|
194
359
|
const results = [];
|
|
195
360
|
if (dryRun) {
|
|
196
361
|
for (const p of plans) {
|
|
@@ -201,99 +366,178 @@ export const onboardCommand = {
|
|
|
201
366
|
? 'would publish + trust'
|
|
202
367
|
: p.action === 'trust'
|
|
203
368
|
? 'would trust'
|
|
204
|
-
:
|
|
369
|
+
: p.action === 'blocked'
|
|
370
|
+
? // Same shape the act phase reports, so a
|
|
371
|
+
// `failed` row in a dry-run summary says WHY
|
|
372
|
+
// rather than only that it did not succeed.
|
|
373
|
+
`state HTTP ${p.httpStatus}`
|
|
374
|
+
: undefined,
|
|
205
375
|
});
|
|
206
376
|
}
|
|
207
|
-
|
|
377
|
+
// A dry-run whose whole job is to REPORT THE PLAN must not exit 0
|
|
378
|
+
// having failed to compute it. Every unreadable package is counted
|
|
379
|
+
// as a failure here: run without a token, every trust read is a 401
|
|
380
|
+
// and the summary is `0 to publish+trust, 0 to trust` — a plan that
|
|
381
|
+
// was never determined, printed in the shape of a plan that found
|
|
382
|
+
// nothing to do. Measured on `gjsify/types`: 703 packages, 703
|
|
383
|
+
// unreadable, exit 0.
|
|
384
|
+
finish(results, plans, {
|
|
385
|
+
asJson,
|
|
386
|
+
dryRun,
|
|
387
|
+
failed: blocked.length,
|
|
388
|
+
root,
|
|
389
|
+
sources,
|
|
390
|
+
discovered: all.length,
|
|
391
|
+
});
|
|
208
392
|
return;
|
|
209
393
|
}
|
|
210
|
-
// 4. Act on the gaps, minimally.
|
|
211
|
-
//
|
|
212
|
-
//
|
|
394
|
+
// 4. Act on the gaps, minimally.
|
|
395
|
+
//
|
|
396
|
+
// The two kinds of work are paced DIFFERENTLY, on purpose.
|
|
397
|
+
//
|
|
398
|
+
// A trust POST is independent of every other trust POST, so they run at
|
|
399
|
+
// `--write-concurrency`. They used to be serial for one reason — "so the
|
|
400
|
+
// shared OTP is prompted at most once" — and that reason is gone now
|
|
401
|
+
// that `OtpProvider.refresh()` is single-flight: concurrent writers share
|
|
402
|
+
// one prompt instead of stacking N of them.
|
|
403
|
+
//
|
|
404
|
+
// What this buys is PROMPTS, not throughput, and the distinction is
|
|
405
|
+
// measured rather than assumed: the real `gjsify/types` run was already
|
|
406
|
+
// being 429'd at SERIAL pace, so npm — not the loop — sets the ceiling.
|
|
407
|
+
// What serial cost was 2FA codes: one lives ~30 s, so the sweep crossed a
|
|
408
|
+
// code boundary every ~38 packages, ~18 codes typed by hand for 703
|
|
409
|
+
// packages. Fewer wall-clock seconds per code means fewer codes.
|
|
410
|
+
// The shared `RateLimitGate` is what makes raising this safe rather than
|
|
411
|
+
// merely faster-to-fail.
|
|
412
|
+
//
|
|
413
|
+
// A publish stays SERIAL. Publish order is a correctness property
|
|
414
|
+
// (docs/publishing.md: an aborted parallel sweep can leave a live bridge
|
|
415
|
+
// pinning a platform child that does not exist), and nothing about OTP
|
|
416
|
+
// pacing is worth trading that for.
|
|
213
417
|
const trustBody = githubTrustBody({ repository, workflow, environment });
|
|
418
|
+
const writeConcurrency = Math.max(1, args['write-concurrency']);
|
|
419
|
+
const writeTotal = toPublish.length + toTrust.length;
|
|
420
|
+
// Results are written by INDEX rather than pushed, so a concurrent phase
|
|
421
|
+
// still reports in plan order — a summary whose order depends on which
|
|
422
|
+
// request happened to answer first is harder to diff between runs.
|
|
423
|
+
const indexed = Array.from({ length: plans.length });
|
|
214
424
|
let failed = 0;
|
|
215
|
-
for
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
await buildIfPresent(p.ws, log);
|
|
231
|
-
}
|
|
232
|
-
catch (err) {
|
|
233
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
234
|
-
log(` build failed: ${msg}`);
|
|
235
|
-
results.push({ name: p.ws.name, result: 'failed', detail: `build: ${msg}` });
|
|
236
|
-
failed++;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
const pub = await publishWorkspace({
|
|
240
|
-
wsDir: p.ws.location,
|
|
241
|
-
tag: 'latest',
|
|
242
|
-
access: 'public',
|
|
243
|
-
provenance: false,
|
|
244
|
-
tolerate: true, // a concurrent/racey publish is harmless
|
|
245
|
-
tolerateUntrustedNew: false,
|
|
246
|
-
trustedFlag: undefined,
|
|
247
|
-
registry: p.registry,
|
|
248
|
-
verbose: false,
|
|
249
|
-
json: false,
|
|
250
|
-
otpProvider,
|
|
251
|
-
seedOtpFirst: false,
|
|
252
|
-
});
|
|
253
|
-
if (!pub.ok) {
|
|
254
|
-
const detail = describePublishFailure(pub);
|
|
255
|
-
log(` publish failed: ${detail}`);
|
|
256
|
-
results.push({ name: p.ws.name, result: 'failed', detail: `publish: ${detail}` });
|
|
257
|
-
failed++;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
publishedNow = pub.action !== 'skipped-untrusted-new';
|
|
261
|
-
log(` published ${pub.name}@${'version' in pub ? pub.version : ''}`);
|
|
262
|
-
}
|
|
263
|
-
// Configure the Trusted Publisher.
|
|
264
|
-
const created = await trustRequest('POST', p.url, trustBody);
|
|
425
|
+
/** POST the Trusted Publisher config for one package and record the outcome. */
|
|
426
|
+
let written = 0;
|
|
427
|
+
let throttledWrites = 0;
|
|
428
|
+
const configureTrust = async (p, i, publishedNow) => {
|
|
429
|
+
// Through the rate-limit-aware wrapper: a 703-package sweep that gets
|
|
430
|
+
// throttled on its READS gets throttled on its WRITES too, and a bare
|
|
431
|
+
// 429 here would be recorded as `trust failed` — a failure attributed
|
|
432
|
+
// to the package rather than to the pacing.
|
|
433
|
+
const created = await requestWaitingOutRateLimit(trustRequest, 'POST', p.url, trustBody, pacing);
|
|
434
|
+
// `n/total` on every line, because the pace of a sweep is the thing a
|
|
435
|
+
// reader is judging while it runs — and the OTP prompt that
|
|
436
|
+
// interrupts it needs a nearby answer to "did the last code get
|
|
437
|
+
// anywhere?".
|
|
438
|
+
const at = `${++written}/${writeTotal}`;
|
|
265
439
|
if (created.status >= 200 && created.status < 300) {
|
|
266
|
-
log(` trusted ${p.ws.name}`);
|
|
267
|
-
|
|
268
|
-
name: p.ws.name,
|
|
269
|
-
result: publishedNow ? 'published+trusted' : 'trusted',
|
|
270
|
-
});
|
|
440
|
+
log(` [${at}] trusted ${p.ws.name}`);
|
|
441
|
+
indexed[i] = { name: p.ws.name, result: publishedNow ? 'published+trusted' : 'trusted' };
|
|
271
442
|
}
|
|
272
443
|
else if (created.status === 409) {
|
|
273
|
-
|
|
444
|
+
indexed[i] = { name: p.ws.name, result: publishedNow ? 'published+trusted' : 'trusted' };
|
|
274
445
|
}
|
|
275
446
|
else if (created.status === 404) {
|
|
276
447
|
// Freshly-published package not yet provisioned for trust config.
|
|
277
|
-
log(` ${p.ws.name}: published but not yet trust-configurable (re-run onboard)`);
|
|
278
|
-
|
|
448
|
+
log(` [${at}] ${p.ws.name}: published but not yet trust-configurable (re-run onboard)`);
|
|
449
|
+
indexed[i] = {
|
|
279
450
|
name: p.ws.name,
|
|
280
451
|
result: publishedNow ? 'published' : 'failed',
|
|
281
452
|
detail: 'trust config deferred (404)',
|
|
282
|
-
}
|
|
453
|
+
};
|
|
283
454
|
if (!publishedNow)
|
|
284
455
|
failed++;
|
|
285
456
|
}
|
|
286
457
|
else {
|
|
287
|
-
log(` trust failed (HTTP ${created.status})`);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
458
|
+
log(` [${at}] trust failed (HTTP ${created.status}) — ${p.ws.name}`);
|
|
459
|
+
indexed[i] = { name: p.ws.name, result: 'failed', detail: `trust HTTP ${created.status}` };
|
|
460
|
+
if (created.status === 429)
|
|
461
|
+
throttledWrites++;
|
|
462
|
+
failed++;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
// 4a. Bookkeeping-only rows, and the serial publish path.
|
|
466
|
+
const trustOnly = [];
|
|
467
|
+
for (let i = 0; i < plans.length; i++) {
|
|
468
|
+
const p = plans[i];
|
|
469
|
+
if (p.action === 'skip') {
|
|
470
|
+
indexed[i] = { name: p.ws.name, result: 'already-done' };
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (p.action === 'blocked') {
|
|
474
|
+
log(`→ ${p.ws.name}: cannot read trust state (HTTP ${p.httpStatus}) — skipped`);
|
|
475
|
+
indexed[i] = { name: p.ws.name, result: 'failed', detail: `state HTTP ${p.httpStatus}` };
|
|
476
|
+
failed++;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (p.action === 'trust') {
|
|
480
|
+
trustOnly.push({ p, i });
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
// publish-and-trust — serial, see above.
|
|
484
|
+
log(`→ ${p.ws.name}: building + publishing…`);
|
|
485
|
+
try {
|
|
486
|
+
if (args.build !== false)
|
|
487
|
+
await buildIfPresent(p.ws, log);
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
491
|
+
log(` build failed: ${msg}`);
|
|
492
|
+
indexed[i] = { name: p.ws.name, result: 'failed', detail: `build: ${msg}` };
|
|
293
493
|
failed++;
|
|
494
|
+
continue;
|
|
294
495
|
}
|
|
496
|
+
const pub = await publishWorkspace({
|
|
497
|
+
wsDir: p.ws.location,
|
|
498
|
+
tag: 'latest',
|
|
499
|
+
access: args.access,
|
|
500
|
+
provenance: false,
|
|
501
|
+
tolerate: true, // a concurrent/racey publish is harmless
|
|
502
|
+
tolerateUntrustedNew: false,
|
|
503
|
+
trustedFlag: undefined,
|
|
504
|
+
registry: p.registry,
|
|
505
|
+
verbose: false,
|
|
506
|
+
json: false,
|
|
507
|
+
otpProvider,
|
|
508
|
+
seedOtpFirst: false,
|
|
509
|
+
});
|
|
510
|
+
if (!pub.ok) {
|
|
511
|
+
const detail = describePublishFailure(pub);
|
|
512
|
+
log(` publish failed: ${detail}`);
|
|
513
|
+
indexed[i] = { name: p.ws.name, result: 'failed', detail: `publish: ${detail}` };
|
|
514
|
+
failed++;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
log(` published ${pub.name}@${'version' in pub ? pub.version : ''}`);
|
|
518
|
+
await configureTrust(p, i, pub.action !== 'skipped-untrusted-new');
|
|
519
|
+
}
|
|
520
|
+
// 4b. The trust-only packages, concurrently.
|
|
521
|
+
if (trustOnly.length > 0) {
|
|
522
|
+
if (trustOnly.length > 1) {
|
|
523
|
+
log(`→ configuring ${trustOnly.length} Trusted Publisher(s), ${writeConcurrency} at a time…`);
|
|
524
|
+
}
|
|
525
|
+
await mapWithConcurrency(trustOnly, writeConcurrency, async ({ p, i }) => configureTrust(p, i, false));
|
|
526
|
+
}
|
|
527
|
+
for (const r of indexed)
|
|
528
|
+
if (r)
|
|
529
|
+
results.push(r);
|
|
530
|
+
// Name the cause at the END too, not only in the plan. A write is
|
|
531
|
+
// throttled minutes after the plan was printed and scrolled away, and
|
|
532
|
+
// "73 failed" on its own reads as 73 broken packages.
|
|
533
|
+
if (throttledWrites > 0) {
|
|
534
|
+
log();
|
|
535
|
+
log(`${throttledWrites} of the failures are npm rate limits (HTTP 429) that outlasted the wait, ` +
|
|
536
|
+
'not a problem with those packages.');
|
|
537
|
+
log(' Re-run to pick them up — the sweep is idempotent and skips everything already configured. ' +
|
|
538
|
+
`A lower --write-concurrency (currently ${writeConcurrency}) provokes the limit less.`);
|
|
295
539
|
}
|
|
296
|
-
finish(results, plans, { asJson, dryRun, failed });
|
|
540
|
+
finish(results, plans, { asJson, dryRun, failed, root, sources, discovered: all.length });
|
|
297
541
|
},
|
|
298
542
|
};
|
|
299
543
|
/** Print the summary + exit with the right code. */
|
|
@@ -303,6 +547,12 @@ function finish(results, plans, opts) {
|
|
|
303
547
|
process.stdout.write(`${JSON.stringify({
|
|
304
548
|
ok: failed === 0,
|
|
305
549
|
dryRun: opts.dryRun,
|
|
550
|
+
// `root`/`sources`/`discovered` travel with the summary so a
|
|
551
|
+
// caller can check WHICH tree was swept, not just that a
|
|
552
|
+
// sweep reported itself finished.
|
|
553
|
+
root: opts.root,
|
|
554
|
+
sources: opts.sources,
|
|
555
|
+
discovered: opts.discovered,
|
|
306
556
|
total: plans.length,
|
|
307
557
|
published: results.filter((r) => r.result === 'published' || r.result === 'published+trusted')
|
|
308
558
|
.length,
|
|
@@ -314,7 +564,10 @@ function finish(results, plans, opts) {
|
|
|
314
564
|
}
|
|
315
565
|
else {
|
|
316
566
|
const line = opts.dryRun
|
|
317
|
-
?
|
|
567
|
+
? failed > 0
|
|
568
|
+
? `Dry-run INCOMPLETE — nothing changed, but ${failed} package(s) had an unreadable state, ` +
|
|
569
|
+
'so the plan above does not cover them. Authenticate (`gjsify login`) and re-run.'
|
|
570
|
+
: 'Dry-run complete — nothing changed.'
|
|
318
571
|
: `Done: ${results.filter((r) => r.result.includes('published')).length} published, ` +
|
|
319
572
|
`${results.filter((r) => r.result === 'trusted' || r.result === 'published+trusted').length} trusted, ` +
|
|
320
573
|
`${results.filter((r) => r.result === 'already-done').length} already done, ${failed} failed.`;
|
package/lib/commands/ship.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
2
|
interface ShipOptions {
|
|
3
|
+
os?: string;
|
|
3
4
|
target?: string[];
|
|
4
5
|
out?: string;
|
|
5
6
|
stage: boolean;
|
|
@@ -7,6 +8,8 @@ interface ShipOptions {
|
|
|
7
8
|
'expect-target'?: string;
|
|
8
9
|
'skip-build': boolean;
|
|
9
10
|
arch?: string;
|
|
11
|
+
sign?: string;
|
|
12
|
+
notarize?: string;
|
|
10
13
|
verbose: boolean;
|
|
11
14
|
}
|
|
12
15
|
export declare const shipCommand: Command<unknown, ShipOptions>;
|