@joshuafolkken/kit 1.1387.0 → 1.1453.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/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -0
- package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
- package/.claude/skills/workflow-commands/backlogrun.md +3 -2
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
- package/.claude/skills/workflow-commands/halfrun.md +1 -1
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/kickoff.md +12 -13
- package/.claude/skills/workflow-commands/observation-filing.md +1 -1
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/.claude/skills/workflow-commands/prerequisite.md +6 -0
- package/.claude/skills/workflow-commands/retrospective.md +5 -5
- package/.claude/skills/workflow-commands/split-assessment.md +2 -2
- package/README.md +27 -4
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +261 -201
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +95 -19
- package/package.json +4 -3
- package/prompts/collaboration-workflow/issue-template.md +10 -8
- package/prompts/review.md +30 -4
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
- package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
- package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
- package/scripts/auto-ok/auto-ok-judge.ts +67 -0
- package/scripts/backlog/backlog-drive-cli.ts +14 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +38 -11
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +18 -4
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +13 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +7 -4
- package/scripts/init/init-ai-copy.ts +34 -11
- package/scripts/init/init-basic.ts +9 -4
- package/scripts/init/init-bootstrap.ts +6 -2
- package/scripts/init/init-dry-run-report.ts +150 -0
- package/scripts/init/init-dry-run.ts +189 -0
- package/scripts/init/init-install.ts +8 -1
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +62 -42
- package/scripts/init/kit-setup-state.ts +11 -17
- package/scripts/init/own-formatter.ts +21 -0
- package/scripts/init/project-profile.ts +60 -12
- package/scripts/init/start-dry-run.ts +70 -0
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +23 -3
- package/scripts/init/start-setup-pr.ts +13 -24
- package/scripts/init/start-steps.ts +62 -1
- package/scripts/init/start.ts +38 -17
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/init/working-tree.ts +116 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-auto-ok.ts +68 -99
- package/scripts/issue/issue-file-cli.ts +17 -17
- package/scripts/issue/issue-file.ts +1 -6
- package/scripts/issue/issue-labels.ts +46 -10
- package/scripts/issue/issue-release-cli.ts +69 -23
- package/scripts/issue/issue-release.ts +3 -1
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-ai.ts +11 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +10 -3
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +26 -2
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/josh/process-identity.ts +32 -6
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-broker-server.ts +69 -0
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/lane-session-broker.ts +363 -0
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/observations/observation-ledger-migrate.ts +3 -15
- package/scripts/release/release-issue-close.ts +133 -0
- package/scripts/release/release-publish.ts +12 -1
- package/scripts/repo/repository-labels.ts +11 -1
- package/scripts/review/comment-ranges.ts +199 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +24 -7
- package/scripts/review/review-code-tree.ts +119 -0
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/review/review-round2-cli.ts +16 -5
- package/scripts/review/review-round2.ts +30 -10
- package/scripts/review/review-stamps.ts +22 -1
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/lane/openai-review-broker.ts +0 -262
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
2
2
|
import { bounded_pool } from '#scripts/lib/bounded-pool'
|
|
3
3
|
import { epic_audit_logic } from './epic-audit'
|
|
4
|
-
import type
|
|
4
|
+
import { epic_bundle, type BacklogIssue } from './epic-bundle'
|
|
5
5
|
import { epic_issue, type EpicIssue } from './epic-issue'
|
|
6
6
|
|
|
7
7
|
// Candidates the open backlog cannot show, read from the subject's own prose.
|
|
@@ -64,15 +64,12 @@ interface ReferencedContext {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function to_backlog_issue(issue: EpicIssue, context: ReferencedContext): BacklogIssue {
|
|
67
|
-
const epic = context.epics.get(issue.number)
|
|
68
|
-
|
|
69
67
|
return {
|
|
70
68
|
number: issue.number,
|
|
71
69
|
repo: context.repo,
|
|
72
70
|
body: issue.body,
|
|
73
71
|
blocked_by: epic_issue.blocker_references_of(issue, context.repo),
|
|
74
|
-
|
|
75
|
-
...(epic !== undefined && { epic }),
|
|
72
|
+
...epic_bundle.membership_of(issue.number, epic_issue.label_names(issue), context),
|
|
76
73
|
}
|
|
77
74
|
}
|
|
78
75
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
2
|
+
import { has_label_name, RELEASE_LABEL } from '#scripts/issue/issue-labels'
|
|
2
3
|
import { epic_audit_logic } from './epic-audit'
|
|
3
4
|
import { epic_graph } from './epic-graph'
|
|
4
5
|
import type { DependencyLink } from './epic-parse'
|
|
@@ -31,6 +32,11 @@ interface BacklogIssue {
|
|
|
31
32
|
// its parent, so without this every child would find its own epic as a candidate and be told to
|
|
32
33
|
// bundle with it.
|
|
33
34
|
is_epic?: boolean
|
|
35
|
+
// Whether this issue is the release issue. `issue:file --release` records every change awaiting
|
|
36
|
+
// publication as its blocker, so the relation is mechanical and says nothing about the work
|
|
37
|
+
// belonging together — read as a signal, it proposed one epic for the release and every
|
|
38
|
+
// unrelated change waiting on it.
|
|
39
|
+
is_release?: boolean
|
|
34
40
|
// Repository-qualified, not bare numbers. Issue numbers are unique per
|
|
35
41
|
// repository, so a blocker read as `40` alone matched any candidate numbered 40 — including one in
|
|
36
42
|
// a different repository entirely. That produced a bundle nobody's data supported, and, through
|
|
@@ -79,6 +85,36 @@ function has_recorded_dependency(subject: BacklogIssue, other: BacklogIssue): bo
|
|
|
79
85
|
return names_as_blocker(subject, other) || names_as_blocker(other, subject)
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
// The epic view an issue is placed against: which epic tracks each issue, and the epics themselves.
|
|
89
|
+
interface MembershipContext {
|
|
90
|
+
epics: ReadonlyMap<number, number>
|
|
91
|
+
epic_numbers: ReadonlySet<number>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type Membership = Pick<BacklogIssue, 'epic' | 'is_epic' | 'is_release'>
|
|
95
|
+
|
|
96
|
+
// The fields that decide whether and where an issue takes part in a bundle. Single-sourced so the
|
|
97
|
+
// listing read and the per-reference read cannot disagree about which issue is an epic or the
|
|
98
|
+
// release one.
|
|
99
|
+
function membership_of(
|
|
100
|
+
number: number,
|
|
101
|
+
labels: ReadonlyArray<string>,
|
|
102
|
+
context: MembershipContext,
|
|
103
|
+
): Membership {
|
|
104
|
+
const epic = context.epics.get(number)
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
is_epic: context.epic_numbers.has(number),
|
|
108
|
+
is_release: has_label_name(labels, RELEASE_LABEL),
|
|
109
|
+
...(epic !== undefined && { epic }),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Neither an epic nor the release issue takes part in a bundle, on either side.
|
|
114
|
+
function is_excluded(issue: BacklogIssue): boolean {
|
|
115
|
+
return issue.is_epic === true || issue.is_release === true
|
|
116
|
+
}
|
|
117
|
+
|
|
82
118
|
// A strong signal, and only a strong signal.
|
|
83
119
|
//
|
|
84
120
|
// Similar titles are deliberately not one. "Related" expands without limit, and a threshold is what
|
|
@@ -87,9 +123,7 @@ function has_recorded_dependency(subject: BacklogIssue, other: BacklogIssue): bo
|
|
|
87
123
|
function is_strong_signal(subject: BacklogIssue, other: BacklogIssue): boolean {
|
|
88
124
|
// Neither side. Asked about an epic, the one-sided check found every one of its children through
|
|
89
125
|
// their own `parent: #N` line and proposed bundling a container with its contents.
|
|
90
|
-
if (subject.number === other.number || subject
|
|
91
|
-
return false
|
|
92
|
-
}
|
|
126
|
+
if (subject.number === other.number || is_excluded(subject) || is_excluded(other)) return false
|
|
93
127
|
|
|
94
128
|
return has_mutual_reference(subject, other) || has_recorded_dependency(subject, other)
|
|
95
129
|
}
|
|
@@ -295,10 +329,11 @@ const epic_bundle = {
|
|
|
295
329
|
already_tracked_reason,
|
|
296
330
|
has_recorded_dependency,
|
|
297
331
|
is_strong_signal,
|
|
332
|
+
membership_of,
|
|
298
333
|
decide_bundle,
|
|
299
334
|
bundle_dependency_links,
|
|
300
335
|
bundle_children,
|
|
301
336
|
}
|
|
302
337
|
|
|
303
|
-
export type { BacklogIssue, BundleAction, BundleDecision }
|
|
338
|
+
export type { BacklogIssue, BundleAction, BundleDecision, MembershipContext }
|
|
304
339
|
export { epic_bundle }
|
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
|
|
5
5
|
import { project_checks } from '#scripts/gate/project-checks'
|
|
6
|
+
import { NOTHING_TO_CHECK_MARKER } from '#scripts/test/skip-marker'
|
|
6
7
|
import type { NamespaceMember } from './namespace-members'
|
|
7
8
|
|
|
8
9
|
const CHECK = 'exports'
|
|
@@ -13,7 +14,7 @@ const FAIL_EXIT_CODE = 1
|
|
|
13
14
|
// **Not a skip notice**. `SKIP_MARKER` means "a check passed without running",
|
|
14
15
|
// and the gate withholds its green record on it — so a consumer, where this notice prints on every
|
|
15
16
|
// run, never got a green record. Out of scope by design is not unverified, so the marker stays out.
|
|
16
|
-
const CONSUMER_NOTICE = `josh ${CHECK}: checks only the kit repository itself —
|
|
17
|
+
const CONSUMER_NOTICE = `josh ${CHECK}: checks only the kit repository itself — ${NOTHING_TO_CHECK_MARKER} in a consumer project.`
|
|
17
18
|
|
|
18
19
|
function format_member(root: string, member: NamespaceMember): string {
|
|
19
20
|
return `${path.relative(root, member.file)}:${String(member.line)} ${member.namespace}.${member.member}`
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { SnapshotObserver } from '#scripts/gh/git-pr-checks'
|
|
2
|
+
import { is_coderabbit_check, is_coderabbit_rate_limited } from '#scripts/gh/git-pr-checks-eval'
|
|
3
|
+
import {
|
|
4
|
+
CHECK_STATUS_FAIL,
|
|
5
|
+
CHECK_STATUS_PASS,
|
|
6
|
+
CHECK_STATUS_PENDING,
|
|
7
|
+
type PrStateSnapshot,
|
|
8
|
+
type RollupCheck,
|
|
9
|
+
} from '#scripts/gh/git-pr-checks-parse'
|
|
10
|
+
import {
|
|
11
|
+
run_event_checks,
|
|
12
|
+
type CodeRabbitState,
|
|
13
|
+
type PrChecks,
|
|
14
|
+
} from '#scripts/run/event/run-event-checks'
|
|
15
|
+
import { run_event_stream } from '#scripts/run/event/run-event-stream'
|
|
16
|
+
import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
|
|
17
|
+
|
|
18
|
+
// What `followup`'s check wait tells the run's event stream (joshuafolkken/kit#3770): the CI counts and
|
|
19
|
+
// CodeRabbit's state, one `pr-checks` line each time they change, so `run:board` draws a child waiting
|
|
20
|
+
// in followup from the stream alone. CodeRabbit is counted apart — it has a state of its own and does
|
|
21
|
+
// not gate the merge.
|
|
22
|
+
|
|
23
|
+
const PR_NUMBER_PATTERN = /\/pull\/(?<number>\d+)/u
|
|
24
|
+
const { PR_CHECKS } = run_event_stream.EVENT_KIND
|
|
25
|
+
|
|
26
|
+
function count_of(checks: ReadonlyArray<RollupCheck>, status: string): number {
|
|
27
|
+
return checks.filter((check) => check.status === status).length
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function coderabbit_state_of(check: RollupCheck | undefined): CodeRabbitState {
|
|
31
|
+
if (check === undefined) return 'none'
|
|
32
|
+
if (check.status === CHECK_STATUS_PENDING) return 'running'
|
|
33
|
+
if (check.status !== CHECK_STATUS_PASS) return 'failed'
|
|
34
|
+
|
|
35
|
+
return is_coderabbit_rate_limited(check) ? 'rate-limited' : 'done'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function summary_of(snapshot: PrStateSnapshot, pr: number): PrChecks {
|
|
39
|
+
const ci = snapshot.rollup.filter((check) => !is_coderabbit_check(check.name))
|
|
40
|
+
const coderabbit = snapshot.rollup.find((check) => is_coderabbit_check(check.name))
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
pr,
|
|
44
|
+
pass: count_of(ci, CHECK_STATUS_PASS),
|
|
45
|
+
pending: count_of(ci, CHECK_STATUS_PENDING),
|
|
46
|
+
fail: count_of(ci, CHECK_STATUS_FAIL),
|
|
47
|
+
coderabbit: coderabbit_state_of(coderabbit),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pr_number_of(pr_url = ''): number | undefined {
|
|
52
|
+
const number = PR_NUMBER_PATTERN.exec(pr_url)?.groups?.['number']
|
|
53
|
+
|
|
54
|
+
return number === undefined ? undefined : Number(number)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function skip_snapshot(): Promise<void> {
|
|
58
|
+
// Nothing names the child or its pull request, so there is no row to report to.
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// An observer that writes a line only when it differs from the one this wait last wrote; one that
|
|
62
|
+
// does nothing where the issue or the pull request is unknown.
|
|
63
|
+
function observer_of(issue: string | undefined, pr_url: string | undefined): SnapshotObserver {
|
|
64
|
+
const pr = pr_number_of(pr_url)
|
|
65
|
+
|
|
66
|
+
if (issue === undefined || pr === undefined) return skip_snapshot
|
|
67
|
+
|
|
68
|
+
let last_text = ''
|
|
69
|
+
|
|
70
|
+
return async function report_snapshot(snapshot: PrStateSnapshot): Promise<void> {
|
|
71
|
+
const text = run_event_checks.text_of(issue, summary_of(snapshot, pr))
|
|
72
|
+
|
|
73
|
+
if (text === last_text) return
|
|
74
|
+
|
|
75
|
+
last_text = text
|
|
76
|
+
await run_event_stream_emit.emit(PR_CHECKS, text)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const followup_checks_event = { observer_of, summary_of }
|
|
81
|
+
|
|
82
|
+
export { followup_checks_event }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { git_next_issues } from '#scripts/issue/git-next-issues'
|
|
2
|
+
import { issue_watch } from '#scripts/issue/issue-watch'
|
|
2
3
|
import { review_attest } from '#scripts/review/review-attest'
|
|
3
4
|
import { review_stamps } from '#scripts/review/review-stamps'
|
|
4
5
|
import { run_hold } from '#scripts/run/hold/run-hold'
|
|
@@ -67,11 +68,24 @@ async function clear_review_target(should_merge: boolean): Promise<void> {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
// The
|
|
71
|
-
//
|
|
71
|
+
// The issue watch ends with the run it watched: a record left behind would make the next run's hook
|
|
72
|
+
// poll an issue that is no longer its own.
|
|
73
|
+
async function clear_issue_watch(should_merge: boolean): Promise<void> {
|
|
74
|
+
if (!should_merge) return
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await issue_watch.clear_here()
|
|
78
|
+
} catch {
|
|
79
|
+
/* the next `josh run:entry` replaces the record */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The review records and the issue watch are one step of the tail: all are cleared by a merged run
|
|
84
|
+
// and none is worth reporting on its own.
|
|
72
85
|
async function clear_review_records(should_merge: boolean): Promise<void> {
|
|
73
86
|
clear_round_one_snapshot(should_merge)
|
|
74
87
|
await clear_review_target(should_merge)
|
|
88
|
+
await clear_issue_watch(should_merge)
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
// The working-tree hold a typed entry point claims before it starts is released here, on the one seam every `fullrun` — and every child of an `epicrun` or a `queue` —
|
|
@@ -3,6 +3,7 @@ import { parseArgs } from 'node:util'
|
|
|
3
3
|
import { git_branch } from '#scripts/git/git-branch'
|
|
4
4
|
import { git_error } from '#scripts/git/git-error'
|
|
5
5
|
import { issue_number_shape } from '#scripts/issue/issue-number-shape'
|
|
6
|
+
import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
|
|
6
7
|
import { cli_body } from '#scripts/josh/cli-body'
|
|
7
8
|
import { josh_environment_file } from '#scripts/josh/josh-environment-file'
|
|
8
9
|
import { git_notify, type GitNotifyConfig } from '#scripts/notify/git-notify'
|
|
@@ -151,9 +152,11 @@ async function assert_review_attested(should_merge: boolean): Promise<void> {
|
|
|
151
152
|
|
|
152
153
|
const verdict = await review_attest.check_here()
|
|
153
154
|
|
|
154
|
-
if (is_merge_permitted(verdict.status))
|
|
155
|
+
if (!is_merge_permitted(verdict.status)) throw new Error(review_attest.refusal_message(verdict))
|
|
156
|
+
|
|
157
|
+
const refusal = await issue_watch_gate.review_refusal_here()
|
|
155
158
|
|
|
156
|
-
throw new Error(
|
|
159
|
+
if (refusal !== undefined) throw new Error(refusal)
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
// Recording a review round as a prose request leaves the recurrence ledger blind whenever it is
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
DEFAULT_STABLE_READS,
|
|
5
5
|
git_pr_checks,
|
|
6
6
|
WATCH_CONFIRMED_STABLE_READS,
|
|
7
|
+
type SnapshotObserver,
|
|
7
8
|
} from '#scripts/gh/git-pr-checks'
|
|
8
9
|
import { is_coderabbit_check } from '#scripts/gh/git-pr-checks-eval'
|
|
9
10
|
import { CHECK_STATUS_PASS, type PrStateSnapshot } from '#scripts/gh/git-pr-checks-parse'
|
|
11
|
+
import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
|
|
10
12
|
import { json_value } from '#scripts/lib/json-value'
|
|
11
13
|
|
|
12
14
|
// The check wait and the notes read off its snapshot — split out of `git-pr-followup.ts` when it
|
|
@@ -75,12 +77,15 @@ async function handle_watch_failure(branch_name: string, error: unknown): Promis
|
|
|
75
77
|
// reads the wait still needs. A timed-out watch (`timed_out: true`) never spanned the pending→settled
|
|
76
78
|
// window, so it must keep the full stable-read window rather than claim the confirmation. A rethrown
|
|
77
79
|
// failure — the empty-rollup case — never reaches a caller, so it needs no answer here.
|
|
78
|
-
async function watch_before_polling(
|
|
80
|
+
async function watch_before_polling(
|
|
81
|
+
branch_name: string,
|
|
82
|
+
observe: SnapshotObserver | undefined,
|
|
83
|
+
): Promise<boolean> {
|
|
79
84
|
console.info('')
|
|
80
85
|
console.info('📊 Watching PR checks...')
|
|
81
86
|
|
|
82
87
|
try {
|
|
83
|
-
const result = await git_gh_command.pr_checks_watch(branch_name)
|
|
88
|
+
const result = await git_gh_command.pr_checks_watch(branch_name, observe)
|
|
84
89
|
|
|
85
90
|
return !result.timed_out
|
|
86
91
|
} catch (error) {
|
|
@@ -95,14 +100,23 @@ async function watch_before_polling(branch_name: string): Promise<boolean> {
|
|
|
95
100
|
// once rather than twice — which removes the extra interval `followup` spent re-proving a settled run.
|
|
96
101
|
// A skipped or fallen-through watch keeps `DEFAULT_STABLE_READS`, and a check that turned red is still
|
|
97
102
|
// caught: the poll evaluates the full merge gate and throws on failure before any stable read counts.
|
|
103
|
+
// `observe` is shown each poll's snapshot, the watch's included — the run's `pr-checks` line
|
|
104
|
+
// (joshuafolkken/kit#3770).
|
|
98
105
|
async function run_checks(input: {
|
|
99
106
|
branch_name: string
|
|
100
107
|
is_skip_watch: boolean
|
|
108
|
+
observe?: SnapshotObserver
|
|
101
109
|
}): Promise<PrStateSnapshot> {
|
|
102
|
-
const is_watch_confirmed =
|
|
110
|
+
const is_watch_confirmed =
|
|
111
|
+
!input.is_skip_watch && (await watch_before_polling(input.branch_name, input.observe))
|
|
103
112
|
const stable_reads = is_watch_confirmed ? WATCH_CONFIRMED_STABLE_READS : DEFAULT_STABLE_READS
|
|
104
113
|
|
|
105
|
-
return await git_pr_checks.wait_for_pr_success(
|
|
114
|
+
return await git_pr_checks.wait_for_pr_success(
|
|
115
|
+
input.branch_name,
|
|
116
|
+
stable_reads,
|
|
117
|
+
issue_watch_gate.assert_read_here,
|
|
118
|
+
input.observe,
|
|
119
|
+
)
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
// Temporary: record every CodeRabbit check that was not passing when the merge gate
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { epic_close } from '#scripts/epic/epic-close'
|
|
2
2
|
import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
3
|
+
import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
|
|
3
4
|
import { git_notify, type GitNotifyConfig } from '#scripts/notify/git-notify'
|
|
4
5
|
import { git_followup_cleanup } from './git-followup-cleanup'
|
|
5
6
|
import { git_followup_issue_close } from './git-followup-issue-close'
|
|
@@ -232,8 +233,12 @@ async function run_tail_steps(input: WrapupInput): Promise<void> {
|
|
|
232
233
|
])
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
// The last look at the issue before the merge: the CI poll checked it on every iteration, but a
|
|
237
|
+
// person's update can land between that poll and this step, and only the seconds after this read are
|
|
238
|
+
// left unwatched.
|
|
235
239
|
async function run_wrapup(input: WrapupInput, log: StageLog): Promise<void> {
|
|
236
240
|
if (input.should_merge && input.is_merged !== true) {
|
|
241
|
+
await issue_watch_gate.assert_read_here()
|
|
237
242
|
await git_gh_command.pr_merge(input.branch_name)
|
|
238
243
|
lap(log, STAGE.merge)
|
|
239
244
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type TelegramSendInput,
|
|
11
11
|
type TelegramTaskType,
|
|
12
12
|
} from '#scripts/notify/telegram-notify'
|
|
13
|
+
import { followup_checks_event } from './followup-checks-event'
|
|
13
14
|
import { git_followup_merged } from './git-followup-merged'
|
|
14
15
|
import { git_followup_pending } from './git-followup-pending'
|
|
15
16
|
import { git_followup_stages, type StageLog } from './git-followup-stages'
|
|
@@ -229,6 +230,7 @@ async function run_review_checks(
|
|
|
229
230
|
input: FollowupInput,
|
|
230
231
|
context: TelegramContext,
|
|
231
232
|
log: StageLog,
|
|
233
|
+
issue_number: string | undefined,
|
|
232
234
|
): Promise<{ notes: Array<string>; managed: Array<string> }> {
|
|
233
235
|
if (input.is_merged === true) return { notes: [], managed: [] }
|
|
234
236
|
|
|
@@ -238,6 +240,7 @@ async function run_review_checks(
|
|
|
238
240
|
const snapshot = await run_checks({
|
|
239
241
|
branch_name: input.branch_name,
|
|
240
242
|
is_skip_watch: input.is_skip_watch,
|
|
243
|
+
observe: followup_checks_event.observer_of(issue_number, context.pr_url),
|
|
241
244
|
})
|
|
242
245
|
|
|
243
246
|
lap(log, STAGE.checks_wait)
|
|
@@ -278,7 +281,7 @@ async function run_stages(input: FollowupInput, log: StageLog): Promise<string |
|
|
|
278
281
|
const { issue_number, closes_number, context } = await read_run_context(input)
|
|
279
282
|
|
|
280
283
|
lap(log, STAGE.closes_and_context)
|
|
281
|
-
const checks = await run_review_checks(input, context, log)
|
|
284
|
+
const checks = await run_review_checks(input, context, log, issue_number)
|
|
282
285
|
const is_completion_recorded = await notify_unless_recorded(input, issue_number, context, checks)
|
|
283
286
|
|
|
284
287
|
lap(log, STAGE.telegram)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { parseArgs } from 'node:util'
|
|
6
|
+
import { claude_token } from '#scripts/auth/claude-token'
|
|
7
|
+
import { gh_cli_token } from '#scripts/gh/gh-cli-token'
|
|
8
|
+
import { git_gh_exec } from '#scripts/gh/git-gh-exec'
|
|
9
|
+
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
10
|
+
import { self_sync_guard } from '#scripts/self-sync-guard/self-sync-guard-logic'
|
|
11
|
+
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
12
|
+
import { fresh_machine_docker } from './fresh-machine-docker'
|
|
13
|
+
import { fresh_machine_log } from './fresh-machine-log'
|
|
14
|
+
import { fresh_machine_patterns } from './fresh-machine-patterns'
|
|
15
|
+
import { fresh_machine_run, type RunRequest } from './fresh-machine-run'
|
|
16
|
+
import { fresh_machine_spec, type Scenario } from './fresh-machine-spec'
|
|
17
|
+
|
|
18
|
+
// `josh fresh:run <pattern>` / `josh fresh:run --spec <file>` — run one scenario on a machine that
|
|
19
|
+
// has nothing installed, and keep what happened as a log.
|
|
20
|
+
|
|
21
|
+
const ARGV_OFFSET = 2
|
|
22
|
+
const FAILURE_EXIT_CODE = 1
|
|
23
|
+
const USAGE =
|
|
24
|
+
'Usage: josh fresh:run <pattern> | --spec <file.json> [--package-version <version>] [--log-dir <dir>] [--rebuild] | --list'
|
|
25
|
+
// The scratch directory kit's distributed `.gitignore` already ignores, so a log is never committed
|
|
26
|
+
// from kit or from a consumer toolkit.
|
|
27
|
+
const DEFAULT_LOG_DIRECTORY = '.claude/tmp/fresh-machine'
|
|
28
|
+
const NO_GITHUB_TOKEN =
|
|
29
|
+
'This scenario uses GitHub and `gh auth token` printed no token. Run `gh auth login` on this machine first.'
|
|
30
|
+
const NO_CLAUDE_TOKEN = `This scenario uses Claude Code and neither the macOS keychain nor ${claude_token.VARIABLE} holds a token. ${claude_token.SETUP_HINT}`
|
|
31
|
+
// The patterns install kit by name, while the tarball and the published version are those of the
|
|
32
|
+
// checkout the command is run from — in a toolkit's checkout the two would be different packages.
|
|
33
|
+
const NOT_A_KIT_CHECKOUT = `The named patterns install ${KIT_PACKAGE_NAME} and run from its checkout only. Write this package's scenario as a JSON file and pass it with --spec.`
|
|
34
|
+
const OWNED_REPOSITORIES = 'user/repos?affiliation=owner&per_page=100'
|
|
35
|
+
const REPOSITORY_NAMES = '.[].name'
|
|
36
|
+
|
|
37
|
+
// The option names are the CLI's, so the type is read off the parser rather than restated.
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
39
|
+
function parse(argv: ReadonlyArray<string>) {
|
|
40
|
+
return parseArgs({
|
|
41
|
+
args: [...argv],
|
|
42
|
+
allowPositionals: true,
|
|
43
|
+
options: {
|
|
44
|
+
spec: { type: 'string' },
|
|
45
|
+
'package-version': { type: 'string' },
|
|
46
|
+
'log-dir': { type: 'string' },
|
|
47
|
+
rebuild: { type: 'boolean' },
|
|
48
|
+
list: { type: 'boolean' },
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type CliValues = ReturnType<typeof parse>['values']
|
|
54
|
+
|
|
55
|
+
function pattern_of(name: string): Scenario | undefined {
|
|
56
|
+
const scenario = fresh_machine_patterns.find(name)
|
|
57
|
+
|
|
58
|
+
if (scenario === undefined) return undefined
|
|
59
|
+
|
|
60
|
+
const is_kit_checkout = self_sync_guard.read_package_name(PROJECT_ROOT) === KIT_PACKAGE_NAME
|
|
61
|
+
|
|
62
|
+
if (!is_kit_checkout) throw new Error(NOT_A_KIT_CHECKOUT)
|
|
63
|
+
|
|
64
|
+
return scenario
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scenario_of(values: CliValues, pattern: string | undefined): Scenario | undefined {
|
|
68
|
+
if (values.spec !== undefined) {
|
|
69
|
+
return fresh_machine_spec.parse(readFileSync(path.resolve(PROJECT_ROOT, values.spec), 'utf8'))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return pattern === undefined ? undefined : pattern_of(pattern)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A GitHub run's name is also the repository it creates under the account of the token, and the
|
|
76
|
+
// logs of this checkout are not the only place such a run was started from.
|
|
77
|
+
function repositories_of(scenario: Scenario): Array<string> {
|
|
78
|
+
if (!scenario.requires_github) return []
|
|
79
|
+
|
|
80
|
+
const names = git_gh_exec.exec_gh_api_sync({
|
|
81
|
+
path: OWNED_REPOSITORIES,
|
|
82
|
+
should_paginate: true,
|
|
83
|
+
jq_filter: REPOSITORY_NAMES,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return names.split('\n').filter((name) => name !== '')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function request_of(
|
|
90
|
+
scenario: Scenario,
|
|
91
|
+
values: CliValues,
|
|
92
|
+
secrets: Record<string, string>,
|
|
93
|
+
): RunRequest {
|
|
94
|
+
const log_directory = path.resolve(PROJECT_ROOT, values['log-dir'] ?? DEFAULT_LOG_DIRECTORY)
|
|
95
|
+
|
|
96
|
+
mkdirSync(log_directory, { recursive: true })
|
|
97
|
+
|
|
98
|
+
const run_name = fresh_machine_log.next_name(scenario.name, {
|
|
99
|
+
logs: readdirSync(log_directory),
|
|
100
|
+
repositories: repositories_of(scenario),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
scenario,
|
|
105
|
+
run_name,
|
|
106
|
+
log_path: path.join(log_directory, fresh_machine_log.file_name(run_name)),
|
|
107
|
+
package_version: values['package-version'],
|
|
108
|
+
secrets,
|
|
109
|
+
is_rebuild: values.rebuild === true,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function refuse(message: string): number {
|
|
114
|
+
console.error(message)
|
|
115
|
+
|
|
116
|
+
return FAILURE_EXIT_CODE
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// One secret a scenario asks for: where its value is read on the host, and what to say when there is
|
|
120
|
+
// none. Every one is read before Docker starts, so a missing token stops the run with nothing built.
|
|
121
|
+
interface Secret {
|
|
122
|
+
variable: string
|
|
123
|
+
is_required: (scenario: Scenario) => boolean
|
|
124
|
+
read: () => string | undefined
|
|
125
|
+
missing: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const SECRETS: ReadonlyArray<Secret> = [
|
|
129
|
+
{
|
|
130
|
+
variable: fresh_machine_docker.TOKEN_VARIABLE,
|
|
131
|
+
is_required: (scenario) => scenario.requires_github,
|
|
132
|
+
read: gh_cli_token.get,
|
|
133
|
+
missing: NO_GITHUB_TOKEN,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
variable: claude_token.VARIABLE,
|
|
137
|
+
is_required: (scenario) => scenario.requires_claude,
|
|
138
|
+
read: claude_token.read,
|
|
139
|
+
missing: NO_CLAUDE_TOKEN,
|
|
140
|
+
},
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
// The values found, by variable name, and the message of the first secret that has none.
|
|
144
|
+
interface Secrets {
|
|
145
|
+
values: Record<string, string>
|
|
146
|
+
missing: string | undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function secrets_of(scenario: Scenario): Secrets {
|
|
150
|
+
const required = SECRETS.filter((secret) => secret.is_required(scenario))
|
|
151
|
+
const read = required.map((secret) => ({ secret, value: secret.read() }))
|
|
152
|
+
const found = read.flatMap(({ secret, value }) =>
|
|
153
|
+
value === undefined ? [] : [[secret.variable, value] as const],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
values: Object.fromEntries(found),
|
|
158
|
+
missing: read.find(({ value }) => value === undefined)?.secret.missing,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function run_scenario(scenario: Scenario, values: CliValues): Promise<number> {
|
|
163
|
+
const secrets = secrets_of(scenario)
|
|
164
|
+
|
|
165
|
+
if (secrets.missing !== undefined) return refuse(secrets.missing)
|
|
166
|
+
|
|
167
|
+
const request = request_of(scenario, values, secrets.values)
|
|
168
|
+
const exit_code = await fresh_machine_run.run(request)
|
|
169
|
+
|
|
170
|
+
console.info(`Log: ${request.log_path}`)
|
|
171
|
+
|
|
172
|
+
return exit_code
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function run(argv: ReadonlyArray<string>): Promise<number> {
|
|
176
|
+
const { values, positionals } = parse(argv)
|
|
177
|
+
const patterns = fresh_machine_patterns.names().join('\n')
|
|
178
|
+
|
|
179
|
+
if (values.list === true) {
|
|
180
|
+
console.info(patterns)
|
|
181
|
+
|
|
182
|
+
return 0
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const scenario = scenario_of(values, positionals[0])
|
|
186
|
+
|
|
187
|
+
if (scenario === undefined) return refuse(`${USAGE}\n\nPatterns:\n${patterns}`)
|
|
188
|
+
|
|
189
|
+
return await run_scenario(scenario, values)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function main(argv: ReadonlyArray<string>): Promise<void> {
|
|
193
|
+
try {
|
|
194
|
+
process.exitCode = await run(argv)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
process.exitCode = refuse(error instanceof Error ? error.message : String(error))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const fresh_machine_cli = { run }
|
|
201
|
+
|
|
202
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
|
|
203
|
+
|
|
204
|
+
export { fresh_machine_cli }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fresh_machine_script } from './fresh-machine-script'
|
|
2
|
+
|
|
3
|
+
// The two docker invocations of `josh fresh:run`, as argument lists. A token is passed by name only
|
|
4
|
+
// (`--env GH_TOKEN`): docker reads its value from the environment of the client, so it is in no
|
|
5
|
+
// argument, no image layer and no printed command.
|
|
6
|
+
|
|
7
|
+
const TOKEN_VARIABLE = 'GH_TOKEN'
|
|
8
|
+
const ENV_FLAG = '--env'
|
|
9
|
+
const VOLUME_FLAG = '--volume'
|
|
10
|
+
|
|
11
|
+
interface ContainerRun {
|
|
12
|
+
image: string
|
|
13
|
+
run_name: string
|
|
14
|
+
script: string
|
|
15
|
+
seed_directory: string
|
|
16
|
+
pack_directory: string
|
|
17
|
+
// The packed checkout's file name inside `pack_directory`, for a tarball scenario.
|
|
18
|
+
tarball: string | undefined
|
|
19
|
+
package_version: string | undefined
|
|
20
|
+
// The variables whose values the client's environment carries into the container.
|
|
21
|
+
secret_names: ReadonlyArray<string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The Dockerfile arrives on stdin (`-`), so the build has no context to copy a host file from.
|
|
25
|
+
function build_arguments(image: string, is_rebuild: boolean): Array<string> {
|
|
26
|
+
const cache = is_rebuild ? ['--no-cache'] : []
|
|
27
|
+
|
|
28
|
+
return ['build', '--progress', 'plain', ...cache, '--tag', image, '-']
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function variables(container: ContainerRun): Array<string> {
|
|
32
|
+
const { tarball } = container
|
|
33
|
+
const tarball_path = tarball === undefined ? '' : `${fresh_machine_script.PACK_MOUNT}/${tarball}`
|
|
34
|
+
const assignments = [
|
|
35
|
+
`RUN_NAME=${container.run_name}`,
|
|
36
|
+
`PACKAGE_TARBALL=${tarball_path}`,
|
|
37
|
+
`PACKAGE_VERSION=${container.package_version ?? ''}`,
|
|
38
|
+
...container.secret_names,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
return assignments.flatMap((assignment) => [ENV_FLAG, assignment])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// `bash -i` is the new terminal the prerequisites guide asks for: it reads the profile the pnpm
|
|
45
|
+
// installer edited, which a non-interactive shell on Debian returns from before reaching.
|
|
46
|
+
function run_arguments(container: ContainerRun): Array<string> {
|
|
47
|
+
return [
|
|
48
|
+
'run',
|
|
49
|
+
'--rm',
|
|
50
|
+
...variables(container),
|
|
51
|
+
VOLUME_FLAG,
|
|
52
|
+
`${container.seed_directory}:${fresh_machine_script.SEED_MOUNT}:ro`,
|
|
53
|
+
VOLUME_FLAG,
|
|
54
|
+
`${container.pack_directory}:${fresh_machine_script.PACK_MOUNT}:ro`,
|
|
55
|
+
container.image,
|
|
56
|
+
'bash',
|
|
57
|
+
'-ic',
|
|
58
|
+
container.script,
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fresh_machine_docker = { build_arguments, run_arguments, TOKEN_VARIABLE }
|
|
63
|
+
|
|
64
|
+
export { fresh_machine_docker }
|
|
65
|
+
export type { ContainerRun }
|