mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-08 07:29:13 +00:00
process: the merge deletes the task branch it merged (#702)
370 merged issue/* branches sat on origin; the branch list stopped meaning anything and an agent looking a branch up by number could take a stale one. - merge-candidate.mjs: after a successful push to dev the task branch is deleted with --force-with-lease on the tip the merge saw last — the candidate published into the branch, or the material on fast-forward (the index commit lives only in dev). A commit that landed after the merge keeps the branch, and the merge comment says so; a failed delete never undoes the merge. Failed, stale and conflicting merges keep it. - The one-time cleanup of the already merged branches is not in this commit: the list goes to the owner first. Canon: PROCESS.md §10.4 (exact-candidate merge). Issue: #702 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -101,7 +101,9 @@ export function commentFor(action, ctx) {
|
||||
case 'fast-forward':
|
||||
return `материал \`${short(ctx.material)}\` · dev@\`${short(ctx.devNow)}\` → кандидат \`${short(ctx.candidate)}\``
|
||||
+ (action === 'push' ? ` · Validate ${ctx.runUrl} зелёный` : ' · dev не двигался')
|
||||
+ ' · слито';
|
||||
+ ' · слито'
|
||||
+ (ctx.branchDeleted === true ? ` · ветка \`${ctx.branch}\` удалена` : '')
|
||||
+ (ctx.branchDeleted === false ? ` · ветка \`${ctx.branch}\` оставлена: её вершина сдвинулась после слияния` : '');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -186,6 +188,14 @@ export function realOps({
|
||||
must(exec(process.execPath, [REVIEWS_INDEX_SCRIPT, '--dir=docs/reviews', '--commit-if-stale', `--issue=${issue}`]), 'reviews-index --commit-if-stale');
|
||||
return must(git('rev-parse', 'HEAD'), 'rev-parse HEAD');
|
||||
},
|
||||
// #702: ветка задачи удаляется после слияния — только если её вершина всё
|
||||
// ещё та, что влита (lease): коммит, прилетевший после, не теряется.
|
||||
deleteBranch: (ref, expected) => {
|
||||
const r = git('push', '-q', `--force-with-lease=refs/heads/${ref}:${expected}`, pushUrl, `:refs/heads/${ref}`);
|
||||
if (r.status === 0) return true;
|
||||
if (/stale info|rejected|fetch first|lease/i.test(r.stderr)) return false;
|
||||
throw new Error(`git push :${ref}: ${r.stderr}`);
|
||||
},
|
||||
pushWithLease: (sha, ref, expected) => {
|
||||
const r = git('push', '-q', `--force-with-lease=refs/heads/${ref}:${expected}`, pushUrl, `${sha}:refs/heads/${ref}`);
|
||||
if (r.status === 0) return true;
|
||||
@@ -250,9 +260,18 @@ export async function mergeCandidate({ branch, material, issue, ops, maxAttempts
|
||||
&& ops.diffNames(material, actual, ['.', ':!docs/reviews']).length === 0);
|
||||
const ctx = { branch, material, actual, issue };
|
||||
const finish = (decision, extra = {}) => {
|
||||
const body = commentFor(decision.action, { ...ctx, ...extra, attempt: extra.attempt });
|
||||
if (body) ops.comment(issue, body);
|
||||
const merged = decision.action === 'push' || decision.action === 'fast-forward';
|
||||
// #702: влитая ветка больше не нужна — 368 таких висели на origin, и агент,
|
||||
// искавший ветку по номеру, мог взять устаревшую. `branchTip` — вершина,
|
||||
// которую слияние видело последней: кандидат, опубликованный в ветку, либо
|
||||
// материал при fast-forward. Сбой удаления слияние не отменяет.
|
||||
let branchDeleted = null;
|
||||
if (merged && extra.branchTip) {
|
||||
try { branchDeleted = ops.deleteBranch(branch, extra.branchTip); }
|
||||
catch (error) { ops.log(`ветка ${branch} не удалена: ${error.message}`); }
|
||||
}
|
||||
const body = commentFor(decision.action, { ...ctx, ...extra, attempt: extra.attempt, branchDeleted });
|
||||
if (body) ops.comment(issue, body);
|
||||
ops.log(`решение: ${decision.action} → ${decision.to || '(метка по вердикту)'}`);
|
||||
return { merged, to: decision.to, action: decision.action, candidate: extra.candidate || actual };
|
||||
};
|
||||
@@ -272,7 +291,7 @@ export async function mergeCandidate({ branch, material, issue, ops, maxAttempts
|
||||
const pushed = ops.pushWithLease(target, 'dev', devNow);
|
||||
const decision = decideMerge({ fresh: true, devMoved: false, leaseRejected: !pushed });
|
||||
if (decision.action === 'retry') continue;
|
||||
return finish(decision, { candidate: target, devNow });
|
||||
return finish(decision, { candidate: target, devNow, branchTip: tip });
|
||||
}
|
||||
|
||||
const candidate = ops.rebaseOnto(tip, 'origin/dev');
|
||||
@@ -317,7 +336,7 @@ export async function mergeCandidate({ branch, material, issue, ops, maxAttempts
|
||||
const pushed = ops.pushWithLease(candidate, 'dev', devNow);
|
||||
decision = decideMerge({ fresh: true, devMoved: true, patchIdEqual: true, validate: result, leaseRejected: !pushed, attempt, maxAttempts });
|
||||
if (decision.action === 'retry') { ops.log('dev двинулся снова — ещё попытка'); continue; }
|
||||
return finish(decision, { candidate, devNow, runUrl: url, attempt });
|
||||
return finish(decision, { candidate, devNow, runUrl: url, attempt, branchTip: candidate });
|
||||
}
|
||||
return finish(decideMerge({ fresh: true, devMoved: true, patchIdEqual: true, validate: 'green', leaseRejected: true, attempt: maxAttempts, maxAttempts }), { candidate: tip, attempt: maxAttempts });
|
||||
}
|
||||
|
||||
@@ -5509,6 +5509,18 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: " const pushed = ops.pushWithLease(candidate, 'dev', devNow) || true; // mutant: lease never rejected\n decision = decideMerge(",
|
||||
}],
|
||||
},
|
||||
// #702: влитая ветка задачи удаляется слиянием.
|
||||
{
|
||||
id: 'merged-task-branch-kept',
|
||||
guard: 'node --test --test-name-pattern="#702" test/merge-candidate.test.mjs',
|
||||
because: '#702: 368 merged issue/* branches piled up on origin; an agent looking a branch up by '
|
||||
+ 'number could take a stale one — the merge deletes the branch it merged',
|
||||
patches: [{
|
||||
file: 'scripts/merge-candidate.mjs',
|
||||
find: ' if (merged && extra.branchTip) {',
|
||||
replace: ' if (false && merged && extra.branchTip) { // mutant: branch kept',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'nightly-does-not-wait',
|
||||
guard: 'node --test --test-name-pattern="nightly ждёт запущенный Validate" test/nightly-workflow.test.mjs',
|
||||
|
||||
Reference in New Issue
Block a user