Require commit SHA for /test and /test-extended on external PRs (#1731)

* Add `/test <sha` command

Add test helpers to improve test readability

* Make command tests more consistent

* Add comment on running test, update run-test.sh

* Extract /test command handling to separate function

* Add SHA validation for external PRs for /test-extended

* Update maintainers.md with /test sha requirements

* Add tests for non-mergeable PRs

* Handle extra spaces between /test and sha

When copy/pasting SHA in UI, it's easy to add an extra space
This change makes the pr-bot tolerant of that

* PR feedback
This commit is contained in:
Stuart Leeks 2022-04-27 13:46:17 +01:00 коммит произвёл GitHub
Родитель 143877440d
Коммит 87c73d2424
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 992 добавлений и 344 удалений

82
.github/scripts/build.js поставляемый
Просмотреть файл

@ -1,3 +1,9 @@
//
// This file contains functions that are used in GitHub workflows
// (e.g. to implement the pr-bot for running tests
// There are tests for this code in build.test.js
// These tests can be run from the dev container using the run-tests.sh script
//
const { createHash } = require('crypto');
async function getCommandFromComment({ core, context, github }) {
@ -9,6 +15,7 @@ async function getCommandFromComment({ core, context, github }) {
const prNumber = context.payload.issue.number;
const commentLink = context.payload.comment.html_url;
const runId = context.runId;
const prAuthorUsername = context.payload.issue.user.login;
// only allow actions for users with write access
if (!await userHasWriteAccessToRepo({ core, github }, commentUsername, repoOwner, repoName)) {
@ -19,6 +26,7 @@ async function getCommandFromComment({ core, context, github }) {
issue_number: prNumber,
body: `Sorry, @${commentUsername}, only users with write access to the repo can run pr-bot commands.`
});
logAndSetOutput(core, "command", "none");
return "none";
}
@ -57,33 +65,40 @@ async function getCommandFromComment({ core, context, github }) {
let command = "none";
const trimmedFirstLine = commentFirstLine.trim();
if (trimmedFirstLine[0] === "/") {
switch (trimmedFirstLine) {
const parts = trimmedFirstLine.split(' ').filter(p=>p !== '');
const commandText = parts[0];
switch (commandText) {
case "/test":
{
if (gotNonDocChanges) {
command = "run-tests";
const message = `:runner: Running tests: https://github.com/${repoFullName}/actions/runs/${runId} (with refid \`${prRefId}\`)`;
await addActionComment({ github }, repoOwner, repoName, prNumber, commentUsername, commentLink, message);
} else {
// Docs only changes don't run tests with secrets so don't require the check for
// whether a SHA needs to be supplied
if (!gotNonDocChanges) {
command = "test-force-approve";
const message = `:white_check_mark: PR only contains docs changes - marking tests as complete`;
await addActionComment({ github }, repoOwner, repoName, prNumber, commentUsername, commentLink, message);
break;
}
const runTests = await handleTestCommand({ core, github }, parts, "tests", runId, { number: prNumber, authorUsername: prAuthorUsername, repoOwner, repoName, headSha: prHeadSha, refId: prRefId, details: pr }, { username: commentUsername, link: commentLink });
if (runTests) {
command = "run-tests";
}
break;
}
case "/test-extended":
{
command = "run-tests-extended";
const message = `:runner: Running extended tests: https://github.com/${repoFullName}/actions/runs/${runId} (with refid \`${prRefId}\`)`;
await addActionComment({ github }, repoOwner, repoName, prNumber, commentUsername, commentLink, message);
const runTests = await handleTestCommand({ core, github }, parts, "extended tests", runId, { number: prNumber, authorUsername: prAuthorUsername, repoOwner, repoName, headSha: prHeadSha, refId: prRefId, details: pr }, { username: commentUsername, link: commentLink });
if (runTests) {
command = "run-tests-extended";
}
break;
}
case "/test-force-approve":
{
command = "test-force-approve";
const message = `:white_check_mark: Marking tests as complete (for commit ${prHeadSha})`;
const message = `:white_check_mark: Marking tests as complete (for commit ${prHeadSha})`;
await addActionComment({ github }, repoOwner, repoName, prNumber, commentUsername, commentLink, message);
break;
}
@ -98,8 +113,8 @@ async function getCommandFromComment({ core, context, github }) {
break;
default:
core.warning(`'${trimmedFirstLine}' not recognised as a valid command`);
await showHelp({ github }, repoOwner, repoName, prNumber, commentUsername, commentLink, trimmedFirstLine);
core.warning(`'${commandText}' not recognised as a valid command`);
await showHelp({ github }, repoOwner, repoName, prNumber, commentUsername, commentLink, commandText);
command = "none";
break;
}
@ -108,6 +123,47 @@ async function getCommandFromComment({ core, context, github }) {
return command;
}
async function handleTestCommand({ core, github }, commandParts, testDescription, runId, pr, comment) {
if (!pr.details.mergeable) {
// Since we use the potential merge commit as the ref to checkout, we can only run if there is such a commit
// If the PR isn't mergeable, add a comment indicating that the merge issue needs addressing
const message = `:warning: Cannot run tests as PR is not mergeable. Ensure that the PR is open and doesn't have any conflicts.`;
await addActionComment({ github }, pr.repoOwner, pr.repoName, pr.number, comment.username, comment.link, message);
return false;
}
// check if this is an external PR (i.e. author not a maintainer)
// if so, need to specify the SHA that has been vetted and check that it matches
// the latest head SHA for the PR
const command = commandParts[0]
const prAuthorHasWriteAccess = await userHasWriteAccessToRepo({ core, github }, pr.authorUsername, pr.repoOwner, pr.repoName);
const externalPr = !prAuthorHasWriteAccess;
if (externalPr) {
if (commandParts.length === 1) {
const message = `:warning: When using \`${command}\` on external PRs, the SHA of the checked commit must be specified`;
await addActionComment({ github }, pr.repoOwner, pr.repoName, pr.number, comment.username, comment.link, message);
return false;
}
const commentSha = commandParts[1];
if (commentSha.length < 7) {
const message = `:warning: When specifying a commit SHA it must be at least 7 characters (received \`${commentSha}\`)`;
await addActionComment({ github }, pr.repoOwner, pr.repoName, pr.number, comment.username, comment.link, message);
return false;
}
if (!pr.headSha.startsWith(commentSha)) {
const message = `:warning: The specified SHA \`${commentSha}\` is not the latest commit on the PR. Please validate the latest commit and re-run \`/test\``;
await addActionComment({ github }, pr.repoOwner, pr.repoName, pr.number, comment.username, comment.link, message);
return false;
}
}
const message = `:runner: Running ${testDescription}: https://github.com/${pr.repoOwner}/${pr.repoName}/actions/runs/${runId} (with refid \`${pr.refId}\`)`;
await addActionComment({ github }, pr.repoOwner, pr.repoName, pr.number, comment.username, comment.link, message);
return true
}
async function prContainsNonDocChanges(github, repoOwner, repoName, prNumber) {
const prFilesResponse = await github.paginate(github.rest.pulls.listFiles, {
owner: repoOwner,
@ -177,7 +233,7 @@ You can use the following commands:
&nbsp;&nbsp;&nbsp;&nbsp;/test-destroy-env - delete the validation environment for a PR (e.g. to enable testing a deployment from a clean start after previous tests)
&nbsp;&nbsp;&nbsp;&nbsp;/help - show this help`;
await addActionComment({github}, repoOwner, repoName, prNumber, commentUser, commentLink, body);
await addActionComment({ github }, repoOwner, repoName, prNumber, commentUser, commentLink, body);
}
async function addActionComment({ github }, repoOwner, repoName, prNumber, commentUser, commentLink, message) {

838
.github/scripts/build.test.js поставляемый
Просмотреть файл

@ -1,117 +1,10 @@
const { getCommandFromComment, labelAsExternalIfAuthorDoesNotHaveWriteAccess } = require('./build.js')
const { createGitHubContext, PR_NUMBER, outputFor, toHaveComment } = require('./test-helpers.js')
const PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES = 123;
const PR_NUMBER_UPSTREAM_DOCS_ONLY_CHANGES = 125;
const PR_NUMBER_FORK_NON_DOCS_CHANGES = 456;
expect.extend({
toHaveComment
})
function createGitHubContext() {
mockGithubRestIssuesAddLabels = jest.fn();
mockGithubRestIssuesCreateComment = jest.fn();
mockGithubRestPullsListFiles = jest.fn();
mockCoreSetOutput = jest.fn();
mockCoreNotice = jest.fn();
mockCoreInfo = jest.fn();
mockCoreWarning = jest.fn();
mockCoreError = jest.fn();
return {
mockGithubRestIssuesAddLabels,
mockGithubRestIssuesCreateComment,
mockCoreSetOutput,
mockCoreInfo,
mockCoreNotice,
mockCoreWarning,
mockCoreError,
core: {
setOutput: mockCoreSetOutput,
info: mockCoreInfo,
notice: mockCoreNotice,
warning: mockCoreWarning,
error: mockCoreError,
},
github: {
paginate: (func, params) => {
// thin fake for paginate -> faked function being paginated should return a single page of data!
// if you're getting a `func is not a function` error then check that a func is being passed in
return func(params);
},
request: async (route, data) => {
if (route === 'GET /repos/{owner}/{repo}/collaborators/{username}') {
if (data.username === "admin") {
return {
status: 204
};
} else {
throw {
status: 404,
};
}
}
},
rest: {
issues: {
addLabels: mockGithubRestIssuesAddLabels,
createComment: mockGithubRestIssuesCreateComment,
},
pulls: {
get: async (params) => {
if (params.owner === 'someOwner'
&& params.repo === 'someRepo') {
switch (params.pull_number) {
case PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES: // PR from the upstream repo with non-docs changes
return {
data: {
head: {
ref: 'pr-head-ref',
sha: '0123456789',
repo: { full_name: 'someOwner/someRepo' },
},
merge_commit_sha: '123456789a',
},
}
case PR_NUMBER_UPSTREAM_DOCS_ONLY_CHANGES: // PR from the upstream repo with docs-only changes
return {
data: {
head: {
ref: 'pr-head-ref',
sha: '0123456789',
repo: { full_name: 'someOwner/someRepo' },
},
merge_commit_sha: '123456789a',
},
}
case PR_NUMBER_FORK_NON_DOCS_CHANGES: // PR from a forked repo
return {
data: {
head: {
ref: 'pr-head-ref',
sha: '23456789ab',
repo: { full_name: 'anotherOwner/someRepo' },
},
merge_commit_sha: '3456789abc',
},
}
}
}
throw 'Unhandled params in fake pulls.get: ' + JSON.stringify(params)
},
listFiles: async (params) => {
if (params.owner === 'someOwner'
&& params.repo === 'someRepo') {
switch (params.pull_number) {
case PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES: // PR from the upstream repo with non-docs changes
case PR_NUMBER_FORK_NON_DOCS_CHANGES: // PR from a forked repo
return [{ filename: 'test.py' }, { filename: 'test.md' }];
case PR_NUMBER_UPSTREAM_DOCS_ONLY_CHANGES: // PR from the upstream repo with docs-only changes
return [{ filename: 'mkdocs.yml' }, { filename: 'test.md' }, { filename: 'docs/README.md' }];
}
}
throw 'Unhandled params in fake pulls.listFiles: ' + JSON.stringify(params)
}
},
},
}
};
}
describe('getCommandFromComment', () => {
@ -123,12 +16,15 @@ describe('getCommandFromComment', () => {
({ core, github, mockCoreSetOutput, mockGithubRestIssuesCreateComment } = createGitHubContext());
});
function createCommentContext({ username, pullRequestNumber, body }) {
function createCommentContext({ username, pullRequestNumber, body, authorUsername }) {
if (!username) {
username = 'admin'; // most tests will assume admin (i.e. user can run commands)
}
if (!authorUsername) {
authorUsername = 'admin'; // most tests will assume admin (i.e. user not external)
}
if (!pullRequestNumber) {
pullRequestNumber = PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES;
pullRequestNumber = PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES;
}
if (!body) {
body = "nothing to see here";
@ -144,6 +40,9 @@ describe('getCommandFromComment', () => {
},
issue: {
number: pullRequestNumber,
user: {
login: authorUsername,
},
},
repository: {
full_name: 'someOwner/someRepo'
@ -160,23 +59,22 @@ describe('getCommandFromComment', () => {
body: '/test',
});
const command = await getCommandFromComment({ core, context, github });
expect(command).toBe('none');
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
test(`should add a comment indicating that the user cannot run commands`, async () => {
const context = createCommentContext({
username: 'non-contributor',
body: '/test',
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES,
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment.mock.calls.length).toBe(1);
const createCommentCall = mockGithubRestIssuesCreateComment.mock.calls[0];
const createCommentParam = createCommentCall[0];
expect(createCommentParam.owner).toBe("someOwner");
expect(createCommentParam.repo).toBe("someRepo");
expect(createCommentParam.issue_number).toBe(PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES);
expect(createCommentParam.body).toBe('Sorry, @non-contributor, only users with write access to the repo can run pr-bot commands.');
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
bodyMatcher: /Sorry, @non-contributor, only users with write access to the repo can run pr-bot commands./
});
});
});
@ -188,184 +86,489 @@ describe('getCommandFromComment', () => {
body: 'foo',
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('command', 'none');
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
describe('and single line comments', () => {
describe(`for '/test' for PR with non-docs changes`, () => {
describe(`for '/test' for non-external PR with non-docs changes`, () => {
test(`should set command to 'run-tests'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test',
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES,
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('command', 'run-tests');
expect(outputFor(mockCoreSetOutput, 'command')).toBe('run-tests');
});
test(`should set nonDocsChanges to 'true'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test',
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES,
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('nonDocsChanges', 'true');
expect(outputFor(mockCoreSetOutput, 'nonDocsChanges')).toBe('true');
});
test(`should add comment with run link`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test',
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES,
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment.mock.calls.length).toBe(1);
const createCommentCall = mockGithubRestIssuesCreateComment.mock.calls[0];
const createCommentParam = createCommentCall[0];
expect(createCommentParam.owner).toBe("someOwner");
expect(createCommentParam.repo).toBe("someRepo");
expect(createCommentParam.issue_number).toBe(PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES);
expect(createCommentParam.body).toMatch(/Running tests: https:\/\/github.com\/someOwner\/someRepo\/actions\/runs\/11112222 \(with refid `cbce50da`\)/);
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
bodyMatcher: /Running tests: https:\/\/github.com\/someOwner\/someRepo\/actions\/runs\/11112222 \(with refid `cbce50da`\)/,
});
});
})
});
describe(`for '/test' for PR with docs-only changes`, () => {
describe(`for '/test' for non-external PR with docs-only changes`, () => {
test(`should set command to 'test-force-approve'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test',
pullRequestNumber: PR_NUMBER_UPSTREAM_DOCS_ONLY_CHANGES,
pullRequestNumber: PR_NUMBER.UPSTREAM_DOCS_ONLY_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('command', 'test-force-approve');
expect(outputFor(mockCoreSetOutput, 'command')).toBe('test-force-approve');
});
test(`should set nonDocsChanges to 'false'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test',
pullRequestNumber: PR_NUMBER_UPSTREAM_DOCS_ONLY_CHANGES,
pullRequestNumber: PR_NUMBER.UPSTREAM_DOCS_ONLY_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('nonDocsChanges', 'false');
expect(outputFor(mockCoreSetOutput, 'nonDocsChanges')).toBe('false');
});
test(`should add comment with for skipping checks`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test',
pullRequestNumber: PR_NUMBER_UPSTREAM_DOCS_ONLY_CHANGES,
pullRequestNumber: PR_NUMBER.UPSTREAM_DOCS_ONLY_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment.mock.calls.length).toBe(1);
const createCommentCall = mockGithubRestIssuesCreateComment.mock.calls[0];
const createCommentParam = createCommentCall[0];
expect(createCommentParam.owner).toBe("someOwner");
expect(createCommentParam.repo).toBe("someRepo");
expect(createCommentParam.issue_number).toBe(PR_NUMBER_UPSTREAM_DOCS_ONLY_CHANGES);
expect(createCommentParam.body).toMatch(/PR only contains docs changes - marking tests as complete/);
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.UPSTREAM_DOCS_ONLY_CHANGES,
bodyMatcher: /PR only contains docs changes - marking tests as complete/,
});
});
});
describe(`for '/test' for non-mergeable PR`, () => {
test(`should set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test',
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_MERGEABLE,
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
test(`should add comment with for skipping checks`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test',
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_MERGEABLE,
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.UPSTREAM_NON_MERGEABLE,
bodyMatcher: /Cannot run tests as PR is not mergeable. Ensure that the PR is open and doesn't have any conflicts./,
});
});
});
describe(`for '/test' for external PR (i.e. without commit SHA specified)`, () => {
test(`should set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
test(`should add comment with prompt to specify SHA`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.FORK_NON_DOCS_CHANGES,
bodyMatcher: /When using `\/test` on external PRs, the SHA of the checked commit must be specified/,
});
});
});
describe(`for '/test 00000000' for external PR (i.e. with non-latest commit SHA specified)`, () => {
test(`should set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test 00000000',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
test(`should add comment with prompt that the SHA is out-dated`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test 00000000',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.FORK_NON_DOCS_CHANGES,
bodyMatcher: /The specified SHA `00000000` is not the latest commit on the PR. Please validate the latest commit and re-run `\/test`/,
});
});
})
test(`for '/test-extended' should set command to 'run-tests-extended'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended',
describe(`for '/test 234567' for external PR (i.e. with insufficiently long commit SHA specified)`, () => {
test(`should set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test 234567',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
test(`should add comment with prompt that the SHA is out-dated`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test 234567',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.FORK_NON_DOCS_CHANGES,
bodyMatcher: /When specifying a commit SHA it must be at least 7 characters \(received `234567`\)/,
});
});
})
describe(`for '/test 2345678' for external PR (i.e. with latest commit SHA specified)`, () => {
test(`should set command to 'run-tests'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test 2345678',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('run-tests');
});
test(`should add comment with run link`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test 2345678',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.FORK_NON_DOCS_CHANGES,
bodyMatcher: /Running tests: https:\/\/github.com\/someOwner\/someRepo\/actions\/runs\/11112222 \(with refid `6db070b1`\)/,
});
});
})
describe(`for '/test 2345678' for external PR (i.e. with latest commit SHA specified but extra space after test)`, () => {
test(`should set command to 'run-tests'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test 2345678',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('run-tests');
});
test(`should add comment with run link`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test 2345678',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.FORK_NON_DOCS_CHANGES,
bodyMatcher: /Running tests: https:\/\/github.com\/someOwner\/someRepo\/actions\/runs\/11112222 \(with refid `6db070b1`\)/,
});
});
})
describe(`for '/test-extended'`, () => {
test(`should set command to 'run-tests-extended'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('run-tests-extended');
});
test(`should add comment with run link`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended',
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
bodyMatcher: /Running extended tests: https:\/\/github.com\/someOwner\/someRepo\/actions\/runs\/11112222 \(with refid `cbce50da`\)/,
});
});
const command = await getCommandFromComment({ core, context, github });
expect(command).toBe('run-tests-extended');
});
test(`for '/test-extended' should add comment with run link`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended',
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES,
describe(`for '/test-extended' for external PR (i.e. without commit SHA specified)`, () => {
test(`should set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
test(`should add comment with prompt to specify SHA`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.FORK_NON_DOCS_CHANGES,
bodyMatcher: /When using `\/test-extended` on external PRs, the SHA of the checked commit must be specified/,
});
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment.mock.calls.length).toBe(1);
const createCommentCall = mockGithubRestIssuesCreateComment.mock.calls[0];
const createCommentParam = createCommentCall[0];
expect(createCommentParam.owner).toBe("someOwner");
expect(createCommentParam.repo).toBe("someRepo");
expect(createCommentParam.issue_number).toBe(PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES);
expect(createCommentParam.body).toMatch(/Running extended tests: https:\/\/github.com\/someOwner\/someRepo\/actions\/runs\/11112222 \(with refid `cbce50da`\)/);
});
test(`for '/test-force-approve' should set command to 'test-force-approve'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-force-approve',
describe(`for '/test-extended 00000000' for external PR (i.e. with non-latest commit SHA specified)`, () => {
test(`should set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended 00000000',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
test(`should add comment with prompt that the SHA is out-dated`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended 00000000',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.FORK_NON_DOCS_CHANGES,
bodyMatcher: /The specified SHA `00000000` is not the latest commit on the PR. Please validate the latest commit and re-run `\/test`/,
});
});
})
describe(`for '/test-extended 234567' for external PR (i.e. with insufficiently long commit SHA specified)`, () => {
test(`should set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended 234567',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
test(`should add comment with prompt that the SHA is out-dated`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended 234567',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.FORK_NON_DOCS_CHANGES,
bodyMatcher: /When specifying a commit SHA it must be at least 7 characters \(received `234567`\)/,
});
});
})
describe(`for '/test-extended 2345678' for external PR (i.e. with latest commit SHA specified)`, () => {
test(`should set command to 'run-tests-extended'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended 2345678',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('run-tests-extended');
});
test(`should add comment with run link`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-extended 2345678',
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES,
authorUsername: 'non-contributor',
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.FORK_NON_DOCS_CHANGES,
bodyMatcher: /Running extended tests: https:\/\/github.com\/someOwner\/someRepo\/actions\/runs\/11112222 \(with refid `6db070b1`\)/,
});
});
})
describe(`for '/test-force-approve'`, () => {
test(`should set command to 'test-force-approve'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-force-approve',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('test-force-approve');
});
test(`should add comment`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-force-approve',
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
bodyMatcher: /Marking tests as complete \(for commit 0123456789\)/,
});
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('command', 'test-force-approve');
});
test(`for '/test-force-approve' should add comment`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-force-approve',
describe(`for '/test-destroy-env'`, () => {
test(`should set command to 'test-destroy-env'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-destroy-env',
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('test-destroy-env');
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment.mock.calls.length).toBe(1);
const createCommentCall = mockGithubRestIssuesCreateComment.mock.calls[0];
const createCommentParam = createCommentCall[0];
expect(createCommentParam.owner).toBe("someOwner");
expect(createCommentParam.repo).toBe("someRepo");
expect(createCommentParam.issue_number).toBe(PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES);
expect(createCommentParam.body).toMatch(/Marking tests as complete \(for commit 0123456789\)/);
});
test(`for '/test-destroy-env' should set command to 'test-destroy-env'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/test-destroy-env',
describe(`for '/help'`, () => {
test(`should set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/help',
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
test(`should add help comment`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/help',
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
bodyMatcher: /Hello!\n\nYou can use the following commands:/,
});
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('command', 'test-destroy-env');
});
test(`for '/help' should add help comment and set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/help',
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES,
describe(`for '/not-a-command'`, () => {
test(`should set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/not-a-command',
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('command', 'none');
expect(mockGithubRestIssuesCreateComment.mock.calls.length).toBe(1);
const createCommentCall = mockGithubRestIssuesCreateComment.mock.calls[0];
const createCommentParam = createCommentCall[0];
expect(createCommentParam.owner).toBe("someOwner");
expect(createCommentParam.repo).toBe("someRepo");
expect(createCommentParam.issue_number).toBe(PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES);
expect(createCommentParam.body).toMatch(/Hello!\n\nYou can use the following commands:/);
});
test(`for '/not-a-command' should add help comment and set command to 'none'`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/not-a-command',
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES,
test(`should add help comment`, async () => {
const context = createCommentContext({
username: 'admin',
body: '/not-a-command',
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await getCommandFromComment({ core, context, github });
expect(mockGithubRestIssuesCreateComment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
bodyMatcher: /`\/not-a-command` is not recognised as a valid command.\n\nYou can use the following commands:/,
});
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('command', 'none');
expect(mockGithubRestIssuesCreateComment.mock.calls.length).toBe(1);
const createCommentCall = mockGithubRestIssuesCreateComment.mock.calls[0];
const createCommentParam = createCommentCall[0];
expect(createCommentParam.owner).toBe("someOwner");
expect(createCommentParam.repo).toBe("someRepo");
expect(createCommentParam.issue_number).toBe(PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES);
expect(createCommentParam.body).toMatch(/`\/not-a-command` is not recognised as a valid command.\n\nYou can use the following commands:/);
});
});
describe('and multi-line comments', () => {
test(`when first line of comment is '/test' should set command to 'run-tests'`, async () => {
const context = createCommentContext({
@ -375,7 +578,7 @@ Other comment content
goes here`,
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('command', 'run-tests');
expect(outputFor(mockCoreSetOutput, 'command')).toBe('run-tests');
});
test(`when comment doesn't start with '/' (even if later lines contain '/test') should set command to 'none' `, async () => {
@ -387,113 +590,110 @@ Other comment content
goes here`,
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('command', 'none');
expect(outputFor(mockCoreSetOutput, 'command')).toBe('none');
});
});
describe('PR context', () => {
test('should set prRef output', async () => {
// prRef should be set to the SHA for the potentialMergeCommit for the PR
const context = createCommentContext({
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'prRef')).toBe('123456789a');
});
test('should set prRefId output', async () => {
// Using a PR number of 123 should give a refid of 'cbce50da'
// Based on running `echo "refs/pull/123/merge" | shasum | cut -c1-8` (as per the original bash scripts)
const context = createCommentContext({
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'prRefId')).toBe('cbce50da');
});
test('should not set branchRefId output for PR from forked repo', async () => {
// Using PR 456 which is faked as a PR from a fork
// Since branch-based environments are only for upstream branches, the branchRefId
// output should not be set for this PR
const context = createCommentContext({
pullRequestNumber: PR_NUMBER.FORK_NON_DOCS_CHANGES
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'branchRefId')).toBe(null);
});
test('should set branchRefId for PR from upstream repo', async () => {
// Using PR 123 which is faked as a PR from the upstream repo
// The Using a PR number of 123 should give a refid of '71f7c907'
// Based on running `echo "refs/heads/pr-head-ref" | shasum | cut -c1-8` (as per the original bash scripts)
const context = createCommentContext({
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'branchRefId')).toBe('71f7c907');
});
test('should set prHeadSha output', async () => {
// prHeadSha should be the SHA for the head commit for the PR
const context = createCommentContext({
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES
});
await getCommandFromComment({ core, context, github });
expect(outputFor(mockCoreSetOutput, 'prHeadSha')).toBe('0123456789');
});
})
});
describe('PR context', () => {
test('should set prRef output', async () => {
// prRef should be set to the SHA for the potentialMergeCommit for the PR
const context = createCommentContext({
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('prRef', '123456789a');
describe('labelAsExternalIfAuthorDoesNotHaveWriteAccess', () => {
var core;
var github;
var mockGithubRestIssuesAddLabels;
beforeEach(() => {
({ core, github, mockGithubRestIssuesAddLabels } = createGitHubContext());
});
test('should set prRefId output', async () => {
// Using a PR number of 123 should give a refid of 'cbce50da'
// Based on running `echo "refs/pull/123/merge" | shasum | cut -c1-8` (as per the original bash scripts)
const context = createCommentContext({
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('prRefId', 'cbce50da');
expect(mockCoreSetOutput.mock.calls.some(c => c[0] == 'prRefId')).toBe(true);
});
test('should not set branchRefId output for PR from forked repo', async () => {
// Using PR 456 which is faked as a PR from a fork
// Since branch-based environments are only for upstream branches, the branchRefId
// output should not be set for this PR
const context = createCommentContext({
pullRequestNumber: PR_NUMBER_FORK_NON_DOCS_CHANGES
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput.mock.calls.some(c => c[0] == 'branchRefId')).toBe(false);
});
test('should set branchRefId for PR from upstream repo', async () => {
// Using PR 123 which is faked as a PR from the upstream repo
// The Using a PR number of 123 should give a refid of '71f7c907'
// Based on running `echo "refs/heads/pr-head-ref" | shasum | cut -c1-8` (as per the original bash scripts)
const context = createCommentContext({
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('branchRefId', '71f7c907');
});
test('should set prHeadSha output', async () => {
// prHeadSha should be the SHA for the head commit for the PR
const context = createCommentContext({
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES
});
await getCommandFromComment({ core, context, github });
expect(mockCoreSetOutput).toHaveBeenCalledWith('prHeadSha', '0123456789');
});
})
});
describe('labelAsExternalIfAuthorDoesNotHaveWriteAccess', () => {
var core;
var github;
var mockGithubRestIssuesAddLabels;
beforeEach(() => {
({ core, github, mockGithubRestIssuesAddLabels } = createGitHubContext());
});
function createPullRequestContext({ username, pullRequestNumber }) {
return {
payload: {
pull_request: {
user: {
login: username,
function createPullRequestContext({ username, pullRequestNumber }) {
return {
payload: {
pull_request: {
user: {
login: username,
},
number: pullRequestNumber,
},
number: pullRequestNumber,
repository: {
full_name: 'someOwner/SomeRepo'
}
},
repository: {
full_name: 'someOwner/SomeRepo'
}
},
repo: {
owner: 'someOwner',
repo: 'someRepo'
},
};
}
repo: {
owner: 'someOwner',
repo: 'someRepo'
},
};
}
test(`should apply the 'external' label for non-contributor author`, async () => {
const context = createPullRequestContext({
username: 'non-contributor',
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES,
test(`should apply the 'external' label for non-contributor author`, async () => {
const context = createPullRequestContext({
username: 'non-contributor',
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await labelAsExternalIfAuthorDoesNotHaveWriteAccess({ core, context, github });
expect(mockGithubRestIssuesAddLabels).toHaveBeenCalled(); // should set the label for non-contributor
});
await labelAsExternalIfAuthorDoesNotHaveWriteAccess({ core, context, github });
expect(mockGithubRestIssuesAddLabels).toHaveBeenCalled(); // should set the label for non-contributor
});
test(`should return not apply the 'external' label for contributor author`, async () => {
const context = createPullRequestContext({
username: 'admin',
pullRequestNumber: PR_NUMBER_UPSTREAM_NON_DOCS_CHANGES,
test(`should return not apply the 'external' label for contributor author`, async () => {
const context = createPullRequestContext({
username: 'admin',
pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES,
});
await labelAsExternalIfAuthorDoesNotHaveWriteAccess({ core, context, github });
expect(mockGithubRestIssuesAddLabels).toHaveBeenCalledTimes(0); // shouldn't set the label for contributor
});
await labelAsExternalIfAuthorDoesNotHaveWriteAccess({ core, context, github });
expect(mockGithubRestIssuesAddLabels).toHaveBeenCalledTimes(0); // shouldn't set the label for contributor
});
});

3
.github/scripts/package.json поставляемый
Просмотреть файл

@ -8,6 +8,7 @@
"jest": "^27.5.1"
},
"scripts": {
"test": "jest"
"test": "jest --verbose",
"test-watch": "jest --watchAll --verbose"
}
}

61
.github/scripts/run-tests.sh поставляемый
Просмотреть файл

@ -1,18 +1,49 @@
#!/bin/bash
set -e
set -o errexit
set -o pipefail
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
if [[ $(command -v node > /dev/null; echo $?) == 0 ]]; then
echo "node already installed"
else
echo "node not found - installing..."
"$DIR/install-node.sh"
function usage() {
cat <<USAGE
This script runs the GitHub action linter and unit tests for build.js
Dependencies (e.g. node, actionlint) will be installed if not present
Usage: $0 [--watch]
Options:
--watch Run tests in watch mode
USAGE
exit 1
}
watch_tests=false
while [ "$1" != "" ]; do
case $1 in
--watch)
watch_tests=true
;;
*)
echo "Unexpected argument: '$1'"
usage
;;
esac
if [[ -z "$2" ]]; then
# if no more args then stop processing
break
fi
shift # remove the current value for `$1` and use the next
done
test_command="test"
if ${watch_tests}
then
test_command="test-watch"
fi
echo "Running JavaScript build tests..."
(cd "$DIR" && yarn test)
script_temp_dir="$DIR/script_temp"
if [[ -x "$script_temp_dir/actionlint" ]]; then
@ -26,4 +57,16 @@ else
fi
echo "Running actionlint..."
"$script_temp_dir/actionlint"
if [[ $(command -v node > /dev/null; echo $?) == 0 ]]; then
echo "node already installed"
else
echo "node not found - installing..."
"$DIR/install-node.sh"
fi
echo "Running JavaScript build tests..."
(cd "$DIR" && yarn install && yarn "$test_command")
echo "Tests complete"

177
.github/scripts/test-helpers.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,177 @@
function lastCallFor(mock, matcher) {
const matches = mock.mock.calls.filter(matcher);
const lastMatchIndex = matches.length - 1;
if (lastMatchIndex < 0) {
return null;
}
return matches[lastMatchIndex];
}
// outputFor returns the last call to mock with the specified key
function outputFor(mock, key) {
// get last call for mock with first arg matching the key
const lastCall = lastCallFor(mock, c => c[0] === key);
if (lastCall) {
// return second arg (output value)
return lastCall[1];
}
return null;
}
function toHaveComment(received, { owner, repo, issue_number, bodyMatcher }) {
const commentsForIssue = received.mock.calls
.map(c => c[0])
.filter(arg => arg.owner === owner && arg.repo === repo && arg.issue_number === issue_number);
if (commentsForIssue.length === 0) {
return { message: () => `No comments found for issue ${JSON.stringify({ owner, repo, issue_number })}`, pass: false };
}
const gotMatchingBody = commentsForIssue.some(c => bodyMatcher.exec(c.body) !== null)
if (!gotMatchingBody) {
const lastComment = commentsForIssue[commentsForIssue.length - 1]
return { message: () => `No comment found with body matching ${bodyMatcher.toString()}. Last comment for issue: '${lastComment.body}'`, pass: false };
}
return { message: '', pass: true };
}
const PR_NUMBER = {
UPSTREAM_NON_DOCS_CHANGES: 123,
UPSTREAM_DOCS_ONLY_CHANGES: 125,
FORK_NON_DOCS_CHANGES: 456,
UPSTREAM_NON_MERGEABLE: 600,
}
function createGitHubContext() {
mockGithubRestIssuesAddLabels = jest.fn();
mockGithubRestIssuesCreateComment = jest.fn();
mockGithubRestPullsListFiles = jest.fn();
mockCoreSetOutput = jest.fn();
mockCoreNotice = jest.fn();
mockCoreInfo = jest.fn();
mockCoreWarning = jest.fn();
mockCoreError = jest.fn();
return {
mockGithubRestIssuesAddLabels,
mockGithubRestIssuesCreateComment,
mockCoreSetOutput,
mockCoreInfo,
mockCoreNotice,
mockCoreWarning,
mockCoreError,
core: {
setOutput: mockCoreSetOutput,
info: mockCoreInfo,
notice: mockCoreNotice,
warning: mockCoreWarning,
error: mockCoreError,
},
github: {
paginate: (func, params) => {
// thin fake for paginate -> faked function being paginated should return a single page of data!
// if you're getting a `func is not a function` error then check that a func is being passed in
return func(params);
},
request: async (route, data) => {
if (route === 'GET /repos/{owner}/{repo}/collaborators/{username}') {
if (data.username === "admin") {
return {
status: 204
};
} else {
throw {
status: 404,
};
}
}
},
rest: {
issues: {
addLabels: mockGithubRestIssuesAddLabels,
createComment: mockGithubRestIssuesCreateComment,
},
pulls: {
get: async (params) => {
if (params.owner === 'someOwner'
&& params.repo === 'someRepo') {
switch (params.pull_number) {
case PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES: // PR from the upstream repo with non-docs changes
return {
data: {
head: {
ref: 'pr-head-ref',
sha: '0123456789',
repo: { full_name: 'someOwner/someRepo' },
},
merge_commit_sha: '123456789a',
mergeable: true,
},
}
case PR_NUMBER.UPSTREAM_DOCS_ONLY_CHANGES: // PR from the upstream repo with docs-only changes
return {
data: {
head: {
ref: 'pr-head-ref',
sha: '0123456789',
repo: { full_name: 'someOwner/someRepo' },
},
merge_commit_sha: '123456789a',
mergeable: true,
},
}
case PR_NUMBER.FORK_NON_DOCS_CHANGES: // PR from a forked repo
return {
data: {
head: {
ref: 'pr-head-ref',
sha: '23456789ab',
repo: { full_name: 'anotherOwner/someRepo' },
},
merge_commit_sha: '3456789abc',
mergeable: true,
},
}
case PR_NUMBER.UPSTREAM_NON_MERGEABLE: // PR with mergable==false
return {
data: {
head: {
ref: 'pr-head-ref',
sha: '23456789ab',
repo: { full_name: 'anotherOwner/someRepo' },
},
merge_commit_sha: '3456789abc',
mergeable: false,
},
}
}
}
throw 'Unhandled params in fake pulls.get: ' + JSON.stringify(params)
},
listFiles: async (params) => {
if (params.owner === 'someOwner'
&& params.repo === 'someRepo') {
switch (params.pull_number) {
case PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES:
case PR_NUMBER.FORK_NON_DOCS_CHANGES:
case PR_NUMBER.UPSTREAM_NON_MERGEABLE:
return [{ filename: 'test.py' }, { filename: 'test.md' }];
case PR_NUMBER.UPSTREAM_DOCS_ONLY_CHANGES:
return [{ filename: 'mkdocs.yml' }, { filename: 'test.md' }, { filename: 'docs/README.md' }];
}
}
throw 'Unhandled params in fake pulls.listFiles: ' + JSON.stringify(params)
}
},
},
}
};
}
module.exports = {
lastCallFor,
outputFor,
toHaveComment,
createGitHubContext,
PR_NUMBER,
}

161
.github/scripts/test-helpers.test.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,161 @@
const { lastCallFor, outputFor, toHaveComment } = require('./test-helpers.js')
describe('helper tests', () => {
describe('lastCallFor', () => {
var testMock;
beforeEach(() => {
testMock = jest.fn();
});
test('single call matches', () => {
testMock('key', 'value');
expect(lastCallFor(testMock, c => c[0] === 'key')[1]).toBe('value');
})
test('multiple calls matches last value', () => {
testMock('key', 'value');
testMock('key', 'value2');
expect(lastCallFor(testMock, c => c[0] === 'key')[1]).toBe('value2');
})
})
describe('outputFor', () => {
var setOutput;
beforeEach(() => {
setOutput = jest.fn();
});
test('no calls returns null', () => {
expect(outputFor(setOutput, 'key')).toBe(null);
})
test('single call matches', () => {
setOutput('key', 'value');
expect(outputFor(setOutput, 'key')).toBe('value');
})
test('multiple calls matches last value', () => {
setOutput('key', 'value');
setOutput('key', 'value2');
expect(outputFor(setOutput, 'key')).toBe('value2');
})
})
describe('toHaveComment', () => {
expect.extend({
toHaveComment
})
var add_comment;
beforeEach(() => {
add_comment = jest.fn();
});
test('no calls returns false', () => {
expect(add_comment).not.toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 123,
bodyMatcher: /test/,
});
});
test('single matching call returns true', () => {
add_comment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 123,
body: 'This is a test',
})
expect(add_comment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 123,
bodyMatcher: /test/,
});
});
test('single non-matching call returns false', () => {
add_comment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 1234,
body: 'This is a test',
})
expect(add_comment).not.toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 123,
bodyMatcher: /test/,
});
});
test('multiple calls, none matching, returns false', () => {
add_comment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 1234,
body: 'This is a test',
})
add_comment({
owner: 'someOtherOwner',
repo: 'someRepo',
issue_number: 123,
body: 'This is a test',
})
add_comment({
owner: 'someOwner',
repo: 'someOtherRepo',
issue_number: 123,
body: 'This is a test',
})
add_comment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 1234,
body: 'This is a test',
})
expect(add_comment).not.toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 123,
bodyMatcher: /test/,
});
});
test('multiple calls, with matching, returns true', () => {
add_comment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 1234,
body: 'This is a test',
})
add_comment({
owner: 'someOtherOwner',
repo: 'someRepo',
issue_number: 123,
body: 'This is a test',
})
add_comment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 123,
body: 'This is a test',
})
add_comment({
owner: 'someOwner',
repo: 'someOtherRepo',
issue_number: 123,
body: 'This is a test',
})
expect(add_comment).toHaveComment({
owner: 'someOwner',
repo: 'someRepo',
issue_number: 123,
bodyMatcher: /test/,
});
});
})
})

Просмотреть файл

@ -15,10 +15,15 @@ These commands can only be run when commented by a user who is identified as a r
This command will cause the pr-comment-bot to respond with a comment listing the available commands.
### `/test`
### `/test [<sha>]`
This command runs the build, deploy, and smoke tests for a PR.
For PRs from maintainers (i.e. users with write access to microsoft/AzureTRE), `/test` is sufficient.
For other PRs, the checks below should be carried out. Once satisfied that the PR is safe to run tests against, you should use `/test <sha>` where `<sha>` is the SHA for the commit that you have verified.
You can use the full or short form of the SHA, but it must be at least 7 characters (GitHub UI shows 7 characters).
**IMPORTANT**
This command works on PRs from forks, and makes the deployment secrets available.
@ -30,10 +35,15 @@ Check for changes to anything that is run during the build/deploy/test cycle, in
- modifications to scripts
- new python packages being installed
### `/test-extended`
### `/test-extended [<sha>]`
This command runs the build, deploy, and smoke & extended tests for a PR.
For PRs from maintainers (i.e. users with write access to microsoft/AzureTRE), `/test-extended` is sufficient.
For other PRs, the checks below should be carried out. Once satisfied that the PR is safe to run tests against, you should use `/test-extended <sha>` where `<sha>` is the SHA for the commit that you have verified.
You can use the full or short form of the SHA, but it must be at least 7 characters (GitHub UI shows 7 characters).
**IMPORTANT**
As with `/test`, this command works on PRs from forks, and makes the deployment secrets available.