@joshuafolkken/kit 1.1035.0 → 1.1050.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/verify-ui/SKILL.md +1 -1
- package/.claude/skills/workflow-commands/SKILL.md +2 -2
- package/.claude/skills/workflow-commands/background-commands.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-child.md +2 -2
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-park.md +6 -0
- package/.claude/skills/workflow-commands/backlogrun-progress.md +30 -36
- package/.claude/skills/workflow-commands/backlogrun-steps.md +16 -16
- package/.claude/skills/workflow-commands/chain-rule.md +2 -2
- package/.claude/skills/workflow-commands/followup-reference.md +2 -2
- package/.claude/skills/workflow-commands/issue-comments.md +1 -1
- package/.claude/skills/workflow-commands/observation-filing.md +41 -86
- package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -36
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -2
- package/.github/workflows/auto-tag.yml +21 -0
- package/.github/workflows/publish.yml +9 -10
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-HU66PWWL.js → chunk-5BK3K7V4.js} +89 -41
- package/dist/hooks/{chunk-GLBDBJOK.js → chunk-7VPDUBRN.js} +49 -151
- package/dist/hooks/{chunk-GJT4BNVE.js → chunk-DMQN45FN.js} +3 -3
- package/dist/hooks/{chunk-D67KVGGY.js → chunk-EJDX2TOB.js} +13 -3
- package/dist/hooks/{chunk-KG6PZD72.js → chunk-GQ7KP45M.js} +2 -2
- package/dist/hooks/{chunk-Y27M6A7X.js → chunk-HJIISEAC.js} +4 -4
- package/dist/hooks/{chunk-XPLP27WA.js → chunk-NAS4C7GP.js} +4 -26
- package/dist/hooks/{chunk-ZXNE6NY3.js → chunk-NJQOBE6L.js} +2 -2
- package/dist/hooks/{chunk-EIFCAJRB.js → chunk-RLDX5SOC.js} +1184 -946
- package/dist/hooks/{chunk-HOYJU5RM.js → chunk-WQ4OSBSS.js} +10 -3
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +15 -34
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +23 -16
- package/dist/hooks/stop-guard.js +13 -188
- package/dist/josh.js +4 -106
- package/dist/version/index.js +381 -9
- package/package.json +6 -6
- package/prompts/collaboration-workflow/file-edits.md +6 -53
- package/prompts/collaboration-workflow/residency.md +1 -1
- package/prompts/collaboration-workflow/rule-delivery.md +4 -35
- package/prompts/collaboration-workflow/shell-body.md +7 -9
- package/prompts/collaboration-workflow/turn-batching.md +21 -37
- package/prompts/collaboration-workflow/wip-cap.md +15 -18
- package/prompts/sonar-hotspot-handling.md +1 -1
- package/scripts/adopt/adopt-toolkits.ts +2 -0
- package/scripts/auto-ok/auto-ok-cli.ts +9 -9
- package/scripts/backlog/backlog-next.ts +24 -8
- package/scripts/backlog/backlog-plan-cli.ts +5 -1
- package/scripts/backlog/backlog-pool.ts +31 -11
- package/scripts/backlog/backlog-rank.ts +106 -0
- package/scripts/backlog/backlog-waves.ts +42 -29
- package/scripts/build/build-hooks.ts +7 -2
- package/scripts/delegation/delegation-policy.ts +11 -0
- package/scripts/document/document-byte-budget.ts +5 -5
- package/scripts/document/entry-read-budget.ts +5 -4
- package/scripts/epic/epic-lane-offer.ts +11 -22
- package/scripts/epic/epic-rank.ts +37 -0
- package/scripts/epic/epic-solo.ts +29 -28
- package/scripts/git/git-command.ts +1 -1
- package/scripts/git/git-execa-error.ts +6 -1
- package/scripts/git/git-next-issues.ts +34 -5
- package/scripts/git/git-preflight.ts +22 -4
- package/scripts/git/git-push-transport.ts +12 -69
- package/scripts/git/git-spawn.ts +79 -3
- package/scripts/git/git-ssh-keepalive.ts +74 -0
- package/scripts/git/gone-branch.ts +3 -2
- package/scripts/git/issue-labels.ts +12 -0
- package/scripts/git/issue-rank.ts +89 -0
- package/scripts/hooks/format-edited-cli.ts +15 -0
- package/scripts/hooks/format-edited-file.ts +2 -9
- package/scripts/hooks/pretool-guard-cli.ts +21 -0
- package/scripts/hooks/pretool-guard.ts +14 -15
- package/scripts/init/start-exec.ts +7 -1
- package/scripts/init/start-plan.ts +18 -5
- package/scripts/init/start-setup-pr.ts +46 -19
- package/scripts/init/start-steps.ts +41 -9
- package/scripts/init/start.ts +40 -10
- package/scripts/issue/issue-cite.ts +19 -1
- package/scripts/josh/josh-command-map.ts +0 -102
- package/scripts/josh/josh-commands-development.ts +2 -2
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/lane/lane-child-marker.ts +16 -2
- package/scripts/lane/lane-cli.ts +32 -14
- package/scripts/lane/lane-report.ts +10 -4
- package/scripts/propagate/propagate-plan.ts +185 -0
- package/scripts/propagate/propagate-run.ts +5 -1
- package/scripts/propagate/propagate-steps.ts +16 -1
- package/scripts/propagate/propagate-targets.ts +13 -2
- package/scripts/propagate/propagate.ts +60 -42
- package/scripts/registry-migration/migrate-logic.ts +1 -0
- package/scripts/release/github-release.ts +1 -1
- package/scripts/review/review-attest.ts +44 -0
- package/scripts/review/review-brief-cli.ts +49 -2
- package/scripts/review/review-brief.ts +46 -1
- package/scripts/review/review-stamps.ts +7 -0
- package/scripts/rules/implementation-cut.ts +1 -1
- package/scripts/rules/issue-fold.ts +1 -2
- package/scripts/rules/lane-carry-conflict.ts +1 -2
- package/scripts/rules/lane-park.ts +1 -2
- package/scripts/rules/pre-gate-cut.ts +2 -2
- package/scripts/run/run-headless.ts +22 -6
- package/scripts/run/run-parent-cut-hook.ts +144 -0
- package/scripts/run/run-progress-read.ts +2 -1
- package/scripts/run/run-progress.ts +21 -8
- package/scripts/run/run-ship-cli.ts +6 -74
- package/scripts/run/run-ship-preflight.ts +87 -0
- package/scripts/run/run-ship-review-steps.ts +5 -26
- package/scripts/run/run-ship-review.ts +9 -0
- package/scripts/run/run-ship-scoped.ts +44 -0
- package/scripts/run/run-ship-stage.ts +5 -1
- package/scripts/run/run-ship-steps.ts +105 -0
- package/scripts/run/run-ship.ts +2 -0
- package/scripts/run/run-wake-cli.ts +2 -2
- package/scripts/run/run-wake-describe.ts +19 -1
- package/scripts/run/run-watcher-hook.ts +1 -1
- package/scripts/time-runtime/time-bundle-call.ts +2 -2
- package/scripts/version/npm-registry.ts +97 -0
- package/scripts/version/version-commands.ts +5 -4
- package/scripts/version/version-remote.ts +26 -12
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { changed_paths } from '#scripts/git/changed-paths'
|
|
3
|
+
import { git_branch } from '#scripts/git/git-branch'
|
|
4
|
+
import { git_gh_command } from '#scripts/git/git-gh-command'
|
|
5
|
+
import { git_preflight } from '#scripts/git/git-preflight'
|
|
6
|
+
import type { JoshResult } from '#scripts/josh/josh-run'
|
|
7
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
8
|
+
import { live_evidence } from '#scripts/review/live-evidence'
|
|
9
|
+
import { run_ship_scoped } from './run-ship-scoped'
|
|
10
|
+
|
|
11
|
+
// `josh ship`'s first stage (joshuafolkken/kit#2946). A detached ship used to meet the pull request's
|
|
12
|
+
// preconditions only at the commit stage — `git -y`'s preflight — or at the merge — `followup`'s
|
|
13
|
+
// live-evidence refusal — each after the review and the gate had already run, so every such stop
|
|
14
|
+
// relaunched a session to repeat them. **Every precondition decidable before the review is asked here,
|
|
15
|
+
// and all of them are reported at once.** No check is new: the classification, issue and branch answers
|
|
16
|
+
// are `git_preflight.problems_of`, and the evidence answer is `live_evidence.verdict_for` over the same
|
|
17
|
+
// changed paths and the body this ship will deliver — `--body-file` when given, else the open PR's.
|
|
18
|
+
//
|
|
19
|
+
// The scoped pair runs last, met rather than stopped on (`run-ship-scoped.ts`), so the review and the
|
|
20
|
+
// gate start on a tree both checks are green on.
|
|
21
|
+
|
|
22
|
+
const SUCCESS_EXIT_CODE = 0
|
|
23
|
+
const FAILURE_EXIT_CODE = 1
|
|
24
|
+
const EVIDENCE_PROBLEM = `This change touches runtime code, but the PR body has no ${live_evidence.EVIDENCE_HEADING} section: run the acceptance criteria for real and pass the body with --body-file <path>.`
|
|
25
|
+
|
|
26
|
+
interface PreflightRequest {
|
|
27
|
+
title: string
|
|
28
|
+
body_path: string | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function body_text(body_path: string | undefined): Promise<string | undefined> {
|
|
32
|
+
if (body_path !== undefined) return await readFile(body_path, 'utf8')
|
|
33
|
+
|
|
34
|
+
return await git_gh_command.pr_get_body(await git_branch.current())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function evidence_problems(body_path: string | undefined): Promise<Array<string>> {
|
|
38
|
+
const [paths, body] = await Promise.all([
|
|
39
|
+
changed_paths.read_changed_paths(false),
|
|
40
|
+
body_text(body_path),
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
return live_evidence.verdict_for(paths, body) === 'required' ? [EVIDENCE_PROBLEM] : []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function refusal(problems: ReadonlyArray<string>): JoshResult {
|
|
47
|
+
const lines = problems.map((problem) => ` - ${problem}`)
|
|
48
|
+
const head = `${String(problems.length)} pull-request precondition(s) unmet before the review and the gate:`
|
|
49
|
+
|
|
50
|
+
return { code: FAILURE_EXIT_CODE, out: [head, ...lines].join('\n') }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The reads here run in the ship's own process, where the commit stage's ran in a `git -y` child: a
|
|
54
|
+
// throw — a `--body-file` path that does not exist, a git read that failed — would escape `ship()`
|
|
55
|
+
// past the stop a supervised ship hands back on, so it is reported as one more unmet precondition.
|
|
56
|
+
// Each side is caught on its own, so one side's failed read never hides what the other side found.
|
|
57
|
+
async function caught(problems: Promise<Array<string>>): Promise<Array<string>> {
|
|
58
|
+
try {
|
|
59
|
+
return await problems
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return [error_text.message_of(error)]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function asked_problems(request: PreflightRequest): Promise<Array<string>> {
|
|
66
|
+
const [preflight, evidence] = await Promise.all([
|
|
67
|
+
caught(git_preflight.problems_of({ cli_input: request.title, will_open_pr: true })),
|
|
68
|
+
caught(evidence_problems(request.body_path)),
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
return [...preflight, ...evidence]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function stage(request: PreflightRequest): Promise<JoshResult> {
|
|
75
|
+
const problems = await asked_problems(request)
|
|
76
|
+
|
|
77
|
+
if (problems.length > 0) return refusal(problems)
|
|
78
|
+
|
|
79
|
+
const scoped = await run_ship_scoped.scoped_pair()
|
|
80
|
+
|
|
81
|
+
return scoped.code === SUCCESS_EXIT_CODE ? { code: SUCCESS_EXIT_CODE, out: 'ready' } : scoped
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const run_ship_preflight = { EVIDENCE_PROBLEM, stage }
|
|
85
|
+
|
|
86
|
+
export type { PreflightRequest }
|
|
87
|
+
export { run_ship_preflight }
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { agent_argv, type AgentArgv } from '#scripts/agent/agent-argv'
|
|
2
2
|
import { agent_role_profile, type AgentProfile } from '#scripts/agent/agent-role-profile'
|
|
3
|
-
import { gate_tree } from '#scripts/gate/gate-tree'
|
|
4
|
-
import { scoped_green } from '#scripts/gate/scoped-green'
|
|
5
3
|
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
6
4
|
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
7
5
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
@@ -9,6 +7,7 @@ import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
|
9
7
|
import { openai_review_broker } from '#scripts/lane/openai-review-broker'
|
|
10
8
|
import { detached_launch } from './detached-launch'
|
|
11
9
|
import { run_ship_review, type RoundOutcome, type ScoredVerdict } from './run-ship-review'
|
|
10
|
+
import { run_ship_scoped, type Phase } from './run-ship-scoped'
|
|
12
11
|
|
|
13
12
|
// The side effects of `josh ship --review` (joshuafolkken/kit#2427): the round-1 review the chain used
|
|
14
13
|
// to open, launch, join, attest and record across five agent turns, run by the supervisor beside the
|
|
@@ -47,7 +46,10 @@ const FIXED_JOIN_NOTE =
|
|
|
47
46
|
'the round-1 gate read the tree before the review fixes — drained, not judged; the gate stage re-runs it.'
|
|
48
47
|
const ROUND_TWO_SKIPPED_NOTE = 'round 2 not due — nothing left to verify; shipping on.'
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
// The scoped pair's precondition, met by the supervisor itself rather than stopped on
|
|
50
|
+
// (joshuafolkken/kit#2500), single-sourced with the preflight and gate stages (joshuafolkken/kit#2946).
|
|
51
|
+
const { run_phases, scoped_pair } = run_ship_scoped
|
|
52
|
+
|
|
51
53
|
type PromptOf = (brief_path: string, findings_path: string) => string
|
|
52
54
|
|
|
53
55
|
function failure(out: string): JoshResult {
|
|
@@ -153,17 +155,6 @@ async function record_and_route(
|
|
|
153
155
|
return outcome.is_passing ? { code: SUCCESS_EXIT_CODE, out } : failure(out)
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
async function run_phases(phases: ReadonlyArray<Phase>): Promise<JoshResult> {
|
|
157
|
-
let last: JoshResult = { code: SUCCESS_EXIT_CODE, out: '' }
|
|
158
|
-
|
|
159
|
-
for (const phase of phases) {
|
|
160
|
-
last = await phase()
|
|
161
|
-
if (last.code !== SUCCESS_EXIT_CODE) return last
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return last
|
|
165
|
-
}
|
|
166
|
-
|
|
167
158
|
// A brief-minting command, then a reviewer handed what it printed.
|
|
168
159
|
function reviewed(
|
|
169
160
|
open: ReadonlyArray<string>,
|
|
@@ -185,18 +176,6 @@ function reviewed(
|
|
|
185
176
|
]
|
|
186
177
|
}
|
|
187
178
|
|
|
188
|
-
// The scoped pair's precondition, met by the supervisor itself rather than stopped on
|
|
189
|
-
// (joshuafolkken/kit#2500): only the checks with no green record for this tree run — the same
|
|
190
|
-
// question `review:brief` and the local gate refuse on — so a fresh record costs nothing and a stale
|
|
191
|
-
// or absent one costs a scoped run instead of a stop and a relaunched session. A check that genuinely
|
|
192
|
-
// fails still stops the ship, its output forwarded to the ship log.
|
|
193
|
-
async function scoped_pair(): Promise<JoshResult> {
|
|
194
|
-
const tree = await gate_tree.read_gate_tree()
|
|
195
|
-
const scripts = scoped_green.missing_scripts(tree.files, tree.base)
|
|
196
|
-
|
|
197
|
-
return await run_phases(scripts.map((script) => async () => await josh([script])))
|
|
198
|
-
}
|
|
199
|
-
|
|
200
179
|
// Fixes made in place are counted only once the scoped pair is green on the fixed tree. The reviewer is
|
|
201
180
|
// asked to leave it green; this is the check that it did, and a fix it could not get green is routed as
|
|
202
181
|
// the finding it still is — recorded, then stopped before the gate and the commit take it in.
|
|
@@ -154,6 +154,14 @@ function round_one_outcome(verdict: ScoredVerdict): RoundOutcome {
|
|
|
154
154
|
return { is_passing: true, note: verdict.kind === VERDICT.FIXED ? FIXED_NOTE : CLEAN_NOTE }
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// Whether a findings file records a clean round 1 (joshuafolkken/kit#2945). A resumed round 1 narrows to
|
|
158
|
+
// the delta only past a review that found nothing to fix. A blocking one has a finding in a file the
|
|
159
|
+
// delta may not touch. A fixed one approved its own edits, not the briefed tree the delta is measured
|
|
160
|
+
// from, so a file reverted to that tree would drop out of the delta with its finding back in it.
|
|
161
|
+
function is_clean_round_one(text: string | undefined): boolean {
|
|
162
|
+
return read_verdict(text).kind === VERDICT.CLEAN
|
|
163
|
+
}
|
|
164
|
+
|
|
157
165
|
// Round 2 is final and reads only, so it passes clean or Low-only alone — a fix it made would itself be
|
|
158
166
|
// unreviewed.
|
|
159
167
|
function round_two_outcome(verdict: ScoredVerdict): RoundOutcome {
|
|
@@ -166,6 +174,7 @@ const run_ship_review = {
|
|
|
166
174
|
FIXED_PREFIX,
|
|
167
175
|
UNVERIFIED_OUTCOME,
|
|
168
176
|
VERDICT,
|
|
177
|
+
is_clean_round_one,
|
|
169
178
|
read_verdict,
|
|
170
179
|
reviewer_prompt,
|
|
171
180
|
round_one_outcome,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { gate_tree } from '#scripts/gate/gate-tree'
|
|
2
|
+
import { scoped_green } from '#scripts/gate/scoped-green'
|
|
3
|
+
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
4
|
+
|
|
5
|
+
// The scoped pair `josh ship` meets itself rather than stops on (joshuafolkken/kit#2500), shared by
|
|
6
|
+
// every stage that needs a green record for this tree (joshuafolkken/kit#2946): the preflight stage at
|
|
7
|
+
// the start, the review rounds, and the gate stage — a reviewer may have edited the tree after round
|
|
8
|
+
// 1's pair ran, and `josh gate` refuses a tree with no green record.
|
|
9
|
+
|
|
10
|
+
const SUCCESS_EXIT_CODE = 0
|
|
11
|
+
const should_forward_stderr = true
|
|
12
|
+
|
|
13
|
+
type Phase = () => Promise<JoshResult>
|
|
14
|
+
|
|
15
|
+
async function run_phases(phases: ReadonlyArray<Phase>): Promise<JoshResult> {
|
|
16
|
+
let last: JoshResult = { code: SUCCESS_EXIT_CODE, out: '' }
|
|
17
|
+
|
|
18
|
+
for (const phase of phases) {
|
|
19
|
+
last = await phase()
|
|
20
|
+
if (last.code !== SUCCESS_EXIT_CODE) return last
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return last
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Only the checks with no green record for this tree run — the same question `review:brief` and the
|
|
27
|
+
// local gate refuse on — so a fresh record costs nothing and a stale or absent one costs a scoped run
|
|
28
|
+
// instead of a stop and a relaunched session. A check that genuinely fails still stops the ship, its
|
|
29
|
+
// output forwarded to the ship log.
|
|
30
|
+
async function scoped_pair(): Promise<JoshResult> {
|
|
31
|
+
const tree = await gate_tree.read_gate_tree()
|
|
32
|
+
const scripts = scoped_green.missing_scripts(tree.files, tree.base)
|
|
33
|
+
|
|
34
|
+
return await run_phases(
|
|
35
|
+
scripts.map(
|
|
36
|
+
(script) => async () => await josh_command.josh_run([script], should_forward_stderr),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const run_ship_scoped = { run_phases, scoped_pair }
|
|
42
|
+
|
|
43
|
+
export type { Phase }
|
|
44
|
+
export { run_ship_scoped }
|
|
@@ -17,6 +17,7 @@ import { z } from 'zod'
|
|
|
17
17
|
// re-run on an unchanged tree a reuse rather than a second execution. No quality gate is folded away.
|
|
18
18
|
|
|
19
19
|
const STAGE = {
|
|
20
|
+
PREFLIGHT: 'preflight',
|
|
20
21
|
REVIEW: 'review',
|
|
21
22
|
GATE: 'gate',
|
|
22
23
|
COMMIT: 'commit',
|
|
@@ -78,7 +79,7 @@ function mark_done(target: string, stage: Stage): void {
|
|
|
78
79
|
stamp_file.replace_stamp(target, { done: [...done] })
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
// Removed once the report stage completes, so a later ship of the same issue starts from the
|
|
82
|
+
// Removed once the report stage completes, so a later ship of the same issue starts from the preflight.
|
|
82
83
|
function clear(target: string): void {
|
|
83
84
|
stamp_file.remove_stamp(target)
|
|
84
85
|
}
|
|
@@ -90,6 +91,9 @@ function is_shipped(state: ShipState): boolean {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
const DONE_BY: Record<Stage, (done: ReadonlySet<string>, state: ShipState) => boolean> = {
|
|
94
|
+
// The preflight (joshuafolkken/kit#2946) asks what the commit stage would refuse on, so a commit is
|
|
95
|
+
// its output as it is the gate's; a resumed commit stage still asks `git -y`'s own preflight.
|
|
96
|
+
[STAGE.PREFLIGHT]: (_done, state) => state.is_merged || state.is_committed,
|
|
93
97
|
// The `--review` round (joshuafolkken/kit#2427) precedes the commit, so a commit is its output as it
|
|
94
98
|
// is the gate's. A record alone is not honored: before a commit the tree may have been edited since
|
|
95
99
|
// the recorded round, and only a commit pins the tree that round read.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
|
|
2
|
+
import { run_ship } from './run-ship'
|
|
3
|
+
import { run_ship_preflight } from './run-ship-preflight'
|
|
4
|
+
import { run_ship_review_steps } from './run-ship-review-steps'
|
|
5
|
+
import { run_ship_scoped } from './run-ship-scoped'
|
|
6
|
+
import { run_ship_stage, type ShipState, type Stage } from './run-ship-stage'
|
|
7
|
+
|
|
8
|
+
// The stages `josh ship` runs, in the order a change ships (joshuafolkken/kit#2398), kept apart from the
|
|
9
|
+
// chaining CLI so the order is read in one place.
|
|
10
|
+
|
|
11
|
+
const should_forward_stderr = true
|
|
12
|
+
|
|
13
|
+
interface ShipArguments {
|
|
14
|
+
title: string
|
|
15
|
+
number: string
|
|
16
|
+
notify: ReadonlyArray<string>
|
|
17
|
+
body: ReadonlyArray<string>
|
|
18
|
+
body_path: string | undefined
|
|
19
|
+
cites: ReadonlyArray<string>
|
|
20
|
+
is_review: boolean
|
|
21
|
+
is_detach: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Step {
|
|
25
|
+
stage: Stage
|
|
26
|
+
header: string
|
|
27
|
+
run: (args: ShipArguments, state: ShipState) => Promise<JoshResult>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { STAGE } = run_ship_stage
|
|
31
|
+
|
|
32
|
+
async function josh(argv: ReadonlyArray<string>): Promise<JoshResult> {
|
|
33
|
+
return await josh_command.josh_run(argv, should_forward_stderr)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// joshuafolkken/kit#2946: the pull request's preconditions and the scoped pair are asked first, so a
|
|
37
|
+
// stop they would cause lands before the review and the gate rather than after them.
|
|
38
|
+
const PREFLIGHT_STEP: Step = {
|
|
39
|
+
stage: STAGE.PREFLIGHT,
|
|
40
|
+
header: run_ship.PREFLIGHT_HEADER,
|
|
41
|
+
run: async (args) =>
|
|
42
|
+
await run_ship_preflight.stage({ title: args.title, body_path: args.body_path }),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The gate before the commit, the commit/push/PR before the merge. The gate meets the scoped pair
|
|
46
|
+
// first: a round-1 reviewer may have edited the tree since the preflight, and `josh gate` refuses a tree
|
|
47
|
+
// with no green record (joshuafolkken/kit#2946). The commit step carries the `--skip-*` flags a resumed
|
|
48
|
+
// ship needs, so an existing commit or push is never made twice.
|
|
49
|
+
const COMMIT_STEPS: ReadonlyArray<Step> = [
|
|
50
|
+
{
|
|
51
|
+
stage: STAGE.GATE,
|
|
52
|
+
header: run_ship.GATE_HEADER,
|
|
53
|
+
run: async () =>
|
|
54
|
+
await run_ship_scoped.run_phases([
|
|
55
|
+
run_ship_scoped.scoped_pair,
|
|
56
|
+
async () => await josh(['gate']),
|
|
57
|
+
]),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
stage: STAGE.COMMIT,
|
|
61
|
+
header: run_ship.COMMIT_HEADER,
|
|
62
|
+
run: async (args, state) =>
|
|
63
|
+
await josh(['git', '-y', ...run_ship_stage.commit_flags(state), ...args.body, args.title]),
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
// The merge before the report bookkeeping.
|
|
68
|
+
const MERGE_STEPS: ReadonlyArray<Step> = [
|
|
69
|
+
{
|
|
70
|
+
stage: STAGE.FOLLOWUP,
|
|
71
|
+
header: run_ship.FOLLOWUP_HEADER,
|
|
72
|
+
run: async (args) => await josh(['followup', args.title, ...args.notify]),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
stage: STAGE.REPORT,
|
|
76
|
+
header: run_ship.REPORT_HEADER,
|
|
77
|
+
run: async (args) => await josh(['run:tail', args.number, ...args.cites]),
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
// `--review` (joshuafolkken/kit#2427) puts the supervised round-1 review in front of the gate: it
|
|
82
|
+
// launches the gate itself, so the gate stage that follows reuses that tree's green record. It also
|
|
83
|
+
// puts the round-2 pass between the commit and the followup (joshuafolkken/kit#2489), so the PR opens
|
|
84
|
+
// between the rounds and round 2 runs beside CI — a no-op when round 1 left no fix delta.
|
|
85
|
+
const REVIEW_STEP: Step = {
|
|
86
|
+
stage: STAGE.REVIEW,
|
|
87
|
+
header: run_ship.REVIEW_HEADER,
|
|
88
|
+
run: async (args) => await run_ship_review_steps.review_stage(args.number),
|
|
89
|
+
}
|
|
90
|
+
const ROUND_TWO_STEP: Step = {
|
|
91
|
+
stage: STAGE.ROUND_TWO,
|
|
92
|
+
header: run_ship.ROUND_TWO_HEADER,
|
|
93
|
+
run: async (args) => await run_ship_review_steps.round_two_stage(args.number),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function steps(args: ShipArguments): ReadonlyArray<Step> {
|
|
97
|
+
if (!args.is_review) return [PREFLIGHT_STEP, ...COMMIT_STEPS, ...MERGE_STEPS]
|
|
98
|
+
|
|
99
|
+
return [PREFLIGHT_STEP, REVIEW_STEP, ...COMMIT_STEPS, ROUND_TWO_STEP, ...MERGE_STEPS]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const run_ship_steps = { steps }
|
|
103
|
+
|
|
104
|
+
export type { ShipArguments, Step }
|
|
105
|
+
export { run_ship_steps }
|
package/scripts/run/run-ship.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// `run:tail`, ship stops at the first failed step: a red gate must never reach the commit, so the
|
|
10
10
|
// composite ends at the failure, and the report names the step that failed so the run reads only it.
|
|
11
11
|
|
|
12
|
+
const PREFLIGHT_HEADER = '=== preflight ==='
|
|
12
13
|
const REVIEW_HEADER = '=== review ==='
|
|
13
14
|
const GATE_HEADER = '=== gate ==='
|
|
14
15
|
const COMMIT_HEADER = '=== commit/push/PR ==='
|
|
@@ -63,6 +64,7 @@ const run_ship = {
|
|
|
63
64
|
COMMIT_HEADER,
|
|
64
65
|
FOLLOWUP_HEADER,
|
|
65
66
|
GATE_HEADER,
|
|
67
|
+
PREFLIGHT_HEADER,
|
|
66
68
|
REPORT_HEADER,
|
|
67
69
|
REVIEW_HEADER,
|
|
68
70
|
ROUND_TWO_HEADER,
|
|
@@ -290,7 +290,7 @@ function start(context: WakeContext, interval: string | undefined): number {
|
|
|
290
290
|
const existing = run_wake.read_wake(context.wake_target)
|
|
291
291
|
|
|
292
292
|
if (existing !== undefined && run_wake.is_supervisor_live(existing)) {
|
|
293
|
-
console.error(run_wake_describe.
|
|
293
|
+
console.error(run_wake_describe.describe_wake_cited(existing, context))
|
|
294
294
|
|
|
295
295
|
return report(RUNNING_VERDICT)
|
|
296
296
|
}
|
|
@@ -342,7 +342,7 @@ function list(context: WakeContext): number {
|
|
|
342
342
|
|
|
343
343
|
if (wake === undefined) return report(NONE_VERDICT)
|
|
344
344
|
|
|
345
|
-
console.error(run_wake_describe.
|
|
345
|
+
console.error(run_wake_describe.describe_wake_cited(wake, context))
|
|
346
346
|
|
|
347
347
|
return report(run_wake.is_supervisor_live(wake) ? SUPERVISING_VERDICT : STALE_VERDICT)
|
|
348
348
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { agent_role_profile } from '#scripts/agent/agent-role-profile'
|
|
2
|
+
import { gh_spawn } from '#scripts/gh/gh-spawn'
|
|
3
|
+
import { issue_citation } from '#scripts/rules/issue-citation'
|
|
2
4
|
import { run_carry, type CarryRead } from './run-carry'
|
|
3
5
|
import { run_event_stream } from './run-event-stream'
|
|
4
6
|
import { run_liveness } from './run-liveness'
|
|
@@ -9,6 +11,7 @@ import { run_wake, type RunWake } from './run-wake'
|
|
|
9
11
|
// record reads back to a person lives here, one cohesive block.
|
|
10
12
|
|
|
11
13
|
const UNKNOWN_CUTS = 'an unreadable number of'
|
|
14
|
+
const REPO_LOOKUP_TIMEOUT_MS = 10_000
|
|
12
15
|
const STOP_COMMAND = 'pnpm josh run:wake --stop'
|
|
13
16
|
|
|
14
17
|
// The three paths every verb needs: the carry record it reads, its own record, and the work tree a
|
|
@@ -91,7 +94,22 @@ function describe_wake(wake: RunWake, context: WakeContext): string {
|
|
|
91
94
|
.join('\n')
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
// The description as a person reads it, every issue on it a number-link (joshuafolkken/kit#2943). The
|
|
98
|
+
// invocation and the newest event both name issues (`backlogrun #1 #2`, `#1904 merged`), and a report
|
|
99
|
+
// that copies them bare is what the Stop guard sends back. The repository is asked here, bounded, rather
|
|
100
|
+
// than in the context every verb resolves: only the two describing verbs print it, and a lookup that
|
|
101
|
+
// does not answer leaves the description as it was rather than withholding it.
|
|
102
|
+
function cite_description(description: string, repo: string | undefined): string {
|
|
103
|
+
return repo === undefined ? description : issue_citation.linkify(description, repo)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function describe_wake_cited(wake: RunWake, context: WakeContext): string {
|
|
107
|
+
const repo = gh_spawn.get_repo_name_with_owner_within(REPO_LOOKUP_TIMEOUT_MS)
|
|
108
|
+
|
|
109
|
+
return cite_description(describe_wake(wake, context), repo)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const run_wake_describe = { STOP_COMMAND, cite_description, describe_wake, describe_wake_cited }
|
|
95
113
|
|
|
96
114
|
export type { WakeContext }
|
|
97
115
|
export { run_wake_describe }
|
|
@@ -81,6 +81,6 @@ async function watcher_hook_reason(
|
|
|
81
81
|
return await stale_reason(transcript, now_ms)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const run_watcher_hook = { SWITCH_ENV_KEY, STAMP_PREFIX, watcher_hook_reason }
|
|
84
|
+
const run_watcher_hook = { SWITCH_ENV_KEY, STAMP_PREFIX, transcript_of, watcher_hook_reason }
|
|
85
85
|
|
|
86
86
|
export { run_watcher_hook }
|
|
@@ -336,8 +336,8 @@ function josh_subcommand(command: string): string | undefined {
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
// Whether a shell line is one of the read-only `josh` bookkeeping commands, and nothing else. A chained
|
|
339
|
-
// line falls through. An alias is resolved to its canonical name first
|
|
340
|
-
//
|
|
339
|
+
// line falls through. An alias is resolved to its canonical name first, so the allow-list holds
|
|
340
|
+
// canonical names alone. Everything not on the list falls
|
|
341
341
|
// through to the read/mutation test, which is what keeps `josh gate` / `followup` / `git`
|
|
342
342
|
// non-bundleable (joshuafolkken/kit#1875).
|
|
343
343
|
function is_read_josh(command: string): boolean {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { migrate_logic } from '#scripts/registry-migration/migrate-logic'
|
|
2
|
+
import { execaSync } from 'execa'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { safe_json_parse } from './parse-json'
|
|
5
|
+
import { release_age } from './release-age'
|
|
6
|
+
|
|
7
|
+
// The public npm registry, read without credentials so a consumer who installed from public npm
|
|
8
|
+
// can see the latest release without a `read:packages` GitHub token (joshuafolkken/kit#2882). The
|
|
9
|
+
// GitHub Packages versions API stays the source for a scope routed there, and the fallback for a
|
|
10
|
+
// package public npm does not carry.
|
|
11
|
+
const PUBLIC_REGISTRY = 'https://registry.npmjs.org'
|
|
12
|
+
// The abbreviated packument carries `dist-tags` without every version's full manifest.
|
|
13
|
+
const ABBREVIATED_ACCEPT = 'application/vnd.npm.install-v1+json'
|
|
14
|
+
const FULL_ACCEPT = 'application/json'
|
|
15
|
+
const REQUEST_TIMEOUT_MS = 10_000
|
|
16
|
+
const SCOPE_SEPARATOR = '/'
|
|
17
|
+
const ENCODED_SCOPE_SEPARATOR = '%2f'
|
|
18
|
+
const SCOPE_PATTERN = /^@[^/]+(?=\/)/u
|
|
19
|
+
const SCOPE_REGISTRY_SUFFIX = ':registry'
|
|
20
|
+
const github_scopes = new Map<string, boolean>()
|
|
21
|
+
|
|
22
|
+
// The version commands are synchronous end to end and exported that way to consumers, so the
|
|
23
|
+
// request runs in a child Node process rather than turning the whole chain async. Node's own
|
|
24
|
+
// `fetch` needs no external binary, and a non-2xx answer exits non-zero so it reads as "no answer".
|
|
25
|
+
const FETCH_SCRIPT = [
|
|
26
|
+
'const [url, accept] = process.argv.slice(1)',
|
|
27
|
+
'const response = await fetch(url, { headers: { accept } })',
|
|
28
|
+
'if (!response.ok) process.exit(1)',
|
|
29
|
+
'process.stdout.write(await response.text())',
|
|
30
|
+
].join('\n')
|
|
31
|
+
|
|
32
|
+
const latest_schema = z.looseObject({ 'dist-tags': z.looseObject({ latest: z.string() }) })
|
|
33
|
+
const times_schema = z.looseObject({ time: release_age.release_times_schema })
|
|
34
|
+
|
|
35
|
+
// The packument URL for a package name; a scoped name keeps its `@` and encodes the separator, the
|
|
36
|
+
// form the registry documents for scoped packages.
|
|
37
|
+
function packument_url(package_name: string): string {
|
|
38
|
+
return `${PUBLIC_REGISTRY}/${package_name.replace(SCOPE_SEPARATOR, () => ENCODED_SCOPE_SEPARATOR)}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function read_packument(package_name: string, accept: string): unknown {
|
|
42
|
+
const result = execaSync(
|
|
43
|
+
process.execPath,
|
|
44
|
+
['--input-type=module', '-e', FETCH_SCRIPT, packument_url(package_name), accept],
|
|
45
|
+
{ reject: false, timeout: REQUEST_TIMEOUT_MS },
|
|
46
|
+
)
|
|
47
|
+
if (result.exitCode !== 0) return undefined
|
|
48
|
+
|
|
49
|
+
return safe_json_parse(result.stdout)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Whether this checkout installs the package's scope from GitHub Packages, read with the same
|
|
53
|
+
// `pnpm config get` `josh registry:migrate` uses (project `.npmrc` over user `~/.npmrc`). The two
|
|
54
|
+
// registries are published by parallel jobs and can drift, so a consumer routed to GitHub Packages
|
|
55
|
+
// keeps reading it: public npm could otherwise offer a release its registry does not carry yet.
|
|
56
|
+
// Memoized per scope — kit and app-kit share one, and one report asks for both packages.
|
|
57
|
+
function is_github_scope(scope: string): boolean {
|
|
58
|
+
const is_cached_github = github_scopes.get(scope)
|
|
59
|
+
if (is_cached_github !== undefined) return is_cached_github
|
|
60
|
+
const result = execaSync('pnpm', ['config', 'get', `${scope}${SCOPE_REGISTRY_SUFFIX}`], {
|
|
61
|
+
reject: false,
|
|
62
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
63
|
+
})
|
|
64
|
+
const is_github = result.exitCode === 0 && migrate_logic.is_github_tarball(result.stdout.trim())
|
|
65
|
+
|
|
66
|
+
github_scopes.set(scope, is_github)
|
|
67
|
+
|
|
68
|
+
return is_github
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function is_public_source(package_name: string): boolean {
|
|
72
|
+
const scope = SCOPE_PATTERN.exec(package_name)?.[0]
|
|
73
|
+
|
|
74
|
+
return scope === undefined || !is_github_scope(scope)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The `latest` dist-tag on public npm, or nothing when the package is not installed from there or
|
|
78
|
+
// the registry cannot answer for it (offline, timed out, or not published there).
|
|
79
|
+
function read_latest(package_name: string): string | undefined {
|
|
80
|
+
if (!is_public_source(package_name)) return undefined
|
|
81
|
+
const parsed = latest_schema.safeParse(read_packument(package_name, ABBREVIATED_ACCEPT))
|
|
82
|
+
|
|
83
|
+
return parsed.success ? parsed.data['dist-tags'].latest : undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The version → publish-date map from public npm, under the same source rule as `read_latest`. It
|
|
87
|
+
// also carries `created` / `modified` keys, which the release-age selector skips as non-semver.
|
|
88
|
+
function read_release_times(package_name: string): Record<string, string> | undefined {
|
|
89
|
+
if (!is_public_source(package_name)) return undefined
|
|
90
|
+
const parsed = times_schema.safeParse(read_packument(package_name, FULL_ACCEPT))
|
|
91
|
+
|
|
92
|
+
return parsed.success ? parsed.data.time : undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const npm_registry = { packument_url, read_latest, read_release_times }
|
|
96
|
+
|
|
97
|
+
export { npm_registry }
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type VersionSnapshot,
|
|
14
14
|
} from './version-check-logic'
|
|
15
15
|
import type {
|
|
16
|
+
PackageVersionConfig,
|
|
16
17
|
UpstreamHookContext,
|
|
17
18
|
UpstreamVersionConfig,
|
|
18
19
|
VersionCommandConfig,
|
|
@@ -29,11 +30,11 @@ const NO_QUARANTINE_MINUTES = 0
|
|
|
29
30
|
// whose `installable` is undefined when the timestamps cannot be read — the report then renders
|
|
30
31
|
// exactly as it did before (joshuafolkken/kit#808).
|
|
31
32
|
//
|
|
32
|
-
// Skipped entirely unless some target is behind `latest`: the timestamps cost a second
|
|
33
|
+
// Skipped entirely unless some target is behind `latest`: the timestamps cost a second registry
|
|
33
34
|
// round trip per package, and a package with nothing stale has no gap for a hold to explain. On the
|
|
34
35
|
// common all-current run the report costs exactly what it did before this feature.
|
|
35
36
|
function read_release_hold(
|
|
36
|
-
|
|
37
|
+
package_config: PackageVersionConfig,
|
|
37
38
|
latest: string,
|
|
38
39
|
versions: ReadonlyArray<string | undefined>,
|
|
39
40
|
): ReleaseHold | undefined {
|
|
@@ -45,7 +46,7 @@ function read_release_hold(
|
|
|
45
46
|
// a registry round trip discovering that.
|
|
46
47
|
if (minimum_age_minutes <= NO_QUARANTINE_MINUTES) return undefined
|
|
47
48
|
const installable = release_hold.resolve_installable(
|
|
48
|
-
fetch_release_times(versions_endpoint),
|
|
49
|
+
fetch_release_times(package_config.versions_endpoint, package_config.package_name),
|
|
49
50
|
latest,
|
|
50
51
|
minimum_age_minutes,
|
|
51
52
|
Date.now(),
|
|
@@ -157,7 +158,7 @@ function read_upstream_report(
|
|
|
157
158
|
// After the effective install is attached, so its version counts toward the staleness gate.
|
|
158
159
|
// Only the effective install is peer-resolved, so it is the only target the window can hold back.
|
|
159
160
|
const hold = is_hold_wanted
|
|
160
|
-
? read_release_hold(upstream
|
|
161
|
+
? read_release_hold(upstream, latest, [report.effective?.version])
|
|
161
162
|
: undefined
|
|
162
163
|
if (hold !== undefined) report.hold = hold
|
|
163
164
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { git_gh_exec } from '#scripts/git/git-gh-exec'
|
|
2
2
|
import { error_text } from '#scripts/lib/error-message'
|
|
3
|
+
import { npm_registry } from './npm-registry'
|
|
3
4
|
import { safe_json_parse } from './parse-json'
|
|
4
5
|
import { release_age } from './release-age'
|
|
5
6
|
|
|
@@ -22,14 +23,18 @@ function require_endpoint(versions_endpoint: string | undefined, package_name: s
|
|
|
22
23
|
)
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
// Fetch the latest published version
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// `
|
|
29
|
-
//
|
|
30
|
-
//
|
|
26
|
+
// Fetch the latest published version: public npm's `latest` dist-tag first, which needs no
|
|
27
|
+
// credentials (joshuafolkken/kit#2882), then the GitHub Packages versions endpoint for a package
|
|
28
|
+
// public npm does not answer for. The endpoint is supplied per package (e.g.
|
|
29
|
+
// `/users/joshuafolkken/packages/npm/kit/versions?per_page=1`) so the same fetcher serves kit, jgame,
|
|
30
|
+
// and app-kit. Guards an undefined/empty endpoint and wraps `gh api` failures with an actionable
|
|
31
|
+
// message instead of a raw ExecaSyncError stack. The detail is the shared layer's failure text: gh's
|
|
32
|
+
// own stderr (e.g. "gh: Not Found (HTTP 404)") when it wrote one, execa's message otherwise, followed
|
|
33
|
+
// by any JSON error body gh wrote to stdout.
|
|
31
34
|
function fetch_latest_version(versions_endpoint: string | undefined, package_name: string): string {
|
|
32
35
|
const endpoint = require_endpoint(versions_endpoint, package_name)
|
|
36
|
+
const public_latest = npm_registry.read_latest(package_name)
|
|
37
|
+
if (public_latest !== undefined) return public_latest
|
|
33
38
|
|
|
34
39
|
try {
|
|
35
40
|
return git_gh_exec.exec_gh_api_sync({ path: endpoint, jq_filter: '.[0].name' }).trim()
|
|
@@ -59,19 +64,28 @@ function with_page_size(endpoint: string, page_size: number): string {
|
|
|
59
64
|
return `${path_part}?${query.toString()}`
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
function read_github_release_times(versions_endpoint: string): Record<string, string> | undefined {
|
|
68
|
+
const endpoint = with_page_size(versions_endpoint, TIMES_PAGE_SIZE)
|
|
69
|
+
const stdout = git_gh_exec.read_gh_api_sync({ path: endpoint, jq_filter: TIMES_JQ })
|
|
70
|
+
if (stdout === undefined) return undefined
|
|
71
|
+
const parsed = release_age.release_times_schema.safeParse(safe_json_parse(stdout))
|
|
72
|
+
|
|
73
|
+
return parsed.success ? parsed.data : undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
// Publish timestamps for the package's recent releases, or nothing. Unlike `fetch_latest_version`
|
|
63
77
|
// this never throws: the timestamps only enrich an explanation, so a package whose history cannot be
|
|
64
|
-
// read keeps the report it had before (joshuafolkken/kit#808).
|
|
78
|
+
// read keeps the report it had before (joshuafolkken/kit#808). Read in the same source order as
|
|
79
|
+
// the latest, so the hold is explained against the registry `latest` came from.
|
|
65
80
|
function fetch_release_times(
|
|
66
81
|
versions_endpoint: string | undefined,
|
|
82
|
+
package_name: string,
|
|
67
83
|
): Record<string, string> | undefined {
|
|
68
84
|
if (versions_endpoint === undefined || versions_endpoint.trim() === '') return undefined
|
|
69
|
-
const endpoint = with_page_size(versions_endpoint, TIMES_PAGE_SIZE)
|
|
70
|
-
const stdout = git_gh_exec.read_gh_api_sync({ path: endpoint, jq_filter: TIMES_JQ })
|
|
71
|
-
if (stdout === undefined) return undefined
|
|
72
|
-
const parsed = release_age.release_times_schema.safeParse(safe_json_parse(stdout))
|
|
73
85
|
|
|
74
|
-
return
|
|
86
|
+
return (
|
|
87
|
+
npm_registry.read_release_times(package_name) ?? read_github_release_times(versions_endpoint)
|
|
88
|
+
)
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
export { fetch_latest_version, fetch_release_times, with_page_size }
|