rvlib-shipkit 0.38.1 → 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 +367 -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 +209 -15
- 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/locked-write.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// never corrupts, never blocks.
|
|
6
6
|
import { linkSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from 'fs'
|
|
7
7
|
import { basename, dirname, join } from 'path'
|
|
8
|
+
import { LAYOUT } from './layout.ts'
|
|
9
|
+
import { announceInstance, instanceGone, thisInstance } from './instance.ts'
|
|
8
10
|
|
|
9
11
|
// a writer that died without unlocking must not wedge the file forever
|
|
10
12
|
const LOCK_STALE_MS = 120_000
|
|
@@ -60,9 +62,11 @@ export function atomicWrite(path: string, content: string): void {
|
|
|
60
62
|
// grabbing.
|
|
61
63
|
//
|
|
62
64
|
// Bounded in every direction, because a gate that hangs is worse than a gate
|
|
63
|
-
// that overlaps. A holder is stale when its
|
|
64
|
-
//
|
|
65
|
-
//
|
|
65
|
+
// that overlaps. A holder is stale when its IMAGE is not running (its pid is
|
|
66
|
+
// gone, or the pid runs another image after a restart in place:
|
|
67
|
+
// core/instance.ts) OR when the lock has not been touched for LOCK_STALE_MS,
|
|
68
|
+
// so a pid reused by a long lived process cannot wedge a repo forever. A
|
|
69
|
+
// holder that is genuinely still
|
|
66
70
|
// working HEARTBEATS the lock while it holds it, so the age rule only ever
|
|
67
71
|
// fires on a run that is gone or wedged. A waiter gives up after maxWaitMs and
|
|
68
72
|
// says so by name, path and escape hatch. Every line it prints carries a
|
|
@@ -78,37 +82,42 @@ const LEASE_HEARTBEAT_MS = 20_000
|
|
|
78
82
|
* SIGTERMed mid wait with no cause on screen */
|
|
79
83
|
const LEASE_MAX_WAIT_MS = 60_000
|
|
80
84
|
|
|
81
|
-
type Holder = { pid: number; at: number; label: string }
|
|
85
|
+
type Holder = { pid: number; nonce?: string; at: number; label: string; meta?: Record<string, string> }
|
|
82
86
|
|
|
83
87
|
let leaseSeq = 0
|
|
84
88
|
|
|
85
89
|
const readHolder = (lockPath: string): Holder | null => {
|
|
86
90
|
try {
|
|
87
91
|
const h = JSON.parse(readFileSync(lockPath, 'utf8')) as Partial<Holder>
|
|
88
|
-
|
|
92
|
+
if (typeof h.pid !== 'number') return null
|
|
93
|
+
return {
|
|
94
|
+
pid: h.pid,
|
|
95
|
+
...(typeof h.nonce === 'string' ? { nonce: h.nonce } : {}),
|
|
96
|
+
at: typeof h.at === 'number' ? h.at : 0,
|
|
97
|
+
label: String(h.label ?? '?'),
|
|
98
|
+
...(h.meta != null && typeof h.meta === 'object' ? { meta: h.meta } : {}),
|
|
99
|
+
}
|
|
89
100
|
} catch { return null }
|
|
90
101
|
}
|
|
91
102
|
|
|
92
|
-
/**
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** publish a COMPLETE lock under a name that did not exist a moment ago */
|
|
100
|
-
const grabLease = (lockPath: string, label: string): boolean => {
|
|
103
|
+
/** publish a COMPLETE lock under a name that did not exist a moment ago.
|
|
104
|
+
* 'held' = the name exists, which is the only answer worth waiting on. Any
|
|
105
|
+
* other failure (the dir gone or not writable) comes back as its message:
|
|
106
|
+
* nobody holds that lock, and retrying cannot make it appear. */
|
|
107
|
+
const grabLease = (lockPath: string, label: string, meta?: Record<string, string>): 'taken' | 'held' | { error: string } => {
|
|
101
108
|
const tmp = `${lockPath}.tmp-${process.pid}-${leaseSeq++}`
|
|
109
|
+
const me = announceInstance()
|
|
102
110
|
try {
|
|
103
|
-
writeFileSync(tmp, JSON.stringify({ pid:
|
|
111
|
+
writeFileSync(tmp, JSON.stringify({ pid: me.pid, nonce: me.nonce, at: Date.now(), label, meta } satisfies Holder))
|
|
104
112
|
linkSync(tmp, lockPath)
|
|
105
|
-
return
|
|
106
|
-
} catch {
|
|
107
|
-
|
|
113
|
+
return 'taken'
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return (e as NodeJS.ErrnoException).code === 'EEXIST' ? 'held' : { error: (e as Error).message }
|
|
116
|
+
} finally { rmSync(tmp, { force: true }) }
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
const sameHolder = (a: Holder | null, b: Holder | null): boolean =>
|
|
111
|
-
a == null ? b == null : b != null && a.pid === b.pid && a.at === b.at
|
|
120
|
+
a == null ? b == null : b != null && a.pid === b.pid && a.nonce === b.nonce && a.at === b.at
|
|
112
121
|
|
|
113
122
|
/** move the lock aside, and ONLY the lock we judged dead. Exactly one racer
|
|
114
123
|
* can win the rename; the rest see ENOENT and go back to grabbing.
|
|
@@ -137,16 +146,78 @@ const stealLease = (lockPath: string, expect: Holder | null): boolean => {
|
|
|
137
146
|
* lease two others believe they hold */
|
|
138
147
|
const releaseLease = (lockPath: string): void => {
|
|
139
148
|
const h = readHolder(lockPath)
|
|
140
|
-
|
|
149
|
+
const me = thisInstance()
|
|
150
|
+
if (h == null || (h.pid === me.pid && (h.nonce == null || h.nonce === me.nonce))) rmSync(lockPath, { force: true })
|
|
141
151
|
}
|
|
142
152
|
|
|
143
|
-
|
|
153
|
+
/** why the holder no longer holds, or null while it does: its image is gone
|
|
154
|
+
* (core/instance.ts), or it is wedged (nothing has touched it for the stale
|
|
155
|
+
* window, and a live holder heartbeats), or a body no reader can make sense
|
|
156
|
+
* of is old enough not to be a write in flight */
|
|
157
|
+
const deadBecause = (holder: Holder | null, ageMs: number): string | null => {
|
|
158
|
+
if (holder == null) return ageMs > 2_000 ? `an unreadable lock, untouched ${Math.round(ageMs / 1000)}s` : null
|
|
159
|
+
const gone = instanceGone(holder)
|
|
160
|
+
if (gone != null) return gone
|
|
161
|
+
return ageMs > LOCK_STALE_MS ? `pid ${holder.pid}, untouched ${Math.round(ageMs / 1000)}s` : null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** a phase that legitimately outlives the stale window must keep proving it
|
|
165
|
+
* is working, or the age rule steals from it. unref'd, so a held lease never
|
|
166
|
+
* keeps a process alive on its own */
|
|
167
|
+
const holdLease = (lockPath: string): { release: () => void } => {
|
|
168
|
+
const beat = setInterval(() => { try { utimesSync(lockPath, new Date(), new Date()) } catch { /* stolen or gone: release sorts it out */ } }, LEASE_HEARTBEAT_MS)
|
|
169
|
+
beat.unref()
|
|
170
|
+
return { release: () => { clearInterval(beat); releaseLease(lockPath) } }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** ONE non blocking attempt at `<path>.lock`: taken (a holder that is gone or
|
|
174
|
+
* wedged is stolen first), or null while a live holder has it. Held the same
|
|
175
|
+
* way withMutateLease holds it, heartbeat included. `meta` rides in the lock
|
|
176
|
+
* so a reader can say who holds it. */
|
|
177
|
+
export function tryLease(path: string, label: string, meta?: Record<string, string>): { release: () => void } | null {
|
|
178
|
+
const lockPath = `${path}.lock`
|
|
179
|
+
for (let round = 0; round < 3; round++) {
|
|
180
|
+
const grab = grabLease(lockPath, label, meta)
|
|
181
|
+
if (grab === 'taken') return holdLease(lockPath)
|
|
182
|
+
if (grab !== 'held') return null
|
|
183
|
+
const holder = readHolder(lockPath)
|
|
184
|
+
let ageMs = 0
|
|
185
|
+
try { ageMs = Date.now() - statSync(lockPath).mtimeMs } catch { continue }
|
|
186
|
+
if (deadBecause(holder, ageMs) == null) return null
|
|
187
|
+
stealLease(lockPath, holder)
|
|
188
|
+
}
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** who holds `<path>.lock` right now, or null when nobody live does */
|
|
193
|
+
export function leaseHolder(path: string): { pid: number; at: number; label: string; meta: Record<string, string> } | null {
|
|
194
|
+
const lockPath = `${path}.lock`
|
|
195
|
+
const h = readHolder(lockPath)
|
|
196
|
+
let ageMs = 0
|
|
197
|
+
try { ageMs = Date.now() - statSync(lockPath).mtimeMs } catch { return null }
|
|
198
|
+
return h == null || deadBecause(h, ageMs) != null ? null : { pid: h.pid, at: h.at, label: h.label, meta: h.meta ?? {} }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type LeaseOutcome<T> = { ok: true; value: T; waitedMs: number } | { ok: false; warn: string; aborted?: true }
|
|
144
202
|
|
|
145
203
|
/** hold `<path>.lock` for the duration of fn. Blocking, stolen from a holder
|
|
146
204
|
* that is gone or wedged, refused (never hung) past maxWaitMs. */
|
|
147
205
|
export async function withMutateLease<T>(
|
|
148
206
|
path: string,
|
|
149
|
-
o: {
|
|
207
|
+
o: {
|
|
208
|
+
label: string
|
|
209
|
+
print?: (line: string) => void
|
|
210
|
+
maxWaitMs?: number
|
|
211
|
+
escape?: string
|
|
212
|
+
announceMs?: number
|
|
213
|
+
pollMs?: number
|
|
214
|
+
/** called once, the first time a LIVE holder makes this caller wait */
|
|
215
|
+
onWait?: (holder: { pid: number; label: string }) => void
|
|
216
|
+
/** called on every steal this caller wins, with the dead holder */
|
|
217
|
+
onSteal?: (holder: { pid: number; nonce?: string }) => void
|
|
218
|
+
/** polled while waiting: true stops the wait, and fn never runs */
|
|
219
|
+
abort?: () => boolean
|
|
220
|
+
},
|
|
150
221
|
fn: () => T | Promise<T>,
|
|
151
222
|
): Promise<LeaseOutcome<T>> {
|
|
152
223
|
const lockPath = `${path}.lock`
|
|
@@ -156,23 +227,28 @@ export async function withMutateLease<T>(
|
|
|
156
227
|
// the first announcement is due at LEASE_ANNOUNCE_MS, every later one a
|
|
157
228
|
// re-announce interval after it
|
|
158
229
|
let announced = -LEASE_REANNOUNCE_MS
|
|
230
|
+
let heard = false
|
|
159
231
|
mkdirSync(dirname(lockPath), { recursive: true })
|
|
160
232
|
|
|
161
233
|
for (;;) {
|
|
162
|
-
|
|
234
|
+
const grab = grabLease(lockPath, o.label)
|
|
235
|
+
if (grab === 'taken') break
|
|
236
|
+
if (grab !== 'held') return { ok: false, warn: `${o.label} lease: cannot create ${lockPath}: ${grab.error}` }
|
|
163
237
|
const holder = readHolder(lockPath)
|
|
164
238
|
let ageMs = 0
|
|
165
239
|
// the lock vanished between grab and stat: the holder just finished,
|
|
166
240
|
// so go straight back and take it
|
|
167
241
|
try { ageMs = Date.now() - statSync(lockPath).mtimeMs } catch { continue }
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
242
|
+
const dead = deadBecause(holder, ageMs)
|
|
243
|
+
if (dead != null) {
|
|
244
|
+
if (stealLease(lockPath, holder)) {
|
|
245
|
+
print(`ok: stole the ${o.label} lease from a holder that is gone (${dead}): a run died mid phase`)
|
|
246
|
+
if (holder != null) o.onSteal?.(holder)
|
|
247
|
+
}
|
|
174
248
|
continue
|
|
175
249
|
}
|
|
250
|
+
if (o.abort?.() === true) return { ok: false, warn: `${o.label} lease: stopped waiting`, aborted: true }
|
|
251
|
+
if (!heard && holder != null) { heard = true; o.onWait?.(holder) }
|
|
176
252
|
const waited = Date.now() - start
|
|
177
253
|
if (waited > maxWait)
|
|
178
254
|
return {
|
|
@@ -183,26 +259,21 @@ export async function withMutateLease<T>(
|
|
|
183
259
|
announced = waited
|
|
184
260
|
print(`ok: waiting ${Math.round(waited / 1000)}s for the ${o.label} lease — ${holder == null ? 'an unreadable lock holds it' : `pid ${holder.pid} holds it (${holder.label})`}, lock ${lockPath}`)
|
|
185
261
|
}
|
|
186
|
-
await Bun.sleep(LEASE_POLL_MS)
|
|
262
|
+
await Bun.sleep(o.pollMs ?? LEASE_POLL_MS)
|
|
187
263
|
}
|
|
188
264
|
|
|
189
|
-
|
|
190
|
-
// is working, or the age rule steals from it. unref'd, so a held lease
|
|
191
|
-
// never keeps a process alive on its own
|
|
192
|
-
const beat = setInterval(() => { try { utimesSync(lockPath, new Date(), new Date()) } catch { /* stolen or gone: the finally sorts it out */ } }, LEASE_HEARTBEAT_MS)
|
|
193
|
-
beat.unref()
|
|
265
|
+
const held = holdLease(lockPath)
|
|
194
266
|
try {
|
|
195
267
|
return { ok: true, value: await fn(), waitedMs: Date.now() - start }
|
|
196
268
|
} finally {
|
|
197
|
-
|
|
198
|
-
releaseLease(lockPath)
|
|
269
|
+
held.release()
|
|
199
270
|
}
|
|
200
271
|
}
|
|
201
272
|
|
|
202
|
-
/** where a repo's mutate leases live. Under the gitignored
|
|
273
|
+
/** where a repo's mutate leases live. Under the gitignored scratch dir so a lease
|
|
203
274
|
* file can never enter a commit, and per WORKTREE, because the phases it
|
|
204
275
|
* guards rewrite worktree-local files. The NAME is the concern: two phases
|
|
205
276
|
* that cannot corrupt each other take different leases, so `asks` never
|
|
206
277
|
* blocks a `fix`. `git config` is the one shared-across-worktrees write and
|
|
207
278
|
* carries its own retry (hooks/run-hooks.ts). */
|
|
208
|
-
export const mutateLease = (root: string, name: string): string => join(root,
|
|
279
|
+
export const mutateLease = (root: string, name: string): string => join(root, LAYOUT.tmp, `shipkit-${name}`)
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
// not worth a red gate. `SHIPKIT_NO_AUTOCOMMIT=1` turns it off.
|
|
19
19
|
import { existsSync } from 'fs'
|
|
20
20
|
import { join, resolve } from 'path'
|
|
21
|
+
import { gitRoot } from './git-root.ts'
|
|
21
22
|
import { nestedConfigRoot } from './nesting.ts'
|
|
23
|
+
import { gitBin } from './git-bin.ts'
|
|
22
24
|
|
|
23
25
|
const git = (cwd: string, args: string[]): { ok: boolean; out: string } => {
|
|
24
|
-
const r = Bun.spawnSync([
|
|
26
|
+
const r = Bun.spawnSync([gitBin(), ...args], { cwd })
|
|
25
27
|
return { ok: r.exitCode === 0, out: `${r.stdout.toString()}${r.stderr.toString()}`.trim() }
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -38,9 +40,8 @@ export function machineCommit(o: MachineCommit): boolean {
|
|
|
38
40
|
if (process.env['SHIPKIT_NO_AUTOCOMMIT'] === '1') return false
|
|
39
41
|
if (o.paths.length === 0) return false
|
|
40
42
|
try {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
const root = top.out
|
|
43
|
+
const root = gitRoot(resolve(o.anchor))?.top
|
|
44
|
+
if (root == null) return false // not a git repo, nothing to land
|
|
44
45
|
// an anchor inside a nested shipkit repo would land on the parent's branch
|
|
45
46
|
if (nestedConfigRoot(resolve(o.anchor), root) != null) return false
|
|
46
47
|
// a detached HEAD or a repo with no commit yet has nowhere sane to land
|