mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(ci): allow two concurrent contributor PR runs split by PR parity
This commit is contained in:
@@ -12,9 +12,12 @@ execute the gate and report cancelled prerequisites as a failure.
|
||||
|
||||
## Contributor concurrency
|
||||
|
||||
PRs from anyone other than the repository owner share a main `CI` concurrency
|
||||
group keyed by the PR author's stable numeric user ID. Only one run in that
|
||||
author's group executes at a time; other authors have independent groups.
|
||||
PRs from anyone other than the repository owner share main `CI` concurrency
|
||||
groups keyed by the PR author's stable numeric user ID and the parity of the PR
|
||||
number (`pr-author-<id>-even` / `pr-author-<id>-odd`). At most two runs per author
|
||||
execute at a time — one per parity group — and other authors have independent
|
||||
groups. Actions expressions have no modulo, so parity is read from the PR number's
|
||||
last digit with `endsWith`. Two PRs of the same parity still queue behind each other.
|
||||
The PR author, not the trigger actor or author association, determines this
|
||||
policy, so a maintainer retrigger cannot bypass contributor limits.
|
||||
`cancel-in-progress: false` preserves the executing PR, and `queue: max` retains
|
||||
|
||||
@@ -18,29 +18,35 @@ function job(name, content = workflow) {
|
||||
function concurrencySettings(github) {
|
||||
const block = workflow.match(/^concurrency:\n((?: [^\n]+\n)+)/m)?.[1];
|
||||
assert.ok(block, "missing workflow concurrency");
|
||||
const format = (template, value) => template.replace("{0}", String(value));
|
||||
const format = (template, ...values) => template.replace(/\{(\d+)\}/g, (_, index) => String(values[Number(index)]));
|
||||
const endsWith = (search, value) => String(search).toLowerCase().endsWith(String(value).toLowerCase());
|
||||
return Object.fromEntries(block.trim().split("\n").map((line) => {
|
||||
const [, setting, value] = line.match(/^\s*([\w-]+):\s*(.+)$/);
|
||||
const evaluate = (expression) => new Function("github", "format", `return (${expression});`)(github, format);
|
||||
const evaluate = (expression) =>
|
||||
new Function("github", "format", "endsWith", `return (${expression});`)(github, format, endsWith);
|
||||
const expressions = [...value.matchAll(/\$\{\{\s*(.*?)\s*\}\}/g)];
|
||||
return [setting, expressions.length === 1 && expressions[0][0] === value
|
||||
? evaluate(expressions[0][1]) : value.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_, expression) => evaluate(expression))];
|
||||
}));
|
||||
}
|
||||
|
||||
function concurrencyContext({ authorId = 101, author = "contributor", actor = author, ref = "refs/pull/100/merge", eventName = "pull_request" } = {}) {
|
||||
function concurrencyContext({ authorId = 101, author = "contributor", actor = author, number = 100, ref = `refs/pull/${number}/merge`, eventName = "pull_request" } = {}) {
|
||||
return { workflow: "CI", repository_owner: "t8y2", actor, ref, event_name: eventName,
|
||||
event: eventName === "pull_request" ? { pull_request: { user: { id: authorId, login: author } } } : {} };
|
||||
event: eventName === "pull_request" ? { pull_request: { number, user: { id: authorId, login: author } } } : {} };
|
||||
}
|
||||
|
||||
test("a contributor shares one main CI queue across all PRs regardless of trigger actor", () => {
|
||||
const settings = concurrencySettings(concurrencyContext());
|
||||
assert.deepEqual(settings, { group: "CI-pr-author-101", "cancel-in-progress": false, queue: "max" });
|
||||
for (const context of [concurrencyContext({ ref: "refs/pull/200/merge" }), concurrencyContext({ actor: "t8y2" }),
|
||||
test("a contributor's PRs share two main CI queues split by PR number parity, regardless of trigger actor", () => {
|
||||
const even = concurrencySettings(concurrencyContext());
|
||||
assert.deepEqual(even, { group: "CI-pr-author-101-even", "cancel-in-progress": false, queue: "max" });
|
||||
for (const context of [concurrencyContext({ number: 202 }), concurrencyContext({ actor: "t8y2" }),
|
||||
concurrencyContext({ actor: "github-actions[bot]" }), concurrencyContext({ author: "renamed-contributor" })]) {
|
||||
assert.deepEqual(concurrencySettings(context), settings);
|
||||
assert.deepEqual(concurrencySettings(context), even);
|
||||
}
|
||||
assert.notEqual(concurrencySettings(concurrencyContext({ authorId: 102, author: "another-contributor" })).group, settings.group);
|
||||
for (const number of [101, 11849, 11853, 7]) {
|
||||
assert.deepEqual(concurrencySettings(concurrencyContext({ number })), { ...even, group: "CI-pr-author-101-odd" }, String(number));
|
||||
}
|
||||
for (const number of [2, 4, 6, 8, 11850]) assert.equal(concurrencySettings(concurrencyContext({ number })).group, even.group);
|
||||
assert.notEqual(concurrencySettings(concurrencyContext({ authorId: 102, author: "another-contributor" })).group, even.group);
|
||||
});
|
||||
|
||||
test("repository-owner PRs remain independent and superseded runs are cancelled only within a PR", () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
branches: [ main ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.user.login != github.repository_owner && format('pr-author-{0}', github.event.pull_request.user.id) || github.ref }}
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.user.login != github.repository_owner && format('pr-author-{0}-{1}', github.event.pull_request.user.id, (endsWith(github.event.pull_request.number, '0') || endsWith(github.event.pull_request.number, '2') || endsWith(github.event.pull_request.number, '4') || endsWith(github.event.pull_request.number, '6') || endsWith(github.event.pull_request.number, '8')) && 'even' || 'odd') || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login == github.repository_owner }}
|
||||
queue: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login != github.repository_owner && 'max' || 'single' }}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user