Skip to content
Merged
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
16 changes: 16 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
11 changes: 11 additions & 0 deletions 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"
Comment on lines +1 to +11
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.

action.yml is invalid YAML / invalid action metadata: the file starts with an unexpected r merge-retries: key and the new inputs are defined before name: and outside the inputs: map. GitHub Actions will ignore/break these inputs. Move merge-retries and merge-retry-sleep under the existing inputs: section and remove the stray leading r/fix indentation so the file has a valid top-level schema (name, description, inputs, runs, etc.).

Copilot uses AI. Check for mistakes.
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
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
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) {
Comment on lines +203 to +204
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.

lastError is assigned but never read. This makes the retry logic harder to follow; remove it or use it to report a final error after the loop (if you change the control flow).

Copilot uses AI. Check for mistakes.
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) {
Comment on lines +199 to +215
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 executes dist/index.js (action.ymlruns.main), but dist/index.js still contains the old non-retrying enqueue logic (no merge-retries/merge-retry-sleep handling). Run the build (npm run build) and commit the updated dist/ output so the published action actually includes the retry feature.

Copilot uses AI. Check for mistakes.
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