FN-9047: invalidate stale workspace landing proof after revert
Preserve workspace landing attribution while allowing cleanly reverted work to land again. - Record a per-repository revert boundary after clean git-mode workspace reverts. - Reject recorded and trailer landing proof at or behind that boundary, then clear it on a fresh landing. - Persist boundaries through the revert route and cover re-land behavior with git tests. Files changed: .changeset/fn-9047-workspace-revert-landed-sha.md | 7 +++ docs/architecture.md | 2 + packages/core/src/types/task/task-core.ts | 7 ++- .../src/routes/register-session-diff-routes.ts | 2 +- .../src/routes/register-task-workflow-routes.ts | 11 ++++ .../task-revert.workspace.real-git.test.ts | 27 ++++++++++ ...orkspace-land-predicate.revert-boundary.test.ts | 60 ++++++++++++++++++++++ packages/engine/src/execution/task-revert.ts | 42 +++++++++++++-- packages/engine/src/index.ts | 1 + packages/engine/src/merge/merger-ai.ts | 5 +- .../engine/src/merge/workspace-land-predicate.ts | 25 +++++++-- packages/engine/src/self-healing.ts | 9 +++- 12 files changed, 185 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-9047 Fusion-Task-Lineage: 8ac41347-3c76-4f54-8d69-f16f82084305 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-9047-workspace-revert-landed-sha.md
Normal file
7
.changeset/fn-9047-workspace-revert-landed-sha.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Re-land workspace task work after a clean revert instead of silently skipping it.
|
||||
category: fix
|
||||
dev: Records per-repository `revertBoundarySha` and invalidates stale workspace landing proof.
|
||||
@@ -2417,3 +2417,5 @@ Writers use the `AsyncDataLayer` outbox seam because AgentStore and approval sto
|
||||
Paused-safe housekeeping retains at most 30 days and 50,000 rows per project. The [Agent activity API contract](agent-activity-contract.md) defines the inspectable `GET /api/agent-activity` wire, cursor, and continuation behavior. SSE tails durable rows in ascending seq pages, advances only after sending, serializes reentrant drains, and emits a bounded truncation marker for oversized backlogs; in-process events are latency nudges only, preserving reconnect correctness across processes.
|
||||
|
||||
Scheduler and autopilot mission reconciliation persist the evaluated alignment on linked mission features, including no-delivery-transition outcomes, so roadmap readers consume a durable projection rather than recomputing task reports in the browser.
|
||||
|
||||
**Workspace revert land idempotency (FN-9047).** A clean git-mode workspace revert preserves each sub-repository `landedSha` for commit attribution and session-diff ranges, but records `workspaceWorktrees[repo].revertBoundarySha` at the integration commit created by the revert (or the pre-revert integration HEAD when the repo was already reverted). The canonical workspace landed predicate rejects recorded-SHA and `Fusion-Task-Id` trailer proof at or behind that boundary; a subsequent successful land writes its fresh `landedSha` and clears the boundary. PR-mode, conflicting, unsupported, and human-required reverts do not set boundaries because they do not advance an integration ref.
|
||||
|
||||
@@ -717,8 +717,13 @@ export interface Task {
|
||||
* present AND whose recorded value is an ancestor of (or equals) the repo's
|
||||
* integration tip, so an interrupted multi-repo land retries only the un-landed
|
||||
* repos and never re-advances an already-landed ref (idempotent retry).
|
||||
*
|
||||
* FNXC:Workspace 2026-08-15-06:45:
|
||||
* `revertBoundarySha` is the integration-branch commit after a completed git-mode revert
|
||||
* (the revert commit, or pre-revert HEAD when already reverted). A proven landing at or behind
|
||||
* it is stale and must re-land; `landedSha` remains for attribution and diff consumers.
|
||||
*/
|
||||
workspaceWorktrees?: Record<string, { worktreePath: string; branch: string; baseCommitSha?: string; landedSha?: string }>;
|
||||
workspaceWorktrees?: Record<string, { worktreePath: string; branch: string; baseCommitSha?: string; landedSha?: string; revertBoundarySha?: string }>;
|
||||
steps: TaskStep[];
|
||||
currentStep: number;
|
||||
/**
|
||||
|
||||
@@ -531,7 +531,7 @@ async function computeWorkspaceTaskFiles(
|
||||
task: {
|
||||
id: string;
|
||||
baseBranch?: string;
|
||||
workspaceWorktrees?: Record<string, { worktreePath: string; branch: string; baseCommitSha?: string; landedSha?: string }>;
|
||||
workspaceWorktrees?: Record<string, { worktreePath: string; branch: string; baseCommitSha?: string; landedSha?: string; revertBoundarySha?: string }>;
|
||||
},
|
||||
rootDir: string,
|
||||
timeoutMs: number,
|
||||
|
||||
@@ -98,6 +98,7 @@ import {
|
||||
evaluateTaskReleaseGate,
|
||||
performTaskRevert,
|
||||
revertWorkspaceTask,
|
||||
applyWorkspaceRevertBoundaries,
|
||||
TaskRevertError,
|
||||
createAiUndoTask,
|
||||
prepareRevertPrBranch,
|
||||
@@ -3037,6 +3038,16 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
});
|
||||
|
||||
if (workspaceResult.mode === "git" && "clean" in workspaceResult && workspaceResult.clean === true) {
|
||||
// FNXC:Workspace 2026-08-15-06:45:
|
||||
// The store-free revert service reports boundaries; persist them from a fresh task read so
|
||||
// trailer/landedSha proof at or behind a git-mode revert cannot skip re-done sub-repo work.
|
||||
const latest = await scopedStore.getTask(task.id);
|
||||
if (!latest) throw new TaskRevertError("task disappeared while persisting workspace revert boundaries", "task-not-found");
|
||||
const workspaceWorktrees = applyWorkspaceRevertBoundaries(
|
||||
latest.workspaceWorktrees,
|
||||
workspaceResult.workspace.repos,
|
||||
);
|
||||
await scopedStore.updateTask(task.id, { workspaceWorktrees });
|
||||
await stampReverted();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyWorkspaceRevertBoundaries,
|
||||
resolveWorkspaceTaskRevertCommits,
|
||||
revertWorkspaceTask,
|
||||
} from "../execution/task-revert.js";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { findProvenLandedCommit, isRepoLanded } from "../merge/workspace-land-predicate.js";
|
||||
|
||||
const hasGit = spawnSync("git", ["--version"], { stdio: "pipe" }).status === 0;
|
||||
const describeIfGit = hasGit ? describe : describe.skip;
|
||||
@@ -205,6 +207,9 @@ describeIfGit("task-revert workspace real-git scenarios", { timeout: 30_000 }, (
|
||||
expect(byRepo["repo-a"].revertCommitSha).toBeTruthy();
|
||||
expect(byRepo["repo-b"].classification).toBe("clean");
|
||||
expect(byRepo["repo-b"].revertCommitSha).toBeTruthy();
|
||||
const nextWorktrees = applyWorkspaceRevertBoundaries(task.workspaceWorktrees, result.workspace.repos);
|
||||
expect(nextWorktrees?.["repo-a"]?.revertBoundarySha).toBe(byRepo["repo-a"].revertCommitSha);
|
||||
expect(nextWorktrees?.["repo-b"]?.revertBoundarySha).toBe(byRepo["repo-b"].revertCommitSha);
|
||||
}
|
||||
|
||||
for (const repoRootDir of [repoA, repoB]) {
|
||||
@@ -218,6 +223,28 @@ describeIfGit("task-revert workspace real-git scenarios", { timeout: 30_000 }, (
|
||||
expect(git(repoB, "git show HEAD:b.ts")).toBe("line1");
|
||||
});
|
||||
|
||||
it("revert then rerun invalidates both recorded and trailer landing proof across sub-repos", async () => {
|
||||
const { workspaceRoot, repoA, repoB } = workspaceFixture();
|
||||
const shaA = landTaskCommit(repoA, "a.ts", "line1\nfeature-a\n", "land\n\nFusion-Task-Id: FN-A");
|
||||
const shaB = landTaskCommit(repoB, "b.ts", "line1\nfeature-b\n", "land\n\nFusion-Task-Id: FN-A");
|
||||
const task = makeWorkspaceTask(shaA, shaB);
|
||||
const reverted = await revertWorkspaceTask({ task, workspaceRootDir: workspaceRoot, settings: {} });
|
||||
expect(reverted).toMatchObject({ mode: "git", clean: true });
|
||||
if (reverted.mode !== "git" || !reverted.clean) throw new Error("expected clean revert");
|
||||
const worktrees = applyWorkspaceRevertBoundaries(task.workspaceWorktrees, reverted.workspace.repos)!;
|
||||
|
||||
for (const [repoRel, repoRootDir] of [["repo-a", repoA], ["repo-b", repoB]] as const) {
|
||||
const entry = worktrees[repoRel]!;
|
||||
expect(await isRepoLanded(repoRootDir, "main", entry.landedSha, "FN-A", undefined, entry.revertBoundarySha)).toBe(false);
|
||||
expect(await findProvenLandedCommit(repoRootDir, "main", undefined, "FN-A", undefined, entry.revertBoundarySha)).toBeUndefined();
|
||||
}
|
||||
|
||||
const reLandedA = landTaskCommit(repoA, "a.ts", "line1\nfeature-a-redone\n", "reland\n\nFusion-Task-Id: FN-A");
|
||||
const reLandedB = landTaskCommit(repoB, "b.ts", "line1\nfeature-b-redone\n", "reland\n\nFusion-Task-Id: FN-A");
|
||||
expect(await findProvenLandedCommit(repoA, "main", reLandedA, "FN-A", undefined, worktrees["repo-a"]!.revertBoundarySha)).toBe(reLandedA);
|
||||
expect(await findProvenLandedCommit(repoB, "main", reLandedB, "FN-A", undefined, worktrees["repo-b"]!.revertBoundarySha)).toBe(reLandedB);
|
||||
});
|
||||
|
||||
it("partial-conflict rollback: a later task touching repo-b only leaves BOTH repos byte-identical to pre-call (Symptom Verification)", async () => {
|
||||
const { workspaceRoot, repoA, repoB } = workspaceFixture();
|
||||
const shaA = landTaskCommit(repoA, "a.ts", "line1\nfeature-a\n", "feat(FN-A): add feature in repo-a");
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { execSync, spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { findProvenLandedCommit, isRepoLanded } from "../merge/workspace-land-predicate.js";
|
||||
|
||||
const hasGit = spawnSync("git", ["--version"], { stdio: "pipe" }).status === 0;
|
||||
const describeIfGit = hasGit ? describe : describe.skip;
|
||||
|
||||
function git(repo: string, command: string): string {
|
||||
return execSync(command, { cwd: repo, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Workspace 2026-08-15-06:45:
|
||||
A git-mode revert keeps historical landedSha for attribution, but its integration-tip boundary
|
||||
invalidates both recorded-SHA and task-trailer proof until a newer task landing is created.
|
||||
*/
|
||||
describeIfGit("workspace land predicate revert boundary", () => {
|
||||
const dirs: string[] = [];
|
||||
afterEach(() => dirs.splice(0).forEach((dir) => rmSync(dir, { recursive: true, force: true })));
|
||||
|
||||
function fixture(): string {
|
||||
const repo = mkdtempSync(join(tmpdir(), "fn-9047-predicate-"));
|
||||
dirs.push(repo);
|
||||
git(repo, "git init -b main");
|
||||
git(repo, 'git config user.email "test@example.com"');
|
||||
git(repo, 'git config user.name "Test User"');
|
||||
writeFileSync(join(repo, "file.txt"), "base\n");
|
||||
git(repo, "git add file.txt && git commit -m init");
|
||||
return repo;
|
||||
}
|
||||
|
||||
function commit(repo: string, content: string, subject: string, trailer = false): string {
|
||||
writeFileSync(join(repo, "file.txt"), content);
|
||||
git(repo, "git add file.txt");
|
||||
git(repo, `git commit -m ${JSON.stringify(subject)}${trailer ? " -m 'Fusion-Task-Id: FN-A'" : ""}`);
|
||||
return git(repo, "git rev-parse HEAD");
|
||||
}
|
||||
|
||||
it("invalidates recorded and trailer landing proof at the revert boundary, then accepts a new landing", async () => {
|
||||
const repo = fixture();
|
||||
const landed = commit(repo, "landed\n", "land", true);
|
||||
const boundary = commit(repo, "reverted\n", "revert", true);
|
||||
|
||||
await expect(isRepoLanded(repo, "main", landed, "FN-A", undefined, boundary)).resolves.toBe(false);
|
||||
await expect(findProvenLandedCommit(repo, "main", undefined, "FN-A", undefined, boundary)).resolves.toBeUndefined();
|
||||
await expect(isRepoLanded(repo, "main", landed, "FN-A")).resolves.toBe(true);
|
||||
|
||||
const relanded = commit(repo, "relanded\n", "reland", true);
|
||||
await expect(findProvenLandedCommit(repo, "main", relanded, "FN-A", undefined, boundary)).resolves.toBe(relanded);
|
||||
});
|
||||
|
||||
it("keeps legacy behavior for an unresolvable boundary", async () => {
|
||||
const repo = fixture();
|
||||
const landed = commit(repo, "landed\n", "land", true);
|
||||
await expect(isRepoLanded(repo, "main", landed, "FN-A", undefined, "not-a-sha")).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1058,10 +1058,30 @@ export interface WorkspaceRepoRevertResult {
|
||||
repo: string;
|
||||
classification: TaskRevertClassification;
|
||||
revertCommitSha?: string;
|
||||
/** The integration tip that invalidates prior landing proof after a git-mode revert. */
|
||||
revertBoundarySha?: string;
|
||||
/** Lets callers leave repos with no attributable task work untouched. */
|
||||
attributedCommitCount: number;
|
||||
conflicts?: TaskRevertConflict[];
|
||||
alreadyReverted?: boolean;
|
||||
}
|
||||
|
||||
/** Apply only successful git-mode revert boundaries; this service remains store-free. */
|
||||
export function applyWorkspaceRevertBoundaries(
|
||||
currentWorktrees: Task["workspaceWorktrees"] | undefined,
|
||||
repoResults: readonly WorkspaceRepoRevertResult[],
|
||||
): Task["workspaceWorktrees"] | undefined {
|
||||
if (!currentWorktrees) return currentWorktrees;
|
||||
const next = { ...currentWorktrees };
|
||||
for (const result of repoResults) {
|
||||
const entry = next[result.repo];
|
||||
if (entry && result.attributedCommitCount > 0 && result.revertBoundarySha) {
|
||||
next[result.repo] = { ...entry, revertBoundarySha: result.revertBoundarySha };
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export type WorkspaceTaskRevertResult =
|
||||
| { mode: "git"; clean: true; workspace: { repos: WorkspaceRepoRevertResult[] } }
|
||||
| { mode: "git"; clean: false; workspace: { repos: WorkspaceRepoRevertResult[] }; conflicts: (TaskRevertConflict & { repo: string })[] }
|
||||
@@ -1215,6 +1235,7 @@ export async function revertWorkspaceTask(opts: RevertWorkspaceTaskOptions): Pro
|
||||
classification: ctx.classification.classification,
|
||||
conflicts: ctx.classification.conflicts,
|
||||
alreadyReverted: ctx.classification.alreadyReverted,
|
||||
attributedCommitCount: ctx.commits.length,
|
||||
}));
|
||||
const conflicts = contexts.flatMap((ctx) =>
|
||||
(ctx.classification.conflicts ?? []).map((conflict) => ({ ...conflict, repo: ctx.repo })),
|
||||
@@ -1231,7 +1252,11 @@ export async function revertWorkspaceTask(opts: RevertWorkspaceTaskOptions): Pro
|
||||
try {
|
||||
for (const ctx of contexts) {
|
||||
if (ctx.classification.classification === "already-reverted" || ctx.commits.length === 0) {
|
||||
repos.push({ repo: ctx.repo, classification: "already-reverted", alreadyReverted: true });
|
||||
repos.push({
|
||||
repo: ctx.repo, classification: "already-reverted", alreadyReverted: true,
|
||||
attributedCommitCount: ctx.commits.length,
|
||||
revertBoundarySha: ctx.commits.length > 0 ? ctx.preRevertHead : undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1243,13 +1268,20 @@ export async function revertWorkspaceTask(opts: RevertWorkspaceTaskOptions): Pro
|
||||
});
|
||||
|
||||
if (applied.applied) {
|
||||
repos.push({ repo: ctx.repo, classification: "clean", revertCommitSha: applied.revertCommitSha });
|
||||
repos.push({
|
||||
repo: ctx.repo, classification: "clean", revertCommitSha: applied.revertCommitSha,
|
||||
revertBoundarySha: applied.revertCommitSha, attributedCommitCount: ctx.commits.length,
|
||||
});
|
||||
committedRepos.push({ repo: ctx.repo, repoRootDir: ctx.repoRootDir, preRevertHead: ctx.preRevertHead });
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("alreadyReverted" in applied) {
|
||||
repos.push({ repo: ctx.repo, classification: "already-reverted", alreadyReverted: true });
|
||||
repos.push({
|
||||
repo: ctx.repo, classification: "already-reverted", alreadyReverted: true,
|
||||
attributedCommitCount: ctx.commits.length,
|
||||
revertBoundarySha: ctx.commits.length > 0 ? ctx.preRevertHead : undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1262,7 +1294,7 @@ export async function revertWorkspaceTask(opts: RevertWorkspaceTaskOptions): Pro
|
||||
}
|
||||
const conflictRepos: WorkspaceRepoRevertResult[] = [
|
||||
...repos,
|
||||
{ repo: ctx.repo, classification: "conflicting", conflicts: applied.conflicts },
|
||||
{ repo: ctx.repo, classification: "conflicting", conflicts: applied.conflicts, attributedCommitCount: ctx.commits.length },
|
||||
];
|
||||
const conflicts = (applied.conflicts ?? []).map((conflict) => ({ ...conflict, repo: ctx.repo }));
|
||||
return { mode: "git", clean: false, workspace: { repos: conflictRepos }, conflicts };
|
||||
@@ -1489,6 +1521,7 @@ export async function prepareWorkspaceRevertPrBranches(
|
||||
classification: ctx.classification.classification,
|
||||
conflicts: ctx.classification.conflicts,
|
||||
alreadyReverted: ctx.classification.alreadyReverted,
|
||||
attributedCommitCount: ctx.commits.length,
|
||||
}));
|
||||
const conflicts = contexts.flatMap((ctx) =>
|
||||
(ctx.classification.conflicts ?? []).map((conflict) => ({ ...conflict, repo: ctx.repo })),
|
||||
@@ -1534,6 +1567,7 @@ export async function prepareWorkspaceRevertPrBranches(
|
||||
classification: c.repo === ctx.repo ? "conflicting" : c.classification.classification,
|
||||
conflicts: c.repo === ctx.repo ? outcome.conflicts : c.classification.conflicts,
|
||||
alreadyReverted: c.classification.alreadyReverted,
|
||||
attributedCommitCount: c.commits.length,
|
||||
}));
|
||||
return { eligible: false, classification: "conflicting", conflicts, repos };
|
||||
}
|
||||
|
||||
@@ -430,6 +430,7 @@ export {
|
||||
type WorkspaceRepoRevertResult,
|
||||
type WorkspaceTaskRevertResult,
|
||||
type RevertWorkspaceTaskOptions,
|
||||
applyWorkspaceRevertBoundaries,
|
||||
prepareRevertPrBranch,
|
||||
type PrepareRevertPrBranchResult,
|
||||
type PrepareRevertPrBranchOptions,
|
||||
|
||||
@@ -2026,6 +2026,7 @@ export async function landWorkspaceTask(
|
||||
entry.landedSha,
|
||||
taskId,
|
||||
entry.branch,
|
||||
entry.revertBoundarySha,
|
||||
);
|
||||
if (provenLandedSha) {
|
||||
/*
|
||||
@@ -2250,7 +2251,9 @@ async function persistRepoLandedSha(
|
||||
const current = latest?.workspaceWorktrees ?? {};
|
||||
const entry = current[repoRel];
|
||||
if (!entry) return; // entry vanished — nothing to merge into
|
||||
const next = { ...current, [repoRel]: { ...entry, landedSha } };
|
||||
// FNXC:Workspace 2026-08-15-06:45: a new landing is strictly after its revert boundary,
|
||||
// so clear that invalidation marker while retaining the fresh landedSha as normal proof.
|
||||
const next = { ...current, [repoRel]: { ...entry, landedSha, revertBoundarySha: undefined } };
|
||||
await store.updateTask(taskId, { workspaceWorktrees: next });
|
||||
}
|
||||
|
||||
|
||||
@@ -92,16 +92,28 @@ export async function findProvenLandedCommit(
|
||||
landedSha: string | undefined,
|
||||
taskId?: string,
|
||||
branch?: string,
|
||||
revertBoundarySha?: string,
|
||||
): Promise<string | undefined> {
|
||||
const intRef = `refs/heads/${integrationBranch}`;
|
||||
if (!(await gitOk(["rev-parse", "--verify", intRef], repoRootDir))) {
|
||||
return undefined;
|
||||
}
|
||||
/*
|
||||
FNXC:Workspace 2026-08-15-06:45:
|
||||
A revert commit carries this task's Fusion-Task-Id trailer, so clearing landedSha alone would
|
||||
still make the trailer fallback skip re-done work. Reject any proof at or behind the durable
|
||||
revert boundary; an unknown boundary deliberately preserves legacy idempotency behavior.
|
||||
*/
|
||||
const isAtOrBehindRevertBoundary = async (sha: string): Promise<boolean> => {
|
||||
if (!revertBoundarySha) return false;
|
||||
return gitOk(["merge-base", "--is-ancestor", sha, revertBoundarySha], repoRootDir);
|
||||
};
|
||||
// Primary: recorded landedSha is an ancestor of (or equals) the integration tip — that SHA
|
||||
// IS the exact landing commit.
|
||||
// IS the exact landing commit unless a completed revert invalidated it.
|
||||
if (
|
||||
landedSha &&
|
||||
(await gitOk(["merge-base", "--is-ancestor", landedSha, intRef], repoRootDir))
|
||||
(await gitOk(["merge-base", "--is-ancestor", landedSha, intRef], repoRootDir)) &&
|
||||
!(await isAtOrBehindRevertBoundary(landedSha))
|
||||
) {
|
||||
return landedSha;
|
||||
}
|
||||
@@ -133,7 +145,11 @@ export async function findProvenLandedCommit(
|
||||
for (const sha of candidates.trim().split("\n")) {
|
||||
if (!sha) continue;
|
||||
const body = await gitCapture(["show", "-s", "--format=%B", sha], repoRootDir);
|
||||
if (body && body.split("\n").some((line) => line.trim() === trailer)) {
|
||||
if (
|
||||
body &&
|
||||
body.split("\n").some((line) => line.trim() === trailer) &&
|
||||
!(await isAtOrBehindRevertBoundary(sha))
|
||||
) {
|
||||
return sha;
|
||||
}
|
||||
}
|
||||
@@ -148,8 +164,9 @@ export async function isRepoLanded(
|
||||
landedSha: string | undefined,
|
||||
taskId?: string,
|
||||
branch?: string,
|
||||
revertBoundarySha?: string,
|
||||
): Promise<boolean> {
|
||||
return Boolean(
|
||||
await findProvenLandedCommit(repoRootDir, integrationBranch, landedSha, taskId, branch),
|
||||
await findProvenLandedCommit(repoRootDir, integrationBranch, landedSha, taskId, branch, revertBoundarySha),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10109,7 +10109,12 @@ const movedTask = await this.store.moveTask(task.id, completeLane);
|
||||
unlandedRepos.push(repoRel);
|
||||
continue;
|
||||
}
|
||||
if (await isRepoLanded(repoRootDir, integrationBranch, entry.landedSha, task.id, entry.branch)) {
|
||||
/*
|
||||
FNXC:Workspace 2026-08-15-06:45:
|
||||
A boundary-invalidated repo is unlanded here and therefore follows the existing
|
||||
branch-present retry path; FORK-A remains fail-closed when its branch is absent.
|
||||
*/
|
||||
if (await isRepoLanded(repoRootDir, integrationBranch, entry.landedSha, task.id, entry.branch, entry.revertBoundarySha)) {
|
||||
landedRepos.push(repoRel);
|
||||
continue;
|
||||
}
|
||||
@@ -10608,7 +10613,7 @@ const movedTask = await this.store.moveTask(task.id, completeLane);
|
||||
repoRootDir,
|
||||
{ ...settings, integrationBranch: undefined, baseBranch: undefined },
|
||||
);
|
||||
safe = await isRepoLanded(repoRootDir, integrationBranch, entry.landedSha, task.id, branch);
|
||||
safe = await isRepoLanded(repoRootDir, integrationBranch, entry.landedSha, task.id, branch, entry.revertBoundarySha);
|
||||
}
|
||||
if (!safe && entry.baseCommitSha) {
|
||||
const count = await this.execWorkspaceTeardownGit(`git rev-list --count ${shellQuote(entry.baseCommitSha)}..${shellQuote(branch)}`, { cwd: repoRootDir, timeout: 120_000 });
|
||||
|
||||
Reference in New Issue
Block a user