140 lines
6.2 KiB
JavaScript
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);
|
|
}
|
|
});
|