From a7a055a68750b8259c6021a9f00516272ff22c7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:25:08 +0000 Subject: [PATCH 1/3] Initial plan From 2762dd6383a4bc630fa2942b4e36224ca1d50584 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:30:24 +0000 Subject: [PATCH 2/3] Add retry logic with exponential backoff for transient failures Co-authored-by: kriscoleman <21978023+kriscoleman@users.noreply.github.com> --- README.md | 2 ++ action.yml | 8 +++++++ dist/index.js | 61 +++++++++++++++++++++++++++++++++++++++++---------- index.js | 61 +++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 110 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e84d50c5..cfdf826c 100644 --- a/README.md +++ b/README.md @@ -135,3 +135,5 @@ host your own instances. | timestamp-servers | Timestamp Authority Servers to use when signing envelope, space-separated | No | | | trace | Enable tracing for the command | No | false | | workingdir | Directory from which commands will run | No | | +| retries | Maximum number of retry attempts for transient failures | No | 2 | +| retry-delay | Initial delay in seconds between retry attempts (uses exponential backoff) | No | 5 | diff --git a/action.yml b/action.yml index 3e05dc12..c45daa07 100644 --- a/action.yml +++ b/action.yml @@ -92,6 +92,14 @@ inputs: workingdir: description: "Directory from which commands will run" required: false + retries: + description: "Maximum number of retry attempts for transient failures" + required: false + default: "2" + retry-delay: + description: "Initial delay in seconds between retry attempts (uses exponential backoff)" + required: false + default: "5" runs: using: "node20" diff --git a/dist/index.js b/dist/index.js index 73e9a1c9..458f74a6 100644 --- a/dist/index.js +++ b/dist/index.js @@ -30115,6 +30115,10 @@ async function run() { const workingdir = core.getInput("workingdir"); const fullWorkspacePath = path.join(index_process.env.GITHUB_WORKSPACE, workingdir); const witnessInstallDir = core.getInput('witness-install-dir') || fullWorkspacePath; + + // Retry configuration + const maxRetries = parseInt(core.getInput("retries") || "2", 10); + const retryDelay = parseInt(core.getInput("retry-delay") || "5", 10); // Download Witness const version = core.getInput("version"); @@ -30281,18 +30285,25 @@ async function run() { commandString = runArray.join(" "); let output = ""; - await exec.exec("sh", ["-c", commandString], { - cwd: index_process.cwd(), - env: index_process.env, - listeners: { - stdout: (data) => { - output += data.toString(); - }, - stderr: (data) => { - output += data.toString(); - }, + await executeWithRetry( + async () => { + output = ""; + await exec.exec("sh", ["-c", commandString], { + cwd: index_process.cwd(), + env: index_process.env, + listeners: { + stdout: (data) => { + output += data.toString(); + }, + stderr: (data) => { + output += data.toString(); + }, + }, + }); }, - }); + maxRetries, + retryDelay + ); // Find the GitOID from the output const gitOIDs = extractDesiredGitOIDs(output); @@ -30335,6 +30346,34 @@ async function run() { exit(0); } +async function executeWithRetry(fn, maxRetries, retryDelay) { + let lastError; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + await fn(); + return; // Success, exit the retry loop + } catch (error) { + lastError = error; + + if (attempt < maxRetries) { + const delay = retryDelay * Math.pow(2, attempt); // Exponential backoff + core.warning(`Command failed (attempt ${attempt + 1}/${maxRetries + 1}). Retrying in ${delay} seconds...`); + core.warning(`Error: ${error.message}`); + await sleep(delay * 1000); + } + } + } + + // If we've exhausted all retries, throw the last error + core.error(`Command failed after ${maxRetries + 1} attempts`); + throw lastError; +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + function extractDesiredGitOIDs(output) { const lines = output.split("\n"); const desiredSubstring = "Stored in archivista as "; diff --git a/index.js b/index.js index f84c7197..1e04f461 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,10 @@ async function run() { const workingdir = core.getInput("workingdir"); const fullWorkspacePath = path.join(process.env.GITHUB_WORKSPACE, workingdir); const witnessInstallDir = core.getInput('witness-install-dir') || fullWorkspacePath; + + // Retry configuration + const maxRetries = parseInt(core.getInput("retries") || "2", 10); + const retryDelay = parseInt(core.getInput("retry-delay") || "5", 10); // Download Witness const version = core.getInput("version"); @@ -178,18 +182,25 @@ async function run() { commandString = runArray.join(" "); let output = ""; - await exec.exec("sh", ["-c", commandString], { - cwd: process.cwd(), - env: process.env, - listeners: { - stdout: (data) => { - output += data.toString(); - }, - stderr: (data) => { - output += data.toString(); - }, + await executeWithRetry( + async () => { + output = ""; + await exec.exec("sh", ["-c", commandString], { + cwd: process.cwd(), + env: process.env, + listeners: { + stdout: (data) => { + output += data.toString(); + }, + stderr: (data) => { + output += data.toString(); + }, + }, + }); }, - }); + maxRetries, + retryDelay + ); // Find the GitOID from the output const gitOIDs = extractDesiredGitOIDs(output); @@ -232,6 +243,34 @@ async function run() { exit(0); } +async function executeWithRetry(fn, maxRetries, retryDelay) { + let lastError; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + await fn(); + return; // Success, exit the retry loop + } catch (error) { + lastError = error; + + if (attempt < maxRetries) { + const delay = retryDelay * Math.pow(2, attempt); // Exponential backoff + core.warning(`Command failed (attempt ${attempt + 1}/${maxRetries + 1}). Retrying in ${delay} seconds...`); + core.warning(`Error: ${error.message}`); + await sleep(delay * 1000); + } + } + } + + // If we've exhausted all retries, throw the last error + core.error(`Command failed after ${maxRetries + 1} attempts`); + throw lastError; +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + function extractDesiredGitOIDs(output) { const lines = output.split("\n"); const desiredSubstring = "Stored in archivista as "; From 2683be9f57488ba6f94ff1f1c0446e3b73c71b65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:34:23 +0000 Subject: [PATCH 3/3] Add retry configuration examples to README Co-authored-by: kriscoleman <21978023+kriscoleman@users.noreply.github.com> --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index cfdf826c..ec5f27f9 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,22 @@ jobs: command: make build ``` +### Configuring Retry Behavior + +By default, the action will retry failed commands up to 2 times with a 5-second initial delay and exponential backoff. You can customize this behavior: + +```yaml +- name: Witness Run with Custom Retries + uses: testifysec/witness-run-action@v1 + with: + step: build + command: npm ci + retries: 3 # Retry up to 3 times (4 total attempts) + retry-delay: 10 # Start with 10-second delay (10s → 20s → 40s) +``` + +This is particularly useful for handling transient network failures when connecting to package registries or external services. + ## Using Reusable Workflows For a streamlined setup, you can use our reusable workflow. This is especially useful when you need to pass secrets like API tokens for authentication: