mirror of
https://gh-proxy.org/https://github.com/softprops/action-gh-release.git
synced 2026-07-20 10:03:01 +08:00
feat: improve release error reporting and test coverage (#813)
Signed-off-by: Rui Chen <rui@chenrui.dev>
This commit is contained in:
+10
-6
@@ -3,7 +3,7 @@ import { statSync } from 'fs';
|
||||
import { open } from 'fs/promises';
|
||||
import { lookup } from 'mime-types';
|
||||
import { basename } from 'path';
|
||||
import { alignAssetName, Config, isTag, normalizeTagName, releaseBody } from './util';
|
||||
import { alignAssetName, Config, errorMessage, isTag, normalizeTagName, releaseBody } from './util';
|
||||
|
||||
type GitHub = InstanceType<typeof GitHub>;
|
||||
|
||||
@@ -933,12 +933,16 @@ async function createRelease(
|
||||
release: canonicalRelease,
|
||||
created: canonicalRelease.id === createdRelease.data.id,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
const githubError = error as {
|
||||
status?: number;
|
||||
response?: { data?: { errors?: Array<{ code?: string }> } };
|
||||
};
|
||||
// presume a race with competing matrix runs
|
||||
console.log(`⚠️ GitHub release failed with status: ${error.status}`);
|
||||
console.log(`${JSON.stringify(error.response.data)}`);
|
||||
console.log(`⚠️ GitHub release failed with status: ${githubError.status}`);
|
||||
console.log(errorMessage(error));
|
||||
|
||||
switch (error.status) {
|
||||
switch (githubError.status) {
|
||||
case 403:
|
||||
console.log(
|
||||
'Skip retry — your GitHub token/PAT does not have the required permission to create a release',
|
||||
@@ -951,7 +955,7 @@ async function createRelease(
|
||||
|
||||
case 422:
|
||||
// Check if this is a race condition with "already_exists" error
|
||||
const errorData = error.response?.data;
|
||||
const errorData = githubError.response?.data;
|
||||
if (errorData?.errors?.[0]?.code === 'already_exists') {
|
||||
console.log(
|
||||
'⚠️ Release already exists (race condition detected), retrying to find and update existing release...',
|
||||
|
||||
+2
-113
@@ -1,114 +1,3 @@
|
||||
import { setFailed, setOutput } from '@actions/core';
|
||||
import { getOctokit } from '@actions/github';
|
||||
import { GitHubReleaser, release, finalizeRelease, upload, listReleaseAssets } from './github';
|
||||
import { isTag, parseConfig, paths, unmatchedPatterns, uploadUrl } from './util';
|
||||
import { run } from './run';
|
||||
|
||||
import { env } from 'process';
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const config = parseConfig(env);
|
||||
if (!config.input_tag_name && !isTag(config.github_ref) && !config.input_draft) {
|
||||
throw new Error(`⚠️ GitHub Releases requires a tag`);
|
||||
}
|
||||
if (config.input_files) {
|
||||
const patterns = unmatchedPatterns(config.input_files, config.input_working_directory);
|
||||
patterns.forEach((pattern) => {
|
||||
if (config.input_fail_on_unmatched_files) {
|
||||
throw new Error(`⚠️ Pattern '${pattern}' does not match any files.`);
|
||||
} else {
|
||||
console.warn(`🤔 Pattern '${pattern}' does not match any files.`);
|
||||
}
|
||||
});
|
||||
if (patterns.length > 0 && config.input_fail_on_unmatched_files) {
|
||||
throw new Error(`⚠️ There were unmatched files`);
|
||||
}
|
||||
}
|
||||
|
||||
// const oktokit = GitHub.plugin(
|
||||
// require("@octokit/plugin-throttling"),
|
||||
// require("@octokit/plugin-retry")
|
||||
// );
|
||||
|
||||
const gh = getOctokit(config.github_token, {
|
||||
//new oktokit(
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter, options) => {
|
||||
console.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
|
||||
if (options.request.retryCount === 0) {
|
||||
// only retries once
|
||||
console.log(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onAbuseLimit: (retryAfter, options) => {
|
||||
// does not retry, only logs a warning
|
||||
console.warn(`Abuse detected for request ${options.method} ${options.url}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
//);
|
||||
const releaser = new GitHubReleaser(gh);
|
||||
const releaseResult = await release(config, releaser);
|
||||
let rel = releaseResult.release;
|
||||
const releaseWasCreated = releaseResult.created;
|
||||
let uploadedAssetIds: Set<number> = new Set();
|
||||
if (config.input_files && config.input_files.length > 0) {
|
||||
const files = paths(config.input_files, config.input_working_directory);
|
||||
if (files.length == 0) {
|
||||
if (config.input_fail_on_unmatched_files) {
|
||||
throw new Error(`⚠️ ${config.input_files} does not include a valid file.`);
|
||||
} else {
|
||||
console.warn(`🤔 ${config.input_files} does not include a valid file.`);
|
||||
}
|
||||
}
|
||||
const currentAssets = rel.assets;
|
||||
|
||||
const uploadFile = async (path: string) => {
|
||||
const json = await upload(config, releaser, uploadUrl(rel.upload_url), path, currentAssets);
|
||||
return json ? (json.id as number) : undefined;
|
||||
};
|
||||
|
||||
let results: (number | undefined)[];
|
||||
if (!config.input_preserve_order) {
|
||||
results = await Promise.all(files.map(uploadFile));
|
||||
} else {
|
||||
results = [];
|
||||
for (const path of files) {
|
||||
results.push(await uploadFile(path));
|
||||
}
|
||||
}
|
||||
|
||||
uploadedAssetIds = new Set(results.filter((id): id is number => id !== undefined));
|
||||
}
|
||||
|
||||
console.log('Finalizing release...');
|
||||
rel = await finalizeRelease(config, releaser, rel, releaseWasCreated);
|
||||
|
||||
// Draft releases use temporary "untagged-..." URLs for assets.
|
||||
// URLs will be changed to correct ones once the release is published.
|
||||
console.log('Getting assets list...');
|
||||
{
|
||||
let assets: any[] = [];
|
||||
if (uploadedAssetIds.size > 0) {
|
||||
const updatedAssets = await listReleaseAssets(config, releaser, rel);
|
||||
assets = updatedAssets
|
||||
.filter((a) => uploadedAssetIds.has(a.id))
|
||||
.map((a) => {
|
||||
const { uploader, ...rest } = a;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
setOutput('assets', assets);
|
||||
}
|
||||
|
||||
console.log(`🎉 Release ready at ${rel.html_url}`);
|
||||
setOutput('url', rel.html_url);
|
||||
setOutput('id', rel.id.toString());
|
||||
setOutput('upload_url', rel.upload_url);
|
||||
} catch (error) {
|
||||
setFailed(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
void run();
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { setFailed, setOutput } from '@actions/core';
|
||||
import { getOctokit } from '@actions/github';
|
||||
import { env } from 'process';
|
||||
import { GitHubReleaser, release, finalizeRelease, upload, listReleaseAssets } from './github';
|
||||
import { errorMessage, isTag, parseConfig, paths, unmatchedPatterns, uploadUrl } from './util';
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const config = parseConfig(env);
|
||||
if (!config.input_tag_name && !isTag(config.github_ref) && !config.input_draft) {
|
||||
throw new Error(`⚠️ GitHub Releases requires a tag`);
|
||||
}
|
||||
if (config.input_files) {
|
||||
const patterns = unmatchedPatterns(config.input_files, config.input_working_directory);
|
||||
patterns.forEach((pattern) => {
|
||||
if (config.input_fail_on_unmatched_files) {
|
||||
throw new Error(`⚠️ Pattern '${pattern}' does not match any files.`);
|
||||
} else {
|
||||
console.warn(`🤔 Pattern '${pattern}' does not match any files.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const gh = getOctokit(config.github_token, {
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter, options) => {
|
||||
console.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
|
||||
if (options.request.retryCount === 0) {
|
||||
console.log(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onAbuseLimit: (_retryAfter, options) => {
|
||||
console.warn(`Abuse detected for request ${options.method} ${options.url}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
const releaser = new GitHubReleaser(gh);
|
||||
const releaseResult = await release(config, releaser);
|
||||
let rel = releaseResult.release;
|
||||
const releaseWasCreated = releaseResult.created;
|
||||
let uploadedAssetIds: Set<number> = new Set();
|
||||
if (config.input_files && config.input_files.length > 0) {
|
||||
const files = paths(config.input_files, config.input_working_directory);
|
||||
if (files.length === 0) {
|
||||
if (config.input_fail_on_unmatched_files) {
|
||||
throw new Error(`⚠️ ${config.input_files} does not include a valid file.`);
|
||||
} else {
|
||||
console.warn(`🤔 ${config.input_files} does not include a valid file.`);
|
||||
}
|
||||
}
|
||||
const currentAssets = rel.assets;
|
||||
|
||||
const uploadFile = async (path: string) => {
|
||||
const json = await upload(config, releaser, uploadUrl(rel.upload_url), path, currentAssets);
|
||||
return json ? (json.id as number) : undefined;
|
||||
};
|
||||
|
||||
let results: (number | undefined)[];
|
||||
if (!config.input_preserve_order) {
|
||||
results = await Promise.all(files.map(uploadFile));
|
||||
} else {
|
||||
results = [];
|
||||
for (const path of files) {
|
||||
results.push(await uploadFile(path));
|
||||
}
|
||||
}
|
||||
|
||||
uploadedAssetIds = new Set(results.filter((id): id is number => id !== undefined));
|
||||
}
|
||||
|
||||
console.log('Finalizing release...');
|
||||
rel = await finalizeRelease(config, releaser, rel, releaseWasCreated);
|
||||
|
||||
// Draft releases use temporary "untagged-..." URLs for assets.
|
||||
// URLs will be changed to correct ones once the release is published.
|
||||
console.log('Getting assets list...');
|
||||
let assets: any[] = [];
|
||||
if (uploadedAssetIds.size > 0) {
|
||||
const updatedAssets = await listReleaseAssets(config, releaser, rel);
|
||||
assets = updatedAssets
|
||||
.filter((asset) => uploadedAssetIds.has(asset.id))
|
||||
.map((asset) => {
|
||||
const { uploader, ...rest } = asset;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
setOutput('assets', assets);
|
||||
|
||||
console.log(`🎉 Release ready at ${rel.html_url}`);
|
||||
setOutput('url', rel.html_url);
|
||||
setOutput('id', rel.id.toString());
|
||||
setOutput('upload_url', rel.upload_url);
|
||||
} catch (error: unknown) {
|
||||
setFailed(errorMessage(error));
|
||||
}
|
||||
}
|
||||
+18
@@ -28,6 +28,24 @@ export interface Config {
|
||||
input_make_latest: 'true' | 'false' | 'legacy' | undefined;
|
||||
}
|
||||
|
||||
export const errorMessage = (error: unknown): string => {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'message' in error &&
|
||||
typeof error.message === 'string'
|
||||
) {
|
||||
return error.message;
|
||||
}
|
||||
if (error === null || error === undefined) {
|
||||
return 'Unknown error';
|
||||
}
|
||||
return String(error);
|
||||
};
|
||||
|
||||
export const uploadUrl = (url: string): string => {
|
||||
const templateMarkerPos = url.indexOf('{');
|
||||
if (templateMarkerPos > -1) {
|
||||
|
||||
Reference in New Issue
Block a user