@defrex/autobuild 0.11.0 → 0.13.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 +3 -1
- package/SPEC.md +335 -31
- package/docs/architecture.md +125 -7
- package/docs/configuration.md +142 -21
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +240 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +97 -7
- package/docs/remote-store-protocol.md +199 -144
- package/docs/remote-ticket-protocol.md +249 -19
- package/docs/setup.md +199 -16
- package/docs/ticket-dependencies.md +1 -1
- package/docs/ticket-migration.md +64 -0
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/build-control.ts +12 -1
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +364 -0
- package/packages/core/src/cli/context.ts +7 -1
- package/packages/core/src/cli/dashboard/model.ts +4 -5
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch.ts +336 -41
- package/packages/core/src/cli/harvest.ts +13 -3
- package/packages/core/src/cli/help.ts +51 -16
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +60 -16
- package/packages/core/src/cli/mcp.ts +9 -2
- package/packages/core/src/cli/pr-summary-publication.ts +220 -0
- package/packages/core/src/cli/pr-summary.ts +2 -0
- package/packages/core/src/cli/project-credentials.ts +132 -0
- package/packages/core/src/cli/repo-state.ts +15 -2
- package/packages/core/src/cli/self-update.ts +15 -1
- package/packages/core/src/cli/store-opening.ts +22 -14
- package/packages/core/src/cli/terminals.ts +0 -10
- package/packages/core/src/cli/ticket-assets.ts +7 -2
- package/packages/core/src/cli/ticket-editor.ts +82 -0
- package/packages/core/src/cli/ticket.ts +172 -35
- package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
- package/packages/core/src/cli/wait.ts +84 -17
- package/packages/core/src/cli/watch.ts +197 -60
- package/packages/core/src/config/live.ts +3 -0
- package/packages/core/src/config/schema.ts +90 -11
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +11 -2
- package/packages/core/src/events/payloads.ts +50 -0
- package/packages/core/src/events/repository.ts +13 -0
- package/packages/core/src/integration/harness.ts +37 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +145 -83
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +11 -1
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index.ts +17 -0
- package/packages/core/src/kernel/pr-summary.ts +89 -0
- package/packages/core/src/kernel/reducer-registry.ts +10 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/ontology.ts +2 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/index.ts +12 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +38 -16
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/ticket-source-backend.ts +17 -20
- package/packages/core/src/operator/tickets.ts +136 -28
- package/packages/core/src/plugin-sdk/index.ts +7 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +7 -1
- package/packages/core/src/ports/forge/fake.ts +13 -1
- package/packages/core/src/ports/forge/github-transport.ts +36 -1
- package/packages/core/src/ports/forge/github.ts +29 -5
- package/packages/core/src/ports/forge/local-git.ts +12 -4
- package/packages/core/src/ports/forge/summary-marker.ts +8 -0
- package/packages/core/src/ports/runner/session-env.ts +3 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +54 -10
- package/packages/core/src/ports/tickets/fake.ts +30 -1
- package/packages/core/src/ports/tickets/file-assets.ts +159 -0
- package/packages/core/src/ports/tickets/file-codec.ts +90 -0
- package/packages/core/src/ports/tickets/file.ts +114 -120
- package/packages/core/src/ports/tickets/grants.ts +47 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +17 -3
- package/packages/core/src/ports/tickets/identity.ts +20 -0
- package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
- package/packages/core/src/ports/tickets/linear.ts +74 -9
- package/packages/core/src/ports/tickets/migration.ts +336 -0
- package/packages/core/src/ports/tickets/operations.ts +38 -7
- package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +96 -0
- package/packages/core/src/ports/tickets/remote.ts +61 -3
- package/packages/core/src/ports/tickets/requested-id.ts +46 -0
- package/packages/core/src/ports/tickets/revision.ts +62 -0
- package/packages/core/src/ports/tickets/states.ts +61 -0
- package/packages/core/src/ports/types.ts +40 -3
- package/packages/core/src/ports/workspace/build-execution.ts +6 -1
- package/packages/core/src/ports/workspace/create.ts +15 -2
- package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
- package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
- package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +51 -19
- package/packages/core/src/processes/build-execution-state.ts +1 -1
- package/packages/core/src/processes/build-runner.ts +87 -26
- package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
- package/packages/core/src/processes/dispatcher.ts +168 -41
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +31 -4
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +166 -104
- package/packages/core/src/processes/harvest.ts +34 -14
- package/packages/core/src/processes/phase-failure-budget.ts +2 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/processes/repo-view.ts +35 -0
- package/packages/core/src/store/contract.ts +32 -1
- package/packages/core/src/store/local/store.ts +1 -1
- package/packages/core/src/store/remote/client.ts +1 -1
- package/packages/core/src/store/remote/compatibility.ts +90 -0
- package/packages/core/src/store/remote/credentials.ts +28 -0
- package/packages/core/src/store/remote/index.ts +14 -1
- package/packages/core/src/store/remote/protocol.ts +25 -0
- package/packages/core/src/store/remote/token.ts +74 -13
- package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
- package/packages/core/src/store/ticket-assets.ts +86 -8
- package/packages/core/src/store/types.ts +1 -1
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +232 -34
- package/skills/guide/references/operator-api.md +142 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +199 -144
- package/skills/guide/references/remote-ticket-protocol.md +253 -18
- package/skills/guide/references/setup.md +199 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/operate/SKILL.md +126 -36
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
|
@@ -116,11 +116,9 @@ export interface PipelineStep {
|
|
|
116
116
|
/** Absent ⇒ the step has never run in the current spec scope ⇒ no time is
|
|
117
117
|
* shown (AC 6). The renderer composes the elapsed segment from this. */
|
|
118
118
|
timing?: StepTiming
|
|
119
|
-
/** Display-only
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* build is current at merge with effective auto-merge off; the terminal
|
|
123
|
-
* row/detail and the web row/detail render the same words. */
|
|
119
|
+
/** Display-only policy skip reason or parked merge explanation. The shared
|
|
120
|
+
* row/detail renderers use this wrapping prose segment for longer reasons;
|
|
121
|
+
* it is never consulted for routing. */
|
|
124
122
|
reason?: string
|
|
125
123
|
}
|
|
126
124
|
|
|
@@ -752,6 +750,7 @@ export function projectBuildFromFacts(
|
|
|
752
750
|
: stepResults.some((r) => r.outcome === 'skipped')
|
|
753
751
|
? 'skipped'
|
|
754
752
|
: undefined,
|
|
753
|
+
reason: stepResults.find((r) => r.skipCause === 'post-reconcile-policy')?.reason,
|
|
755
754
|
count,
|
|
756
755
|
timing: timingFor(intervals, phase, cycleSince, frozenNow),
|
|
757
756
|
}),
|
|
@@ -211,7 +211,7 @@ export class DashboardBuildPollCache {
|
|
|
211
211
|
// both the cached reduction and projected row untouched.
|
|
212
212
|
next.set(
|
|
213
213
|
record.slug,
|
|
214
|
-
configChanged
|
|
214
|
+
configChanged || current.build?.ticketId !== record.ticket?.id
|
|
215
215
|
? {
|
|
216
216
|
...current,
|
|
217
217
|
build: await this.projectRow(record, current.state, current.events, config),
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { issueTicketGrant, ticketGrantRequest } from '../ports/tickets/grants'
|
|
2
|
+
import { RemoteBuildStore } from '../store/remote/client'
|
|
3
|
+
import { delegateCredential } from '../store/remote/credentials'
|
|
4
|
+
import { isRemoteStoreRef } from './repo-state'
|
|
5
|
+
import { projectToken } from './project-credentials'
|
|
6
|
+
import { settleSummaryRequests } from './pr-summary-publication'
|
|
1
7
|
import { readDispatcherLiveness, StandbyIdentityResolver } from './dispatcher-liveness'
|
|
2
8
|
import { formatDispatcherIdentity } from '../kernel/dispatcher-liveness'
|
|
3
9
|
/**
|
|
@@ -182,18 +188,6 @@ export { DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT } from '../store/retention'
|
|
|
182
188
|
// Local uses within this module.
|
|
183
189
|
import { DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT as _effectiveConfigKind } from '../store/retention'
|
|
184
190
|
|
|
185
|
-
/** JSON encoding in the config schema's declarative input shape. Parsed Config
|
|
186
|
-
* has normalized `{steps, stepConfigs}` sections; flattening named step tables
|
|
187
|
-
* lets the frontend validate the artifact with the same strict configSchema. */
|
|
188
|
-
function effectiveConfigContent(config: Config): string {
|
|
189
|
-
const { verify, finalize, ...root } = config
|
|
190
|
-
return JSON.stringify({
|
|
191
|
-
...root,
|
|
192
|
-
verify: { steps: verify.steps, ...verify.stepConfigs },
|
|
193
|
-
finalize: { steps: finalize.steps, ...finalize.stepConfigs },
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
|
|
197
191
|
/** The pre-build naming prompt. Its output is only a proposal: dispatcher.ts
|
|
198
192
|
* owns strict validation, timeout/failure fallback, and store-wide uniqueness. */
|
|
199
193
|
export function slugNamingPrompt(spec: string): string {
|
|
@@ -212,7 +206,8 @@ export function slugNamingPrompt(spec: string): string {
|
|
|
212
206
|
function definedEnv(env: Record<string, string | undefined>): Record<string, string> {
|
|
213
207
|
const defined: Record<string, string> = {}
|
|
214
208
|
for (const [key, value] of Object.entries(env)) {
|
|
215
|
-
if (value !== undefined
|
|
209
|
+
if (value !== undefined && !['AB_TOKEN', 'AB_TICKET_TOKEN', 'AB_TICKET_URL'].includes(key))
|
|
210
|
+
defined[key] = value
|
|
216
211
|
}
|
|
217
212
|
return defined
|
|
218
213
|
}
|
|
@@ -264,6 +259,7 @@ interface ResumePrompt {
|
|
|
264
259
|
/** The real adapters the loop drives — resolved by `wire` (default: the
|
|
265
260
|
* production ports; tests inject fakes). */
|
|
266
261
|
export interface DispatchWiring {
|
|
262
|
+
credentialIssuer?: typeof delegateCredential
|
|
267
263
|
store: BuildStore
|
|
268
264
|
tickets: TicketSource
|
|
269
265
|
forge: Forge
|
|
@@ -282,7 +278,7 @@ export interface DispatchWiring {
|
|
|
282
278
|
* same store as `store`, so an agent's `ab` commands write where the
|
|
283
279
|
* dispatcher reads. */
|
|
284
280
|
storeRef: string
|
|
285
|
-
/**
|
|
281
|
+
/** Private parent authority for remote exchange; never passed to execution. */
|
|
286
282
|
token?: string
|
|
287
283
|
ids: IdSource
|
|
288
284
|
uuids: UuidSource
|
|
@@ -817,6 +813,7 @@ class DispatchLoop {
|
|
|
817
813
|
forge: wiring.forge,
|
|
818
814
|
config,
|
|
819
815
|
getConfig: () => this.liveConfig.current().config,
|
|
816
|
+
repairBuildConfig: (slug) => this.publishBuildConfig(slug),
|
|
820
817
|
repo: this.repoIdentity,
|
|
821
818
|
// Origin mode: the forge answers remote base-branch questions; no
|
|
822
819
|
// checkout git is probed for the served repository's origin.
|
|
@@ -2231,7 +2228,7 @@ class DispatchLoop {
|
|
|
2231
2228
|
}
|
|
2232
2229
|
|
|
2233
2230
|
const { store, tickets, runtimes, ids, uuids, clock, storeRef, token } = this.wiring
|
|
2234
|
-
const
|
|
2231
|
+
const harvestDeps = {
|
|
2235
2232
|
store,
|
|
2236
2233
|
tickets,
|
|
2237
2234
|
config: this.currentConfig().config,
|
|
@@ -2246,16 +2243,175 @@ class DispatchLoop {
|
|
|
2246
2243
|
...(this.repoLeaseHolder !== undefined ? { leaseHolder: this.repoLeaseHolder } : {}),
|
|
2247
2244
|
sessionEnv: {
|
|
2248
2245
|
AB_STORE: storeRef,
|
|
2249
|
-
...(token !== undefined ? { AB_TOKEN: token } : {}),
|
|
2250
2246
|
},
|
|
2251
2247
|
opts: {
|
|
2252
2248
|
maxRecoveryAttempts: this.maxHarvestRecoveryAttempts,
|
|
2253
2249
|
},
|
|
2254
|
-
}
|
|
2250
|
+
}
|
|
2255
2251
|
|
|
2256
2252
|
let tracked: Promise<void>
|
|
2257
|
-
tracked =
|
|
2258
|
-
|
|
2253
|
+
tracked = (async () => {
|
|
2254
|
+
const ticketUrl =
|
|
2255
|
+
harvestDeps.config.tickets.source === 'hosted'
|
|
2256
|
+
? (harvestDeps.config.tickets.url ?? (/^https?:/i.test(storeRef) ? storeRef : undefined))
|
|
2257
|
+
: undefined
|
|
2258
|
+
if (ticketUrl && ticketUrl.replace(/\/+$/, '') !== storeRef.replace(/\/+$/, '')) {
|
|
2259
|
+
const ticketToken = projectToken(ticketUrl, this.repoIdentity, {
|
|
2260
|
+
...this.opts.env,
|
|
2261
|
+
AB_TOKEN: undefined,
|
|
2262
|
+
})
|
|
2263
|
+
if (!ticketToken)
|
|
2264
|
+
throw new Error('Independent hosted harvest requires a ticket-service project credential')
|
|
2265
|
+
const issued = await issueTicketGrant(
|
|
2266
|
+
ticketUrl,
|
|
2267
|
+
ticketToken,
|
|
2268
|
+
this.repoIdentity,
|
|
2269
|
+
harvestDeps.instance,
|
|
2270
|
+
'runner',
|
|
2271
|
+
)
|
|
2272
|
+
let remoteRunnerStore: RemoteBuildStore | undefined
|
|
2273
|
+
let preparedRun: { run: string } | undefined
|
|
2274
|
+
let buildScoped: string | undefined
|
|
2275
|
+
let renew: ReturnType<typeof setInterval> | undefined
|
|
2276
|
+
try {
|
|
2277
|
+
if (/^https?:/i.test(storeRef)) {
|
|
2278
|
+
const prepared = await new HarvestRunner(harvestDeps).run(true)
|
|
2279
|
+
if (prepared.outcome !== 'prepared') {
|
|
2280
|
+
return prepared
|
|
2281
|
+
}
|
|
2282
|
+
preparedRun = { run: prepared.run }
|
|
2283
|
+
await this.repoView.appendRepo(this.repoIdentity, {
|
|
2284
|
+
actor: DISPATCHER,
|
|
2285
|
+
type: 'harvest.execution.authorized',
|
|
2286
|
+
payload: {
|
|
2287
|
+
execution: harvestDeps.instance,
|
|
2288
|
+
run: prepared.run,
|
|
2289
|
+
launch: prepared.launch,
|
|
2290
|
+
leaseHolder: this.repoLeaseHolder ?? harvestDeps.instance,
|
|
2291
|
+
},
|
|
2292
|
+
})
|
|
2293
|
+
buildScoped = await (this.wiring.credentialIssuer ?? delegateCredential)(
|
|
2294
|
+
storeRef,
|
|
2295
|
+
token,
|
|
2296
|
+
{
|
|
2297
|
+
kind: 'harvest-runner',
|
|
2298
|
+
execution: harvestDeps.instance,
|
|
2299
|
+
repo: this.repoIdentity,
|
|
2300
|
+
id: prepared.run,
|
|
2301
|
+
leaseHolder: this.repoLeaseHolder ?? harvestDeps.instance,
|
|
2302
|
+
},
|
|
2303
|
+
)
|
|
2304
|
+
remoteRunnerStore = new RemoteBuildStore({ url: storeRef, token: buildScoped })
|
|
2305
|
+
}
|
|
2306
|
+
const sessionEnv = {
|
|
2307
|
+
...harvestDeps.sessionEnv,
|
|
2308
|
+
...(buildScoped ? { AB_TOKEN: buildScoped } : {}),
|
|
2309
|
+
AB_TICKET_URL: ticketUrl,
|
|
2310
|
+
AB_TICKET_TOKEN: issued.token,
|
|
2311
|
+
}
|
|
2312
|
+
let renewalFailure: unknown
|
|
2313
|
+
renew = setInterval(() => {
|
|
2314
|
+
void ticketGrantRequest(ticketUrl, issued.token, {
|
|
2315
|
+
action: 'renew',
|
|
2316
|
+
repo: this.repoIdentity,
|
|
2317
|
+
id: issued.grant.id,
|
|
2318
|
+
}).catch((error) => {
|
|
2319
|
+
renewalFailure = error
|
|
2320
|
+
})
|
|
2321
|
+
}, 60_000)
|
|
2322
|
+
const runnerTickets = await createTicketSource(
|
|
2323
|
+
harvestDeps.config.tickets,
|
|
2324
|
+
{ ...this.opts.env, ...sessionEnv, AB_TOKEN: undefined },
|
|
2325
|
+
this.opts.targetRepo,
|
|
2326
|
+
undefined,
|
|
2327
|
+
this.wiring.plugins,
|
|
2328
|
+
)
|
|
2329
|
+
const result = await new HarvestRunner({
|
|
2330
|
+
...harvestDeps,
|
|
2331
|
+
...(remoteRunnerStore ? { store: remoteRunnerStore } : {}),
|
|
2332
|
+
...(preparedRun ? { preparedRun } : {}),
|
|
2333
|
+
tickets: runnerTickets,
|
|
2334
|
+
sessionEnv,
|
|
2335
|
+
}).run()
|
|
2336
|
+
if (renewalFailure) throw renewalFailure
|
|
2337
|
+
return result
|
|
2338
|
+
} finally {
|
|
2339
|
+
if (renew) clearInterval(renew)
|
|
2340
|
+
try {
|
|
2341
|
+
if (remoteRunnerStore && preparedRun) {
|
|
2342
|
+
await remoteRunnerStore.appendRepo(this.repoIdentity, {
|
|
2343
|
+
actor: DISPATCHER,
|
|
2344
|
+
type: 'harvest.execution.ended',
|
|
2345
|
+
payload: { execution: harvestDeps.instance, run: preparedRun.run },
|
|
2346
|
+
})
|
|
2347
|
+
await remoteRunnerStore.close()
|
|
2348
|
+
}
|
|
2349
|
+
} finally {
|
|
2350
|
+
await ticketGrantRequest(ticketUrl, ticketToken, {
|
|
2351
|
+
action: 'revoke',
|
|
2352
|
+
repo: this.repoIdentity,
|
|
2353
|
+
id: issued.grant.id,
|
|
2354
|
+
})
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
if (!/^https?:/i.test(storeRef)) return new HarvestRunner(harvestDeps).run()
|
|
2359
|
+
const prepared = await new HarvestRunner(harvestDeps).run(true)
|
|
2360
|
+
if (prepared.outcome !== 'prepared') return prepared
|
|
2361
|
+
await this.repoView.appendRepo(this.repoIdentity, {
|
|
2362
|
+
actor: DISPATCHER,
|
|
2363
|
+
type: 'harvest.execution.authorized',
|
|
2364
|
+
payload: {
|
|
2365
|
+
execution: harvestDeps.instance,
|
|
2366
|
+
run: prepared.run,
|
|
2367
|
+
launch: prepared.launch,
|
|
2368
|
+
leaseHolder: this.repoLeaseHolder ?? harvestDeps.instance,
|
|
2369
|
+
},
|
|
2370
|
+
})
|
|
2371
|
+
const scoped = await (this.wiring.credentialIssuer ?? delegateCredential)(storeRef, token, {
|
|
2372
|
+
kind: 'harvest-runner',
|
|
2373
|
+
execution: harvestDeps.instance,
|
|
2374
|
+
repo: this.repoIdentity,
|
|
2375
|
+
id: prepared.run,
|
|
2376
|
+
leaseHolder: this.repoLeaseHolder ?? harvestDeps.instance,
|
|
2377
|
+
})
|
|
2378
|
+
const runnerStore = new RemoteBuildStore({ url: storeRef, token: scoped })
|
|
2379
|
+
try {
|
|
2380
|
+
const runnerTickets =
|
|
2381
|
+
harvestDeps.config.tickets.source === 'hosted'
|
|
2382
|
+
? await createTicketSource(
|
|
2383
|
+
harvestDeps.config.tickets,
|
|
2384
|
+
{
|
|
2385
|
+
...this.opts.env,
|
|
2386
|
+
AB_STORE: storeRef,
|
|
2387
|
+
AB_TOKEN: scoped,
|
|
2388
|
+
AB_HARVEST: prepared.run,
|
|
2389
|
+
},
|
|
2390
|
+
this.opts.targetRepo,
|
|
2391
|
+
resolveRepoStatePaths({
|
|
2392
|
+
repo: this.repoIdentity,
|
|
2393
|
+
checkout: this.opts.targetRepo,
|
|
2394
|
+
storeRef,
|
|
2395
|
+
}).localStateRoot,
|
|
2396
|
+
this.wiring.plugins,
|
|
2397
|
+
)
|
|
2398
|
+
: harvestDeps.tickets
|
|
2399
|
+
return await new HarvestRunner({
|
|
2400
|
+
...harvestDeps,
|
|
2401
|
+
store: runnerStore,
|
|
2402
|
+
tickets: runnerTickets,
|
|
2403
|
+
preparedRun: { run: prepared.run },
|
|
2404
|
+
sessionEnv: { AB_STORE: storeRef, AB_TOKEN: scoped },
|
|
2405
|
+
}).run()
|
|
2406
|
+
} finally {
|
|
2407
|
+
await runnerStore.appendRepo(this.repoIdentity, {
|
|
2408
|
+
actor: DISPATCHER,
|
|
2409
|
+
type: 'harvest.execution.ended',
|
|
2410
|
+
payload: { execution: harvestDeps.instance, run: prepared.run },
|
|
2411
|
+
})
|
|
2412
|
+
await runnerStore.close()
|
|
2413
|
+
}
|
|
2414
|
+
})()
|
|
2259
2415
|
.then((result) => {
|
|
2260
2416
|
this.recordHarvestResult(result)
|
|
2261
2417
|
if (
|
|
@@ -2299,6 +2455,24 @@ class DispatchLoop {
|
|
|
2299
2455
|
* contract: provisioning takes minutes and must never stall a tick; the
|
|
2300
2456
|
* single-flight guard covers the whole span. Every failure is contained
|
|
2301
2457
|
* and retried on a later threshold tick. */
|
|
2458
|
+
private async prepareHarvest(instance: string): Promise<HarvestRunnerResult> {
|
|
2459
|
+
return new HarvestRunner({
|
|
2460
|
+
store: this.wiring.store,
|
|
2461
|
+
tickets: this.wiring.tickets,
|
|
2462
|
+
config: this.currentConfig().config,
|
|
2463
|
+
getConfig: () => this.currentConfig().config,
|
|
2464
|
+
runtimes: this.wiring.runtimes,
|
|
2465
|
+
repo: this.repoIdentity,
|
|
2466
|
+
workspacePath: this.opts.targetRepo,
|
|
2467
|
+
ids: this.wiring.ids,
|
|
2468
|
+
uuids: this.wiring.uuids,
|
|
2469
|
+
clock: this.wiring.clock,
|
|
2470
|
+
instance,
|
|
2471
|
+
...(this.repoLeaseHolder ? { leaseHolder: this.repoLeaseHolder } : {}),
|
|
2472
|
+
opts: { maxRecoveryAttempts: this.maxHarvestRecoveryAttempts },
|
|
2473
|
+
}).run(true)
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2302
2476
|
private async launchHostedHarvest(
|
|
2303
2477
|
harvestExecution: HarvestExecution,
|
|
2304
2478
|
execution: string,
|
|
@@ -2307,6 +2481,8 @@ class DispatchLoop {
|
|
|
2307
2481
|
const repo = this.repoIdentity
|
|
2308
2482
|
let handle: BuildExecutionHandle | undefined
|
|
2309
2483
|
let startedSeq: number | undefined
|
|
2484
|
+
let ticketAuthority: { url: string; token: string; id: string } | undefined
|
|
2485
|
+
let renewTickets: ReturnType<typeof setInterval> | undefined
|
|
2310
2486
|
// Shared with `stopHostedHarvest` through the `hostedHarvest` entry: a
|
|
2311
2487
|
// detach must stay observable to the completion chain even after the
|
|
2312
2488
|
// entry is cleared, or the chain would classify and reap a live guest.
|
|
@@ -2349,13 +2525,76 @@ class DispatchLoop {
|
|
|
2349
2525
|
if (pressure.trigger === undefined) return
|
|
2350
2526
|
}
|
|
2351
2527
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2528
|
+
const prepared = await this.prepareHarvest(execution)
|
|
2529
|
+
if (prepared.outcome !== 'prepared') return
|
|
2530
|
+
await this.repoView.appendRepo(repo, {
|
|
2531
|
+
actor: DISPATCHER,
|
|
2532
|
+
type: 'harvest.execution.authorized',
|
|
2533
|
+
payload: {
|
|
2534
|
+
execution,
|
|
2535
|
+
run: prepared.run,
|
|
2536
|
+
launch: prepared.launch,
|
|
2537
|
+
leaseHolder: this.repoLeaseHolder ?? execution,
|
|
2538
|
+
},
|
|
2358
2539
|
})
|
|
2540
|
+
const launchAuth = /^https?:/i.test(storeRef)
|
|
2541
|
+
? {
|
|
2542
|
+
token: await (this.wiring.credentialIssuer ?? delegateCredential)(
|
|
2543
|
+
storeRef,
|
|
2544
|
+
this.wiring.token,
|
|
2545
|
+
{
|
|
2546
|
+
kind: 'harvest-runner',
|
|
2547
|
+
execution,
|
|
2548
|
+
repo,
|
|
2549
|
+
id: prepared.run,
|
|
2550
|
+
leaseHolder: this.repoLeaseHolder ?? execution,
|
|
2551
|
+
},
|
|
2552
|
+
),
|
|
2553
|
+
}
|
|
2554
|
+
: undefined
|
|
2555
|
+
const ticketConfig = this.currentConfig().config.tickets
|
|
2556
|
+
if (
|
|
2557
|
+
ticketConfig.source === 'hosted' &&
|
|
2558
|
+
ticketConfig.url &&
|
|
2559
|
+
ticketConfig.url.replace(/\/+$/, '') !== storeRef.replace(/\/+$/, '')
|
|
2560
|
+
) {
|
|
2561
|
+
const ticketToken = projectToken(ticketConfig.url, repo, {
|
|
2562
|
+
...this.opts.env,
|
|
2563
|
+
AB_TOKEN: undefined,
|
|
2564
|
+
})
|
|
2565
|
+
if (!ticketToken || !launchAuth)
|
|
2566
|
+
throw new Error('Independent harvest requires scoped build and ticket credentials')
|
|
2567
|
+
const issued = await issueTicketGrant(
|
|
2568
|
+
ticketConfig.url,
|
|
2569
|
+
ticketToken,
|
|
2570
|
+
repo,
|
|
2571
|
+
execution,
|
|
2572
|
+
'runner',
|
|
2573
|
+
)
|
|
2574
|
+
ticketAuthority = { url: ticketConfig.url, token: issued.token, id: issued.grant.id }
|
|
2575
|
+
renewTickets = setInterval(() => {
|
|
2576
|
+
void ticketGrantRequest(ticketConfig.url!, issued.token, {
|
|
2577
|
+
action: 'renew',
|
|
2578
|
+
repo,
|
|
2579
|
+
id: issued.grant.id,
|
|
2580
|
+
}).catch(() => {
|
|
2581
|
+
// Expiry fails subsequent reads closed; the guest cannot regain project authority.
|
|
2582
|
+
})
|
|
2583
|
+
}, 60_000)
|
|
2584
|
+
}
|
|
2585
|
+
handle = await harvestExecution.start(
|
|
2586
|
+
{
|
|
2587
|
+
preparedRun: { run: prepared.run },
|
|
2588
|
+
storeRef,
|
|
2589
|
+
repo,
|
|
2590
|
+
instance: execution,
|
|
2591
|
+
baseBranch: this.currentConfig().config.baseBranch,
|
|
2592
|
+
...(this.repoLeaseHolder !== undefined ? { leaseHolder: this.repoLeaseHolder } : {}),
|
|
2593
|
+
},
|
|
2594
|
+
launchAuth && ticketAuthority
|
|
2595
|
+
? { ...launchAuth, tickets: { url: ticketAuthority.url, token: ticketAuthority.token } }
|
|
2596
|
+
: launchAuth,
|
|
2597
|
+
)
|
|
2359
2598
|
const identity = handle.identity
|
|
2360
2599
|
if (
|
|
2361
2600
|
identity === undefined ||
|
|
@@ -2432,6 +2671,13 @@ class DispatchLoop {
|
|
|
2432
2671
|
)
|
|
2433
2672
|
}
|
|
2434
2673
|
} finally {
|
|
2674
|
+
if (renewTickets) clearInterval(renewTickets)
|
|
2675
|
+
if (ticketAuthority && !detached.value)
|
|
2676
|
+
await ticketGrantRequest(ticketAuthority.url, ticketAuthority.token, {
|
|
2677
|
+
action: 'revoke',
|
|
2678
|
+
repo,
|
|
2679
|
+
id: ticketAuthority.id,
|
|
2680
|
+
})
|
|
2435
2681
|
this.hostedHarvest = undefined
|
|
2436
2682
|
}
|
|
2437
2683
|
}
|
|
@@ -2628,7 +2874,7 @@ class DispatchLoop {
|
|
|
2628
2874
|
return null
|
|
2629
2875
|
}
|
|
2630
2876
|
|
|
2631
|
-
private async settlePendingPublication(slug: string): Promise<void> {
|
|
2877
|
+
private async settlePendingPublication(slug: string, reservation?: string): Promise<void> {
|
|
2632
2878
|
// The guest appended through its own handle; settle against fresh state.
|
|
2633
2879
|
await this.repoView.refreshBuild(slug)
|
|
2634
2880
|
await settleWorkspacePublication(
|
|
@@ -2645,6 +2891,16 @@ class DispatchLoop {
|
|
|
2645
2891
|
},
|
|
2646
2892
|
slug,
|
|
2647
2893
|
)
|
|
2894
|
+
await settleSummaryRequests({
|
|
2895
|
+
store: this.repoView,
|
|
2896
|
+
forge: this.wiring.forge,
|
|
2897
|
+
slug,
|
|
2898
|
+
workspacePath: this.opts.targetRepo,
|
|
2899
|
+
storeRef: this.wiring.storeRef,
|
|
2900
|
+
clock: this.wiring.clock,
|
|
2901
|
+
holder: `summary:${this.runId}`,
|
|
2902
|
+
...(reservation === undefined ? {} : { reservation }),
|
|
2903
|
+
})
|
|
2648
2904
|
}
|
|
2649
2905
|
|
|
2650
2906
|
private async recordInfrastructureFailure(input: {
|
|
@@ -2736,13 +2992,25 @@ class DispatchLoop {
|
|
|
2736
2992
|
// A prior publication acknowledgement may have been lost after its push
|
|
2737
2993
|
// reached the durable branch. Reconcile that fact before a replacement
|
|
2738
2994
|
// runner can rerun the phase and create a non-fast-forward successor.
|
|
2739
|
-
await this.settlePendingPublication(slug)
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2995
|
+
await this.settlePendingPublication(slug, instance)
|
|
2996
|
+
const launchAuth = /^https?:/i.test(this.wiring.storeRef)
|
|
2997
|
+
? {
|
|
2998
|
+
token: await (this.wiring.credentialIssuer ?? delegateCredential)(
|
|
2999
|
+
this.wiring.storeRef,
|
|
3000
|
+
this.wiring.token,
|
|
3001
|
+
{ kind: 'build-runner', id: slug },
|
|
3002
|
+
),
|
|
3003
|
+
}
|
|
3004
|
+
: undefined
|
|
3005
|
+
const handle = await owning.execution.start(
|
|
3006
|
+
{
|
|
3007
|
+
slug,
|
|
3008
|
+
storeRef: this.wiring.storeRef,
|
|
3009
|
+
instance,
|
|
3010
|
+
workspaceRef,
|
|
3011
|
+
},
|
|
3012
|
+
launchAuth,
|
|
3013
|
+
)
|
|
2746
3014
|
active.handle = handle
|
|
2747
3015
|
const identity = handle.identity ?? {
|
|
2748
3016
|
provider: owning.name,
|
|
@@ -3694,16 +3962,21 @@ function acceptableOriginModeStore(storeRef: string): boolean {
|
|
|
3694
3962
|
* names the operator's own machine (local development, integration harnesses).
|
|
3695
3963
|
* Requirements are validated here so a misconfigured origin-mode launch fails
|
|
3696
3964
|
* before any side effect. */
|
|
3697
|
-
async function resolveOriginModeState(
|
|
3965
|
+
async function resolveOriginModeState(
|
|
3966
|
+
opts: DispatchOpts,
|
|
3967
|
+
configStore?: string,
|
|
3968
|
+
): Promise<RepoStatePaths> {
|
|
3698
3969
|
const identity = normalizeGitRemoteUrl(opts.repository!)
|
|
3699
|
-
const selectedStore = opts.storeRef
|
|
3970
|
+
const selectedStore = opts.storeRef?.trim() || opts.env.AB_STORE?.trim() || configStore
|
|
3700
3971
|
if (selectedStore === undefined || !acceptableOriginModeStore(selectedStore)) {
|
|
3701
3972
|
throw new Error(
|
|
3702
3973
|
'origin-mode dispatch requires an HTTPS BuildStore — set AB_STORE (or --store) to the hosted Store URL',
|
|
3703
3974
|
)
|
|
3704
3975
|
}
|
|
3705
|
-
if (
|
|
3706
|
-
throw new Error(
|
|
3976
|
+
if (!projectToken(selectedStore, identity, opts.env)) {
|
|
3977
|
+
throw new Error(
|
|
3978
|
+
'origin-mode dispatch requires a project credential; run ab connect or set AB_TOKEN',
|
|
3979
|
+
)
|
|
3707
3980
|
}
|
|
3708
3981
|
// Checkout-less hosts (serverless functions, sandboxes) have no gh CLI and
|
|
3709
3982
|
// no keyring, and the only workspace provider origin mode can run —
|
|
@@ -3721,6 +3994,7 @@ async function resolveOriginModeState(opts: DispatchOpts): Promise<RepoStatePath
|
|
|
3721
3994
|
await mkdir(scratch, { recursive: true })
|
|
3722
3995
|
return resolveRepoStatePaths({
|
|
3723
3996
|
repo: identity,
|
|
3997
|
+
configStore,
|
|
3724
3998
|
checkout: scratch,
|
|
3725
3999
|
...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
|
|
3726
4000
|
...(opts.env.AB_STORE !== undefined ? { envStore: opts.env.AB_STORE } : {}),
|
|
@@ -3743,9 +4017,23 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3743
4017
|
if (opts.wire !== undefined && opts.nonStoreWire !== undefined) {
|
|
3744
4018
|
throw new Error('dispatch wire and nonStoreWire are mutually exclusive')
|
|
3745
4019
|
}
|
|
4020
|
+
const explicitOriginStore = opts.storeRef?.trim() || opts.env.AB_STORE?.trim()
|
|
4021
|
+
if (
|
|
4022
|
+
opts.repository !== undefined &&
|
|
4023
|
+
explicitOriginStore &&
|
|
4024
|
+
!acceptableOriginModeStore(explicitOriginStore)
|
|
4025
|
+
) {
|
|
4026
|
+
throw new Error(
|
|
4027
|
+
'origin-mode dispatch requires an HTTPS BuildStore — set AB_STORE (or --store) to the hosted Store URL',
|
|
4028
|
+
)
|
|
4029
|
+
}
|
|
4030
|
+
const originConfig =
|
|
4031
|
+
opts.repository !== undefined
|
|
4032
|
+
? await fetchOriginModeConfig(opts, opts.originConfigTransport)
|
|
4033
|
+
: undefined
|
|
3746
4034
|
const state =
|
|
3747
4035
|
opts.repository !== undefined
|
|
3748
|
-
? await resolveOriginModeState(opts)
|
|
4036
|
+
? await resolveOriginModeState(opts, originConfig?.config.store?.url)
|
|
3749
4037
|
: await resolveRepoState({
|
|
3750
4038
|
targetRepo: opts.targetRepo,
|
|
3751
4039
|
exec: opts.exec,
|
|
@@ -3759,6 +4047,13 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3759
4047
|
const resolvedOpts: DispatchOpts = {
|
|
3760
4048
|
...opts,
|
|
3761
4049
|
targetRepo: state.checkout,
|
|
4050
|
+
env: {
|
|
4051
|
+
...opts.env,
|
|
4052
|
+
AB_STORE: state.storeRef,
|
|
4053
|
+
...(isRemoteStoreRef(state.storeRef)
|
|
4054
|
+
? { AB_TOKEN: projectToken(state.storeRef, state.repo, opts.env) }
|
|
4055
|
+
: {}),
|
|
4056
|
+
},
|
|
3762
4057
|
storeRef: state.storeRef,
|
|
3763
4058
|
repo: opts.repo ?? state.repo,
|
|
3764
4059
|
}
|
|
@@ -3816,7 +4111,7 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3816
4111
|
if (opts.repository !== undefined) {
|
|
3817
4112
|
// Origin mode startup config: the base branch's autobuild.toml, fetched
|
|
3818
4113
|
// through the forge (default branch first — baseBranch itself is config).
|
|
3819
|
-
const fetched =
|
|
4114
|
+
const fetched = originConfig!
|
|
3820
4115
|
configContent = fetched.content
|
|
3821
4116
|
config = fetched.config
|
|
3822
4117
|
if (config.forge !== 'github') {
|
|
@@ -3930,7 +4225,7 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3930
4225
|
},
|
|
3931
4226
|
{
|
|
3932
4227
|
kind: _effectiveConfigKind,
|
|
3933
|
-
content:
|
|
4228
|
+
content: effectiveBuildConfigContent(effectiveConfig),
|
|
3934
4229
|
metadata: { run: runId, effectiveChanged },
|
|
3935
4230
|
},
|
|
3936
4231
|
],
|
|
@@ -3995,7 +4290,7 @@ export async function abDispatch(opts: DispatchOpts): Promise<void> {
|
|
|
3995
4290
|
[
|
|
3996
4291
|
{
|
|
3997
4292
|
kind: _effectiveConfigKind,
|
|
3998
|
-
content:
|
|
4293
|
+
content: effectiveBuildConfigContent(config),
|
|
3999
4294
|
metadata: { run: runId, revision: 0 },
|
|
4000
4295
|
},
|
|
4001
4296
|
],
|
|
@@ -184,10 +184,20 @@ export async function buildHarvestContext(deps: HarvestCliDeps): Promise<Harvest
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
async function readJsonFile(path: string): Promise<unknown> {
|
|
187
|
-
|
|
188
|
-
if (!(await file.exists())) throw new Error(`file not found: ${path}`)
|
|
187
|
+
let text: string
|
|
189
188
|
try {
|
|
190
|
-
|
|
189
|
+
text = await Bun.file(path).text()
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
192
|
+
throw new Error(`file not found: ${path}`, { cause: error })
|
|
193
|
+
}
|
|
194
|
+
throw new Error(
|
|
195
|
+
`cannot read ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
196
|
+
{ cause: error },
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(text)
|
|
191
201
|
} catch (error) {
|
|
192
202
|
throw new Error(
|
|
193
203
|
`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|