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

Better errors #294

Merged
merged 10 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test-meticulous-against-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
pull_request: {}

permissions:
actions: write
contents: read
statuses: write
pull-requests: write
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test-meticulous.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
pull_request: {}

permissions:
actions: write
contents: read
statuses: write
pull-requests: write
Expand Down
22 changes: 13 additions & 9 deletions src/action.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { setFailed } from "@actions/core";
import { context, getOctokit } from "@actions/github";
import { applyDefaultExecutionOptionsFromProject } from "@alwaysmeticulous/client";
import { setMeticulousLocalDataDir } from "@alwaysmeticulous/common";
import {
METICULOUS_LOGGER_NAME,
setMeticulousLocalDataDir,
} from "@alwaysmeticulous/common";
import { executeTestRun } from "@alwaysmeticulous/replay-orchestrator-launcher";
import {
ReplayExecutionOptions,
RunningTestRunExecution,
} from "@alwaysmeticulous/sdk-bundles-api";
import { initSentry } from "@alwaysmeticulous/sentry";
import debounce from "lodash.debounce";
import log from "loglevel";
import { addLocalhostAliases } from "./utils/add-localhost-aliases";
import { throwIfCannotConnectToOrigin } from "./utils/check-connection";
import { LOGICAL_ENVIRONMENT_VERSION } from "./utils/constants";
Expand All @@ -17,7 +21,7 @@ import { getEnvironment } from "./utils/environment.utils";
import { getBaseAndHeadCommitShas } from "./utils/get-base-and-head-commit-shas";
import { getCodeChangeEvent } from "./utils/get-code-change-event";
import { getInputs } from "./utils/get-inputs";
import { initLogger, setLogLevel } from "./utils/logger.utils";
import { initLogger, setLogLevel, shortSha } from "./utils/logger.utils";
import { ResultsReporter } from "./utils/results-reporter";
import { waitForDeploymentUrl } from "./utils/wait-for-deployment-url";

