From 2ff2fbdea48a8f5da77a31e7dd5ecb46c017ffc3 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Thu, 21 May 2020 11:09:16 -0400 Subject: [PATCH] telemetry for incorrect merge commit (#253) --- dist/index.js | 98 +++++++++++++++++++++++++++++++-- src/git-command-manager.ts | 7 +-- src/git-source-provider.ts | 12 ++++- src/ref-helper.ts | 108 +++++++++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 7 deletions(-) diff --git a/dist/index.js b/dist/index.js index 0074e0a..86601d1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3359,7 +3359,7 @@ module.exports = {"name":"@octokit/rest","version":"16.43.1","publishConfig":{"a /***/ }), /***/ 227: -/***/ (function(__unusedmodule, exports) { +/***/ (function(__unusedmodule, exports, __webpack_require__) { "use strict"; @@ -3372,7 +3372,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; Object.defineProperty(exports, "__esModule", { value: true }); +const url_1 = __webpack_require__(835); +const core = __importStar(__webpack_require__(470)); +const github = __importStar(__webpack_require__(469)); function getCheckoutInfo(git, ref, commit) { return __awaiter(this, void 0, void 0, function* () { if (!git) { @@ -3468,6 +3478,85 @@ function getRefSpec(ref, commit) { } } exports.getRefSpec = getRefSpec; +function checkCommitInfo(token, commitInfo, repositoryOwner, repositoryName, ref, commit) { + return __awaiter(this, void 0, void 0, function* () { + try { + // GHES? + if (isGhes()) { + return; + } + // Auth token? + if (!token) { + return; + } + // Public PR synchronize, for workflow repo? + if (fromPayload('repository.private') !== false || + github.context.eventName !== 'pull_request' || + fromPayload('action') !== 'synchronize' || + repositoryOwner !== github.context.repo.owner || + repositoryName !== github.context.repo.repo || + ref !== github.context.ref || + !ref.startsWith('refs/pull/') || + commit !== github.context.sha) { + return; + } + // Head SHA + const expectedHeadSha = fromPayload('after'); + if (!expectedHeadSha) { + core.debug('Unable to determine head sha'); + return; + } + // Base SHA + const expectedBaseSha = fromPayload('pull_request.base.sha'); + if (!expectedBaseSha) { + core.debug('Unable to determine base sha'); + return; + } + // Expected message? + const expectedMessage = `Merge ${expectedHeadSha} into ${expectedBaseSha}`; + if (commitInfo.indexOf(expectedMessage) >= 0) { + return; + } + // Extract details from message + const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/); + if (!match) { + core.debug('Unexpected message format'); + return; + } + // Post telemetry + const actualHeadSha = match[1]; + if (actualHeadSha !== expectedHeadSha) { + core.debug(`Expected head sha ${expectedHeadSha}; actual head sha ${actualHeadSha}`); + const octokit = new github.GitHub(token, { + userAgent: `actions-checkout-tracepoint/1.0 (code=STALE_MERGE;owner=${repositoryOwner};repo=${repositoryName};pr=${fromPayload('number')};run_id=${process.env['GITHUB_RUN_ID']};expected_head_sha=${expectedHeadSha};actual_head_sha=${actualHeadSha})` + }); + yield octokit.repos.get({ owner: repositoryOwner, repo: repositoryName }); + } + } + catch (err) { + core.debug(`Error when validating commit info: ${err.stack}`); + } + }); +} +exports.checkCommitInfo = checkCommitInfo; +function fromPayload(path) { + return select(github.context.payload, path); +} +function select(obj, path) { + if (!obj) { + return undefined; + } + const i = path.indexOf('.'); + if (i < 0) { + return obj[path]; + } + const key = path.substr(0, i); + return select(obj[key], path.substr(i + 1)); +} +function isGhes() { + const ghUrl = new url_1.URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com'); + return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'; +} /***/ }), @@ -5718,7 +5807,8 @@ class GitCommandManager { } log1() { return __awaiter(this, void 0, void 0, function* () { - yield this.execGit(['log', '-1']); + const output = yield this.execGit(['log', '-1']); + return output.stdout; }); } remoteAdd(remoteName, remoteUrl) { @@ -6057,7 +6147,9 @@ function getSource(settings) { } } // Dump some info about the checked out commit - yield git.log1(); + const commitInfo = yield git.log1(); + // Check for incorrect pull request merge commit + yield refHelper.checkCommitInfo(settings.authToken, commitInfo, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit); } finally { // Remove auth diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 4cbfe4a..495075a 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -29,7 +29,7 @@ export interface IGitCommandManager { isDetached(): Promise lfsFetch(ref: string): Promise lfsInstall(): Promise - log1(): Promise + log1(): Promise remoteAdd(remoteName: string, remoteUrl: string): Promise removeEnvironmentVariable(name: string): void setEnvironmentVariable(name: string, value: string): void @@ -225,8 +225,9 @@ class GitCommandManager { await this.execGit(['lfs', 'install', '--local']) } - async log1(): Promise { - await this.execGit(['log', '-1']) + async log1(): Promise { + const output = await this.execGit(['log', '-1']) + return output.stdout } async remoteAdd(remoteName: string, remoteUrl: string): Promise { diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index 6855288..bde53bb 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -170,7 +170,17 @@ export async function getSource(settings: IGitSourceSettings): Promise { } // Dump some info about the checked out commit - await git.log1() + const commitInfo = await git.log1() + + // Check for incorrect pull request merge commit + await refHelper.checkCommitInfo( + settings.authToken, + commitInfo, + settings.repositoryOwner, + settings.repositoryName, + settings.ref, + settings.commit + ) } finally { // Remove auth if (!settings.persistCredentials) { diff --git a/src/ref-helper.ts b/src/ref-helper.ts index ff256af..f600179 100644 --- a/src/ref-helper.ts +++ b/src/ref-helper.ts @@ -1,4 +1,7 @@ +import {URL} from 'url' import {IGitCommandManager} from './git-command-manager' +import * as core from '@actions/core' +import * as github from '@actions/github' export interface ICheckoutInfo { ref: string @@ -107,3 +110,108 @@ export function getRefSpec(ref: string, commit: string): string[] { return [`+${ref}:${ref}`] } } + +export async function checkCommitInfo( + token: string, + commitInfo: string, + repositoryOwner: string, + repositoryName: string, + ref: string, + commit: string +): Promise { + try { + // GHES? + if (isGhes()) { + return + } + + // Auth token? + if (!token) { + return + } + + // Public PR synchronize, for workflow repo? + if ( + fromPayload('repository.private') !== false || + github.context.eventName !== 'pull_request' || + fromPayload('action') !== 'synchronize' || + repositoryOwner !== github.context.repo.owner || + repositoryName !== github.context.repo.repo || + ref !== github.context.ref || + !ref.startsWith('refs/pull/') || + commit !== github.context.sha + ) { + return + } + + // Head SHA + const expectedHeadSha = fromPayload('after') + if (!expectedHeadSha) { + core.debug('Unable to determine head sha') + return + } + + // Base SHA + const expectedBaseSha = fromPayload('pull_request.base.sha') + if (!expectedBaseSha) { + core.debug('Unable to determine base sha') + return + } + + // Expected message? + const expectedMessage = `Merge ${expectedHeadSha} into ${expectedBaseSha}` + if (commitInfo.indexOf(expectedMessage) >= 0) { + return + } + + // Extract details from message + const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/) + if (!match) { + core.debug('Unexpected message format') + return + } + + // Post telemetry + const actualHeadSha = match[1] + if (actualHeadSha !== expectedHeadSha) { + core.debug( + `Expected head sha ${expectedHeadSha}; actual head sha ${actualHeadSha}` + ) + const octokit = new github.GitHub(token, { + userAgent: `actions-checkout-tracepoint/1.0 (code=STALE_MERGE;owner=${repositoryOwner};repo=${repositoryName};pr=${fromPayload( + 'number' + )};run_id=${ + process.env['GITHUB_RUN_ID'] + };expected_head_sha=${expectedHeadSha};actual_head_sha=${actualHeadSha})` + }) + await octokit.repos.get({owner: repositoryOwner, repo: repositoryName}) + } + } catch (err) { + core.debug(`Error when validating commit info: ${err.stack}`) + } +} + +function fromPayload(path: string): any { + return select(github.context.payload, path) +} + +function select(obj: any, path: string): any { + if (!obj) { + return undefined + } + + const i = path.indexOf('.') + if (i < 0) { + return obj[path] + } + + const key = path.substr(0, i) + return select(obj[key], path.substr(i + 1)) +} + +function isGhes(): boolean { + const ghUrl = new URL( + process.env['GITHUB_SERVER_URL'] || 'https://github.com' + ) + return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM' +}