@@ -104,19 +104,25 @@ jobs:
104104 throw new Error(`PR #${pullNumber} merge tree does not match its validated head tree.`);
105105 }
106106
107- const allowedSourceRefs = [
107+ const exactSourceRefs = [
108108 `refs/heads/release-run/${version}`,
109109 `refs/tags/${version}`,
110110 ];
111111 const sourceRef = process.env.EVENT_REF;
112- if (!allowedSourceRefs.includes(sourceRef) || process.env.EVENT_SHA !== releaseSha) {
112+ const master = await github.rest.git.getRef({ owner, repo, ref: 'heads/master' });
113+ const exactReleaseSource =
114+ exactSourceRefs.includes(sourceRef) && process.env.EVENT_SHA === releaseSha;
115+ const trustedMasterRecovery =
116+ sourceRef === 'refs/heads/master' &&
117+ process.env.EVENT_SHA === master.data.object.sha;
118+ if (!exactReleaseSource && !trustedMasterRecovery) {
113119 throw new Error(
114- `Release workflow must run from an exact release ref at ${releaseSha}; got ` +
115- `${process.env.EVENT_REF} at ${process.env.EVENT_SHA}.`,
120+ `Release workflow must run from an exact release ref at ${releaseSha} or the ` +
121+ `current protected master for workflow recovery; got ${sourceRef} at ` +
122+ `${process.env.EVENT_SHA}.`,
116123 );
117124 }
118125
119- const master = await github.rest.git.getRef({ owner, repo, ref: 'heads/master' });
120126 const comparison = await github.rest.repos.compareCommitsWithBasehead({
121127 owner,
122128 repo,
@@ -638,6 +644,23 @@ jobs:
638644 }
639645 }
640646
647+ async function waitForPublishedRelease(releaseId) {
648+ for (let attempt = 0; attempt < 30; attempt += 1) {
649+ const current = await github.rest.repos.getRelease({
650+ owner,
651+ repo,
652+ release_id: releaseId,
653+ });
654+ if (!current.data.draft && current.data.tag_name === version) {
655+ return current.data;
656+ }
657+ if (attempt < 29) {
658+ await new Promise((resolve) => setTimeout(resolve, 1000));
659+ }
660+ }
661+ throw new Error(`GitHub did not converge release ${version} to published state.`);
662+ }
663+
641664 const tagSha = await resolveTagCommit(version);
642665 if (tagSha !== releaseSha) {
643666 throw new Error(`Tag ${version} points to ${tagSha}, expected ${releaseSha}.`);
@@ -685,6 +708,8 @@ jobs:
685708 owner,
686709 repo,
687710 release_id: release.id,
711+ tag_name: version,
712+ target_commitish: releaseSha,
688713 name: version,
689714 body: notes,
690715 draft: true,
@@ -718,40 +743,28 @@ jobs:
718743 if (await resolveTagCommit(version) !== releaseSha) {
719744 throw new Error(`Release tag ${version} changed before publication.`);
720745 }
721- try {
722- const published = await github.rest.repos.updateRelease({
723- owner,
724- repo,
725- release_id: release.id,
726- name: version,
727- body: notes,
728- draft: false,
729- prerelease: false,
730- make_latest: 'true',
731- });
732- if (published.data.draft || published.data.tag_name !== version) {
733- throw new Error(`GitHub did not publish release ${version}.`);
734- }
735- if (await resolveTagCommit(version) !== releaseSha) {
736- throw new Error(`Release tag ${version} changed during publication.`);
737- }
738- await verifyRemoteAssets(release.id);
739- } catch (error) {
740- try {
741- await github.rest.repos.updateRelease({
742- owner,
743- repo,
744- release_id: release.id,
745- name: version,
746- body: notes,
747- draft: true,
748- prerelease: false,
749- });
750- core.warning(`Returned release ${version} to draft after final verification failed.`);
751- } catch (rollbackError) {
752- core.error(`Could not return release ${version} to draft: ${rollbackError.message}`);
753- }
754- throw error;
746+ await github.rest.repos.updateRelease({
747+ owner,
748+ repo,
749+ release_id: release.id,
750+ tag_name: version,
751+ target_commitish: releaseSha,
752+ name: version,
753+ body: notes,
754+ draft: false,
755+ prerelease: false,
756+ make_latest: 'true',
757+ });
758+ release = await waitForPublishedRelease(release.id);
759+ if (await resolveTagCommit(version) !== releaseSha) {
760+ throw new Error(`Release tag ${version} changed during publication.`);
755761 }
762+ await verifyRemoteAssets(release.id);
763+
764+ // All mutable release inputs were verified before publication. Do not
765+ // demote a published release on a later verification failure: GitHub
766+ // detaches its tag and renames the draft to `untagged-*`. A recovery
767+ // run instead revalidates the public release exactly and finishes only
768+ // when the tag and downloaded asset digests still match.
756769 await removeRecoveryBranch();
757770 core.notice(`Published and verified PodNotes ${version} at ${releaseSha}.`);
0 commit comments