diff --git a/__tests__/github.test.ts b/__tests__/github.test.ts index 545316f..9b7d351 100644 --- a/__tests__/github.test.ts +++ b/__tests__/github.test.ts @@ -610,6 +610,66 @@ describe('github', () => { release_id: duplicateRelease.id, }); }); + + it('deletes the just-created duplicate draft even if recent release listing misses it', async () => { + const canonicalRelease: Release = { + id: 1, + upload_url: 'canonical-upload', + html_url: 'canonical-html', + tag_name: 'v1.0.0', + name: 'canonical', + body: 'test', + target_commitish: 'main', + draft: true, + prerelease: false, + assets: [], + }; + const duplicateRelease: Release = { + id: 2, + upload_url: 'duplicate-upload', + html_url: 'duplicate-html', + tag_name: 'v1.0.0', + name: 'duplicate', + body: 'test', + target_commitish: 'main', + draft: true, + prerelease: false, + assets: [], + }; + + let lookupCount = 0; + const deleteReleaseSpy = vi.fn(async () => undefined); + const mockReleaser: Releaser = { + getReleaseByTag: () => { + lookupCount += 1; + if (lookupCount === 1) { + return Promise.reject({ status: 404 }); + } + return Promise.resolve({ data: canonicalRelease }); + }, + createRelease: () => Promise.resolve({ data: duplicateRelease }), + updateRelease: () => Promise.reject('Not implemented'), + finalizeRelease: () => Promise.reject('Not implemented'), + allReleases: async function* () { + yield { data: [canonicalRelease] }; + }, + listReleaseAssets: () => Promise.reject('Not implemented'), + deleteReleaseAsset: () => Promise.reject('Not implemented'), + deleteRelease: deleteReleaseSpy, + updateReleaseAsset: () => Promise.reject('Not implemented'), + uploadReleaseAsset: () => Promise.reject('Not implemented'), + }; + + const result = await release(config, mockReleaser, 2); + + assert.equal(result.release.id, canonicalRelease.id); + assert.equal(result.created, false); + expect(deleteReleaseSpy).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + release_id: duplicateRelease.id, + }); + }); }); describe('upload', () => { diff --git a/dist/index.js b/dist/index.js index 3d86a1a..6e7cbdd 100644 --- a/dist/index.js +++ b/dist/index.js @@ -56,7 +56,7 @@ ${m.message} ${JSON.stringify(m.errors)}`);if(m.name&&m.name!==a&&m.id){console.log(`\u270F\uFE0F Restoring asset label to ${a}...`);try{let{data:d}=await t.updateReleaseAsset({owner:o,repo:n,asset_id:m.id,name:m.name,label:a});return console.log(`\u2705 Uploaded ${a}`),d}catch(d){console.warn(`error updating release asset label for ${a}: ${d}`)}}return console.log(`\u2705 Uploaded ${a}`),m}catch(E){let m=E?.status??E?.response?.status,d=E?.response?.data;if(e.input_overwrite_files!==!1&&m===422&&d?.errors?.[0]?.code==="already_exists"&&l!==void 0){console.log(`\u26A0\uFE0F Asset ${a} already exists (race condition), refreshing assets and retrying once...`);let C=(await t.listReleaseAssets({owner:o,repo:n,release_id:l})).find(({name:B})=>B==ap(a));if(C){await t.deleteReleaseAsset({owner:o,repo:n,asset_id:C.id});let B=await h(),b=B.data;if(B.status!==201)throw new Error(`Failed to upload release asset ${a}. received status code ${B.status} ${b.message} ${JSON.stringify(b.errors)}`);return console.log(`\u2705 Uploaded ${a}`),b}}throw E}},Ap=async(e,t,s=3)=>{if(s<=0)throw console.log("\u274C Too many retries. Aborting..."),new Error("Too many retries.");let[r,i]=e.github_repository.split("/"),o=e.input_tag_name||(ja(e.github_ref)?e.github_ref.replace("refs/tags/",""):""),n=e.input_discussion_category_name,a=e.input_generate_release_notes;try{let A=await kw(t,r,i,o);if(A===void 0)return await Iw(o,e,t,r,i,n,a,s);let c=A;console.log(`Found release ${c.name} (with id=${c.id})`);let u=c.id,l;e.input_target_commitish&&e.input_target_commitish!==c.target_commitish?(console.log(`Updating commit from "${c.target_commitish}" to "${e.input_target_commitish}"`),l=e.input_target_commitish):l=c.target_commitish;let p=o,g=e.input_name||c.name||o,h=np(e)||"",E=c.body||"",m;e.input_append_body&&h&&E?m=E+` -`+h:m=h||E;let d=e.input_prerelease!==void 0?e.input_prerelease:c.prerelease,f=e.input_make_latest;return{release:(await t.updateRelease({owner:r,repo:i,release_id:u,tag_name:p,target_commitish:l,name:g,body:m,draft:c.draft,prerelease:d,discussion_category_name:n,generate_release_notes:a,make_latest:f})).data,created:!1}}catch(A){if(A.status!==404)throw console.log(`\u26A0\uFE0F Unexpected error fetching GitHub release for tag ${e.github_ref}: ${A}`),A;return await Iw(o,e,t,r,i,n,a,s)}},cp=async(e,t,s,r=!1,i=3)=>{if(e.input_draft===!0||s.draft===!1)return s;if(i<=0)throw console.log("\u274C Too many retries. Aborting..."),new Error("Too many retries.");let[o,n]=e.github_repository.split("/");try{let{data:a}=await t.finalizeRelease({owner:o,repo:n,release_id:s.id,make_latest:e.input_make_latest});return a}catch(a){if(console.warn(`error finalizing release: ${a}`),r&&s.draft&&L_(a)){let A=!1;try{console.log(`\u{1F9F9} Deleting draft release ${s.id} for tag ${s.tag_name} because tag creation is blocked by repository rules...`),await t.deleteRelease({owner:o,repo:n,release_id:s.id}),A=!0}catch(u){console.warn(`error deleting orphan draft release ${s.id}: ${u}`)}let c=A?`Deleted draft release ${s.id} to avoid leaving an orphaned draft release.`:`Failed to delete draft release ${s.id}; manual cleanup may still be required.`;throw new Error(`Tag creation for ${s.tag_name} is blocked by repository rules. ${c}`)}return console.log(`retrying... (${i-1} retries remaining)`),cp(e,t,s,r,i-1)}},lp=async(e,t,s,r=3)=>{if(r<=0)throw console.log("\u274C Too many retries. Aborting..."),new Error("Too many retries.");let[i,o]=e.github_repository.split("/");try{return await t.listReleaseAssets({owner:i,repo:o,release_id:s.id})}catch(n){return console.warn(`error listing assets of release: ${n}`),console.log(`retrying... (${r-1} retries remaining)`),lp(e,t,s,r-1)}};async function kw(e,t,s,r){try{let{data:i}=await e.getReleaseByTag({owner:t,repo:s,tag:r});return i}catch(i){if(i.status===404)return;throw i}}var T_=1e3,F_=2;async function S_(e){await new Promise(t=>setTimeout(t,e))}async function U_(e,t,s,r){let i=[],o=0;for await(let n of e.allReleases({owner:t,repo:s}))if(i.push(...n.data.filter(a=>a.tag_name===r)),o+=1,o>=F_)break;return i}function N_(e,t){return t&&e.some(s=>s.id===t.id)||e.length===0?t:[...e].sort((s,r)=>s.draft!==r.draft?Number(s.draft)-Number(r.draft):s.id-r.id)[0]}async function G_(e,t,s,r,i,o){for(let n of o)if(!(n.id===i||!n.draft||n.assets.length>0))try{console.log(`\u{1F9F9} Removing duplicate draft release ${n.id} for tag ${r}...`),await e.deleteRelease({owner:t,repo:s,release_id:n.id})}catch(a){console.warn(`error deleting duplicate release ${n.id}: ${a}`)}}async function M_(e,t,s,r,i,o){let n=Math.max(o,1);for(let a=1;a<=n;a+=1){let A;try{A=await kw(e,t,s,r)}catch(l){console.warn(`error reloading release for tag ${r}: ${l}`)}let c=[];try{c=await U_(e,t,s,r)}catch(l){console.warn(`error listing recent releases for tag ${r}: ${l}`)}let u=N_(c,A);if(u)return u.id!==i.id&&console.log(`\u21AA\uFE0F Using release ${u.id} for tag ${r} instead of duplicate draft ${i.id}`),await G_(e,t,s,r,u.id,c),u;as==="pre_receive"&&typeof r=="string"&&r.includes("creations being restricted"))}var Dw=require("process");async function __(){try{let e=Qw(Dw.env);if(!e.input_tag_name&&!ja(e.github_ref)&&!e.input_draft)throw new Error("\u26A0\uFE0F GitHub Releases requires a tag");if(e.input_files){let a=Cw(e.input_files,e.input_working_directory);if(a.forEach(A=>{if(e.input_fail_on_unmatched_files)throw new Error(`\u26A0\uFE0F Pattern '${A}' does not match any files.`);console.warn(`\u{1F914} Pattern '${A}' does not match any files.`)}),a.length>0&&e.input_fail_on_unmatched_files)throw new Error("\u26A0\uFE0F There were unmatched files")}let t=jC(e.github_token,{throttle:{onRateLimit:(a,A)=>{if(console.warn(`Request quota exhausted for request ${A.method} ${A.url}`),A.request.retryCount===0)return console.log(`Retrying after ${a} seconds!`),!0},onAbuseLimit:(a,A)=>{console.warn(`Abuse detected for request ${A.method} ${A.url}`)}}}),s=new za(t),r=await Ap(e,s),i=r.release,o=r.created,n=new Set;if(e.input_files&&e.input_files.length>0){let a=Bw(e.input_files,e.input_working_directory);if(a.length==0){if(e.input_fail_on_unmatched_files)throw new Error(`\u26A0\uFE0F ${e.input_files} does not include a valid file.`);console.warn(`\u{1F914} ${e.input_files} does not include a valid file.`)}let A=i.assets,c=async l=>{let p=await vw(e,s,fw(i.upload_url),l,A);return p?p.id:void 0},u;if(!e.input_preserve_order)u=await Promise.all(a.map(c));else{u=[];for(let l of a)u.push(await c(l))}n=new Set(u.filter(l=>l!==void 0))}console.log("Finalizing release..."),i=await cp(e,s,i,o),console.log("Getting assets list...");{let a=[];n.size>0&&(a=(await lp(e,s,i)).filter(c=>n.has(c.id)).map(c=>{let{uploader:u,...l}=c;return l})),$i("assets",a)}console.log(`\u{1F389} Release ready at ${i.html_url}`),$i("url",i.html_url),$i("id",i.id.toString()),$i("upload_url",i.upload_url)}catch(e){XB(e.message)}}__(); +`+h:m=h||E;let d=e.input_prerelease!==void 0?e.input_prerelease:c.prerelease,f=e.input_make_latest;return{release:(await t.updateRelease({owner:r,repo:i,release_id:u,tag_name:p,target_commitish:l,name:g,body:m,draft:c.draft,prerelease:d,discussion_category_name:n,generate_release_notes:a,make_latest:f})).data,created:!1}}catch(A){if(A.status!==404)throw console.log(`\u26A0\uFE0F Unexpected error fetching GitHub release for tag ${e.github_ref}: ${A}`),A;return await Iw(o,e,t,r,i,n,a,s)}},cp=async(e,t,s,r=!1,i=3)=>{if(e.input_draft===!0||s.draft===!1)return s;if(i<=0)throw console.log("\u274C Too many retries. Aborting..."),new Error("Too many retries.");let[o,n]=e.github_repository.split("/");try{let{data:a}=await t.finalizeRelease({owner:o,repo:n,release_id:s.id,make_latest:e.input_make_latest});return a}catch(a){if(console.warn(`error finalizing release: ${a}`),r&&s.draft&&L_(a)){let A=!1;try{console.log(`\u{1F9F9} Deleting draft release ${s.id} for tag ${s.tag_name} because tag creation is blocked by repository rules...`),await t.deleteRelease({owner:o,repo:n,release_id:s.id}),A=!0}catch(u){console.warn(`error deleting orphan draft release ${s.id}: ${u}`)}let c=A?`Deleted draft release ${s.id} to avoid leaving an orphaned draft release.`:`Failed to delete draft release ${s.id}; manual cleanup may still be required.`;throw new Error(`Tag creation for ${s.tag_name} is blocked by repository rules. ${c}`)}return console.log(`retrying... (${i-1} retries remaining)`),cp(e,t,s,r,i-1)}},lp=async(e,t,s,r=3)=>{if(r<=0)throw console.log("\u274C Too many retries. Aborting..."),new Error("Too many retries.");let[i,o]=e.github_repository.split("/");try{return await t.listReleaseAssets({owner:i,repo:o,release_id:s.id})}catch(n){return console.warn(`error listing assets of release: ${n}`),console.log(`retrying... (${r-1} retries remaining)`),lp(e,t,s,r-1)}};async function kw(e,t,s,r){try{let{data:i}=await e.getReleaseByTag({owner:t,repo:s,tag:r});return i}catch(i){if(i.status===404)return;throw i}}var T_=1e3,F_=2;async function S_(e){await new Promise(t=>setTimeout(t,e))}async function U_(e,t,s,r){let i=[],o=0;for await(let n of e.allReleases({owner:t,repo:s}))if(i.push(...n.data.filter(a=>a.tag_name===r)),o+=1,o>=F_)break;return i}function N_(e,t){return t&&e.some(s=>s.id===t.id)||e.length===0?t:[...e].sort((s,r)=>s.draft!==r.draft?Number(s.draft)-Number(r.draft):s.id-r.id)[0]}async function G_(e,t,s,r,i,o){let n=Array.from(new Map(o.map(a=>[a.id,a])).values());for(let a of n)if(!(a.id===i||!a.draft||a.assets.length>0))try{console.log(`\u{1F9F9} Removing duplicate draft release ${a.id} for tag ${r}...`),await e.deleteRelease({owner:t,repo:s,release_id:a.id})}catch(A){console.warn(`error deleting duplicate release ${a.id}: ${A}`)}}async function M_(e,t,s,r,i,o){let n=Math.max(o,1);for(let a=1;a<=n;a+=1){let A;try{A=await kw(e,t,s,r)}catch(l){console.warn(`error reloading release for tag ${r}: ${l}`)}let c=[];try{c=await U_(e,t,s,r)}catch(l){console.warn(`error listing recent releases for tag ${r}: ${l}`)}let u=N_(c,A);if(u)return u.id!==i.id&&console.log(`\u21AA\uFE0F Using release ${u.id} for tag ${r} instead of duplicate draft ${i.id}`),await G_(e,t,s,r,u.id,[i,...c]),u;as==="pre_receive"&&typeof r=="string"&&r.includes("creations being restricted"))}var Dw=require("process");async function __(){try{let e=Qw(Dw.env);if(!e.input_tag_name&&!ja(e.github_ref)&&!e.input_draft)throw new Error("\u26A0\uFE0F GitHub Releases requires a tag");if(e.input_files){let a=Cw(e.input_files,e.input_working_directory);if(a.forEach(A=>{if(e.input_fail_on_unmatched_files)throw new Error(`\u26A0\uFE0F Pattern '${A}' does not match any files.`);console.warn(`\u{1F914} Pattern '${A}' does not match any files.`)}),a.length>0&&e.input_fail_on_unmatched_files)throw new Error("\u26A0\uFE0F There were unmatched files")}let t=jC(e.github_token,{throttle:{onRateLimit:(a,A)=>{if(console.warn(`Request quota exhausted for request ${A.method} ${A.url}`),A.request.retryCount===0)return console.log(`Retrying after ${a} seconds!`),!0},onAbuseLimit:(a,A)=>{console.warn(`Abuse detected for request ${A.method} ${A.url}`)}}}),s=new za(t),r=await Ap(e,s),i=r.release,o=r.created,n=new Set;if(e.input_files&&e.input_files.length>0){let a=Bw(e.input_files,e.input_working_directory);if(a.length==0){if(e.input_fail_on_unmatched_files)throw new Error(`\u26A0\uFE0F ${e.input_files} does not include a valid file.`);console.warn(`\u{1F914} ${e.input_files} does not include a valid file.`)}let A=i.assets,c=async l=>{let p=await vw(e,s,fw(i.upload_url),l,A);return p?p.id:void 0},u;if(!e.input_preserve_order)u=await Promise.all(a.map(c));else{u=[];for(let l of a)u.push(await c(l))}n=new Set(u.filter(l=>l!==void 0))}console.log("Finalizing release..."),i=await cp(e,s,i,o),console.log("Getting assets list...");{let a=[];n.size>0&&(a=(await lp(e,s,i)).filter(c=>n.has(c.id)).map(c=>{let{uploader:u,...l}=c;return l})),$i("assets",a)}console.log(`\u{1F389} Release ready at ${i.html_url}`),$i("url",i.html_url),$i("id",i.id.toString()),$i("upload_url",i.upload_url)}catch(e){XB(e.message)}}__(); /*! Bundled license information: undici/lib/web/fetch/body.js: diff --git a/src/github.ts b/src/github.ts index f0c7440..e148cd6 100644 --- a/src/github.ts +++ b/src/github.ts @@ -707,9 +707,13 @@ async function cleanupDuplicateDraftReleases( repo: string, tag: string, canonicalReleaseId: number, - recentReleases: Release[], + releases: Release[], ): Promise { - for (const duplicate of recentReleases) { + const uniqueReleases = Array.from( + new Map(releases.map((release) => [release.id, release])).values(), + ); + + for (const duplicate of uniqueReleases) { if (duplicate.id === canonicalReleaseId || !duplicate.draft || duplicate.assets.length > 0) { continue; } @@ -760,14 +764,10 @@ async function canonicalizeCreatedRelease( ); } - await cleanupDuplicateDraftReleases( - releaser, - owner, - repo, - tag, - canonicalRelease.id, - recentReleases, - ); + await cleanupDuplicateDraftReleases(releaser, owner, repo, tag, canonicalRelease.id, [ + createdRelease, + ...recentReleases, + ]); return canonicalRelease; }