mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
ci: Add reusable retry script for CI commands (#26815)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
56176bf6b5
commit
60f4569fbe
12
.github/actions/docker-registry-login/action.yml
vendored
12
.github/actions/docker-registry-login/action.yml
vendored
|
|
@ -29,11 +29,13 @@ runs:
|
|||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
if: inputs.login-ghcr == 'true'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
shell: bash
|
||||
env:
|
||||
GHCR_TOKEN: ${{ github.token }}
|
||||
GHCR_USER: ${{ github.actor }}
|
||||
run: |
|
||||
node .github/scripts/retry.mjs --attempts 3 --delay 10 \
|
||||
'echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USER" --password-stdin'
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: inputs.login-dockerhub == 'true'
|
||||
|
|
|
|||
60
.github/scripts/retry.mjs
vendored
Normal file
60
.github/scripts/retry.mjs
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Retry a shell command with configurable attempts and delay.
|
||||
*
|
||||
* Usage: node retry.mjs [--attempts N] [--delay N] '<command>'
|
||||
*
|
||||
* Options:
|
||||
* --attempts N Maximum number of attempts (default: 4)
|
||||
* --delay N Seconds to wait between retries (default: 15)
|
||||
*
|
||||
* The command is executed via shell, so pipes and env-var expansion work.
|
||||
* Exits 0 on first success, 1 if all attempts fail.
|
||||
*/
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function getFlag(name, defaultValue) {
|
||||
const index = args.indexOf(`--${name}`);
|
||||
if (index === -1 || !args[index + 1]) return defaultValue;
|
||||
const value = parseInt(args[index + 1], 10);
|
||||
if (Number.isNaN(value) || value <= 0) {
|
||||
console.error(`Error: --${name} must be a positive integer`);
|
||||
process.exit(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const attempts = getFlag('attempts', 4);
|
||||
const delay = getFlag('delay', 15);
|
||||
|
||||
// Command is the last positional arg (skip flags and their values)
|
||||
const command = args
|
||||
.filter((a, i) => {
|
||||
if (a.startsWith('--')) return false;
|
||||
if (i > 0 && args[i - 1].startsWith('--')) return false;
|
||||
return true;
|
||||
})
|
||||
.pop();
|
||||
|
||||
if (!command) {
|
||||
console.error("Usage: node retry.mjs [--attempts N] [--delay N] '<command>'");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= attempts; i++) {
|
||||
try {
|
||||
execSync(command, { shell: true, stdio: 'inherit' });
|
||||
process.exit(0);
|
||||
} catch {
|
||||
if (i < attempts) {
|
||||
console.error(`Attempt ${i}/${attempts} failed, retrying in ${delay}s...`);
|
||||
execSync(`sleep ${delay}`);
|
||||
} else {
|
||||
console.error(`Attempt ${i}/${attempts} failed, no more retries.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
|
|
@ -36,14 +36,11 @@ jobs:
|
|||
security/vex.openvex.json
|
||||
security/trivy.yaml
|
||||
security/trivy-ignore-policy.rego
|
||||
.github/scripts/retry.mjs
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Pull Docker image with retry
|
||||
run: |
|
||||
for i in {1..4}; do
|
||||
docker pull "${{ inputs.image_ref }}" && break
|
||||
[ "$i" -lt 4 ] && echo "Retry $i failed, waiting..." && sleep 15
|
||||
done
|
||||
run: node .github/scripts/retry.mjs --attempts 4 --delay 15 'docker pull "${{ inputs.image_ref }}"'
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # v0.34.1
|
||||
|
|
|
|||
|
|
@ -24,24 +24,42 @@ interface PullResult {
|
|||
pulled: number;
|
||||
}
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAY_MS = 10_000;
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function pullImage(image: string): Promise<PullResult> {
|
||||
const imageStart = Date.now();
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(`docker pull ${image}`);
|
||||
const output = stdout + stderr;
|
||||
|
||||
// Count cached vs pulled layers
|
||||
const cached = (output.match(/Already exists/g) ?? []).length;
|
||||
const pulled = (output.match(/Pull complete/g) ?? []).length;
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(`docker pull ${image}`);
|
||||
const output = stdout + stderr;
|
||||
|
||||
const duration = ((Date.now() - imageStart) / 1000).toFixed(1);
|
||||
return { image, duration, success: true, cached, pulled };
|
||||
} catch (error) {
|
||||
const duration = ((Date.now() - imageStart) / 1000).toFixed(1);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(` ⚠️ Pull failed for ${image}: ${message}`);
|
||||
return { image, duration, success: false, cached: 0, pulled: 0 };
|
||||
const cached = (output.match(/Already exists/g) ?? []).length;
|
||||
const pulled = (output.match(/Pull complete/g) ?? []).length;
|
||||
|
||||
const duration = ((Date.now() - imageStart) / 1000).toFixed(1);
|
||||
return { image, duration, success: true, cached, pulled };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (attempt < MAX_RETRIES) {
|
||||
console.warn(
|
||||
` ⚠️ Pull attempt ${attempt}/${MAX_RETRIES} failed for ${image}: ${message}`,
|
||||
);
|
||||
console.warn(` ⏳ Retrying in ${RETRY_DELAY_MS / 1000}s...`);
|
||||
await sleep(RETRY_DELAY_MS);
|
||||
} else {
|
||||
console.error(` ❌ Pull failed for ${image} after ${MAX_RETRIES} attempts: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const duration = ((Date.now() - imageStart) / 1000).toFixed(1);
|
||||
return { image, duration, success: false, cached: 0, pulled: 0 };
|
||||
}
|
||||
|
||||
function isValidImageKey(key: string): key is ImageKey {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user