ai-foreman 1.7.4 → 1.7.6
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 +6 -1
- package/dist/activity.d.ts +63 -0
- package/dist/activity.js +244 -0
- package/dist/adapters/claude.d.ts +7 -0
- package/dist/adapters/claude.js +41 -1
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +65 -5
- package/dist/adapters/recovering.js +7 -1
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +5 -0
- package/dist/agentRun.js +5 -1
- package/dist/branch/github.js +5 -3
- package/dist/branch/gitlab.js +5 -3
- package/dist/branch/planner.d.ts +5 -0
- package/dist/branch/planner.js +42 -3
- package/dist/branch/presentation.d.ts +17 -0
- package/dist/branch/presentation.js +38 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +2 -0
- package/dist/branch/runner.js +44 -25
- package/dist/buildRuns.d.ts +41 -17
- package/dist/buildRuns.js +243 -22
- package/dist/cli/events.js +24 -5
- package/dist/cli/runtimeAuthPrompt.js +5 -1
- package/dist/cli/start.js +138 -45
- package/dist/cli/tickets.d.ts +10 -1
- package/dist/cli/tickets.js +418 -79
- package/dist/foreman.d.ts +8 -5
- package/dist/foreman.js +38 -14
- package/dist/index.js +3 -1
- package/dist/providerQuestions.d.ts +23 -0
- package/dist/providerQuestions.js +24 -0
- package/dist/qaReview.d.ts +1 -0
- package/dist/qaReview.js +23 -15
- package/dist/qaSnapshot.d.ts +14 -0
- package/dist/qaSnapshot.js +117 -1
- package/dist/runtimeReadiness.js +5 -0
- package/dist/sources/sourceRegistry.js +13 -6
- package/dist/tickets/details.d.ts +17 -0
- package/dist/tickets/details.js +94 -0
- package/dist/tickets/eligibility.d.ts +28 -0
- package/dist/tickets/eligibility.js +71 -0
- package/dist/tickets/importer.js +13 -8
- package/dist/tickets/reset.d.ts +27 -0
- package/dist/tickets/reset.js +213 -0
- package/dist/tickets/reviewStandards.d.ts +7 -0
- package/dist/tickets/reviewStandards.js +53 -0
- package/dist/tickets/setupConfig.d.ts +22 -0
- package/dist/tickets/setupConfig.js +105 -1
- package/dist/tickets/sourceFetch.js +19 -0
- package/dist/tickets/stateDb.d.ts +8 -1
- package/dist/tickets/stateDb.js +54 -23
- package/dist/workflowDb.d.ts +3 -0
- package/dist/workflowDb.js +26 -0
- package/package.json +15 -3
package/dist/branch/github.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { branchNodeFooter } from "./presentation.js";
|
|
5
|
+
import { configuredReviewBody, configuredReviewTitle } from "../tickets/reviewStandards.js";
|
|
4
6
|
const DEFAULT_COMMAND_TIMEOUT_MS = 15_000;
|
|
5
7
|
const OUTPUT_LIMIT = 2_000;
|
|
6
8
|
function runCommand(cwd, bin, args, timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS) {
|
|
@@ -133,7 +135,7 @@ export function createOrReusePr(cwd, opts) {
|
|
|
133
135
|
const bodyPath = join(cwd, ".foreman", "pr-bodies", opts.runId, `${opts.node.ticket.id}.md`);
|
|
134
136
|
try {
|
|
135
137
|
mkdirSync(join(cwd, ".foreman", "pr-bodies", opts.runId), { recursive: true });
|
|
136
|
-
writeFileSync(bodyPath,
|
|
138
|
+
writeFileSync(bodyPath, configuredReviewBody(cwd, opts.node, opts.qaEvidence, opts.commit), "utf8");
|
|
137
139
|
}
|
|
138
140
|
catch (err) {
|
|
139
141
|
return prFailure(prBodyWriteFailure(opts.runId, bodyPath, err));
|
|
@@ -146,7 +148,7 @@ export function createOrReusePr(cwd, opts) {
|
|
|
146
148
|
"--head",
|
|
147
149
|
opts.node.branch,
|
|
148
150
|
"--title",
|
|
149
|
-
|
|
151
|
+
configuredReviewTitle(cwd, opts.node.ticket),
|
|
150
152
|
"--body-file",
|
|
151
153
|
bodyPath,
|
|
152
154
|
];
|
|
@@ -440,7 +442,7 @@ function buildPrBody(opts) {
|
|
|
440
442
|
`- Dependencies: ${opts.node.dependencies.length ? opts.node.dependencies.join(", ") : "None"}`,
|
|
441
443
|
`- Commit: ${opts.commit ?? "N/A"}`,
|
|
442
444
|
"",
|
|
443
|
-
|
|
445
|
+
branchNodeFooter(opts.node),
|
|
444
446
|
"",
|
|
445
447
|
].join("\n");
|
|
446
448
|
}
|
package/dist/branch/gitlab.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { branchNodeFooter } from "./presentation.js";
|
|
5
|
+
import { configuredReviewBody, configuredReviewTitle } from "../tickets/reviewStandards.js";
|
|
4
6
|
const DEFAULT_COMMAND_TIMEOUT_MS = 15_000;
|
|
5
7
|
const OUTPUT_LIMIT = 2_000;
|
|
6
8
|
export function preflightGlab(cwd) {
|
|
@@ -54,7 +56,7 @@ export function createOrReuseMr(cwd, opts) {
|
|
|
54
56
|
const bodyPath = join(cwd, ".foreman", "mr-bodies", opts.runId, `${opts.node.ticket.id}.md`);
|
|
55
57
|
try {
|
|
56
58
|
mkdirSync(join(cwd, ".foreman", "mr-bodies", opts.runId), { recursive: true });
|
|
57
|
-
writeFileSync(bodyPath,
|
|
59
|
+
writeFileSync(bodyPath, configuredReviewBody(cwd, opts.node, opts.qaEvidence, opts.commit), "utf8");
|
|
58
60
|
}
|
|
59
61
|
catch (err) {
|
|
60
62
|
return prFailure(failure("mr_create_failed", `Failed to write GitLab MR body file at ${bodyPath}.`, [
|
|
@@ -69,7 +71,7 @@ export function createOrReuseMr(cwd, opts) {
|
|
|
69
71
|
"--target-branch",
|
|
70
72
|
opts.node.baseBranch,
|
|
71
73
|
"--title",
|
|
72
|
-
|
|
74
|
+
configuredReviewTitle(cwd, opts.node.ticket),
|
|
73
75
|
"--description",
|
|
74
76
|
readBodyArg(bodyPath),
|
|
75
77
|
"--yes",
|
|
@@ -272,7 +274,7 @@ function buildMrBody(opts) {
|
|
|
272
274
|
`- Dependencies: ${opts.node.dependencies.length ? opts.node.dependencies.join(", ") : "None"}`,
|
|
273
275
|
`- Commit: ${opts.commit ?? "N/A"}`,
|
|
274
276
|
"",
|
|
275
|
-
|
|
277
|
+
branchNodeFooter(opts.node),
|
|
276
278
|
"",
|
|
277
279
|
].join("\n");
|
|
278
280
|
}
|
package/dist/branch/planner.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TicketDef } from "../tickets/ticketSchema.js";
|
|
2
2
|
import type { TicketState } from "../tickets/stateDb.js";
|
|
3
3
|
import type { BranchPlan } from "./types.js";
|
|
4
|
+
import type { DeliveryConfig } from "../tickets/delivery.js";
|
|
5
|
+
import type { TicketBuildSetupConfig } from "../tickets/setupConfig.js";
|
|
4
6
|
export interface AuditDependency {
|
|
5
7
|
ticket: string;
|
|
6
8
|
dependsOn: string;
|
|
@@ -18,5 +20,8 @@ export interface BuildBranchPlanOptions {
|
|
|
18
20
|
export declare function buildBranchPlan(tickets: TicketDef[], states: Map<string, TicketState>, opts: BuildBranchPlanOptions): BranchPlan;
|
|
19
21
|
export declare function selectTicketsForBranchRun(tickets: TicketDef[], states: Map<string, TicketState>, steps: number, ticketIds?: string[]): TicketDef[];
|
|
20
22
|
export declare function applySharedDeliveryBranch(plan: BranchPlan, unitId: string, allRemainingTicketIds: string[], branchPrefix?: string): BranchPlan;
|
|
23
|
+
/** Apply branch sharing only; this deliberately never creates delivery stacks or dependency edges. */
|
|
24
|
+
export declare function applySizeBranchPolicy(plan: BranchPlan, policy: TicketBuildSetupConfig["branch_policy"], delivery?: DeliveryConfig, branchPrefix?: string): BranchPlan;
|
|
21
25
|
export declare function parseAuditDependencies(text: string): AuditDependency[];
|
|
22
26
|
export declare function buildBranchAuditInstruction(tickets: TicketDef[]): string;
|
|
27
|
+
export declare function normalizeBranchPrefix(prefix: string | undefined): string;
|
package/dist/branch/planner.js
CHANGED
|
@@ -99,11 +99,11 @@ export function selectTicketsForBranchRun(tickets, states, steps, ticketIds) {
|
|
|
99
99
|
}
|
|
100
100
|
return selected;
|
|
101
101
|
}
|
|
102
|
-
export function applySharedDeliveryBranch(plan, unitId, allRemainingTicketIds, branchPrefix = "
|
|
102
|
+
export function applySharedDeliveryBranch(plan, unitId, allRemainingTicketIds, branchPrefix = "feature") {
|
|
103
103
|
if (plan.nodes.length === 0)
|
|
104
104
|
return plan;
|
|
105
105
|
const slug = unitId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group";
|
|
106
|
-
const branch = `${branchPrefix
|
|
106
|
+
const branch = `${normalizeBranchPrefix(branchPrefix)}/${slug}`;
|
|
107
107
|
const finalSelected = plan.nodes.at(-1)?.ticket.id;
|
|
108
108
|
const completesUnit = plan.nodes.length === allRemainingTicketIds.length
|
|
109
109
|
&& allRemainingTicketIds.every((id) => plan.nodes.some((node) => node.ticket.id === id));
|
|
@@ -118,6 +118,38 @@ export function applySharedDeliveryBranch(plan, unitId, allRemainingTicketIds, b
|
|
|
118
118
|
})),
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
|
+
/** Apply branch sharing only; this deliberately never creates delivery stacks or dependency edges. */
|
|
122
|
+
export function applySizeBranchPolicy(plan, policy, delivery, branchPrefix = "feature") {
|
|
123
|
+
if (policy.mode !== "size" || plan.nodes.length < 2)
|
|
124
|
+
return plan;
|
|
125
|
+
const explicitlyAssigned = new Set(delivery?.units.flatMap((unit) => unit.tickets) ?? []);
|
|
126
|
+
const candidates = plan.nodes.filter((node) => policy.by_size[node.ticket.size] === "shared" && !explicitlyAssigned.has(node.ticket.id));
|
|
127
|
+
const groups = new Map();
|
|
128
|
+
for (const node of candidates) {
|
|
129
|
+
// Tickets with selected dependency edges need their existing topology. Independent
|
|
130
|
+
// tickets sharing the same base are safe to execute serially in one worktree/review.
|
|
131
|
+
if (node.dependencies.length)
|
|
132
|
+
continue;
|
|
133
|
+
const key = node.baseBranch;
|
|
134
|
+
groups.set(key, [...(groups.get(key) ?? []), node]);
|
|
135
|
+
}
|
|
136
|
+
const replacement = new Map();
|
|
137
|
+
for (const [base, nodes] of groups) {
|
|
138
|
+
if (nodes.length < 2)
|
|
139
|
+
continue;
|
|
140
|
+
const ids = nodes.map((node) => node.ticket.id.toLowerCase()).join("-");
|
|
141
|
+
const groupId = `size-small-${ids}`;
|
|
142
|
+
const branch = `${normalizeBranchPrefix(branchPrefix)}/${groupId}`.slice(0, 120).replace(/-+$/, "");
|
|
143
|
+
nodes.forEach((node, index) => replacement.set(node.ticket.id, {
|
|
144
|
+
...node,
|
|
145
|
+
branch,
|
|
146
|
+
baseBranch: base,
|
|
147
|
+
deliveryUnitId: groupId,
|
|
148
|
+
deliveryUnitFinal: index === nodes.length - 1,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
return { ...plan, nodes: plan.nodes.map((node) => replacement.get(node.ticket.id) ?? node) };
|
|
152
|
+
}
|
|
121
153
|
export function parseAuditDependencies(text) {
|
|
122
154
|
const deps = [];
|
|
123
155
|
for (const line of text.split(/\r?\n/)) {
|
|
@@ -145,7 +177,7 @@ Only name dependencies where both tickets are in this selected batch. If there a
|
|
|
145
177
|
${body}`;
|
|
146
178
|
}
|
|
147
179
|
function uniqueBranchName(prefix, ticket, used) {
|
|
148
|
-
const cleanPrefix = prefix
|
|
180
|
+
const cleanPrefix = normalizeBranchPrefix(prefix);
|
|
149
181
|
const baseSlug = `${ticket.id}-${ticket.title}`
|
|
150
182
|
.toLowerCase()
|
|
151
183
|
.replace(/[^a-z0-9]+/g, "-")
|
|
@@ -160,6 +192,13 @@ function uniqueBranchName(prefix, ticket, used) {
|
|
|
160
192
|
used.add(branch);
|
|
161
193
|
return branch;
|
|
162
194
|
}
|
|
195
|
+
export function normalizeBranchPrefix(prefix) {
|
|
196
|
+
const value = prefix === undefined || prefix === "" ? "feature" : prefix;
|
|
197
|
+
if (value.startsWith("/") || value.endsWith("/") || value.includes("..") || value.includes("//") || /[~^:?*\[\\\s]/.test(value)) {
|
|
198
|
+
throw new Error(`invalid Git branch prefix: ${value}`);
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
163
202
|
function detectSelectedCycles(tickets, deps) {
|
|
164
203
|
const selected = new Set(tickets.map((ticket) => ticket.id));
|
|
165
204
|
const visited = new Set();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BranchPlan, BranchPlanNode } from "./types.js";
|
|
2
|
+
export type BranchAllocationMode = "per-ticket" | "shared" | "mixed";
|
|
3
|
+
export interface BranchPresentation {
|
|
4
|
+
allocationMode: BranchAllocationMode;
|
|
5
|
+
description: "one branch per ticket" | "one shared branch" | "mixed shared and per-ticket branches" | "stacked delivery branches";
|
|
6
|
+
banner: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface BranchPresentationOptions {
|
|
10
|
+
stacked?: boolean;
|
|
11
|
+
resumed?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function branchPlanLogMetadata(plan: BranchPlan, presentation: BranchPresentation): Record<string, unknown>;
|
|
14
|
+
/** Describe an isolated branch run from its final, transformed plan. */
|
|
15
|
+
export declare function presentBranchPlan(plan: BranchPlan, options?: BranchPresentationOptions): BranchPresentation;
|
|
16
|
+
export declare function branchNodeBehavior(node: BranchPlanNode): "shared delivery branch" | "per-ticket branch";
|
|
17
|
+
export declare function branchNodeFooter(node: BranchPlanNode): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function branchPlanLogMetadata(plan, presentation) {
|
|
2
|
+
return {
|
|
3
|
+
baseRef: plan.baseRef,
|
|
4
|
+
tickets: plan.nodes.map((node) => node.ticket.id),
|
|
5
|
+
branches: plan.nodes.map((node) => ({ ticket: node.ticket.id, branch: node.branch, base: node.baseBranch })),
|
|
6
|
+
issues: plan.issues,
|
|
7
|
+
branchMode: presentation.allocationMode,
|
|
8
|
+
executionDescription: presentation.description,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Describe an isolated branch run from its final, transformed plan. */
|
|
12
|
+
export function presentBranchPlan(plan, options = {}) {
|
|
13
|
+
const sharedNodes = plan.nodes.filter((node) => Boolean(node.deliveryUnitId)).length;
|
|
14
|
+
const allocationMode = sharedNodes === 0
|
|
15
|
+
? "per-ticket"
|
|
16
|
+
: sharedNodes === plan.nodes.length
|
|
17
|
+
? "shared"
|
|
18
|
+
: "mixed";
|
|
19
|
+
const description = options.stacked
|
|
20
|
+
? "stacked delivery branches"
|
|
21
|
+
: allocationMode === "shared"
|
|
22
|
+
? "one shared branch"
|
|
23
|
+
: allocationMode === "mixed"
|
|
24
|
+
? "mixed shared and per-ticket branches"
|
|
25
|
+
: "one branch per ticket";
|
|
26
|
+
return {
|
|
27
|
+
allocationMode,
|
|
28
|
+
description,
|
|
29
|
+
banner: `foreman: ${options.resumed ? "resuming " : ""}${description} for ${plan.nodes.length} ticket(s)`,
|
|
30
|
+
prompt: `Proceed with ${description}?`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function branchNodeBehavior(node) {
|
|
34
|
+
return node.deliveryUnitId ? "shared delivery branch" : "per-ticket branch";
|
|
35
|
+
}
|
|
36
|
+
export function branchNodeFooter(node) {
|
|
37
|
+
return `Generated by Rafi ai-foreman for a ${branchNodeBehavior(node)}.`;
|
|
38
|
+
}
|
package/dist/branch/resume.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface BranchResumeSession {
|
|
|
16
16
|
reviewProvider?: string;
|
|
17
17
|
prReady?: boolean;
|
|
18
18
|
keepWorktrees?: boolean;
|
|
19
|
+
deliveryUnitId?: string;
|
|
20
|
+
deliveryUnitFinal?: boolean;
|
|
19
21
|
ts?: string;
|
|
20
22
|
}
|
|
21
23
|
export declare function findResumableBranchSessions(foremanDir: string): BranchResumeSession[];
|
package/dist/branch/resume.js
CHANGED
|
@@ -43,6 +43,8 @@ export function findResumableBranchSessions(foremanDir) {
|
|
|
43
43
|
reviewProvider: stringField(record.reviewProvider),
|
|
44
44
|
prReady: booleanField(record.prReady),
|
|
45
45
|
keepWorktrees: booleanField(record.keepWorktrees),
|
|
46
|
+
deliveryUnitId: stringField(record.deliveryUnitId),
|
|
47
|
+
deliveryUnitFinal: booleanField(record.deliveryUnitFinal),
|
|
46
48
|
ts: stringField(record.ts),
|
|
47
49
|
});
|
|
48
50
|
}
|
package/dist/branch/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildDurableQaFixHandoff, compactWithRetry, runIsolatedQa } from "../qaReview.js";
|
|
2
|
-
import {
|
|
2
|
+
import { changeManifestAsync } from "../qaSnapshot.js";
|
|
3
3
|
import { Foreman, MARKER_SPEC, parseStepStatus } from "../foreman.js";
|
|
4
4
|
import { fireNotification } from "../notify.js";
|
|
5
5
|
import { cmdBlock, cmdComplete, cmdUpdate } from "../tickets/commands.js";
|
|
@@ -9,6 +9,7 @@ import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranch
|
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { WorkflowDb } from "../workflowDb.js";
|
|
12
|
+
import { currentActivity, withActivityPhase } from "../activity.js";
|
|
12
13
|
export function readDeliveryUnitSession(projectDir, unitId) {
|
|
13
14
|
const path = deliverySessionPath(projectDir, unitId);
|
|
14
15
|
if (!existsSync(path))
|
|
@@ -176,6 +177,8 @@ export async function runBranchPlan(opts) {
|
|
|
176
177
|
reviewProvider,
|
|
177
178
|
prReady: opts.prReady,
|
|
178
179
|
keepWorktrees: opts.keepWorktrees,
|
|
180
|
+
deliveryUnitId: node.deliveryUnitId,
|
|
181
|
+
deliveryUnitFinal: node.deliveryUnitFinal,
|
|
179
182
|
});
|
|
180
183
|
if (node.deliveryUnitId) {
|
|
181
184
|
const path = deliverySessionPath(opts.projectDir, node.deliveryUnitId);
|
|
@@ -228,7 +231,7 @@ export async function runBranchPlan(opts) {
|
|
|
228
231
|
await builder.close();
|
|
229
232
|
builder = await opts.createBuilder(worktreePath);
|
|
230
233
|
}
|
|
231
|
-
const digest =
|
|
234
|
+
const digest = (await withActivityPhase("recording QA fix changes", () => changeManifestAsync(worktreePath, (state, detail) => currentActivity()?.update(state, detail), "recording QA fix changes"))).diffDigest;
|
|
232
235
|
const fixed = await builder.sendTurn(buildDurableQaFixHandoff(node.ticket, issues, worktreePath, result.text, digest));
|
|
233
236
|
const fixedStatus = parseStepStatus(fixed.text);
|
|
234
237
|
builderStreamSession = opts.builderSessionStrategy === "compact" ? builder.sessionId() : undefined;
|
|
@@ -287,9 +290,12 @@ export async function runBranchPlan(opts) {
|
|
|
287
290
|
workflowCheckpoint(opts.projectDir, opts.runId, "push-before", node.ticket.id, { branch: node.branch, sha: commit });
|
|
288
291
|
const pushOperation = `${opts.runId}:push:${node.branch}`;
|
|
289
292
|
planJournal(opts.projectDir, opts.runId, pushOperation, "push", { branch: node.branch, sha: commit, remote: "origin" });
|
|
290
|
-
const push =
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
const push = await withActivityPhase(`pushing ${node.branch}`, async () => {
|
|
294
|
+
currentActivity()?.update(`pushing ${node.branch}`, `publishing ${node.ticket.id}`);
|
|
295
|
+
return reviewProvider === "gitlab"
|
|
296
|
+
? pushBranchForMr(worktreePath, node.branch)
|
|
297
|
+
: pushBranchForPr(worktreePath, node.branch);
|
|
298
|
+
});
|
|
293
299
|
if (push.ok) {
|
|
294
300
|
confirmJournal(opts.projectDir, pushOperation, node.branch, { sha: commit });
|
|
295
301
|
opts.log.write("branch-push", { ticket: node.ticket.id, branch: node.branch, status: "pushed" });
|
|
@@ -326,24 +332,27 @@ export async function runBranchPlan(opts) {
|
|
|
326
332
|
workflowCheckpoint(opts.projectDir, opts.runId, "review-creation-before", node.ticket.id, { provider: reviewProvider, head: node.branch, base: node.baseBranch });
|
|
327
333
|
const reviewOperation = `${opts.runId}:${reviewProvider === "gitlab" ? "mr" : "pr"}:${node.branch}:${node.baseBranch}`;
|
|
328
334
|
planJournal(opts.projectDir, opts.runId, reviewOperation, reviewProvider === "gitlab" ? "mr-create" : "pr-create", { head: node.branch, base: node.baseBranch, sha: commit });
|
|
329
|
-
const pr = reviewProvider === "gitlab"
|
|
330
|
-
?
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
335
|
+
const pr = await withActivityPhase(`creating ${reviewProvider === "gitlab" ? "merge request" : "pull request"}`, async () => {
|
|
336
|
+
currentActivity()?.update(`creating ${reviewProvider === "gitlab" ? "merge request" : "pull request"}`, node.ticket.id);
|
|
337
|
+
return reviewProvider === "gitlab"
|
|
338
|
+
? createOrReuseMr(opts.projectDir, {
|
|
339
|
+
node,
|
|
340
|
+
ready: opts.prReady || completionMode === "auto-merge",
|
|
341
|
+
runId: opts.runId,
|
|
342
|
+
qaEvidence: qaSummary,
|
|
343
|
+
commit,
|
|
344
|
+
autoMerge: false,
|
|
345
|
+
cleanup: opts.cleanupBranches ?? true,
|
|
346
|
+
mergeMethod: opts.mergeMethod ?? "squash",
|
|
347
|
+
})
|
|
348
|
+
: createOrReusePr(opts.projectDir, {
|
|
349
|
+
node,
|
|
350
|
+
ready: opts.prReady || completionMode === "auto-merge",
|
|
351
|
+
runId: opts.runId,
|
|
352
|
+
qaEvidence: qaSummary,
|
|
353
|
+
commit,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
347
356
|
summary.pr = pr;
|
|
348
357
|
if (pr.status === "created" || pr.status === "existing") {
|
|
349
358
|
confirmJournal(opts.projectDir, reviewOperation, pr.url, { head: node.branch, base: node.baseBranch, url: pr.url });
|
|
@@ -362,7 +371,10 @@ export async function runBranchPlan(opts) {
|
|
|
362
371
|
continue;
|
|
363
372
|
}
|
|
364
373
|
if (completionMode === "auto-merge" && reviewProvider === "github") {
|
|
365
|
-
const autoMerge =
|
|
374
|
+
const autoMerge = await withActivityPhase("enabling GitHub auto-merge", async () => {
|
|
375
|
+
currentActivity()?.update("enabling GitHub auto-merge", node.branch);
|
|
376
|
+
return enableGitHubAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
377
|
+
});
|
|
366
378
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
367
379
|
if (autoMerge.status === "failed") {
|
|
368
380
|
opts.log.write("pr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -373,7 +385,10 @@ export async function runBranchPlan(opts) {
|
|
|
373
385
|
opts.log.write("pr-auto-merge-enabled", { ticket: node.ticket.id, branch: node.branch, url: summary.pr.url });
|
|
374
386
|
}
|
|
375
387
|
else if (completionMode === "auto-merge" && reviewProvider === "gitlab") {
|
|
376
|
-
const autoMerge =
|
|
388
|
+
const autoMerge = await withActivityPhase("enabling GitLab auto-merge", async () => {
|
|
389
|
+
currentActivity()?.update("enabling GitLab auto-merge", node.branch);
|
|
390
|
+
return enableGitLabAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
391
|
+
});
|
|
377
392
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
378
393
|
if (autoMerge.status === "failed") {
|
|
379
394
|
opts.log.write("mr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -501,6 +516,9 @@ function workflowCheckpoint(projectDir, runId, checkpoint, ticket, payload) {
|
|
|
501
516
|
}
|
|
502
517
|
}
|
|
503
518
|
async function waitForAutoMergeDependencies(opts, node, provider) {
|
|
519
|
+
return withActivityPhase("checking dependency merges", () => waitForAutoMergeDependenciesInternal(opts, node, provider));
|
|
520
|
+
}
|
|
521
|
+
async function waitForAutoMergeDependenciesInternal(opts, node, provider) {
|
|
504
522
|
const dependencyNodes = node.dependencies
|
|
505
523
|
.map((dep) => opts.plan.nodes.find((candidate) => candidate.ticket.id === dep))
|
|
506
524
|
.filter((dep) => Boolean(dep));
|
|
@@ -532,6 +550,7 @@ async function waitForAutoMergeDependencies(opts, node, provider) {
|
|
|
532
550
|
if (pending.length === 0)
|
|
533
551
|
return { ok: true };
|
|
534
552
|
const message = `auto-merge dependency ${pending.join(", ")} has not merged into the root base yet`;
|
|
553
|
+
currentActivity()?.update("waiting for dependency merge", pending.join(", "));
|
|
535
554
|
if (!opts.autoMergeWait) {
|
|
536
555
|
return {
|
|
537
556
|
ok: false,
|
package/dist/buildRuns.d.ts
CHANGED
|
@@ -1,42 +1,66 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare const BUILD_RUN_VERSION =
|
|
1
|
+
import type { BuildRunRecordV2, ResolvedAgentSettings } from "rafi-spec";
|
|
2
|
+
export declare const BUILD_RUN_VERSION = 2;
|
|
3
3
|
export declare const BUILD_RUN_DIRECTORY = ".foreman/runs";
|
|
4
4
|
export declare const BUILD_HEARTBEAT_MS = 10000;
|
|
5
5
|
export declare const BUILD_LEASE_STALE_MS = 45000;
|
|
6
6
|
export interface CreateBuildRunInput {
|
|
7
7
|
tickets: string[];
|
|
8
8
|
deliveryUnit?: string;
|
|
9
|
-
branchMode?:
|
|
9
|
+
branchMode?: BuildRunRecordV2["branchMode"];
|
|
10
10
|
repositoryRoot: string;
|
|
11
11
|
worktree?: string;
|
|
12
12
|
branch?: string;
|
|
13
13
|
baseHead?: string;
|
|
14
|
+
baseRef?: string;
|
|
14
15
|
startHead?: string;
|
|
15
16
|
builder?: LegacyResolvedAgentSettings;
|
|
16
17
|
qa?: LegacyResolvedAgentSettings;
|
|
17
18
|
now?: Date;
|
|
18
19
|
}
|
|
19
|
-
export declare function createBuildRun(input: CreateBuildRunInput):
|
|
20
|
+
export declare function createBuildRun(input: CreateBuildRunInput): BuildRunRecordV2;
|
|
20
21
|
export declare function resumeBuildRun(projectDir: string, runId: string, patch: {
|
|
21
22
|
builder?: LegacyResolvedAgentSettings;
|
|
22
23
|
qa?: LegacyResolvedAgentSettings;
|
|
23
24
|
builderSessionId?: string | null;
|
|
24
|
-
}, now?: Date):
|
|
25
|
+
}, now?: Date): BuildRunRecordV2;
|
|
25
26
|
type LegacyResolvedAgentSettings = Omit<ResolvedAgentSettings, "session_strategy" | "settings_revision"> & Partial<Pick<ResolvedAgentSettings, "session_strategy" | "settings_revision">>;
|
|
26
|
-
export declare function saveBuildRun(projectDir: string, run:
|
|
27
|
-
export declare function checkpointBuildRun(projectDir: string, run:
|
|
28
|
-
export declare function persistBuildSession(projectDir: string, run:
|
|
29
|
-
export declare function recordBuildReceipt(projectDir: string, run:
|
|
27
|
+
export declare function saveBuildRun(projectDir: string, run: BuildRunRecordV2, now?: Date): BuildRunRecordV2;
|
|
28
|
+
export declare function checkpointBuildRun(projectDir: string, run: BuildRunRecordV2, checkpoint: string, patch?: Partial<BuildRunRecordV2>): BuildRunRecordV2;
|
|
29
|
+
export declare function persistBuildSession(projectDir: string, run: BuildRunRecordV2, role: "builder" | "qa", sessionId: string): BuildRunRecordV2;
|
|
30
|
+
export declare function recordBuildReceipt(projectDir: string, run: BuildRunRecordV2, operationId: string, detail?: {
|
|
30
31
|
externalId?: string;
|
|
31
32
|
detail?: string;
|
|
32
|
-
}):
|
|
33
|
-
export declare function heartbeatBuildRun(projectDir: string, run:
|
|
34
|
-
export declare function releaseBuildLease(projectDir: string, run:
|
|
35
|
-
export declare function completeBuildRun(projectDir: string, run:
|
|
36
|
-
export declare function readBuildRuns(projectDir: string):
|
|
37
|
-
export declare function recoverableBuildRuns(projectDir: string, now?: Date): Array<
|
|
33
|
+
}): BuildRunRecordV2;
|
|
34
|
+
export declare function heartbeatBuildRun(projectDir: string, run: BuildRunRecordV2, now?: Date): BuildRunRecordV2;
|
|
35
|
+
export declare function releaseBuildLease(projectDir: string, run: BuildRunRecordV2, status?: BuildRunRecordV2["status"]): BuildRunRecordV2;
|
|
36
|
+
export declare function completeBuildRun(projectDir: string, run: BuildRunRecordV2, now?: Date): BuildRunRecordV2;
|
|
37
|
+
export declare function readBuildRuns(projectDir: string): BuildRunRecordV2[];
|
|
38
|
+
export declare function recoverableBuildRuns(projectDir: string, now?: Date): Array<BuildRunRecordV2 & {
|
|
38
39
|
active: boolean;
|
|
39
40
|
}>;
|
|
40
|
-
export declare function isLeaseActive(run:
|
|
41
|
-
export declare function buildRecoveryPreview(run:
|
|
41
|
+
export declare function isLeaseActive(run: BuildRunRecordV2, now?: Date): boolean;
|
|
42
|
+
export declare function buildRecoveryPreview(run: BuildRunRecordV2): string[];
|
|
43
|
+
export interface BuildRecoveryProjection {
|
|
44
|
+
run: BuildRunRecordV2;
|
|
45
|
+
ticketId?: string;
|
|
46
|
+
ticketTitle?: string;
|
|
47
|
+
compactLabel: string;
|
|
48
|
+
compactHint: string;
|
|
49
|
+
completed: string[];
|
|
50
|
+
remaining: string[];
|
|
51
|
+
lastSuccess: string;
|
|
52
|
+
interruption: string;
|
|
53
|
+
validation: string;
|
|
54
|
+
expectedChanges: string[];
|
|
55
|
+
unexpectedChanges: Array<{
|
|
56
|
+
path: string;
|
|
57
|
+
risk: string;
|
|
58
|
+
}>;
|
|
59
|
+
nextAction: string;
|
|
60
|
+
worktree: string;
|
|
61
|
+
branch?: string;
|
|
62
|
+
exactSessionId?: string;
|
|
63
|
+
}
|
|
64
|
+
export declare function projectBuildRecovery(projectDir: string, run: BuildRunRecordV2, now?: Date, ticketOverride?: string): BuildRecoveryProjection;
|
|
65
|
+
export declare function formatBuildRecoveryProjection(projection: BuildRecoveryProjection): string[];
|
|
42
66
|
export {};
|