@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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The "fresh machine" of `josh fresh:run`: a Debian image with curl and without Node.js, pnpm, git or
|
|
2
|
+
// gh, on which a normal user runs the Linux route of `docs/setup/prerequisites.md` and nothing else.
|
|
3
|
+
// The Dockerfile is built here rather than kept as a file because `docs/` is not in the published
|
|
4
|
+
// package and a consumer toolkit builds the same machine; `fresh-machine-image.test.ts` holds the
|
|
5
|
+
// commands below to the guide's own lines.
|
|
6
|
+
|
|
7
|
+
const BASE_IMAGE = 'buildpack-deps:bookworm-curl'
|
|
8
|
+
const MACHINE_USER = 'dev'
|
|
9
|
+
const HOME_DIRECTORY = `/home/${MACHINE_USER}`
|
|
10
|
+
const TAG_PREFIX = 'josh-fresh-machine'
|
|
11
|
+
|
|
12
|
+
// `docs/setup/prerequisites.md` → "Linux and macOS". The first line runs in the shell a new machine
|
|
13
|
+
// starts with; the guide then says to open a new terminal, which is what `NEW_TERMINAL` does.
|
|
14
|
+
const INSTALL_PNPM = 'curl -fsSL https://get.pnpm.io/install.sh | sh -'
|
|
15
|
+
const NEW_TERMINAL_COMMANDS = ['pnpm runtime set node 22 -g', 'node --version']
|
|
16
|
+
|
|
17
|
+
// The guide sends a Linux reader to the gh installation page; these are that page's Debian steps,
|
|
18
|
+
// run as root in place of `sudo`, with the curl the image already has. git comes from the same
|
|
19
|
+
// package manager, and so does procps: kit's lane commands read processes with its ps and pgrep, which
|
|
20
|
+
// the slim base image does not ship. Only a GitHub scenario's machine has any of them.
|
|
21
|
+
const KEYRING = '/etc/apt/keyrings/githubcli-archive-keyring.gpg'
|
|
22
|
+
const APT_UPDATE = 'apt-get update'
|
|
23
|
+
const GITHUB_TOOLS = [
|
|
24
|
+
APT_UPDATE,
|
|
25
|
+
'apt-get install -y git procps',
|
|
26
|
+
'mkdir -p -m 755 /etc/apt/keyrings',
|
|
27
|
+
`curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o ${KEYRING}`,
|
|
28
|
+
`chmod go+r ${KEYRING}`,
|
|
29
|
+
`echo "deb [arch=$(dpkg --print-architecture) signed-by=${KEYRING}] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list`,
|
|
30
|
+
APT_UPDATE,
|
|
31
|
+
'apt-get install -y gh',
|
|
32
|
+
].join(' && ')
|
|
33
|
+
|
|
34
|
+
function new_terminal(command: string): string {
|
|
35
|
+
return `RUN bash -ic '${command}'`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// `SHELL` is the one line the guide does not have: the pnpm installer reads it to pick the profile
|
|
39
|
+
// it edits, and a container build does not set it.
|
|
40
|
+
function dockerfile(requires_github: boolean): string {
|
|
41
|
+
const github_tools = requires_github ? [`RUN ${GITHUB_TOOLS}`] : []
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
`FROM ${BASE_IMAGE}`,
|
|
45
|
+
...github_tools,
|
|
46
|
+
`RUN useradd --create-home --shell /bin/bash ${MACHINE_USER}`,
|
|
47
|
+
`USER ${MACHINE_USER}`,
|
|
48
|
+
`WORKDIR ${HOME_DIRECTORY}`,
|
|
49
|
+
'ENV SHELL=/bin/bash',
|
|
50
|
+
`RUN ${INSTALL_PNPM}`,
|
|
51
|
+
...NEW_TERMINAL_COMMANDS.map((command) => new_terminal(command)),
|
|
52
|
+
'',
|
|
53
|
+
].join('\n')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tag(requires_github: boolean): string {
|
|
57
|
+
return `${TAG_PREFIX}:${requires_github ? 'github' : 'base'}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fresh_machine_image = { dockerfile, tag }
|
|
61
|
+
|
|
62
|
+
export { fresh_machine_image }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The two rules a `josh fresh:run` log follows: a run never reuses a name, and no token passed to
|
|
2
|
+
// the container reaches the file.
|
|
3
|
+
|
|
4
|
+
const LOG_EXTENSION = '.log'
|
|
5
|
+
const FIRST_RUN = 1
|
|
6
|
+
|
|
7
|
+
// What already carries a run's name: the logs of the directory this run writes to and, for a GitHub
|
|
8
|
+
// scenario, the account's repositories — another checkout, or a lane worktree since removed, numbers
|
|
9
|
+
// from its own logs and leaves repositories this directory has no log of.
|
|
10
|
+
interface TakenNames {
|
|
11
|
+
logs: ReadonlyArray<string>
|
|
12
|
+
repositories: ReadonlyArray<string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function logged_names(logs: ReadonlyArray<string>): Array<string> {
|
|
16
|
+
return logs
|
|
17
|
+
.filter((file) => file.endsWith(LOG_EXTENSION))
|
|
18
|
+
.map((file) => file.slice(0, -LOG_EXTENSION.length))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function run_number(scenario_name: string, run_name: string): number | undefined {
|
|
22
|
+
const prefix = `${scenario_name}-`
|
|
23
|
+
|
|
24
|
+
if (!run_name.startsWith(prefix)) return undefined
|
|
25
|
+
|
|
26
|
+
const digits = run_name.slice(prefix.length)
|
|
27
|
+
|
|
28
|
+
return /^\d+$/u.test(digits) ? Number(digits) : undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The name is the project directory and, for a GitHub scenario, the repository `josh start` creates.
|
|
32
|
+
// It is numbered past every earlier log and repository, so a rerun leaves the last run's repository
|
|
33
|
+
// in place.
|
|
34
|
+
function next_name(scenario_name: string, taken_names: TakenNames): string {
|
|
35
|
+
const taken = [...logged_names(taken_names.logs), ...taken_names.repositories]
|
|
36
|
+
.map((run_name) => run_number(scenario_name, run_name))
|
|
37
|
+
.filter((number) => number !== undefined)
|
|
38
|
+
const next = taken.length === 0 ? FIRST_RUN : Math.max(...taken) + 1
|
|
39
|
+
|
|
40
|
+
return `${scenario_name}-${String(next)}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function file_name(run_name: string): string {
|
|
44
|
+
return `${run_name}${LOG_EXTENSION}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function redact(line: string, secrets: ReadonlyArray<string>): string {
|
|
48
|
+
const present = secrets.filter((secret) => secret !== '')
|
|
49
|
+
let redacted = line
|
|
50
|
+
|
|
51
|
+
for (const secret of present) {
|
|
52
|
+
redacted = redacted.replaceAll(secret, '[REDACTED]')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return redacted
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const fresh_machine_log = { next_name, file_name, redact }
|
|
59
|
+
|
|
60
|
+
export { fresh_machine_log }
|
|
61
|
+
export type { TakenNames }
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
2
|
+
import { fresh_machine_spec, type Scenario } from './fresh-machine-spec'
|
|
3
|
+
|
|
4
|
+
// kit's own six patterns: three projects (one Lua file, one `index.html`, a minimal TypeScript
|
|
5
|
+
// SvelteKit app from `sv`) by two commands (`josh init`, `josh start`), each from the starting state
|
|
6
|
+
// joshuafolkken/kit#2848 gave it. `josh init` installs the packed checkout, so a fix is retested
|
|
7
|
+
// without a release; `josh start` installs a published version, because its setup pull request
|
|
8
|
+
// would carry a tarball path CI cannot resolve.
|
|
9
|
+
|
|
10
|
+
const KIT = KIT_PACKAGE_NAME
|
|
11
|
+
const ALLOW_BUILDS = '--allow-build=esbuild --allow-build=unrs-resolver'
|
|
12
|
+
// What `pnpm dlx @joshuafolkken/kit init` writes before it installs kit.
|
|
13
|
+
const BARE_MANIFEST = String.raw`printf '{ "private": true }\n' > package.json`
|
|
14
|
+
const CREATE_SVELTEKIT =
|
|
15
|
+
'pnpm dlx sv create --template minimal --types ts --no-add-ons --install pnpm .'
|
|
16
|
+
// The `sv` that created the project is the one `pnpm dlx` resolved, so its version is recorded.
|
|
17
|
+
const SV_VERSION = 'pnpm dlx sv --version'
|
|
18
|
+
// The app builds and type-checks before kit is installed and still does after it.
|
|
19
|
+
const SVELTEKIT_CHECKS = 'pnpm build && pnpm check'
|
|
20
|
+
const INSTALL_TARBALL = `pnpm add -D ${ALLOW_BUILDS} "$PACKAGE_TARBALL"`
|
|
21
|
+
// A version published minutes ago is inside pnpm's release-age wait; kit alone is excluded from it
|
|
22
|
+
// (joshuafolkken/kit#2869), so every other dependency keeps the wait.
|
|
23
|
+
const EXCLUDE_RELEASE_AGE = String.raw`printf 'minimumReleaseAgeExclude:\n - "${KIT}"\n' >> pnpm-workspace.yaml`
|
|
24
|
+
const INSTALL_PUBLISHED = `pnpm add -D ${ALLOW_BUILDS} ${KIT}@"$PACKAGE_VERSION"`
|
|
25
|
+
const GATE = 'pnpm josh gate'
|
|
26
|
+
const INIT_COMMANDS = [INSTALL_TARBALL, 'pnpm exec josh init', GATE]
|
|
27
|
+
// `josh start` leaves the project on its setup branch, where the gate asks for the scoped checks
|
|
28
|
+
// first; this is the line its refusal hands back.
|
|
29
|
+
const SCOPED_CHECKS = 'pnpm josh lint:related && pnpm josh test:related'
|
|
30
|
+
const START = 'pnpm exec josh start --yes --github'
|
|
31
|
+
|
|
32
|
+
// An agent names the profile rather than leaving it to detection, so every pattern passes its own.
|
|
33
|
+
function start_commands(profile: string): Array<string> {
|
|
34
|
+
return [
|
|
35
|
+
EXCLUDE_RELEASE_AGE,
|
|
36
|
+
INSTALL_PUBLISHED,
|
|
37
|
+
`${START} --profile ${profile}`,
|
|
38
|
+
SCOPED_CHECKS,
|
|
39
|
+
GATE,
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `gh auth login` cannot run without a person, and it is what points git at gh for credentials; with
|
|
44
|
+
// the token in the environment this is the same step.
|
|
45
|
+
const SIGN_IN = 'gh auth setup-git'
|
|
46
|
+
// What the prerequisites guide has a person run once per machine. Only the pattern with no repository
|
|
47
|
+
// uses it: `josh start` makes the first commit there, and stops before writing without it (#3754).
|
|
48
|
+
const MACHINE_IDENTITY =
|
|
49
|
+
'git config --global user.name kit-test && git config --global user.email kit-test@example.com'
|
|
50
|
+
// The identity is the repository's own, so the machine stays one nobody has configured git on.
|
|
51
|
+
const FIRST_COMMIT = [
|
|
52
|
+
'git init --initial-branch main',
|
|
53
|
+
'git config user.name kit-test && git config user.email kit-test@example.com',
|
|
54
|
+
'git add --all && git commit --message "Initial commit"',
|
|
55
|
+
]
|
|
56
|
+
const CREATE_REPOSITORY = 'gh repo create "$RUN_NAME" --private --source . --push'
|
|
57
|
+
// The labels a repository already had, recorded before `josh start` so the probe after it shows none
|
|
58
|
+
// was removed or changed.
|
|
59
|
+
const LABELS = 'gh label list --limit 100'
|
|
60
|
+
// The setup pull request's CI, waited for and recorded; the pull request itself is never merged.
|
|
61
|
+
const SETUP_CI = 'gh pr checks --watch --interval 60'
|
|
62
|
+
|
|
63
|
+
// The two states #3712 was found in: a shell that reads no rc file, and a PATH with `node` on it and
|
|
64
|
+
// no `pnpm`.
|
|
65
|
+
const BARE_SHELL =
|
|
66
|
+
'env -i HOME="$HOME" PATH=/usr/local/bin:/usr/bin:/bin bash --noprofile --norc -c'
|
|
67
|
+
const NODE_ONLY = '"$HOME/node-only"'
|
|
68
|
+
const PATH_PROBES = [
|
|
69
|
+
`${BARE_SHELL} '${GATE}'`,
|
|
70
|
+
`mkdir -p ${NODE_ONLY} && ln -sf "$(command -v node)" ${NODE_ONLY}/node && ${BARE_SHELL} 'PATH="$HOME/node-only:$PATH" ./node_modules/.bin/josh gate'`,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
// What a `josh start` run is checked against before the PATH states: the profile it recorded, the
|
|
74
|
+
// repository's visibility and branch, its labels, and the files the first commit on `main` holds. Where
|
|
75
|
+
// `main` already had a commit, `josh start` opens a setup Issue and pull request instead of pushing to
|
|
76
|
+
// it; the last four record those, the files the pull request carries and the `main` GitHub holds.
|
|
77
|
+
const PROFILE = 'pnpm josh profile'
|
|
78
|
+
const START_PROBES = [
|
|
79
|
+
PROFILE,
|
|
80
|
+
'gh repo view --json visibility,defaultBranchRef',
|
|
81
|
+
LABELS,
|
|
82
|
+
'git show --name-only --format= "$(git rev-list --max-parents=0 main)"',
|
|
83
|
+
'gh issue list --state all --json number,title,state,labels',
|
|
84
|
+
'gh pr list --state all --json number,title,state,headRefName,labels,closingIssuesReferences',
|
|
85
|
+
'gh pr diff --name-only',
|
|
86
|
+
'git log --oneline origin/main',
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
// What a `josh init` run is checked against after the gate: the profile it chose, the files it left
|
|
90
|
+
// (a `./.git` line is a repository it should not have made), and a second run that changes nothing.
|
|
91
|
+
const PROJECT_FILES =
|
|
92
|
+
'find . -name node_modules -prune -o -name .git -prune -o -type f -print | sort'
|
|
93
|
+
const CHECKSUMS = `${PROJECT_FILES} | xargs sha256sum`
|
|
94
|
+
const INIT_PROBES = [
|
|
95
|
+
PROFILE,
|
|
96
|
+
'find . -name node_modules -prune -o -print | sort',
|
|
97
|
+
`${CHECKSUMS} > "$HOME/before-second-init.sha" && pnpm exec josh init && ${CHECKSUMS} | diff "$HOME/before-second-init.sha" -`,
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
const LUA_FILE = 'main.lua'
|
|
101
|
+
const HTML_FILE = 'index.html'
|
|
102
|
+
const LUA_FILES = { [LUA_FILE]: 'print("Hello")\n' }
|
|
103
|
+
const HTML_FILES = { [HTML_FILE]: '<!doctype html><html><body><h1>Hello</h1></body></html>\n' }
|
|
104
|
+
|
|
105
|
+
function init_pattern(
|
|
106
|
+
project: string,
|
|
107
|
+
files: Record<string, string>,
|
|
108
|
+
setup: ReadonlyArray<string>,
|
|
109
|
+
project_probes: ReadonlyArray<string> = [],
|
|
110
|
+
): Scenario {
|
|
111
|
+
return fresh_machine_spec.define({
|
|
112
|
+
name: `kit-test-${project}-init`,
|
|
113
|
+
files,
|
|
114
|
+
commands: [...setup, ...INIT_COMMANDS],
|
|
115
|
+
probes: [...INIT_PROBES, ...project_probes, ...PATH_PROBES],
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// What a `josh start` pattern adds to the shared steps: the state before the install, the profile it
|
|
120
|
+
// names, the commands that must pass after the gate, and the probes recorded before the PATH states.
|
|
121
|
+
interface StartProject {
|
|
122
|
+
setup: ReadonlyArray<string>
|
|
123
|
+
profile: string
|
|
124
|
+
checks?: ReadonlyArray<string>
|
|
125
|
+
probes?: ReadonlyArray<string>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function start_pattern(
|
|
129
|
+
project: string,
|
|
130
|
+
files: Record<string, string>,
|
|
131
|
+
{ setup, profile, checks = [], probes = [] }: StartProject,
|
|
132
|
+
): Scenario {
|
|
133
|
+
return fresh_machine_spec.define({
|
|
134
|
+
name: `kit-test-${project}-start`,
|
|
135
|
+
requires_github: true,
|
|
136
|
+
package_source: 'published',
|
|
137
|
+
files,
|
|
138
|
+
commands: [SIGN_IN, ...setup, ...start_commands(profile), ...checks],
|
|
139
|
+
probes: [...START_PROBES, ...probes, ...PATH_PROBES],
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const PATTERNS: ReadonlyArray<Scenario> = [
|
|
144
|
+
init_pattern('lua', LUA_FILES, [BARE_MANIFEST]),
|
|
145
|
+
// No Git: `josh start` creates the repository, the first commit and the GitHub repository.
|
|
146
|
+
start_pattern('lua', LUA_FILES, { setup: [MACHINE_IDENTITY, BARE_MANIFEST], profile: 'basic' }),
|
|
147
|
+
init_pattern('html', HTML_FILES, [BARE_MANIFEST]),
|
|
148
|
+
// Git with a first commit on `main`, no GitHub repository.
|
|
149
|
+
start_pattern('html', HTML_FILES, { setup: [...FIRST_COMMIT, BARE_MANIFEST], profile: 'basic' }),
|
|
150
|
+
init_pattern('svelte', {}, [CREATE_SVELTEKIT, SV_VERSION, SVELTEKIT_CHECKS], [SVELTEKIT_CHECKS]),
|
|
151
|
+
// A GitHub repository with `main` already pushed, its labels recorded before the install; the app
|
|
152
|
+
// still builds and type-checks on the setup branch, and the setup pull request's CI is waited for.
|
|
153
|
+
start_pattern(
|
|
154
|
+
'svelte',
|
|
155
|
+
{},
|
|
156
|
+
{
|
|
157
|
+
setup: [CREATE_SVELTEKIT, SV_VERSION, ...FIRST_COMMIT, CREATE_REPOSITORY, LABELS],
|
|
158
|
+
profile: 'full',
|
|
159
|
+
checks: [SVELTEKIT_CHECKS],
|
|
160
|
+
probes: [SETUP_CI],
|
|
161
|
+
},
|
|
162
|
+
),
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
function names(): Array<string> {
|
|
166
|
+
return PATTERNS.map((pattern) => pattern.name)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function find(name: string): Scenario | undefined {
|
|
170
|
+
return PATTERNS.find((pattern) => pattern.name === name)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const fresh_machine_patterns = { names, find }
|
|
174
|
+
|
|
175
|
+
export { fresh_machine_patterns }
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
chmodSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs'
|
|
11
|
+
import os from 'node:os'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
14
|
+
import { execa, type Options } from 'execa'
|
|
15
|
+
import { z } from 'zod'
|
|
16
|
+
import { fresh_machine_docker, type ContainerRun } from './fresh-machine-docker'
|
|
17
|
+
import { fresh_machine_image } from './fresh-machine-image'
|
|
18
|
+
import { fresh_machine_log } from './fresh-machine-log'
|
|
19
|
+
import { fresh_machine_script } from './fresh-machine-script'
|
|
20
|
+
import type { Scenario } from './fresh-machine-spec'
|
|
21
|
+
|
|
22
|
+
// The I/O of `josh fresh:run`: build the machine, stage what the container may read, run the
|
|
23
|
+
// scenario, and put every line of all three into one log.
|
|
24
|
+
|
|
25
|
+
const DOCKER = 'docker'
|
|
26
|
+
const PNPM = 'pnpm'
|
|
27
|
+
const FAILURE_EXIT_CODE = 1
|
|
28
|
+
const STAGE_PREFIX = 'josh-fresh-machine-'
|
|
29
|
+
// The container's user is not the host's, so what it reads has to be readable by anyone — whatever
|
|
30
|
+
// the host's umask gave the seed files and the tarball `pnpm pack` wrote.
|
|
31
|
+
const SHARED_DIRECTORY_MODE = 0o755
|
|
32
|
+
const SHARED_FILE_MODE = 0o644
|
|
33
|
+
const TARBALL_EXTENSION = '.tgz'
|
|
34
|
+
const MANIFEST_SCHEMA = z.object({ name: z.string() })
|
|
35
|
+
|
|
36
|
+
interface RunRequest {
|
|
37
|
+
scenario: Scenario
|
|
38
|
+
run_name: string
|
|
39
|
+
log_path: string
|
|
40
|
+
package_version: string | undefined
|
|
41
|
+
// Variable name → value, each passed into the container by name and replaced in every line.
|
|
42
|
+
secrets: Readonly<Record<string, string>>
|
|
43
|
+
is_rebuild: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Stage {
|
|
47
|
+
root: string
|
|
48
|
+
seed_directory: string
|
|
49
|
+
pack_directory: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Write = (line: string) => void
|
|
53
|
+
|
|
54
|
+
function logger(request: RunRequest): Write {
|
|
55
|
+
const secrets = Object.values(request.secrets)
|
|
56
|
+
|
|
57
|
+
return function write(line: string): void {
|
|
58
|
+
const safe = fresh_machine_log.redact(line, secrets)
|
|
59
|
+
|
|
60
|
+
console.info(safe)
|
|
61
|
+
appendFileSync(request.log_path, `${safe}\n`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// One host command, its output streamed line by line into the log. A command that could not start
|
|
66
|
+
// (no docker on the machine) has no exit code, and its reason is the only line there is to log.
|
|
67
|
+
async function stream(
|
|
68
|
+
file: string,
|
|
69
|
+
arguments_: Array<string>,
|
|
70
|
+
options: Options,
|
|
71
|
+
write: Write,
|
|
72
|
+
): Promise<number> {
|
|
73
|
+
const subprocess = execa(file, arguments_, { ...options, all: true, reject: false })
|
|
74
|
+
const lines = subprocess.iterable({ from: 'all' })
|
|
75
|
+
|
|
76
|
+
for await (const line of lines) write(String(line))
|
|
77
|
+
|
|
78
|
+
const result = await subprocess
|
|
79
|
+
|
|
80
|
+
if (result.exitCode === undefined) write(result.shortMessage ?? `${file} did not start`)
|
|
81
|
+
|
|
82
|
+
return result.exitCode ?? FAILURE_EXIT_CODE
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function staged_directory(root: string, name: string): string {
|
|
86
|
+
const directory = path.join(root, name)
|
|
87
|
+
|
|
88
|
+
mkdirSync(directory)
|
|
89
|
+
|
|
90
|
+
return directory
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stage(scenario: Scenario): Stage {
|
|
94
|
+
const root = mkdtempSync(path.join(os.tmpdir(), STAGE_PREFIX))
|
|
95
|
+
const seed_directory = staged_directory(root, 'seed')
|
|
96
|
+
|
|
97
|
+
for (const [file, content] of Object.entries(scenario.files)) {
|
|
98
|
+
const target = path.join(seed_directory, file)
|
|
99
|
+
|
|
100
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
101
|
+
writeFileSync(target, content)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { root, seed_directory, pack_directory: staged_directory(root, 'pack') }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Run once everything is staged, so the tarball is covered as well as the seed. The root itself stays
|
|
108
|
+
// as private as `mkdtemp` made it: a bind mount never enters a source through its parent, and on a
|
|
109
|
+
// shared host an open root would let every other user read the tarball for the length of the run.
|
|
110
|
+
function share(root: string): void {
|
|
111
|
+
const entries = readdirSync(root, { withFileTypes: true, recursive: true })
|
|
112
|
+
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const mode = entry.isDirectory() ? SHARED_DIRECTORY_MODE : SHARED_FILE_MODE
|
|
115
|
+
|
|
116
|
+
chmodSync(path.join(entry.parentPath, entry.name), mode)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// `pnpm pack` runs the checkout's own `prepack`, so the tarball holds what a release of this tree
|
|
121
|
+
// would.
|
|
122
|
+
async function pack(pack_directory: string, write: Write): Promise<string> {
|
|
123
|
+
const pack_arguments = ['pack', '--pack-destination', pack_directory]
|
|
124
|
+
|
|
125
|
+
write(`$ ${PNPM} ${pack_arguments.join(' ')}`)
|
|
126
|
+
await stream(PNPM, pack_arguments, { cwd: PROJECT_ROOT }, write)
|
|
127
|
+
|
|
128
|
+
const tarball = readdirSync(pack_directory).find((file) => file.endsWith(TARBALL_EXTENSION))
|
|
129
|
+
|
|
130
|
+
if (tarball === undefined) throw new Error('pnpm pack produced no tarball')
|
|
131
|
+
|
|
132
|
+
return tarball
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function package_name(): string {
|
|
136
|
+
const manifest = readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8')
|
|
137
|
+
|
|
138
|
+
return MANIFEST_SCHEMA.parse(JSON.parse(manifest)).name
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The published version is always written out in the log and the install command, so a run can be
|
|
142
|
+
// repeated against the same release; without `--package-version` it is the registry's latest.
|
|
143
|
+
async function published_version(requested: string | undefined): Promise<string> {
|
|
144
|
+
if (requested !== undefined) return requested
|
|
145
|
+
|
|
146
|
+
const { stdout } = await execa(PNPM, ['view', package_name(), 'version'], { cwd: PROJECT_ROOT })
|
|
147
|
+
|
|
148
|
+
return stdout.trim()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function container_run(
|
|
152
|
+
request: RunRequest,
|
|
153
|
+
staged: Stage,
|
|
154
|
+
write: Write,
|
|
155
|
+
): Promise<ContainerRun> {
|
|
156
|
+
const { scenario } = request
|
|
157
|
+
const is_tarball = scenario.package_source === 'tarball'
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
image: fresh_machine_image.tag(scenario.requires_github),
|
|
161
|
+
run_name: request.run_name,
|
|
162
|
+
script: fresh_machine_script.build(scenario),
|
|
163
|
+
seed_directory: staged.seed_directory,
|
|
164
|
+
pack_directory: staged.pack_directory,
|
|
165
|
+
tarball: is_tarball ? await pack(staged.pack_directory, write) : undefined,
|
|
166
|
+
package_version: is_tarball ? undefined : await published_version(request.package_version),
|
|
167
|
+
secret_names: Object.keys(request.secrets),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function build_image(request: RunRequest, write: Write): Promise<number> {
|
|
172
|
+
const { scenario } = request
|
|
173
|
+
const image = fresh_machine_image.tag(scenario.requires_github)
|
|
174
|
+
const build_arguments = fresh_machine_docker.build_arguments(image, request.is_rebuild)
|
|
175
|
+
const input = fresh_machine_image.dockerfile(scenario.requires_github)
|
|
176
|
+
|
|
177
|
+
write(`$ ${DOCKER} ${build_arguments.join(' ')}`)
|
|
178
|
+
|
|
179
|
+
return await stream(DOCKER, build_arguments, { input }, write)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function run_container(request: RunRequest, staged: Stage, write: Write): Promise<number> {
|
|
183
|
+
const container = await container_run(request, staged, write)
|
|
184
|
+
|
|
185
|
+
share(staged.root)
|
|
186
|
+
write(`$ ${DOCKER} run ${container.image} — ${request.run_name}`)
|
|
187
|
+
write(`# package: ${container.tarball ?? container.package_version ?? ''}`)
|
|
188
|
+
|
|
189
|
+
return await stream(
|
|
190
|
+
DOCKER,
|
|
191
|
+
fresh_machine_docker.run_arguments(container),
|
|
192
|
+
{ env: request.secrets },
|
|
193
|
+
write,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function run(request: RunRequest): Promise<number> {
|
|
198
|
+
const write = logger(request)
|
|
199
|
+
const build_code = await build_image(request, write)
|
|
200
|
+
|
|
201
|
+
if (build_code !== 0) return build_code
|
|
202
|
+
|
|
203
|
+
const staged = stage(request.scenario)
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
return await run_container(request, staged, write)
|
|
207
|
+
} finally {
|
|
208
|
+
rmSync(staged.root, { recursive: true, force: true })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const fresh_machine_run = { run }
|
|
213
|
+
|
|
214
|
+
export { fresh_machine_run }
|
|
215
|
+
export type { RunRequest }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Scenario } from './fresh-machine-spec'
|
|
2
|
+
|
|
3
|
+
// The script one container runs. Every command is printed before it runs and its exit status after,
|
|
4
|
+
// so the log reads as a terminal session; the first failing command ends the run, and the probes
|
|
5
|
+
// that follow a clean run are recorded whatever they exit with.
|
|
6
|
+
|
|
7
|
+
const SEED_MOUNT = '/seed'
|
|
8
|
+
const PACK_MOUNT = '/pack'
|
|
9
|
+
const COPY_SEED = `cp -R ${SEED_MOUNT}/. .`
|
|
10
|
+
const PROBES_HEADING = '# probes — recorded, never failing the run'
|
|
11
|
+
const ESCAPED_QUOTE = String.raw`'\''`
|
|
12
|
+
const PREAMBLE = [
|
|
13
|
+
String.raw`step() { printf '\n$ %s\n' "$1"; eval "$1"; local code=$?; printf '[exit %s]\n' "$code"; return "$code"; }`,
|
|
14
|
+
'mkdir -p "$HOME/$RUN_NAME" && cd "$HOME/$RUN_NAME" || exit 1',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function quote(command: string): string {
|
|
18
|
+
const escaped = command.split("'").join(ESCAPED_QUOTE)
|
|
19
|
+
|
|
20
|
+
return `'${escaped}'`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function required(command: string): string {
|
|
24
|
+
return `step ${quote(command)} || exit $?`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function probe_lines(probes: ReadonlyArray<string>): Array<string> {
|
|
28
|
+
if (probes.length === 0) return []
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
String.raw`printf '\n%s\n' ${quote(PROBES_HEADING)}`,
|
|
32
|
+
...probes.map((probe) => `step ${quote(probe)}`),
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function build(scenario: Scenario): string {
|
|
37
|
+
const seed = Object.keys(scenario.files).length > 0 ? [COPY_SEED] : []
|
|
38
|
+
const commands = [...seed, ...scenario.commands].map((command) => required(command))
|
|
39
|
+
|
|
40
|
+
return [...PREAMBLE, ...commands, ...probe_lines(scenario.probes), 'exit 0', ''].join('\n')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fresh_machine_script = { build, SEED_MOUNT, PACK_MOUNT }
|
|
44
|
+
|
|
45
|
+
export { fresh_machine_script }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
// A scenario is everything one `josh fresh:run` needs to know: the files that are in the project
|
|
5
|
+
// before anything is installed, the commands a person would type, and the probes recorded after
|
|
6
|
+
// them. kit's own six patterns and a consumer toolkit's spec file are the same shape, so the machine
|
|
7
|
+
// and the log are shared and nothing about them is kit's alone (joshuafolkken/kit#3714).
|
|
8
|
+
//
|
|
9
|
+
// Inside the container a command reads `$RUN_NAME` (the numbered project and repository name),
|
|
10
|
+
// `$PACKAGE_TARBALL` (the packed checkout, for `package_source: tarball`) and `$PACKAGE_VERSION`
|
|
11
|
+
// (the published version, for `package_source: published`).
|
|
12
|
+
|
|
13
|
+
function is_project_relative(file: string): boolean {
|
|
14
|
+
const normalized = path.posix.normalize(file)
|
|
15
|
+
|
|
16
|
+
return !path.posix.isAbsolute(normalized) && !normalized.startsWith('..')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const COMMAND_SCHEMA = z.string().min(1)
|
|
20
|
+
const SCENARIO_SCHEMA = z.strictObject({
|
|
21
|
+
name: z.string().regex(/^[a-z0-9][a-z0-9-]*$/u),
|
|
22
|
+
// A GitHub scenario runs on the image that also has git and gh, and receives the host's token.
|
|
23
|
+
requires_github: z.boolean().default(false),
|
|
24
|
+
// A Claude Code scenario receives the host's long-lived token as `CLAUDE_CODE_OAUTH_TOKEN`.
|
|
25
|
+
requires_claude: z.boolean().default(false),
|
|
26
|
+
package_source: z.enum(['tarball', 'published']).default('tarball'),
|
|
27
|
+
files: z.record(z.string().refine(is_project_relative), z.string()).default({}),
|
|
28
|
+
// A failing command ends the run; a failing probe is only recorded.
|
|
29
|
+
commands: z.array(COMMAND_SCHEMA).min(1),
|
|
30
|
+
probes: z.array(COMMAND_SCHEMA).default([]),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
type Scenario = z.infer<typeof SCENARIO_SCHEMA>
|
|
34
|
+
type ScenarioInput = z.input<typeof SCENARIO_SCHEMA>
|
|
35
|
+
|
|
36
|
+
function define(input: ScenarioInput): Scenario {
|
|
37
|
+
return SCENARIO_SCHEMA.parse(input)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function problems(error: z.ZodError): string {
|
|
41
|
+
return error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A spec file is written by hand in another repository, so a mistake is reported as the fields that
|
|
45
|
+
// are wrong rather than as a stack trace.
|
|
46
|
+
function parse(json: string): Scenario {
|
|
47
|
+
const result = SCENARIO_SCHEMA.safeParse(JSON.parse(json))
|
|
48
|
+
|
|
49
|
+
if (!result.success) throw new Error(`Invalid scenario spec — ${problems(result.error)}`)
|
|
50
|
+
|
|
51
|
+
return result.data
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fresh_machine_spec = { define, parse }
|
|
55
|
+
|
|
56
|
+
export { fresh_machine_spec }
|
|
57
|
+
export type { Scenario, ScenarioInput }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kit-test-backlogrun-python",
|
|
3
|
+
"requires_github": true,
|
|
4
|
+
"package_source": "tarball",
|
|
5
|
+
"requires_claude": true,
|
|
6
|
+
"files": {
|
|
7
|
+
"pyproject.toml": "[project]\nname = \"greeting\"\nversion = \"0.1.0\"\nrequires-python = \">=3.9\"\n",
|
|
8
|
+
"main.py": "def main() -> None:\n print(\"Hello\")\n\n\nif __name__ == \"__main__\":\n main()\n"
|
|
9
|
+
},
|
|
10
|
+
"commands": [
|
|
11
|
+
"gh auth setup-git",
|
|
12
|
+
"git config --global user.name kit-test && git config --global user.email kit-test@example.com",
|
|
13
|
+
"printf '{ \"private\": true }\\n' > package.json",
|
|
14
|
+
"cp \"$PACKAGE_TARBALL\" . && pnpm add -D --allow-build=esbuild --allow-build=unrs-resolver \"./$(basename \"$PACKAGE_TARBALL\")\"",
|
|
15
|
+
"pnpm exec josh start --yes --github",
|
|
16
|
+
"pnpm josh profile | grep -x 'profile: basic (package.json josh.profile)'",
|
|
17
|
+
"grep -q '\"workflow\": \"github\"' package.json && test -f .claude/settings.json",
|
|
18
|
+
"curl -fsSL https://claude.ai/install.sh | bash && export PATH=\"$HOME/.local/bin:$PATH\" && claude --version",
|
|
19
|
+
"gh issue create --title 'Print a greeting with a name in main.py' --body 'Make main.py print \"Hello, Josh\" instead of \"Hello\".' --label auto-ok --label run:lane",
|
|
20
|
+
"timeout 60 pnpm josh backlogrun > \"$HOME/backlogrun.out\" 2>&1; cat \"$HOME/backlogrun.out\"; grep -q '^backlogrun: started' \"$HOME/backlogrun.out\"",
|
|
21
|
+
"pnpm josh run:event --from 0 | grep 'starting session'",
|
|
22
|
+
"for i in $(seq 1 90); do pnpm josh run:event --from 0 | grep -q 'dispatching' && break; sleep 10; done; pnpm josh run:event --from 0 | grep 'dispatching'",
|
|
23
|
+
"for i in $(seq 1 120); do test \"$(gh pr list --state all --json number --jq length)\" -gt 0 && break; sleep 10; done; pnpm josh run:board --chat",
|
|
24
|
+
"pnpm josh backlogrun --stop"
|
|
25
|
+
],
|
|
26
|
+
"probes": [
|
|
27
|
+
"tail -n 40 \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\" | cut -c1-400",
|
|
28
|
+
"grep -c 'requires approval' \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\"",
|
|
29
|
+
"grep -c 'run:carry --begin' \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\"",
|
|
30
|
+
"tail -n 40 /tmp/josh-lane-dispatch-*.log | cut -c1-400",
|
|
31
|
+
"cat /tmp/josh-lane-dispatch-*.log | grep -c 'requires approval'",
|
|
32
|
+
"gh pr list --state all"
|
|
33
|
+
]
|
|
34
|
+
}
|