@joshuafolkken/kit 1.1229.0 → 1.1240.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/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-progress.md +2 -2
- package/.claude/skills/workflow-commands/followup.md +5 -0
- package/.claude/skills/workflow-commands/kickoff.md +1 -0
- package/.claude/skills/workflow-commands/latest-gate.md +1 -1
- package/.github/workflows/publish.yml +2 -0
- package/README.md +30 -31
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-JUDKDCDP.js → chunk-4TZVB44A.js} +8 -8
- package/dist/hooks/{chunk-RNWHN5BP.js → chunk-F3ELQ2AF.js} +3 -3
- package/dist/hooks/{chunk-VC3PZPZO.js → chunk-GPTFMK5E.js} +1 -1
- package/dist/hooks/{chunk-FIVDHQGT.js → chunk-KHGB5S3Y.js} +1 -1
- package/dist/hooks/{chunk-NV2JRZZO.js → chunk-KHX4U3BG.js} +17 -2
- package/dist/hooks/{chunk-F56MFS4B.js → chunk-KRBERBBI.js} +3 -3
- package/dist/hooks/{chunk-TQNPDZNH.js → chunk-QVSWU24O.js} +4 -4
- package/dist/hooks/{chunk-55HHG5PX.js → chunk-WK2XSHSI.js} +284 -63
- package/dist/hooks/{chunk-5VMKCCC4.js → chunk-XZYKEM6J.js} +2 -2
- package/dist/hooks/{chunk-6HJJXRBX.js → chunk-ZQGD4GBY.js} +1 -1
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/inputs.json +172 -170
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +3 -17
- package/dist/josh.js +279 -63
- package/package.json +1 -1
- package/prompts/collaboration-workflow/issue-template.md +1 -0
- package/scripts/gate/core-admission.ts +162 -0
- package/scripts/gate/core-budget.ts +121 -67
- package/scripts/gate/gate-plan.ts +35 -2
- package/scripts/gate/gate-report.ts +28 -9
- package/scripts/gate/machine-capacity.ts +175 -0
- package/scripts/gate/verification-gate.ts +7 -2
- package/scripts/init/init.ts +5 -1
- package/scripts/init/package-manager-pin.ts +35 -0
- package/scripts/issue/issue-auto-ok.ts +31 -6
- package/scripts/issue/issue-file-cli.ts +31 -8
- package/scripts/issue/issue-file.ts +5 -0
- package/scripts/issue/issue-labels.ts +13 -0
- package/scripts/issue/issue-release-cli.ts +155 -0
- package/scripts/issue/issue-release.ts +58 -0
- package/scripts/josh/josh-command-types.ts +3 -0
- package/scripts/josh/josh-commands-development.ts +8 -0
- package/scripts/josh/josh-commands-issue.ts +8 -1
- package/scripts/josh/josh-commands-lane.ts +2 -1
- package/scripts/josh/josh-commands-maintenance.ts +2 -2
- package/scripts/josh/josh-logic.ts +4 -3
- package/scripts/lane/lane-cli.ts +9 -1
- package/scripts/lane/lane-leftover.ts +43 -29
- package/scripts/lane/lane-open.ts +5 -5
- package/scripts/lane/lane-paths.ts +13 -0
- package/scripts/lane/lane-registry.ts +11 -0
- package/scripts/lane/lane-stray.ts +140 -0
- package/scripts/release/github-release-environment.ts +1 -0
- package/scripts/release/github-release-runs.ts +115 -0
- package/scripts/release/github-release.ts +13 -67
- package/scripts/run/cut/run-cut-cli.ts +16 -3
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/run-step.ts +5 -2
- package/scripts/run/wake/run-wake-cli.ts +2 -3
- package/scripts/run/wake/run-wake-describe.ts +56 -5
- package/scripts/run/wake/run-wake-loop.ts +5 -13
- package/scripts/run/wake/run-wake.ts +15 -64
- package/scripts/version/latest-corepack.ts +109 -78
- package/templates/workflows/github-release.yml +4 -0
|
@@ -126,14 +126,6 @@ interface RunWake {
|
|
|
126
126
|
// session that is merely slow is still doing the run's work, and a supervisor that killed what it
|
|
127
127
|
// could not account for would destroy exactly the work it exists to keep going.
|
|
128
128
|
woke_pid?: number | undefined
|
|
129
|
-
// When the wait on a still-running predecessor began. **It is its own field rather than a second
|
|
130
|
-
// use of `woke_at`, and that is the whole of the bound working as described.** Marked on `woke_at`,
|
|
131
|
-
// the first pass of a wait was the last one that could see the predecessor exit — every later pass
|
|
132
|
-
// read the mark as an outstanding wake and answered `pending` — so a predecessor that ended two
|
|
133
|
-
// seconds after the hold still cost the full grace window on a supervisor whose point is not to
|
|
134
|
-
// stall. Kept apart, the predecessor's liveness is read on every pass and the mark only bounds how
|
|
135
|
-
// long the wait may last.
|
|
136
|
-
held_at?: string | undefined
|
|
137
129
|
// The transcript ids of every session this supervisor forced with `--session-id`
|
|
138
130
|
// (joshuafolkken/kit#2407). `josh time --run` attributes a whiff only to a session in this list, so
|
|
139
131
|
// an unrelated read-only session that moved while the supervisor was alive is no longer counted
|
|
@@ -164,7 +156,6 @@ type WakeDecision =
|
|
|
164
156
|
| { kind: 'idle' }
|
|
165
157
|
| { kind: 'wait' }
|
|
166
158
|
| { kind: 'pending' }
|
|
167
|
-
| { kind: 'hold' }
|
|
168
159
|
| { kind: 'failed' }
|
|
169
160
|
| { kind: 'stop'; reason: WakeStopReason }
|
|
170
161
|
|
|
@@ -172,16 +163,16 @@ interface WakeDecisionInput {
|
|
|
172
163
|
read: CarryRead
|
|
173
164
|
woke_at: string | undefined
|
|
174
165
|
attempts: number
|
|
175
|
-
// Whether the process named on the carry record is still running
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
166
|
+
// Whether the process named on the carry record is still running — read only for a record no cut
|
|
167
|
+
// handed off, where it tells a session at work from a crashed one (`decide_not_handed_off`).
|
|
168
|
+
//
|
|
169
|
+
// **A handed-off record is launched from whether its cutting process lives or not**
|
|
170
|
+
// (joshuafolkken/kit#3363). The supervisor used to wait that process out, for up to the grace
|
|
171
|
+
// window, because `run-carry.ts` → `classify_claim` once tested liveness before the hand-off and
|
|
172
|
+
// would have answered the successor `busy`. joshuafolkken/kit#1935 put the hand-off first, so the
|
|
173
|
+
// wait guarded nothing — and an interactive session outlives its own cut, so every cut paid the
|
|
174
|
+
// whole ten minutes before the driver started.
|
|
181
175
|
is_owner_live: boolean
|
|
182
|
-
// When the wait on that predecessor began, so the wait can be bounded without spending the mark
|
|
183
|
-
// that measures a launch.
|
|
184
|
-
held_at: string | undefined
|
|
185
176
|
// **Whether a woken session would find anything to do** (joshuafolkken/kit#2417). Before this the
|
|
186
177
|
// decision read the carry record alone, so a session was launched whether or not the backlog held a
|
|
187
178
|
// runnable issue or a lane was free — and a session that found nothing ended without touching
|
|
@@ -198,7 +189,6 @@ const WAKE_DECISION: WakeDecision = { kind: 'wake' }
|
|
|
198
189
|
const IDLE_DECISION: WakeDecision = { kind: 'idle' }
|
|
199
190
|
const WAIT_DECISION: WakeDecision = { kind: 'wait' }
|
|
200
191
|
const PENDING_DECISION: WakeDecision = { kind: 'pending' }
|
|
201
|
-
const HOLD_DECISION: WakeDecision = { kind: 'hold' }
|
|
202
192
|
const FAILED_DECISION: WakeDecision = { kind: 'failed' }
|
|
203
193
|
|
|
204
194
|
// The three carry reads that are not `carried`, each mapped to why the supervisor stops. `none` is the
|
|
@@ -220,7 +210,6 @@ const run_wake_schema = process_owner_schema.extend({
|
|
|
220
210
|
// reads as "no attempt has been made yet" and would restart the retry count on every pass.
|
|
221
211
|
attempts: z.number().optional(),
|
|
222
212
|
woke_pid: z.number().optional(),
|
|
223
|
-
held_at: z.string().optional(),
|
|
224
213
|
spawned: z.array(z.string()).optional(),
|
|
225
214
|
idle_since: z.string().optional(),
|
|
226
215
|
profile: agent_role_profile.PROFILE_SCHEMA.optional(),
|
|
@@ -279,32 +268,10 @@ function decide_not_handed_off(input: WakeDecisionInput): WakeDecision {
|
|
|
279
268
|
return decide_launch(input)
|
|
280
269
|
}
|
|
281
270
|
|
|
282
|
-
//
|
|
283
|
-
// woken session declares, and waiting on it again would stall the grace window indefinitely.
|
|
284
|
-
//
|
|
285
|
-
// **The wait is bounded, and the bound is not decoration.** The owner is the agent process of the
|
|
286
|
-
// session that cut, and an interactive one often outlives its own cut — waited on without a bound, the
|
|
287
|
-
// supervisor would pend every interval for the record's whole life, wake nothing, and end on `expired`,
|
|
288
|
-
// which sends no warning. That is a silent overnight failure, which is worse than the `busy` race this
|
|
289
|
-
// wait exists to avoid: a `busy` merely costs one attempt, and attempts are retried. So the wait is
|
|
290
|
-
// marked when it starts and expires into an ordinary wake through the same grace window everything
|
|
291
|
-
// else uses.
|
|
292
|
-
//
|
|
293
|
-
// **And the wait ends the moment the predecessor does, because its liveness is read on every pass.**
|
|
294
|
-
// The bound is the ceiling on the wait, never its length: a predecessor that exits two seconds after
|
|
295
|
-
// the hold is waited two seconds. That is what `held_at` is a separate field for — marked on `woke_at`,
|
|
296
|
-
// this test could not fire a second time, so every wait cost the whole ceiling.
|
|
297
|
-
function is_predecessor_exiting(input: WakeDecisionInput): boolean {
|
|
298
|
-
if (input.woke_at !== undefined || !input.is_owner_live) return false
|
|
299
|
-
|
|
300
|
-
return input.held_at === undefined || !is_overdue(input.held_at, input.now)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// The whole policy, in four lines. Nothing else in this module decides whether to wake.
|
|
271
|
+
// The whole policy, in three lines. Nothing else in this module decides whether to wake.
|
|
304
272
|
function decide(input: WakeDecisionInput): WakeDecision {
|
|
305
273
|
if (input.read.kind !== 'carried') return { kind: 'stop', reason: STOP_REASONS[input.read.kind] }
|
|
306
274
|
if (input.read.carry.is_handed_off !== true) return decide_not_handed_off(input)
|
|
307
|
-
if (is_predecessor_exiting(input)) return HOLD_DECISION
|
|
308
275
|
|
|
309
276
|
return decide_launch(input)
|
|
310
277
|
}
|
|
@@ -367,10 +334,9 @@ function is_supervisor_live(wake: RunWake): boolean {
|
|
|
367
334
|
// inside the grace window would otherwise see no wake mark, launch a second session for the *same*
|
|
368
335
|
// cut, count it as another cut served, and leave two sessions racing for one record. The invocation
|
|
369
336
|
// has to match — a different one is a different run, and its state is not this one's.
|
|
370
|
-
// **`woke_pid`
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
// restarts the bounded wait, which is the ceiling sliding by another road.
|
|
337
|
+
// **`woke_pid` comes across too.** Without it, a restart inside the grace window that then spends its
|
|
338
|
+
// retries reports "the last one was none" in the warning — the one fact the warning exists to hand the
|
|
339
|
+
// person, dropped exactly where it is needed.
|
|
374
340
|
function carried_state(existing: RunWake | undefined, invocation: string): Partial<RunWake> {
|
|
375
341
|
if (existing?.invocation !== invocation) return { woke: NO_WAKES }
|
|
376
342
|
|
|
@@ -379,7 +345,6 @@ function carried_state(existing: RunWake | undefined, invocation: string): Parti
|
|
|
379
345
|
woke_at: existing.woke_at,
|
|
380
346
|
attempts: existing.attempts,
|
|
381
347
|
woke_pid: existing.woke_pid,
|
|
382
|
-
held_at: existing.held_at,
|
|
383
348
|
// **`spawned` carries across too, or a restart mid-run would lose the ids of every session it
|
|
384
349
|
// already started** (joshuafolkken/kit#2407) — and `josh time --run`, reading the record after
|
|
385
350
|
// the restart, would under-count the whiffs and read the loss as a saving.
|
|
@@ -447,24 +412,12 @@ function count_wake(wake: RunWake, now: Date, pid: number, session_id: string):
|
|
|
447
412
|
spawned,
|
|
448
413
|
woke_at,
|
|
449
414
|
woke_pid: pid,
|
|
450
|
-
held_at: undefined,
|
|
451
415
|
idle_since: undefined,
|
|
452
416
|
}
|
|
453
417
|
}
|
|
454
418
|
|
|
455
|
-
// Starts the
|
|
456
|
-
//
|
|
457
|
-
// running until the carry record does.
|
|
458
|
-
//
|
|
459
|
-
// **Marked once, so the ceiling does not slide.** A mark rewritten on every pass would measure from the
|
|
460
|
-
// latest pass rather than from the start of the wait, and a predecessor that never exits would then be
|
|
461
|
-
// waited on for ever — the unbounded wait this bound exists to prevent, arrived at by another road.
|
|
462
|
-
function mark_wait(wake: RunWake, now: Date): RunWake {
|
|
463
|
-
return wake.held_at === undefined ? { ...wake, held_at: now.toISOString() } : wake
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Starts the idle stretch a deferred launch is bounded by (joshuafolkken/kit#2417). Marked once, for
|
|
467
|
-
// the reason `mark_wait` is: rewritten each pass, the ceiling would slide and never be reached.
|
|
419
|
+
// Starts the idle stretch a deferred launch is bounded by (joshuafolkken/kit#2417). Marked once:
|
|
420
|
+
// rewritten each pass, the ceiling would slide and never be reached.
|
|
468
421
|
function mark_idle(wake: RunWake, now: Date): RunWake {
|
|
469
422
|
return wake.idle_since === undefined ? { ...wake, idle_since: now.toISOString() } : wake
|
|
470
423
|
}
|
|
@@ -489,7 +442,6 @@ function count_claim(wake: RunWake): RunWake {
|
|
|
489
442
|
woke,
|
|
490
443
|
woke_at: undefined,
|
|
491
444
|
attempts: undefined,
|
|
492
|
-
held_at: undefined,
|
|
493
445
|
idle_since: undefined,
|
|
494
446
|
}
|
|
495
447
|
}
|
|
@@ -589,7 +541,6 @@ const run_wake = {
|
|
|
589
541
|
fresh_wake,
|
|
590
542
|
is_supervisor_live,
|
|
591
543
|
mark_idle,
|
|
592
|
-
mark_wait,
|
|
593
544
|
parse_wake,
|
|
594
545
|
read_own_wake,
|
|
595
546
|
read_wake,
|
|
@@ -30,8 +30,12 @@
|
|
|
30
30
|
* manifest that arrived with the two fields out of step kept the pnpm dual-declaration
|
|
31
31
|
* warning forever. The alignment is idempotent, so running it unconditionally is free.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* A failed bump exits non-zero with its cause, the manifest restored (#3361). It used to be
|
|
34
|
+
* swallowed as a skip, so `latest:scope --record` marked the run fresh and a pnpm that could not
|
|
35
|
+
* self-update (the Corepack shim refuses it) stayed pinned silently. `josh latest` runs this step
|
|
36
|
+
* last, right before the record, so the dependency update and the audit still finish and only the
|
|
37
|
+
* record is withheld. A registry answer not newer than the pin, or nothing aged past the
|
|
38
|
+
* quarantine window yet, remains a non-fatal skip.
|
|
35
39
|
*
|
|
36
40
|
* Usage: tsx scripts/version/latest-corepack.ts
|
|
37
41
|
*/
|
|
@@ -54,9 +58,19 @@ const WORKSPACE_PATH = 'pnpm-workspace.yaml'
|
|
|
54
58
|
const SHA512_BYTES = 64
|
|
55
59
|
const INTEGRITY_RE = /^sha512-([A-Za-z0-9+/]+={0,2})$/u
|
|
56
60
|
const PACKAGE_MANAGER_VALUE_RE = /("packageManager"\s*:\s*")pnpm@[^"]+(")/u
|
|
61
|
+
|
|
62
|
+
// `target` is undefined both when the registry did not answer and when it answered but nothing has
|
|
63
|
+
// aged past the quarantine window yet; only the first is a failure (#3361).
|
|
64
|
+
interface TargetResolution {
|
|
65
|
+
target: string | undefined
|
|
66
|
+
is_registry_reachable: boolean
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
const PNPM_DEVELOPMENT_MANAGER_SCHEMA = z.object({ name: z.literal('pnpm'), version: z.string() })
|
|
58
70
|
const DEV_ENGINES_SCHEMA = z.object({ packageManager: PNPM_DEVELOPMENT_MANAGER_SCHEMA })
|
|
59
71
|
const DEV_ENGINES_PNPM_SCHEMA = z.object({ devEngines: DEV_ENGINES_SCHEMA.optional() })
|
|
72
|
+
const COREPACK_REMEDY =
|
|
73
|
+
'pnpm runs through the Corepack shim, where pnpm 11+ refuses self-update (ERR_PNPM_CANT_SELF_UPDATE_IN_COREPACK). Run `corepack disable pnpm`, install a standalone pnpm (https://pnpm.io/installation), then rerun josh latest.'
|
|
60
74
|
|
|
61
75
|
function extract_development_engines_version(package_json_content: string): string | undefined {
|
|
62
76
|
const parsed = DEV_ENGINES_PNPM_SCHEMA.safeParse(
|
|
@@ -128,22 +142,33 @@ function query_release_times(): Record<string, string> | undefined {
|
|
|
128
142
|
return extract_times_json(result.stdout)
|
|
129
143
|
}
|
|
130
144
|
|
|
145
|
+
function to_resolution(
|
|
146
|
+
version: string | undefined,
|
|
147
|
+
is_registry_reachable: boolean,
|
|
148
|
+
): TargetResolution {
|
|
149
|
+
const target = version === undefined ? undefined : `${TARGET_PREFIX}${version}`
|
|
150
|
+
|
|
151
|
+
return { target, is_registry_reachable }
|
|
152
|
+
}
|
|
153
|
+
|
|
131
154
|
// Ask the registry for the newest pnpm release on the pinned major that has aged past the
|
|
132
|
-
// quarantine window.
|
|
133
|
-
function
|
|
155
|
+
// quarantine window.
|
|
156
|
+
function resolve_major_target(major: string): TargetResolution {
|
|
134
157
|
const times = query_release_times()
|
|
135
|
-
if (times === undefined) return undefined
|
|
158
|
+
if (times === undefined) return to_resolution(undefined, false)
|
|
136
159
|
|
|
137
160
|
// The project's own `pnpm-workspace.yaml`, deliberately not the upward walk the version check
|
|
138
161
|
// uses: `josh latest` reads `package.json` and writes the pnpm pin relative to the working
|
|
139
162
|
// directory, so it has no subdirectory case — and honouring an ancestor's policy here could freeze
|
|
140
163
|
// pnpm bumps in a project that declares none (joshuafolkken/kit#808).
|
|
141
|
-
|
|
164
|
+
const version = release_age.select_aged_version(
|
|
142
165
|
times,
|
|
143
166
|
major,
|
|
144
167
|
release_age.read_minimum_release_age(WORKSPACE_PATH),
|
|
145
168
|
Date.now(),
|
|
146
169
|
)
|
|
170
|
+
|
|
171
|
+
return to_resolution(version, true)
|
|
147
172
|
}
|
|
148
173
|
|
|
149
174
|
function parse_latest_version(stdout: string): string | undefined {
|
|
@@ -163,24 +188,40 @@ function query_latest_version(): string | undefined {
|
|
|
163
188
|
return parse_latest_version(result.stdout)
|
|
164
189
|
}
|
|
165
190
|
|
|
166
|
-
// The
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
// The target handed to pnpm self-update: an exact registry-resolved version on the pinned major, or
|
|
192
|
+
// the latest exact version when no major can be read from package.json. The latest-version query
|
|
193
|
+
// has no quarantine selection, so resolving nothing there always means the registry did not answer.
|
|
194
|
+
function resolve_target(major: string | undefined): TargetResolution {
|
|
195
|
+
if (major !== undefined) return resolve_major_target(major)
|
|
196
|
+
const version = query_latest_version()
|
|
197
|
+
|
|
198
|
+
return to_resolution(version, version !== undefined)
|
|
199
|
+
}
|
|
172
200
|
|
|
173
|
-
|
|
201
|
+
function resolve_corepack_target(major: string | undefined): string | undefined {
|
|
202
|
+
return resolve_target(major).target
|
|
174
203
|
}
|
|
175
204
|
|
|
176
|
-
// Non-fatal skip message
|
|
177
|
-
// (latest:update, audit) running instead of aborting on a bump failure.
|
|
205
|
+
// Non-fatal skip message for a registry answer with nothing to adopt — nothing to bump, nothing failed.
|
|
178
206
|
function warn_skip(reason: string): void {
|
|
179
|
-
console.warn(`⚠ Skipped pnpm bump (${reason})
|
|
207
|
+
console.warn(`⚠ Skipped pnpm bump (${reason}).`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Every failure path ends here, so the exit status the `josh latest` chain stops on is one value.
|
|
211
|
+
function fail_bump(reason: string): number {
|
|
212
|
+
console.error(
|
|
213
|
+
`✖ pnpm bump failed (${reason}); the run is not recorded, so the next josh latest retries it.`,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return FAILURE_EXIT_CODE
|
|
180
217
|
}
|
|
181
218
|
|
|
182
|
-
|
|
183
|
-
|
|
219
|
+
// Corepack exports COREPACK_ROOT to the package manager it launches, and pnpm 11+ refuses
|
|
220
|
+
// `self-update` under it (ERR_PNPM_CANT_SELF_UPDATE_IN_COREPACK), so the remedy is named there.
|
|
221
|
+
function fail_self_update(status: number, environment: NodeJS.ProcessEnv = process.env): number {
|
|
222
|
+
if (environment['COREPACK_ROOT'] !== undefined) console.error(COREPACK_REMEDY)
|
|
223
|
+
|
|
224
|
+
return fail_bump(`pnpm self-update exited ${String(status)}`)
|
|
184
225
|
}
|
|
185
226
|
|
|
186
227
|
function run_pnpm_update(target: string): number {
|
|
@@ -193,17 +234,6 @@ function run_pnpm_update(target: string): number {
|
|
|
193
234
|
return result.exitCode ?? FAILURE_EXIT_CODE
|
|
194
235
|
}
|
|
195
236
|
|
|
196
|
-
// The target is an already-resolved exact version, so a non-zero status here is a
|
|
197
|
-
// genuine pnpm or network failure; a later run retries. Returns whether a skip
|
|
198
|
-
// happened.
|
|
199
|
-
function did_warn_skip(status: number): boolean {
|
|
200
|
-
if (status === 0) return false
|
|
201
|
-
|
|
202
|
-
warn_skip(`pnpm self-update exited ${String(status)}`)
|
|
203
|
-
|
|
204
|
-
return true
|
|
205
|
-
}
|
|
206
|
-
|
|
207
237
|
// Realign `devEngines.packageManager.version` with the `packageManager` pin so the two
|
|
208
238
|
// fields keep matching (pnpm suppresses the dual-declaration warning only on an exact
|
|
209
239
|
// match). Runs on every path, not just after a successful bump: a repository that arrives
|
|
@@ -224,7 +254,7 @@ function restore_package_json(
|
|
|
224
254
|
package_json_path: string = PACKAGE_JSON_PATH,
|
|
225
255
|
): void {
|
|
226
256
|
writeFileSync(package_json_path, content)
|
|
227
|
-
console.info('✔ Restored package.json pin (pnpm bump
|
|
257
|
+
console.info('✔ Restored package.json pin (pnpm bump failed)')
|
|
228
258
|
}
|
|
229
259
|
|
|
230
260
|
function decode_integrity(encoded: string): string | undefined {
|
|
@@ -292,35 +322,19 @@ function notify_skipped_bump(target: string, pinned_version: string): void {
|
|
|
292
322
|
warn_skip(`registry answered ${target}, below the pinned ${pinned_version}`)
|
|
293
323
|
}
|
|
294
324
|
|
|
295
|
-
|
|
296
|
-
// skipped and the reason has already been logged.
|
|
297
|
-
function resolve_floored_target(original: string, major: string | undefined): string | undefined {
|
|
298
|
-
const target = resolve_corepack_target(major)
|
|
299
|
-
|
|
300
|
-
if (target === undefined) {
|
|
301
|
-
warn_unresolved(major)
|
|
302
|
-
|
|
303
|
-
return undefined
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const pinned_version = extract_pinned_version(original)
|
|
307
|
-
if (!is_target_not_newer_than_pin(target, pinned_version)) return target
|
|
308
|
-
|
|
309
|
-
notify_skipped_bump(target, pinned_version ?? '')
|
|
310
|
-
|
|
311
|
-
return undefined
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function restore_after_update(original: string, target: string, integrity: string): void {
|
|
325
|
+
function restore_after_update(original: string, target: string, integrity: string): number {
|
|
315
326
|
try {
|
|
316
327
|
restore_integrity(target, integrity)
|
|
328
|
+
|
|
329
|
+
return 0
|
|
317
330
|
} catch {
|
|
318
331
|
restore_package_json(original)
|
|
319
|
-
|
|
332
|
+
|
|
333
|
+
return fail_bump('pnpm self-update wrote an unexpected pin')
|
|
320
334
|
}
|
|
321
335
|
}
|
|
322
336
|
|
|
323
|
-
function pin_unpinned_manifest(original: string, target: string, integrity: string):
|
|
337
|
+
function pin_unpinned_manifest(original: string, target: string, integrity: string): number {
|
|
324
338
|
const manifest = z.record(z.string(), z.unknown()).parse(json_value.parse_or_undefined(original))
|
|
325
339
|
const pinned = JSON.stringify(
|
|
326
340
|
{ ...manifest, packageManager: `${target}+${integrity}` },
|
|
@@ -334,53 +348,71 @@ function pin_unpinned_manifest(original: string, target: string, integrity: stri
|
|
|
334
348
|
)
|
|
335
349
|
|
|
336
350
|
const result = execaSync('pnpm', ['--version'], { reject: false })
|
|
337
|
-
if (result.exitCode === 0 && result.stdout.trim() === target.slice(TARGET_PREFIX.length)) return
|
|
351
|
+
if (result.exitCode === 0 && result.stdout.trim() === target.slice(TARGET_PREFIX.length)) return 0
|
|
338
352
|
|
|
339
353
|
restore_package_json(original)
|
|
340
|
-
warn_skip('the newly pinned pnpm version could not start')
|
|
341
|
-
}
|
|
342
354
|
|
|
343
|
-
|
|
344
|
-
|
|
355
|
+
return fail_bump('the newly pinned pnpm version could not start')
|
|
356
|
+
}
|
|
345
357
|
|
|
346
|
-
|
|
347
|
-
|
|
358
|
+
// The target is an already-resolved exact version, so a non-zero status here is a genuine
|
|
359
|
+
// pnpm, Corepack or network failure.
|
|
360
|
+
function update_pinned_manifest(original: string, target: string, integrity: string): number {
|
|
361
|
+
const status = run_pnpm_update(target)
|
|
362
|
+
if (status === 0) return restore_after_update(original, target, integrity)
|
|
348
363
|
|
|
349
|
-
|
|
350
|
-
}
|
|
364
|
+
restore_package_json(original)
|
|
351
365
|
|
|
352
|
-
|
|
366
|
+
return fail_self_update(status)
|
|
353
367
|
}
|
|
354
368
|
|
|
355
369
|
// Query integrity before mutating the manifest; self-update writes both pins without it.
|
|
356
|
-
function bump_package_manager(original: string, target: string):
|
|
370
|
+
function bump_package_manager(original: string, target: string): number {
|
|
357
371
|
const integrity = query_integrity(target)
|
|
372
|
+
if (integrity === undefined) return fail_bump(`no integrity for ${target}`)
|
|
373
|
+
if (!PACKAGE_MANAGER_RE.test(original)) return pin_unpinned_manifest(original, target, integrity)
|
|
358
374
|
|
|
359
|
-
|
|
360
|
-
|
|
375
|
+
return update_pinned_manifest(original, target, integrity)
|
|
376
|
+
}
|
|
361
377
|
|
|
362
|
-
|
|
363
|
-
|
|
378
|
+
// An unreachable registry fails, so the unrecorded run is retried. A reachable one with nothing
|
|
379
|
+
// aged past the quarantine window is the kit#768 skip — like a not-newer answer, there is nothing
|
|
380
|
+
// to adopt yet, and failing would stall every run for up to the whole window.
|
|
381
|
+
function report_unresolved(major: string | undefined, is_registry_reachable: boolean): number {
|
|
382
|
+
const label = `pnpm ${major ?? ''}`.trimEnd()
|
|
383
|
+
if (!is_registry_reachable) return fail_bump(`the registry did not answer for ${label}`)
|
|
364
384
|
|
|
365
|
-
|
|
366
|
-
pin_unpinned_manifest(original, target, integrity)
|
|
385
|
+
warn_skip(`no ${label} release has aged past the quarantine window yet`)
|
|
367
386
|
|
|
368
|
-
|
|
387
|
+
return 0
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Resolve the pnpm target with the pin floor applied, then bump.
|
|
391
|
+
function bump_to_registry_target(original: string, major: string | undefined): number {
|
|
392
|
+
const { target, is_registry_reachable } = resolve_target(major)
|
|
393
|
+
if (target === undefined) return report_unresolved(major, is_registry_reachable)
|
|
394
|
+
|
|
395
|
+
const pinned_version = extract_pinned_version(original)
|
|
396
|
+
|
|
397
|
+
if (!is_target_not_newer_than_pin(target, pinned_version)) {
|
|
398
|
+
return bump_package_manager(original, target)
|
|
369
399
|
}
|
|
370
400
|
|
|
371
|
-
|
|
401
|
+
notify_skipped_bump(target, pinned_version ?? '')
|
|
402
|
+
|
|
403
|
+
return 0
|
|
372
404
|
}
|
|
373
405
|
|
|
374
|
-
function main():
|
|
406
|
+
function main(): number {
|
|
375
407
|
const original = readFileSync(PACKAGE_JSON_PATH, 'utf8')
|
|
376
|
-
const
|
|
377
|
-
const target = resolve_floored_target(original, major)
|
|
378
|
-
if (target !== undefined) bump_package_manager(original, target)
|
|
408
|
+
const status = bump_to_registry_target(original, extract_pnpm_major(original))
|
|
379
409
|
|
|
380
410
|
sync_development_engines()
|
|
411
|
+
|
|
412
|
+
return status
|
|
381
413
|
}
|
|
382
414
|
|
|
383
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) main()
|
|
415
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) process.exitCode = main()
|
|
384
416
|
|
|
385
417
|
const latest_corepack = {
|
|
386
418
|
extract_pnpm_major,
|
|
@@ -388,12 +420,11 @@ const latest_corepack = {
|
|
|
388
420
|
is_target_not_newer_than_pin,
|
|
389
421
|
notify_skipped_bump,
|
|
390
422
|
extract_times_json,
|
|
391
|
-
query_major_latest_version,
|
|
392
423
|
resolve_corepack_target,
|
|
393
424
|
run_pnpm_update,
|
|
394
425
|
query_integrity,
|
|
395
426
|
restore_integrity,
|
|
396
|
-
|
|
427
|
+
fail_self_update,
|
|
397
428
|
sync_development_engines,
|
|
398
429
|
restore_package_json,
|
|
399
430
|
main,
|
|
@@ -7,6 +7,9 @@ name: GitHub Release
|
|
|
7
7
|
# started by the GITHUB_TOKEN never fires one (joshuafolkken/kit#3138). kit's own release is the
|
|
8
8
|
# `create-release` job of its publish.yml, which runs the same `josh release:github`, so this file is
|
|
9
9
|
# distributed but never runs in kit.
|
|
10
|
+
# The title names the tag, so a later tag's release can tell this run is still at work on it.
|
|
11
|
+
run-name: GitHub Release ${{ github.event.client_payload.tag }}
|
|
12
|
+
|
|
10
13
|
on:
|
|
11
14
|
repository_dispatch:
|
|
12
15
|
types: [new-tag-created]
|
|
@@ -67,3 +70,4 @@ jobs:
|
|
|
67
70
|
RELEASE_TAG: ${{ steps.release.outputs.tag }}
|
|
68
71
|
RELEASE_WORKFLOW: ${{ steps.release.outputs.workflow }}
|
|
69
72
|
RELEASE_AWAIT_PUBLISH: ${{ steps.release.outputs.await }}
|
|
73
|
+
RELEASE_RUN_WORKFLOW: github-release.yml
|