rvlib-shipkit 0.38.0 → 1.0.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/CHANGELOG.md +371 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +216 -18
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
package/src/core/run-ci.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// behind cmd_Ci (logic stays out of command
|
|
4
|
-
// gate in-process. Returns the exit code; json
|
|
5
|
-
// Every run refreshes STATUS.md
|
|
6
|
-
// status:false, and the fix pass plus
|
|
7
|
-
// read-only unless frozen is set and
|
|
1
|
+
// THE GATE — `shipkit ci`: the mechanical fix pass, then every check the gate
|
|
2
|
+
// asks for through the one runner (core/check-runner.ts, core/run-check.ts holds
|
|
3
|
+
// the shared body). Plain module behind cmd_Ci (logic stays out of command
|
|
4
|
+
// classes) so tests can run the gate in-process. Returns the exit code; json
|
|
5
|
+
// prints the structured summary. Every run refreshes STATUS.md
|
|
6
|
+
// (write-status.ts — atomic + lock) unless status:false, and the fix pass plus
|
|
7
|
+
// some checks WRITE, so a run is not read-only unless frozen is set and
|
|
8
|
+
// --no-fix is passed.
|
|
8
9
|
import { join } from 'path'
|
|
9
|
-
import {
|
|
10
|
+
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
10
11
|
import { reportToMotherhome } from './motherhome.ts'
|
|
11
|
-
import {
|
|
12
|
+
import { runCheckPass, type CheckOpts } from './run-check.ts'
|
|
13
|
+
import { poolLines } from './slots.ts'
|
|
12
14
|
import { runFixPass } from './run-fixers.ts'
|
|
13
|
-
import { formatLine } from './wire.ts'
|
|
14
|
-
import { runTasks, selectTasks, type TaskResult } from './task-runner.ts'
|
|
15
|
+
import { formatLine, type Line } from './wire.ts'
|
|
15
16
|
import { writeStatus } from './write-status.ts'
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
17
|
+
import { allTasks, composeChecks, envTier, FULL_TESTS_ENV, gateInventory, type TestTier } from '../ledger/gate-checks.ts'
|
|
18
|
+
import { CHECK_TABLE_FILE, writeCheckTable } from '../ledger/check-table.ts'
|
|
19
|
+
import { Ledger } from '../ledger/ledger.ts'
|
|
20
|
+
import { RunLog, type InvokedBy } from '../ledger/run-log.ts'
|
|
21
|
+
import { gateBinary } from '../ledger/toolchain-identity.ts'
|
|
22
|
+
import type { CheckResult } from './check-runner.ts'
|
|
20
23
|
|
|
21
24
|
export type CiOpts = {
|
|
22
25
|
bail?: boolean
|
|
@@ -33,76 +36,81 @@ export type CiOpts = {
|
|
|
33
36
|
fixRules?: boolean
|
|
34
37
|
/** self-arm the git hooks in the fix pass (interactive ci entry only) */
|
|
35
38
|
armHooks?: boolean
|
|
36
|
-
/** run only the checks that can block (core/run-
|
|
39
|
+
/** run only the checks that can block (core/run-checks onlyBlocking) */
|
|
37
40
|
onlyBlockingChecks?: boolean
|
|
38
|
-
/**
|
|
39
|
-
* that gate the whole repo, since the
|
|
41
|
+
/** check ids the caller does not need. A sub-unit release skips the steps
|
|
42
|
+
* that gate the whole repo, since the check's own gate already ran. */
|
|
40
43
|
skipTaskIds?: string[]
|
|
41
|
-
/** report-only for self-maintaining
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
44
|
+
/** report-only for self-maintaining checks (SHIPKIT_FROZEN travels to
|
|
45
|
+
* children). The release path passes it: a check that rewrites
|
|
46
|
+
* package.json between the gate and `npm publish` packs a tree that
|
|
47
|
+
* exists in no commit. */
|
|
45
48
|
frozen?: boolean
|
|
46
|
-
/** treat a NON-blocking
|
|
47
|
-
*
|
|
49
|
+
/** treat a NON-blocking failure as fatal too. `release.requireHealthy` is
|
|
50
|
+
* the only caller: a repo that wants a missing cred to stop a release
|
|
48
51
|
* asks for it, and every other run keeps the declared severity. */
|
|
49
52
|
failOnAdvisory?: boolean
|
|
50
|
-
/**
|
|
51
|
-
* panel, and running
|
|
52
|
-
|
|
53
|
+
/** the BUILD steps alone. The TUI owns this: it renders the rest in its
|
|
54
|
+
* own panel, and running them twice per refresh proves the same thing
|
|
55
|
+
* twice. */
|
|
56
|
+
buildOnly?: boolean
|
|
53
57
|
/** line sink — commands pass their context stream; defaults to console */
|
|
54
58
|
print?: (line: string) => void
|
|
55
|
-
/** per-
|
|
56
|
-
|
|
59
|
+
/** per-CHECK completion tap, handed straight to the runner. The hub passes
|
|
60
|
+
* it, so one gate run colours every row as it lands. */
|
|
61
|
+
onCheck?: CheckOpts['onCheck']
|
|
57
62
|
/** the already-resolved config. A caller that holds one MUST pass it: two
|
|
58
63
|
* loads of repo.config.ts can disagree, and the release path classifies
|
|
59
|
-
*
|
|
64
|
+
* severity from the config it holds. */
|
|
60
65
|
config?: RepoConfig
|
|
61
|
-
/**
|
|
62
|
-
* run
|
|
66
|
+
/** run everything: no answer comes from the cache, and what this
|
|
67
|
+
* run finds is stored fresh. Travels to children as SHIPKIT_FRESH. */
|
|
63
68
|
fresh?: boolean
|
|
69
|
+
/** the gate binary this run is keyed on (default: the one running it).
|
|
70
|
+
* Toolchain identity content-hashes the directory the entry lives in, so
|
|
71
|
+
* a caller whose answers must survive an edit to that tree hands in a
|
|
72
|
+
* stable one instead of betting on nobody writing to it mid-run. */
|
|
73
|
+
cliEntry?: string
|
|
64
74
|
/** read the ledger at all. The release path passes false, unconditionally
|
|
65
75
|
* and not configurably: every remaining hole in the key is then capped at
|
|
66
76
|
* "an agent saw a stale local green", never "a stale green authorised a
|
|
67
77
|
* publish" (ledger/ledger.ts rule 4). */
|
|
68
78
|
ledgerRead?: boolean
|
|
69
79
|
/** which test tier the run asks the repo's suite for. 'quick' is the
|
|
70
|
-
* working default; 'full' sets SHIPKIT_TEST_FULL=1 for every
|
|
71
|
-
* a suite reads to include the slow
|
|
72
|
-
*
|
|
73
|
-
* partial suite is a green nobody measured.
|
|
74
|
-
* `SHIPKIT_TEST_FULL=1 shipkit ci` works with no flag. */
|
|
80
|
+
* working default; 'full' sets SHIPKIT_TEST_FULL=1 for every build step,
|
|
81
|
+
* which a suite reads to include the slow tiers it otherwise skips. The
|
|
82
|
+
* release path always asks for 'full': a version published behind a
|
|
83
|
+
* partial suite is a green nobody measured. */
|
|
75
84
|
tests?: TestTier
|
|
85
|
+
/** who started this run, as the run record files it */
|
|
86
|
+
invokedBy?: InvokedBy
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
export type { TestTier } from '../ledger/gate-
|
|
79
|
-
export { FULL_TESTS_ENV } from '../ledger/gate-
|
|
89
|
+
export type { TestTier } from '../ledger/gate-checks.ts'
|
|
90
|
+
export { FULL_TESTS_ENV } from '../ledger/gate-checks.ts'
|
|
80
91
|
|
|
81
|
-
/** what STATUS.md may claim about this run. `--with` ADDS a
|
|
82
|
-
* the full gate; `--only`, `--skip` and
|
|
92
|
+
/** what STATUS.md may claim about this run. `--with` ADDS a check, so it stays
|
|
93
|
+
* the full gate; `--only`, `--skip` and buildOnly each remove coverage, and
|
|
83
94
|
* a narrowed run that stamps the full gate's verdict is a green nobody
|
|
84
95
|
* measured. The label names the invocation either way. */
|
|
85
96
|
function ciScope(o: CiOpts): { command: string; fullGate: boolean } {
|
|
86
97
|
const narrowing = [
|
|
87
98
|
...(o.onlyIds ?? []).map((i) => `--only ${i}`),
|
|
88
99
|
...(o.skipGroups ?? []).map((g) => `--skip ${g}`),
|
|
89
|
-
...(o.
|
|
100
|
+
...(o.buildOnly === true ? ['(build steps only)'] : []),
|
|
90
101
|
]
|
|
91
|
-
|
|
102
|
+
// the tier is part of what ran: a full tier gate runs every slow suite, one
|
|
103
|
+
// at a time, and a label without it reads as an everyday run
|
|
104
|
+
const tier = (o.tests ?? envTier()) === 'full' ? ['--full'] : []
|
|
105
|
+
const parts = [...narrowing, ...(o.withIds ?? []).map((i) => `--with ${i}`), ...tier]
|
|
92
106
|
return { command: parts.length === 0 ? 'shipkit ci' : `shipkit ci ${parts.join(' ')}`, fullGate: narrowing.length === 0 }
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
96
|
-
// ONE gate run: the `contracts` task evaluates, rule SK002 reads what it
|
|
97
|
-
// computed instead of evaluating a second time (contracts-once.ts)
|
|
98
|
-
return await withContractsScope(() => runCiInScope(root, o))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function runCiInScope(root: string, o: CiOpts = {}): Promise<number> {
|
|
102
110
|
const print = o.print ?? ((l: string) => console.log(l))
|
|
103
111
|
const collected: string[] = []
|
|
104
112
|
const sink = (l: string): void => { collected.push(l); print(l) }
|
|
105
|
-
|
|
113
|
+
let c = o.config ?? (await loadRepoConfig(root))
|
|
106
114
|
// mechanical failures are not information, so they never cost a round trip
|
|
107
115
|
let fixFailed = false
|
|
108
116
|
// a NARROWED run repairs only the mechanical slots. The structural rule
|
|
@@ -110,185 +118,142 @@ async function runCiInScope(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
110
118
|
// that is the right price for the full gate and the wrong one for
|
|
111
119
|
// `bun run test`, which routes here and must answer the one question its
|
|
112
120
|
// name promises without leaving files in the agent's diff that it never
|
|
113
|
-
// wrote. Under CI it is worse than surprising
|
|
121
|
+
// wrote. Under CI it is worse than surprising: a formatting rewrite comes
|
|
114
122
|
// back as a test failure.
|
|
115
123
|
const narrowed = (o.onlyIds ?? []).length > 0
|
|
116
|
-
if (o.fix !== false)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
if (o.fix !== false) {
|
|
125
|
+
const pass = await runFixPass(root, c, { rules: !narrowed && o.fixRules !== false, armHooks: !narrowed && o.armHooks === true, print: sink })
|
|
126
|
+
fixFailed = pass.failed
|
|
127
|
+
// a fixer that rewrote the config or moved a check file leaves the
|
|
128
|
+
// config above describing a repo that is gone, so the gate reads the
|
|
129
|
+
// one on disk. A config that no longer loads is the gate's answer
|
|
130
|
+
if (pass.reshaped) {
|
|
131
|
+
try { c = await loadRepoConfig(root, { fresh: true }) } catch (e) {
|
|
132
|
+
sink(`FAIL: repo.config.ts no longer loads after the fix pass — ${String(e).slice(0, 200)}`)
|
|
133
|
+
return 1
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} else sink('skip: fix pass (--no-fix)')
|
|
122
137
|
const tier: TestTier = o.tests ?? envTier()
|
|
123
|
-
const tasks = all.tasks
|
|
124
|
-
.filter((t) => !skipIds.has(t.id))
|
|
125
|
-
// the whole run carries the flag rather than one task id: a repo names
|
|
126
|
-
// its suites what it likes (`test`, `test:libs`, a prepublish script),
|
|
127
|
-
// and a gate that guesses which one is the tests will miss one
|
|
128
|
-
.map((t) => (tier === 'full' ? { ...t, env: { ...t.env, [FULL_TESTS_ENV]: '1' } } : t))
|
|
129
138
|
// announced BEFORE the run, so a log that died mid-way still says which
|
|
130
139
|
// tier it was asking for
|
|
131
140
|
sink(tier === 'full'
|
|
132
141
|
? `ok: test tier FULL — every suite, the slow ones included (${FULL_TESTS_ENV}=1)`
|
|
133
142
|
: `skip: full test tier (quick run — \`shipkit ci --full\` or ${FULL_TESTS_ENV}=1 includes it)`)
|
|
134
|
-
for (const
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
for (const l of poolLines()) sink(l)
|
|
144
|
+
// a floor slot nothing could fill is VISIBLE — a thin gate must never look
|
|
145
|
+
// like a full one (the fresh-repo floor gap)
|
|
146
|
+
const floor = await allTasks(c, root)
|
|
147
|
+
for (const n of floor.notes ?? []) sink(n)
|
|
148
|
+
for (const u of floor.unfilled) {
|
|
149
|
+
sink(`skip: ${u.id} (${u.reason})`)
|
|
150
|
+
// asked for BY NAME and unfillable: this is where an agent lands after
|
|
151
|
+
// the harness refused it the raw runner, so the line says what to add
|
|
152
|
+
// rather than leaving it with `unknown check id` and no next move
|
|
153
|
+
if ((o.onlyIds ?? []).includes(u.id))
|
|
154
|
+
sink(` fix: add a '${u.id}:raw' script holding the tool, then \`shipkit ci --only ${u.id}\` runs it (meta/rv-conventions/package-json.md)`)
|
|
155
|
+
}
|
|
156
|
+
// the run record: one run id for every check of this gate, so a breakdown
|
|
157
|
+
// of the wall time is reconstructable. It is never read here, only written
|
|
158
|
+
const scope = ciScope(o)
|
|
159
|
+
const runLog = RunLog.open(root, { invocation: { kind: 'ci', label: scope.command, by: o.invokedBy ?? 'cli', narrowed: !scope.fullGate } })
|
|
137
160
|
// THE LEDGER, opened once per run and AFTER the fix pass: the fixers
|
|
138
|
-
// rewrite source, and a scan taken before them would have every
|
|
161
|
+
// rewrite source, and a scan taken before them would have every check
|
|
139
162
|
// discarded by its own gate's repair work.
|
|
140
163
|
//
|
|
141
164
|
// Registration is validated on the way in, and it is loud in the gate
|
|
142
|
-
// rather than only in `shipkit keys`, because a
|
|
165
|
+
// rather than only in `shipkit keys`, because a check declaring two
|
|
143
166
|
// contradictory populations is a bug in what binds every repo. The
|
|
144
167
|
// burn-down is one line, never a wall.
|
|
145
168
|
let registrationFailed = false
|
|
146
169
|
let ledger: Ledger | null = null
|
|
170
|
+
// ONE walk for the whole gate: the ledger is keyed on it and the run
|
|
171
|
+
// measures it, and two walks can disagree about what exists
|
|
172
|
+
const composed = await composeChecks(root, c, { tier })
|
|
147
173
|
try {
|
|
148
|
-
ledger = Ledger.open(root,
|
|
149
|
-
cliEntry:
|
|
174
|
+
ledger = Ledger.open(root, composed.map((g) => g.row.decl), {
|
|
175
|
+
cliEntry: o.cliEntry ?? gateBinary(),
|
|
150
176
|
read: o.ledgerRead,
|
|
151
177
|
fresh: o.fresh,
|
|
152
178
|
})
|
|
153
179
|
const reg = ledger.layer.registration
|
|
154
180
|
for (const e of reg.errors) { registrationFailed = true; sink(`FAIL: ${e}`) }
|
|
155
|
-
if (registrationFailed) sink(' fix: a
|
|
181
|
+
if (registrationFailed) sink(' fix: a check declares `inputs` (content), or `ttl` (world), or `never: <reason>`, and never two of them')
|
|
156
182
|
// both counts, always. Printing only the burn-down made `never:` the
|
|
157
183
|
// cheap way to shrink the visible number, and that field carries no
|
|
158
184
|
// verification at all: a growing decided population has to be as
|
|
159
185
|
// visible as a shrinking undeclared one.
|
|
160
|
-
const decided = reg.
|
|
186
|
+
const decided = reg.checks - reg.burnDown.length - reg.declared
|
|
161
187
|
if (reg.burnDown.length > 0 || decided > 0)
|
|
162
|
-
sink(`skip: cache declarations — ${reg.declared}/${reg.
|
|
188
|
+
sink(`skip: cache declarations — ${reg.declared}/${reg.checks} declared, ${decided} never cached on purpose, ${reg.burnDown.length} undeclared, so those run every time (shipkit keys --burn-down)`)
|
|
163
189
|
if (ledger.layer.failure != null)
|
|
164
|
-
sink(`WARN the git scan failed, so NOTHING
|
|
190
|
+
sink(`WARN the git scan failed, so NOTHING comes from the cache and nothing is stored this run — ${ledger.layer.failure}`)
|
|
165
191
|
} catch (e) {
|
|
166
192
|
ledger = null
|
|
167
|
-
sink(`WARN
|
|
193
|
+
sink(`WARN cache unavailable (${String(e).slice(0, 160)}) — every check runs this time, so the gate is correct and slower`)
|
|
168
194
|
}
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
replayedTasks.set(t.id, hit)
|
|
195
|
-
for (const l of stampReplayed(hit.lines, hit.measuredMs)) sink(formatLine(l, false))
|
|
196
|
-
return false
|
|
197
|
-
})
|
|
198
|
-
// a dep ON a replayed task is DROPPED, not left dangling. The runner
|
|
199
|
-
// validates every dep against the list it was handed and dies with
|
|
200
|
-
// `unknown dep` otherwise, and dropping is the honest edge here: only a
|
|
201
|
-
// GREEN is ever replayed, so the dependent's precondition is met and
|
|
202
|
-
// this is the same "a filtered dep counts as satisfied" the runner
|
|
203
|
-
// already applies to --skip and opt-in tasks.
|
|
204
|
-
.map((t) => (t.deps?.some((d) => replayedTasks.has(d)) === true ? { ...t, deps: t.deps.filter((d) => !replayedTasks.has(d)) } : t))
|
|
205
|
-
// a NAMED task that replayed leaves the list, so its name has to leave the
|
|
206
|
-
// filters with it: the runner validates `--only`/`--with` against the list
|
|
207
|
-
// it was handed and answers `unknown task id`. `bun run typecheck` and
|
|
208
|
-
// `bun run test` route through `--only`, so this is the reflex verb every
|
|
209
|
-
// agent in every wired repo types, going red on a warm store.
|
|
210
|
-
const onlyIds = (o.onlyIds ?? []).filter((id) => !replayedTasks.has(id))
|
|
211
|
-
const withIds = (o.withIds ?? []).filter((id) => !replayedTasks.has(id))
|
|
212
|
-
// narrowed to nothing is NOT the same as "no filter": an empty --only would
|
|
213
|
-
// widen the run to every task
|
|
214
|
-
const nothingToRun = replayedTasks.size > 0 && (toRun.length === 0 || ((o.onlyIds ?? []).length > 0 && onlyIds.length === 0))
|
|
215
|
-
// every asked-for task replayed: the DAG has nothing to schedule, and
|
|
216
|
-
// running it anyway would print `all 0 tasks green`, which reads as a gate
|
|
217
|
-
// that measured nothing rather than one that already knew the answer
|
|
218
|
-
const summary = nothingToRun
|
|
219
|
-
? { ok: true, results: [], interrupted: false }
|
|
220
|
-
: await runTasks({
|
|
221
|
-
tasks: toRun,
|
|
222
|
-
logDir: c.logDir ?? join(root, 'tmp/shipkit-ci'),
|
|
223
|
-
cwd: root,
|
|
224
|
-
bail: o.bail ?? false,
|
|
225
|
-
skipGroups: o.skipGroups ?? [],
|
|
226
|
-
withIds,
|
|
227
|
-
onlyIds,
|
|
228
|
-
otherIds,
|
|
195
|
+
// THE RUN. One engine, one DAG, one pass over everything this gate asks
|
|
196
|
+
// for: `gate = false` opts a check out of the everyday gate, and
|
|
197
|
+
// failOnAdvisory (release.requireHealthy) re-arms the world probes,
|
|
198
|
+
// because a repo asking for exactly those must not have them silently
|
|
199
|
+
// dropped by the opt-out.
|
|
200
|
+
const run = await runCheckPass(root, {
|
|
201
|
+
status: false,
|
|
202
|
+
config: c,
|
|
203
|
+
composed,
|
|
204
|
+
audience: 'gate',
|
|
205
|
+
includeProactive: o.failOnAdvisory === true,
|
|
206
|
+
frozen: o.frozen,
|
|
207
|
+
onlyBlocking: o.onlyBlockingChecks,
|
|
208
|
+
only: o.onlyIds,
|
|
209
|
+
withIds: o.withIds,
|
|
210
|
+
skipGroups: o.skipGroups,
|
|
211
|
+
exclude: o.skipTaskIds,
|
|
212
|
+
buildOnly: o.buildOnly,
|
|
213
|
+
bail: o.bail,
|
|
214
|
+
fresh: o.fresh,
|
|
215
|
+
tier,
|
|
216
|
+
ledger,
|
|
217
|
+
runLog,
|
|
218
|
+
onCheck: o.onCheck,
|
|
219
|
+
reportHome: false,
|
|
229
220
|
print: sink,
|
|
230
|
-
onTaskFinish: o.onTaskFinish,
|
|
231
221
|
})
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
status: surfaced.some((l) => l.kind === 'warn' || l.kind === 'fix-warn') ? 'warn' : 'pass',
|
|
240
|
-
// the headline is REBUILT rather than captured: the runner renders
|
|
241
|
-
// the wall time into it, and a duration baked into a stored line
|
|
242
|
-
// would be replayed as if this run had measured it
|
|
243
|
-
lines: [{ kind: 'ok', text: r.id }, ...surfaced],
|
|
244
|
-
lastLine: r.lastLine,
|
|
245
|
-
measuredMs: r.durationMs,
|
|
246
|
-
})
|
|
247
|
-
}
|
|
248
|
-
const ranTests = [...summary.results.map((r) => r.lastLine ?? ''), ...[...replayedTasks.values()].map((o) => o.lastLine ?? '')]
|
|
249
|
-
.map((l) => /Ran (\d+) tests?/.exec(l)?.[1])
|
|
250
|
-
.find((n) => n != null)
|
|
251
|
-
if (ranTests != null) sink(`ok: test tier ${tier}: ${ranTests} tests ran`)
|
|
252
|
-
// the checks, in the SAME command. there is no second surface: a check
|
|
253
|
-
// declares whether it blocks, and that is the only question about it.
|
|
254
|
-
// a narrowed run says what it did NOT look at: a green that only ever
|
|
255
|
-
// asked one question must not read like the full gate
|
|
256
|
-
if ((o.onlyIds ?? []).length > 0 && o.skipChecks !== true) sink(`skip: checks not named by --only (${(o.onlyIds ?? []).join(', ')})`)
|
|
257
|
-
// no release survey is pre-warmed here. It is ~3s of registry and `gh`
|
|
258
|
-
// network, and the only thing that ever read it is the lib-drift probe,
|
|
259
|
-
// which asks what is PUBLISHED and so has no business in a gate that asks
|
|
260
|
-
// whether this change is good. collectChecks owns the share path, so the
|
|
261
|
-
// checks that do want it still pay for exactly one survey between them.
|
|
262
|
-
const checks = o.skipChecks === true
|
|
263
|
-
? { lines: [], blocked: false, advisoryFailures: [], undeclared: [] }
|
|
264
|
-
// `gate = false` opts a check out of the EVERYDAY gate. failOnAdvisory
|
|
265
|
-
// is release.requireHealthy, a repo saying it wants a missing cred to
|
|
266
|
-
// stop its publish, so it re-arms them: the opt-out must not silently
|
|
267
|
-
// drop the one request that asks for exactly these probes.
|
|
268
|
-
: await collectChecks(root, { status: false, config: c, frozen: o.frozen, onlyBlocking: o.onlyBlockingChecks, only: o.onlyIds, fresh: o.fresh, ledger, reportHome: false, gateOnly: o.failOnAdvisory !== true })
|
|
269
|
-
const color = process.stdout.isTTY && process.env.NO_COLOR == null
|
|
270
|
-
for (const l of checks.lines) { collected.push(formatLine(l, false)); print(formatLine(l, color)) }
|
|
271
|
-
for (const u of checks.undeclared) {
|
|
222
|
+
// what the suite ACTUALLY ran, from its own headline. A flag that was set
|
|
223
|
+
// proves the intent; the count proves the tests
|
|
224
|
+
// proves the intent; the count proves the tests. Every test check counts,
|
|
225
|
+
// one per unit
|
|
226
|
+
const counts = run.results.map((r) => /Ran (\d+) tests?/.exec(r.lastLine)?.[1]).filter((n) => n != null).map(Number)
|
|
227
|
+
if (counts.length > 0) sink(`ok: test tier ${tier}: ${counts.reduce((a, b) => a + b, 0)} tests ran${counts.length > 1 ? ` in ${counts.length} test checks` : ''}`)
|
|
228
|
+
for (const u of run.undeclared) {
|
|
272
229
|
sink(`FAIL: ${u} does not declare whether it blocks`)
|
|
273
|
-
sink(` fix: add \`
|
|
230
|
+
sink(` fix: add \`blocking: true\` (about this repo) or \`false\` (about the world) to that check`)
|
|
231
|
+
}
|
|
232
|
+
// a switch wired to no check does nothing, silently, which is the one thing
|
|
233
|
+
// a toggle must never do
|
|
234
|
+
for (const id of run.unknownWhen) {
|
|
235
|
+
sink(`FAIL: when.${id} names no check of this repo`)
|
|
236
|
+
sink(` fix: \`shipkit checks\` lists every id; drop the entry or correct it`)
|
|
274
237
|
}
|
|
275
238
|
let advisoryFatal = false
|
|
276
|
-
if (
|
|
239
|
+
if (run.advisoryFailures.length > 0) {
|
|
277
240
|
if (o.failOnAdvisory === true) {
|
|
278
241
|
advisoryFatal = true
|
|
279
|
-
sink(`FAIL: ${
|
|
242
|
+
sink(`FAIL: ${run.advisoryFailures.length} non-blocking check(s) failing and release.requireHealthy is on: ${run.advisoryFailures.join(', ')}`)
|
|
280
243
|
sink(' fix: shipkit check')
|
|
281
|
-
} else sink(`WARN ${
|
|
244
|
+
} else sink(`WARN ${run.advisoryFailures.length} non-blocking check(s) failing, about the world rather than this repo: ${run.advisoryFailures.join(', ')}`)
|
|
282
245
|
}
|
|
283
246
|
// the post-run re-hash, then the store. NOTHING was written while the gate
|
|
284
|
-
// ran: a
|
|
247
|
+
// ran: a check whose inputs moved under it answered about a tree that no
|
|
285
248
|
// longer exists, and this is where that result is thrown away instead of
|
|
286
249
|
// stored under a key that describes the file as it was before the edit.
|
|
287
250
|
if (ledger != null) for (const l of ledger.header(ledger.commit())) sink(formatLine(l, false))
|
|
288
|
-
|
|
251
|
+
// one write for the whole gate, after everything has finished
|
|
252
|
+
for (const l of runLog.flush()) sink(formatLine(l, false))
|
|
253
|
+
const code = run.blocked || fixFailed || registrationFailed || advisoryFatal || run.undeclared.length > 0 || run.unknownWhen.length > 0 ? 1 : 0
|
|
289
254
|
// motherhome row (docsBase wiki on this disk) — never affects the gate,
|
|
290
|
-
// and never fires under frozen: a bump/release gate must not re-dirty
|
|
291
|
-
//
|
|
255
|
+
// and never fires under frozen: a bump/release gate must not re-dirty the
|
|
256
|
+
// tree it just proved clean
|
|
292
257
|
if (o.frozen !== true) {
|
|
293
258
|
const mh = reportToMotherhome(root, c)
|
|
294
259
|
if (mh.note != null) print(mh.note)
|
|
@@ -301,14 +266,13 @@ async function runCiInScope(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
301
266
|
...ciScope(o),
|
|
302
267
|
lines: collected,
|
|
303
268
|
exitCode: code,
|
|
304
|
-
sidecar: { file:
|
|
269
|
+
sidecar: { file: CHECK_TABLE_FILE, says: 'Every check this repo can run, and whether it blocks you' },
|
|
305
270
|
})
|
|
306
271
|
if (!st.written) print(`WARN ${st.warn}`)
|
|
307
|
-
const
|
|
308
|
-
if (!
|
|
309
|
-
|
|
272
|
+
const checks = writeCheckTable(root, { name: c.name, rows: composed.map((g) => g.row) })
|
|
273
|
+
if (!checks.written) print(`WARN ${checks.warn}`)
|
|
310
274
|
}
|
|
311
275
|
if (o.json === true)
|
|
312
|
-
print(JSON.stringify({ ok:
|
|
276
|
+
print(JSON.stringify({ ok: !run.blocked, interrupted: run.interrupted, disabled: floor.disabled, checks: run.results.map((r: CheckResult) => ({ ...r, lines: undefined })) }))
|
|
313
277
|
return code
|
|
314
278
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// `shipkit check --fix [<id> …]` — apply check autofixes, then re-run and
|
|
2
|
+
// show the real end state. Never runs under ci (the gate stays pure), never
|
|
3
|
+
// caches (a fixer must see reality). No ids = every fixable check; a
|
|
4
|
+
// requested id that is unknown or unfixable fails loud instead of silently
|
|
5
|
+
// narrowing.
|
|
6
|
+
import { CheckCtx } from './check-ctx.ts'
|
|
7
|
+
import { formatLine } from './wire.ts'
|
|
8
|
+
import { whenOfRule, whenOfCheck, type WhenDecl } from './when.ts'
|
|
9
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
10
|
+
import { discoverChecks } from './discover-checks.ts'
|
|
11
|
+
import { runCheckBody, type CheckDef, type CheckEnv } from '../check.ts'
|
|
12
|
+
import type { RepoConfig } from '../repo-config.ts'
|
|
13
|
+
|
|
14
|
+
export type FixOpts = {
|
|
15
|
+
ids?: string[]
|
|
16
|
+
/** drop the closing advisory: inside the ci fix pass it is noise, and it is
|
|
17
|
+
* not wire-shaped, which the frozen output contract forbids */
|
|
18
|
+
quiet?: boolean
|
|
19
|
+
print?: (line: string) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
23
|
+
const print = o.print ?? ((l: string) => console.log(l))
|
|
24
|
+
const config = await loadRepoConfig(root)
|
|
25
|
+
const env: CheckEnv = { root, config }
|
|
26
|
+
// a fresh context per body: a fixer may rewrite the config, and the body
|
|
27
|
+
// after it must read the one on disk now
|
|
28
|
+
const ctx = (c: RepoConfig): CheckCtx => CheckCtx.open({ root, config: c })
|
|
29
|
+
const color = process.stdout.isTTY && process.env.NO_COLOR == null
|
|
30
|
+
/** one fixable check, whatever declared it */
|
|
31
|
+
type Target = { id: string; title: string; owner: string; run: CheckDef['run']; fix?: CheckDef['fix']; when: WhenDecl }
|
|
32
|
+
// a rule the repo still registers fixes exactly like a check file, one
|
|
33
|
+
// code path. Only a check that declared a `fix` is a target at all
|
|
34
|
+
const all: Target[] = [
|
|
35
|
+
...(await discoverChecks(root)).map((u): Target => ({ id: u.id, title: u.title, owner: u.owner, run: u.def.run, fix: u.def.fix, when: whenOfCheck(config, u) })),
|
|
36
|
+
...(config.rules ?? []).map((r): Target => ({
|
|
37
|
+
id: r.id, title: r.title, owner: r.owner, when: whenOfRule(config, r.id),
|
|
38
|
+
run: (c) => r.lint(c, { root: c.root, config: c.config }),
|
|
39
|
+
fix: r.fix == null ? undefined : (c) => r.fix!({ root: c.root, config: c.config }),
|
|
40
|
+
})),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
let targets = all.filter((r) => r.fix != null)
|
|
44
|
+
if (o.ids != null && o.ids.length > 0) {
|
|
45
|
+
for (const id of o.ids) {
|
|
46
|
+
const r = all.find((x) => x.id === id)
|
|
47
|
+
if (r == null) { print(`FAIL: unknown check '${id}' — see: shipkit checks`); return 1 }
|
|
48
|
+
if (r.fix == null) { print(`FAIL: [${id}] has no autofix — fix by hand: ${config.docsBase ?? ''}${r.owner}`); return 1 }
|
|
49
|
+
}
|
|
50
|
+
const wanted = new Set(o.ids)
|
|
51
|
+
targets = targets.filter((r) => wanted.has(r.id))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let exit = 0
|
|
55
|
+
for (const rule of targets) {
|
|
56
|
+
const before = ctx(env.config)
|
|
57
|
+
const off = rule.when
|
|
58
|
+
await runCheckBody(rule, before, off.at === 'off' ? { reason: off.reason } : undefined)
|
|
59
|
+
// a check that is OFF never autofixes: the repo broke that invariant on purpose
|
|
60
|
+
if (off.at === 'off') {
|
|
61
|
+
if (o.quiet !== true) print(`skip: [${rule.id}] ${rule.id} exempt: ${off.reason ?? ''}`)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
if (!before.failed && !before.warned) {
|
|
65
|
+
// inside the ci fix pass, a line per already-clean rule is a wall of
|
|
66
|
+
// nothing above the run that reports those same rules for real
|
|
67
|
+
if (o.quiet !== true) print(`ok: [${rule.id}] already clean — nothing to fix`)
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
const f = await rule.fix!(ctx(env.config))
|
|
71
|
+
// an unchanged fix is the DESIGNED outcome for judgment findings, so
|
|
72
|
+
// it is an ok, not a warning. It carries a frozen wire prefix like
|
|
73
|
+
// every other line the fix pass emits: `shipkit ci` promises its whole
|
|
74
|
+
// output is parseable, and a bare line breaks that for every reader.
|
|
75
|
+
print(f.changed ? `ok: [${rule.id}] fixed: ${f.summary}` : `ok: [${rule.id}] fix changed nothing — ${f.summary}`)
|
|
76
|
+
// a fixer may rewrite repo.config.ts, and the re-run below and every
|
|
77
|
+
// fixer after it must read what is on disk now, never the config this
|
|
78
|
+
// pass loaded before the rewrite
|
|
79
|
+
if (f.changed) {
|
|
80
|
+
try { env.config = await loadRepoConfig(root, { fresh: true }) } catch (e) {
|
|
81
|
+
print(`FAIL: [${rule.id}] repo.config.ts no longer loads after the fix — ${String(e).slice(0, 200)}`)
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const after = ctx(env.config)
|
|
86
|
+
await runCheckBody(rule, after)
|
|
87
|
+
// the fix already ran: advertising it again on surviving findings
|
|
88
|
+
// would send an agent in a loop
|
|
89
|
+
for (const l of after.lines) print(formatLine({ ...l, text: l.text.replace(/ · autofix: shipkit check --fix \S+$/, '') }, color))
|
|
90
|
+
if (after.failed) exit = 1
|
|
91
|
+
}
|
|
92
|
+
const unfixable = all.filter((r) => r.fix == null).length
|
|
93
|
+
if (unfixable > 0 && o.quiet !== true && (o.ids == null || o.ids.length === 0))
|
|
94
|
+
print(`${unfixable} check(s) have no autofix — they report in the run below`)
|
|
95
|
+
return exit
|
|
96
|
+
}
|