mirror of
https://gh-proxy.org/https://github.com/softprops/action-gh-release.git
synced 2026-07-17 16:13:01 +08:00
fix: publish existing draft releases as prereleases (#801)
* fix: respect input_draft when updating existing release * fix: reuse draft releases before publishing Signed-off-by: Rui Chen <rui@chenrui.dev> * fix: bound and canonicalize draft discovery Signed-off-by: Rui Chen <rui@chenrui.dev> * fix: preserve unowned draft releases Signed-off-by: Rui Chen <rui@chenrui.dev> * fix: recheck duplicate drafts before cleanup Signed-off-by: Rui Chen <rui@chenrui.dev> --------- Signed-off-by: Rui Chen <rui@chenrui.dev> Co-authored-by: Rui Chen <rui@chenrui.dev>
This commit is contained in:
+81
-47
@@ -531,23 +531,31 @@ export const release = async (
|
||||
if (generate_release_notes && previous_tag_name) {
|
||||
console.log(`📝 Generating release notes using previous tag ${previous_tag_name}`);
|
||||
}
|
||||
let _release: Release | undefined;
|
||||
try {
|
||||
const _release: Release | undefined = await findTagFromReleases(releaser, owner, repo, tag);
|
||||
_release = await findTagFromReleases(releaser, owner, repo, tag, maxRetries);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (_release === undefined) {
|
||||
return await createRelease(
|
||||
tag,
|
||||
config,
|
||||
releaser,
|
||||
owner,
|
||||
repo,
|
||||
discussion_category_name,
|
||||
generate_release_notes,
|
||||
maxRetries,
|
||||
previous_tag_name,
|
||||
);
|
||||
}
|
||||
if (_release === undefined) {
|
||||
return await createRelease(
|
||||
tag,
|
||||
config,
|
||||
releaser,
|
||||
owner,
|
||||
repo,
|
||||
discussion_category_name,
|
||||
generate_release_notes,
|
||||
maxRetries,
|
||||
previous_tag_name,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
let existingRelease: Release = _release!;
|
||||
console.log(`Found release ${existingRelease.name} (with id=${existingRelease.id})`);
|
||||
|
||||
@@ -733,14 +741,16 @@ export const listReleaseAssets = async (
|
||||
/**
|
||||
* Finds a release by tag name.
|
||||
*
|
||||
* Uses the direct getReleaseByTag API for O(1) lookup instead of iterating
|
||||
* through all releases. This also avoids GitHub's API pagination limit of
|
||||
* 10000 results which would cause failures for repositories with many releases.
|
||||
* Uses the direct getReleaseByTag API for O(1) lookup. Because GitHub does not
|
||||
* expose draft releases through that endpoint, a 404 falls back to a bounded
|
||||
* scan of recent releases and briefly retries in case the listing is not yet
|
||||
* consistent.
|
||||
*
|
||||
* @param releaser - The GitHub API wrapper for release operations
|
||||
* @param owner - The owner of the repository
|
||||
* @param repo - The name of the repository
|
||||
* @param tag - The tag name to search for
|
||||
* @param listingAttempts - The maximum number of listing attempts after a direct 404
|
||||
* @returns The release with the given tag name, or undefined if no release with that tag name is found
|
||||
*/
|
||||
export async function findTagFromReleases(
|
||||
@@ -748,18 +758,35 @@ export async function findTagFromReleases(
|
||||
owner: string,
|
||||
repo: string,
|
||||
tag: string,
|
||||
listingAttempts: number = 1,
|
||||
): Promise<Release | undefined> {
|
||||
try {
|
||||
const { data: release } = await releaser.getReleaseByTag({ owner, repo, tag });
|
||||
return release;
|
||||
} catch (error) {
|
||||
// Release not found (404) or other error - return undefined to allow creation
|
||||
if (error.status === 404) {
|
||||
return undefined;
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
// Re-throw unexpected errors
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (listingAttempts <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const attempts = Math.max(1, listingAttempts);
|
||||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||
const recentReleases = await recentReleasesByTag(releaser, owner, repo, tag);
|
||||
const canonicalRelease = pickCanonicalRelease(recentReleases, undefined);
|
||||
if (canonicalRelease) {
|
||||
return canonicalRelease;
|
||||
}
|
||||
|
||||
if (attempt < attempts - 1) {
|
||||
await sleep(CREATED_RELEASE_DISCOVERY_RETRY_DELAY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const CREATED_RELEASE_DISCOVERY_RETRY_DELAY_MS = 1000;
|
||||
@@ -811,33 +838,31 @@ function pickCanonicalRelease(
|
||||
})[0];
|
||||
}
|
||||
|
||||
async function cleanupDuplicateDraftReleases(
|
||||
async function cleanupCreatedDuplicateDraftRelease(
|
||||
releaser: Releaser,
|
||||
owner: string,
|
||||
repo: string,
|
||||
tag: string,
|
||||
canonicalReleaseId: number,
|
||||
releases: Release[],
|
||||
createdRelease: Release,
|
||||
): Promise<void> {
|
||||
const uniqueReleases = Array.from(
|
||||
new Map(releases.map((release) => [release.id, release])).values(),
|
||||
);
|
||||
if (
|
||||
createdRelease.id === canonicalReleaseId ||
|
||||
!createdRelease.draft ||
|
||||
createdRelease.assets.length > 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const duplicate of uniqueReleases) {
|
||||
if (duplicate.id === canonicalReleaseId || !duplicate.draft || duplicate.assets.length > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🧹 Removing duplicate draft release ${duplicate.id} for tag ${tag}...`);
|
||||
await releaser.deleteRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: duplicate.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`error deleting duplicate release ${duplicate.id}: ${error}`);
|
||||
}
|
||||
try {
|
||||
console.log(`🧹 Removing duplicate draft release ${createdRelease.id} for tag ${tag}...`);
|
||||
await releaser.deleteRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: createdRelease.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`error deleting duplicate release ${createdRelease.id}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,7 +879,7 @@ async function canonicalizeCreatedRelease(
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
let releaseByTag: Release | undefined;
|
||||
try {
|
||||
releaseByTag = await findTagFromReleases(releaser, owner, repo, tag);
|
||||
releaseByTag = await findTagFromReleases(releaser, owner, repo, tag, 0);
|
||||
} catch (error) {
|
||||
console.warn(`error reloading release for tag ${tag}: ${error}`);
|
||||
}
|
||||
@@ -874,10 +899,19 @@ async function canonicalizeCreatedRelease(
|
||||
);
|
||||
}
|
||||
|
||||
await cleanupDuplicateDraftReleases(releaser, owner, repo, tag, canonicalRelease.id, [
|
||||
createdRelease,
|
||||
...recentReleases,
|
||||
]);
|
||||
const refreshedCreatedRelease = recentReleases.find(
|
||||
(release) => release.id === createdRelease.id,
|
||||
);
|
||||
if (refreshedCreatedRelease) {
|
||||
await cleanupCreatedDuplicateDraftRelease(
|
||||
releaser,
|
||||
owner,
|
||||
repo,
|
||||
tag,
|
||||
canonicalRelease.id,
|
||||
refreshedCreatedRelease,
|
||||
);
|
||||
}
|
||||
return canonicalRelease;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user