@kungfu-tech/buildchain 3.0.6 → 3.0.7-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/actions/promote-buildchain-ref/README.md +10 -0
- package/actions/release-tail/README.md +17 -0
- package/bin/buildchain.mjs +11 -0
- package/bin/internal/command-registry.mjs +2 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
- package/contracts/engineering-housekeeper-v1.schema.json +143 -0
- package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
- package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
- package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
- package/contracts/release-tail-capabilities-v1.schema.json +199 -0
- package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +42 -27
- package/dist/site/buildchain-site.json +384 -33
- package/dist/site/capability-registry.json +15 -12
- package/dist/site/cli-registry.json +102 -0
- package/dist/site/controller-registry.json +19 -3
- package/dist/site/kfd-claims.json +359 -17
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +53 -7
- package/dist/site/node-api-registry.json +5882 -3605
- package/dist/site/page-registry.json +348 -21
- package/dist/site/public-surface-audit.json +396 -16
- package/dist/site/publication-authority-registry.json +106 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +7 -0
- package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
- package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
- package/dist/site/site-manifest.json +35 -11
- package/dist/site/workflow-registry.json +204 -10
- package/docs/MAP.md +6 -2
- package/docs/auditable-demo.md +2 -2
- package/docs/cli-reference.md +136 -0
- package/docs/dev-delivery-warrant.md +49 -4
- package/docs/engineering-housekeeper.md +138 -0
- package/docs/lifecycle-protocol.md +4 -2
- package/docs/node-api-reference.md +495 -281
- package/docs/publication-rehearsal.md +94 -0
- package/docs/release-governance.md +17 -2
- package/docs/release-tail-contract.md +160 -0
- package/docs/release-tail-provider-plane.md +120 -0
- package/docs/reusable-build-surface.md +11 -0
- package/package.json +11 -3
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-agent-manuals.js +3 -0
- package/packages/core/buildchain-config.js +66 -6
- package/packages/core/buildchain-kfd-claims.js +1 -1
- package/packages/core/buildchain-publication-authority.js +5 -0
- package/packages/core/controller-evidence.js +2 -1
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-shadow.js +502 -0
- package/packages/core/dev-delivery-warrant.js +15 -6
- package/packages/core/diagnostics.js +8 -3
- package/packages/core/engineering-housekeeper-github-client.js +222 -0
- package/packages/core/engineering-housekeeper-github.js +501 -0
- package/packages/core/engineering-housekeeper.js +259 -0
- package/packages/core/index.js +42 -0
- package/packages/core/kfd-gate.js +45 -15
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper-repository.js +1 -0
- package/packages/core/paper-scaffold-content.js +21 -0
- package/packages/core/paper.js +28 -2
- package/packages/core/publication-rehearsal-projection.js +173 -0
- package/packages/core/publication-rehearsal-runtime.js +921 -0
- package/packages/core/release-passport.js +130 -20
- package/packages/core/release-tail-compatibility.js +60 -0
- package/packages/core/release-tail-provider-adapters.js +461 -0
- package/packages/core/release-tail-provider-plane.js +1228 -0
- package/scripts/assemble-publication-artifact-admission.mjs +1 -1
- package/scripts/assemble-self-publication-admission.mjs +2 -2
- package/scripts/audit-publication-control-plane.mjs +1 -1
- package/scripts/auditable-demo-bundle-verification.mjs +2 -3
- package/scripts/auditable-demo-platform.mjs +2 -2
- package/scripts/auditable-demo-renditions.mjs +1 -1
- package/scripts/auditable-demo.mjs +2 -2
- package/scripts/build-contract-core.mjs +8 -3
- package/scripts/build-standalone-binary.mjs +14 -3
- package/scripts/buildchain-cli-help.mjs +13 -0
- package/scripts/check-core-mechanism-inventory.mjs +347 -0
- package/scripts/check-inventory.mjs +10 -8
- package/scripts/check-maintainability.mjs +9 -2
- package/scripts/check-release-tail-contract.mjs +435 -0
- package/scripts/dev-delivery-warrant.mjs +31 -4
- package/scripts/dev-pr-auto-merge.mjs +30 -4
- package/scripts/dev-pr-delivery-warrant.mjs +50 -0
- package/scripts/engineering-housekeeper-workflow.mjs +394 -0
- package/scripts/generate-channel-promotion-workflow.mjs +10 -8
- package/scripts/generate-site-bundle.mjs +47 -4
- package/scripts/init-repo.mjs +26 -2
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/materialize-self-release-candidate-version.mjs +137 -0
- package/scripts/publication-commit-evidence.mjs +69 -23
- package/scripts/release-candidate-resolver.mjs +16 -10
- package/scripts/release-tail.mjs +159 -0
- package/scripts/resume-from-candidate-run.mjs +123 -9
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +13 -0
- package/scripts/v4-architecture.mjs +600 -0
- package/scripts/web-surface-core.mjs +8 -2
- package/scripts/workflow-call-contract.mjs +184 -5
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOUSEKEEPER_REASON_CODES,
|
|
3
|
+
classifyHousekeeperReplay,
|
|
4
|
+
createEngineeringHousekeeperPlan,
|
|
5
|
+
createEngineeringHousekeeperReceipt,
|
|
6
|
+
engineeringHousekeeperRoot,
|
|
7
|
+
revalidateHousekeeperBranchAction,
|
|
8
|
+
} from "./engineering-housekeeper.js";
|
|
9
|
+
import { GitHubHousekeeperProviderError } from "./engineering-housekeeper-github-client.js";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
GitHubHousekeeperClient,
|
|
13
|
+
GitHubHousekeeperProviderError,
|
|
14
|
+
} from "./engineering-housekeeper-github-client.js";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_STALE_DAYS = 30;
|
|
17
|
+
const DEFAULT_MAX_ACTIONS = 20;
|
|
18
|
+
function requiredString(value, field) {
|
|
19
|
+
const normalized = String(value || "").trim();
|
|
20
|
+
if (!normalized) throw new Error(`${field} is required`);
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
function normalizeRepository(value) {
|
|
24
|
+
const normalized = requiredString(value?.fullName || value, "repository");
|
|
25
|
+
const match = normalized.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
26
|
+
if (!match)
|
|
27
|
+
throw new Error(`repository must be owner/repo, got: ${normalized}`);
|
|
28
|
+
return { owner: match[1], repo: match[2], fullName: normalized };
|
|
29
|
+
}
|
|
30
|
+
function normalizeDate(value, field) {
|
|
31
|
+
const date = new Date(requiredString(value, field));
|
|
32
|
+
if (Number.isNaN(date.getTime()))
|
|
33
|
+
throw new Error(`${field} must be an ISO date-time`);
|
|
34
|
+
return date;
|
|
35
|
+
}
|
|
36
|
+
function normalizePositiveInteger(value, fallback, field) {
|
|
37
|
+
const selected = value === undefined ? fallback : Number(value);
|
|
38
|
+
if (!Number.isInteger(selected) || selected < 1) {
|
|
39
|
+
throw new Error(`${field} must be a positive integer`);
|
|
40
|
+
}
|
|
41
|
+
return selected;
|
|
42
|
+
}
|
|
43
|
+
function providerError(error, operation) {
|
|
44
|
+
if (error instanceof GitHubHousekeeperProviderError) return error;
|
|
45
|
+
return new GitHubHousekeeperProviderError(
|
|
46
|
+
`${operation} failed: ${error?.message || String(error)}`,
|
|
47
|
+
{ operation, cause: error },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function headOid(branch) {
|
|
52
|
+
return String(
|
|
53
|
+
branch?.commit?.sha || branch?.object?.sha || branch?.headOid || "",
|
|
54
|
+
).toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pullRequestHeadRepository(pullRequest) {
|
|
58
|
+
return String(
|
|
59
|
+
pullRequest?.head?.repo?.full_name || pullRequest?.headRepository || "",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizePullRequest(pullRequest, repository, observedAt, staleDays) {
|
|
64
|
+
const updatedAt = new Date(
|
|
65
|
+
pullRequest.updated_at || pullRequest.updatedAt || "",
|
|
66
|
+
);
|
|
67
|
+
const staleBefore = observedAt.getTime() - staleDays * 24 * 60 * 60 * 1000;
|
|
68
|
+
return {
|
|
69
|
+
repository,
|
|
70
|
+
number: Number(pullRequest.number),
|
|
71
|
+
state: pullRequest.draft ? "draft" : String(pullRequest.state || "open"),
|
|
72
|
+
stale:
|
|
73
|
+
!Number.isNaN(updatedAt.getTime()) && updatedAt.getTime() <= staleBefore,
|
|
74
|
+
headRepository: pullRequestHeadRepository(pullRequest),
|
|
75
|
+
headRef: String(pullRequest?.head?.ref || pullRequest.headRef || ""),
|
|
76
|
+
headOid: String(
|
|
77
|
+
pullRequest?.head?.sha || pullRequest.headOid || "",
|
|
78
|
+
).toLowerCase(),
|
|
79
|
+
labels: (pullRequest.labels || []).map((label) =>
|
|
80
|
+
String(label?.name || label),
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function openPullRequestNumbers(pullRequests, repository, branchName) {
|
|
86
|
+
return pullRequests
|
|
87
|
+
.filter(
|
|
88
|
+
(pullRequest) =>
|
|
89
|
+
pullRequest.headRepository === repository &&
|
|
90
|
+
pullRequest.headRef === branchName &&
|
|
91
|
+
["open", "draft"].includes(pullRequest.state),
|
|
92
|
+
)
|
|
93
|
+
.map((pullRequest) => pullRequest.number)
|
|
94
|
+
.sort((left, right) => left - right);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function branchAncestry(client, repository, branchOid, targetOid) {
|
|
98
|
+
if (branchOid === targetOid) return "ancestor";
|
|
99
|
+
const comparison = await client.compareCommits(
|
|
100
|
+
repository,
|
|
101
|
+
branchOid,
|
|
102
|
+
targetOid,
|
|
103
|
+
);
|
|
104
|
+
return String(comparison?.merge_base_commit?.sha || "").toLowerCase() ===
|
|
105
|
+
branchOid
|
|
106
|
+
? "ancestor"
|
|
107
|
+
: "ambiguous";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function collectGitHubHousekeeperInventory({
|
|
111
|
+
client,
|
|
112
|
+
repository,
|
|
113
|
+
targetBranch,
|
|
114
|
+
observedAt,
|
|
115
|
+
staleDays = DEFAULT_STALE_DAYS,
|
|
116
|
+
policy = {},
|
|
117
|
+
}) {
|
|
118
|
+
const coordinate = normalizeRepository(repository);
|
|
119
|
+
const targetName = requiredString(targetBranch, "targetBranch").replace(
|
|
120
|
+
/^refs\/heads\//,
|
|
121
|
+
"",
|
|
122
|
+
);
|
|
123
|
+
const observation = normalizeDate(observedAt, "observedAt");
|
|
124
|
+
const staleWindow = normalizePositiveInteger(
|
|
125
|
+
staleDays,
|
|
126
|
+
DEFAULT_STALE_DAYS,
|
|
127
|
+
"staleDays",
|
|
128
|
+
);
|
|
129
|
+
try {
|
|
130
|
+
const [repositoryState, branchStates, pullRequestStates, targetState] =
|
|
131
|
+
await Promise.all([
|
|
132
|
+
client.getRepository(coordinate.fullName),
|
|
133
|
+
client.listBranches(coordinate.fullName),
|
|
134
|
+
client.listOpenPullRequests(coordinate.fullName),
|
|
135
|
+
client.getBranch(coordinate.fullName, targetName),
|
|
136
|
+
]);
|
|
137
|
+
const target = { name: targetName, headOid: headOid(targetState) };
|
|
138
|
+
if (!target.headOid)
|
|
139
|
+
throw new Error(`target branch ${targetName} has no head OID`);
|
|
140
|
+
const pullRequests = pullRequestStates
|
|
141
|
+
.map((entry) =>
|
|
142
|
+
normalizePullRequest(
|
|
143
|
+
entry,
|
|
144
|
+
coordinate.fullName,
|
|
145
|
+
observation,
|
|
146
|
+
staleWindow,
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
.sort((left, right) => left.number - right.number);
|
|
150
|
+
const branches = [];
|
|
151
|
+
for (const branchState of [...branchStates].sort((left, right) =>
|
|
152
|
+
String(left.name).localeCompare(String(right.name)),
|
|
153
|
+
)) {
|
|
154
|
+
const branchOid = headOid(branchState);
|
|
155
|
+
if (!branchOid)
|
|
156
|
+
throw new Error(`branch ${branchState.name} has no head OID`);
|
|
157
|
+
branches.push({
|
|
158
|
+
repository: coordinate.fullName,
|
|
159
|
+
sourceRepository: coordinate.fullName,
|
|
160
|
+
name: String(branchState.name),
|
|
161
|
+
headOid: branchOid,
|
|
162
|
+
target,
|
|
163
|
+
isDefault:
|
|
164
|
+
String(repositoryState.default_branch) === String(branchState.name),
|
|
165
|
+
isProtected: branchState.protected === true,
|
|
166
|
+
ancestry: await branchAncestry(
|
|
167
|
+
client,
|
|
168
|
+
coordinate.fullName,
|
|
169
|
+
branchOid,
|
|
170
|
+
target.headOid,
|
|
171
|
+
),
|
|
172
|
+
openPullRequestNumbers: openPullRequestNumbers(
|
|
173
|
+
pullRequests,
|
|
174
|
+
coordinate.fullName,
|
|
175
|
+
String(branchState.name),
|
|
176
|
+
),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return createEngineeringHousekeeperPlan({
|
|
180
|
+
repository: coordinate.fullName,
|
|
181
|
+
target,
|
|
182
|
+
branches,
|
|
183
|
+
pullRequests,
|
|
184
|
+
policy,
|
|
185
|
+
observedAt: observation.toISOString(),
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw providerError(error, `inventory ${coordinate.fullName}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function actionIdentity(action) {
|
|
193
|
+
return action.name
|
|
194
|
+
? `${action.kind}:${action.name}`
|
|
195
|
+
: `${action.kind}:#${action.number}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function rejectedOutcome(action, reasonCodes, details = {}) {
|
|
199
|
+
return {
|
|
200
|
+
action: actionIdentity(action),
|
|
201
|
+
status: "rejected",
|
|
202
|
+
reasonCodes: [...new Set(reasonCodes)].sort(),
|
|
203
|
+
...details,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function providerFailureOutcome(action, error) {
|
|
208
|
+
return {
|
|
209
|
+
action: actionIdentity(action),
|
|
210
|
+
status: "provider-error",
|
|
211
|
+
providerError: {
|
|
212
|
+
operation: String(error?.operation || "github"),
|
|
213
|
+
status: Number(error?.status || 0),
|
|
214
|
+
message: String(error?.message || error),
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function rootOutcome(outcome) {
|
|
220
|
+
return { ...outcome, outcomeRoot: engineeringHousekeeperRoot(outcome) };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function currentBranchForAction(client, plan, action) {
|
|
224
|
+
const repository = plan.repository;
|
|
225
|
+
let source;
|
|
226
|
+
try {
|
|
227
|
+
source = await client.getBranch(repository, action.name);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (Number(error?.status) === 404) return { missing: true };
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
const target = await client.getBranch(repository, action.targetName);
|
|
233
|
+
const pullRequests = (await client.listOpenPullRequests(repository)).map(
|
|
234
|
+
(entry) =>
|
|
235
|
+
normalizePullRequest(
|
|
236
|
+
entry,
|
|
237
|
+
repository,
|
|
238
|
+
new Date(plan.observedAt),
|
|
239
|
+
DEFAULT_STALE_DAYS,
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
const sourceOid = headOid(source);
|
|
243
|
+
const targetOid = headOid(target);
|
|
244
|
+
const ancestry = await branchAncestry(
|
|
245
|
+
client,
|
|
246
|
+
repository,
|
|
247
|
+
sourceOid,
|
|
248
|
+
targetOid,
|
|
249
|
+
);
|
|
250
|
+
const [finalSource, finalTarget, finalPullRequests, repositoryState] =
|
|
251
|
+
await Promise.all([
|
|
252
|
+
client.getBranch(repository, action.name),
|
|
253
|
+
client.getBranch(repository, action.targetName),
|
|
254
|
+
client.listOpenPullRequests(repository),
|
|
255
|
+
client.getRepository(repository),
|
|
256
|
+
]);
|
|
257
|
+
const finalSourceOid = headOid(finalSource);
|
|
258
|
+
const finalTargetOid = headOid(finalTarget);
|
|
259
|
+
const normalizedFinalPullRequests = finalPullRequests.map((entry) =>
|
|
260
|
+
normalizePullRequest(
|
|
261
|
+
entry,
|
|
262
|
+
repository,
|
|
263
|
+
new Date(plan.observedAt),
|
|
264
|
+
DEFAULT_STALE_DAYS,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
return {
|
|
268
|
+
name: action.name,
|
|
269
|
+
repository,
|
|
270
|
+
sourceRepository: repository,
|
|
271
|
+
headOid: finalSourceOid,
|
|
272
|
+
target: { name: action.targetName, headOid: finalTargetOid },
|
|
273
|
+
isDefault: String(repositoryState.default_branch) === action.name,
|
|
274
|
+
isProtected: finalSource.protected === true,
|
|
275
|
+
ancestry:
|
|
276
|
+
sourceOid === finalSourceOid && targetOid === finalTargetOid
|
|
277
|
+
? ancestry
|
|
278
|
+
: "ambiguous",
|
|
279
|
+
openPullRequestNumbers: openPullRequestNumbers(
|
|
280
|
+
normalizedFinalPullRequests,
|
|
281
|
+
repository,
|
|
282
|
+
action.name,
|
|
283
|
+
),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function applyBranchAction(client, plan, action) {
|
|
288
|
+
const current = await currentBranchForAction(client, plan, action);
|
|
289
|
+
if (current.missing) {
|
|
290
|
+
return rejectedOutcome(action, [HOUSEKEEPER_REASON_CODES.RENAMED]);
|
|
291
|
+
}
|
|
292
|
+
const validation = revalidateHousekeeperBranchAction(
|
|
293
|
+
action,
|
|
294
|
+
current,
|
|
295
|
+
plan.policy,
|
|
296
|
+
);
|
|
297
|
+
if (!validation.ok) {
|
|
298
|
+
return rejectedOutcome(action, validation.reasonCodes, {
|
|
299
|
+
currentHeadOid: validation.currentHeadOid,
|
|
300
|
+
currentTargetHeadOid: validation.currentTargetHeadOid,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const [fencedSource, fencedTarget, fencedPullRequests] = await Promise.all([
|
|
304
|
+
client.getBranch(plan.repository, action.name),
|
|
305
|
+
client.getBranch(plan.repository, action.targetName),
|
|
306
|
+
client.listOpenPullRequests(plan.repository),
|
|
307
|
+
]);
|
|
308
|
+
const fenced = {
|
|
309
|
+
...current,
|
|
310
|
+
headOid: headOid(fencedSource),
|
|
311
|
+
target: { name: action.targetName, headOid: headOid(fencedTarget) },
|
|
312
|
+
isProtected: fencedSource.protected === true,
|
|
313
|
+
openPullRequestNumbers: openPullRequestNumbers(
|
|
314
|
+
fencedPullRequests.map((entry) =>
|
|
315
|
+
normalizePullRequest(
|
|
316
|
+
entry,
|
|
317
|
+
plan.repository,
|
|
318
|
+
new Date(plan.observedAt),
|
|
319
|
+
DEFAULT_STALE_DAYS,
|
|
320
|
+
),
|
|
321
|
+
),
|
|
322
|
+
plan.repository,
|
|
323
|
+
action.name,
|
|
324
|
+
),
|
|
325
|
+
};
|
|
326
|
+
const fencedValidation = revalidateHousekeeperBranchAction(
|
|
327
|
+
action,
|
|
328
|
+
fenced,
|
|
329
|
+
plan.policy,
|
|
330
|
+
);
|
|
331
|
+
if (!fencedValidation.ok) {
|
|
332
|
+
return rejectedOutcome(action, fencedValidation.reasonCodes, {
|
|
333
|
+
currentHeadOid: fencedValidation.currentHeadOid,
|
|
334
|
+
currentTargetHeadOid: fencedValidation.currentTargetHeadOid,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
await client.deleteBranch(plan.repository, action.name, {
|
|
338
|
+
expectedHeadOid: action.expectedHeadOid,
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
action: actionIdentity(action),
|
|
342
|
+
status: "deleted",
|
|
343
|
+
headOid: action.expectedHeadOid,
|
|
344
|
+
targetHeadOid: action.expectedTargetHeadOid,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function pullRequestRejection(action, current, stale) {
|
|
349
|
+
const reasons = [];
|
|
350
|
+
if (current.headOid !== action.expectedHeadOid) {
|
|
351
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.HEAD_ADVANCED);
|
|
352
|
+
}
|
|
353
|
+
if (!["open", "draft"].includes(current.state)) {
|
|
354
|
+
reasons.push("pull-request.not-active");
|
|
355
|
+
}
|
|
356
|
+
if (!stale) reasons.push("pull-request.not-stale");
|
|
357
|
+
return reasons;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function applyPullRequestAction(
|
|
361
|
+
client,
|
|
362
|
+
plan,
|
|
363
|
+
action,
|
|
364
|
+
appliedAt,
|
|
365
|
+
staleDays,
|
|
366
|
+
) {
|
|
367
|
+
const read = async () =>
|
|
368
|
+
normalizePullRequest(
|
|
369
|
+
await client.getPullRequest(plan.repository, action.number),
|
|
370
|
+
plan.repository,
|
|
371
|
+
appliedAt,
|
|
372
|
+
staleDays,
|
|
373
|
+
);
|
|
374
|
+
let current = await read();
|
|
375
|
+
let reasons = pullRequestRejection(action, current, current.stale);
|
|
376
|
+
if (reasons.length > 0) return rejectedOutcome(action, reasons);
|
|
377
|
+
if (action.kind === "report-pull-request") {
|
|
378
|
+
return {
|
|
379
|
+
action: actionIdentity(action),
|
|
380
|
+
status: "reported",
|
|
381
|
+
headOid: current.headOid,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const label = String(plan.policy.pullRequests.label || "");
|
|
385
|
+
if (!label)
|
|
386
|
+
return rejectedOutcome(action, [
|
|
387
|
+
HOUSEKEEPER_REASON_CODES.PR_LABEL_ELIGIBLE,
|
|
388
|
+
]);
|
|
389
|
+
current = await read();
|
|
390
|
+
reasons = pullRequestRejection(action, current, current.stale);
|
|
391
|
+
if (reasons.length > 0) return rejectedOutcome(action, reasons);
|
|
392
|
+
if (current.labels.includes(label)) {
|
|
393
|
+
return {
|
|
394
|
+
action: actionIdentity(action),
|
|
395
|
+
status: "already-labeled",
|
|
396
|
+
label,
|
|
397
|
+
headOid: current.headOid,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
await client.addLabels(plan.repository, action.number, [label]);
|
|
401
|
+
return {
|
|
402
|
+
action: actionIdentity(action),
|
|
403
|
+
status: "labeled",
|
|
404
|
+
label,
|
|
405
|
+
headOid: current.headOid,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function applyGitHubHousekeeperPlan({
|
|
410
|
+
client,
|
|
411
|
+
plan,
|
|
412
|
+
dryRun = true,
|
|
413
|
+
priorReceipt,
|
|
414
|
+
appliedAt = new Date().toISOString(),
|
|
415
|
+
staleDays = DEFAULT_STALE_DAYS,
|
|
416
|
+
maxActions = DEFAULT_MAX_ACTIONS,
|
|
417
|
+
}) {
|
|
418
|
+
const applied = normalizeDate(appliedAt, "appliedAt");
|
|
419
|
+
const actionLimit = normalizePositiveInteger(
|
|
420
|
+
maxActions,
|
|
421
|
+
DEFAULT_MAX_ACTIONS,
|
|
422
|
+
"maxActions",
|
|
423
|
+
);
|
|
424
|
+
const staleWindow = normalizePositiveInteger(
|
|
425
|
+
staleDays,
|
|
426
|
+
DEFAULT_STALE_DAYS,
|
|
427
|
+
"staleDays",
|
|
428
|
+
);
|
|
429
|
+
const replay = classifyHousekeeperReplay(plan, priorReceipt);
|
|
430
|
+
if (replay.alreadyApplied) {
|
|
431
|
+
return createEngineeringHousekeeperReceipt({
|
|
432
|
+
plan,
|
|
433
|
+
appliedAt: applied.toISOString(),
|
|
434
|
+
outcomes: [
|
|
435
|
+
rootOutcome({
|
|
436
|
+
action: "replay",
|
|
437
|
+
status: "no-op",
|
|
438
|
+
reasonCodes: replay.reasonCodes,
|
|
439
|
+
priorReceiptRoot: priorReceipt.receiptRoot,
|
|
440
|
+
}),
|
|
441
|
+
],
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
const outcomes = [];
|
|
445
|
+
for (const [index, action] of plan.actions.entries()) {
|
|
446
|
+
if (index >= actionLimit) {
|
|
447
|
+
outcomes.push({
|
|
448
|
+
action: actionIdentity(action),
|
|
449
|
+
status: "limit-skipped",
|
|
450
|
+
});
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (dryRun) {
|
|
454
|
+
outcomes.push({ action: actionIdentity(action), status: "dry-run" });
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
outcomes.push(
|
|
459
|
+
action.kind === "delete-branch"
|
|
460
|
+
? await applyBranchAction(client, plan, action)
|
|
461
|
+
: await applyPullRequestAction(
|
|
462
|
+
client,
|
|
463
|
+
plan,
|
|
464
|
+
action,
|
|
465
|
+
applied,
|
|
466
|
+
staleWindow,
|
|
467
|
+
),
|
|
468
|
+
);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
outcomes.push(
|
|
471
|
+
providerFailureOutcome(
|
|
472
|
+
action,
|
|
473
|
+
providerError(error, actionIdentity(action)),
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return createEngineeringHousekeeperReceipt({
|
|
479
|
+
plan,
|
|
480
|
+
outcomes: outcomes.map(rootOutcome),
|
|
481
|
+
appliedAt: applied.toISOString(),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export async function runGitHubHousekeeper(options) {
|
|
486
|
+
const plan = await collectGitHubHousekeeperInventory(options);
|
|
487
|
+
const receipt = await applyGitHubHousekeeperPlan({ ...options, plan });
|
|
488
|
+
return { plan, receipt };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function formatGitHubHousekeeperPlan(plan) {
|
|
492
|
+
const lines = [
|
|
493
|
+
`Engineering Housekeeper plan ${plan.planRoot}`,
|
|
494
|
+
`Repository: ${plan.repository}`,
|
|
495
|
+
`Target: ${plan.target.name}@${plan.target.headOid}`,
|
|
496
|
+
`Observed: ${plan.observedAt}`,
|
|
497
|
+
`Actions: ${plan.actions.length}`,
|
|
498
|
+
];
|
|
499
|
+
for (const action of plan.actions) lines.push(`- ${actionIdentity(action)}`);
|
|
500
|
+
return `${lines.join("\n")}\n`;
|
|
501
|
+
}
|