diff --git a/README.md b/README.md index 33637cd..b0e3190 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,22 @@ jobs: label: "enqueue-pullrequest" skip-labels: "wip,do-not-merge" ``` +### Merge Retry Options + +Sometimes, the pull request check runs haven't finished yet, so the action will retry the merge after some time. You can control this behavior with the following options: + +- `merge-retries`: Number of times to retry enqueueing if it fails. Default is 6. Set to 0 to disable retry logic. +- `merge-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 }} + merge-retries: 8 + merge-retry-sleep: 7000 +``` Then add the `enqueue-pullrequest` label to any PR you want automatically enqueued once it's ready. @@ -80,6 +96,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` | +| `merge-retries` | Number of times to retry enqueueing if it fails. Set to `0` to disable retry logic. | `6` | +| `merge-retry-sleep` | Time (in milliseconds) to sleep between retries. Set to `0` to disable sleeping between retries. | `5000` | ## Token permissions diff --git a/action.yml b/action.yml index 5aae716..e8943ee 100644 --- a/action.yml +++ b/action.yml @@ -1,3 +1,14 @@ +r merge-retries: + description: > + Number of times to retry enqueueing if it fails. Default is 6. Set to 0 to disable retry logic. + required: false + default: "6" + + merge-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" name: "Enqueue Pull Request" description: "Automatically enqueue pull requests into GitHub's native merge queue based on labels and review status" author: "waheedahmed" @@ -55,7 +66,7 @@ inputs: default: "0" runs: - using: "node20" + using: "node22" main: "dist/index.js" branding: diff --git a/dist/index.js b/dist/index.js index 023e203..3637324 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, + mergeRetries: parseInt(core.getInput("merge-retries"), 10), + mergeRetrySleep: parseInt(core.getInput("merge-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.mergeRetries === 'number' && !isNaN(config.mergeRetries) ? config.mergeRetries : 6; + const retrySleep = typeof config.mergeRetrySleep === 'number' && !isNaN(config.mergeRetrySleep) ? config.mergeRetrySleep : 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..f379012 100644 --- a/src/__tests__/index.test.js +++ b/src/__tests__/index.test.js @@ -266,7 +266,7 @@ describe("enqueue-pullrequest action", () => { describe("error handling", () => { test("fails the workflow when enqueue throws", async () => { - setupInputs(); + setupInputs({ "merge-retries": "0" }); mockOctokit.graphql .mockResolvedValueOnce({ repository: { pullRequest: makePRPayload() } }) .mockResolvedValueOnce(mqEnabled) diff --git a/src/api.js b/src/api.js index 5795105..0ae8a94 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, + mergeRetries: parseInt(core.getInput("merge-retries"), 10), + mergeRetrySleep: parseInt(core.getInput("merge-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.mergeRetries === 'number' && !isNaN(config.mergeRetries) ? config.mergeRetries : 6; + const retrySleep = typeof config.mergeRetrySleep === 'number' && !isNaN(config.mergeRetrySleep) ? config.mergeRetrySleep : 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++; } }