Skip to content

Commit

Permalink
Split up GitHub context functions, provide fork info
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelmeuli committed Feb 22, 2020
1 parent b20f60f commit 168fd66
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 223 deletions.
7 changes: 3 additions & 4 deletions src/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,11 @@ function hasChanges() {

/**
* Pushes all changes to the GitHub repository
* @param {{actor: string, branch: string, event: object, eventName: string, repository: string,
* token: string, username: string, workspace: string}} context - Object information about the
* GitHub repository and action trigger event
* @param {import('./github/context').GithubContext} context - Information about the GitHub
* repository and action trigger event
*/
function pushChanges(context) {
const remote = `https://${context.actor}:${context.token}@github.com/${context.username}/${context.repository}.git`;
const remote = `https://${context.actor}:${context.token}@github.com/${context.repository.repoName}.git`;
const localBranch = "HEAD";
const remoteBranch = context.branch;

Expand Down
120 changes: 0 additions & 120 deletions src/github.js

This file was deleted.

68 changes: 68 additions & 0 deletions src/github/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const { name: actionName } = require("../../package");
const { log } = require("../utils/action");
const request = require("../utils/request");
const { capitalizeFirstLetter } = require("../utils/string");

/**
* Creates a new check on GitHub which annotates the relevant commit with linting errors
* @param {string} linterName - Name of the linter for which a check should be created
* @param {string} sha - SHA of the commit which should be annotated
* @param {import('./context').GithubContext} context - Information about the GitHub repository and
* action trigger event
* @param {{isSuccess: boolean, warning: [], error: []}} lintResult - Parsed lint result
* @param {string} summary - Summary for the GitHub check
*/
async function createCheck(linterName, sha, context, lintResult, summary) {
let annotations = [];
for (const level of ["warning", "error"]) {
annotations = [
...annotations,
...lintResult[level].map(result => ({
path: result.path,
start_line: result.firstLine,
end_line: result.lastLine,
annotation_level: level === "warning" ? "warning" : "failure",
message: result.message,
})),
];
}

// Only use the first 50 annotations (limit for a single API request)
if (annotations.length > 50) {
log(
`There are more than 50 errors/warnings from ${linterName}. Annotations are created for the first 50 issues only.`,
);
annotations = annotations.slice(0, 50);
}

const body = {
name: linterName,
head_sha: sha,
conclusion: lintResult.isSuccess ? "success" : "failure",
output: {
title: capitalizeFirstLetter(summary),
summary: `${linterName} found ${summary}`,
annotations,
},
};
try {
log(`Creating GitHub check with ${annotations.length} annotations for ${linterName}…`);
await request(`https://api.github.com/repos/${context.repository.repoName}/check-runs`, {
method: "POST",
headers: {
"Content-Type": "application/json",
// "Accept" header is required to access Checks API during preview period
Accept: "application/vnd.github.antiope-preview+json",
Authorization: `Bearer ${context.token}`,
"User-Agent": actionName,
},
body,
});
log(`${linterName} check created successfully`);
} catch (err) {
log(err, "error");
throw new Error(`Error trying to create GitHub check for ${linterName}: ${err.message}`);
}
}

module.exports = { createCheck };
116 changes: 116 additions & 0 deletions src/github/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const { readFileSync } = require("fs");

const { name: actionName } = require("../../package");
const { getEnv, getInput } = require("../utils/action");

/**
* GitHub Actions workflow's environment variables
* @typedef {{actor: string, eventName: string, eventPath: string, token: string, workspace:
* string}} ActionEnv
*/

/**
* Information about the GitHub repository and its fork (if it exists)
* @typedef {{repoName: string, forkName: string, hasFork: boolean}} GithubRepository
*/

/**
* Information about the GitHub repository and action trigger event
* @typedef {{actor: string, branch: string, event: object, eventName: string, repository:
* GithubRepository, token: string, workspace: string}} GithubContext
*/

/**
* Returns the GitHub Actions workflow's environment variables
* @returns {ActionEnv} GitHub Actions workflow's environment variables
*/
function parseActionEnv() {
return {
// Information provided by environment
actor: getEnv("github_actor", true),
eventName: getEnv("github_event_name", true),
eventPath: getEnv("github_event_path", true),
workspace: getEnv("github_workspace", true),

// Information provided by action user
token: getInput("github_token", true),
};
}

/**
* Parse `event.json` file (file with the complete webhook event payload, automatically provided by
* GitHub)
* @param {string} eventPath - Path to the `event.json` file
* @returns {object} - Webhook event payload
*/
function parseEnvFile(eventPath) {
const eventBuffer = readFileSync(eventPath);
return JSON.parse(eventBuffer);
}

/**
* 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
* @returns {string} - Branch name
*/
function parseBranch(eventName, event) {
if (eventName === "push") {
return event.ref.substring(11); // Remove "refs/heads/" from start of string
}
if (eventName === "pull_request") {
return event.pull_request.head.ref;
}
throw Error(`${actionName} does not support "${eventName}" GitHub events`);
}

/**
* Parses the name of the current repository and determines whether it has a corresponding fork.
* Fork detection is only supported for the "pull_request" event
* @param {string} eventName - GitHub event type
* @param {object} event - GitHub webhook event payload
* @returns {GithubRepository} - Information about the GitHub repository and its fork (if it exists)
*/
function parseRepository(eventName, event) {
const repoName = event.repository.full_name;
let forkName;
if (eventName === "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;
forkName = repoName === headRepoName ? undefined : headRepoName;
}
return {
repoName,
forkName,
hasFork: forkName != null && forkName !== repoName,
};
}

/**
* Returns information about the GitHub repository and action trigger event
* @returns {GithubContext} context - Information about the GitHub repository and action trigger
* event
*/
function getContext() {
const { actor, eventName, eventPath, token, workspace } = parseActionEnv();
const event = parseEnvFile(eventPath);
return {
actor,
branch: parseBranch(eventName, event),
event,
eventName,
repository: parseRepository(eventName, event),
token,
workspace,
};
}

module.exports = {
getContext,
parseActionEnv,
parseBranch,
parseEnvFile,
parseRepository,
};
7 changes: 4 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const { join } = require("path");

const git = require("./git");
const github = require("./github");
const { createCheck } = require("./github/api");
const { getContext } = require("./github/context");
const linters = require("./linters");
const { getInput, log } = require("./utils/action");
const { getSummary } = require("./utils/lint-result");
Expand All @@ -19,7 +20,7 @@ process.on("unhandledRejection", err => {
* Parses the action configuration and runs all enabled linters on matching files
*/
async function runAction() {
const context = github.getContext();
const context = getContext();
const autoFix = getInput("auto_fix") === "true";
const commitMsg = getInput("commit_message", true);

Expand Down Expand Up @@ -85,7 +86,7 @@ async function runAction() {
const headSha = git.getHeadSha();
await Promise.all(
checks.map(({ checkName, lintResult, summary }) =>
github.createCheck(checkName, headSha, context, lintResult, summary),
createCheck(checkName, headSha, context, lintResult, summary),
),
);
}
Expand Down
48 changes: 48 additions & 0 deletions test/github/api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { createCheck } = require("../../src/github/api");
const {
EVENT_NAME,
EVENT_PATH,
FORK_REPOSITORY,
REPOSITORY,
REPOSITORY_DIR,
TOKEN,
USERNAME,
} = require("./test-constants");

jest.mock("../../src/utils/request", () =>
// eslint-disable-next-line global-require
jest.fn().mockReturnValue(require("./api-responses/check-runs.json")),
);

describe("createCheck()", () => {
const LINT_RESULT = {
isSuccess: true,
warning: [],
error: [],
};
const context = {
actor: USERNAME,
event: {},
eventName: EVENT_NAME,
eventPath: EVENT_PATH,
repository: {
repoName: REPOSITORY,
forkName: FORK_REPOSITORY,
hasFork: false,
},
token: TOKEN,
workspace: REPOSITORY_DIR,
};

test("mocked request should be successful", async () => {
await expect(
createCheck("check-name", "sha", context, LINT_RESULT, "summary"),
).resolves.toEqual(undefined);
});

test("mocked request should fail when no lint results are provided", async () => {
await expect(createCheck("check-name", "sha", context, null, "summary")).rejects.toEqual(
expect.any(Error),
);
});
});
Loading

0 comments on commit 168fd66

Please sign in to comment.