Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Pull Request comment event #583

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
22 changes: 22 additions & 0 deletions .github/workflows/test-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:
pull_request_target:
branches:
- master
issue_comment:
types:
- created

permissions:
checks: write
Expand Down Expand Up @@ -36,6 +39,25 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Get Pull Request (issue_comment)
if: github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, 'run-lint')
id: get-pr
uses: actions/github-script@v6.3.3
with:
retries: 2
script: |
return github.rest.pulls.get({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
})

- name: Check out repository (issue_comment)
if: github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, 'run-lint')
uses: actions/checkout@v3
with:
ref: ${{ fromJson(steps.get-pr.outputs.result).data.head.ref }}

- name: Set up Node.js
uses: actions/setup-node@v3
with:
Expand Down
3 changes: 2 additions & 1 deletion src/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ function checkOutRemoteBranch(context) {

// Switch to remote branch
core.info(`Switching to the "${context.branch}" branch`);
run(`git branch --force ${context.branch} --track ${remote}/${context.branch}`);
// run(`git branch --force ${context.branch} --track ${remote}/${context.branch}`);
run(`git checkout ${context.branch}`);
run(`git reset --hard ${remote}/${context.branch}`);
}

/**
Expand Down
50 changes: 49 additions & 1 deletion src/github/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,52 @@ async function createCheck(linterName, sha, context, lintResult, neutralCheckOnW
}
}

module.exports = { createCheck };
/**
* Fetches a Pull Request on GitHub
* @param {string} repository - The Github Repository with user name ex: wearerequired/master
* @param {string} pullRequestNumber - SHA of the commit which should be annotated
* @param {string} token - github token for authentication
* @returns {object} pull request information
*/
async function fetchPullRequest(repository, pullRequestNumber, token) {
try {
core.info(`fetchPullRequest for owner ${repository}`);
const response = await request(
`${process.env.GITHUB_API_URL}/repos/${repository}/pulls/${pullRequestNumber}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${token}`,
"User-Agent": actionName,
},
},
);
core.info(
`fetchPullRequest completed successfully for owner:${repository} pull:${pullRequestNumber}`,
);
return response.data;
} catch (err) {
let errorMessage = err.message;
if (err.data) {
try {
const errorData = JSON.parse(err.data);
if (errorData.message) {
errorMessage += `. ${errorData.message}`;
}
if (errorData.documentation_url) {
errorMessage += ` ${errorData.documentation_url}`;
}
} catch (e) {
// Ignore
}
}
core.error(errorMessage);
throw new Error(
`Error while trying to fetch PR for owner:${repository} pull:${pullRequestNumber}`,
);
}
}

module.exports = { createCheck, fetchPullRequest };
57 changes: 46 additions & 11 deletions src/github/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const core = require("@actions/core");

const { name: actionName } = require("../../package.json");
const { getEnv } = require("../utils/action");
const { fetchPullRequest } = require("./api");

