rvlib-shipkit 1.2.0 → 1.4.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 +104 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +15 -10
- package/src/check.ts +26 -10
- package/src/checks/SK001-package-json-shape.check.ts +34 -5
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +103 -61
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK014-prose-ste.check.ts +16 -6
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK016-legacy-check-shapes.check.ts +11 -5
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
- package/src/checks/SK024-identity-colors.check.ts +5 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +50 -0
- package/src/checks/SK028-readme.check.ts +83 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Ci.ts +5 -0
- package/src/cli/cmd_CrossRepoActivity.ts +119 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +9 -1
- package/src/collections/checks.ts +3 -2
- package/src/collections/collection-types.ts +15 -0
- package/src/collections/providers.ts +7 -3
- package/src/collections/test-time.ts +10 -1
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-runner.ts +66 -17
- package/src/core/discover-checks.ts +10 -5
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +1 -1
- package/src/core/layout.ts +2 -0
- package/src/core/motherhome.ts +88 -66
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +14 -5
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +8 -7
- package/src/core/run-ci.ts +46 -15
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/wire.ts +49 -9
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +138 -0
- package/src/home/activity.ts +256 -0
- package/src/home/global-units.ts +3 -2
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +10 -2
- package/src/home/home-routes.ts +34 -6
- package/src/home/home-start.ts +33 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +40 -7
- package/src/home/process-rows.ts +222 -0
- package/src/home/resources.ts +99 -20
- package/src/home/run-daemon.ts +12 -7
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +6 -3
- package/src/home/supervise.ts +16 -1
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +32 -5
- package/src/ledger/db.ts +1 -1
- package/src/ledger/gate-checks.ts +20 -6
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +39 -10
- package/src/ledger/store.ts +30 -2
- package/src/ledger/world-cache.ts +63 -14
- package/src/plan/cadence.ts +21 -5
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +138 -13
- package/src/plan/repo-source.ts +120 -8
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +17 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-words.ts +2 -0
- package/src/serve/hub-app/checks-svc.ts +2 -1
- package/src/serve/hub-app/col-tables-view.tsx +124 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +28 -0
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/hash-route.ts +7 -1
- package/src/serve/hub-app/header-bar.tsx +79 -104
- package/src/serve/hub-app/home-st.ts +50 -13
- package/src/serve/hub-app/home-strip.css +81 -23
- package/src/serve/hub-app/home-strip.tsx +243 -102
- package/src/serve/hub-app/home.html +4 -0
- package/src/serve/hub-app/hub-app.tsx +103 -161
- package/src/serve/hub-app/hub-types.ts +6 -3
- package/src/serve/hub-app/hub.css +101 -39
- package/src/serve/hub-app/icons.tsx +27 -3
- package/src/serve/hub-app/index.html +4 -0
- package/src/serve/hub-app/keys.ts +44 -9
- package/src/serve/hub-app/live-svc.ts +24 -64
- package/src/serve/hub-app/machine-st.ts +65 -13
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/next-run.ts +1 -1
- package/src/serve/hub-app/palette-st.ts +13 -9
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +21 -85
- package/src/serve/hub-app/planned-view.tsx +12 -7
- package/src/serve/hub-app/planned.css +5 -2
- package/src/serve/hub-app/project-list.css +43 -21
- package/src/serve/hub-app/project-list.tsx +122 -23
- package/src/serve/hub-app/projects-svc.ts +120 -12
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +26 -7
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/tests-view.tsx +31 -4
- package/src/serve/hub-app/timeline-chart.tsx +31 -27
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +2 -2
- package/src/serve/hub-app/timeline-view.tsx +29 -9
- package/src/serve/hub-runtime.ts +17 -9
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +100 -20
- package/src/serve/project-gutter.ts +34 -13
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/serve-hub.ts +13 -8
- package/src/serve/tracked-watch.ts +33 -1
- package/src/serve/warm-cell.ts +49 -16
- package/src/shipkit-files.ts +20 -4
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/checks/SK023-stores-under-cap.check.ts +0 -49
- package/src/checks/contracts.check.ts +0 -20
|
@@ -1,27 +1,30 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// visibility with it. gh keeps that login outside any secrets manager, so
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// the gh login the toolkit itself runs on: `init` and `start` create a
|
|
2
|
+
// PRIVATE repo with it, every push and release goes through it, SK007 reads
|
|
3
|
+
// visibility with it. gh keeps that login outside any secrets manager, so the
|
|
4
|
+
// house repo's one `github-login` check (a machine fact, asked once, never in
|
|
5
|
+
// every repo) runs `probeGhLogin`. It is a `core` credential: red on the
|
|
6
|
+
// secrets page and in `shipkit needs` (collections/providers.ts reads the
|
|
7
|
+
// `cred <name> missing` line).
|
|
8
8
|
//
|
|
9
9
|
// A private repo needs the classic `repo` scope. The token itself is never
|
|
10
10
|
// asked for: `gh auth status --json` carries no token field, and the text
|
|
11
11
|
// answer masks it. github.com unreachable is `unreachable`, a WARN that
|
|
12
12
|
// measured nothing, never a lost login.
|
|
13
13
|
import { carveGhJson, firstGhLine } from '../git-bootstrap.ts'
|
|
14
|
-
import {
|
|
14
|
+
import type { CheckCtx } from './check-ctx.ts'
|
|
15
15
|
|
|
16
16
|
const HOST = 'github.com'
|
|
17
17
|
|
|
18
|
-
/** the
|
|
19
|
-
|
|
18
|
+
/** the id of the house check that runs `probeGhLogin` */
|
|
19
|
+
export const GH_LOGIN_CHECK = 'github-login'
|
|
20
|
+
|
|
21
|
+
/** the secrets page row the probe proves: a built-in `core` credential, shown
|
|
22
|
+
* in the repo where the `github-login` check has run */
|
|
20
23
|
export const GH_LOGIN_CRED = {
|
|
21
24
|
name: 'gh/github/login',
|
|
22
25
|
service: 'gh login',
|
|
23
26
|
level: 'core' as const,
|
|
24
|
-
probedBy:
|
|
27
|
+
probedBy: GH_LOGIN_CHECK,
|
|
25
28
|
mintUrl: 'https://cli.github.com/manual/gh_auth_login',
|
|
26
29
|
/** what the row offers to copy when no failing line names a better fix */
|
|
27
30
|
command: 'gh auth login',
|
|
@@ -76,48 +79,36 @@ export function readGhText(out: string): GhLogin {
|
|
|
76
79
|
return { state: 'ok', login: login[1]!, scopes: scopesOf(scopes?.[1]) }
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
when: 'health',
|
|
88
|
-
ttl: '1h',
|
|
89
|
-
run: async (c) => {
|
|
90
|
-
const cred = GH_LOGIN_CRED.name
|
|
91
|
-
// the switch that keeps every github step off (git-bootstrap.ts), which every test suite sets
|
|
92
|
-
if ((process.env['SHIPKIT_NO_GITHUB'] ?? '') !== '') return void c.lines.push({ kind: 'skip', text: 'gh login not probed, SHIPKIT_NO_GITHUB is set' })
|
|
93
|
-
const gh = c.tool('gh')
|
|
94
|
-
if (gh == null)
|
|
95
|
-
return c.fail(`cred ${cred} missing: gh is not installed, so shipkit cannot create a repo, push or release`, 'install gh (https://cli.github.com), then run `gh auth login`')
|
|
96
|
-
|
|
97
|
-
let r = await c.exec([gh, 'auth', 'status', '--hostname', HOST, '--active', '--json', 'hosts'], { cwd: c.root, timeoutMs: 20_000 })
|
|
98
|
-
let seen: GhLogin
|
|
99
|
-
if (r.code !== 0 && /unknown flag/i.test(r.out)) {
|
|
100
|
-
r = await c.exec([gh, 'auth', 'status', '--hostname', HOST], { cwd: c.root, timeoutMs: 20_000 })
|
|
101
|
-
seen = readGhText(r.out)
|
|
102
|
-
} else seen = r.timedOut ? { state: 'unreachable', why: 'gh auth status timed out' } : readGhJson(r.out)
|
|
82
|
+
/** the probe: logged in to github.com, with the repo scope */
|
|
83
|
+
export async function probeGhLogin(c: CheckCtx): Promise<void> {
|
|
84
|
+
const cred = GH_LOGIN_CRED.name
|
|
85
|
+
// the switch that keeps every github step off (git-bootstrap.ts), which every test suite sets
|
|
86
|
+
if ((process.env['SHIPKIT_NO_GITHUB'] ?? '') !== '') return void c.lines.push({ kind: 'skip', text: 'gh login not probed, SHIPKIT_NO_GITHUB is set' })
|
|
87
|
+
const gh = c.tool('gh')
|
|
88
|
+
if (gh == null)
|
|
89
|
+
return c.fail(`cred ${cred} missing: gh is not installed, so shipkit cannot create a repo, push or release`, 'install gh (https://cli.github.com), then run `gh auth login`')
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return c.unreachable(`gh login unverified: ${HOST} unreachable (${seen.why.slice(0, 120)})`)
|
|
111
|
-
case 'unreadable':
|
|
112
|
-
return c.unreachable(`gh login unverified: gh answered something unreadable (${seen.why})`)
|
|
113
|
-
case 'ok':
|
|
114
|
-
if (seen.scopes == null)
|
|
115
|
-
return c.warn(`gh is logged in to ${HOST} as ${seen.login}, but its token lists no oauth scopes (a fine grained or app token), so creating a private repo is unverified`, 'gh auth login')
|
|
116
|
-
if (!seen.scopes.includes('repo'))
|
|
117
|
-
return c.fail(`cred ${cred} missing: the gh login ${seen.login} lacks the repo scope (it has ${seen.scopes.join(', ')}), so shipkit cannot create a private repo`, `gh auth refresh -h ${HOST} -s repo`)
|
|
118
|
-
return c.ok(`gh is logged in to ${HOST} as ${seen.login}, with the repo scope`)
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
})
|
|
91
|
+
let r = await c.exec([gh, 'auth', 'status', '--hostname', HOST, '--active', '--json', 'hosts'], { cwd: c.root, timeoutMs: 20_000 })
|
|
92
|
+
let seen: GhLogin
|
|
93
|
+
if (r.code !== 0 && /unknown flag/i.test(r.out)) {
|
|
94
|
+
r = await c.exec([gh, 'auth', 'status', '--hostname', HOST], { cwd: c.root, timeoutMs: 20_000 })
|
|
95
|
+
seen = readGhText(r.out)
|
|
96
|
+
} else seen = r.timedOut ? { state: 'unreachable', why: 'gh auth status timed out' } : readGhJson(r.out)
|
|
122
97
|
|
|
123
|
-
|
|
98
|
+
switch (seen.state) {
|
|
99
|
+
case 'none':
|
|
100
|
+
return c.fail(`cred ${cred} missing: gh is not logged in to ${HOST}, so shipkit cannot create a repo, push or release`, 'gh auth login')
|
|
101
|
+
case 'refused':
|
|
102
|
+
return c.fail(`cred ${cred} missing: the gh login from ${seen.source} was refused by ${HOST} (${seen.why.slice(0, 120)})`, 'gh auth login')
|
|
103
|
+
case 'unreachable':
|
|
104
|
+
return c.unreachable(`gh login unverified: ${HOST} unreachable (${seen.why.slice(0, 120)})`)
|
|
105
|
+
case 'unreadable':
|
|
106
|
+
return c.unreachable(`gh login unverified: gh answered something unreadable (${seen.why})`)
|
|
107
|
+
case 'ok':
|
|
108
|
+
if (seen.scopes == null)
|
|
109
|
+
return c.warn(`gh is logged in to ${HOST} as ${seen.login}, but its token lists no oauth scopes (a fine grained or app token), so creating a private repo is unverified`, 'gh auth login')
|
|
110
|
+
if (!seen.scopes.includes('repo'))
|
|
111
|
+
return c.fail(`cred ${cred} missing: the gh login ${seen.login} lacks the repo scope (it has ${seen.scopes.join(', ')}), so shipkit cannot create a private repo`, `gh auth refresh -h ${HOST} -s repo`)
|
|
112
|
+
return c.ok(`gh is logged in to ${HOST} as ${seen.login}, with the repo scope`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -116,7 +116,7 @@ const KEY_RE = /(?:^|[{,])\s*(['"]?)identity\1\s*:\s*/gm
|
|
|
116
116
|
|
|
117
117
|
/** per char of the text: the object depth inside `defineRepo({` after that
|
|
118
118
|
* char (1 on the top level keys), or -1 inside a string or a comment */
|
|
119
|
-
function configDepths(text: string): Int32Array {
|
|
119
|
+
export function configDepths(text: string): Int32Array {
|
|
120
120
|
const depth = new Int32Array(text.length).fill(0)
|
|
121
121
|
const open = /defineRepo\(\s*\{/.exec(text)
|
|
122
122
|
if (open == null) return depth
|
package/src/core/layout.ts
CHANGED
|
@@ -32,6 +32,8 @@ export const LAYOUT = {
|
|
|
32
32
|
reviews: sk('reviews'),
|
|
33
33
|
// scratch: plans, logs, leases, detached runs, screenshots
|
|
34
34
|
tmp: sk('tmp'),
|
|
35
|
+
// built app bundles, one per global process opened as an app (home/gui-app.ts)
|
|
36
|
+
wrapper: sk('wrapper'),
|
|
35
37
|
// generated on every run
|
|
36
38
|
status: sk('STATUS.md'),
|
|
37
39
|
statusFull: sk('STATUS.full.md'),
|
package/src/core/motherhome.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
// Report to the motherhome
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
1
|
+
// Report to the motherhome: a repo's row in the docsBase wiki's
|
|
2
|
+
// `build/shipkit/local-projects.md`, in two halves.
|
|
3
|
+
//
|
|
4
|
+
// - `reportToMotherhome` (every ci/check/init) files the row in the MACHINE
|
|
5
|
+
// DB and nothing else. A gate answers about its own repo, so it never
|
|
6
|
+
// writes, let alone commits, in the docs repo.
|
|
7
|
+
// - `landMotherhome` (the daemon's hourly sweep) renders every filed row into
|
|
8
|
+
// its page and lands the page as ONE commit, at most once per LAND_GAP_MS
|
|
9
|
+
// per page, only when a row's data changed or its last-seen day is older
|
|
10
|
+
// than STALE_DAYS.
|
|
11
|
+
//
|
|
12
|
+
// The lib NEVER creates the page and never reads a relative or URL docsBase:
|
|
13
|
+
// the page is a tracked wiki file, so a real docs checkout has it, and a
|
|
14
|
+
// consumer that merely set docsBase as a fix-pointer prefix must never find
|
|
15
|
+
// surprise files in its docs tree. The page's read-modify-write runs under
|
|
16
|
+
// the shared file lock (core/locked-write). Rows whose repo path no longer
|
|
17
|
+
// exists are pruned from the page and from the machine db.
|
|
13
18
|
import { existsSync, readFileSync } from 'fs'
|
|
14
19
|
import { homedir } from 'os'
|
|
15
20
|
import { join, resolve } from 'path'
|
|
@@ -17,46 +22,31 @@ import type { RepoConfig } from '../repo-config.ts'
|
|
|
17
22
|
import { atomicWrite, withFileLock } from './locked-write.ts'
|
|
18
23
|
import { isLinkedWorktree } from './git-root.ts'
|
|
19
24
|
import { machineCommit } from './machine-commit.ts'
|
|
25
|
+
import { machineDelete, machineGet, machineList, machineSet } from './machine-db.ts'
|
|
20
26
|
import { LIB_VERSION } from './lib-version.ts'
|
|
21
27
|
|
|
22
28
|
const BEGIN = '<!-- >>> shipkit local-projects — machine-written, edit outside the markers -->'
|
|
23
29
|
const END = '<!-- <<< shipkit local-projects -->'
|
|
24
30
|
const STALE_DAYS = 7
|
|
31
|
+
/** the longest the page waits for a changed row: one commit a day at most */
|
|
32
|
+
export const LAND_GAP_MS = 24 * 60 * 60 * 1000
|
|
33
|
+
/** a row filed in the machine db, by its repo's path */
|
|
34
|
+
const ROW = 'motherhome:'
|
|
35
|
+
/** when a page last landed, by its path */
|
|
36
|
+
const LANDED = 'motherhome-landed:'
|
|
25
37
|
|
|
26
38
|
type Row = { name: string; root: string; version: string; mode: string; seen: string }
|
|
39
|
+
type Filed = Row & { page: string }
|
|
27
40
|
|
|
28
41
|
const rowLine = (r: Row): string => `- ${r.name} — \`${r.root}\` — shipkit ${r.version} (${r.mode}) — last seen ${r.seen}`
|
|
29
42
|
|
|
30
|
-
/**
|
|
31
|
-
* Commit the row we just wrote, and ONLY that file.
|
|
32
|
-
*
|
|
33
|
-
* This page is machine written and rewrites whenever a repo's shipkit version
|
|
34
|
-
* or consumption mode changes, so left alone it dirties the docs repo from a
|
|
35
|
-
* gate run nobody associates with it, and the dirt then rides into an unrelated
|
|
36
|
-
* commit or blocks a release. The row is not worth a human's attention in
|
|
37
|
-
* either direction, so the machine that writes it also lands it.
|
|
38
|
-
*
|
|
39
|
-
* The scoping, the refusals and the escape hatch live in `core/machine-commit`,
|
|
40
|
-
* which every machine written path commits through. This one fires only when
|
|
41
|
-
* the row's DATA changed, not on every gate, so the hooks it runs are affordable.
|
|
42
|
-
*/
|
|
43
|
-
function autoCommitRow(page: string, name: string): string {
|
|
44
|
-
const landed = machineCommit({
|
|
45
|
-
anchor: resolve(page, '..'),
|
|
46
|
-
message: `chore(machine): refresh local-projects row (${name})`,
|
|
47
|
-
paths: [page],
|
|
48
|
-
})
|
|
49
|
-
return landed ? ' + committed' : ''
|
|
50
|
-
}
|
|
51
|
-
|
|
52
43
|
const parseRow = (line: string): Row | null => {
|
|
53
44
|
const m = line.match(/^- (.+?) — `(.+?)` — shipkit (\S+) \((.+?)\) — last seen (\d{4}-\d{2}-\d{2})$/)
|
|
54
45
|
return m == null ? null : { name: m[1]!, root: m[2]!, version: m[3]!, mode: m[4]!, seen: m[5]! }
|
|
55
46
|
}
|
|
56
47
|
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
// read failure must never be rendered as the fact 'path')
|
|
48
|
+
// how this repo consumes the lib: `?` on an unreadable package.json (a read
|
|
49
|
+
// failure must never be rendered as the fact 'path')
|
|
60
50
|
function consumptionMode(root: string): string {
|
|
61
51
|
try {
|
|
62
52
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
@@ -70,47 +60,79 @@ function consumptionMode(root: string): string {
|
|
|
70
60
|
|
|
71
61
|
export type MotherhomeOutcome = { wrote: boolean; note?: string }
|
|
72
62
|
|
|
63
|
+
/** file this repo's row in the machine db. `wrote` says the filed row changed */
|
|
73
64
|
export function reportToMotherhome(root: string, config: RepoConfig, today: string = new Date().toISOString().slice(0, 10)): MotherhomeOutcome {
|
|
74
65
|
try {
|
|
75
66
|
if (config.docsBase == null) return { wrote: false }
|
|
76
67
|
const base = config.docsBase.startsWith('~/') ? join(homedir(), config.docsBase.slice(2)) : config.docsBase
|
|
77
|
-
// absolute local paths only
|
|
68
|
+
// absolute local paths only: a relative docsBase would resolve
|
|
78
69
|
// against CWD, a URL is a display prefix
|
|
79
70
|
if (!base.startsWith('/')) return { wrote: false }
|
|
80
71
|
const page = join(base, 'build/shipkit/local-projects.md')
|
|
81
72
|
if (!existsSync(page)) return { wrote: false }
|
|
82
|
-
// a
|
|
83
|
-
//
|
|
84
|
-
// side checkout, and the write lands as unexplained dirt in a tree
|
|
85
|
-
// somebody else is working in. The worktree's own runs are covered
|
|
86
|
-
// when its branch merges.
|
|
73
|
+
// a linked worktree is a temporary side checkout: its runs are
|
|
74
|
+
// covered when its branch merges
|
|
87
75
|
if (isLinkedWorktree(root)) return { wrote: false, note: `motherhome: skipped (${config.name} is a linked worktree — its row belongs to the main checkout)` }
|
|
88
|
-
|
|
89
76
|
const mode = consumptionMode(root)
|
|
90
77
|
// source-consumed code is not the published release its version
|
|
91
|
-
// string names
|
|
78
|
+
// string names: disclose it
|
|
92
79
|
const version = mode === 'npm' ? LIB_VERSION : `${LIB_VERSION}+src`
|
|
93
|
-
const me:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const b = lines.indexOf(BEGIN)
|
|
98
|
-
const e = lines.indexOf(END)
|
|
99
|
-
if (b === -1 || e === -1 || e < b) return { wrote: false, note: `WARN motherhome upsert skipped — markers missing/corrupted in ${page}` }
|
|
100
|
-
const rows = lines.slice(b + 1, e).map(parseRow).filter((row): row is Row => row != null)
|
|
101
|
-
const cur = rows.find((row) => row.root === me.root)
|
|
102
|
-
const dataSame = cur != null && cur.name === me.name && cur.version === me.version && cur.mode === me.mode
|
|
103
|
-
const fresh = cur != null && (Date.parse(today) - Date.parse(cur.seen)) / 86_400_000 < STALE_DAYS
|
|
104
|
-
if (dataSame && fresh) return { wrote: false }
|
|
105
|
-
// prune rows for repos that no longer exist at their recorded path
|
|
106
|
-
const next = [...rows.filter((row) => row.root !== me.root && existsSync(row.root)), me].sort((a, z) => a.name.localeCompare(z.name))
|
|
107
|
-
atomicWrite(page, [...lines.slice(0, b + 1), ...next.map(rowLine), ...lines.slice(e)].join('\n'))
|
|
108
|
-
const committed = autoCommitRow(page, me.name)
|
|
109
|
-
return { wrote: true, note: `motherhome: local-projects.md ${cur == null ? 'row added' : 'row refreshed'} (${me.name})${committed}` }
|
|
110
|
-
})
|
|
111
|
-
// lock lost to a racing reporter — self-heals on the next run
|
|
112
|
-
return r.ok ? r.value : { wrote: false }
|
|
80
|
+
const me: Filed = { page, name: config.name, root: resolve(root), version, mode, seen: today }
|
|
81
|
+
const key = `${ROW}${me.root}`
|
|
82
|
+
if (JSON.stringify(machineGet<Filed>(key)) === JSON.stringify(me)) return { wrote: false }
|
|
83
|
+
return machineSet(key, me) ? { wrote: true } : { wrote: false, note: 'WARN motherhome row not filed: the machine db could not be written' }
|
|
113
84
|
} catch (err) {
|
|
114
|
-
return { wrote: false, note: `WARN motherhome
|
|
85
|
+
return { wrote: false, note: `WARN motherhome row not filed (${String(err).slice(0, 120)})` }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** a page's rows once the filed ones are merged in: a filed row replaces the
|
|
90
|
+
* page's only when its data changed or the page's day is STALE_DAYS old,
|
|
91
|
+
* and a row whose repo path is gone is pruned */
|
|
92
|
+
function mergeRows(onPage: Row[], filed: Row[]): Row[] {
|
|
93
|
+
const by = new Map(onPage.map((r) => [r.root, r]))
|
|
94
|
+
for (const f of filed) {
|
|
95
|
+
const cur = by.get(f.root)
|
|
96
|
+
const same = cur != null && cur.name === f.name && cur.version === f.version && cur.mode === f.mode
|
|
97
|
+
const fresh = cur != null && (Date.parse(f.seen) - Date.parse(cur.seen)) / 86_400_000 < STALE_DAYS
|
|
98
|
+
if (!(same && fresh)) by.set(f.root, { name: f.name, root: f.root, version: f.version, mode: f.mode, seen: f.seen })
|
|
99
|
+
}
|
|
100
|
+
return [...by.values()].filter((r) => existsSync(r.root)).sort((a, z) => a.name.localeCompare(z.name) || a.root.localeCompare(z.root))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** render every filed row into its page and land each changed page as one
|
|
104
|
+
* commit, at most once per LAND_GAP_MS. Returns what to print */
|
|
105
|
+
export function landMotherhome(o: { now?: number; gapMs?: number } = {}): string[] {
|
|
106
|
+
const now = o.now ?? Date.now()
|
|
107
|
+
const notes: string[] = []
|
|
108
|
+
const filed = machineList<Filed>(ROW)
|
|
109
|
+
for (const f of filed) if (!existsSync(f.value.root)) machineDelete(f.name)
|
|
110
|
+
const pages = new Map<string, Row[]>()
|
|
111
|
+
for (const f of filed) if (existsSync(f.value.root)) pages.set(f.value.page, [...(pages.get(f.value.page) ?? []), f.value])
|
|
112
|
+
for (const [page, rows] of pages) {
|
|
113
|
+
try {
|
|
114
|
+
if (!existsSync(page)) continue
|
|
115
|
+
const last = machineGet<number>(`${LANDED}${page}`)
|
|
116
|
+
if (last != null && now - last < (o.gapMs ?? LAND_GAP_MS)) continue
|
|
117
|
+
const r = withFileLock(page, (): boolean => {
|
|
118
|
+
const text = readFileSync(page, 'utf8')
|
|
119
|
+
const lines = text.split('\n')
|
|
120
|
+
const b = lines.indexOf(BEGIN)
|
|
121
|
+
const e = lines.indexOf(END)
|
|
122
|
+
if (b === -1 || e === -1 || e < b) { notes.push(`WARN motherhome: markers missing or out of order in ${page}, rows not written`); return false }
|
|
123
|
+
const onPage = lines.slice(b + 1, e).map(parseRow).filter((row): row is Row => row != null)
|
|
124
|
+
const next = [...lines.slice(0, b + 1), ...mergeRows(onPage, rows).map(rowLine), ...lines.slice(e)].join('\n')
|
|
125
|
+
if (next === text) return false
|
|
126
|
+
atomicWrite(page, next)
|
|
127
|
+
return true
|
|
128
|
+
})
|
|
129
|
+
if (!r.ok || !r.value) continue
|
|
130
|
+
machineSet(`${LANDED}${page}`, now)
|
|
131
|
+
const landed = machineCommit({ anchor: resolve(page, '..'), message: 'chore(machine): refresh local-projects rows', paths: [page] })
|
|
132
|
+
notes.push(`motherhome: ${page} rows refreshed${landed ? ' + committed' : ', not committed'}`)
|
|
133
|
+
} catch (err) {
|
|
134
|
+
notes.push(`WARN motherhome: ${page} not refreshed (${String(err).slice(0, 120)})`)
|
|
135
|
+
}
|
|
115
136
|
}
|
|
137
|
+
return notes
|
|
116
138
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// what a repo declares about itself and its root unit, read from its config.
|
|
2
|
+
// Its own module, free of any fs import, so a check that asks it stays keyed
|
|
3
|
+
// on what it read.
|
|
4
|
+
import type { RepoConfig } from '../repo-config.ts'
|
|
5
|
+
|
|
6
|
+
/** the github visibility this repo declares (absent = 'private') */
|
|
7
|
+
export const declaredVisibility = (c: RepoConfig): 'private' | 'public' => c.visibility ?? 'private'
|
|
8
|
+
|
|
9
|
+
/** declared public: strangers read the tracked tree */
|
|
10
|
+
export const declaredPublic = (c: RepoConfig): boolean => c.visibility === 'public'
|
|
11
|
+
|
|
12
|
+
/** the root unit declares an npm target. Declared, never inferred from a
|
|
13
|
+
* missing `private` flag, so an app that forgot the flag is still an app */
|
|
14
|
+
export const rootPublishesNpm = (c: RepoConfig): boolean => c.release?.npm === true
|
package/src/core/retention.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// NOTHING GROWS UNBOUND: every store shipkit writes declares a cap here (an
|
|
2
2
|
// age, a count, a size), and one sweep enforces all of them. The daemon runs
|
|
3
|
-
// it on boot and hourly for every repo it loads and for the machine;
|
|
4
|
-
//
|
|
5
|
-
// is loud. Each store's sweep lives beside the code that writes it, so a gate
|
|
3
|
+
// it on boot and hourly for every repo it loads and for the machine; the
|
|
4
|
+
// house repo's one `stores-under-cap` check fails when a store is past its
|
|
5
|
+
// cap, so a sweep that stopped running is loud. Each store's sweep lives beside the code that writes it, so a gate
|
|
6
6
|
// on a machine with no daemon still keeps its own stores in bounds.
|
|
7
7
|
//
|
|
8
8
|
// The one constraint a reader breaks first: a sweep never removes what a live
|
|
@@ -45,8 +45,8 @@ export type Store = {
|
|
|
45
45
|
sweep(now: number): void | Promise<void>
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/** how far past its cap a store may drift between two sweeps before
|
|
49
|
-
* calls it over: the daemon sweeps hourly, and gates write in between */
|
|
48
|
+
/** how far past its cap a store may drift between two sweeps before the
|
|
49
|
+
* house check calls it over: the daemon sweeps hourly, and gates write in between */
|
|
50
50
|
export const SLACK = { factor: 2, ageMs: 2 * 60 * 60 * 1000 }
|
|
51
51
|
|
|
52
52
|
/** why a store is past its cap (with the drift a sweep interval allows), or
|
|
@@ -59,6 +59,15 @@ export function overCap(use: Use, cap: Cap, now: number, slack = SLACK): string
|
|
|
59
59
|
return null
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** a cap in words: `200 entries, 30d, 5.0 MB a file` */
|
|
63
|
+
export const capWords = (s: Store): string =>
|
|
64
|
+
[
|
|
65
|
+
s.cap.maxCount != null ? `${s.cap.maxCount} entries` : null,
|
|
66
|
+
s.cap.maxAgeMs != null ? fmtAge(s.cap.maxAgeMs) : null,
|
|
67
|
+
s.cap.maxFileBytes != null ? `${fmtBytes(s.cap.maxFileBytes)} a file` : null,
|
|
68
|
+
s.cap.maxBytes != null ? fmtBytes(s.cap.maxBytes) : null,
|
|
69
|
+
].filter((x) => x != null).join(', ')
|
|
70
|
+
|
|
62
71
|
export const fmtBytes = (n: number): string => (n >= 1 << 20 ? `${(n / (1 << 20)).toFixed(1)} MB` : n >= 1024 ? `${Math.round(n / 1024)} KB` : `${n} B`)
|
|
63
72
|
export const fmtAge = (ms: number): string => (ms >= 86_400_000 ? `${Math.round(ms / 86_400_000)}d` : ms >= 3_600_000 ? `${Math.round(ms / 3_600_000)}h` : `${Math.round(ms / 60_000)}m`)
|
|
64
73
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// built-in check ids that no longer exist, and where their question went. An
|
|
2
|
+
// id is never reused, so a doc or a changelog may still name one, and
|
|
3
|
+
// discovery refuses a built-in that takes one again.
|
|
4
|
+
export const RETIRED_CHECK_IDS: Readonly<Record<string, string>> = {
|
|
5
|
+
SK021: "the gh login is a machine fact: the house repo's github-login check runs src/core/gh-login.ts",
|
|
6
|
+
SK023: "the store caps are the machine daemon's health: the house repo's stores-under-cap check",
|
|
7
|
+
}
|
package/src/core/run-check.ts
CHANGED
|
@@ -37,6 +37,9 @@ export type CheckOpts = {
|
|
|
37
37
|
status?: boolean
|
|
38
38
|
/** line sink — commands pass their context stream; defaults to console */
|
|
39
39
|
print?: (line: string) => void
|
|
40
|
+
/** where the lines nobody acts on go: the parked list and the green
|
|
41
|
+
* summary. `shipkit ci` shows them only under --verbose. Default: print */
|
|
42
|
+
detail?: (line: string) => void
|
|
40
43
|
/** per-CHECK completion tap: the real status, whether it replayed, what it
|
|
41
44
|
* cost. The demotion of a non-blocking failure happens in the LINES (a
|
|
42
45
|
* FAIL is rewritten to a warn before anything sees it), so `status` here
|
|
@@ -84,11 +87,8 @@ export type CheckOpts = {
|
|
|
84
87
|
/** the gate binary that own ledger is keyed on (default: the one running
|
|
85
88
|
* it). Ignored when `ledger` is passed, which carries its own. */
|
|
86
89
|
cliEntry?: string
|
|
87
|
-
/**
|
|
88
|
-
*
|
|
89
|
-
* `wiki/`, so writing it before the post-run re-hash makes the gate look
|
|
90
|
-
* like an agent editing the tree and throws away every result the run just
|
|
91
|
-
* measured. */
|
|
90
|
+
/** file the motherhome row in the machine db (core/motherhome.ts). runCi
|
|
91
|
+
* passes false and does it ITSELF, once, after its verdict. */
|
|
92
92
|
reportHome?: boolean
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -197,10 +197,11 @@ export async function runCheckPass(root: string, o: CheckOpts = {}): Promise<Che
|
|
|
197
197
|
bail: o.bail,
|
|
198
198
|
color: o.color,
|
|
199
199
|
print: sink,
|
|
200
|
+
detail: o.detail,
|
|
200
201
|
onCheck: (e) => { collected.push(...e.lines); o.onCheck?.(e) },
|
|
201
202
|
})
|
|
202
203
|
const lines: Line[] = []
|
|
203
|
-
const say = (l: Line): void => { lines.push(l);
|
|
204
|
+
const say = (l: Line, to: (line: string) => void = print): void => { lines.push(l); to(formatLine(l, false)) }
|
|
204
205
|
// a declaration in a shape the check file replaced is reported by check
|
|
205
206
|
// SK016, which also carries the fixer that rewrites it: one owner, one
|
|
206
207
|
// line, one command, rather than a warning per surface.
|
|
@@ -210,7 +211,7 @@ export async function runCheckPass(root: string, o: CheckOpts = {}): Promise<Che
|
|
|
210
211
|
// page just replaced
|
|
211
212
|
if (summary.parked.length > 0) {
|
|
212
213
|
const head = summary.parked.slice(0, 8).join(', ')
|
|
213
|
-
say({ kind: 'skip', text: `${summary.parked.length} check(s) not in this run (health only, release only at the quick tier, or a skipped group): ${head}${summary.parked.length > 8 ? `, and ${summary.parked.length - 8} more` : ''}` })
|
|
214
|
+
say({ kind: 'skip', text: `${summary.parked.length} check(s) not in this run (health only, release only at the quick tier, or a skipped group): ${head}${summary.parked.length > 8 ? `, and ${summary.parked.length - 8} more` : ''}` }, o.detail ?? print)
|
|
214
215
|
}
|
|
215
216
|
// the survey dies with the run that made it, and only the run that made it
|
|
216
217
|
if (inherited == null || inherited === '') for (const f of [sharePath, `${sharePath}.lock`]) { try { rmSync(f, { force: true }) } catch {} }
|
package/src/core/run-ci.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { passScope, runCheckPass, type CheckOpts } from './run-check.ts'
|
|
|
12
12
|
import { poolLines } from './slots.ts'
|
|
13
13
|
import { runFixPass } from './run-fixers.ts'
|
|
14
14
|
import { formatLine } from './wire.ts'
|
|
15
|
+
import { outcomeCounts } from './outcome-words.ts'
|
|
15
16
|
import { writeStatus } from './write-status.ts'
|
|
16
17
|
import { allTasks, composeChecks, envTier, FULL_TESTS_ENV, type TestTier } from '../ledger/gate-checks.ts'
|
|
17
18
|
import { CHECK_TABLE_FILE, writeCheckTable } from '../ledger/check-table.ts'
|
|
@@ -85,6 +86,10 @@ export type CiOpts = {
|
|
|
85
86
|
tests?: TestTier
|
|
86
87
|
/** who started this run, as the run record files it */
|
|
87
88
|
invokedBy?: InvokedBy
|
|
89
|
+
/** print every line, the ones nobody acts on included: the clean fix
|
|
90
|
+
* pass, the pool, the tier, the cache declarations, the parked checks,
|
|
91
|
+
* the cache counts. STATUS.md always holds them all */
|
|
92
|
+
verbose?: boolean
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
export type { TestTier } from '../ledger/gate-checks.ts'
|
|
@@ -107,10 +112,26 @@ function ciScope(o: CiOpts): { command: string; fullGate: boolean } {
|
|
|
107
112
|
return { command: parts.length === 0 ? 'shipkit ci' : `shipkit ci ${parts.join(' ')}`, fullGate: narrowing.length === 0 }
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
/** `ok: 12 checks green`, or which ones warned. A whole gate adds its tier
|
|
116
|
+
* and where its answers came from */
|
|
117
|
+
export function verdictLine(results: CheckResult[], whole: { tier: TestTier; cache: { hit: number; miss: number; attached: number; uncacheable: number } | null } | null): string | null {
|
|
118
|
+
const ran = results.filter((r) => r.status !== 'skip').length
|
|
119
|
+
if (ran === 0) return null
|
|
120
|
+
const warned = results.filter((r) => r.status === 'warn' || (r.status === 'fail' && r.advisory)).map((r) => r.id)
|
|
121
|
+
const head = warned.length === 0 ? `${ran} check${ran === 1 ? '' : 's'} green` : `${ran} check${ran === 1 ? '' : 's'} passed, ${warned.length} with a warning: ${warned.join(', ')}`
|
|
122
|
+
if (whole == null) return `ok: ${head}`
|
|
123
|
+
const c = whole.cache
|
|
124
|
+
const cache = c == null ? '' : `; ${outcomeCounts({ cached: c.hit, ran: c.miss, attached: c.attached, neverCached: c.uncacheable })}`
|
|
125
|
+
return `ok: ${head}; ${whole.tier} tier${cache}`
|
|
126
|
+
}
|
|
127
|
+
|
|
110
128
|
export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
111
129
|
const print = o.print ?? ((l: string) => console.log(l))
|
|
112
130
|
const collected: string[] = []
|
|
113
131
|
const sink = (l: string): void => { collected.push(l); print(l) }
|
|
132
|
+
// what the reader never acts on: kept for STATUS.md and --verbose, never
|
|
133
|
+
// printed on a default run, where every line costs a reader tokens
|
|
134
|
+
const detail = (l: string): void => { collected.push(l); if (o.verbose === true) print(l) }
|
|
114
135
|
let c = o.config ?? (await loadRepoConfig(root))
|
|
115
136
|
// mechanical failures are not information, so they never cost a round trip
|
|
116
137
|
let fixFailed = false
|
|
@@ -123,7 +144,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
123
144
|
// back as a test failure.
|
|
124
145
|
const narrowed = (o.onlyIds ?? []).length > 0
|
|
125
146
|
if (o.fix !== false) {
|
|
126
|
-
const pass = await runFixPass(root, c, { rules: !narrowed && o.fixRules !== false, armHooks: !narrowed && o.armHooks === true, print: sink })
|
|
147
|
+
const pass = await runFixPass(root, c, { rules: !narrowed && o.fixRules !== false, armHooks: !narrowed && o.armHooks === true, print: sink, detail })
|
|
127
148
|
fixFailed = pass.failed
|
|
128
149
|
// a fixer that rewrote the config or moved a check file leaves the
|
|
129
150
|
// config above describing a repo that is gone, so the gate reads the
|
|
@@ -134,20 +155,22 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
134
155
|
return 1
|
|
135
156
|
}
|
|
136
157
|
}
|
|
137
|
-
} else
|
|
158
|
+
} else detail('skip: fix pass (--no-fix)')
|
|
138
159
|
const tier: TestTier = o.tests ?? envTier()
|
|
139
|
-
// announced BEFORE the run
|
|
140
|
-
// tier it was asking for
|
|
141
|
-
|
|
160
|
+
// announced BEFORE the run (under --verbose, and in every release log),
|
|
161
|
+
// so a log that died mid-way still says which tier it was asking for
|
|
162
|
+
detail(tier === 'full'
|
|
142
163
|
? `ok: test tier FULL — every suite, the slow ones included (${FULL_TESTS_ENV}=1)`
|
|
143
164
|
: `skip: full test tier (quick run — \`shipkit ci --full\` or ${FULL_TESTS_ENV}=1 includes it)`)
|
|
144
|
-
for (const l of poolLines()) sink(l)
|
|
165
|
+
for (const l of poolLines()) (l.startsWith('WARN') ? sink : detail)(l)
|
|
145
166
|
// a floor slot nothing could fill is VISIBLE — a thin gate must never look
|
|
146
167
|
// like a full one (the fresh-repo floor gap)
|
|
147
168
|
const floor = await allTasks(c, root)
|
|
148
|
-
|
|
169
|
+
// a narrowed run says nothing about the slots it never asked about
|
|
170
|
+
const asked = (id: string): boolean => !narrowed || (o.onlyIds ?? []).includes(id)
|
|
171
|
+
for (const n of floor.notes ?? []) (narrowed ? detail : sink)(n)
|
|
149
172
|
for (const u of floor.unfilled) {
|
|
150
|
-
sink(`skip: ${u.id} (${u.reason})`)
|
|
173
|
+
(asked(u.id) ? sink : detail)(`skip: ${u.id} (${u.reason})`)
|
|
151
174
|
// asked for BY NAME and unfillable: this is where an agent lands after
|
|
152
175
|
// the harness refused it the raw runner, so the line says what to add
|
|
153
176
|
// rather than leaving it with `unknown check id` and no next move
|
|
@@ -210,7 +233,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
210
233
|
// visible as a shrinking undeclared one.
|
|
211
234
|
const decided = reg.checks - reg.burnDown.length - reg.declared
|
|
212
235
|
if (reg.burnDown.length > 0 || decided > 0)
|
|
213
|
-
|
|
236
|
+
detail(`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)`)
|
|
214
237
|
if (ledger.layer.failure != null)
|
|
215
238
|
sink(`WARN the git scan failed, so NOTHING comes from the cache and nothing is stored this run — ${ledger.layer.failure}`)
|
|
216
239
|
} catch (e) {
|
|
@@ -225,13 +248,14 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
225
248
|
onCheck: o.onCheck,
|
|
226
249
|
reportHome: false,
|
|
227
250
|
print: sink,
|
|
251
|
+
detail,
|
|
228
252
|
})
|
|
229
253
|
// what the suite ACTUALLY ran, from its own headline. A flag that was set
|
|
230
254
|
// proves the intent; the count proves the tests
|
|
231
255
|
// proves the intent; the count proves the tests. Every test check counts,
|
|
232
256
|
// one per unit
|
|
233
257
|
const counts = run.results.map((r) => /Ran (\d+) tests?/.exec(r.lastLine)?.[1]).filter((n) => n != null).map(Number)
|
|
234
|
-
if (counts.length > 0)
|
|
258
|
+
if (counts.length > 0) detail(`ok: test tier ${tier}: ${counts.reduce((a, b) => a + b, 0)} tests ran${counts.length > 1 ? ` in ${counts.length} test checks` : ''}`)
|
|
235
259
|
for (const u of run.undeclared) {
|
|
236
260
|
sink(`FAIL: ${u} does not declare whether it blocks`)
|
|
237
261
|
sink(` fix: add \`blocking: true\` (about this repo) or \`false\` (about the world) to that check`)
|
|
@@ -248,19 +272,26 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
248
272
|
advisoryFatal = true
|
|
249
273
|
sink(`FAIL: ${run.advisoryFailures.length} non-blocking check(s) failing and release.requireHealthy is on: ${run.advisoryFailures.join(', ')}`)
|
|
250
274
|
sink(' fix: shipkit check')
|
|
251
|
-
} else
|
|
275
|
+
} else detail(`WARN ${run.advisoryFailures.length} non-blocking check(s) failing, about the world rather than this repo: ${run.advisoryFailures.join(', ')}`)
|
|
252
276
|
}
|
|
253
277
|
// the post-run re-hash, then the store. NOTHING was written while the gate
|
|
254
278
|
// ran: a check whose inputs moved under it answered about a tree that no
|
|
255
279
|
// longer exists, and this is where that result is thrown away instead of
|
|
256
280
|
// stored under a key that describes the file as it was before the edit.
|
|
257
|
-
|
|
281
|
+
// the counts line folds into the verdict; what else it says (a store
|
|
282
|
+
// that failed, answers thrown away) is printed as it is
|
|
283
|
+
if (ledger != null) for (const [i, l] of ledger.header(ledger.commit()).entries()) (i === 0 ? detail : sink)(formatLine(l, false))
|
|
258
284
|
// one write for the whole gate, after everything has finished
|
|
259
285
|
for (const l of runLog.flush()) sink(formatLine(l, false))
|
|
260
286
|
const code = run.blocked || fixFailed || registrationFailed || advisoryFatal || run.undeclared.length > 0 || run.unknownWhen.length > 0 || run.unknownBudgets.length > 0 ? 1 : 0
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
287
|
+
// THE VERDICT, one line and only on a pass: a red run already said so in
|
|
288
|
+
// its FAIL lines. The whole gate adds the tier and the cache counts, which
|
|
289
|
+
// a narrowed run asked no question about
|
|
290
|
+
const verdict = code === 0 && !run.interrupted ? verdictLine(run.results, scope.fullGate ? { tier, cache: ledger?.counts ?? null } : null) : null
|
|
291
|
+
if (verdict != null) sink(verdict)
|
|
292
|
+
// the motherhome row, filed in the machine db and landed in the docsBase
|
|
293
|
+
// page by the daemon (core/motherhome.ts): never affects the gate, never
|
|
294
|
+
// writes in another repo, and a frozen run files nothing
|
|
264
295
|
if (o.frozen !== true) {
|
|
265
296
|
const mh = reportToMotherhome(root, c)
|
|
266
297
|
if (mh.note != null) print(mh.note)
|