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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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

Expand Down
16 changes: 14 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""

Expand All @@ -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:
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,
enqueueRetries: parseInt(core.getInput("enqueue-retries"), 10),
enqueueRetrySleep: parseInt(core.getInput("enqueue-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.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++;
}
}

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: 2 additions & 0 deletions src/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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] ?? "")
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,
enqueueRetries: parseInt(core.getInput("enqueue-retries"), 10),
enqueueRetrySleep: parseInt(core.getInput("enqueue-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.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++;
}
}

Expand Down
Loading