@davidahmann/mill 0.1.4
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 +202 -0
- package/README.md +335 -0
- package/dist/audit/repository.d.ts +8 -0
- package/dist/audit/repository.d.ts.map +1 -0
- package/dist/audit/repository.js +232 -0
- package/dist/audit/repository.js.map +1 -0
- package/dist/cli-program.d.ts +12 -0
- package/dist/cli-program.d.ts.map +1 -0
- package/dist/cli-program.js +1020 -0
- package/dist/cli-program.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/lock.d.ts +12 -0
- package/dist/config/lock.d.ts.map +1 -0
- package/dist/config/lock.js +83 -0
- package/dist/config/lock.js.map +1 -0
- package/dist/contracts/canonical.d.ts +7 -0
- package/dist/contracts/canonical.d.ts.map +1 -0
- package/dist/contracts/canonical.js +30 -0
- package/dist/contracts/canonical.js.map +1 -0
- package/dist/contracts/schemas.d.ts +2705 -0
- package/dist/contracts/schemas.d.ts.map +1 -0
- package/dist/contracts/schemas.js +988 -0
- package/dist/contracts/schemas.js.map +1 -0
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +138 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +31 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/prd.d.ts +14 -0
- package/dist/intake/prd.d.ts.map +1 -0
- package/dist/intake/prd.js +39 -0
- package/dist/intake/prd.js.map +1 -0
- package/dist/planning/impact.d.ts +59 -0
- package/dist/planning/impact.d.ts.map +1 -0
- package/dist/planning/impact.js +400 -0
- package/dist/planning/impact.js.map +1 -0
- package/dist/planning/specification.d.ts +42 -0
- package/dist/planning/specification.d.ts.map +1 -0
- package/dist/planning/specification.js +235 -0
- package/dist/planning/specification.js.map +1 -0
- package/dist/qualification/public-alpha.d.ts +16 -0
- package/dist/qualification/public-alpha.d.ts.map +1 -0
- package/dist/qualification/public-alpha.js +128 -0
- package/dist/qualification/public-alpha.js.map +1 -0
- package/dist/recipes/node-typescript-next-web.d.ts +19 -0
- package/dist/recipes/node-typescript-next-web.d.ts.map +1 -0
- package/dist/recipes/node-typescript-next-web.js +136 -0
- package/dist/recipes/node-typescript-next-web.js.map +1 -0
- package/dist/repository/integration.d.ts +58 -0
- package/dist/repository/integration.d.ts.map +1 -0
- package/dist/repository/integration.js +1385 -0
- package/dist/repository/integration.js.map +1 -0
- package/dist/repository/scan.d.ts +27 -0
- package/dist/repository/scan.d.ts.map +1 -0
- package/dist/repository/scan.js +388 -0
- package/dist/repository/scan.js.map +1 -0
- package/dist/result.d.ts +23 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +24 -0
- package/dist/result.js.map +1 -0
- package/dist/runtime/codex.d.ts +28 -0
- package/dist/runtime/codex.d.ts.map +1 -0
- package/dist/runtime/codex.js +439 -0
- package/dist/runtime/codex.js.map +1 -0
- package/dist/runtime/context.d.ts +10 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +151 -0
- package/dist/runtime/context.js.map +1 -0
- package/dist/runtime/delivery.d.ts +58 -0
- package/dist/runtime/delivery.d.ts.map +1 -0
- package/dist/runtime/delivery.js +1073 -0
- package/dist/runtime/delivery.js.map +1 -0
- package/dist/runtime/dependencies.d.ts +19 -0
- package/dist/runtime/dependencies.d.ts.map +1 -0
- package/dist/runtime/dependencies.js +495 -0
- package/dist/runtime/dependencies.js.map +1 -0
- package/dist/runtime/github.d.ts +112 -0
- package/dist/runtime/github.d.ts.map +1 -0
- package/dist/runtime/github.js +510 -0
- package/dist/runtime/github.js.map +1 -0
- package/dist/runtime/inputs.d.ts +24 -0
- package/dist/runtime/inputs.d.ts.map +1 -0
- package/dist/runtime/inputs.js +237 -0
- package/dist/runtime/inputs.js.map +1 -0
- package/dist/runtime/lease.d.ts +11 -0
- package/dist/runtime/lease.d.ts.map +1 -0
- package/dist/runtime/lease.js +52 -0
- package/dist/runtime/lease.js.map +1 -0
- package/dist/runtime/lifecycle.d.ts +87 -0
- package/dist/runtime/lifecycle.d.ts.map +1 -0
- package/dist/runtime/lifecycle.js +1024 -0
- package/dist/runtime/lifecycle.js.map +1 -0
- package/dist/runtime/process.d.ts +39 -0
- package/dist/runtime/process.d.ts.map +1 -0
- package/dist/runtime/process.js +304 -0
- package/dist/runtime/process.js.map +1 -0
- package/dist/runtime/repository.d.ts +36 -0
- package/dist/runtime/repository.d.ts.map +1 -0
- package/dist/runtime/repository.js +458 -0
- package/dist/runtime/repository.js.map +1 -0
- package/dist/runtime/state.d.ts +121 -0
- package/dist/runtime/state.d.ts.map +1 -0
- package/dist/runtime/state.js +1017 -0
- package/dist/runtime/state.js.map +1 -0
- package/dist/runtime/verifier.d.ts +31 -0
- package/dist/runtime/verifier.d.ts.map +1 -0
- package/dist/runtime/verifier.js +478 -0
- package/dist/runtime/verifier.js.map +1 -0
- package/dist/runtime/worker.d.ts +67 -0
- package/dist/runtime/worker.d.ts.map +1 -0
- package/dist/runtime/worker.js +34 -0
- package/dist/runtime/worker.js.map +1 -0
- package/dist/security/safe-path.d.ts +3 -0
- package/dist/security/safe-path.d.ts.map +1 -0
- package/dist/security/safe-path.js +102 -0
- package/dist/security/safe-path.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/workflows/founder.d.ts +33 -0
- package/dist/workflows/founder.d.ts.map +1 -0
- package/dist/workflows/founder.js +422 -0
- package/dist/workflows/founder.js.map +1 -0
- package/package.json +79 -0
- package/recipes/node-typescript-next-web/.github/workflows/ci.yml +34 -0
- package/recipes/node-typescript-next-web/.node-version +1 -0
- package/recipes/node-typescript-next-web/.tool-versions +1 -0
- package/recipes/node-typescript-next-web/AGENTS.md +44 -0
- package/recipes/node-typescript-next-web/README.md +16 -0
- package/recipes/node-typescript-next-web/WORKFLOW.md +6 -0
- package/recipes/node-typescript-next-web/app/api/health/route.ts +5 -0
- package/recipes/node-typescript-next-web/app/layout.tsx +19 -0
- package/recipes/node-typescript-next-web/app/page.tsx +12 -0
- package/recipes/node-typescript-next-web/app/styles.css +21 -0
- package/recipes/node-typescript-next-web/docs/development.md +16 -0
- package/recipes/node-typescript-next-web/eslint.config.mjs +9 -0
- package/recipes/node-typescript-next-web/gitignore.template +5 -0
- package/recipes/node-typescript-next-web/next-env.d.ts +7 -0
- package/recipes/node-typescript-next-web/next.config.ts +11 -0
- package/recipes/node-typescript-next-web/package-lock.json +7540 -0
- package/recipes/node-typescript-next-web/package.json +39 -0
- package/recipes/node-typescript-next-web/playwright.config.ts +23 -0
- package/recipes/node-typescript-next-web/recipe.yaml +71 -0
- package/recipes/node-typescript-next-web/scripts/check-package.mjs +3 -0
- package/recipes/node-typescript-next-web/src/health.ts +3 -0
- package/recipes/node-typescript-next-web/test/browser/home.spec.ts +14 -0
- package/recipes/node-typescript-next-web/test/integration/health-route.test.ts +11 -0
- package/recipes/node-typescript-next-web/test/unit/health.test.ts +9 -0
- package/recipes/node-typescript-next-web/tsconfig.json +33 -0
- package/schemas/README.md +64 -0
- package/schemas/audit-report.schema.json +59 -0
- package/schemas/blueprint.schema.json +34 -0
- package/schemas/context-manifest.schema.json +88 -0
- package/schemas/delivery-record.schema.json +266 -0
- package/schemas/impact-manifest.schema.json +146 -0
- package/schemas/managed-repository.schema.json +36 -0
- package/schemas/mill-config.schema.json +212 -0
- package/schemas/mill-lock.schema.json +96 -0
- package/schemas/outcome-plan.schema.json +51 -0
- package/schemas/product-contract.schema.json +181 -0
- package/schemas/public-alpha-qualification.schema.json +391 -0
- package/schemas/recipe-manifest.schema.json +187 -0
- package/schemas/release-evidence.schema.json +170 -0
- package/schemas/repository-integration-plan.schema.json +156 -0
- package/schemas/review-result.schema.json +57 -0
- package/schemas/scenario-set.schema.json +84 -0
- package/schemas/source-manifest.schema.json +96 -0
- package/schemas/specification-proposal.schema.json +406 -0
- package/schemas/support-tuple.schema.json +107 -0
- package/schemas/task-packet.schema.json +395 -0
- package/schemas/validation-evidence.schema.json +125 -0
- package/schemas/worker-invocation.schema.json +119 -0
- package/schemas/worker-profile.schema.json +53 -0
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
import { canonicalDigest } from "../contracts/canonical.js";
|
|
2
|
+
import { deliveryRecordSchema, reviewResultSchema, validationEvidenceSchema, } from "../contracts/schemas.js";
|
|
3
|
+
import { ExitCode, MillError, asMillError } from "../errors.js";
|
|
4
|
+
import { createGitHubAdapter, } from "./github.js";
|
|
5
|
+
import { loadRuntimeInputs } from "./inputs.js";
|
|
6
|
+
import { assertRunBindings } from "./lifecycle.js";
|
|
7
|
+
import { commonGitDirectory, repositoryRemoteUrl } from "./repository.js";
|
|
8
|
+
import { acquireWriterLease, publicRunRecord, StateStore, } from "./state.js";
|
|
9
|
+
const maximumRemoteEffectAttempts = 2;
|
|
10
|
+
function operationDeadline(config) {
|
|
11
|
+
return Date.now() + config.pollTimeoutSeconds * 1000;
|
|
12
|
+
}
|
|
13
|
+
async function openDeliveryContext(root, taskPath, authorityMode = "authorize") {
|
|
14
|
+
const inputs = await loadRuntimeInputs(root, taskPath, authorityMode);
|
|
15
|
+
if (inputs.config.trustCeiling !== "propose" ||
|
|
16
|
+
inputs.config.propose === undefined) {
|
|
17
|
+
throw new MillError("PROPOSE_NOT_AUTHORIZED", "mill.yaml does not grant the propose trust ceiling and exact GitHub policy.", ExitCode.configuration);
|
|
18
|
+
}
|
|
19
|
+
const commonDirectory = await commonGitDirectory(root);
|
|
20
|
+
return {
|
|
21
|
+
inputs,
|
|
22
|
+
config: inputs.config.propose,
|
|
23
|
+
store: await StateStore.open(inputs.config.repositoryId, commonDirectory),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function storedDelivery(run) {
|
|
27
|
+
if (run.deliveryJson === undefined) {
|
|
28
|
+
throw new MillError("DELIVERY_PLAN_MISSING", "Run has no approved draft-PR delivery plan.", ExitCode.configuration);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return deliveryRecordSchema.parse(JSON.parse(run.deliveryJson));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new MillError("DELIVERY_RECORD_INVALID", "Stored draft-PR delivery state is invalid.", ExitCode.data, { cause: String(error) });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function persistDelivery(store, runId, value, eventType, details = {}) {
|
|
38
|
+
const parsed = deliveryRecordSchema.parse({
|
|
39
|
+
...value,
|
|
40
|
+
updatedAt: new Date().toISOString(),
|
|
41
|
+
});
|
|
42
|
+
store.setDelivery(runId, JSON.stringify(parsed), eventType, details);
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
function cancellationRequested(store, runId) {
|
|
46
|
+
return store.getRun(runId).cancelRequested;
|
|
47
|
+
}
|
|
48
|
+
function stopCancelledDelivery(store, runId, delivery) {
|
|
49
|
+
const current = store.getRun(runId);
|
|
50
|
+
if (!current.cancelRequested) {
|
|
51
|
+
throw new MillError("CANCELLATION_STATE_INVALID", "Remote delivery cancellation was requested without durable state.", ExitCode.data);
|
|
52
|
+
}
|
|
53
|
+
persistDelivery(store, runId, { ...delivery, state: "cancelled", lastErrorCode: "OPERATOR_CANCELLED" }, "delivery.cancelled");
|
|
54
|
+
if (current.status !== "cancelled") {
|
|
55
|
+
store.transition(runId, "cancelled", "run.cancelled", {
|
|
56
|
+
code: "OPERATOR_CANCELLED",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
throw new MillError("OPERATOR_CANCELLED", "The operator cancelled remote delivery before another external effect.", ExitCode.temporary);
|
|
60
|
+
}
|
|
61
|
+
function assertRemoteMutationNotCancelled(store, runId, delivery) {
|
|
62
|
+
if (cancellationRequested(store, runId)) {
|
|
63
|
+
stopCancelledDelivery(store, runId, delivery);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function setRunBlocker(store, run, code, eventType, details = {}) {
|
|
67
|
+
return run.status === "blocked"
|
|
68
|
+
? store.replaceBlocker(run.id, code, eventType, details)
|
|
69
|
+
: store.transition(run.id, "blocked", eventType, { code, ...details });
|
|
70
|
+
}
|
|
71
|
+
function reconcileAbsentEffect(store, run, delivery, effectValue) {
|
|
72
|
+
if (cancellationRequested(store, run.id)) {
|
|
73
|
+
stopCancelledDelivery(store, run.id, delivery);
|
|
74
|
+
}
|
|
75
|
+
if (effectValue.attemptCount >= maximumRemoteEffectAttempts) {
|
|
76
|
+
const blocked = persistDelivery(store, run.id, {
|
|
77
|
+
...upsertEffect(delivery, {
|
|
78
|
+
...effectValue,
|
|
79
|
+
status: "blocked",
|
|
80
|
+
errorCode: "REMOTE_EFFECT_RETRY_EXHAUSTED",
|
|
81
|
+
updatedAt: new Date().toISOString(),
|
|
82
|
+
}),
|
|
83
|
+
state: "blocked",
|
|
84
|
+
lastErrorCode: "REMOTE_EFFECT_RETRY_EXHAUSTED",
|
|
85
|
+
}, "delivery.retry_exhausted", { effectId: effectValue.id });
|
|
86
|
+
const blockedRun = setRunBlocker(store, run, "REMOTE_EFFECT_RETRY_EXHAUSTED", "delivery.blocked");
|
|
87
|
+
return { run: publicRunRecord(blockedRun), delivery: blocked };
|
|
88
|
+
}
|
|
89
|
+
const retryable = persistDelivery(store, run.id, {
|
|
90
|
+
...upsertEffect(delivery, {
|
|
91
|
+
...effectValue,
|
|
92
|
+
status: "retryable_absent",
|
|
93
|
+
errorCode: null,
|
|
94
|
+
updatedAt: new Date().toISOString(),
|
|
95
|
+
}),
|
|
96
|
+
state: "proposing",
|
|
97
|
+
lastErrorCode: null,
|
|
98
|
+
}, "delivery.effect_absent", { effectId: effectValue.id, attemptCount: effectValue.attemptCount });
|
|
99
|
+
const retryableRun = store.transition(run.id, "proposing", "delivery.retry_authorized");
|
|
100
|
+
return { run: publicRunRecord(retryableRun), delivery: retryable };
|
|
101
|
+
}
|
|
102
|
+
async function assertReviewedCandidate(root, run, inputs) {
|
|
103
|
+
if (run.status !== "reviewed" && run.status !== "proposing") {
|
|
104
|
+
throw new MillError("RUN_NOT_REVIEWED", "Only an exact locally reviewed candidate may enter draft-PR delivery.", ExitCode.configuration);
|
|
105
|
+
}
|
|
106
|
+
if (run.validationJson === undefined || run.reviewJson === undefined) {
|
|
107
|
+
throw new MillError("LOCAL_EVIDENCE_MISSING", "Draft-PR delivery requires exact validation and local review evidence.", ExitCode.configuration);
|
|
108
|
+
}
|
|
109
|
+
let validation;
|
|
110
|
+
let review;
|
|
111
|
+
try {
|
|
112
|
+
validation = validationEvidenceSchema.parse(JSON.parse(run.validationJson));
|
|
113
|
+
review = reviewResultSchema.parse(JSON.parse(run.reviewJson));
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new MillError("LOCAL_EVIDENCE_INVALID", "Stored validation or local review evidence is invalid.", ExitCode.data, { cause: String(error) });
|
|
117
|
+
}
|
|
118
|
+
if (!validation.passed ||
|
|
119
|
+
validation.candidateCommit !== run.candidateCommit ||
|
|
120
|
+
review.candidateCommit !== run.candidateCommit ||
|
|
121
|
+
review.findings.length > 0) {
|
|
122
|
+
throw new MillError("LOCAL_EVIDENCE_STALE", "Validation and local review must pass on the exact candidate head.", ExitCode.configuration);
|
|
123
|
+
}
|
|
124
|
+
const candidate = await assertRunBindings(root, run, inputs);
|
|
125
|
+
return { commit: candidate.commit, tree: candidate.tree };
|
|
126
|
+
}
|
|
127
|
+
function expectedRemoteUrls(config, cloneUrl) {
|
|
128
|
+
return new Set([
|
|
129
|
+
cloneUrl,
|
|
130
|
+
`git@${config.host}:${config.owner}/${config.repository}.git`,
|
|
131
|
+
`ssh://git@${config.host}/${config.owner}/${config.repository}.git`,
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
async function assertBinding(root, config, binding) {
|
|
135
|
+
const expectedFullName = `${config.owner}/${config.repository}`;
|
|
136
|
+
const remote = await repositoryRemoteUrl(root, config.remoteName);
|
|
137
|
+
if (binding.repositoryNodeId !== config.repositoryNodeId ||
|
|
138
|
+
binding.fullName !== expectedFullName ||
|
|
139
|
+
binding.defaultBranch !== config.baseBranch ||
|
|
140
|
+
binding.fork ||
|
|
141
|
+
!config.allowedActors.includes(binding.actorLogin) ||
|
|
142
|
+
!expectedRemoteUrls(config, binding.cloneUrl).has(remote)) {
|
|
143
|
+
throw new MillError("GITHUB_BINDING_MISMATCH", "The live actor, repository, default branch, node identity, or local remote does not match mill.yaml.", ExitCode.configuration, {
|
|
144
|
+
actorLogin: binding.actorLogin,
|
|
145
|
+
fullName: binding.fullName,
|
|
146
|
+
repositoryNodeId: binding.repositoryNodeId,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function branchName(config, run) {
|
|
151
|
+
return `${config.branchPrefix}${run.taskId.slice(0, 32)}-${run.id.slice(0, 8)}`;
|
|
152
|
+
}
|
|
153
|
+
function target(config, binding) {
|
|
154
|
+
return {
|
|
155
|
+
forge: "github",
|
|
156
|
+
host: config.host,
|
|
157
|
+
owner: config.owner,
|
|
158
|
+
repository: config.repository,
|
|
159
|
+
repositoryNodeId: binding.repositoryNodeId,
|
|
160
|
+
cloneUrl: binding.cloneUrl,
|
|
161
|
+
remoteName: config.remoteName,
|
|
162
|
+
baseBranch: config.baseBranch,
|
|
163
|
+
actorLogin: binding.actorLogin,
|
|
164
|
+
actorId: binding.actorId,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function proposalDigest(input) {
|
|
168
|
+
return canonicalDigest({
|
|
169
|
+
schemaVersion: "1",
|
|
170
|
+
runId: input.run.id,
|
|
171
|
+
taskDigest: input.run.taskDigest,
|
|
172
|
+
configDigest: input.run.configDigest,
|
|
173
|
+
candidateCommit: input.candidate.commit,
|
|
174
|
+
candidateTree: input.candidate.tree,
|
|
175
|
+
target: input.target,
|
|
176
|
+
branchName: input.branchName,
|
|
177
|
+
approvalExpiresAt: input.approvalExpiresAt,
|
|
178
|
+
requiredChecks: input.config.requiredChecks,
|
|
179
|
+
reviewPolicy: input.config.reviewPolicy,
|
|
180
|
+
allowedMergeMethods: input.config.allowedMergeMethods,
|
|
181
|
+
allowedMergerLogins: input.config.allowedMergerLogins,
|
|
182
|
+
effects: ["push_exact_candidate", "create_draft_pull_request"],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function effectId(deliveryKey, kind, candidateCommit) {
|
|
186
|
+
return canonicalDigest({
|
|
187
|
+
schemaVersion: "1",
|
|
188
|
+
deliveryKey,
|
|
189
|
+
kind,
|
|
190
|
+
candidateCommit,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function effect(delivery, kind, candidateCommit) {
|
|
194
|
+
return delivery.effects.find((item) => item.id === effectId(delivery.deliveryKey, kind, candidateCommit));
|
|
195
|
+
}
|
|
196
|
+
function upsertEffect(delivery, value) {
|
|
197
|
+
return {
|
|
198
|
+
...delivery,
|
|
199
|
+
effects: [
|
|
200
|
+
...delivery.effects.filter((item) => item.id !== value.id),
|
|
201
|
+
value,
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function deliveryMarker(deliveryKey) {
|
|
206
|
+
return `<!-- mill-delivery-key:${deliveryKey} -->`;
|
|
207
|
+
}
|
|
208
|
+
function exactPullRequest(pullRequest, delivery) {
|
|
209
|
+
return (pullRequest.headRef === delivery.branchName &&
|
|
210
|
+
pullRequest.baseRef === delivery.target.baseBranch &&
|
|
211
|
+
pullRequest.body.includes(deliveryMarker(delivery.deliveryKey)));
|
|
212
|
+
}
|
|
213
|
+
function assertExactPullRequest(pullRequest, delivery, requireDraft) {
|
|
214
|
+
const recorded = delivery.pullRequest;
|
|
215
|
+
if (!exactPullRequest(pullRequest, delivery) ||
|
|
216
|
+
(recorded !== null &&
|
|
217
|
+
(pullRequest.number !== recorded.number ||
|
|
218
|
+
pullRequest.nodeId !== recorded.nodeId)) ||
|
|
219
|
+
pullRequest.headSha !== delivery.candidateCommit ||
|
|
220
|
+
pullRequest.state !== "open" ||
|
|
221
|
+
(requireDraft && !pullRequest.draft)) {
|
|
222
|
+
throw new MillError("PULL_REQUEST_IDENTITY_MISMATCH", "GitHub pull request identity does not match the exact delivery plan.", ExitCode.configuration);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function assertPushBoundaryPullRequest(pullRequest, delivery, expectedOldCommit) {
|
|
226
|
+
const recorded = delivery.pullRequest;
|
|
227
|
+
if (recorded === null) {
|
|
228
|
+
if (pullRequest !== null) {
|
|
229
|
+
throw new MillError("PULL_REQUEST_IDENTITY_MISMATCH", "An unrecorded pull request conflicts with the push boundary readback.", ExitCode.configuration);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (pullRequest === null ||
|
|
234
|
+
!exactPullRequest(pullRequest, delivery) ||
|
|
235
|
+
pullRequest.number !== recorded.number ||
|
|
236
|
+
pullRequest.nodeId !== recorded.nodeId ||
|
|
237
|
+
pullRequest.headSha !== expectedOldCommit ||
|
|
238
|
+
pullRequest.state !== "open" ||
|
|
239
|
+
!pullRequest.draft) {
|
|
240
|
+
throw new MillError("PULL_REQUEST_IDENTITY_MISMATCH", "The recorded pull request is no longer the expected open draft at the observed branch head.", ExitCode.configuration);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function assertDeliveryContinuity(input) {
|
|
244
|
+
const { run, inputs, config, delivery, binding } = input;
|
|
245
|
+
if (run.taskDigest !== inputs.taskDigest ||
|
|
246
|
+
run.configDigest !== inputs.configDigest ||
|
|
247
|
+
delivery.runId !== run.id ||
|
|
248
|
+
delivery.candidateCommit !== run.candidateCommit ||
|
|
249
|
+
delivery.candidateTree !== run.candidateTree ||
|
|
250
|
+
delivery.target.owner !== config.owner ||
|
|
251
|
+
delivery.target.repository !== config.repository ||
|
|
252
|
+
delivery.target.repositoryNodeId !== config.repositoryNodeId ||
|
|
253
|
+
delivery.target.remoteName !== config.remoteName ||
|
|
254
|
+
delivery.target.baseBranch !== config.baseBranch ||
|
|
255
|
+
delivery.target.actorLogin !== binding.actorLogin ||
|
|
256
|
+
delivery.target.actorId !== binding.actorId ||
|
|
257
|
+
delivery.target.repositoryNodeId !== binding.repositoryNodeId ||
|
|
258
|
+
delivery.target.cloneUrl !== binding.cloneUrl ||
|
|
259
|
+
JSON.stringify(delivery.requiredChecks) !==
|
|
260
|
+
JSON.stringify(config.requiredChecks) ||
|
|
261
|
+
JSON.stringify(delivery.reviewPolicy) !==
|
|
262
|
+
JSON.stringify(config.reviewPolicy) ||
|
|
263
|
+
JSON.stringify(delivery.allowedMergerLogins) !==
|
|
264
|
+
JSON.stringify(config.allowedMergerLogins) ||
|
|
265
|
+
JSON.stringify(delivery.allowedMergeMethods) !==
|
|
266
|
+
JSON.stringify(config.allowedMergeMethods)) {
|
|
267
|
+
throw new MillError("DELIVERY_AUTHORITY_DRIFT", "The task, configuration, candidate, actor, or repository changed after delivery approval.", ExitCode.configuration);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function checkDecision(required, checks) {
|
|
271
|
+
const missing = [];
|
|
272
|
+
const failed = [];
|
|
273
|
+
let pending = false;
|
|
274
|
+
for (const name of required) {
|
|
275
|
+
const matching = checks.filter((check) => check.name === name);
|
|
276
|
+
if (matching.length === 0) {
|
|
277
|
+
missing.push(name);
|
|
278
|
+
pending = true;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (matching.some((check) => check.status !== "completed" || check.conclusion === null)) {
|
|
282
|
+
pending = true;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!matching.every((check) => check.conclusion === "success")) {
|
|
286
|
+
failed.push(name);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
status: failed.length > 0 ? "failed" : pending ? "pending" : "passed",
|
|
291
|
+
missing,
|
|
292
|
+
failed,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function actionableFeedback(observation, reviewPolicy, candidateCommit) {
|
|
296
|
+
if (reviewPolicy.mode !== "github_required")
|
|
297
|
+
return [];
|
|
298
|
+
return observation.feedback.filter((item) => item.commitId === candidateCommit &&
|
|
299
|
+
reviewPolicy.requiredReviewerLogins.includes(item.actorLogin) &&
|
|
300
|
+
item.priority !== "P3");
|
|
301
|
+
}
|
|
302
|
+
function reviewsPassed(observation, reviewPolicy, candidateCommit) {
|
|
303
|
+
if (reviewPolicy.mode === "local_only")
|
|
304
|
+
return true;
|
|
305
|
+
return reviewPolicy.requiredReviewerLogins.every((login) => {
|
|
306
|
+
const latest = observation.reviews
|
|
307
|
+
.filter((review) => review.actorLogin === login && review.commitId === candidateCommit)
|
|
308
|
+
.at(-1);
|
|
309
|
+
return latest?.state === "APPROVED" || latest?.state === "COMMENTED";
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function feedbackAsReview(candidateCommit, feedback) {
|
|
313
|
+
return reviewResultSchema.parse({
|
|
314
|
+
schemaVersion: "1",
|
|
315
|
+
candidateCommit,
|
|
316
|
+
summary: "GitHub review requires one attended systemic repair.",
|
|
317
|
+
findings: feedback.map((item) => ({
|
|
318
|
+
id: `github-${item.id}`,
|
|
319
|
+
severity: item.priority === "unclassified" ? "P1" : item.priority,
|
|
320
|
+
class: "correctness",
|
|
321
|
+
title: item.body.split("\n", 1)[0]?.slice(0, 200) ?? "GitHub review finding",
|
|
322
|
+
body: `${item.body}\n\nSource: ${item.url}`,
|
|
323
|
+
file: item.path,
|
|
324
|
+
line: item.line,
|
|
325
|
+
})),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function reconcileReadback(input) {
|
|
329
|
+
const [branchSha, pullRequests] = await Promise.all([
|
|
330
|
+
input.adapter.readBranch({
|
|
331
|
+
config: input.config,
|
|
332
|
+
branch: input.delivery.branchName,
|
|
333
|
+
deadlineMs: input.deadlineMs,
|
|
334
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
335
|
+
}),
|
|
336
|
+
input.adapter.findPullRequests({
|
|
337
|
+
config: input.config,
|
|
338
|
+
branch: input.delivery.branchName,
|
|
339
|
+
deadlineMs: input.deadlineMs,
|
|
340
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
341
|
+
}),
|
|
342
|
+
]);
|
|
343
|
+
const matching = pullRequests.filter((item) => exactPullRequest(item, input.delivery));
|
|
344
|
+
if (matching.length > 1) {
|
|
345
|
+
throw new MillError("DUPLICATE_PULL_REQUESTS", "More than one pull request carries the immutable delivery identity.", ExitCode.configuration);
|
|
346
|
+
}
|
|
347
|
+
return { branchSha, pullRequest: matching[0] ?? null };
|
|
348
|
+
}
|
|
349
|
+
export async function planDraftPr(input) {
|
|
350
|
+
const context = await openDeliveryContext(input.root, input.taskPath);
|
|
351
|
+
const { inputs, config, store } = context;
|
|
352
|
+
let lease;
|
|
353
|
+
try {
|
|
354
|
+
lease = await acquireWriterLease(store);
|
|
355
|
+
const run = store.getRun(input.runId);
|
|
356
|
+
const candidate = await assertReviewedCandidate(input.root, run, inputs);
|
|
357
|
+
const adapter = input.adapter ?? createGitHubAdapter(input.root);
|
|
358
|
+
const binding = await adapter.inspect({
|
|
359
|
+
config,
|
|
360
|
+
deadlineMs: operationDeadline(config),
|
|
361
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
362
|
+
});
|
|
363
|
+
await assertBinding(input.root, config, binding);
|
|
364
|
+
const plannedBranch = branchName(config, run);
|
|
365
|
+
const approvalExpiresAt = new Date(Date.now() + config.approvalTtlSeconds * 1000).toISOString();
|
|
366
|
+
const plannedTarget = target(config, binding);
|
|
367
|
+
const existing = run.deliveryJson === undefined ? undefined : storedDelivery(run);
|
|
368
|
+
const deliveryKey = existing?.deliveryKey ??
|
|
369
|
+
canonicalDigest({
|
|
370
|
+
schemaVersion: "1",
|
|
371
|
+
repositoryId: inputs.config.repositoryId,
|
|
372
|
+
runId: run.id,
|
|
373
|
+
repositoryNodeId: binding.repositoryNodeId,
|
|
374
|
+
branchName: plannedBranch,
|
|
375
|
+
});
|
|
376
|
+
const now = new Date().toISOString();
|
|
377
|
+
const delivery = deliveryRecordSchema.parse({
|
|
378
|
+
schemaVersion: "1",
|
|
379
|
+
runId: run.id,
|
|
380
|
+
deliveryKey,
|
|
381
|
+
proposalDigest: proposalDigest({
|
|
382
|
+
run,
|
|
383
|
+
candidate,
|
|
384
|
+
target: plannedTarget,
|
|
385
|
+
branchName: plannedBranch,
|
|
386
|
+
approvalExpiresAt,
|
|
387
|
+
config,
|
|
388
|
+
}),
|
|
389
|
+
approvalExpiresAt,
|
|
390
|
+
state: "planned",
|
|
391
|
+
target: plannedTarget,
|
|
392
|
+
branchName: plannedBranch,
|
|
393
|
+
candidateCommit: candidate.commit,
|
|
394
|
+
candidateTree: candidate.tree,
|
|
395
|
+
requiredChecks: config.requiredChecks,
|
|
396
|
+
reviewPolicy: config.reviewPolicy,
|
|
397
|
+
allowedMergerLogins: config.allowedMergerLogins,
|
|
398
|
+
allowedMergeMethods: config.allowedMergeMethods,
|
|
399
|
+
effects: existing?.effects ?? [],
|
|
400
|
+
remoteHeadCommit: existing?.remoteHeadCommit ?? null,
|
|
401
|
+
pullRequest: existing?.pullRequest ?? null,
|
|
402
|
+
observation: null,
|
|
403
|
+
merge: null,
|
|
404
|
+
lastErrorCode: null,
|
|
405
|
+
createdAt: existing?.createdAt ?? now,
|
|
406
|
+
updatedAt: now,
|
|
407
|
+
});
|
|
408
|
+
const persisted = persistDelivery(store, run.id, delivery, "delivery.planned", { candidateCommit: candidate.commit, deliveryKey });
|
|
409
|
+
const updatedRun = run.status === "reviewed"
|
|
410
|
+
? store.transition(run.id, "proposing", "delivery.awaiting_approval")
|
|
411
|
+
: store.getRun(run.id);
|
|
412
|
+
return { run: publicRunRecord(updatedRun), delivery: persisted };
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
try {
|
|
416
|
+
await lease?.release();
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
store.close();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function markUnknown(store, run, delivery, effectValue, code) {
|
|
424
|
+
const unknown = persistDelivery(store, run.id, {
|
|
425
|
+
...upsertEffect(delivery, {
|
|
426
|
+
...effectValue,
|
|
427
|
+
status: "effect_unknown",
|
|
428
|
+
errorCode: code,
|
|
429
|
+
updatedAt: new Date().toISOString(),
|
|
430
|
+
}),
|
|
431
|
+
state: "effect_unknown",
|
|
432
|
+
lastErrorCode: code,
|
|
433
|
+
}, "delivery.effect_unknown", { effectId: effectValue.id, code });
|
|
434
|
+
const current = store.getRun(run.id);
|
|
435
|
+
if (current.status !== "effect_unknown") {
|
|
436
|
+
store.transition(run.id, "effect_unknown", "delivery.reconciliation_required", {
|
|
437
|
+
code,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
throw new MillError(code, "A GitHub effect has an unknown outcome; run pr reconcile before any further mutation.", ExitCode.temporary, { deliveryKey: unknown.deliveryKey, effectId: effectValue.id });
|
|
441
|
+
}
|
|
442
|
+
export async function openDraftPr(input) {
|
|
443
|
+
if (!input.attended) {
|
|
444
|
+
throw new MillError("ATTENDED_ACKNOWLEDGEMENT_REQUIRED", "Draft-PR mutation requires an explicit attended acknowledgement.", ExitCode.configuration);
|
|
445
|
+
}
|
|
446
|
+
const context = await openDeliveryContext(input.root, input.taskPath);
|
|
447
|
+
const { inputs, config, store } = context;
|
|
448
|
+
let lease;
|
|
449
|
+
try {
|
|
450
|
+
lease = await acquireWriterLease(store);
|
|
451
|
+
let run = store.getRun(input.runId);
|
|
452
|
+
if (run.status !== "proposing") {
|
|
453
|
+
throw new MillError("DELIVERY_NOT_APPLICABLE", "Draft-PR open requires an approved plan with no unknown external effect.", ExitCode.configuration);
|
|
454
|
+
}
|
|
455
|
+
const candidate = await assertReviewedCandidate(input.root, run, inputs);
|
|
456
|
+
let delivery = storedDelivery(run);
|
|
457
|
+
assertRemoteMutationNotCancelled(store, run.id, delivery);
|
|
458
|
+
if (delivery.candidateCommit !== candidate.commit ||
|
|
459
|
+
delivery.candidateTree !== candidate.tree ||
|
|
460
|
+
delivery.proposalDigest !== input.approvalDigest ||
|
|
461
|
+
Date.parse(delivery.approvalExpiresAt) <= Date.now()) {
|
|
462
|
+
throw new MillError("DELIVERY_APPROVAL_MISMATCH", "Approval is missing, expired, or bound to another candidate or delivery plan.", ExitCode.configuration);
|
|
463
|
+
}
|
|
464
|
+
const adapter = input.adapter ?? createGitHubAdapter(input.root);
|
|
465
|
+
const deadlineMs = operationDeadline(config);
|
|
466
|
+
const binding = await adapter.inspect({
|
|
467
|
+
config,
|
|
468
|
+
deadlineMs,
|
|
469
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
470
|
+
});
|
|
471
|
+
await assertBinding(input.root, config, binding);
|
|
472
|
+
const liveDigest = proposalDigest({
|
|
473
|
+
run,
|
|
474
|
+
candidate,
|
|
475
|
+
target: target(config, binding),
|
|
476
|
+
branchName: delivery.branchName,
|
|
477
|
+
approvalExpiresAt: delivery.approvalExpiresAt,
|
|
478
|
+
config,
|
|
479
|
+
});
|
|
480
|
+
if (liveDigest !== input.approvalDigest) {
|
|
481
|
+
throw new MillError("DELIVERY_BINDING_DRIFT", "Live GitHub identity or proposal policy changed after approval.", ExitCode.configuration);
|
|
482
|
+
}
|
|
483
|
+
delivery = persistDelivery(store, run.id, { ...delivery, state: "proposing", lastErrorCode: null }, "delivery.started");
|
|
484
|
+
let readback = await reconcileReadback({
|
|
485
|
+
adapter,
|
|
486
|
+
config,
|
|
487
|
+
delivery,
|
|
488
|
+
deadlineMs,
|
|
489
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
490
|
+
});
|
|
491
|
+
assertPushBoundaryPullRequest(readback.pullRequest, delivery, readback.branchSha);
|
|
492
|
+
let push = effect(delivery, "push", candidate.commit);
|
|
493
|
+
if (readback.branchSha !== candidate.commit) {
|
|
494
|
+
if (readback.branchSha !== null &&
|
|
495
|
+
readback.branchSha !== delivery.remoteHeadCommit) {
|
|
496
|
+
throw new MillError("REMOTE_BRANCH_CONFLICT", "The Mill delivery branch moved outside the exact expected-head precondition.", ExitCode.configuration);
|
|
497
|
+
}
|
|
498
|
+
if (push !== undefined &&
|
|
499
|
+
(push.status === "call_started" || push.status === "effect_unknown")) {
|
|
500
|
+
markUnknown(store, run, delivery, push, "GITHUB_PUSH_OUTCOME_UNKNOWN");
|
|
501
|
+
}
|
|
502
|
+
if (push?.status === "verified") {
|
|
503
|
+
throw new MillError("REMOTE_BRANCH_CONFLICT", "A previously verified Mill branch no longer has its bound candidate head.", ExitCode.configuration);
|
|
504
|
+
}
|
|
505
|
+
if ((push?.attemptCount ?? 0) >= maximumRemoteEffectAttempts) {
|
|
506
|
+
throw new MillError("REMOTE_EFFECT_RETRY_EXHAUSTED", "The exact push retry budget is exhausted.", ExitCode.configuration);
|
|
507
|
+
}
|
|
508
|
+
push = {
|
|
509
|
+
...(push ?? {
|
|
510
|
+
id: effectId(delivery.deliveryKey, "push", candidate.commit),
|
|
511
|
+
kind: "push",
|
|
512
|
+
candidateCommit: candidate.commit,
|
|
513
|
+
attemptCount: 0,
|
|
514
|
+
expectedOldCommit: delivery.remoteHeadCommit,
|
|
515
|
+
}),
|
|
516
|
+
status: "intent",
|
|
517
|
+
errorCode: null,
|
|
518
|
+
updatedAt: new Date().toISOString(),
|
|
519
|
+
};
|
|
520
|
+
delivery = persistDelivery(store, run.id, upsertEffect(delivery, push), "delivery.push_intent", { effectId: push.id });
|
|
521
|
+
readback = await reconcileReadback({
|
|
522
|
+
adapter,
|
|
523
|
+
config,
|
|
524
|
+
delivery,
|
|
525
|
+
deadlineMs,
|
|
526
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
527
|
+
});
|
|
528
|
+
if (readback.branchSha !== push.expectedOldCommit) {
|
|
529
|
+
throw new MillError("REMOTE_BRANCH_CONFLICT", "The Mill delivery branch changed after push intent and before the mutation boundary.", ExitCode.configuration);
|
|
530
|
+
}
|
|
531
|
+
assertPushBoundaryPullRequest(readback.pullRequest, delivery, push.expectedOldCommit);
|
|
532
|
+
push = {
|
|
533
|
+
...push,
|
|
534
|
+
status: "call_started",
|
|
535
|
+
attemptCount: push.attemptCount + 1,
|
|
536
|
+
updatedAt: new Date().toISOString(),
|
|
537
|
+
};
|
|
538
|
+
delivery = persistDelivery(store, run.id, upsertEffect(delivery, push), "delivery.push_started", { effectId: push.id });
|
|
539
|
+
assertRemoteMutationNotCancelled(store, run.id, delivery);
|
|
540
|
+
try {
|
|
541
|
+
await adapter.pushExact({
|
|
542
|
+
root: input.root,
|
|
543
|
+
config,
|
|
544
|
+
cloneUrl: delivery.target.cloneUrl,
|
|
545
|
+
branch: delivery.branchName,
|
|
546
|
+
candidateCommit: candidate.commit,
|
|
547
|
+
expectedOldCommit: push.expectedOldCommit,
|
|
548
|
+
deadlineMs,
|
|
549
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
550
|
+
cancellationRequested: () => cancellationRequested(store, run.id),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
const failure = asMillError(error);
|
|
555
|
+
try {
|
|
556
|
+
readback = await reconcileReadback({
|
|
557
|
+
adapter,
|
|
558
|
+
config,
|
|
559
|
+
delivery,
|
|
560
|
+
deadlineMs,
|
|
561
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
markUnknown(store, run, delivery, push, failure.code);
|
|
566
|
+
}
|
|
567
|
+
if (readback.branchSha !== candidate.commit) {
|
|
568
|
+
markUnknown(store, run, delivery, push, failure.code);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
readback = await reconcileReadback({
|
|
572
|
+
adapter,
|
|
573
|
+
config,
|
|
574
|
+
delivery,
|
|
575
|
+
deadlineMs,
|
|
576
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
577
|
+
});
|
|
578
|
+
assertPushBoundaryPullRequest(readback.pullRequest, delivery, readback.branchSha);
|
|
579
|
+
if (readback.branchSha !== candidate.commit) {
|
|
580
|
+
markUnknown(store, run, delivery, push, "GITHUB_PUSH_READBACK_MISMATCH");
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (push === undefined) {
|
|
584
|
+
push = {
|
|
585
|
+
id: effectId(delivery.deliveryKey, "push", candidate.commit),
|
|
586
|
+
kind: "push",
|
|
587
|
+
candidateCommit: candidate.commit,
|
|
588
|
+
status: "verified",
|
|
589
|
+
attemptCount: 0,
|
|
590
|
+
expectedOldCommit: delivery.remoteHeadCommit,
|
|
591
|
+
errorCode: null,
|
|
592
|
+
updatedAt: new Date().toISOString(),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
push = {
|
|
597
|
+
...push,
|
|
598
|
+
status: "verified",
|
|
599
|
+
errorCode: null,
|
|
600
|
+
updatedAt: new Date().toISOString(),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
delivery = persistDelivery(store, run.id, {
|
|
604
|
+
...upsertEffect(delivery, push),
|
|
605
|
+
remoteHeadCommit: candidate.commit,
|
|
606
|
+
}, "delivery.push_verified", { effectId: push.id, candidateCommit: candidate.commit });
|
|
607
|
+
assertRemoteMutationNotCancelled(store, run.id, delivery);
|
|
608
|
+
readback = await reconcileReadback({
|
|
609
|
+
adapter,
|
|
610
|
+
config,
|
|
611
|
+
delivery,
|
|
612
|
+
deadlineMs,
|
|
613
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
614
|
+
});
|
|
615
|
+
let pullRequest = readback.pullRequest;
|
|
616
|
+
let prEffect = effect(delivery, "pull_request", candidate.commit);
|
|
617
|
+
if (pullRequest === null && delivery.pullRequest !== null) {
|
|
618
|
+
throw new MillError("PULL_REQUEST_DISAPPEARED", "The recorded pull request cannot be found by immutable delivery identity.", ExitCode.configuration);
|
|
619
|
+
}
|
|
620
|
+
if (pullRequest === null) {
|
|
621
|
+
if (prEffect !== undefined &&
|
|
622
|
+
(prEffect.status === "call_started" ||
|
|
623
|
+
prEffect.status === "effect_unknown")) {
|
|
624
|
+
markUnknown(store, run, delivery, prEffect, "GITHUB_PR_OUTCOME_UNKNOWN");
|
|
625
|
+
}
|
|
626
|
+
if (prEffect?.status === "verified" || delivery.pullRequest !== null) {
|
|
627
|
+
throw new MillError("PULL_REQUEST_DISAPPEARED", "A previously verified pull request cannot be found by immutable delivery identity.", ExitCode.configuration);
|
|
628
|
+
}
|
|
629
|
+
if ((prEffect?.attemptCount ?? 0) >= maximumRemoteEffectAttempts) {
|
|
630
|
+
throw new MillError("REMOTE_EFFECT_RETRY_EXHAUSTED", "The draft pull-request retry budget is exhausted.", ExitCode.configuration);
|
|
631
|
+
}
|
|
632
|
+
prEffect = {
|
|
633
|
+
...(prEffect ?? {
|
|
634
|
+
id: effectId(delivery.deliveryKey, "pull_request", candidate.commit),
|
|
635
|
+
kind: "pull_request",
|
|
636
|
+
candidateCommit: candidate.commit,
|
|
637
|
+
attemptCount: 0,
|
|
638
|
+
expectedOldCommit: null,
|
|
639
|
+
}),
|
|
640
|
+
status: "intent",
|
|
641
|
+
errorCode: null,
|
|
642
|
+
updatedAt: new Date().toISOString(),
|
|
643
|
+
};
|
|
644
|
+
delivery = persistDelivery(store, run.id, upsertEffect(delivery, prEffect), "delivery.pull_request_intent", { effectId: prEffect.id });
|
|
645
|
+
prEffect = {
|
|
646
|
+
...prEffect,
|
|
647
|
+
status: "call_started",
|
|
648
|
+
attemptCount: prEffect.attemptCount + 1,
|
|
649
|
+
updatedAt: new Date().toISOString(),
|
|
650
|
+
};
|
|
651
|
+
delivery = persistDelivery(store, run.id, upsertEffect(delivery, prEffect), "delivery.pull_request_started", { effectId: prEffect.id });
|
|
652
|
+
assertRemoteMutationNotCancelled(store, run.id, delivery);
|
|
653
|
+
try {
|
|
654
|
+
pullRequest = await adapter.createDraftPullRequest({
|
|
655
|
+
config,
|
|
656
|
+
branch: delivery.branchName,
|
|
657
|
+
title: inputs.task.commit.message.slice(0, 240),
|
|
658
|
+
body: `${deliveryMarker(delivery.deliveryKey)}\n\nGenerated by Mill from an attended, locally validated and reviewed run. The current candidate identity is the pull-request head; configured human merge authority remains external.`,
|
|
659
|
+
deadlineMs,
|
|
660
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
661
|
+
cancellationRequested: () => cancellationRequested(store, run.id),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
const failure = asMillError(error);
|
|
666
|
+
try {
|
|
667
|
+
readback = await reconcileReadback({
|
|
668
|
+
adapter,
|
|
669
|
+
config,
|
|
670
|
+
delivery,
|
|
671
|
+
deadlineMs,
|
|
672
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
673
|
+
});
|
|
674
|
+
pullRequest = readback.pullRequest;
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
markUnknown(store, run, delivery, prEffect, failure.code);
|
|
678
|
+
}
|
|
679
|
+
if (pullRequest === null) {
|
|
680
|
+
markUnknown(store, run, delivery, prEffect, failure.code);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
assertExactPullRequest(pullRequest, delivery, true);
|
|
684
|
+
prEffect = {
|
|
685
|
+
...prEffect,
|
|
686
|
+
status: "verified",
|
|
687
|
+
errorCode: null,
|
|
688
|
+
updatedAt: new Date().toISOString(),
|
|
689
|
+
};
|
|
690
|
+
delivery = upsertEffect(delivery, prEffect);
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
assertExactPullRequest(pullRequest, delivery, true);
|
|
694
|
+
prEffect = {
|
|
695
|
+
...(prEffect ?? {
|
|
696
|
+
id: effectId(delivery.deliveryKey, "pull_request", candidate.commit),
|
|
697
|
+
kind: "pull_request",
|
|
698
|
+
candidateCommit: candidate.commit,
|
|
699
|
+
attemptCount: 0,
|
|
700
|
+
expectedOldCommit: null,
|
|
701
|
+
}),
|
|
702
|
+
status: "verified",
|
|
703
|
+
errorCode: null,
|
|
704
|
+
updatedAt: new Date().toISOString(),
|
|
705
|
+
};
|
|
706
|
+
delivery = upsertEffect(delivery, prEffect);
|
|
707
|
+
}
|
|
708
|
+
delivery = persistDelivery(store, run.id, {
|
|
709
|
+
...delivery,
|
|
710
|
+
state: "awaiting_ci",
|
|
711
|
+
pullRequest: {
|
|
712
|
+
number: pullRequest.number,
|
|
713
|
+
nodeId: pullRequest.nodeId,
|
|
714
|
+
url: pullRequest.url,
|
|
715
|
+
},
|
|
716
|
+
observation: null,
|
|
717
|
+
lastErrorCode: null,
|
|
718
|
+
}, "delivery.pull_request_verified", { pullRequestNumber: pullRequest.number });
|
|
719
|
+
assertRemoteMutationNotCancelled(store, run.id, delivery);
|
|
720
|
+
run = store.getRun(run.id);
|
|
721
|
+
if (run.status !== "awaiting_ci") {
|
|
722
|
+
run = store.transition(run.id, "awaiting_ci", "delivery.awaiting_ci");
|
|
723
|
+
}
|
|
724
|
+
return { run: publicRunRecord(run), delivery };
|
|
725
|
+
}
|
|
726
|
+
finally {
|
|
727
|
+
try {
|
|
728
|
+
await lease?.release();
|
|
729
|
+
}
|
|
730
|
+
finally {
|
|
731
|
+
store.close();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
export async function reconcileDraftPr(input) {
|
|
736
|
+
const context = await openDeliveryContext(input.root, input.taskPath, "readback");
|
|
737
|
+
const { inputs, config, store } = context;
|
|
738
|
+
let lease;
|
|
739
|
+
try {
|
|
740
|
+
lease = await acquireWriterLease(store);
|
|
741
|
+
let run = store.getRun(input.runId);
|
|
742
|
+
if (run.status !== "effect_unknown") {
|
|
743
|
+
throw new MillError("RECONCILIATION_NOT_REQUIRED", "The run has no unknown GitHub effect to reconcile.", ExitCode.configuration);
|
|
744
|
+
}
|
|
745
|
+
let delivery = storedDelivery(run);
|
|
746
|
+
const adapter = input.adapter ?? createGitHubAdapter(input.root);
|
|
747
|
+
const deadlineMs = operationDeadline(config);
|
|
748
|
+
const binding = await adapter.inspect({
|
|
749
|
+
config,
|
|
750
|
+
deadlineMs,
|
|
751
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
752
|
+
});
|
|
753
|
+
await assertBinding(input.root, config, binding);
|
|
754
|
+
assertDeliveryContinuity({ run, inputs, config, delivery, binding });
|
|
755
|
+
const readback = await reconcileReadback({
|
|
756
|
+
adapter,
|
|
757
|
+
config,
|
|
758
|
+
delivery,
|
|
759
|
+
deadlineMs,
|
|
760
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
761
|
+
});
|
|
762
|
+
const unknownEffects = delivery.effects.filter((item) => item.status === "effect_unknown");
|
|
763
|
+
if (unknownEffects.length !== 1) {
|
|
764
|
+
throw new MillError("GITHUB_RECONCILIATION_STATE_INVALID", "Exactly one external effect must be unknown before reconciliation.", ExitCode.data);
|
|
765
|
+
}
|
|
766
|
+
const unknownEffect = unknownEffects[0];
|
|
767
|
+
if (unknownEffect === undefined) {
|
|
768
|
+
throw new MillError("GITHUB_RECONCILIATION_STATE_INVALID", "The unknown external effect could not be identified.", ExitCode.data);
|
|
769
|
+
}
|
|
770
|
+
if (unknownEffect.kind === "push" &&
|
|
771
|
+
(readback.branchSha === unknownEffect.expectedOldCommit ||
|
|
772
|
+
readback.branchSha === delivery.candidateCommit)) {
|
|
773
|
+
assertPushBoundaryPullRequest(readback.pullRequest, delivery, readback.branchSha);
|
|
774
|
+
}
|
|
775
|
+
let effectAbsent = false;
|
|
776
|
+
if (unknownEffect.kind === "pull_request") {
|
|
777
|
+
effectAbsent =
|
|
778
|
+
readback.pullRequest === null &&
|
|
779
|
+
readback.branchSha === delivery.candidateCommit;
|
|
780
|
+
}
|
|
781
|
+
else if (readback.branchSha === unknownEffect.expectedOldCommit) {
|
|
782
|
+
effectAbsent = true;
|
|
783
|
+
}
|
|
784
|
+
if (effectAbsent) {
|
|
785
|
+
return reconcileAbsentEffect(store, run, delivery, unknownEffect);
|
|
786
|
+
}
|
|
787
|
+
if (unknownEffect.kind === "push" &&
|
|
788
|
+
readback.branchSha === delivery.candidateCommit &&
|
|
789
|
+
readback.pullRequest === null) {
|
|
790
|
+
delivery = persistDelivery(store, run.id, {
|
|
791
|
+
...upsertEffect(delivery, {
|
|
792
|
+
...unknownEffect,
|
|
793
|
+
status: "verified",
|
|
794
|
+
errorCode: null,
|
|
795
|
+
updatedAt: new Date().toISOString(),
|
|
796
|
+
}),
|
|
797
|
+
state: "proposing",
|
|
798
|
+
remoteHeadCommit: readback.branchSha,
|
|
799
|
+
lastErrorCode: null,
|
|
800
|
+
}, "delivery.push_reconciled", { effectId: unknownEffect.id });
|
|
801
|
+
assertRemoteMutationNotCancelled(store, run.id, delivery);
|
|
802
|
+
run = store.transition(run.id, "proposing", "delivery.reconciled");
|
|
803
|
+
return { run: publicRunRecord(run), delivery };
|
|
804
|
+
}
|
|
805
|
+
if (readback.branchSha !== delivery.candidateCommit) {
|
|
806
|
+
throw new MillError("REMOTE_BRANCH_CONFLICT", "Authoritative readback found a branch head outside the reconciled effect precondition.", ExitCode.configuration);
|
|
807
|
+
}
|
|
808
|
+
if (readback.pullRequest === null) {
|
|
809
|
+
throw new MillError("GITHUB_RECONCILIATION_STATE_INVALID", "Reconciliation could not classify the pull-request effect.", ExitCode.data);
|
|
810
|
+
}
|
|
811
|
+
assertExactPullRequest(readback.pullRequest, delivery, true);
|
|
812
|
+
delivery = persistDelivery(store, run.id, {
|
|
813
|
+
...delivery,
|
|
814
|
+
state: "awaiting_ci",
|
|
815
|
+
remoteHeadCommit: readback.branchSha,
|
|
816
|
+
pullRequest: {
|
|
817
|
+
number: readback.pullRequest.number,
|
|
818
|
+
nodeId: readback.pullRequest.nodeId,
|
|
819
|
+
url: readback.pullRequest.url,
|
|
820
|
+
},
|
|
821
|
+
effects: delivery.effects.map((item) => item.id === unknownEffect.id
|
|
822
|
+
? {
|
|
823
|
+
...item,
|
|
824
|
+
status: "verified",
|
|
825
|
+
errorCode: null,
|
|
826
|
+
updatedAt: new Date().toISOString(),
|
|
827
|
+
}
|
|
828
|
+
: item),
|
|
829
|
+
lastErrorCode: null,
|
|
830
|
+
}, "delivery.reconciled", { pullRequestNumber: readback.pullRequest.number });
|
|
831
|
+
assertRemoteMutationNotCancelled(store, run.id, delivery);
|
|
832
|
+
run = store.transition(run.id, "awaiting_ci", "delivery.awaiting_ci");
|
|
833
|
+
return { run: publicRunRecord(run), delivery };
|
|
834
|
+
}
|
|
835
|
+
finally {
|
|
836
|
+
try {
|
|
837
|
+
await lease?.release();
|
|
838
|
+
}
|
|
839
|
+
finally {
|
|
840
|
+
store.close();
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function assertObservationIdentity(observation, delivery, allowDeletedBranch) {
|
|
845
|
+
const pull = observation.pullRequest;
|
|
846
|
+
if (!exactPullRequest(pull, delivery) ||
|
|
847
|
+
pull.number !== delivery.pullRequest?.number ||
|
|
848
|
+
pull.nodeId !== delivery.pullRequest.nodeId ||
|
|
849
|
+
pull.headSha !== delivery.candidateCommit ||
|
|
850
|
+
(!allowDeletedBranch &&
|
|
851
|
+
observation.branchSha !== delivery.candidateCommit) ||
|
|
852
|
+
(allowDeletedBranch &&
|
|
853
|
+
observation.branchSha !== null &&
|
|
854
|
+
observation.branchSha !== delivery.candidateCommit)) {
|
|
855
|
+
throw new MillError("REMOTE_IDENTITY_DRIFT", "GitHub branch or pull request identity drifted from the exact delivery record.", ExitCode.configuration);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
export async function observeDraftPr(input) {
|
|
859
|
+
const context = await openDeliveryContext(input.root, input.taskPath, "readback");
|
|
860
|
+
const { inputs, config, store } = context;
|
|
861
|
+
let lease;
|
|
862
|
+
try {
|
|
863
|
+
lease = await acquireWriterLease(store);
|
|
864
|
+
let run = store.getRun(input.runId);
|
|
865
|
+
if (run.status !== "awaiting_ci" &&
|
|
866
|
+
run.status !== "awaiting_human" &&
|
|
867
|
+
!(run.status === "blocked" &&
|
|
868
|
+
["REMOTE_CHECKS_FAILED", "REMOTE_REVIEW_FINDINGS"].includes(run.blockCode ?? ""))) {
|
|
869
|
+
throw new MillError("DELIVERY_NOT_OBSERVABLE", "Run has no draft pull request awaiting policy observation.", ExitCode.configuration);
|
|
870
|
+
}
|
|
871
|
+
let delivery = storedDelivery(run);
|
|
872
|
+
if (delivery.pullRequest === null) {
|
|
873
|
+
throw new MillError("PULL_REQUEST_IDENTITY_MISSING", "Delivery has no verified pull request identity.", ExitCode.configuration);
|
|
874
|
+
}
|
|
875
|
+
const adapter = input.adapter ?? createGitHubAdapter(input.root);
|
|
876
|
+
const deadlineMs = operationDeadline(config);
|
|
877
|
+
const binding = await adapter.inspect({
|
|
878
|
+
config,
|
|
879
|
+
deadlineMs,
|
|
880
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
881
|
+
});
|
|
882
|
+
await assertBinding(input.root, config, binding);
|
|
883
|
+
assertDeliveryContinuity({ run, inputs, config, delivery, binding });
|
|
884
|
+
const observation = await adapter.observe({
|
|
885
|
+
config,
|
|
886
|
+
pullRequestNumber: delivery.pullRequest.number,
|
|
887
|
+
deadlineMs,
|
|
888
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
889
|
+
});
|
|
890
|
+
assertObservationIdentity(observation, delivery, false);
|
|
891
|
+
const checks = checkDecision(delivery.requiredChecks, observation.checks);
|
|
892
|
+
const feedback = actionableFeedback(observation, delivery.reviewPolicy, delivery.candidateCommit);
|
|
893
|
+
const observationRecord = {
|
|
894
|
+
headSha: observation.pullRequest.headSha,
|
|
895
|
+
branchSha: observation.branchSha,
|
|
896
|
+
checkDecision: checks,
|
|
897
|
+
checks: observation.checks,
|
|
898
|
+
reviews: observation.reviews,
|
|
899
|
+
feedback,
|
|
900
|
+
observedAt: new Date().toISOString(),
|
|
901
|
+
};
|
|
902
|
+
if (feedback.length > 0) {
|
|
903
|
+
store.setRemoteFeedback(run.id, JSON.stringify(feedbackAsReview(delivery.candidateCommit, feedback)));
|
|
904
|
+
delivery = persistDelivery(store, run.id, {
|
|
905
|
+
...delivery,
|
|
906
|
+
state: "blocked",
|
|
907
|
+
observation: observationRecord,
|
|
908
|
+
lastErrorCode: "REMOTE_REVIEW_FINDINGS",
|
|
909
|
+
}, "delivery.remote_review_blocked", { findings: feedback.length });
|
|
910
|
+
run = setRunBlocker(store, run, "REMOTE_REVIEW_FINDINGS", "delivery.blocked");
|
|
911
|
+
return { run: publicRunRecord(run), delivery };
|
|
912
|
+
}
|
|
913
|
+
if (checks.status === "failed") {
|
|
914
|
+
delivery = persistDelivery(store, run.id, {
|
|
915
|
+
...delivery,
|
|
916
|
+
state: "blocked",
|
|
917
|
+
observation: observationRecord,
|
|
918
|
+
lastErrorCode: "REMOTE_CHECKS_FAILED",
|
|
919
|
+
}, "delivery.remote_checks_failed", { failed: checks.failed.length });
|
|
920
|
+
run = setRunBlocker(store, run, "REMOTE_CHECKS_FAILED", "delivery.blocked");
|
|
921
|
+
return { run: publicRunRecord(run), delivery };
|
|
922
|
+
}
|
|
923
|
+
const ready = checks.status === "passed" &&
|
|
924
|
+
reviewsPassed(observation, delivery.reviewPolicy, delivery.candidateCommit);
|
|
925
|
+
const nextState = ready ? "awaiting_human" : "awaiting_ci";
|
|
926
|
+
delivery = persistDelivery(store, run.id, {
|
|
927
|
+
...delivery,
|
|
928
|
+
state: nextState,
|
|
929
|
+
observation: observationRecord,
|
|
930
|
+
lastErrorCode: null,
|
|
931
|
+
}, ready ? "delivery.awaiting_human" : "delivery.policy_pending");
|
|
932
|
+
if (run.status === "blocked") {
|
|
933
|
+
run = store.transition(run.id, "awaiting_ci", "delivery.reobserved");
|
|
934
|
+
}
|
|
935
|
+
if (ready && run.status !== "awaiting_human") {
|
|
936
|
+
run = store.transition(run.id, "awaiting_human", "delivery.awaiting_human");
|
|
937
|
+
}
|
|
938
|
+
else if (!ready && run.status !== "awaiting_ci") {
|
|
939
|
+
run = store.transition(run.id, "awaiting_ci", "delivery.policy_pending");
|
|
940
|
+
}
|
|
941
|
+
return { run: publicRunRecord(run), delivery };
|
|
942
|
+
}
|
|
943
|
+
finally {
|
|
944
|
+
try {
|
|
945
|
+
await lease?.release();
|
|
946
|
+
}
|
|
947
|
+
finally {
|
|
948
|
+
store.close();
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function mergeMethod(observation) {
|
|
953
|
+
const commit = observation.mergeCommit;
|
|
954
|
+
if (commit === null) {
|
|
955
|
+
throw new MillError("MERGE_COMMIT_MISSING", "GitHub did not return the exact merge commit identity.", ExitCode.data);
|
|
956
|
+
}
|
|
957
|
+
if (commit.parents.length >= 2)
|
|
958
|
+
return "merge";
|
|
959
|
+
if (commit.parents.length === 1) {
|
|
960
|
+
return "linear_tree_preserving";
|
|
961
|
+
}
|
|
962
|
+
throw new MillError("MERGE_TOPOLOGY_INVALID", "The observed merge commit has no parent and cannot be classified safely.", ExitCode.data);
|
|
963
|
+
}
|
|
964
|
+
export async function finalizeDraftPr(input) {
|
|
965
|
+
const context = await openDeliveryContext(input.root, input.taskPath, "readback");
|
|
966
|
+
const { inputs, config, store } = context;
|
|
967
|
+
let lease;
|
|
968
|
+
try {
|
|
969
|
+
lease = await acquireWriterLease(store);
|
|
970
|
+
let run = store.getRun(input.runId);
|
|
971
|
+
if (run.status !== "awaiting_human" &&
|
|
972
|
+
run.status !== "merged" &&
|
|
973
|
+
run.status !== "post_merge_verified" &&
|
|
974
|
+
!(run.status === "blocked" && run.blockCode === "POST_MERGE_CHECKS_FAILED")) {
|
|
975
|
+
throw new MillError("DELIVERY_NOT_FINALIZABLE", "Run is not awaiting human merge or post-merge verification.", ExitCode.configuration);
|
|
976
|
+
}
|
|
977
|
+
let delivery = storedDelivery(run);
|
|
978
|
+
if (delivery.pullRequest === null) {
|
|
979
|
+
throw new MillError("PULL_REQUEST_IDENTITY_MISSING", "Delivery has no verified pull request identity.", ExitCode.configuration);
|
|
980
|
+
}
|
|
981
|
+
const adapter = input.adapter ?? createGitHubAdapter(input.root);
|
|
982
|
+
const deadlineMs = operationDeadline(config);
|
|
983
|
+
const binding = await adapter.inspect({
|
|
984
|
+
config,
|
|
985
|
+
deadlineMs,
|
|
986
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
987
|
+
});
|
|
988
|
+
await assertBinding(input.root, config, binding);
|
|
989
|
+
assertDeliveryContinuity({ run, inputs, config, delivery, binding });
|
|
990
|
+
const observation = await adapter.observe({
|
|
991
|
+
config,
|
|
992
|
+
pullRequestNumber: delivery.pullRequest.number,
|
|
993
|
+
deadlineMs,
|
|
994
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
995
|
+
});
|
|
996
|
+
assertObservationIdentity(observation, delivery, true);
|
|
997
|
+
if (!observation.pullRequest.merged) {
|
|
998
|
+
if (observation.pullRequest.state === "closed") {
|
|
999
|
+
throw new MillError("PULL_REQUEST_CLOSED_UNMERGED", "The delivery pull request was closed without a merge.", ExitCode.configuration);
|
|
1000
|
+
}
|
|
1001
|
+
throw new MillError("HUMAN_MERGE_PENDING", "The draft pull request has not been merged by the human owner.", ExitCode.temporary);
|
|
1002
|
+
}
|
|
1003
|
+
if (observation.mergeCommit === null) {
|
|
1004
|
+
throw new MillError("MERGE_READBACK_MISMATCH", "GitHub readback cannot prove the exact merge commit.", ExitCode.configuration);
|
|
1005
|
+
}
|
|
1006
|
+
const mergeCommit = observation.mergeCommit;
|
|
1007
|
+
if (observation.pullRequest.mergeCommitSha !== mergeCommit.sha ||
|
|
1008
|
+
observation.pullRequest.mergedByLogin === null ||
|
|
1009
|
+
observation.pullRequest.mergedAt === null ||
|
|
1010
|
+
!observation.mergeIsOnDefaultBranch) {
|
|
1011
|
+
throw new MillError("MERGE_READBACK_MISMATCH", "GitHub readback cannot prove the exact merge is contained by the default branch.", ExitCode.configuration);
|
|
1012
|
+
}
|
|
1013
|
+
const mergedByLogin = observation.pullRequest.mergedByLogin;
|
|
1014
|
+
if (!delivery.allowedMergerLogins.includes(mergedByLogin)) {
|
|
1015
|
+
throw new MillError("MERGER_NOT_ALLOWED", "The pull request was merged by an identity outside the approved human merge authority.", ExitCode.configuration, { mergedByLogin });
|
|
1016
|
+
}
|
|
1017
|
+
const method = mergeMethod(observation);
|
|
1018
|
+
if (!delivery.allowedMergeMethods.includes(method)) {
|
|
1019
|
+
throw new MillError("MERGE_METHOD_NOT_ALLOWED", "The observed merge method is outside the repository policy.", ExitCode.configuration);
|
|
1020
|
+
}
|
|
1021
|
+
if (mergeCommit.tree !== delivery.candidateTree) {
|
|
1022
|
+
throw new MillError("MERGE_TREE_REVALIDATION_REQUIRED", "The merged tree differs from the reviewed candidate and requires a fresh exact-tree validation before closure.", ExitCode.configuration);
|
|
1023
|
+
}
|
|
1024
|
+
delivery = persistDelivery(store, run.id, {
|
|
1025
|
+
...delivery,
|
|
1026
|
+
state: "merged",
|
|
1027
|
+
merge: {
|
|
1028
|
+
commit: mergeCommit.sha,
|
|
1029
|
+
tree: mergeCommit.tree,
|
|
1030
|
+
method,
|
|
1031
|
+
mergedByLogin,
|
|
1032
|
+
mergedAt: observation.pullRequest.mergedAt,
|
|
1033
|
+
defaultBranchHead: observation.defaultBranchHead,
|
|
1034
|
+
},
|
|
1035
|
+
observation: {
|
|
1036
|
+
mergeChecks: observation.mergeChecks,
|
|
1037
|
+
observedAt: new Date().toISOString(),
|
|
1038
|
+
},
|
|
1039
|
+
lastErrorCode: null,
|
|
1040
|
+
}, "delivery.merge_verified", { mergeCommit: mergeCommit.sha, method });
|
|
1041
|
+
if (run.status !== "merged" && run.status !== "post_merge_verified") {
|
|
1042
|
+
run = store.transition(run.id, "merged", "delivery.merged");
|
|
1043
|
+
}
|
|
1044
|
+
const checks = checkDecision(delivery.requiredChecks, observation.mergeChecks);
|
|
1045
|
+
if (checks.status === "pending") {
|
|
1046
|
+
return { run: publicRunRecord(run), delivery };
|
|
1047
|
+
}
|
|
1048
|
+
if (checks.status === "failed") {
|
|
1049
|
+
delivery = persistDelivery(store, run.id, {
|
|
1050
|
+
...delivery,
|
|
1051
|
+
state: "blocked",
|
|
1052
|
+
lastErrorCode: "POST_MERGE_CHECKS_FAILED",
|
|
1053
|
+
}, "delivery.post_merge_checks_failed", { failed: checks.failed.length });
|
|
1054
|
+
run = setRunBlocker(store, run, "POST_MERGE_CHECKS_FAILED", "delivery.blocked");
|
|
1055
|
+
return { run: publicRunRecord(run), delivery };
|
|
1056
|
+
}
|
|
1057
|
+
if (run.status !== "post_merge_verified") {
|
|
1058
|
+
run = store.transition(run.id, "post_merge_verified", "delivery.post_merge_verified");
|
|
1059
|
+
}
|
|
1060
|
+
delivery = persistDelivery(store, run.id, { ...delivery, state: "closed", lastErrorCode: null }, "delivery.closed");
|
|
1061
|
+
run = store.transition(run.id, "closed", "run.closed");
|
|
1062
|
+
return { run: publicRunRecord(run), delivery };
|
|
1063
|
+
}
|
|
1064
|
+
finally {
|
|
1065
|
+
try {
|
|
1066
|
+
await lease?.release();
|
|
1067
|
+
}
|
|
1068
|
+
finally {
|
|
1069
|
+
store.close();
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
//# sourceMappingURL=delivery.js.map
|