@naxodev/apnea 0.1.0 → 0.2.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/README.md +8 -10
- package/dist/cli.js +552 -530
- package/docs/adr/0005-harness-profiles.md +1 -1
- package/docs/adr/0010-package-split.md +1 -1
- package/docs/protocol/config.md +7 -22
- package/docs/protocol/manual-gate.md +1 -1
- package/docs/protocol/overview.md +1 -1
- package/extension/domain/herdr.ts +0 -86
- package/extension/domain/paths.ts +1 -2
- package/extension/domain/setup.ts +0 -20
- package/extension/domain/types.ts +0 -9
- package/extension/domain/verify-commands.ts +200 -108
- package/extension/errors.ts +1 -1
- package/extension/schema/config.ts +31 -17
- package/extension/schema/state.ts +16 -3
- package/extension/services/herdr.ts +5 -161
- package/extension/services/vcs.ts +393 -21
- package/extension/workflows/commit.ts +8 -5
- package/extension/workflows/dispatch.ts +60 -177
- package/extension/workflows/setup.ts +2 -109
- package/extension/workflows/start.ts +0 -1
- package/extension/workflows/wait.ts +1 -57
- package/package.json +1 -2
- package/schemas/config.schema.json +6 -6
- package/schemas/state.schema.json +5 -1
- package/herdr-plugin/herdr-plugin.toml +0 -15
- package/herdr-plugin/scripts/run-task.sh +0 -8
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process"
|
|
1
|
+
import { spawn, spawnSync, type ChildProcess } from "node:child_process"
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
2
4
|
import * as path from "node:path"
|
|
3
5
|
import { Context, Effect, Layer } from "effect"
|
|
6
|
+
import {
|
|
7
|
+
formatVerifyBlock,
|
|
8
|
+
normalizeVerifySource,
|
|
9
|
+
type VerifyBlock,
|
|
10
|
+
} from "../domain/verify-commands.ts"
|
|
4
11
|
import { VcsError } from "../errors.ts"
|
|
5
12
|
import type { VcsBackend } from "../domain/types.ts"
|
|
6
13
|
import { FileSystem } from "./file-system.ts"
|
|
@@ -27,7 +34,7 @@ export interface VcsService {
|
|
|
27
34
|
) => Effect.Effect<void>
|
|
28
35
|
readonly runVerify: (
|
|
29
36
|
root: string,
|
|
30
|
-
|
|
37
|
+
blocks: readonly VerifyBlock[],
|
|
31
38
|
timeoutMs: number,
|
|
32
39
|
) => Effect.Effect<{ ok: boolean; log: string }>
|
|
33
40
|
}
|
|
@@ -52,6 +59,300 @@ function run(
|
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
function verificationError(
|
|
63
|
+
error: unknown,
|
|
64
|
+
temporaryDirectory?: string,
|
|
65
|
+
): string {
|
|
66
|
+
const message =
|
|
67
|
+
error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
|
68
|
+
return temporaryDirectory
|
|
69
|
+
? message.replaceAll(
|
|
70
|
+
temporaryDirectory,
|
|
71
|
+
"[temporary verification directory]",
|
|
72
|
+
)
|
|
73
|
+
: message
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const VERIFY_LOG_LIMIT = 10 * 1024 * 1024
|
|
77
|
+
const VERIFY_KILL_CLOSE_GRACE_MS = 2_500
|
|
78
|
+
const VERIFY_RESULT_RESERVE = 2_048
|
|
79
|
+
const VERIFY_WRAPPER_SOURCE = `exec 2>&1
|
|
80
|
+
exec "$1" -e "$2"
|
|
81
|
+
`
|
|
82
|
+
const VERIFY_DISPLAY_LIMIT_NOTICE = `verification log limit of ${VERIFY_LOG_LIMIT} bytes would be exceeded by the verification block display; block was not executed`
|
|
83
|
+
const VERIFY_LOG_LIMIT_NOTICE = `verification log limit of ${VERIFY_LOG_LIMIT} bytes reached; output was truncated and verification stopped`
|
|
84
|
+
const VERIFY_LIMIT_NOTICE_RESERVE =
|
|
85
|
+
1 +
|
|
86
|
+
Math.max(
|
|
87
|
+
Buffer.byteLength(VERIFY_DISPLAY_LIMIT_NOTICE),
|
|
88
|
+
Buffer.byteLength(VERIFY_LOG_LIMIT_NOTICE),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
export function utf8BytesAfterAppend(
|
|
92
|
+
usedBytes: number,
|
|
93
|
+
limitBytes: number,
|
|
94
|
+
text: string,
|
|
95
|
+
): number | null {
|
|
96
|
+
const nextBytes = usedBytes + Buffer.byteLength(text)
|
|
97
|
+
return nextBytes <= limitBytes ? nextBytes : null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class VerificationLog {
|
|
101
|
+
readonly #chunks: string[] = []
|
|
102
|
+
readonly #contentLimit: number
|
|
103
|
+
#bytes = 0
|
|
104
|
+
#limited = false
|
|
105
|
+
|
|
106
|
+
constructor(readonly limit: number) {
|
|
107
|
+
this.#contentLimit = Math.max(0, limit - VERIFY_LIMIT_NOTICE_RESERVE)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get remaining(): number {
|
|
111
|
+
return this.#contentLimit - this.#bytes
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
canAppendBytes(bytes: number): boolean {
|
|
115
|
+
return bytes <= this.remaining
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
append(text: string): boolean {
|
|
119
|
+
const nextBytes = utf8BytesAfterAppend(
|
|
120
|
+
this.#bytes,
|
|
121
|
+
this.#contentLimit,
|
|
122
|
+
text,
|
|
123
|
+
)
|
|
124
|
+
if (nextBytes === null) return false
|
|
125
|
+
this.#chunks.push(text)
|
|
126
|
+
this.#bytes = nextBytes
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
addLimitNotice(notice: string): void {
|
|
131
|
+
if (this.#limited) return
|
|
132
|
+
this.#limited = true
|
|
133
|
+
const previous = this.#chunks.at(-1)
|
|
134
|
+
if (this.#bytes > 0 && !previous?.endsWith("\n")) {
|
|
135
|
+
this.#chunks.push("\n")
|
|
136
|
+
this.#bytes += 1
|
|
137
|
+
}
|
|
138
|
+
this.#chunks.push(notice)
|
|
139
|
+
this.#bytes += Buffer.byteLength(notice)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
toString(): string {
|
|
143
|
+
return this.#chunks.join("").trimEnd()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function verifyBlockDisplayByteLength(block: VerifyBlock): number {
|
|
148
|
+
const source = block.source
|
|
149
|
+
const bodyEnd = source.endsWith("\n") ? source.length - 1 : source.length
|
|
150
|
+
let lineCount = 1
|
|
151
|
+
for (let index = 0; index < bodyEnd; index++) {
|
|
152
|
+
if (source.charCodeAt(index) === 10) lineCount += 1
|
|
153
|
+
}
|
|
154
|
+
const bodyBytes =
|
|
155
|
+
Buffer.byteLength(source) - (bodyEnd < source.length ? 1 : 0)
|
|
156
|
+
return (
|
|
157
|
+
Buffer.byteLength(`${block.interpreter} -e [verification block]\n`) +
|
|
158
|
+
bodyBytes +
|
|
159
|
+
lineCount * 2
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function taskkillTree(pid: number): Promise<boolean> {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
let settled = false
|
|
166
|
+
let killer: ChildProcess
|
|
167
|
+
const finish = (ok: boolean) => {
|
|
168
|
+
if (settled) return
|
|
169
|
+
settled = true
|
|
170
|
+
clearTimeout(timer)
|
|
171
|
+
resolve(ok)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
killer = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
176
|
+
stdio: "ignore",
|
|
177
|
+
windowsHide: true,
|
|
178
|
+
})
|
|
179
|
+
} catch {
|
|
180
|
+
resolve(false)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const timer = setTimeout(() => {
|
|
185
|
+
try {
|
|
186
|
+
killer.kill("SIGKILL")
|
|
187
|
+
} catch {
|
|
188
|
+
// The fallback below still targets the verification child.
|
|
189
|
+
}
|
|
190
|
+
finish(false)
|
|
191
|
+
}, 2_000)
|
|
192
|
+
killer.once("error", () => finish(false))
|
|
193
|
+
killer.once("close", (code) => finish(code === 0))
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function snapshotProcessDescendants(rootPid: number): number[] {
|
|
198
|
+
const snapshot = spawnSync("ps", ["-axo", "pid=,ppid="], {
|
|
199
|
+
encoding: "utf8",
|
|
200
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
201
|
+
})
|
|
202
|
+
if (snapshot.status !== 0 || snapshot.error) return []
|
|
203
|
+
|
|
204
|
+
const children = new Map<number, number[]>()
|
|
205
|
+
for (const line of (snapshot.stdout ?? "").split("\n")) {
|
|
206
|
+
const match = /^\s*(\d+)\s+(\d+)\s*$/.exec(line)
|
|
207
|
+
if (!match) continue
|
|
208
|
+
const pid = Number(match[1])
|
|
209
|
+
const parentPid = Number(match[2])
|
|
210
|
+
const siblings = children.get(parentPid)
|
|
211
|
+
if (siblings) siblings.push(pid)
|
|
212
|
+
else children.set(parentPid, [pid])
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const descendants: number[] = []
|
|
216
|
+
const pending = [...(children.get(rootPid) ?? [])]
|
|
217
|
+
for (let index = 0; index < pending.length; index++) {
|
|
218
|
+
const pid = pending[index]!
|
|
219
|
+
descendants.push(pid)
|
|
220
|
+
pending.push(...(children.get(pid) ?? []))
|
|
221
|
+
}
|
|
222
|
+
return descendants
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function killProcessTree(child: ChildProcess): Promise<void> {
|
|
226
|
+
const pid = child.pid
|
|
227
|
+
if (process.platform === "win32" && pid !== undefined) {
|
|
228
|
+
if (await taskkillTree(pid)) return
|
|
229
|
+
} else if (pid !== undefined) {
|
|
230
|
+
const descendants = snapshotProcessDescendants(pid)
|
|
231
|
+
try {
|
|
232
|
+
process.kill(-pid, "SIGKILL")
|
|
233
|
+
} catch {
|
|
234
|
+
// The process may have exited between timeout and termination.
|
|
235
|
+
}
|
|
236
|
+
for (const descendantPid of descendants) {
|
|
237
|
+
try {
|
|
238
|
+
process.kill(descendantPid, "SIGKILL")
|
|
239
|
+
} catch {
|
|
240
|
+
// Process-group termination may already have killed this descendant.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// This is also the fallback when Windows taskkill cannot kill the tree.
|
|
246
|
+
try {
|
|
247
|
+
child.kill("SIGKILL")
|
|
248
|
+
} catch {
|
|
249
|
+
// A concurrently exited child needs no further termination.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
type VerificationProcessResult = {
|
|
254
|
+
code: number
|
|
255
|
+
output: string
|
|
256
|
+
error?: string
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function runVerificationProcess(
|
|
260
|
+
wrapper: string,
|
|
261
|
+
interpreter: VerifyBlock["interpreter"],
|
|
262
|
+
script: string,
|
|
263
|
+
cwd: string,
|
|
264
|
+
timeoutMs: number,
|
|
265
|
+
outputLimit: number,
|
|
266
|
+
): Promise<VerificationProcessResult> {
|
|
267
|
+
return new Promise((resolve) => {
|
|
268
|
+
let child: ChildProcess
|
|
269
|
+
try {
|
|
270
|
+
child = spawn("sh", [wrapper, interpreter, script], {
|
|
271
|
+
cwd,
|
|
272
|
+
detached: process.platform !== "win32",
|
|
273
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
274
|
+
windowsHide: true,
|
|
275
|
+
})
|
|
276
|
+
} catch (error) {
|
|
277
|
+
resolve({
|
|
278
|
+
code: 1,
|
|
279
|
+
output: "",
|
|
280
|
+
error: verificationError(error),
|
|
281
|
+
})
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const output: Buffer[] = []
|
|
286
|
+
let outputSize = 0
|
|
287
|
+
let outputExceeded = false
|
|
288
|
+
let processError: string | undefined
|
|
289
|
+
let termination: Promise<void> | undefined
|
|
290
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
291
|
+
let postKillCompletion: ReturnType<typeof setTimeout> | undefined
|
|
292
|
+
let settled = false
|
|
293
|
+
|
|
294
|
+
const terminate = (message: string) => {
|
|
295
|
+
if (settled) return
|
|
296
|
+
processError ??= message
|
|
297
|
+
if (termination) return
|
|
298
|
+
termination = killProcessTree(child)
|
|
299
|
+
postKillCompletion = setTimeout(() => {
|
|
300
|
+
child.stdout?.destroy()
|
|
301
|
+
finish(1)
|
|
302
|
+
}, VERIFY_KILL_CLOSE_GRACE_MS)
|
|
303
|
+
}
|
|
304
|
+
const capture = (chunk: Buffer) => {
|
|
305
|
+
if (outputExceeded) return
|
|
306
|
+
const available = outputLimit - outputSize
|
|
307
|
+
if (chunk.length > available) {
|
|
308
|
+
if (available > 0) output.push(chunk.subarray(0, available))
|
|
309
|
+
outputSize = outputLimit
|
|
310
|
+
outputExceeded = true
|
|
311
|
+
terminate(`verification output exceeded ${outputLimit} bytes`)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
output.push(chunk)
|
|
315
|
+
outputSize += chunk.length
|
|
316
|
+
}
|
|
317
|
+
const onStdout = (chunk: Buffer) => capture(chunk)
|
|
318
|
+
const onError = (error: Error) => {
|
|
319
|
+
terminate(`verification process error: ${verificationError(error)}`)
|
|
320
|
+
}
|
|
321
|
+
const finish = (code: number) => {
|
|
322
|
+
if (settled) return
|
|
323
|
+
settled = true
|
|
324
|
+
if (timeout) clearTimeout(timeout)
|
|
325
|
+
if (postKillCompletion) clearTimeout(postKillCompletion)
|
|
326
|
+
child.stdout?.off("data", onStdout)
|
|
327
|
+
child.off("error", onError)
|
|
328
|
+
child.off("close", onClose)
|
|
329
|
+
resolve({
|
|
330
|
+
code,
|
|
331
|
+
output: Buffer.concat(output).toString("utf8"),
|
|
332
|
+
...(processError === undefined ? {} : { error: processError }),
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
const onClose = (code: number | null) => {
|
|
336
|
+
if (termination) {
|
|
337
|
+
void termination.then(
|
|
338
|
+
() => finish(code ?? 1),
|
|
339
|
+
() => finish(code ?? 1),
|
|
340
|
+
)
|
|
341
|
+
} else {
|
|
342
|
+
finish(code ?? 1)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
child.stdout?.on("data", onStdout)
|
|
347
|
+
child.once("error", onError)
|
|
348
|
+
child.once("close", onClose)
|
|
349
|
+
|
|
350
|
+
timeout = setTimeout(() => {
|
|
351
|
+
terminate(`verification timed out after ${timeoutMs}ms`)
|
|
352
|
+
}, timeoutMs)
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
55
356
|
/** Drop .apnea/ runtime paths from VCS summaries (artifacts are allowed). */
|
|
56
357
|
export function filterAppPaths(summary: string): string {
|
|
57
358
|
return summary
|
|
@@ -205,32 +506,103 @@ export const VcsLive = Layer.effect(
|
|
|
205
506
|
|
|
206
507
|
const runVerify = (
|
|
207
508
|
root: string,
|
|
208
|
-
|
|
509
|
+
blocks: readonly VerifyBlock[],
|
|
209
510
|
timeoutMs: number,
|
|
210
511
|
): Effect.Effect<{ ok: boolean; log: string }> =>
|
|
211
|
-
Effect.
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
512
|
+
Effect.promise(async () => {
|
|
513
|
+
const log = new VerificationLog(VERIFY_LOG_LIMIT)
|
|
514
|
+
let temporaryDirectory: string | undefined
|
|
515
|
+
let ok = true
|
|
516
|
+
let operation = "create temporary verification directory"
|
|
517
|
+
try {
|
|
518
|
+
temporaryDirectory = mkdtempSync(path.join(tmpdir(), "apnea-verify-"))
|
|
519
|
+
const wrapper = path.join(temporaryDirectory, "run-block.sh")
|
|
520
|
+
operation = "write verification wrapper"
|
|
521
|
+
writeFileSync(wrapper, VERIFY_WRAPPER_SOURCE, {
|
|
217
522
|
encoding: "utf8",
|
|
218
|
-
|
|
219
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
523
|
+
mode: 0o600,
|
|
220
524
|
})
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
525
|
+
for (const [index, block] of blocks.entries()) {
|
|
526
|
+
const source = normalizeVerifySource(block.source)
|
|
527
|
+
const normalizedBlock = { ...block, source }
|
|
528
|
+
const script = path.join(
|
|
529
|
+
temporaryDirectory,
|
|
530
|
+
`block-${index + 1}.${block.interpreter}`,
|
|
531
|
+
)
|
|
532
|
+
const displayBytes =
|
|
533
|
+
2 + verifyBlockDisplayByteLength(normalizedBlock) + 1
|
|
534
|
+
if (!log.canAppendBytes(displayBytes)) {
|
|
535
|
+
log.addLimitNotice(VERIFY_DISPLAY_LIMIT_NOTICE)
|
|
536
|
+
ok = false
|
|
537
|
+
break
|
|
538
|
+
}
|
|
539
|
+
log.append(`$ ${formatVerifyBlock(normalizedBlock)}\n`)
|
|
540
|
+
operation = `write ${block.interpreter} verification block`
|
|
541
|
+
writeFileSync(script, source, {
|
|
542
|
+
encoding: "utf8",
|
|
543
|
+
mode: 0o600,
|
|
544
|
+
})
|
|
545
|
+
operation = `run ${block.interpreter} verification block`
|
|
546
|
+
const result = await runVerificationProcess(
|
|
547
|
+
wrapper,
|
|
548
|
+
block.interpreter,
|
|
549
|
+
script,
|
|
550
|
+
root,
|
|
551
|
+
timeoutMs,
|
|
552
|
+
Math.max(0, log.remaining - VERIFY_RESULT_RESERVE),
|
|
553
|
+
)
|
|
554
|
+
const output = verificationError(
|
|
555
|
+
result.output.trimEnd(),
|
|
556
|
+
temporaryDirectory,
|
|
557
|
+
)
|
|
558
|
+
if (output && !log.append(`${output}\n`)) {
|
|
559
|
+
log.addLimitNotice(VERIFY_LOG_LIMIT_NOTICE)
|
|
560
|
+
ok = false
|
|
561
|
+
break
|
|
562
|
+
}
|
|
563
|
+
if (!log.append(`exit=${result.code}\n`)) {
|
|
564
|
+
log.addLimitNotice(VERIFY_LOG_LIMIT_NOTICE)
|
|
565
|
+
ok = false
|
|
566
|
+
break
|
|
567
|
+
}
|
|
568
|
+
if (result.error) {
|
|
569
|
+
const error = verificationError(result.error, temporaryDirectory)
|
|
570
|
+
if (!log.append(`${error}\n`)) {
|
|
571
|
+
log.addLimitNotice(VERIFY_LOG_LIMIT_NOTICE)
|
|
572
|
+
}
|
|
573
|
+
ok = false
|
|
574
|
+
break
|
|
575
|
+
}
|
|
576
|
+
if (result.code !== 0) {
|
|
577
|
+
ok = false
|
|
578
|
+
break
|
|
579
|
+
}
|
|
580
|
+
if (index < blocks.length - 1 && !log.append("\n")) {
|
|
581
|
+
log.addLimitNotice(VERIFY_LOG_LIMIT_NOTICE)
|
|
582
|
+
ok = false
|
|
583
|
+
break
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch (error) {
|
|
587
|
+
const message = `${operation} failed: ${verificationError(error, temporaryDirectory)}\n`
|
|
588
|
+
if (!log.append(message)) {
|
|
589
|
+
log.addLimitNotice(VERIFY_LOG_LIMIT_NOTICE)
|
|
227
590
|
}
|
|
228
|
-
|
|
229
|
-
|
|
591
|
+
ok = false
|
|
592
|
+
} finally {
|
|
593
|
+
if (temporaryDirectory) {
|
|
594
|
+
try {
|
|
595
|
+
rmSync(temporaryDirectory, { recursive: true, force: true })
|
|
596
|
+
} catch (error) {
|
|
597
|
+
const message = `clean up temporary verification directory failed: ${verificationError(error, temporaryDirectory)}\n`
|
|
598
|
+
if (!log.append(message)) {
|
|
599
|
+
log.addLimitNotice(VERIFY_LOG_LIMIT_NOTICE)
|
|
600
|
+
}
|
|
601
|
+
ok = false
|
|
602
|
+
}
|
|
230
603
|
}
|
|
231
|
-
lines.push("")
|
|
232
604
|
}
|
|
233
|
-
return { ok
|
|
605
|
+
return { ok, log: log.toString() }
|
|
234
606
|
})
|
|
235
607
|
|
|
236
608
|
return Vcs.of({
|
|
@@ -3,7 +3,10 @@ import { Effect, Result } from "effect"
|
|
|
3
3
|
import { asVerdict, parseFrontMatter } from "../domain/frontmatter.ts"
|
|
4
4
|
import { abs, phaseDir, rel } from "../domain/paths.ts"
|
|
5
5
|
import { nextAfter, toolAllowed } from "../domain/state-machine.ts"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
extractVerifyBlocks,
|
|
8
|
+
formatVerifyCommand,
|
|
9
|
+
} from "../domain/verify-commands.ts"
|
|
7
10
|
import {
|
|
8
11
|
ArtifactInvalid,
|
|
9
12
|
GateRefused,
|
|
@@ -86,8 +89,8 @@ export const commitWorkflow = (
|
|
|
86
89
|
})
|
|
87
90
|
}
|
|
88
91
|
const pkgText = yield* fs.readFile(pkgAbs)
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
92
|
+
const blocks = extractVerifyBlocks(pkgText)
|
|
93
|
+
if (!blocks.length) {
|
|
91
94
|
return yield* new ArtifactInvalid({
|
|
92
95
|
artifact: pkgRel,
|
|
93
96
|
message: "no verify commands found in phase package (need ```sh block)",
|
|
@@ -96,7 +99,7 @@ export const commitWorkflow = (
|
|
|
96
99
|
|
|
97
100
|
const cfg = yield* config.load(root)
|
|
98
101
|
const verifyTimeout = cfg.timeouts_ms.verify ?? 900_000
|
|
99
|
-
const verify = yield* vcs.runVerify(root,
|
|
102
|
+
const verify = yield* vcs.runVerify(root, blocks, verifyTimeout)
|
|
100
103
|
|
|
101
104
|
const vlog = path.join(path.dirname(reviewAbs), "verify.log")
|
|
102
105
|
yield* fs.mkdir(path.dirname(vlog), { recursive: true })
|
|
@@ -104,7 +107,7 @@ export const commitWorkflow = (
|
|
|
104
107
|
|
|
105
108
|
if (!verify.ok) {
|
|
106
109
|
return yield* new VerifyFailed({
|
|
107
|
-
commands:
|
|
110
|
+
commands: blocks.map(formatVerifyCommand),
|
|
108
111
|
outputs: [verify.log.slice(-2000)],
|
|
109
112
|
// `outputs` is a tail — point the caller at the full log on disk.
|
|
110
113
|
verify_log: rel(vlog, root),
|