Skip to content

Commit 55f9b71

Browse files
svelderrainruizGitHub Copilot
andauthored
Fix rollback drill remote fallback (#936) (#941)
* fix(release): repair rollback drill remote resolution (#936) * test(release): cover rollback sync origin seam (#936) --------- Co-authored-by: GitHub Copilot <copilot@users.noreply.github.com>
1 parent abd8215 commit 55f9b71

2 files changed

Lines changed: 207 additions & 14 deletions

File tree

tools/priority/__tests__/rollback-release.test.mjs

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
parseArgs,
55
filterReleasesForStream,
66
resolveRollbackPointer,
7-
evaluateRollbackValidation
7+
evaluateRollbackValidation,
8+
resolveRollbackRemoteName,
9+
runRollback
810
} from '../rollback-release.mjs';
911
import { normalizeReleaseRollbackPolicy } from '../lib/release-rollback-policy.mjs';
1012

@@ -116,3 +118,141 @@ test('evaluateRollbackValidation reports branch and policy failures', () => {
116118
assert.ok(validation.failures.some((failure) => failure.code === 'policy-sync-failed'));
117119
});
118120

121+
test('resolveRollbackRemoteName falls back to origin when it matches the repository slug', () => {
122+
const resolution = resolveRollbackRemoteName({
123+
repoRoot: '/repo',
124+
preferredRemote: 'upstream',
125+
repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action',
126+
tryResolveRemoteFn: (_repoRoot, remoteName) => {
127+
if (remoteName === 'origin') {
128+
return {
129+
parsed: {
130+
owner: 'LabVIEW-Community-CI-CD',
131+
repo: 'compare-vi-cli-action'
132+
}
133+
};
134+
}
135+
return null;
136+
}
137+
});
138+
139+
assert.equal(resolution.configuredRemote, 'upstream');
140+
assert.equal(resolution.effectiveRemote, 'origin');
141+
assert.match(resolution.fallbackReason, /using origin/i);
142+
});
143+
144+
test('resolveRollbackRemoteName rejects missing configured remote when origin is a fork', () => {
145+
assert.throws(
146+
() =>
147+
resolveRollbackRemoteName({
148+
repoRoot: '/repo',
149+
preferredRemote: 'upstream',
150+
repository: 'LabVIEW-Community-CI-CD/compare-vi-cli-action',
151+
tryResolveRemoteFn: (_repoRoot, remoteName) => {
152+
if (remoteName === 'origin') {
153+
return {
154+
parsed: {
155+
owner: 'fork-owner',
156+
repo: 'compare-vi-cli-action'
157+
}
158+
};
159+
}
160+
return null;
161+
}
162+
}),
163+
/origin does not match repository/i
164+
);
165+
});
166+
167+
test('runRollback uses origin fallback for dry-run planning when upstream is unavailable', async () => {
168+
const policy = normalizeReleaseRollbackPolicy({});
169+
const fetchCalls = [];
170+
const resolution = await runRollback(
171+
{
172+
...parseArgs(['node', 'rollback-release.mjs']),
173+
repo: 'LabVIEW-Community-CI-CD/compare-vi-cli-action'
174+
},
175+
{
176+
repoRoot: '/repo',
177+
policy,
178+
fetchReleaseRecords: () => [
179+
{ tag_name: 'v1.2.3', draft: false, prerelease: false, published_at: '2026-03-05T00:00:00Z' },
180+
{ tag_name: 'v1.2.2', draft: false, prerelease: false, published_at: '2026-03-01T00:00:00Z' }
181+
],
182+
remoteResolver: () => ({
183+
configuredRemote: 'upstream',
184+
effectiveRemote: 'origin',
185+
fallbackReason: 'Configured rollback remote is missing; using origin.'
186+
}),
187+
fetchRemoteRefs: (_repoRoot, remote, branches) => {
188+
fetchCalls.push({ remote, branches });
189+
},
190+
resolveTagCommit: () => 'deadbeef',
191+
tryResolveRef: (_repoRoot, ref) => `sha-for:${ref}`
192+
}
193+
);
194+
195+
assert.equal(fetchCalls.length, 1);
196+
assert.equal(fetchCalls[0].remote, 'origin');
197+
assert.deepEqual(fetchCalls[0].branches, ['main', 'develop']);
198+
assert.equal(resolution.policy.configuredRemote, 'upstream');
199+
assert.equal(resolution.policy.remote, 'origin');
200+
assert.match(resolution.policy.remoteFallbackReason, /using origin/i);
201+
assert.equal(resolution.branches[0].remote, 'origin');
202+
assert.equal(resolution.summary.status, 'pass');
203+
});
204+
205+
test('runRollback uses injected ref resolver throughout apply and sync-origin flows', async () => {
206+
const policy = normalizeReleaseRollbackPolicy({});
207+
const seenRefs = [];
208+
const forcePushCalls = [];
209+
const refCounts = new Map();
210+
const targetCommit = 'deadbeef';
211+
212+
const resolution = await runRollback(
213+
{
214+
...parseArgs(['node', 'rollback-release.mjs']),
215+
repo: 'LabVIEW-Community-CI-CD/compare-vi-cli-action',
216+
apply: true,
217+
syncOrigin: true
218+
},
219+
{
220+
repoRoot: '/repo',
221+
policy,
222+
fetchReleaseRecords: () => [
223+
{ tag_name: 'v1.2.3', draft: false, prerelease: false, published_at: '2026-03-05T00:00:00Z' },
224+
{ tag_name: 'v1.2.2', draft: false, prerelease: false, published_at: '2026-03-01T00:00:00Z' }
225+
],
226+
remoteResolver: () => ({
227+
configuredRemote: 'upstream',
228+
effectiveRemote: 'mirror',
229+
fallbackReason: null
230+
}),
231+
fetchRemoteRefs: () => {},
232+
resolveTagCommit: () => targetCommit,
233+
tryResolveRef: (_repoRoot, ref) => {
234+
seenRefs.push(ref);
235+
const count = (refCounts.get(ref) || 0) + 1;
236+
refCounts.set(ref, count);
237+
return count === 1 ? `lease:${ref}` : targetCommit;
238+
},
239+
forcePushBranch: (_repoRoot, remote, branch, commit, leaseCommit) => {
240+
forcePushCalls.push({ remote, branch, commit, leaseCommit });
241+
},
242+
policySync: () => ({
243+
executed: true,
244+
status: 'pass',
245+
exitCode: 0,
246+
stdout: '',
247+
stderr: ''
248+
})
249+
}
250+
);
251+
252+
assert.ok(seenRefs.includes('mirror/main'));
253+
assert.ok(seenRefs.includes('mirror/develop'));
254+
assert.ok(seenRefs.includes('origin/main'));
255+
assert.ok(seenRefs.includes('origin/develop'));
256+
assert.equal(forcePushCalls.length, 4);
257+
assert.equal(resolution.summary.status, 'pass');
258+
});

tools/priority/rollback-release.mjs

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import process from 'node:process';
66
import { spawnSync } from 'node:child_process';
77
import { fileURLToPath } from 'node:url';
88
import { ensureCleanWorkingTree, getRepoRoot, run } from './lib/branch-utils.mjs';
9-
import { ensureGhCli, resolveUpstream } from './lib/remote-utils.mjs';
9+
import { ensureGhCli, resolveUpstream, tryResolveRemote } from './lib/remote-utils.mjs';
1010
import {
1111
DEFAULT_RELEASE_ROLLBACK_POLICY_PATH,
1212
loadReleaseRollbackPolicy,
@@ -220,6 +220,45 @@ function resolveRepositorySlug(explicitRepo, repoRoot) {
220220
return `${upstream.owner}/${upstream.repo}`;
221221
}
222222

223+
function toRepositorySlug(parsedRemote) {
224+
if (!parsedRemote?.owner || !parsedRemote?.repo) {
225+
return null;
226+
}
227+
return `${parsedRemote.owner}/${parsedRemote.repo}`.toLowerCase();
228+
}
229+
230+
export function resolveRollbackRemoteName({
231+
repoRoot,
232+
preferredRemote,
233+
repository,
234+
tryResolveRemoteFn = tryResolveRemote
235+
}) {
236+
const configuredRemote = String(preferredRemote || 'upstream').trim() || 'upstream';
237+
const configured = tryResolveRemoteFn(repoRoot, configuredRemote);
238+
if (configured?.parsed) {
239+
return {
240+
configuredRemote,
241+
effectiveRemote: configuredRemote,
242+
fallbackReason: null
243+
};
244+
}
245+
246+
const repositorySlug = String(repository || '').trim().toLowerCase();
247+
const origin = configuredRemote === 'origin' ? configured : tryResolveRemoteFn(repoRoot, 'origin');
248+
const originSlug = toRepositorySlug(origin?.parsed);
249+
if (originSlug && repositorySlug && originSlug === repositorySlug) {
250+
return {
251+
configuredRemote,
252+
effectiveRemote: 'origin',
253+
fallbackReason: `Configured rollback remote '${configuredRemote}' is missing; using origin because it matches ${repository}.`
254+
};
255+
}
256+
257+
throw new Error(
258+
`Rollback remote '${configuredRemote}' is not configured and origin does not match repository '${repository || '<unknown>'}'.`
259+
);
260+
}
261+
223262
function fetchReleaseRecords(repoRoot, repository, maxReleases) {
224263
const perPage = Math.min(100, Math.max(2, maxReleases));
225264
const payload = runGhApiJson(
@@ -352,7 +391,15 @@ export async function runRollback(options, dependencies = {}) {
352391
const policy = dependencies.policy || loadReleaseRollbackPolicy(options.policyPath);
353392
const streamPolicy = getReleaseRollbackStreamPolicy(policy, options.stream);
354393
const repository = resolveRepositorySlug(options.repo, repoRoot);
355-
const rollbackRemote = options.remote || policy.rollback.remote || 'upstream';
394+
const remoteResolver =
395+
dependencies.remoteResolver ||
396+
((args) => resolveRollbackRemoteName({ ...args, tryResolveRemoteFn: dependencies.tryResolveRemote || tryResolveRemote }));
397+
const rollbackRemoteResolution = remoteResolver({
398+
repoRoot,
399+
preferredRemote: options.remote || policy.rollback.remote || 'upstream',
400+
repository
401+
});
402+
const rollbackRemote = rollbackRemoteResolution.effectiveRemote;
356403
const targetBranches = [options.mainBranch, options.developBranch];
357404

358405
const failures = [];
@@ -368,28 +415,33 @@ export async function runRollback(options, dependencies = {}) {
368415
targetTag: options.targetTag
369416
});
370417

371-
fetchRemoteRefs(repoRoot, rollbackRemote, targetBranches);
372-
const targetCommit = resolveTagCommit(repoRoot, pointer.target.tag);
418+
const fetchRemoteRefsFn = dependencies.fetchRemoteRefs || fetchRemoteRefs;
419+
const resolveTagCommitFn = dependencies.resolveTagCommit || resolveTagCommit;
420+
const tryResolveRefFn = dependencies.tryResolveRef || tryResolveRef;
421+
const forcePushBranchFn = dependencies.forcePushBranch || forcePushBranch;
422+
423+
fetchRemoteRefsFn(repoRoot, rollbackRemote, targetBranches);
424+
const targetCommit = resolveTagCommitFn(repoRoot, pointer.target.tag);
373425

374426
const branches = targetBranches.map((name) => ({
375427
name,
376428
remote: rollbackRemote,
377-
before: tryResolveRef(repoRoot, `${rollbackRemote}/${name}`),
429+
before: tryResolveRefFn(repoRoot, `${rollbackRemote}/${name}`),
378430
after: null,
379431
pushed: false,
380432
matchesTarget: false
381433
}));
382434

383435
if (options.apply) {
384436
for (const branch of branches) {
385-
forcePushBranch(repoRoot, rollbackRemote, branch.name, targetCommit, branch.before);
437+
forcePushBranchFn(repoRoot, rollbackRemote, branch.name, targetCommit, branch.before);
386438
branch.pushed = true;
387439
}
388-
fetchRemoteRefs(repoRoot, rollbackRemote, targetBranches);
440+
fetchRemoteRefsFn(repoRoot, rollbackRemote, targetBranches);
389441
}
390442

391443
for (const branch of branches) {
392-
branch.after = tryResolveRef(repoRoot, `${rollbackRemote}/${branch.name}`);
444+
branch.after = tryResolveRefFn(repoRoot, `${rollbackRemote}/${branch.name}`);
393445
branch.matchesTarget = options.apply ? branch.after === targetCommit : false;
394446
}
395447

@@ -398,7 +450,7 @@ export async function runRollback(options, dependencies = {}) {
398450
const originBranches = targetBranches.map((name) => ({
399451
name,
400452
remote: 'origin',
401-
before: tryResolveRef(repoRoot, `origin/${name}`),
453+
before: tryResolveRefFn(repoRoot, `origin/${name}`),
402454
after: null,
403455
pushed: false,
404456
matchesTarget: false
@@ -408,12 +460,12 @@ export async function runRollback(options, dependencies = {}) {
408460
addFailure(failures, 'origin-branch-missing', `origin/${branch.name} does not exist for sync-origin.`);
409461
continue;
410462
}
411-
forcePushBranch(repoRoot, 'origin', branch.name, targetCommit, branch.before);
463+
forcePushBranchFn(repoRoot, 'origin', branch.name, targetCommit, branch.before);
412464
branch.pushed = true;
413465
}
414-
fetchRemoteRefs(repoRoot, 'origin', targetBranches);
466+
fetchRemoteRefsFn(repoRoot, 'origin', targetBranches);
415467
for (const branch of originBranches) {
416-
branch.after = tryResolveRef(repoRoot, `origin/${branch.name}`);
468+
branch.after = tryResolveRefFn(repoRoot, `origin/${branch.name}`);
417469
branch.matchesTarget = branch.after === targetCommit;
418470
}
419471
originSync = originBranches;
@@ -461,7 +513,9 @@ export async function runRollback(options, dependencies = {}) {
461513
policy: {
462514
path: options.policyPath,
463515
schema: policy.schema,
516+
configuredRemote: rollbackRemoteResolution.configuredRemote,
464517
remote: rollbackRemote,
518+
remoteFallbackReason: rollbackRemoteResolution.fallbackReason,
465519
targetBranches,
466520
minimumHistory: streamPolicy.minimumHistory
467521
},
@@ -559,4 +613,3 @@ if (invokedPath && invokedPath === modulePath) {
559613
process.exitCode = 1;
560614
});
561615
}
562-

0 commit comments

Comments
 (0)