Skip to content
Closed
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 @@ -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.

Expand Down Expand Up @@ -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` |
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README uses both "enqueuing" and "enqueueing" (e.g., the label/required-approvals rows use "enqueuing", but these new rows use "enqueueing"). Consider standardizing the spelling across the README for consistency (pick one form and use it everywhere).

Suggested change
| `merge-retries` | Number of times to retry enqueueing if it fails. Set to `0` to disable retry logic. | `6` |
| `merge-retries` | Number of times to retry enqueuing if it fails. Set to `0` to disable retry logic. | `6` |

Copilot uses AI. Check for mistakes.
| `merge-retry-sleep` | Time (in milliseconds) to sleep between retries. Set to `0` to disable sleeping between retries. | `5000` |

## Token permissions

Expand Down
13 changes: 12 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -55,7 +66,7 @@ inputs:
default: "0"

runs:
using: "node20"
using: "node22"
main: "dist/index.js"
Comment on lines 68 to 70
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The action runtime is being bumped to Node 22, but the repo CI workflow (.github/workflows/test.yml) still runs lint/tests on Node 20 only. To reduce the risk of Node-version-specific regressions, consider updating CI to run on Node 22 (or a matrix including both 20 and 22) so the tested runtime matches what users will run in production.

Copilot uses AI. Check for mistakes.

branding:
Expand Down
39 changes: 29 additions & 10 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
}

Expand Down Expand Up @@ -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++;
}
}

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 29 additions & 10 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
}

Expand Down Expand Up @@ -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++;
}
}

Expand Down
Loading