rvlib-shipkit 0.35.7 → 0.35.8
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 +7 -1
- package/package.json +1 -1
- package/src/core/run-check.ts +17 -6
- package/src/core/run-ci.ts +12 -3
- package/src/core/task-runner.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.8
|
|
4
|
+
|
|
5
|
+
- **`release.requireHealthy` had become a no-op for exactly the checks it exists for.** `runCi` hardcoded `gateOnly: true` on its checks call, and `requireHealthy` is implemented one layer up as `onlyBlockingChecks: false` plus `failOnAdvisory: true`. The gate filter runs before severity is considered, so a repo that asked for a missing publish credential to stop its release had that request silently dropped the moment its cred check declared `gate = false`. Reproduced through `runCi` with the exact option shape the release path builds, against a check exiting 1 with a FAIL line: the line never appeared and the gate returned 0. `gateOnly` now yields to `failOnAdvisory`, so `requireHealthy` re-arms every opted-out check. Two tests, one per option shape.
|
|
6
|
+
- **`--only <check>` ran the check and then exited 1 saying it did not exist.** `selectTasks` validates the flag against the TASK list alone, so naming a discovered check or a rule ran it and still printed `unknown task id`. The flag addresses three surfaces, so the task runner is now handed the ids owned by the other two (rule ids, rule titles, check names, check paths). This is what makes the opt-out safe to use: a check taken off the gate stays reachable by name.
|
|
7
|
+
- The `run-check.ts` comment on the release-survey share path no longer describes a pre-warm `runCi` deleted, and says who the single owner is.
|
|
8
|
+
|
|
3
9
|
## 0.35.7
|
|
4
10
|
|
|
5
11
|
- **`shipkit ci` no longer pre-warms a release survey nothing in the gate reads.** `runCi` started `collectReports` next to the tasks and awaited it before the checks, to hide ~3s of npm registry and `gh` network behind the task DAG. Its only consumer was ever the lib-drift probe, which asks what is PUBLISHED and therefore belongs to `shipkit releases` and the bump gate rather than to a gate that asks whether this change is good. Measured on a repo whose drift checks had left the gate: warm no-op 4.3s with the pre-warm, 1.2s without. Deleted rather than made conditional, because nothing is left to condition it on and a second mechanism guessing which check wants a survey is exactly the source-sniffing this scanner already retired. `collectChecks` creates, shares and cleans up the share path when it was not handed one, so the checks that DO want a survey still pay for exactly one between them, and `shipkit check` is unchanged. Honest cost: a repo that keeps a lib-drift check inside its gate loses the overlap and is up to 3s slower there.
|
|
6
|
-
- **`export const gate = false` was parsed, filtered on, and never honoured** (the fix itself shipped in 0.35.6, which carried no note for it). The CLI declares `--only` as `Option.Array('--only', [])`, so a plain `shipkit ci` hands the check filter an EMPTY ARRAY and never `undefined`. The guard tested `only == null`, which is false for `[]`, so every gate run read as a narrowed one and no check ever opted out: a repo could declare fifteen world probes off-gate and watch all fifteen run anyway. `matchesOnly`, two hundred lines up in the same file, already used the right predicate, and the `only` field's own doc comment already said "Empty or absent means everything". Both readers now call one exported `isNarrowed
|
|
12
|
+
- **`export const gate = false` was parsed, filtered on, and never honoured** (the fix itself shipped in 0.35.6, which carried no note for it). The CLI declares `--only` as `Option.Array('--only', [])`, so a plain `shipkit ci` hands the check filter an EMPTY ARRAY and never `undefined`. The guard tested `only == null`, which is false for `[]`, so every gate run read as a narrowed one and no check ever opted out: a repo could declare fifteen world probes off-gate and watch all fifteen run anyway. `matchesOnly`, two hundred lines up in the same file, already used the right predicate, and the `only` field's own doc comment already said "Empty or absent means everything". Both readers of the CHECK filter now call one exported `isNarrowed`. `--only` also names rules and discovered checks, so the task runner is handed those ids too rather than answering `unknown task id` for a check it was never asked to run. Pinned by three tests that call `collectChecks` the way the CLI calls it: the gate skips a `gate = false` check, `shipkit check` still runs it, and naming it in `--only` still reaches it.
|
|
7
13
|
- **The adopt path writes `.vscode/tasks.json` alone, and derives what goes in it.** 0.35.6 wrote `settings.json` too, which turns eslint off and hands formatting to an oxc extension: landing that in a repo that lints with eslint silences the linter that repo chose, and that is an answer only its own author gets to give. It also wrote a canned `👁 TSC` watcher pointed at `tsconfig.json`. Against a solution config (`files: []` plus `references`) that watcher needs `--build`, type checks ZERO files without it, and reports a clean problems pane forever, which is worse than no task at all. The TSC subtask is now written only when the root tsconfig resolves inputs, `build/vscode-tasks.md` stays the owner of the per-project analysis for every other watcher, and `init` names that page in its output rather than implying the file is finished.
|
|
8
14
|
- **A symlinked `.vscode/` is left alone.** It is a dotfiles pattern pointing at a SHARED directory, and writing through it landed files outside the repo being wired.
|
|
9
15
|
- **A `repo.config.ts` with no `name` no longer kills `init` with an opaque `TypeError`.** The config loader validates nothing and hands back the raw default export, and the port arithmetic iterates the name it is given. That resolution now sits inside the same guard as the write, falls back to the directory name, and a failure there costs one WARN after everything else has already landed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.8",
|
|
4
4
|
"funding": "https://github.com/sponsors/rvion",
|
|
5
5
|
"description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
|
|
6
6
|
"license": "MIT",
|
package/src/core/run-check.ts
CHANGED
|
@@ -78,15 +78,25 @@ export type CheckOpts = {
|
|
|
78
78
|
* one they last saw on the wire. */
|
|
79
79
|
export function matchesOnly(only: string[] | undefined, ...names: string[]): boolean {
|
|
80
80
|
if (!isNarrowed(only)) return true
|
|
81
|
-
return only
|
|
81
|
+
return only.some((want) => names.some((n) => n.toLowerCase() === want.toLowerCase()))
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/** did the operator narrow this run? The ONE reading of `--only` everywhere: an
|
|
85
85
|
* empty list is not a selection, it is the absence of one. */
|
|
86
|
-
export function isNarrowed(only: string[] | undefined):
|
|
86
|
+
export function isNarrowed(only: string[] | undefined): only is string[] {
|
|
87
87
|
return only != null && only.length > 0
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** every id `--only` can name that is NOT a task: rule ids, rule titles,
|
|
91
|
+
* discovered check names and their repo-relative paths. `--only` addresses all
|
|
92
|
+
* three surfaces, so the task runner has to be told which ids belong to the
|
|
93
|
+
* other two, or it answers `unknown task id` for a check that does run. */
|
|
94
|
+
export function checkSideIds(root: string, c: RepoConfig): string[] {
|
|
95
|
+
const ids = [...RULES, ...(c.rules ?? [])].flatMap((r) => [r.id, r.title])
|
|
96
|
+
for (const d of discoverChecks(root)) ids.push(d.name, relative(root, d.path))
|
|
97
|
+
return ids
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
export type CheckRun = {
|
|
91
101
|
/** wire lines, with non-blocking failures already downgraded to warnings */
|
|
92
102
|
lines: Line[]
|
|
@@ -194,12 +204,13 @@ export async function collectChecks(root: string, o: CheckOpts = {}): Promise<Ch
|
|
|
194
204
|
logic: async (t) => { await runRuleLint(rule, t, { root, config: c }); demote(t, rule.blocking, rule.title) },
|
|
195
205
|
})
|
|
196
206
|
}
|
|
197
|
-
// ONE release survey for the whole run:
|
|
198
|
-
// collectReports, each in its own subprocess, and the survey is ~
|
|
207
|
+
// ONE release survey for the whole run: several discovered checks call
|
|
208
|
+
// collectReports, each in its own subprocess, and the survey is ~3s of
|
|
199
209
|
// network. The path is per RUN and never reused across runs, so nothing
|
|
200
210
|
// here can serve a stale answer to a later gate (releases-share.ts).
|
|
201
|
-
//
|
|
202
|
-
//
|
|
211
|
+
// This is the ONLY owner: nothing else in shipkit sets the variable, and an
|
|
212
|
+
// inherited one can only come from a caller outside the binary, which then
|
|
213
|
+
// owns its own cleanup.
|
|
203
214
|
const inherited = process.env[SHARE_ENV]
|
|
204
215
|
const sharePath = inherited != null && inherited !== '' ? inherited : join(root, 'tmp/shipkit-ci', `releases-${process.pid}-${Date.now()}.json`)
|
|
205
216
|
const childEnv: Record<string, string | undefined> = {
|
package/src/core/run-ci.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { join } from 'path'
|
|
9
9
|
import { allTasks, loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
10
10
|
import { reportToMotherhome } from './motherhome.ts'
|
|
11
|
-
import { collectChecks } from './run-check.ts'
|
|
11
|
+
import { checkSideIds, collectChecks } from './run-check.ts'
|
|
12
12
|
import { runFixPass } from './run-fixers.ts'
|
|
13
13
|
import { formatLine } from './wire.ts'
|
|
14
14
|
import { runTasks, selectTasks, type TaskResult } from './task-runner.ts'
|
|
@@ -178,7 +178,11 @@ async function runCiInScope(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
178
178
|
// the run entirely, which is also what makes its dependents correct: the
|
|
179
179
|
// runner treats a filtered dep as satisfied, and only a GREEN is ever
|
|
180
180
|
// stored, so a dependent can never start on the back of a red nobody saw.
|
|
181
|
-
|
|
181
|
+
// --only addresses tasks, rules and checks alike, so the task runner is
|
|
182
|
+
// told which ids belong to the other two surfaces rather than answering
|
|
183
|
+
// `unknown task id` for a check it was never asked to run
|
|
184
|
+
const otherIds = checkSideIds(root, c)
|
|
185
|
+
const selectable = new Set(selectTasks({ tasks, logDir: '', skipGroups: o.skipGroups ?? [], withIds: o.withIds ?? [], onlyIds: o.onlyIds ?? [], otherIds }).selected.map((t) => t.id))
|
|
182
186
|
const replayedTasks = new Map<string, UnitOutcome>()
|
|
183
187
|
const toRun = tasks
|
|
184
188
|
.filter((t) => {
|
|
@@ -219,6 +223,7 @@ async function runCiInScope(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
219
223
|
skipGroups: o.skipGroups ?? [],
|
|
220
224
|
withIds,
|
|
221
225
|
onlyIds,
|
|
226
|
+
otherIds,
|
|
222
227
|
print: sink,
|
|
223
228
|
onTaskFinish: o.onTaskFinish,
|
|
224
229
|
})
|
|
@@ -254,7 +259,11 @@ async function runCiInScope(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
254
259
|
// checks that do want it still pay for exactly one survey between them.
|
|
255
260
|
const checks = o.skipChecks === true
|
|
256
261
|
? { lines: [], blocked: false, advisoryFailures: [], undeclared: [] }
|
|
257
|
-
|
|
262
|
+
// `gate = false` opts a check out of the EVERYDAY gate. failOnAdvisory
|
|
263
|
+
// is release.requireHealthy, a repo saying it wants a missing cred to
|
|
264
|
+
// stop its publish, so it re-arms them: the opt-out must not silently
|
|
265
|
+
// drop the one request that asks for exactly these probes.
|
|
266
|
+
: 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 })
|
|
258
267
|
const color = process.stdout.isTTY && process.env.NO_COLOR == null
|
|
259
268
|
for (const l of checks.lines) { collected.push(formatLine(l, false)); print(formatLine(l, color)) }
|
|
260
269
|
for (const u of checks.undeclared) {
|
package/src/core/task-runner.ts
CHANGED
|
@@ -78,6 +78,10 @@ export type RunOpts = {
|
|
|
78
78
|
skipGroups?: string[]
|
|
79
79
|
onlyIds?: string[]
|
|
80
80
|
withIds?: string[]
|
|
81
|
+
/** ids `--only` may name that are NOT tasks: discovered checks and rules.
|
|
82
|
+
* The flag addresses all three surfaces, so a check name must not come
|
|
83
|
+
* back as `unknown task id` from the one surface that does not own it. */
|
|
84
|
+
otherIds?: string[]
|
|
81
85
|
/** print sink — tests capture, production defaults to console.log */
|
|
82
86
|
print?: (line: string) => void
|
|
83
87
|
color?: boolean
|
|
@@ -115,7 +119,9 @@ export function selectTasks(o: RunOpts): { selected: TaskDef[]; errors: string[]
|
|
|
115
119
|
if ((t.cmd == null) === (t.logic == null)) errors.push(`task ${t.id}: exactly one of cmd/logic required`)
|
|
116
120
|
for (const d of t.deps ?? []) if (!ids.has(d)) errors.push(`task ${t.id}: unknown dep '${d}'`)
|
|
117
121
|
}
|
|
118
|
-
|
|
122
|
+
const elsewhere = new Set((o.otherIds ?? []).map((i) => i.toLowerCase()))
|
|
123
|
+
for (const id of [...(o.onlyIds ?? []), ...(o.withIds ?? [])])
|
|
124
|
+
if (!ids.has(id) && !elsewhere.has(id.toLowerCase())) errors.push(`unknown task id '${id}'`)
|
|
119
125
|
// cycle check — a dep cycle must die loud at select time, not hang the loop
|
|
120
126
|
const state = new Map<string, 0 | 1>()
|
|
121
127
|
const byId = new Map(o.tasks.map((t) => [t.id, t]))
|