/**
* GitHub Actions workflow's environment variables
Expand Down Expand Up @@ -69,14 +70,23 @@ function parseEnvFile(eventPath) {
* Parses the name of the current branch from the GitHub webhook event
* @param {string} eventName - GitHub event type
* @param {object} event - GitHub webhook event payload
* @param {object | undefined} pullRequest - pull request payload associated to event
* @returns {string} - Branch name
*/
function parseBranch(eventName, event) {
function parseBranch(eventName, event, pullRequest) {
if (eventName === "push" || eventName === "workflow_dispatch") {
return event.ref.substring(11); // Remove "refs/heads/" from start of string
}
if (eventName === "pull_request" || eventName === "pull_request_target") {
return event.pull_request.head.ref;
return pullRequest.head.ref;
}
if (eventName === "issue_comment") {
if (event.issue.pull_request) {
return pullRequest.head.ref;
}
throw Error(
`${actionName} does not support issue_comment event that is not associated to a PR`,
);
}
throw Error(`${actionName} does not support "${eventName}" GitHub events`);
}
Expand All @@ -86,20 +96,26 @@ function parseBranch(eventName, event) {
* Fork detection is only supported for the "pull_request" event
* @param {string} eventName - GitHub event type
* @param {object} event - GitHub webhook event payload
* @param {object | undefined} pullRequest - pull request payload associated to event
* @returns {GithubRepository} - Information about the GitHub repository and its fork (if it exists)
*/
function parseRepository(eventName, event) {
function parseRepository(eventName, event, pullRequest) {
const repoName = event.repository.full_name;
const cloneUrl = event.repository.clone_url;
let forkName;
let forkCloneUrl;
if (eventName === "pull_request" || eventName === "pull_request_target") {

if (
eventName === "pull_request" ||
eventName === "pull_request_target" ||
(eventName === "issue_comment" && event.issue.pull_request)
) {
// "pull_request" events are triggered on the repository where the PR is made. The PR branch can
// be on the same repository (`forkRepository` is set to `null`) or on a fork (`forkRepository`
// is defined)
const headRepoName = event.pull_request.head.repo.full_name;
const headRepoName = pullRequest.head.repo.full_name;
forkName = repoName === headRepoName ? undefined : headRepoName;
const headForkCloneUrl = event.pull_request.head.repo.clone_url;
const headForkCloneUrl = pullRequest.head.repo.clone_url;
forkCloneUrl = cloneUrl === headForkCloneUrl ? undefined : headForkCloneUrl;
}
return {
Expand All @@ -111,20 +127,38 @@ function parseRepository(eventName, event) {
};
}

/**
* Parses the name of the current branch from the GitHub webhook event
* @param {string} eventName - GitHub event type
* @param {object} event - GitHub webhook event payload
* @param {string} token - GitHub token
* @returns {Promise<object | undefined>} - The payload corresponding to the pull request
*/
async function parsePullRequest(eventName, event, token) {
if (eventName === "pull_request" || eventName === "pull_request_target") {
return event.pull_request;
}
if (eventName === "issue_comment" && event.issue.pull_request) {
return fetchPullRequest(event.repository.full_name, event.issue.number, token);
}
return undefined;
}

/**
* Returns information about the GitHub repository and action trigger event
* @returns {GithubContext} context - Information about the GitHub repository and action trigger
* event
* @returns {Promise<GithubContext>} context - Information about the GitHub repository
* and action trigger event
*/
function getContext() {
async function getContext() {
const { actor, eventName, eventPath, token, workspace } = parseActionEnv();
const event = parseEnvFile(eventPath);
const pullRequest = await parsePullRequest(eventName, event, token);
return {
actor,
branch: parseBranch(eventName, event),
branch: await parseBranch(eventName, event, pullRequest),
event,
eventName,
repository: parseRepository(eventName, event),
repository: parseRepository(eventName, event, pullRequest),
token,
workspace,
};
Expand All @@ -136,4 +170,5 @@ module.exports = {
parseBranch,
parseEnvFile,
parseRepository,
parsePullRequest,
};
12 changes: 9 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const { getSummary } = require("./utils/lint-result");
* Parses the action configuration and runs all enabled linters on matching files
*/
async function runAction() {
const context = getContext();
const context = await getContext();
const autoFix = core.getInput("auto_fix") === "true";
const commit = core.getInput("commit") === "true";
const skipVerification = core.getInput("git_no_verify") === "true";
Expand All @@ -24,10 +24,16 @@ async function runAction() {
const checkName = core.getInput("check_name", { required: true });
const neutralCheckOnWarning = core.getInput("neutral_check_on_warning") === "true";
const isPullRequest =
context.eventName === "pull_request" || context.eventName === "pull_request_target";
context.eventName === "pull_request" ||
context.eventName === "pull_request_target" ||
(context.eventName === "issue_comment" && context.event.issue.pull_request);

// If on a PR from fork: Display messages regarding action limitations
if (context.eventName === "pull_request" && context.repository.hasFork) {
if (
(context.eventName === "pull_request" ||
(context.eventName === "issue_comment" && context.event.issue.pull_request)) &&
context.repository.hasFork
) {
core.error(
"This action does not have permission to create annotations on forks. You may want to run it only on `pull_request_target` events with checks permissions set to write. See https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions for details.",
);
Expand Down
35 changes: 22 additions & 13 deletions test/github/context.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
parseBranch,
parseEnvFile,
parseRepository,
parsePullRequest,
} = require("../../src/github/context");
const prOpenEvent = require("./events/pull-request-open.json");
const prSyncEvent = require("./events/pull-request-sync.json");
Expand Down Expand Up @@ -87,13 +88,16 @@ describe("parseEnvFile()", () => {
});

describe("parseBranch()", () => {
test('works with "push" event', () => {
expect(parseBranch("push", pushEvent)).toEqual(BRANCH);
test('works with "push" event', async () => {
const pr = await parsePullRequest("push", pushEvent);
expect(parseBranch("push", pushEvent, pr)).toEqual(BRANCH);
});

test('works with "pull_request" event', () => {
expect(parseBranch("pull_request", prOpenEvent)).toEqual(BRANCH);
expect(parseBranch("pull_request", prSyncEvent)).toEqual(BRANCH);
test('works with "pull_request" event', async () => {
const prOpen = await parsePullRequest("pull_request", prOpenEvent);
const prSync = await parsePullRequest("pull_request", prSyncEvent);
expect(parseBranch("pull_request", prOpenEvent, prOpen)).toEqual(BRANCH);
expect(parseBranch("pull_request", prSyncEvent, prSync)).toEqual(BRANCH);
});

test("throws error for unsupported event", () => {
Expand All @@ -102,9 +106,10 @@ describe("parseBranch()", () => {
});

describe("parseRepository()", () => {
test('works with "push" event', () => {
test('works with "push" event', async () => {
// Fork detection is not supported for "push" events
expect(parseRepository("push", pushEvent)).toEqual({
const pr = await parsePullRequest("push", pushEvent);
expect(parseRepository("push", pushEvent, pr)).toEqual({
repoName: REPOSITORY,
cloneUrl: `https://github.com/${REPOSITORY}.git`,
forkName: undefined,
Expand All @@ -113,16 +118,18 @@ describe("parseRepository()", () => {
});
});

test('works with "pull_request" event on repository without fork', () => {
expect(parseRepository("pull_request", prOpenEvent)).toEqual({
test('works with "pull_request" event on repository without fork', async () => {
const prOpen = await parsePullRequest("pull_request", prOpenEvent);
const prSync = await parsePullRequest("pull_request", prSyncEvent);
expect(parseRepository("pull_request", prOpenEvent, prOpen)).toEqual({
repoName: REPOSITORY,
cloneUrl: `https://github.com/${REPOSITORY}.git`,
forkName: undefined,
forkCloneUrl: undefined,
hasFork: false,
});

expect(parseRepository("pull_request", prSyncEvent)).toEqual({
expect(parseRepository("pull_request", prSyncEvent, prSync)).toEqual({
repoName: REPOSITORY,
cloneUrl: `https://github.com/${REPOSITORY}.git`,
forkName: undefined,
Expand All @@ -131,11 +138,12 @@ describe("parseRepository()", () => {
});
});

test('works with "pull_request" event on repository with fork', () => {
test('works with "pull_request" event on repository with fork', async () => {
const prOpenEventMod = { ...prOpenEvent };
prOpenEventMod.pull_request.head.repo.full_name = FORK_REPOSITORY;
prOpenEventMod.pull_request.head.repo.clone_url = `https://github.com/${FORK_REPOSITORY}.git`;
expect(parseRepository("pull_request", prOpenEventMod)).toEqual({
const prOpenMod = await parsePullRequest("pull_request", prOpenEventMod);
expect(parseRepository("pull_request", prOpenEventMod, prOpenMod)).toEqual({
repoName: REPOSITORY,
cloneUrl: `https://github.com/${REPOSITORY}.git`,
forkName: FORK_REPOSITORY,
Expand All @@ -146,7 +154,8 @@ describe("parseRepository()", () => {
const prSyncEventMod = { ...prSyncEvent };
prSyncEventMod.pull_request.head.repo.full_name = FORK_REPOSITORY;
prSyncEventMod.pull_request.head.repo.clone_url = `https://github.com/${FORK_REPOSITORY}.git`;
expect(parseRepository("pull_request", prSyncEventMod)).toEqual({
const prSyncMod = await parsePullRequest("pull_request", prOpenEventMod);
expect(parseRepository("pull_request", prSyncEventMod, prSyncMod)).toEqual({
repoName: REPOSITORY,
cloneUrl: `https://github.com/${REPOSITORY}.git`,
forkName: FORK_REPOSITORY,
Expand Down