diff --git a/.github/workflows/test-action.yml b/.github/workflows/test-action.yml index 6ce4937c..1f36a3f9 100644 --- a/.github/workflows/test-action.yml +++ b/.github/workflows/test-action.yml @@ -7,6 +7,9 @@ on: pull_request_target: branches: - master + issue_comment: + types: + - created permissions: checks: write @@ -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: diff --git a/src/git.js b/src/git.js index a67cb312..d8ed78aa 100644 --- a/src/git.js +++ b/src/git.js @@ -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}`); } /** diff --git a/src/github/api.js b/src/github/api.js index c28b7a36..addb8b42 100644 --- a/src/github/api.js +++ b/src/github/api.js @@ -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 }; diff --git a/src/github/context.js b/src/github/context.js index 0f19c694..2e9fbadd 100644 --- a/src/github/context.js +++ b/src/github/context.js @@ -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 @@ -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`); } @@ -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 { @@ -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} - 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} 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, }; @@ -136,4 +170,5 @@ module.exports = { parseBranch, parseEnvFile, parseRepository, + parsePullRequest, }; diff --git a/src/index.js b/src/index.js index ee488adb..4865d46c 100644 --- a/src/index.js +++ b/src/index.js @@ -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"; @@ -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.", ); diff --git a/test/github/context.test.js b/test/github/context.test.js index 04dd22d4..d9542880 100644 --- a/test/github/context.test.js +++ b/test/github/context.test.js @@ -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"); @@ -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", () => { @@ -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, @@ -113,8 +118,10 @@ 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, @@ -122,7 +129,7 @@ describe("parseRepository()", () => { 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, @@ -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, @@ -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,