@@ -25669,6 +25669,8 @@ function dumpsPretty(data) {
2566925669 });
2567025670}
2567125671
25672+ const MAX_PUSH_ATTEMPTS = 5;
25673+
2567225674// ── Core retire logic ───────────────────────────────────────────────────────
2567325675
2567425676/**
@@ -25777,41 +25779,64 @@ async function run() {
2577725779 await exec.exec('git', ['-C', repoDir, 'config', 'user.email', 'svc.spring-builds@broadcom.com']);
2577825780
2577925781 const projectsFile = path.join(repoDir, 'config', 'projects.json');
25780- const data = JSON.parse(fs.readFileSync(projectsFile, 'utf-8'));
2578125782
25782- const changed = retireBranch(data, repo, branch);
25783+ // Several runs can update projects.json at once (mark-release-train-ready dispatches one
25784+ // per project), so a push can be rejected because main moved after the clone. On a
25785+ // rejection, reset to the new main and apply the change again to the fresh file rather
25786+ // than rebasing: two runs touching neighbouring lines of the JSON would conflict as text
25787+ // even though their changes don't overlap.
25788+ for (let attempt = 1; ; attempt++) {
25789+ const data = JSON.parse(fs.readFileSync(projectsFile, 'utf-8'));
2578325790
25784- if (!changed) {
25785- core.info('No changes required.');
25786- return;
25787- }
25791+ const changed = retireBranch(data, repo, branch);
2578825792
25789- fs.writeFileSync(projectsFile, dumpsPretty(data) + '\n', 'utf-8');
25793+ if (!changed) {
25794+ core.info('No changes required.');
25795+ return;
25796+ }
2579025797
25791- await exec.exec('git', ['-C', repoDir, 'add ', 'config/projects.json'] );
25798+ fs.writeFileSync(projectsFile, dumpsPretty(data) + '\n ', 'utf-8' );
2579225799
25793- const diffCode = await exec.exec(
25794- 'git', ['-C', repoDir, 'diff', '--cached', '--quiet'],
25795- { ignoreReturnCode: true }
25796- );
25797- if (diffCode === 0) {
25798- core.info('projects.json is unchanged — nothing to commit.');
25799- return;
25800- }
25800+ await exec.exec('git', ['-C', repoDir, 'add', 'config/projects.json']);
2580125801
25802- await exec.exec('git', [
25803- '-C', repoDir, 'commit', '-m',
25804- `Update projects.json: retire ${baseName} branch ${branch}`,
25805- ]);
25806- await exec.exec('git', ['-C', repoDir, 'push', 'origin', 'main']);
25802+ const diffCode = await exec.exec(
25803+ 'git', ['-C', repoDir, 'diff', '--cached', '--quiet'],
25804+ { ignoreReturnCode: true }
25805+ );
25806+ if (diffCode === 0) {
25807+ core.info('projects.json is unchanged — nothing to commit.');
25808+ return;
25809+ }
25810+
25811+ await exec.exec('git', [
25812+ '-C', repoDir, 'commit', '-m',
25813+ `Update projects.json: retire ${baseName} branch ${branch}`,
25814+ ]);
25815+ const pushCode = await exec.exec(
25816+ 'git', ['-C', repoDir, 'push', 'origin', 'main'],
25817+ { ignoreReturnCode: true }
25818+ );
25819+ if (pushCode === 0) break;
25820+
25821+ if (attempt >= MAX_PUSH_ATTEMPTS) {
25822+ throw new Error(`push to main was rejected ${MAX_PUSH_ATTEMPTS} times`);
25823+ }
25824+ // Jittered, so runs that collided once don't collide again in lockstep.
25825+ const delayMs = 2000 * attempt + Math.floor(Math.random() * 3000);
25826+ core.warning(`Push to main rejected (attempt ${attempt}/${MAX_PUSH_ATTEMPTS}); ` +
25827+ `retrying on the latest main in ${Math.round(delayMs / 1000)}s.`);
25828+ await new Promise(resolve => setTimeout(resolve, delayMs));
25829+ await exec.exec('git', ['-C', repoDir, 'fetch', 'origin', 'main']);
25830+ await exec.exec('git', ['-C', repoDir, 'reset', '--hard', 'origin/main']);
25831+ }
2580725832
2580825833 core.info('projects.json committed and pushed.');
2580925834 } catch (err) {
2581025835 core.setFailed(`Action failed: ${err.message}`);
2581125836 }
2581225837}
2581325838
25814- module.exports = { dumpsPretty, retireBranch };
25839+ module.exports = { dumpsPretty, retireBranch, run };
2581525840
2581625841if (require.main === require.cache[eval('__filename')]) {
2581725842 run();
0 commit comments