@defrex/autobuild 0.8.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/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FakeForge (SPEC §3.2): in-memory Forge for seam tests. Gate existence and
|
|
3
|
+
* current PR merge state are independent inputs, matching GitHubForge's safety
|
|
4
|
+
* model; every native/direct mutation is journaled.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
classifyAutoMergeEnable,
|
|
8
|
+
type MergeGatePresence,
|
|
9
|
+
type MergeStateStatus,
|
|
10
|
+
} from '../../kernel/auto-merge'
|
|
11
|
+
import type {
|
|
12
|
+
AutoMergeDeferralReason,
|
|
13
|
+
AutoMergeResult,
|
|
14
|
+
ClosePrResult,
|
|
15
|
+
PrAttachmentHosting,
|
|
16
|
+
PrAttachmentReclaimRequest,
|
|
17
|
+
PrAttachmentUploadRequest,
|
|
18
|
+
Forge,
|
|
19
|
+
PrRef,
|
|
20
|
+
PrState,
|
|
21
|
+
} from '../types'
|
|
22
|
+
import type { HostedPrAttachmentAsset } from '../../ontology'
|
|
23
|
+
|
|
24
|
+
export interface PushRecord {
|
|
25
|
+
workspacePath: string
|
|
26
|
+
branch: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface OpenPrRecord {
|
|
30
|
+
workspacePath: string
|
|
31
|
+
head: string
|
|
32
|
+
base: string
|
|
33
|
+
title: string
|
|
34
|
+
body: string
|
|
35
|
+
mergeMessage?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CommentRecord {
|
|
39
|
+
workspacePath: string
|
|
40
|
+
number: number
|
|
41
|
+
body: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GetPrStateRecord {
|
|
45
|
+
workspacePath: string
|
|
46
|
+
number: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AutoMergeRecord {
|
|
50
|
+
workspacePath: string
|
|
51
|
+
number: number
|
|
52
|
+
enabled: boolean
|
|
53
|
+
/** False on an idempotent retry or a result that leaves native state off. */
|
|
54
|
+
changed: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SquashMergeRecord {
|
|
58
|
+
workspacePath: string
|
|
59
|
+
number: number
|
|
60
|
+
expectedHeadSha: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ClosePrRecord {
|
|
64
|
+
workspacePath: string
|
|
65
|
+
number: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DeleteBranchRecord {
|
|
69
|
+
workspacePath: string
|
|
70
|
+
branch: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type PrAttachmentUploadRecord = PrAttachmentUploadRequest
|
|
74
|
+
export type PrAttachmentReclaimRecord = PrAttachmentReclaimRequest
|
|
75
|
+
|
|
76
|
+
/** Constant sha, or derived from the assigned PR number. */
|
|
77
|
+
export type HeadSha = string | ((number: number) => string)
|
|
78
|
+
export type MergeSha = string | ((number: number) => string)
|
|
79
|
+
|
|
80
|
+
type FakeGateState = MergeGatePresence | AutoMergeDeferralReason
|
|
81
|
+
|
|
82
|
+
export class FakeForge implements Forge {
|
|
83
|
+
readonly name = 'fake'
|
|
84
|
+
readonly prAttachments?: PrAttachmentHosting
|
|
85
|
+
|
|
86
|
+
/** Journals — public so tests assert directly on call order and args. */
|
|
87
|
+
readonly pushes: PushRecord[] = []
|
|
88
|
+
readonly opened: OpenPrRecord[] = []
|
|
89
|
+
readonly comments: CommentRecord[] = []
|
|
90
|
+
readonly getPrStateCalls: GetPrStateRecord[] = []
|
|
91
|
+
readonly autoMergeCalls: AutoMergeRecord[] = []
|
|
92
|
+
readonly squashMergeCalls: SquashMergeRecord[] = []
|
|
93
|
+
readonly closePrCalls: ClosePrRecord[] = []
|
|
94
|
+
readonly deleteBranchCalls: DeleteBranchRecord[] = []
|
|
95
|
+
readonly prAttachmentUploads: PrAttachmentUploadRecord[] = []
|
|
96
|
+
readonly prAttachmentReclaims: PrAttachmentReclaimRecord[] = []
|
|
97
|
+
|
|
98
|
+
private nextNumber = 1
|
|
99
|
+
private nextAttachmentAssetId = 1
|
|
100
|
+
private headSha: HeadSha
|
|
101
|
+
private mergeSha: MergeSha
|
|
102
|
+
private readonly defaultGatePresence: MergeGatePresence
|
|
103
|
+
private readonly prs = new Map<number, PrState>()
|
|
104
|
+
private readonly headShas = new Map<number, string>()
|
|
105
|
+
private readonly mergeStates = new Map<number, MergeStateStatus>()
|
|
106
|
+
private readonly gates = new Map<number, FakeGateState>()
|
|
107
|
+
private readonly autoMerge = new Map<number, boolean>()
|
|
108
|
+
private readonly nativeDeferrals = new Map<number, AutoMergeDeferralReason>()
|
|
109
|
+
private readonly squashErrors = new Map<number, string>()
|
|
110
|
+
private readonly attachmentAssets = new Map<string, HostedPrAttachmentAsset>()
|
|
111
|
+
private readonly attachmentUploadErrors: string[] = []
|
|
112
|
+
private readonly attachmentReclaimErrors: string[] = []
|
|
113
|
+
private readonly closePrErrors: string[] = []
|
|
114
|
+
private readonly deleteBranchErrors: string[] = []
|
|
115
|
+
private readonly publishedBranches = new Set<string>()
|
|
116
|
+
|
|
117
|
+
constructor(
|
|
118
|
+
opts: {
|
|
119
|
+
headSha?: HeadSha
|
|
120
|
+
mergeSha?: MergeSha
|
|
121
|
+
gatePresence?: MergeGatePresence
|
|
122
|
+
/** Hosting is unsupported by default, matching a forge without this
|
|
123
|
+
* optional capability. Tests opt in explicitly. */
|
|
124
|
+
prAttachments?: boolean
|
|
125
|
+
} = {},
|
|
126
|
+
) {
|
|
127
|
+
this.headSha = opts.headSha ?? ((n) => `sha-${n}`)
|
|
128
|
+
this.mergeSha = opts.mergeSha ?? ((n) => `squash-${n}`)
|
|
129
|
+
// Existing tests model the historical, gated native path unless they opt
|
|
130
|
+
// into the ungated repository scenario explicitly.
|
|
131
|
+
this.defaultGatePresence = opts.gatePresence ?? 'present'
|
|
132
|
+
if (opts.prAttachments === true) {
|
|
133
|
+
this.prAttachments = {
|
|
134
|
+
upload: (request) => this.uploadPrAttachment(request),
|
|
135
|
+
reclaim: (request) => this.reclaimPrAttachment(request),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private resolveHeadSha(number: number): string {
|
|
141
|
+
return typeof this.headSha === 'string' ? this.headSha : this.headSha(number)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private resolveMergeSha(number: number): string {
|
|
145
|
+
return typeof this.mergeSha === 'string' ? this.mergeSha : this.mergeSha(number)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private assertPr(number: number): void {
|
|
149
|
+
if (!this.prs.has(number)) throw new Error(`FakeForge: unknown PR #${number}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private registerPr(number: number, state: PrState): void {
|
|
153
|
+
this.prs.set(number, state)
|
|
154
|
+
if (!this.headShas.has(number)) this.headShas.set(number, this.resolveHeadSha(number))
|
|
155
|
+
if (!this.autoMerge.has(number)) this.autoMerge.set(number, false)
|
|
156
|
+
if (!this.gates.has(number)) this.gates.set(number, this.defaultGatePresence)
|
|
157
|
+
if (!this.mergeStates.has(number)) {
|
|
158
|
+
const mergeState: MergeStateStatus =
|
|
159
|
+
state.state !== 'open'
|
|
160
|
+
? 'UNKNOWN'
|
|
161
|
+
: state.mergeable === true
|
|
162
|
+
? 'CLEAN'
|
|
163
|
+
: state.mergeable === false
|
|
164
|
+
? 'DIRTY'
|
|
165
|
+
: 'UNKNOWN'
|
|
166
|
+
this.mergeStates.set(number, mergeState)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Overrides the headSha for PRs opened after this call. */
|
|
171
|
+
setHeadSha(headSha: HeadSha): void {
|
|
172
|
+
this.headSha = headSha
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Drives lifecycle/mergeability independently from gate presence. */
|
|
176
|
+
setPrState(number: number, state: PrState): void {
|
|
177
|
+
const known = this.prs.has(number)
|
|
178
|
+
this.registerPr(number, state)
|
|
179
|
+
if (known && state.state === 'open') {
|
|
180
|
+
this.mergeStates.set(
|
|
181
|
+
number,
|
|
182
|
+
state.mergeable === true ? 'CLEAN' : state.mergeable === false ? 'DIRTY' : 'UNKNOWN',
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Seed the exact GitHub mergeStateStatus without changing mergeability. */
|
|
188
|
+
setMergeStateStatus(number: number, state: MergeStateStatus): void {
|
|
189
|
+
this.assertPr(number)
|
|
190
|
+
this.mergeStates.set(number, state)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Seed gate existence independently from current merge status/check health. */
|
|
194
|
+
setGatePresence(number: number, presence: MergeGatePresence): void {
|
|
195
|
+
this.assertPr(number)
|
|
196
|
+
this.gates.set(number, presence)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Seed a typed, fail-closed gate-probe/application deferral. */
|
|
200
|
+
setAutoMergeDeferral(number: number, reason: AutoMergeDeferralReason): void {
|
|
201
|
+
this.assertPr(number)
|
|
202
|
+
this.gates.set(number, reason)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Compatibility convenience for generic gate-probe/auth/schema failures. */
|
|
206
|
+
setGateProbeError(number: number, message: string): void {
|
|
207
|
+
this.setAutoMergeDeferral(number, { code: 'unproven-gate-state', detail: message })
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
setRulesetsPlanLimitation(
|
|
211
|
+
number: number,
|
|
212
|
+
message = 'rulesets require a paid account plan',
|
|
213
|
+
): void {
|
|
214
|
+
this.setAutoMergeDeferral(number, { code: 'github-plan-limitation', detail: message })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
setRepositoryAutoMergeDisabled(
|
|
218
|
+
number: number,
|
|
219
|
+
message = 'GitHub reports allow_auto_merge=false',
|
|
220
|
+
): void {
|
|
221
|
+
this.setAutoMergeDeferral(number, {
|
|
222
|
+
code: 'repository-auto-merge-disabled',
|
|
223
|
+
detail: message,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Simulate a typed native auto-merge mutation/application failure. */
|
|
228
|
+
setNativeAutoMergeError(number: number, message: string): void {
|
|
229
|
+
this.assertPr(number)
|
|
230
|
+
this.nativeDeferrals.set(number, { code: 'unproven-gate-state', detail: message })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
clearNativeAutoMergeError(number: number): void {
|
|
234
|
+
this.assertPr(number)
|
|
235
|
+
this.nativeDeferrals.delete(number)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Simulate a guarded direct-squash command failure. */
|
|
239
|
+
setSquashMergeError(number: number, message: string): void {
|
|
240
|
+
this.assertPr(number)
|
|
241
|
+
this.squashErrors.set(number, message)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Fail the next opt-in hosting call; queues permit deterministic mid-set failures. */
|
|
245
|
+
failNextPrAttachmentUpload(message: string): void {
|
|
246
|
+
this.attachmentUploadErrors.push(message)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Fail the next cleanup call; the following retry resumes normal behavior. */
|
|
250
|
+
failNextPrAttachmentReclaim(message: string): void {
|
|
251
|
+
this.attachmentReclaimErrors.push(message)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private attachmentAssetKey(request: PrAttachmentUploadRequest): string {
|
|
255
|
+
return [
|
|
256
|
+
request.target.repository,
|
|
257
|
+
request.target.releaseId,
|
|
258
|
+
request.prUrl,
|
|
259
|
+
request.attachment.artifact.kind,
|
|
260
|
+
request.attachment.artifact.rev,
|
|
261
|
+
request.attachment.filename,
|
|
262
|
+
request.attachment.mediaType,
|
|
263
|
+
request.sha256,
|
|
264
|
+
].join('\0')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private async uploadPrAttachment(
|
|
268
|
+
request: PrAttachmentUploadRequest,
|
|
269
|
+
): Promise<HostedPrAttachmentAsset> {
|
|
270
|
+
this.prAttachmentUploads.push({
|
|
271
|
+
...request,
|
|
272
|
+
content: request.content.slice(),
|
|
273
|
+
})
|
|
274
|
+
const error = this.attachmentUploadErrors.shift()
|
|
275
|
+
if (error !== undefined) throw new Error(error)
|
|
276
|
+
const key = this.attachmentAssetKey(request)
|
|
277
|
+
const existing = this.attachmentAssets.get(key)
|
|
278
|
+
if (existing !== undefined) return existing
|
|
279
|
+
const assetId = this.nextAttachmentAssetId++
|
|
280
|
+
const asset: HostedPrAttachmentAsset = {
|
|
281
|
+
provider: 'github-release',
|
|
282
|
+
repository: request.target.repository,
|
|
283
|
+
releaseId: request.target.releaseId,
|
|
284
|
+
assetId,
|
|
285
|
+
url:
|
|
286
|
+
`https://fake.forge/pr-attachments/${assetId}/` +
|
|
287
|
+
encodeURIComponent(request.attachment.filename),
|
|
288
|
+
}
|
|
289
|
+
this.attachmentAssets.set(key, asset)
|
|
290
|
+
return asset
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async reclaimPrAttachment(request: PrAttachmentReclaimRequest): Promise<void> {
|
|
294
|
+
this.prAttachmentReclaims.push({ ...request, asset: { ...request.asset } })
|
|
295
|
+
const error = this.attachmentReclaimErrors.shift()
|
|
296
|
+
if (error !== undefined) throw new Error(error)
|
|
297
|
+
for (const [key, asset] of this.attachmentAssets) {
|
|
298
|
+
if (asset.assetId === request.asset.assetId) this.attachmentAssets.delete(key)
|
|
299
|
+
}
|
|
300
|
+
// Missing means it was already deleted: cleanup is idempotent.
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
failNextClosePr(message: string): void {
|
|
304
|
+
this.closePrErrors.push(message)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
failNextDeleteBranch(message: string): void {
|
|
308
|
+
this.deleteBranchErrors.push(message)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
hasPublishedBranch(branch: string): boolean {
|
|
312
|
+
return this.publishedBranches.has(branch)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Move the PR head to exercise --match-head-commit race rejection. */
|
|
316
|
+
setPrHeadSha(number: number, headSha: string): void {
|
|
317
|
+
this.assertPr(number)
|
|
318
|
+
this.headShas.set(number, headSha)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Seed native state without journaling a forge call. */
|
|
322
|
+
setAutoMergeState(number: number, enabled: boolean): void {
|
|
323
|
+
this.assertPr(number)
|
|
324
|
+
this.autoMerge.set(number, enabled)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Inspect native state in seam/integration assertions. */
|
|
328
|
+
isAutoMergeEnabled(number: number): boolean {
|
|
329
|
+
this.assertPr(number)
|
|
330
|
+
return this.autoMerge.get(number) ?? false
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async pushBranch(workspacePath: string, branch: string): Promise<void> {
|
|
334
|
+
this.pushes.push({ workspacePath, branch })
|
|
335
|
+
this.publishedBranches.add(branch)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async closePr(workspacePath: string, number: number): Promise<ClosePrResult> {
|
|
339
|
+
this.closePrCalls.push({ workspacePath, number })
|
|
340
|
+
const error = this.closePrErrors.shift()
|
|
341
|
+
if (error !== undefined) throw new Error(error)
|
|
342
|
+
const state = this.prs.get(number)
|
|
343
|
+
if (state === undefined) throw new Error(`FakeForge: unknown PR #${number}`)
|
|
344
|
+
if (state.state === 'merged') return state
|
|
345
|
+
if (state.state === 'open') this.prs.set(number, { state: 'closed' })
|
|
346
|
+
return { state: 'closed' }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async deleteBranch(workspacePath: string, branch: string): Promise<void> {
|
|
350
|
+
this.deleteBranchCalls.push({ workspacePath, branch })
|
|
351
|
+
const error = this.deleteBranchErrors.shift()
|
|
352
|
+
if (error !== undefined) throw new Error(error)
|
|
353
|
+
this.publishedBranches.delete(branch)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async openPr(opts: {
|
|
357
|
+
workspacePath: string
|
|
358
|
+
head: string
|
|
359
|
+
base: string
|
|
360
|
+
title: string
|
|
361
|
+
body: string
|
|
362
|
+
mergeMessage?: string
|
|
363
|
+
}): Promise<PrRef> {
|
|
364
|
+
// Idempotent by head branch, mirroring GitHubForge (SPEC §8.7).
|
|
365
|
+
for (let i = this.opened.length - 1; i >= 0; i -= 1) {
|
|
366
|
+
const number = i + 1
|
|
367
|
+
if (this.opened[i]!.head !== opts.head) continue
|
|
368
|
+
if (this.prs.get(number)?.state !== 'open') continue
|
|
369
|
+
return {
|
|
370
|
+
number,
|
|
371
|
+
url: `https://fake.forge/pr/${number}`,
|
|
372
|
+
headSha: this.headShas.get(number)!,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const number = this.nextNumber++
|
|
376
|
+
this.opened.push({ ...opts })
|
|
377
|
+
this.registerPr(number, { state: 'open', mergeable: null })
|
|
378
|
+
return {
|
|
379
|
+
number,
|
|
380
|
+
url: `https://fake.forge/pr/${number}`,
|
|
381
|
+
headSha: this.headShas.get(number)!,
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async getPrState(workspacePath: string, number: number): Promise<PrState> {
|
|
386
|
+
this.getPrStateCalls.push({ workspacePath, number })
|
|
387
|
+
const state = this.prs.get(number)
|
|
388
|
+
if (!state) throw new Error(`FakeForge: unknown PR #${number}`)
|
|
389
|
+
return state
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async setAutoMerge(
|
|
393
|
+
workspacePath: string,
|
|
394
|
+
number: number,
|
|
395
|
+
enabled: boolean,
|
|
396
|
+
): Promise<AutoMergeResult> {
|
|
397
|
+
this.assertPr(number)
|
|
398
|
+
const currentlyEnabled = this.autoMerge.get(number) ?? false
|
|
399
|
+
|
|
400
|
+
if (!enabled || currentlyEnabled) {
|
|
401
|
+
const changed = currentlyEnabled !== enabled
|
|
402
|
+
this.autoMerge.set(number, enabled)
|
|
403
|
+
this.autoMergeCalls.push({ workspacePath, number, enabled, changed })
|
|
404
|
+
return { kind: 'applied' }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const gate = this.gates.get(number) ?? this.defaultGatePresence
|
|
408
|
+
if (typeof gate === 'object') {
|
|
409
|
+
this.autoMergeCalls.push({ workspacePath, number, enabled: true, changed: false })
|
|
410
|
+
return { kind: 'deferred', reason: gate }
|
|
411
|
+
}
|
|
412
|
+
const mergeState = this.mergeStates.get(number) ?? 'UNKNOWN'
|
|
413
|
+
const disposition = classifyAutoMergeEnable(mergeState, gate)
|
|
414
|
+
switch (disposition.kind) {
|
|
415
|
+
case 'native': {
|
|
416
|
+
const reason = this.nativeDeferrals.get(number)
|
|
417
|
+
if (reason !== undefined) {
|
|
418
|
+
this.autoMergeCalls.push({ workspacePath, number, enabled: true, changed: false })
|
|
419
|
+
return { kind: 'deferred', reason }
|
|
420
|
+
}
|
|
421
|
+
this.autoMerge.set(number, true)
|
|
422
|
+
this.autoMergeCalls.push({
|
|
423
|
+
workspacePath,
|
|
424
|
+
number,
|
|
425
|
+
enabled: true,
|
|
426
|
+
changed: true,
|
|
427
|
+
})
|
|
428
|
+
return { kind: 'applied' }
|
|
429
|
+
}
|
|
430
|
+
case 'direct':
|
|
431
|
+
this.autoMergeCalls.push({
|
|
432
|
+
workspacePath,
|
|
433
|
+
number,
|
|
434
|
+
enabled: true,
|
|
435
|
+
changed: false,
|
|
436
|
+
})
|
|
437
|
+
return { kind: 'ungated', headSha: this.headShas.get(number)! }
|
|
438
|
+
case 'deferred':
|
|
439
|
+
this.autoMergeCalls.push({
|
|
440
|
+
workspacePath,
|
|
441
|
+
number,
|
|
442
|
+
enabled: true,
|
|
443
|
+
changed: false,
|
|
444
|
+
})
|
|
445
|
+
return { kind: 'deferred' }
|
|
446
|
+
case 'error':
|
|
447
|
+
this.autoMergeCalls.push({ workspacePath, number, enabled: true, changed: false })
|
|
448
|
+
return {
|
|
449
|
+
kind: 'deferred',
|
|
450
|
+
reason: { code: 'unproven-gate-state', detail: disposition.reason },
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async squashMerge(workspacePath: string, number: number, expectedHeadSha: string): Promise<void> {
|
|
456
|
+
this.assertPr(number)
|
|
457
|
+
const error = this.squashErrors.get(number)
|
|
458
|
+
if (error !== undefined) throw new Error(error)
|
|
459
|
+
|
|
460
|
+
const state = this.prs.get(number)!
|
|
461
|
+
if (state.state !== 'open' || state.mergeable !== true) {
|
|
462
|
+
throw new Error(`FakeForge: PR #${number} is not positively mergeable`)
|
|
463
|
+
}
|
|
464
|
+
const actualHeadSha = this.headShas.get(number)!
|
|
465
|
+
if (actualHeadSha !== expectedHeadSha) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`FakeForge: PR #${number} head changed (expected ${expectedHeadSha}, found ${actualHeadSha})`,
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
const gate = this.gates.get(number) ?? this.defaultGatePresence
|
|
471
|
+
if (typeof gate === 'object') throw new Error(gate.detail)
|
|
472
|
+
if (gate !== 'absent') {
|
|
473
|
+
throw new Error(`FakeForge: PR #${number} is protected by a merge-blocking gate`)
|
|
474
|
+
}
|
|
475
|
+
const disposition = classifyAutoMergeEnable(this.mergeStates.get(number) ?? 'UNKNOWN', gate)
|
|
476
|
+
if (disposition.kind !== 'direct') {
|
|
477
|
+
throw new Error(`FakeForge: PR #${number} is no longer direct-merge eligible`)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
this.squashMergeCalls.push({ workspacePath, number, expectedHeadSha })
|
|
481
|
+
this.prs.set(number, { state: 'merged', sha: this.resolveMergeSha(number) })
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async commentOnPr(workspacePath: string, number: number, body: string): Promise<void> {
|
|
485
|
+
this.assertPr(number)
|
|
486
|
+
this.comments.push({ workspacePath, number, body })
|
|
487
|
+
}
|
|
488
|
+
}
|