Files

140 lines
6.2 KiB
JavaScript

// Execute the workflow's actual script with API doubles; this is not a GitHub Actions integration test.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const workflow = readFileSync(new URL('../.github/workflows/pr-labeler.yml', import.meta.url), 'utf8');
const block = workflow.match(/^ {10}script: \|\n((?: {12}.*\n|\n)+)$/m);
assert.ok(block, 'the workflow contains one inline script');
const script = block[1].replace(/^ {12}/gm, '');
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const run = new AsyncFunction('github', 'context', 'core', script);
const repo = { owner: 'sganggs', repo: 'Stronghold-Protocol' };
function harness(title, { labels = [], action = 'opened', changes, eventTitle = title, labelError, addError } = {}) {
const calls = [];
const messages = [];
const pr = { title, labels: labels.map((name) => ({ name })) };
const context = {
repo,
issue: { ...repo, number: 123 },
payload: {
action, changes,
pull_request: {
number: 123, title: eventTitle,
head: { ref: 'fix/example', repo: { full_name: 'contributor/Stronghold-Protocol', fork: true } },
},
},
};
const github = { rest: {
pulls: { get: async (params) => {
assert.deepEqual(params, { ...repo, pull_number: 123 });
calls.push(['get', params]);
return { data: pr };
} },
issues: {
getLabel: async (params) => {
assert.deepEqual(params, { ...repo, name: params.name });
calls.push(['getLabel', params]);
if (labelError) throw labelError;
return { data: { name: params.name } };
},
addLabels: async (params) => {
assert.deepEqual(params, { ...repo, issue_number: 123, labels: params.labels });
calls.push(['addLabels', params]);
if (addError) throw addError;
for (const name of params.labels) pr.labels.push({ name });
},
},
} };
return { calls, messages, pr, context, run: () => run(github, context, { info: (message) => messages.push(message) }) };
}
test('only PR metadata events, minimal permission, pinned action and no executable PR input', () => {
assert.match(workflow, /^on:\n {2}pull_request_target:\n {4}types: \[opened, edited, reopened\]\n\npermissions:/m);
assert.match(workflow, /^permissions:\n {2}pull-requests: write\n\njobs:/m);
assert.equal((workflow.match(/^ {6}- uses:/gm) || []).length, 1);
assert.match(workflow, /uses: actions\/github-script@[a-f0-9]{40} # v8/);
assert.doesNotMatch(workflow, /actions\/checkout|^\s*run:|\$\{\{|secrets\./m);
});
for (const [title, expected] of [
['fix: repair', 'bug'], ['bug: repair', 'bug'], ['docs: update', 'documentation'],
['feat: add', 'enhancement'], ['feature: add', 'enhancement'],
['FIX(ui): repair', 'bug'], ['BuG(server):修复', 'bug'], ['Docs(readme):说明', 'documentation'],
['FEAT(ui): add', 'enhancement'], ['Feature(界面):新增', 'enhancement'], [' fix:修复 ', 'bug'],
['fix: " ; process.exit(1); // ${{ secrets.TOKEN }} $(echo unsafe)', 'bug'],
]) {
test(`maps ${JSON.stringify(title)} to ${expected}`, async () => {
const h = harness(title, { labels: ['help wanted'] });
await h.run();
assert.deepEqual(h.calls.map(([name]) => name), ['get', 'getLabel', 'addLabels']);
assert.deepEqual(h.calls[2][1].labels, [expected]);
assert.deepEqual(h.pr.labels.map(({ name }) => name), ['help wanted', expected]);
});
}
for (const title of [
'chore: maintain', 'refactor: cleanup', 'test: coverage', 'ci: improve', 'perf: speed', 'style: format',
'bugfix: repair', 'fixes: repair', 'prefix fix: repair', 'fix repair', 'fix[]: repair',
'fix(): repair', 'fix(ui)(server): repair', 'fix(ui: repair', 'fix(ui\nserver): repair', '',
]) {
test(`ignores ${JSON.stringify(title)}`, async () => {
const h = harness(title, { labels: ['help wanted'] });
await h.run();
assert.deepEqual(h.calls.map(([name]) => name), ['get']);
assert.deepEqual(h.pr.labels, [{ name: 'help wanted' }]);
});
}
test('repeated runs skip an existing label and preserve every previous label', async () => {
const h = harness('fix: repair', { labels: ['help wanted', 'enhancement'] });
await h.run();
await h.run();
assert.equal(h.calls.filter(([name]) => name === 'addLabels').length, 1);
assert.deepEqual(h.pr.labels.map(({ name }) => name), ['help wanted', 'enhancement', 'bug']);
const existing = harness('BUG:修复', { labels: ['BUG', 'documentation'] });
await existing.run();
assert.deepEqual(existing.calls.map(([name]) => name), ['get']);
});
test('title edits use current PR metadata and add the new type without removing the old type', async () => {
const h = harness('docs(ui):说明', {
action: 'edited', changes: { title: { from: 'fix: repair' } }, eventTitle: 'fix: repair', labels: ['bug'],
});
await h.run();
assert.deepEqual(h.pr.labels.map(({ name }) => name), ['bug', 'documentation']);
});
test('body or base edits do not make any API calls', async () => {
for (const changes of [undefined, {}, { body: { from: 'old body' } }, { base: { ref: { from: 'old' } } }]) {
const h = harness('fix: repair', { action: 'edited', changes });
await h.run();
assert.deepEqual(h.calls, []);
}
});
test('opened and reopened fork events add labels in the base repository', async () => {
for (const action of ['opened', 'reopened']) {
const h = harness('feat: add', { action });
await h.run();
assert.deepEqual(h.calls.at(-1), ['addLabels', { ...repo, issue_number: 123, labels: ['enhancement'] }]);
}
});
test('a missing repository label is skipped without creating or adding it', async () => {
const h = harness('fix: repair', { labelError: Object.assign(new Error('Not Found'), { status: 404 }) });
await h.run();
assert.deepEqual(h.calls.map(([name]) => name), ['get', 'getLabel']);
assert.deepEqual(h.pr.labels, []);
assert.equal(h.messages.length, 1);
});
test('permission and API failures remain visible', async () => {
for (const status of [403, 500]) {
const error = Object.assign(new Error('API failure'), { status });
await assert.rejects(harness('fix: repair', { labelError: error }).run(), error);
await assert.rejects(harness('fix: repair', { addError: error }).run(), error);
}
});