Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -135,3 +151,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 |
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
61 changes: 50 additions & 11 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 ";
Expand Down
61 changes: 50 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 ";
Expand Down