mirror of
https://github.com/softprops/action-gh-release.git
synced 2026-03-23 11:59:56 +08:00
fix: clarify immutable prerelease uploads (#763)
* fix: draft prereleases before uploading assets Signed-off-by: Rui Chen <rui@chenrui.dev> * fix: clarify immutable prerelease uploads Signed-off-by: Rui Chen <rui@chenrui.dev> --------- Signed-off-by: Rui Chen <rui@chenrui.dev>
This commit is contained in:
@@ -240,6 +240,12 @@ will retain its original info.
|
|||||||
existing draft release, set `draft: true` to keep it draft; if `draft` is omitted,
|
existing draft release, set `draft: true` to keep it draft; if `draft` is omitted,
|
||||||
the action will publish that draft after uploading assets.
|
the action will publish that draft after uploading assets.
|
||||||
|
|
||||||
|
💡 GitHub immutable releases lock assets after publication. Standard releases in this
|
||||||
|
action already upload assets before publishing, but prereleases stay published by
|
||||||
|
default so `release.prereleased` workflows keep firing. On an immutable-release
|
||||||
|
repository, use `draft: true` for prereleases that upload assets, then publish that
|
||||||
|
draft later and subscribe downstream workflows to `release.published`.
|
||||||
|
|
||||||
💡 `files` is glob-based, so literal filenames that contain glob metacharacters such as
|
💡 `files` is glob-based, so literal filenames that contain glob metacharacters such as
|
||||||
`[` or `]` must be escaped in the pattern.
|
`[` or `]` must be escaped in the pattern.
|
||||||
|
|
||||||
|
|||||||
@@ -614,6 +614,53 @@ describe('github', () => {
|
|||||||
expect(uploadReleaseAsset).toHaveBeenCalledTimes(2);
|
expect(uploadReleaseAsset).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('surfaces an actionable immutable-release error for prerelease uploads', async () => {
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'gh-release-immutable-'));
|
||||||
|
const assetPath = join(tempDir, 'draft-false.txt');
|
||||||
|
writeFileSync(assetPath, 'hello');
|
||||||
|
|
||||||
|
const uploadReleaseAsset = vi.fn().mockRejectedValue({
|
||||||
|
status: 422,
|
||||||
|
response: {
|
||||||
|
data: {
|
||||||
|
message: 'Cannot upload assets to an immutable release.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockReleaser: Releaser = {
|
||||||
|
getReleaseByTag: () => Promise.reject('Not implemented'),
|
||||||
|
createRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateRelease: () => Promise.reject('Not implemented'),
|
||||||
|
finalizeRelease: () => Promise.reject('Not implemented'),
|
||||||
|
allReleases: async function* () {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
listReleaseAssets: () => Promise.resolve([]),
|
||||||
|
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
|
deleteRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
|
uploadReleaseAsset,
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
upload(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
input_prerelease: true,
|
||||||
|
},
|
||||||
|
mockReleaser,
|
||||||
|
'https://uploads.github.com/repos/owner/repo/releases/1/assets',
|
||||||
|
assetPath,
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
).rejects.toThrow(
|
||||||
|
'Cannot upload asset draft-false.txt to an immutable release. GitHub only allows asset uploads before a release is published, but draft prereleases publish with the release.published event instead of release.prereleased.',
|
||||||
|
);
|
||||||
|
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
it('retries upload after deleting a conflicting renamed asset matched by label', async () => {
|
it('retries upload after deleting a conflicting renamed asset matched by label', async () => {
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), 'gh-release-race-dotfile-'));
|
const tempDir = mkdtempSync(join(tmpdir(), 'gh-release-race-dotfile-'));
|
||||||
const dotfilePath = join(tempDir, '.config');
|
const dotfilePath = join(tempDir, '.config');
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ inputs:
|
|||||||
description: "Gives a tag name. Defaults to github.ref_name. refs/tags/<name> values are normalized to <name>."
|
description: "Gives a tag name. Defaults to github.ref_name. refs/tags/<name> values are normalized to <name>."
|
||||||
required: false
|
required: false
|
||||||
draft:
|
draft:
|
||||||
description: "Keeps the release as a draft. Defaults to false. When reusing an existing draft release, set this to true to keep it draft; omit it to publish after upload."
|
description: "Keeps the release as a draft. Defaults to false. When reusing an existing draft release, set this to true to keep it draft; omit it to publish after upload. On immutable-release repositories, use this for prereleases that upload assets and publish the draft later."
|
||||||
required: false
|
required: false
|
||||||
prerelease:
|
prerelease:
|
||||||
description: "Identify the release as a prerelease. Defaults to false"
|
description: "Identify the release as a prerelease. Defaults to false"
|
||||||
|
|||||||
28
dist/index.js
vendored
28
dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -283,6 +283,21 @@ const isReleaseAssetUpdateNotFound = (error: any): boolean => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isImmutableReleaseAssetUploadFailure = (error: any): boolean => {
|
||||||
|
const errorStatus = error?.status ?? error?.response?.status;
|
||||||
|
const errorMessage = error?.response?.data?.message ?? error?.message;
|
||||||
|
|
||||||
|
return errorStatus === 422 && /immutable release/i.test(String(errorMessage));
|
||||||
|
};
|
||||||
|
|
||||||
|
const immutableReleaseAssetUploadMessage = (
|
||||||
|
name: string,
|
||||||
|
prerelease: boolean | undefined,
|
||||||
|
): string =>
|
||||||
|
prerelease
|
||||||
|
? `Cannot upload asset ${name} to an immutable release. GitHub only allows asset uploads before a release is published, but draft prereleases publish with the release.published event instead of release.prereleased. If you need prereleases with assets on an immutable-release repository, keep the release as a draft with draft: true, then publish it later from that draft and subscribe downstream workflows to release.published.`
|
||||||
|
: `Cannot upload asset ${name} to an immutable release. GitHub only allows asset uploads before a release is published, so upload assets to a draft release before you publish it.`;
|
||||||
|
|
||||||
export const upload = async (
|
export const upload = async (
|
||||||
config: Config,
|
config: Config,
|
||||||
releaser: Releaser,
|
releaser: Releaser,
|
||||||
@@ -423,6 +438,10 @@ export const upload = async (
|
|||||||
const errorStatus = error?.status ?? error?.response?.status;
|
const errorStatus = error?.status ?? error?.response?.status;
|
||||||
const errorData = error?.response?.data;
|
const errorData = error?.response?.data;
|
||||||
|
|
||||||
|
if (isImmutableReleaseAssetUploadFailure(error)) {
|
||||||
|
throw new Error(immutableReleaseAssetUploadMessage(name, config.input_prerelease));
|
||||||
|
}
|
||||||
|
|
||||||
if (releaseId !== undefined && isReleaseAssetUpdateNotFound(error)) {
|
if (releaseId !== undefined && isReleaseAssetUpdateNotFound(error)) {
|
||||||
try {
|
try {
|
||||||
const latestAsset = await findReleaseAsset((currentAsset) =>
|
const latestAsset = await findReleaseAsset((currentAsset) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user