@defrex/autobuild 0.8.0 → 0.10.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 +1 -1
- package/SPEC.md +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline-source resolution (SPEC §16.1): where a build's build-owned config
|
|
3
|
+
* sections come from. A build's pipeline is pinned to its own branch — the
|
|
4
|
+
* branch head once the build has published commits there, otherwise the
|
|
5
|
+
* recorded base commit its workspace was cut from. The dispatcher resolves
|
|
6
|
+
* this once per deposit (launch and reload), records the exact commit in the
|
|
7
|
+
* effective-config artifact's metadata, and never substitutes a later
|
|
8
|
+
* base-branch change (the AUT-366 class of failure).
|
|
9
|
+
*/
|
|
10
|
+
import { readFile } from 'node:fs/promises'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import type { AbEvent } from '../events/catalog'
|
|
13
|
+
import { defineReducer } from '../kernel/incremental'
|
|
14
|
+
import type { Forge } from '../ports/types'
|
|
15
|
+
import type { Exec } from '../ports/workspace/git-worktree'
|
|
16
|
+
import type { BuildRecord } from '../store/types'
|
|
17
|
+
import { parseConfig } from './load'
|
|
18
|
+
import type { Config } from './schema'
|
|
19
|
+
|
|
20
|
+
/** Which fact the pinned pipeline was read from. `legacy-fallback` marks the
|
|
21
|
+
* no-migration path: no recorded base (or an unreadable one), so the deposit
|
|
22
|
+
* is the dispatcher's live snapshot exactly as before this change.
|
|
23
|
+
* `worktree-dirty` marks the checkout-mode worktree read whose git-normalized
|
|
24
|
+
* content does not match the resolved branch head (uncommitted edits, or a
|
|
25
|
+
* file never committed): no commit is recorded, because attributing worktree
|
|
26
|
+
* content to the branch-head commit would be false provenance. */
|
|
27
|
+
export type PipelineSourceRef = 'branch-head' | 'base' | 'legacy-fallback' | 'worktree-dirty'
|
|
28
|
+
|
|
29
|
+
const PIPELINE_SOURCE_REFS: readonly PipelineSourceRef[] = [
|
|
30
|
+
'branch-head',
|
|
31
|
+
'base',
|
|
32
|
+
'legacy-fallback',
|
|
33
|
+
'worktree-dirty',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
export function isPipelineSourceRef(value: unknown): value is PipelineSourceRef {
|
|
37
|
+
return typeof value === 'string' && (PIPELINE_SOURCE_REFS as readonly string[]).includes(value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Metadata recorded alongside every deposited effective-config artifact. */
|
|
41
|
+
export interface PipelineSourceMeta {
|
|
42
|
+
ref: PipelineSourceRef
|
|
43
|
+
/** Exact commit the pipeline sections were parsed from. Absent for the
|
|
44
|
+
* legacy fallback (no build-branch/base read was possible) and for
|
|
45
|
+
* `worktree-dirty` (worktree bytes that no commit claims). */
|
|
46
|
+
commit?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PipelineSource {
|
|
50
|
+
meta: PipelineSourceMeta
|
|
51
|
+
/** The parsed build-owned tables. */
|
|
52
|
+
config: Config
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PipelineSourceInput {
|
|
56
|
+
slug: string
|
|
57
|
+
record: Pick<BuildRecord, 'branch'>
|
|
58
|
+
events: readonly AbEvent[]
|
|
59
|
+
/** 'origin' reads through the forge; 'checkout' reads through git exec in
|
|
60
|
+
* the dispatcher's checkout (build branches are refs of the shared repo). */
|
|
61
|
+
mode: 'origin' | 'checkout'
|
|
62
|
+
/** Origin-mode read capability. Required to read through the forge; absent
|
|
63
|
+
* degrades to the recorded base / legacy fallback. */
|
|
64
|
+
forge?: Forge
|
|
65
|
+
/** Checkout-mode seam: the physical main checkout and exec. */
|
|
66
|
+
checkout?: string
|
|
67
|
+
exec?: Exec
|
|
68
|
+
/** Checkout-mode seam: the open build worktree, whose `autobuild.toml` is
|
|
69
|
+
* the branch head. Preferred over `git show` when the file exists. */
|
|
70
|
+
workspacePath?: string
|
|
71
|
+
/** Optional degradation sink: one failure per read/parse attempt that forced
|
|
72
|
+
* a fallback, in order. Callers surface the actionable ones as per-build
|
|
73
|
+
* diagnostics (SPEC §16.1) instead of the resolver throwing. */
|
|
74
|
+
onFailure?: (failure: PipelineSourceFailure) => void
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Why a resolution degraded. `unpublished`, `base-source` and `no-base` are
|
|
78
|
+
* the documented fallback order (a build whose branch is not published yet, a
|
|
79
|
+
* legacy build with no readable recorded base) — normal operation, quiet.
|
|
80
|
+
* `capability` and `branch-source` are failures an operator can act on: a
|
|
81
|
+
* wiring gap, or a build-branch source that resolved but cannot be read or
|
|
82
|
+
* parsed. */
|
|
83
|
+
export type PipelineSourceFailureKind =
|
|
84
|
+
| 'capability'
|
|
85
|
+
| 'unpublished'
|
|
86
|
+
| 'branch-source'
|
|
87
|
+
| 'base-source'
|
|
88
|
+
| 'no-base'
|
|
89
|
+
| 'store'
|
|
90
|
+
|
|
91
|
+
export interface PipelineSourceFailure {
|
|
92
|
+
kind: PipelineSourceFailureKind
|
|
93
|
+
detail: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The failure kinds worth an operator-facing diagnostic: a wiring gap, a
|
|
97
|
+
* build-branch source that resolved but cannot be read or parsed, or a store
|
|
98
|
+
* failure — each something an operator (or the build's author) can fix. */
|
|
99
|
+
export function isActionablePipelineFailure(failure: PipelineSourceFailure): boolean {
|
|
100
|
+
return (
|
|
101
|
+
failure.kind === 'capability' || failure.kind === 'branch-source' || failure.kind === 'store'
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const RECORDED_BASE_SHA_REDUCER_VERSION = 1
|
|
106
|
+
|
|
107
|
+
/** The recorded base commit of the build's latest workspace. The newest
|
|
108
|
+
* `workspace.provisioned` wins: a re-provisioned workspace is what the build's
|
|
109
|
+
* current environment was cut from. */
|
|
110
|
+
export const recordedBaseShaReducer = defineReducer<{ base?: string }, AbEvent, string | undefined>(
|
|
111
|
+
{
|
|
112
|
+
version: RECORDED_BASE_SHA_REDUCER_VERSION,
|
|
113
|
+
initial: () => ({}),
|
|
114
|
+
fold(acc, events) {
|
|
115
|
+
for (const event of events) {
|
|
116
|
+
if (event.type === 'workspace.provisioned') acc.base = event.payload.base.sha
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
finish: (acc) => acc.base,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
export function recordedBaseSha(events: readonly AbEvent[]): string | undefined {
|
|
124
|
+
return recordedBaseShaReducer.reduce(events)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildBranch(record: Pick<BuildRecord, 'branch'>, slug: string): string {
|
|
128
|
+
return record.branch ?? `ab/${slug}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function git(
|
|
132
|
+
args: string[],
|
|
133
|
+
cwd: string,
|
|
134
|
+
exec: Exec,
|
|
135
|
+
): Promise<{ stdout: string; exitCode: number; stderr: string }> {
|
|
136
|
+
return exec(['git', ...args], { cwd })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function readCheckoutFile(
|
|
140
|
+
ref: string,
|
|
141
|
+
path: string,
|
|
142
|
+
checkout: string,
|
|
143
|
+
exec: Exec,
|
|
144
|
+
): Promise<string> {
|
|
145
|
+
const result = await git(['show', `${ref}:${path}`], checkout, exec)
|
|
146
|
+
if (result.exitCode !== 0) {
|
|
147
|
+
throw new Error(`git show ${ref}:${path} failed: ${result.stderr.trim() || '(no output)'}`)
|
|
148
|
+
}
|
|
149
|
+
return result.stdout
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function message(error: unknown): string {
|
|
153
|
+
return error instanceof Error ? error.message : String(error)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Parse a read file into a pipeline source, or `undefined` when the TOML is
|
|
157
|
+
* malformed. A branch-source parse failure is terminal: the build's own branch
|
|
158
|
+
* exists but carries an unparseable pipeline, so falling back to the recorded
|
|
159
|
+
* base would run the build under a pipeline that is not its own — the AUT-366
|
|
160
|
+
* poisoning by another door. The caller keeps the build's last good deposit
|
|
161
|
+
* instead (SPEC §16.1). */
|
|
162
|
+
function parsePipeline(
|
|
163
|
+
commit: string | undefined,
|
|
164
|
+
ref: PipelineSourceRef,
|
|
165
|
+
content: string,
|
|
166
|
+
onFailure: (failure: PipelineSourceFailure) => void,
|
|
167
|
+
): PipelineSource | undefined {
|
|
168
|
+
const label = commit === undefined ? ref : `${ref}@${commit}`
|
|
169
|
+
try {
|
|
170
|
+
return {
|
|
171
|
+
meta: commit === undefined ? { ref } : { ref, commit },
|
|
172
|
+
config: parseConfig(content, `autobuild.toml@${label}`),
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
onFailure({
|
|
176
|
+
// `worktree-dirty` is a branch-source failure: the malformed file sits on
|
|
177
|
+
// the build's own side (its open worktree), so a base fallback would run
|
|
178
|
+
// the build under a pipeline that is not its own — same as `branch-head`.
|
|
179
|
+
kind: ref === 'base' ? 'base-source' : 'branch-source',
|
|
180
|
+
detail: `malformed autobuild.toml at ${label}: ${message(error)}`,
|
|
181
|
+
})
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolve the pipeline source for one build, or `undefined` for the legacy
|
|
188
|
+
* fallback. Every failure — missing capability, missing base record, unread
|
|
189
|
+
* file, malformed TOML — degrades to the fallback instead of failing the
|
|
190
|
+
* caller: a config deposit is never allowed to fail a dispatcher tick or a
|
|
191
|
+
* launch.
|
|
192
|
+
*
|
|
193
|
+
* The resolution rule (SPEC §16.1): the build's branch head once it resolves
|
|
194
|
+
* (origin mode: the forge's remote branch; checkout mode: the shared repo's
|
|
195
|
+
* branch ref), else the workspace's recorded base commit. The caller re-
|
|
196
|
+
* resolves at every launch, so a build that publishes pipeline changes to its
|
|
197
|
+
* own branch picks them up at its next launch.
|
|
198
|
+
*/
|
|
199
|
+
export async function resolvePipelineSource(
|
|
200
|
+
input: PipelineSourceInput,
|
|
201
|
+
): Promise<PipelineSource | undefined> {
|
|
202
|
+
const { slug, record, events } = input
|
|
203
|
+
const onFailure = input.onFailure ?? (() => {})
|
|
204
|
+
const branch = buildBranch(record, slug)
|
|
205
|
+
const path = 'autobuild.toml'
|
|
206
|
+
|
|
207
|
+
if (input.mode === 'origin') {
|
|
208
|
+
const forge = input.forge
|
|
209
|
+
if (forge === undefined) {
|
|
210
|
+
onFailure({ kind: 'capability', detail: 'origin mode with no forge' })
|
|
211
|
+
return undefined
|
|
212
|
+
}
|
|
213
|
+
const { remoteBranchSha, readFile: forgeRead } = forge
|
|
214
|
+
if (remoteBranchSha === undefined || forgeRead === undefined) {
|
|
215
|
+
onFailure({
|
|
216
|
+
kind: 'capability',
|
|
217
|
+
detail:
|
|
218
|
+
`forge lacks the ${remoteBranchSha === undefined ? 'remoteBranchSha' : 'readFile'} ` +
|
|
219
|
+
'capability the pipeline-source read needs',
|
|
220
|
+
})
|
|
221
|
+
} else {
|
|
222
|
+
let head: string | undefined
|
|
223
|
+
try {
|
|
224
|
+
head = await remoteBranchSha.call(forge, branch)
|
|
225
|
+
} catch (error) {
|
|
226
|
+
// Branch not yet published (or the forge failed): fall through to the
|
|
227
|
+
// recorded base commit, which is exactly what the workspace was cut
|
|
228
|
+
// from. Surfaced as `unpublished` either way — the normal order.
|
|
229
|
+
onFailure({
|
|
230
|
+
kind: 'unpublished',
|
|
231
|
+
detail: `forge remoteBranchSha(${branch}) failed: ${message(error)}`,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
if (head !== undefined) {
|
|
235
|
+
let content: string | undefined
|
|
236
|
+
try {
|
|
237
|
+
content = await forgeRead.call(forge, path, head)
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// The branch resolved but its file did not come back: an actionable
|
|
240
|
+
// forge failure, not an unpublished branch.
|
|
241
|
+
onFailure({
|
|
242
|
+
kind: 'branch-source',
|
|
243
|
+
detail: `forge readFile(${path}, ${head}) failed: ${message(error)}`,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
if (content !== undefined) {
|
|
247
|
+
// A malformed build-branch file is terminal: the build's own branch
|
|
248
|
+
// exists, so the recorded base is not its pipeline either.
|
|
249
|
+
return parsePipeline(head, 'branch-head', content, onFailure)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const baseSha = recordedBaseSha(events)
|
|
254
|
+
if (baseSha === undefined) {
|
|
255
|
+
onFailure({ kind: 'no-base', detail: 'no recorded base commit (legacy build)' })
|
|
256
|
+
return undefined
|
|
257
|
+
}
|
|
258
|
+
if (forgeRead === undefined) return undefined
|
|
259
|
+
let baseContent: string | undefined
|
|
260
|
+
try {
|
|
261
|
+
baseContent = await forgeRead.call(forge, path, baseSha)
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// Base file unreadable (legacy logs, deleted history): legacy fallback.
|
|
264
|
+
onFailure({
|
|
265
|
+
kind: 'base-source',
|
|
266
|
+
detail: `forge readFile(${path}, base ${baseSha}) failed: ${message(error)}`,
|
|
267
|
+
})
|
|
268
|
+
return undefined
|
|
269
|
+
}
|
|
270
|
+
return parsePipeline(baseSha, 'base', baseContent, onFailure)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { checkout, exec, workspacePath } = input
|
|
274
|
+
if (checkout === undefined || exec === undefined) {
|
|
275
|
+
onFailure({ kind: 'capability', detail: 'checkout mode with no checkout or exec seam' })
|
|
276
|
+
return undefined
|
|
277
|
+
}
|
|
278
|
+
// The build branch is a ref of the shared repository: it exists from
|
|
279
|
+
// provision time (cut from base) and survives workspace release, so the
|
|
280
|
+
// committed head is readable at every launch boundary — including the
|
|
281
|
+
// relaunch after finalize. The recorded commit is the exact sha read.
|
|
282
|
+
let head: string | undefined
|
|
283
|
+
try {
|
|
284
|
+
const resolved = await git(['rev-parse', '--verify', `refs/heads/${branch}`], checkout, exec)
|
|
285
|
+
if (resolved.exitCode === 0) {
|
|
286
|
+
head = resolved.stdout.trim()
|
|
287
|
+
} else {
|
|
288
|
+
// Unpublished branch: the normal order, resolved from the base next.
|
|
289
|
+
onFailure({
|
|
290
|
+
kind: 'unpublished',
|
|
291
|
+
detail: `git rev-parse refs/heads/${branch} failed: ${
|
|
292
|
+
resolved.stderr.trim() || '(no output)'
|
|
293
|
+
}`,
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
// Fall through to the recorded base commit.
|
|
298
|
+
onFailure({
|
|
299
|
+
kind: 'unpublished',
|
|
300
|
+
detail: `git rev-parse refs/heads/${branch} failed: ${message(error)}`,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
if (head !== undefined) {
|
|
304
|
+
// Checkout mode's primary read is the open build worktree's file — it is
|
|
305
|
+
// the branch head. The committed ref read is the released-workspace
|
|
306
|
+
// fallback, and the recorded commit stays the resolved branch head.
|
|
307
|
+
if (workspacePath !== undefined) {
|
|
308
|
+
try {
|
|
309
|
+
const content = await readFile(join(workspacePath, path), 'utf8')
|
|
310
|
+
// The worktree file may carry uncommitted edits, so it is only the
|
|
311
|
+
// branch head's when git sees no difference between them. Compare
|
|
312
|
+
// git-normalized identity, not raw bytes: the stored blob can differ
|
|
313
|
+
// textually from the worktree file (core.autocrlf, eol attributes)
|
|
314
|
+
// while carrying the same content, so byte equality against `git show`
|
|
315
|
+
// output would misclassify a clean worktree as dirty. Hash the
|
|
316
|
+
// worktree file the way git would store it — `git hash-object` run in
|
|
317
|
+
// the workspace applies the same clean conversion as check-in — and
|
|
318
|
+
// compare against the committed blob's oid. Equal → attribute to the
|
|
319
|
+
// head commit as before; different bytes, a file the head never
|
|
320
|
+
// carried, or any failed comparison (a non-git workspace, a transient
|
|
321
|
+
// git error) → `worktree-dirty` with no commit: the provenance
|
|
322
|
+
// declines to attribute worktree bytes to any commit. A comparison
|
|
323
|
+
// failure is caught here, not by the committed-ref fallback below —
|
|
324
|
+
// it degrades to dirty, never to a different source.
|
|
325
|
+
let clean = false
|
|
326
|
+
try {
|
|
327
|
+
const [committedBlob, worktreeBlob] = await Promise.all([
|
|
328
|
+
git(['rev-parse', '--verify', `${head}:${path}`], checkout, exec),
|
|
329
|
+
exec(['git', 'hash-object', '--', path], { cwd: workspacePath }),
|
|
330
|
+
])
|
|
331
|
+
clean =
|
|
332
|
+
committedBlob.exitCode === 0 &&
|
|
333
|
+
worktreeBlob.exitCode === 0 &&
|
|
334
|
+
committedBlob.stdout.trim() === worktreeBlob.stdout.trim()
|
|
335
|
+
} catch {
|
|
336
|
+
clean = false
|
|
337
|
+
}
|
|
338
|
+
if (clean) {
|
|
339
|
+
// A malformed worktree file is terminal: the committed ref carries the
|
|
340
|
+
// same content, so the recorded base is not the build's pipeline
|
|
341
|
+
// either. The caller keeps the build's last good deposit.
|
|
342
|
+
return parsePipeline(head, 'branch-head', content, onFailure)
|
|
343
|
+
}
|
|
344
|
+
return parsePipeline(undefined, 'worktree-dirty', content, onFailure)
|
|
345
|
+
} catch (error) {
|
|
346
|
+
// Workspace released or unreadable: read the committed branch head.
|
|
347
|
+
onFailure({
|
|
348
|
+
kind: 'unpublished',
|
|
349
|
+
detail: `workspace file ${join(workspacePath, path)} unreadable: ${message(error)}`,
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
let content: string | undefined
|
|
354
|
+
try {
|
|
355
|
+
content = await readCheckoutFile(head, path, checkout, exec)
|
|
356
|
+
} catch (error) {
|
|
357
|
+
// The ref resolved but its file did not come back: corruption or an
|
|
358
|
+
// exec failure — actionable, unlike an unpublished branch.
|
|
359
|
+
onFailure({
|
|
360
|
+
kind: 'branch-source',
|
|
361
|
+
detail: `git show ${head}:${path} failed: ${message(error)}`,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
if (content !== undefined) {
|
|
365
|
+
// Malformed committed branch file: terminal, as above.
|
|
366
|
+
const parsed = parsePipeline(head, 'branch-head', content, onFailure)
|
|
367
|
+
if (parsed === undefined) return undefined
|
|
368
|
+
return parsed
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const baseSha = recordedBaseSha(events)
|
|
372
|
+
if (baseSha === undefined) {
|
|
373
|
+
onFailure({ kind: 'no-base', detail: 'no recorded base commit (legacy build)' })
|
|
374
|
+
return undefined
|
|
375
|
+
}
|
|
376
|
+
let baseContent: string | undefined
|
|
377
|
+
try {
|
|
378
|
+
baseContent = await readCheckoutFile(baseSha, path, checkout, exec)
|
|
379
|
+
} catch (error) {
|
|
380
|
+
// Base file unreadable: legacy fallback.
|
|
381
|
+
onFailure({
|
|
382
|
+
kind: 'base-source',
|
|
383
|
+
detail: `git show ${baseSha}:${path} failed: ${message(error)}`,
|
|
384
|
+
})
|
|
385
|
+
return undefined
|
|
386
|
+
}
|
|
387
|
+
return parsePipeline(baseSha, 'base', baseContent, onFailure)
|
|
388
|
+
}
|