mirror of
https://github.com/docker/build-push-action.git
synced 2026-01-20 09:28:56 +08:00
feat: Add retry mechanism with configurable attempts, wait time, and timeout
New input parameters: - max-attempts (default: 1) - Maximum number of build attempts - retry-wait-seconds (default: 0) - Delay between retry attempts - timeout-minutes (default: 0) - Timeout per attempt (0 = no timeout) Implementation: - Wraps build execution in retry loop with comprehensive logging - Adds timeout support per attempt using Promise.race() - Fully backward compatible (default values maintain current behavior) - Adds 2 test cases Signed-off-by: Mor Weinberger <test@example.com>
This commit is contained in:
parent
9e436ba9f2
commit
e4bf8b61e3
7 changed files with 201 additions and 26 deletions
|
|
@ -41,6 +41,9 @@ export interface Inputs {
|
|||
target: string;
|
||||
ulimit: string[];
|
||||
'github-token': string;
|
||||
'max-attempts': number;
|
||||
'retry-wait-seconds': number;
|
||||
'timeout-minutes': number;
|
||||
}
|
||||
|
||||
export async function getInputs(): Promise<Inputs> {
|
||||
|
|
@ -77,7 +80,10 @@ export async function getInputs(): Promise<Inputs> {
|
|||
tags: Util.getInputList('tags'),
|
||||
target: core.getInput('target'),
|
||||
ulimit: Util.getInputList('ulimit', {ignoreComma: true}),
|
||||
'github-token': core.getInput('github-token')
|
||||
'github-token': core.getInput('github-token'),
|
||||
'max-attempts': Util.getInputNumber('max-attempts') ?? 1,
|
||||
'retry-wait-seconds': Util.getInputNumber('retry-wait-seconds') ?? 5,
|
||||
'timeout-minutes': Util.getInputNumber('timeout-minutes') ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
107
src/main.ts
107
src/main.ts
|
|
@ -97,25 +97,7 @@ actionsToolkit.run(
|
|||
core.debug(`buildCmd.command: ${buildCmd.command}`);
|
||||
core.debug(`buildCmd.args: ${JSON.stringify(buildCmd.args)}`);
|
||||
|
||||
let err: Error | undefined;
|
||||
await Exec.getExecOutput(buildCmd.command, buildCmd.args, {
|
||||
ignoreReturnCode: true,
|
||||
env: Object.assign({}, process.env, {
|
||||
BUILDX_METADATA_WARNINGS: 'true'
|
||||
}) as {
|
||||
[key: string]: string;
|
||||
}
|
||||
}).then(res => {
|
||||
if (res.exitCode != 0) {
|
||||
if (inputs.call && inputs.call === 'check' && res.stdout.length > 0) {
|
||||
// checks warnings are printed to stdout: https://github.com/docker/buildx/pull/2647
|
||||
// take the first line with the message summaryzing the warnings
|
||||
err = new Error(res.stdout.split('\n')[0]?.trim());
|
||||
} else if (res.stderr.length > 0) {
|
||||
err = new Error(`buildx failed with: ${res.stderr.match(/(.*)\s*$/)?.[0]?.trim() ?? 'unknown error'}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
await executeBuildWithRetry(buildCmd, inputs);
|
||||
|
||||
const imageID = toolkit.buildxBuild.resolveImageID();
|
||||
const metadata = toolkit.buildxBuild.resolveMetadata();
|
||||
|
|
@ -182,10 +164,6 @@ actionsToolkit.run(
|
|||
stateHelper.setSummarySupported();
|
||||
}
|
||||
});
|
||||
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
// post
|
||||
async () => {
|
||||
|
|
@ -238,6 +216,89 @@ actionsToolkit.run(
|
|||
}
|
||||
);
|
||||
|
||||
async function executeBuildWithRetry(buildCmd: {command: string; args: string[]}, inputs: context.Inputs): Promise<void> {
|
||||
// Validate and sanitize retry inputs
|
||||
let maxAttempts = inputs['max-attempts'];
|
||||
if (isNaN(maxAttempts) || maxAttempts < 1) {
|
||||
core.warning(`Invalid max-attempts value '${inputs['max-attempts']}'. Using default: 1`);
|
||||
maxAttempts = 1;
|
||||
}
|
||||
|
||||
let retryWaitSeconds = inputs['retry-wait-seconds'];
|
||||
if (isNaN(retryWaitSeconds) || retryWaitSeconds < 0) {
|
||||
core.warning(`Invalid retry-wait-seconds value '${inputs['retry-wait-seconds']}'. Using default: 5`);
|
||||
retryWaitSeconds = 5;
|
||||
}
|
||||
|
||||
let timeoutMinutes = inputs['timeout-minutes'];
|
||||
if (isNaN(timeoutMinutes) || timeoutMinutes < 0) {
|
||||
core.warning(`Invalid timeout-minutes value '${inputs['timeout-minutes']}'. Using default: 0`);
|
||||
timeoutMinutes = 0;
|
||||
}
|
||||
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
if (maxAttempts > 1) {
|
||||
core.info(`Build attempt ${attempt} of ${maxAttempts}`);
|
||||
}
|
||||
|
||||
await executeBuildWithTimeout(buildCmd, inputs, timeoutMinutes);
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
core.warning(`Build failed on attempt ${attempt}: ${lastError.message}`);
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
if (retryWaitSeconds > 0) {
|
||||
core.info(`Retrying in ${retryWaitSeconds} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryWaitSeconds * 1000));
|
||||
} else {
|
||||
core.info('Retrying immediately...');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastError) {
|
||||
core.error(`All ${maxAttempts} attempts failed`);
|
||||
throw lastError;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeBuildWithTimeout(buildCmd: {command: string; args: string[]}, inputs: context.Inputs, timeoutMinutes: number): Promise<void> {
|
||||
const buildPromise = Exec.getExecOutput(buildCmd.command, buildCmd.args, {
|
||||
ignoreReturnCode: true,
|
||||
env: Object.assign({}, process.env, {
|
||||
BUILDX_METADATA_WARNINGS: 'true'
|
||||
}) as {
|
||||
[key: string]: string;
|
||||
}
|
||||
}).then(res => {
|
||||
if (res.exitCode != 0) {
|
||||
if (inputs.call && inputs.call === 'check' && res.stdout.length > 0) {
|
||||
throw new Error(res.stdout.split('\n')[0]?.trim());
|
||||
} else if (res.stderr.length > 0) {
|
||||
throw new Error(`buildx failed with: ${res.stderr.match(/(.*)\s*$/)?.[0]?.trim() ?? 'unknown error'}`);
|
||||
} else {
|
||||
throw new Error('buildx failed with unknown error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (timeoutMinutes <= 0) {
|
||||
return buildPromise;
|
||||
}
|
||||
|
||||
let timeoutHandle: NodeJS.Timeout;
|
||||
const timeoutPromise = new Promise<void>((_, reject) => {
|
||||
timeoutHandle = setTimeout(() => reject(new Error(`Build attempt timed out after ${timeoutMinutes} minutes`)), timeoutMinutes * 60 * 1000);
|
||||
});
|
||||
|
||||
return Promise.race([buildPromise, timeoutPromise]).finally(() => clearTimeout(timeoutHandle));
|
||||
}
|
||||
|
||||
async function buildRef(toolkit: Toolkit, since: Date, builder?: string): Promise<string> {
|
||||
// get ref from metadata file
|
||||
const ref = toolkit.buildxBuild.resolveRef();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue