diff --git a/README.md b/README.md index e4888ae..3ba42cd 100644 --- a/README.md +++ b/README.md @@ -27,17 +27,6 @@ jobs: runs-on: ubuntu-latest steps: - # Perform a minimal checkout to set up this repository for the rest of the - # workflow. - # - # This step must be configured exactly as below: using different - # actions/checkout arguments is not supported. - - name: Checkout this repository - uses: actions/checkout@v4 - with: - filter: tree:0 - - # Run the estimation tool - name: Run sizeup # TODO: Replace the version below with your desired version. uses: lerebear/sizeup-action@v0.4.2 diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 7ed6295..315d725 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -9,21 +9,31 @@ import * as core from '@actions/core' import * as main from '../src/main' import * as initializer from '../src/initializer' +import { Git } from '../src/git' import * as github from '@actions/github' function pullRequestEventContext(overrides = {}): object { return { eventName: 'pull_request', + repo: { + owner: 'lerebear', + name: 'sizeup-action' + }, payload: { pull_request: { base: { + ref: 'main', repo: { + full_name: 'lerebear/sizeup-action', name: 'sizeup-action', owner: { login: 'lerebear' } } }, + head: { + ref: 'topic' + }, labels: [], number: 1, user: { @@ -69,9 +79,12 @@ describe('action', () => { // Shallow clone original @actions/github context const originalContext = { ...github.context } + // Mock cloning the repo + jest.spyOn(Git.prototype, 'clone').mockImplementation(async () => {}) + // Mock the diff that we use for evaluation. jest - .spyOn(initializer, 'fetchDiff') + .spyOn(Git.prototype, 'diff') .mockImplementation(async () => Promise.resolve( '--- README.md 2023-10-16 16:35:38\n+++ README-AGAIN.md 2023-10-16 16:36:07\n@@ -0,0 +1 @@\n+# Hello, World!' diff --git a/badges/coverage.svg b/badges/coverage.svg index d179b1d..344c2dc 100644 --- a/badges/coverage.svg +++ b/badges/coverage.svg @@ -1 +1 @@ -Coverage: 63.23%Coverage63.23% \ No newline at end of file +Coverage: 64.49%Coverage64.49% \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 8cef7fe..e38cc15 100644 --- a/dist/index.js +++ b/dist/index.js @@ -16317,6 +16317,95 @@ function wrappy (fn, cb) { } +/***/ }), + +/***/ 6350: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.Git = void 0; +const core = __importStar(__nccwpck_require__(2186)); +const lib = __importStar(__nccwpck_require__(9103)); +class Git { + client; + constructor() { + const basicCredential = Buffer.from(`x-access-token:${core.getInput('token')}`, 'utf8').toString('base64'); + const authorizationHeader = `AUTHORIZATION: basic ${basicCredential}`; + core.setSecret(basicCredential); + this.client = lib.simpleGit('.', { + trimmed: true, + config: [`http.extraheader=${authorizationHeader}`] + }); + } + /** + * Clones the repository from which this workflow was triggered. + * + * @param repo The repository to clone in the format "/" + * @param headRef The single branch to clone, which should correspond to the + * head ref of the pull request that triggered this workflow. This is + * required for efficiency. + * @param targetDirectory The directory in which to clone the repository. + */ + async clone(repo, headRef, targetDirectory = '.') { + core.info(`Cloning ${repo} with the single branch "${headRef}"`); + await this.client.clone(`https://github.com/${repo}`, targetDirectory, [ + `--branch=${headRef}`, + '--filter=tree:0', + '--no-tags', + '--single-branch' + ]); + } + /** + * Retrieves the diff of the pull request that triggered this workflow which we + * will use for evaluation. + * + * @param baseRef The base branch relative to which we should produce a diff. This method assumes + * that the head ref containing the changes has already been fetched. + * @returns The diff of the given pull request or `undefined` if we failed to retrieve it. + */ + async diff(baseRef) { + core.debug(`Fetching base ref "${baseRef}"`); + await this.client.fetch([ + 'origin', + `+${baseRef}:${baseRef}`, + `--filter=tree:0`, + '--no-tags', + '--prune', + '--no-recurse-submodules' + ]); + const diffArgs = ['--merge-base', baseRef].concat(core.getInput('git-diff-options').split(/\s+/)); + core.info(`Retrieving diff with \`git diff ${diffArgs.join(' ')}\``); + return this.client.diff(diffArgs); + } +} +exports.Git = Git; + + /***/ }), /***/ 9477: @@ -16348,13 +16437,11 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.fetchDiff = exports.pullRequestAuthorHasNotOptedIn = exports.workflowTriggeredForUnsupportedEvent = exports.loadConfiguration = void 0; +exports.pullRequestAuthorHasNotOptedIn = exports.loadConfiguration = void 0; const core = __importStar(__nccwpck_require__(2186)); -const github = __importStar(__nccwpck_require__(5438)); const fs = __importStar(__nccwpck_require__(7147)); const path = __importStar(__nccwpck_require__(1017)); const YAML = __importStar(__nccwpck_require__(4083)); -const simple_git_1 = __nccwpck_require__(9103); /** * Loads either the configuration file provided to the workflow, or the default * configuration file from "./config/default.yaml". @@ -16373,69 +16460,17 @@ function loadConfiguration() { return YAML.parse(fs.readFileSync(configFile, 'utf8')); } exports.loadConfiguration = loadConfiguration; -function workflowTriggeredForUnsupportedEvent() { - if (github.context.eventName !== 'pull_request') { - core.setFailed("This action is only supported on the 'pull_request' event, " + - `but it was triggered for '${github.context.eventName}'`); - return true; - } - return false; -} -exports.workflowTriggeredForUnsupportedEvent = workflowTriggeredForUnsupportedEvent; -function pullRequestAuthorHasNotOptedIn(config, pullRequest) { +function pullRequestAuthorHasNotOptedIn(config, pull) { const usersWhoHaveOptedin = config.optIns || []; if (usersWhoHaveOptedin.length && - !usersWhoHaveOptedin.find((login) => login === pullRequest.user.login)) { - core.info(`Skipping evaluation because pull request author @${pullRequest.user.login} has not opted` + + !usersWhoHaveOptedin.find((login) => login === pull.user.login)) { + core.info(`Skipping evaluation because pull request author @${pull.user.login} has not opted` + ' into this workflow'); return true; } return false; } exports.pullRequestAuthorHasNotOptedIn = pullRequestAuthorHasNotOptedIn; -/** - * Retrieves the diff of the pull request that triggered this workflow which we - * will use for evaluation. - * - * @param pull The pull request that triggered this workflow. - * @returns The diff of the given pull request or `undefined` if we failed to retrieve it. - */ -async function fetchDiff(pull) { - const git = (0, simple_git_1.simpleGit)('.', { trimmed: true }); - // let baseRefExists = false - // try { - // baseRefExists = !!(await git.raw('rev-parse', '--verify', pull.base.ref)) - // } catch (e) { - // core.error( - // `Error from 'git rev-parse --verfy ${pull.base.ref}': ${ - // (e as Error).message - // }` - // ) - // } - // if (!baseRefExists) { - // core.setFailed( - // `Could not find pull request base branch ${pull.base.ref}. ` + - // `Please make sure actions/checkout was used beforehand to fetch ${pull.base.ref}.` - // ) - // return - // } - core.debug(`Fetching base ref "${pull.base.ref}" and head ref "${pull.head.ref}"`); - await git.fetch([ - 'origin', - `+${pull.base.ref}:${pull.base.ref}`, - `+${pull.head.ref}:${pull.head.ref}`, - `--filter=tree:0`, - '--no-tags', - '--prune', - '--no-recurse-submodules' - ]); - core.debug(`Switching to head ref "${pull.head.ref}"`); - await git.raw('switch', pull.head.ref); - const diffArgs = ['--merge-base', pull.base.ref].concat(core.getInput('git-diff-options').split(/\s+/)); - core.info(`Retrieving diff with \`git diff ${diffArgs.join(' ')}\``); - return git.diff(diffArgs); -} -exports.fetchDiff = fetchDiff; /***/ }), @@ -16477,6 +16512,7 @@ const YAML = __importStar(__nccwpck_require__(4083)); const fs = __importStar(__nccwpck_require__(7147)); const path = __importStar(__nccwpck_require__(1017)); const initializer_1 = __nccwpck_require__(9477); +const git_1 = __nccwpck_require__(6350); const DEFAULT_LABEL_PREFIX = 'sizeup/'; const DEFAULT_COMMENT_TEMPLATE = ` 👋 @{{author}} this pull request exceeds the configured reviewability score threshold of {{threshold}}. Your actual score was {{score}}. @@ -16493,13 +16529,18 @@ const DEFAULT_SCORE_THRESHOLD = 100; */ async function run() { try { - if ((0, initializer_1.workflowTriggeredForUnsupportedEvent)()) + if (github.context.eventName !== 'pull_request') { + core.setFailed("This action is only supported on the 'pull_request' event, " + + `but it was triggered for '${github.context.eventName}'`); return; - const config = (0, initializer_1.loadConfiguration)(); + } const pullRequest = github.context.payload.pull_request; + const git = new git_1.Git(); + await git.clone(pullRequest.base.repo.full_name, pullRequest.head.ref); + const config = (0, initializer_1.loadConfiguration)(); if ((0, initializer_1.pullRequestAuthorHasNotOptedIn)(config, pullRequest)) return; - const diff = await (0, initializer_1.fetchDiff)(pullRequest); + const diff = await git.diff(pullRequest.base.ref); if (!diff) return; const score = await evaluatePullRequest(pullRequest, diff, config); @@ -16523,7 +16564,7 @@ exports.run = run; * @returns The score that the pull request received */ async function evaluatePullRequest(pull, diff, config) { - const pullRequestNickname = `${pull.base.repo.owner.login}/${pull.base.repo.name}#${pull.number}`; + const pullRequestNickname = `${pull.base.repo.full_name}#${pull.number}`; core.info(`Evaluating pull request ${pullRequestNickname}`); let sizeupConfigFile = undefined; if (config.sizeup) { diff --git a/src/git.ts b/src/git.ts new file mode 100644 index 0000000..0d2b15d --- /dev/null +++ b/src/git.ts @@ -0,0 +1,70 @@ +import * as core from '@actions/core' +import * as lib from 'simple-git' + +export class Git { + private client: lib.SimpleGit + + constructor() { + const basicCredential = Buffer.from( + `x-access-token:${core.getInput('token')}`, + 'utf8' + ).toString('base64') + const authorizationHeader = `AUTHORIZATION: basic ${basicCredential}` + core.setSecret(basicCredential) + + this.client = lib.simpleGit('.', { + trimmed: true, + config: [`http.extraheader=${authorizationHeader}`] + }) + } + + /** + * Clones the repository from which this workflow was triggered. + * + * @param repo The repository to clone in the format "/" + * @param headRef The single branch to clone, which should correspond to the + * head ref of the pull request that triggered this workflow. This is + * required for efficiency. + * @param targetDirectory The directory in which to clone the repository. + */ + async clone( + repo: string, + headRef: string, + targetDirectory = '.' + ): Promise { + core.info(`Cloning ${repo} with the single branch "${headRef}"`) + + await this.client.clone(`https://github.com/${repo}`, targetDirectory, [ + `--branch=${headRef}`, + '--filter=tree:0', + '--no-tags', + '--single-branch' + ]) + } + + /** + * Retrieves the diff of the pull request that triggered this workflow which we + * will use for evaluation. + * + * @param baseRef The base branch relative to which we should produce a diff. This method assumes + * that the head ref containing the changes has already been fetched. + * @returns The diff of the given pull request or `undefined` if we failed to retrieve it. + */ + async diff(baseRef: string): Promise { + core.debug(`Fetching base ref "${baseRef}"`) + await this.client.fetch([ + 'origin', + `+${baseRef}:${baseRef}`, + `--filter=tree:0`, + '--no-tags', + '--prune', + '--no-recurse-submodules' + ]) + + const diffArgs = ['--merge-base', baseRef].concat( + core.getInput('git-diff-options').split(/\s+/) + ) + core.info(`Retrieving diff with \`git diff ${diffArgs.join(' ')}\``) + return this.client.diff(diffArgs) + } +} diff --git a/src/initializer.ts b/src/initializer.ts index c4eb934..f72286c 100644 --- a/src/initializer.ts +++ b/src/initializer.ts @@ -1,10 +1,8 @@ import * as core from '@actions/core' -import * as github from '@actions/github' import { Configuration } from './configuration' import * as fs from 'fs' import * as path from 'path' import * as YAML from 'yaml' -import { simpleGit } from 'simple-git' import { PullRequest } from '@octokit/webhooks-types' // eslint-disable-line import/no-unresolved /** @@ -26,32 +24,18 @@ export function loadConfiguration(): Configuration { return YAML.parse(fs.readFileSync(configFile, 'utf8')) as Configuration } -export function workflowTriggeredForUnsupportedEvent(): boolean { - if (github.context.eventName !== 'pull_request') { - core.setFailed( - "This action is only supported on the 'pull_request' event, " + - `but it was triggered for '${github.context.eventName}'` - ) - return true - } - - return false -} - export function pullRequestAuthorHasNotOptedIn( config: Configuration, - pullRequest: PullRequest + pull: PullRequest ): boolean { const usersWhoHaveOptedin = config.optIns || [] if ( usersWhoHaveOptedin.length && - !usersWhoHaveOptedin.find( - (login: string) => login === pullRequest.user.login - ) + !usersWhoHaveOptedin.find((login: string) => login === pull.user.login) ) { core.info( - `Skipping evaluation because pull request author @${pullRequest.user.login} has not opted` + + `Skipping evaluation because pull request author @${pull.user.login} has not opted` + ' into this workflow' ) return true @@ -59,58 +43,3 @@ export function pullRequestAuthorHasNotOptedIn( return false } - -/** - * Retrieves the diff of the pull request that triggered this workflow which we - * will use for evaluation. - * - * @param pull The pull request that triggered this workflow. - * @returns The diff of the given pull request or `undefined` if we failed to retrieve it. - */ -export async function fetchDiff( - pull: PullRequest -): Promise { - const git = simpleGit('.', { trimmed: true }) - - // let baseRefExists = false - // try { - // baseRefExists = !!(await git.raw('rev-parse', '--verify', pull.base.ref)) - // } catch (e) { - // core.error( - // `Error from 'git rev-parse --verfy ${pull.base.ref}': ${ - // (e as Error).message - // }` - // ) - // } - - // if (!baseRefExists) { - // core.setFailed( - // `Could not find pull request base branch ${pull.base.ref}. ` + - // `Please make sure actions/checkout was used beforehand to fetch ${pull.base.ref}.` - // ) - // return - // } - - core.debug( - `Fetching base ref "${pull.base.ref}" and head ref "${pull.head.ref}"` - ) - - await git.fetch([ - 'origin', - `+${pull.base.ref}:${pull.base.ref}`, - `+${pull.head.ref}:${pull.head.ref}`, - `--filter=tree:0`, - '--no-tags', - '--prune', - '--no-recurse-submodules' - ]) - - core.debug(`Switching to head ref "${pull.head.ref}"`) - await git.raw('switch', pull.head.ref) - - const diffArgs = ['--merge-base', pull.base.ref].concat( - core.getInput('git-diff-options').split(/\s+/) - ) - core.info(`Retrieving diff with \`git diff ${diffArgs.join(' ')}\``) - return git.diff(diffArgs) -} diff --git a/src/main.ts b/src/main.ts index cf531cc..03f5256 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,11 +7,10 @@ import * as fs from 'fs' import * as path from 'path' import { Configuration } from './configuration' import { - fetchDiff, loadConfiguration, - pullRequestAuthorHasNotOptedIn, - workflowTriggeredForUnsupportedEvent + pullRequestAuthorHasNotOptedIn } from './initializer' +import { Git } from './git' const DEFAULT_LABEL_PREFIX = 'sizeup/' const DEFAULT_COMMENT_TEMPLATE = ` @@ -30,14 +29,23 @@ const DEFAULT_SCORE_THRESHOLD = 100 */ export async function run(): Promise { try { - if (workflowTriggeredForUnsupportedEvent()) return + if (github.context.eventName !== 'pull_request') { + core.setFailed( + "This action is only supported on the 'pull_request' event, " + + `but it was triggered for '${github.context.eventName}'` + ) + return + } - const config = loadConfiguration() const pullRequest = github.context.payload.pull_request as PullRequest + const git = new Git() + await git.clone(pullRequest.base.repo.full_name, pullRequest.head.ref) + + const config = loadConfiguration() if (pullRequestAuthorHasNotOptedIn(config, pullRequest)) return - const diff = await fetchDiff(pullRequest) + const diff = await git.diff(pullRequest.base.ref) if (!diff) return const score = await evaluatePullRequest(pullRequest, diff, config) @@ -64,7 +72,7 @@ async function evaluatePullRequest( diff: string, config: Configuration ): Promise { - const pullRequestNickname = `${pull.base.repo.owner.login}/${pull.base.repo.name}#${pull.number}` + const pullRequestNickname = `${pull.base.repo.full_name}#${pull.number}` core.info(`Evaluating pull request ${pullRequestNickname}`) let sizeupConfigFile = undefined