Expand Down Expand Up @@ -73,9 +77,10 @@ export const runMeticulousTestsAction = async (): Promise<void> => {
const event = getCodeChangeEvent(context.eventName, payload);
const { owner, repo } = context.repo;
const octokit = getOctokitOrFail(githubToken);
const logger = log.getLogger(METICULOUS_LOGGER_NAME);

if (event == null) {
console.warn(
logger.warn(
`Running report-diffs-action is only supported for 'push', \
'pull_request' and 'workflow_dispatch' events, but was triggered \
on a '${context.eventName}' event. Skipping execution.`
Expand All @@ -97,19 +102,19 @@ export const runMeticulousTestsAction = async (): Promise<void> => {
});

if (shaToCompareAgainst != null && event.type === "pull_request") {
console.log(
logger.info(
`Comparing screenshots for the commit head of this PR, ${shortSha(
head
)}, against ${shortSha(shaToCompareAgainst)}`
);
} else if (shaToCompareAgainst != null) {
console.log(
logger.info(
`Comparing screenshots for commit ${shortSha(
head
)} against commit ${shortSha(shaToCompareAgainst)}}`
);
} else {
console.log(`Generating screenshots for commit ${shortSha(head)}`);
logger.info(`Generating screenshots for commit ${shortSha(head)}`);
}

const resultsReporter = new ResultsReporter({
Expand Down Expand Up @@ -218,11 +223,10 @@ const getOctokitOrFail = (githubToken: string | null) => {
try {
return getOctokit(githubToken);
} catch (err) {
console.error(err);
const logger = log.getLogger(METICULOUS_LOGGER_NAME);
logger.error(err);
throw new Error(
"Error connecting to GitHub. Did you specify a valid 'github-token'?"
);
}
};

const shortSha = (sha: string) => sha.slice(0, 7);
2 changes: 2 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export const EXPECTED_PERMISSIONS_BLOCK = [
// the environment changes in a way that would cause a replay to behave differently, e.g. upgrading to a newer
// replay-orchestrator-launcher version, or changing the version of puppeteer.
export const LOGICAL_ENVIRONMENT_VERSION = 2;

export const DOCS_URL = "https://app.meticulous.ai/docs/github-actions-v2";
41 changes: 30 additions & 11 deletions src/utils/ensure-base-exists.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { METICULOUS_LOGGER_NAME } from "@alwaysmeticulous/common";
import log from "loglevel";
import { Duration } from "luxon";
import { CodeChangeEvent } from "../types";
import { LOGICAL_ENVIRONMENT_VERSION } from "./constants";
import { DOCS_URL, LOGICAL_ENVIRONMENT_VERSION } from "./constants";
import {
DEFAULT_FAILED_OCTOKIT_REQUEST_MESSAGE,
isGithubPermissionsError,
} from "./error.utils";
import {
getCurrentWorkflowId,
getPendingWorkflowRun,
Expand Down Expand Up @@ -68,7 +72,7 @@ export const ensureBaseTestsExists = async ({
});

if (testRun != null) {
logger.log(`Tests already exist for commit ${base} (${testRun.id})`);
logger.info(`Tests already exist for commit ${base} (${testRun.id})`);
return { shaToCompareAgainst: base };
}

Expand All @@ -82,7 +86,7 @@ export const ensureBaseTestsExists = async ({
octokit,
});
if (alreadyPending != null) {
logger.log(
logger.info(
`Waiting on workflow run on base commit (${base}) to compare against: ${alreadyPending.html_url}`
);

Expand Down Expand Up @@ -163,7 +167,7 @@ export const ensureBaseTestsExists = async ({
return { shaToCompareAgainst: null };
}

logger.log(`Waiting on workflow run: ${workflowRun.html_url}`);
logger.info(`Waiting on workflow run: ${workflowRun.html_url}`);
await waitForWorkflowCompletionAndThrowIfFailed({
owner,
repo,
Expand Down Expand Up @@ -251,11 +255,26 @@ const getHeadCommitForRef = async ({
ref: string;
octokit: InstanceType<typeof GitHub>;
}): Promise<string> => {
const result = await octokit.rest.repos.getBranch({
owner,
repo,
branch: ref,
});
const commitSha = result.data.commit.sha;
return commitSha;
try {
const result = await octokit.rest.repos.getBranch({
owner,
repo,
branch: ref,
});
const commitSha = result.data.commit.sha;
return commitSha;
} catch (err: unknown) {
if (isGithubPermissionsError(err)) {
// https://docs.github.com/en/rest/overview/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-contents
throw new Error(
`Missing permission to get the head commit of the branch '${ref}'. This is required in order to correctly calculate the two commits to compare.` +
` Please add the 'contents: read' permission to your workflow YAML file: see ${DOCS_URL} for the correct setup.`
);
}
const logger = log.getLogger(METICULOUS_LOGGER_NAME);
logger.error(
`Unable to get head commit of branch '${ref}'. This is required in order to correctly calculate the two commits to compare. ${DEFAULT_FAILED_OCTOKIT_REQUEST_MESSAGE}`
);
throw err;
}
};
29 changes: 29 additions & 0 deletions src/utils/error.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DOCS_URL } from "./constants";

export const isGithubPermissionsError = (
error: MaybeOctokitRequestError | unknown
): boolean => {
const message = getErrorMessage(error);
return (
message.toLowerCase().includes("resource not accessible by integration") ||
(error as MaybeOctokitRequestError)?.status === 403
);
};

export const getErrorMessage = (error: unknown) => {
const message = (error as MaybeOctokitRequestError)?.message ?? "";
return typeof message === "string" ? message : "";
};

// Octokit returns RequestErrors, which have a message and a code
// But we can't make any strong assumptions, so we type it on the safe side
// https://github.com/octokit/request-error.js/blob/372097e9b16f70d4ad75089003dc9154e304faa7/src/index.ts#L16
type MaybeOctokitRequestError =
| {
message?: string | unknown;
status?: number | unknown;
}
| null
| undefined;

export const DEFAULT_FAILED_OCTOKIT_REQUEST_MESSAGE = `Please check www.githubstatus.com, and that you have setup the action correctly, including with the correct permissions: see ${DOCS_URL} for the correct setup.`;
19 changes: 12 additions & 7 deletions src/utils/get-base-and-head-commit-shas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { execSync } from "child_process";
import { context } from "@actions/github";
import { METICULOUS_LOGGER_NAME } from "@alwaysmeticulous/common";
import log from "loglevel";
import { CodeChangeEvent } from "../types";

interface BaseAndHeadCommitShas {
Expand Down Expand Up @@ -53,6 +55,8 @@ const tryGetMergeBaseOfHeadCommit = (
pullRequestBaseSha: string,
baseRef: string
): string | null => {
const logger = log.getLogger(METICULOUS_LOGGER_NAME);

try {
markGitDirectoryAsSafe();
// Only a single commit is fetched by the checkout action by default
Expand All @@ -69,7 +73,7 @@ const tryGetMergeBaseOfHeadCommit = (
if (!isValidGitSha(mergeBase)) {
// Note: the GITHUB_SHA is always a merge commit, even if the merge is a no-op because the PR is up to date
// So this should never happen
console.error(
logger.error(
`Failed to get merge base of ${pullRequestHeadSha} and ${baseRef}: value returned by 'git merge-base' was not a valid git SHA ('${mergeBase}').` +
`Using the base of the pull request instead (${pullRequestBaseSha}).`
);
Expand All @@ -78,7 +82,7 @@ const tryGetMergeBaseOfHeadCommit = (

return mergeBase;
} catch (error) {
console.error(
logger.error(
`Failed to get merge base of ${pullRequestHeadSha} and ${baseRef}. Error: ${error}. Using the base of the pull request instead (${pullRequestBaseSha}).`
);
return null;
Expand All @@ -90,9 +94,10 @@ const tryGetMergeBaseOfTemporaryMergeCommit = (
pullRequestBaseSha: string
): string | null => {
const mergeCommitSha = process.env.GITHUB_SHA;
const logger = log.getLogger(METICULOUS_LOGGER_NAME);

if (mergeCommitSha == null) {
console.error(
logger.error(
`No GITHUB_SHA environment var set, so can't work out true base of the merge commit. Using the base of the pull request instead (${pullRequestBaseSha}).`
);
return null;
Expand All @@ -104,7 +109,7 @@ const tryGetMergeBaseOfTemporaryMergeCommit = (
.toString()
.trim();
if (headCommitSha !== mergeCommitSha) {
console.log(
logger.info(
`The head commit SHA (${headCommitSha}) does not equal GITHUB_SHA environment variable (${mergeCommitSha}).
This is likely because a custom ref has been passed to the 'actions/checkout' action. We're assuming therefore
that the head commit SHA is not a temporary merge commit, but rather the head of the branch. Therefore we're
Expand All @@ -124,7 +129,7 @@ const tryGetMergeBaseOfTemporaryMergeCommit = (
if (parents.length !== 2) {
// Note: the GITHUB_SHA is always a merge commit, even if the merge is a no-op because the PR is up to date
// So this should never happen
console.error(
logger.error(
`GITHUB_SHA (${mergeCommitSha}) is not a merge commit, so can't work out true base of the merge commit. Using the base of the pull request instead.`
);
return null;
Expand All @@ -134,15 +139,15 @@ const tryGetMergeBaseOfTemporaryMergeCommit = (
const mergeBaseSha = parents[0];
const mergeHeadSha = parents[1];
if (mergeHeadSha !== pullRequestHeadSha) {
console.error(
logger.error(
`The second parent (${parents[1]}) of the GITHUB_SHA merge commit (${mergeCommitSha}) is not equal to the head of the PR (${pullRequestHeadSha}),
so can not confidently determine the base of the merge commit to compare against. Using the base of the pull request instead (${pullRequestBaseSha}).`
);
return null;
}
return mergeBaseSha;
} catch (e) {
console.error(
logger.error(
`Error getting base of merge commit (${mergeCommitSha}). Using the base of the pull request instead (${pullRequestBaseSha}).`,
e
);
Expand Down
2 changes: 2 additions & 0 deletions src/utils/logger.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ export const setLogLevel: (logLevel: string | undefined) => void = (
break;
}
};

export const shortSha = (sha: string) => sha.slice(0, 7);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat duplicative with the logic in our monorepo (source). Is it worth moving to the sdk or too small?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think too small to be worth it

49 changes: 37 additions & 12 deletions src/utils/results-reporter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { getOctokit } from "@actions/github";
import { Project } from "@alwaysmeticulous/api";
import { METICULOUS_LOGGER_NAME } from "@alwaysmeticulous/common";
import {
ExecuteTestRunResult,
RunningTestRunExecution,
} from "@alwaysmeticulous/sdk-bundles-api";
import log from "loglevel";
import { CodeChangeEvent } from "../types";
import { DOCS_URL } from "./constants";
import {
DEFAULT_FAILED_OCTOKIT_REQUEST_MESSAGE,
isGithubPermissionsError,
} from "./error.utils";
import { shortSha } from "./logger.utils";
import { updateStatusComment } from "./update-status-comment";

const SHORT_SHA_LENGTH = 7;
Expand Down Expand Up @@ -160,18 +168,35 @@ export class ResultsReporter {
targetUrl?: string;
}) {
const { octokit, owner, repo, headSha } = this.options;
return octokit.rest.repos.createCommitStatus({
owner,
repo,
context:
this.options.testSuiteId != null
? `Meticulous (${this.options.testSuiteId})`
: "Meticulous",
description,
state,
sha: headSha,
...(targetUrl ? { target_url: targetUrl } : {}),
});
try {
return octokit.rest.repos.createCommitStatus({
owner,
repo,
context:
this.options.testSuiteId != null
? `Meticulous (${this.options.testSuiteId})`
: "Meticulous",
description,
state,
sha: headSha,
...(targetUrl ? { target_url: targetUrl } : {}),
});
} catch (err: unknown) {
if (isGithubPermissionsError(err)) {
// https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
throw new Error(
`Missing permission to create and update commit statuses.` +
` Please add the 'statuses: write' permission to your workflow YAML file: see ${DOCS_URL} for the correct setup.`
);
}
const logger = log.getLogger(METICULOUS_LOGGER_NAME);
logger.error(
`Unable to create commit status for commit '${shortSha(
headSha
)}'. ${DEFAULT_FAILED_OCTOKIT_REQUEST_MESSAGE}`
);
throw err;
}
}

private setStatusComment({
Expand Down
Loading
Loading