Skip to content
This repository has been archived by the owner on Sep 1, 2024. It is now read-only.

Commit

Permalink
[FLAKE-174] Resolve detached git HEADs to abbreviated ref names (#1)
Browse files Browse the repository at this point in the history
This should support GitHub Actions `pull_request` events.
  • Loading branch information
ramosbugs authored Mar 29, 2022
1 parent a75339a commit 527603e
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 23 deletions.
6 changes: 0 additions & 6 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ jobs:
steps:
- uses: actions/checkout@v2

# FIXME: remove these
- name: Print git branch name
run: git rev-parse --abbrev-ref HEAD
- run: git show-ref | grep $(git rev-parse HEAD)
- run: cat .git/HEAD

- uses: actions/setup-node@v2
with:
node-version: '16'
Expand Down
58 changes: 58 additions & 0 deletions packages/jest-plugin/src/git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { SimpleGit } from "simple-git";
import _debug = require("debug");

const debug = _debug("unflakable:git");

export const getCurrentGitBranch = async (
git: SimpleGit,
commitSha: string
): Promise<string | undefined> => {
// In the common case (an attached HEAD), we can just use `git rev-parse`.
const headRef = await git.revparse(["--abbrev-ref", "HEAD"]);

// If `git rev-parse` returns `HEAD`, then we have a detached head, and we need to see if the
// current commit SHA matches any known refs (i.e., local/remote branches or tags). This happens
// when running GitHub Actions in response to a `pull_request` event. In that case, the commit
// is a detached HEAD, but there's a `refs/remotes/pull/PR_NUMBER/merge` ref we can use as the
// "branch" (abbreviated to pull/PR_NUMBER/merge).
if (headRef !== "HEAD") {
return headRef;
}

// The code below runs the equivalent of `git show-ref | grep $(git rev-parse HEAD)`.
const gitOutput = await git.raw(["show-ref"]);
const matchingRefs = gitOutput
.split("\n")
.map((line) => {
const [sha, refName] = line.split(" ", 2);
return {
sha,
refName,
};
})
.filter(({ sha }) => sha === commitSha);
debug(
`git show-ref returned ${
matchingRefs.length
} ref(s) SHA ${commitSha}: ${matchingRefs
.map((ref) => ref.refName)
.join(", ")}`
);

if (matchingRefs.length === 0) {
return undefined;
}

// `git show-ref` returns refs sorted lexicographically:
// refs/heads/*
// refs/remotes/*
// refs/stash
// refs/tags/*
// We just take the first matching ref and use its abbreviation (i.e., removing the refs/remotes
// prefix) as the branch name. Users can override this behavior by setting the UNFLAKABLE_BRANCH
// environment variable.
return git.revparse(["--abbrev-ref", matchingRefs[0].refName]);
};

export const getCurrentGitCommit = (git: SimpleGit): Promise<string> =>
git.revparse("HEAD");
13 changes: 8 additions & 5 deletions packages/jest-plugin/src/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from "path";

//
import type {
AggregatedResult,
AssertionResult,
Expand Down Expand Up @@ -46,6 +46,7 @@ import _debug = require("debug");
import SummaryReporter from "./vendored/SummaryReporter";
import { getResultHeader } from "./vendored/getResultHeader";
import { formatTime } from "./vendored/formatTime";
import { getCurrentGitBranch, getCurrentGitCommit } from "./git";

const debug = _debug("unflakable:reporter");

Expand Down Expand Up @@ -504,11 +505,13 @@ export default class UnflakableReporter extends BaseReporter {
try {
const git = simpleGit();
if (await git.checkIsRepo()) {
if (branch === undefined || branch.length === 0) {
branch = await git.revparse(["--abbrev-ref", "HEAD"]);
}
const gitCommit = await getCurrentGitCommit(git);
if (commit === undefined || commit.length === 0) {
commit = await git.revparse("HEAD");
commit = gitCommit;
}

if (branch === undefined || branch.length === 0) {
branch = await getCurrentGitBranch(git, gitCommit);
}
}
} catch (e) {
Expand Down
6 changes: 5 additions & 1 deletion packages/jest-plugin/test/integration/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ export const integrationTest = async (testCase: TestCase): Promise<void> => {
failToFetchManifest: false,
failToUploadResults: false,
git: {
branch: "MOCK_BRANCH",
abbreviatedRefs: {
HEAD: "MOCK_BRANCH",
"refs/heads/MOCK_BRANCH": "MOCK_BRANCH",
},
refs: [{ sha: "MOCK_COMMIT", refName: "refs/heads/MOCK_BRANCH" }],
commit: "MOCK_COMMIT",
isRepo: true,
},
Expand Down
28 changes: 28 additions & 0 deletions packages/jest-plugin/test/integration/src/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,34 @@ integrationTestSuite(() => {
expectedResults: defaultExpectedResults,
}));

// This tests the environment present in GitHub Actions for a `pull_request` event.
it("git repo with detached HEAD", () =>
integrationTest({
params: {
git: {
abbreviatedRefs: {
// Mock a detached HEAD.
HEAD: "HEAD",
"refs/remote/pull/MOCK_PR_NUMBER/merge":
"pull/MOCK_PR_NUMBER/merge",
},
commit: "MOCK_PR_COMMIT",
isRepo: true,
// Mock the `git show-ref` response.
refs: [
{
sha: "MOCK_PR_COMMIT",
refName: "refs/remote/pull/MOCK_PR_NUMBER/merge",
},
],
},
expectedCommit: "MOCK_PR_COMMIT",
expectedBranch: "pull/MOCK_PR_NUMBER/merge",
},
expectedExitCode: 1,
expectedResults: defaultExpectedResults,
}));

it("read branch/commit from environment", () =>
integrationTest({
params: {
Expand Down
55 changes: 44 additions & 11 deletions packages/jest-plugin/test/integration/src/runTestCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,25 @@ const userAgentRegex = new RegExp(
"unflakable-js-api/(?:[-0-9.]|alpha|beta)+ unflakable-jest-plugin/(?:[-0-9.]|alpha|beta)+ \\(Jest [0-9]+\\.[0-9]+\\.[0-9]+; Node v[0-9]+\\.[0-9]+\\.[0-9]\\)"
);

export type SimpleGitMockRef = {
sha: string;
refName: string;
};
export type SimpleGitMockParams =
| { branch?: undefined; commit?: undefined; isRepo: false }
| { branch: string; commit: string; isRepo: true };
| {
abbreviatedRefs?: undefined;
commit?: undefined;
isRepo: false;
refs?: undefined;
}
| {
// Maps ref name (e.g., HEAD or refs/remotes/pull/1/merge) to the `git --abbrev-ref <refname>`
// response (e.g., branch-name, pull/1/merge, or in the case of a detached HEAD, HEAD).
abbreviatedRefs: { [key in string]: string };
commit: string;
isRepo: true;
refs: SimpleGitMockRef[];
};

export type TestCaseParams = {
config: Partial<UnflakableConfig> | null;
Expand Down Expand Up @@ -59,22 +75,39 @@ export type TestCaseParams = {

const originalStderrWrite = process.stderr.write.bind(process.stderr);

const mockSimpleGit = ({
branch,
commit,
isRepo,
}: SimpleGitMockParams): void => {
const mockSimpleGit = (params: SimpleGitMockParams): void => {
(simpleGit as jest.Mock).mockImplementationOnce(
() =>
({
checkIsRepo: jest.fn(
() => Promise.resolve(isRepo) as GitResponse<boolean>
() => Promise.resolve(params.isRepo) as GitResponse<boolean>
),
revparse: jest.fn((options: string | TaskOptions) => {
if (deepEqual(options, ["--abbrev-ref", "HEAD"])) {
return Promise.resolve(branch) as GitResponse<string>;
if (!params.isRepo) {
throw new Error("not a git repository");
} else if (
Array.isArray(options) &&
options.length === 2 &&
options[0] === "--abbrev-ref"
) {
return Promise.resolve(
params.abbreviatedRefs[options[1]] ?? "HEAD"
) as GitResponse<string>;
} else if (options === "HEAD") {
return Promise.resolve(commit) as GitResponse<string>;
return Promise.resolve(params.commit) as GitResponse<string>;
} else {
throw new Error(`unexpected options ${options.toString()}`);
}
}),
raw: jest.fn((options: string | TaskOptions) => {
if (!params.isRepo) {
throw new Error("not a git repository");
} else if (deepEqual(options, ["show-ref"])) {
return Promise.resolve(
(params.refs ?? [])
.map((mockRef) => `${mockRef.sha} ${mockRef.refName}`)
.join("\n") + "\n"
) as GitResponse<string>;
} else {
throw new Error(`unexpected options ${options.toString()}`);
}
Expand Down

0 comments on commit 527603e

Please sign in to comment.