diff --git a/README.md b/README.md index 33637cd..0596be3 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,23 @@ jobs: skip-labels: "wip,do-not-merge" ``` +### Enqueue Retry Options + +Sometimes, the pull request check runs haven't finished yet, so the action will retry the enqueue after some time. You can control this behavior with the following options: + +- `enqueue-retries`: Number of times to retry enqueuing if it fails. Default is 6. Set to 0 to disable retry logic. +- `enqueue-retry-sleep`: Time (in milliseconds) to sleep between retries. Default is 5000 (5 seconds). Set to 0 to disable sleeping between retries. + +Example usage: + +```yaml + - uses: waheedahmed/enqueue-pullrequest@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + enqueue-retries: 8 + enqueue-retry-sleep: 7000 +``` + Then add the `enqueue-pullrequest` label to any PR you want automatically enqueued once it's ready. To process only PRs from a specific branch (useful for `schedule` or `workflow_dispatch` triggers), set `branch`: @@ -80,6 +97,8 @@ To enqueue **all** open PRs without requiring a label, set `label` to an empty s | `base-branches` | Comma-separated list of allowed base branches. Empty = all branches. | `""` | | `skip-drafts` | Skip draft pull requests. | `true` | | `required-approvals` | Minimum approving reviews before enqueuing. `0` = rely on branch protection rules. | `0` | +| `enqueue-retries` | Number of times to retry enqueuing if it fails. | `6` | +| `enqueue-retry-sleep` | Time (in milliseconds) to sleep between retries. | `5000` | ## Token permissions diff --git a/action.yml b/action.yml index 5aae716..37cac6e 100644 --- a/action.yml +++ b/action.yml @@ -31,7 +31,7 @@ inputs: skip-labels: description: > Comma-separated list of labels that prevent a PR from being added to the - merge queue (e.g. "wip, do-not-merge, blocked"). + merge queue (e.g. "wip, do-not-enqueue, blocked"). required: false default: "" @@ -54,8 +54,20 @@ inputs: required: false default: "0" + enqueue-retries: + description: > + Number of times to retry enqueuing if it fails. Default is 6. Set to 0 to disable retry logic. + required: false + default: "6" + + enqueue-retry-sleep: + description: > + Time (in milliseconds) to sleep between retries. Default is 5000 (5 seconds). Set to 0 to disable sleeping between retries. + required: false + default: "5000" + runs: - using: "node20" + using: "node22" main: "dist/index.js" branding: diff --git a/dist/index.js b/dist/index.js index 023e203..13451e7 100644 --- a/dist/index.js +++ b/dist/index.js @@ -29949,6 +29949,8 @@ function getConfig() { .filter(Boolean), skipDrafts: core.getInput("skip-drafts") !== "false", requiredApprovals: parseInt(core.getInput("required-approvals"), 10) || 0, + enqueueRetries: parseInt(core.getInput("enqueue-retries"), 10), + enqueueRetrySleep: parseInt(core.getInput("enqueue-retry-sleep"), 10), }; } @@ -30122,17 +30124,34 @@ async function processPR(octokit, owner, repo, prNumber, config) { core.info("Adding to merge queue…"); - try { - const entry = await enqueue(octokit, pr.id); - if (entry) { - core.info( - `Added to merge queue: position=${entry.position}, state=${entry.state}` - ); - } else { - core.info("Added to merge queue (no entry details returned)"); + // Retry logic for enqueue + const maxRetries = typeof config.enqueueRetries === 'number' && !isNaN(config.enqueueRetries) ? config.enqueueRetries : 6; + const retrySleep = typeof config.enqueueRetrySleep === 'number' && !isNaN(config.enqueueRetrySleep) ? config.enqueueRetrySleep : 5000; + + let attempt = 0; + while (attempt <= maxRetries) { + try { + const entry = await enqueue(octokit, pr.id); + if (entry) { + core.info( + `Added to merge queue: position=${entry.position}, state=${entry.state}` + ); + } else { + core.info("Added to merge queue (no entry details returned)"); + } + return; + } catch (err) { + if (maxRetries === 0 || attempt === maxRetries) { + core.setFailed(`Failed to enqueue PR #${prNumber}: ${err.message}`); + return; + } + core.warning(`Enqueue failed (attempt ${attempt + 1}/${maxRetries + 1}): ${err.message}`); + if (retrySleep > 0) { + core.info(`Sleeping for ${retrySleep}ms before retrying…`); + await new Promise((resolve) => setTimeout(resolve, retrySleep)); + } } - } catch (err) { - core.setFailed(`Failed to enqueue PR #${prNumber}: ${err.message}`); + attempt++; } } diff --git a/package-lock.json b/package-lock.json index f164fe0..b74c764 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "enqueue-pullrequest", - "version": "0.0.1", + "version": "0.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "enqueue-pullrequest", - "version": "0.0.1", + "version": "0.0.5", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", diff --git a/package.json b/package.json index 0f9880a..6cfc73f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enqueue-pullrequest", - "version": "0.0.1", + "version": "0.0.5", "description": "GitHub Action to automatically enqueue pull requests into GitHub's native merge queue", "main": "dist/index.js", "scripts": { diff --git a/src/__tests__/index.test.js b/src/__tests__/index.test.js index d94e018..9b6627b 100644 --- a/src/__tests__/index.test.js +++ b/src/__tests__/index.test.js @@ -50,6 +50,8 @@ describe("enqueue-pullrequest action", () => { "base-branches": "", "skip-drafts": "true", "required-approvals": "0", + "enqueue-retries": "0", + "enqueue-retry-sleep": "0", }; jest.spyOn(core, "getInput").mockImplementation( (name) => ({ ...defaults, ...overrides }[name] ?? "") diff --git a/src/api.js b/src/api.js index 5795105..95bdb91 100644 --- a/src/api.js +++ b/src/api.js @@ -21,6 +21,8 @@ function getConfig() { .filter(Boolean), skipDrafts: core.getInput("skip-drafts") !== "false", requiredApprovals: parseInt(core.getInput("required-approvals"), 10) || 0, + enqueueRetries: parseInt(core.getInput("enqueue-retries"), 10), + enqueueRetrySleep: parseInt(core.getInput("enqueue-retry-sleep"), 10), }; } @@ -194,17 +196,34 @@ async function processPR(octokit, owner, repo, prNumber, config) { core.info("Adding to merge queue…"); - try { - const entry = await enqueue(octokit, pr.id); - if (entry) { - core.info( - `Added to merge queue: position=${entry.position}, state=${entry.state}` - ); - } else { - core.info("Added to merge queue (no entry details returned)"); + // Retry logic for enqueue + const maxRetries = typeof config.enqueueRetries === 'number' && !isNaN(config.enqueueRetries) ? config.enqueueRetries : 6; + const retrySleep = typeof config.enqueueRetrySleep === 'number' && !isNaN(config.enqueueRetrySleep) ? config.enqueueRetrySleep : 5000; + + let attempt = 0; + while (attempt <= maxRetries) { + try { + const entry = await enqueue(octokit, pr.id); + if (entry) { + core.info( + `Added to merge queue: position=${entry.position}, state=${entry.state}` + ); + } else { + core.info("Added to merge queue (no entry details returned)"); + } + return; + } catch (err) { + if (maxRetries === 0 || attempt === maxRetries) { + core.setFailed(`Failed to enqueue PR #${prNumber}: ${err.message}`); + return; + } + core.warning(`Enqueue failed (attempt ${attempt + 1}/${maxRetries + 1}): ${err.message}`); + if (retrySleep > 0) { + core.info(`Sleeping for ${retrySleep}ms before retrying…`); + await new Promise((resolve) => setTimeout(resolve, retrySleep)); + } } - } catch (err) { - core.setFailed(`Failed to enqueue PR #${prNumber}: ${err.message}`); + attempt++; } }