diff --git a/action.yml b/action.yml index f545f515..772dbdc8 100644 --- a/action.yml +++ b/action.yml @@ -17,8 +17,16 @@ inputs: default: ${{ github.head_ref }} target_ref: - description: The commit sha of the event that triggered this workflow (required for push). + description: The commit sha of the event that triggered this workflow. default: ${{ github.sha }} + + ref: + description: The ref (branch or tag) of the event that triggered this workflow (required for pushes). + default: ${{ github.ref }} + + ref_type: + description: The ref type of the event that triggered this workflow (required for pushes). Only branch refs are validated. + default: ${{ github.ref_type }} extra_config: description: A newline-separated list of commitlint-config npm packages to install. \ No newline at end of file diff --git a/jest.config.json b/jest.config.json index 14a787d9..8a6b8523 100644 --- a/jest.config.json +++ b/jest.config.json @@ -1,4 +1,5 @@ { "clearMocks": true, - "testPathIgnorePatterns": [ "/dist/", "/node_modules/" ] + "testPathIgnorePatterns": [ "/dist/", "/node_modules/" ], + "modulePaths": ["/src/"] } \ No newline at end of file diff --git a/src/events/gitEvent.test.ts b/src/events/gitEvent.test.ts new file mode 100644 index 00000000..2456178a --- /dev/null +++ b/src/events/gitEvent.test.ts @@ -0,0 +1,106 @@ +import { execSync } from "child_process"; +import * as actions from "@actions/core"; +import TestUtils from "../test/util"; +import preinstall from "../preinstall"; +import * as commitlintExec from "../commitlint"; +import GitEvent from "./gitEvent"; + +describe("src/events/gitEvent", () => { + const commitlint = jest.spyOn(commitlintExec, 'default'); + const actionsInfo = jest.spyOn(actions, "info"); + + const { + createTempDirectory, + intializeGitRepo, + getNthCommitBack, + teardownGitRepo, + teardownTestDirectory, + addInvalidCommit, + addValidCommit, + setupTestDirectory, + options, + } = new TestUtils(); + + beforeAll(() => { + options.cwd = createTempDirectory(); + setupTestDirectory(options.cwd); + preinstall('@joberstein12/commitlint-config', options); + }); + + beforeEach(() => { + intializeGitRepo(); + }); + + afterEach(() => { + teardownGitRepo(); + + expect(actionsInfo).toHaveBeenCalled(); + expect(actionsInfo).not.toHaveBeenCalledWith( + expect.stringContaining('Skipping commit validation') + ); + + expect(commitlint).toHaveBeenCalledTimes(1); + }); + + afterAll(() => { + teardownTestDirectory(options.cwd as string); + }); + + it("Validates a git event with no target ref", async () => { + const event = new GitEvent({ target: '' }, options); + console.log(execSync('git log', options).toString()); + + await expect(event.validateCommits()).resolves.not.toThrow(); + + expect(commitlint).toHaveBeenCalledWith( + expect.objectContaining({ from: undefined }) + ); + }); + + describe("Validating a git event with a target ref", () => { + it("Successfully validates the target commit", async () => { + const target = getNthCommitBack(1); + const event = new GitEvent({ target }, options); + + await expect(event.validateCommits()).resolves.not.toThrow(); + + expect(commitlint).toHaveBeenCalledWith( + expect.objectContaining({ from: `${target}^` }) + ); + }); + + it("Successfully validates commits a merge commit", async () => { + execSync("git checkout -qb other", options); + addValidCommit(); + + execSync([ + 'git checkout master', + 'git merge --no-ff other', + ].join(" && "), options); + + const event = new GitEvent({ + target: getNthCommitBack(1), + }, options); + + await expect(event.validateCommits()).resolves.not.toThrow(); + + expect(commitlint).toHaveBeenCalledWith( + expect.objectContaining({ from: `${event.target}^` }) + ); + }); + + it("Fails when the target commit is invalid", async () => { + addInvalidCommit(); + + const event = new GitEvent({ + target: getNthCommitBack(1) + }, options); + + await expect(event.validateCommits()).rejects.toThrow(); + + expect(commitlint).toHaveBeenCalledWith( + expect.objectContaining({ from: `${event.target}^` }) + ); + }); + }); +}); \ No newline at end of file diff --git a/src/events/gitEvent.ts b/src/events/gitEvent.ts new file mode 100644 index 00000000..bb5b39af --- /dev/null +++ b/src/events/gitEvent.ts @@ -0,0 +1,75 @@ +import { ExecSyncOptions, execSync } from "child_process"; +import commitlint from "commitlint"; +import { info } from "@actions/core"; + +/** + * Represents a base Git event. + */ +class GitEvent { + #args: GitEventArgs; + options?: ExecSyncOptions; + + constructor(args: GitEventArgs, options?: ExecSyncOptions) { + this.options = options; + this.#args = args; + } + + get target() { + return this.#args.target; + } + + /** + * Provide a list of refs to checkout for this Git event. + * @returns A list of refs + */ + getRefsToCheckout(): string[] { + return [this.#args.target]; + } + + /** + * Perform a checkout on all truthy refs returned from {@link GitEvent#getRefsToCheckout} + */ + performCheckouts(): void { + this.getRefsToCheckout() + .filter(ref => ref) + .forEach(ref => execSync(`git checkout ${ref}`, this.options).toString()); + } + + /** + * Provide a list of commits for commitlint to start validation from. + * @returns A list of commit hashes + */ + getFromCommits(): string[] { + return [this.#args.target]; + } + + /** + * Provide a reason to skip commit validation. + * @returns A truthy value explaining why validation is skipped, or a falsey value to proceed with commit validation. + */ + getSkipValidationReason(): string | undefined { + return; + } + + /** + * Executes commit validation for this Git event. + */ + async validateCommits(): Promise { + const skipReason = this.getSkipValidationReason(); + + if (skipReason) { + info(`Skipping commit validation: ${skipReason}`); + return; + } + + await Promise.all( + this.getFromCommits() + .map(from => commitlint({ + from: from ? `${from}^` : undefined, + cwd: this.options?.cwd?.toString() || undefined, + })) + ); + } +} + +export default GitEvent; \ No newline at end of file diff --git a/src/events/pullRequest.test.ts b/src/events/pullRequest.test.ts new file mode 100644 index 00000000..27035966 --- /dev/null +++ b/src/events/pullRequest.test.ts @@ -0,0 +1,101 @@ +import { execSync } from "child_process"; +import * as actions from "@actions/core"; +import TestUtils from "../test/util"; +import preinstall from "../preinstall"; +import * as commitlintExec from "../commitlint"; +import PullRequest from "./pullRequest"; + +describe("src/events/pullRequest", () => { + const commitlint = jest.spyOn(commitlintExec, 'default'); + const actionsInfo = jest.spyOn(actions, "info"); + + const { + createTempDirectory, + intializeGitRepo, + getNthCommitBack, + teardownGitRepo, + teardownTestDirectory, + addInvalidCommit, + addValidCommit, + setupTestDirectory, + options, + } = new TestUtils(); + + beforeAll(() => { + options.cwd = createTempDirectory(); + setupTestDirectory(options.cwd); + preinstall('@joberstein12/commitlint-config', options); + }); + + beforeEach(() => { + intializeGitRepo(); + + execSync("git checkout -qb other", options); + [ ...Array(3).keys() ].forEach(addValidCommit); + }); + + afterEach(() => { + teardownGitRepo(); + + expect(actionsInfo).toHaveBeenCalled(); + expect(actionsInfo).not.toHaveBeenCalledWith( + expect.stringContaining('Skipping commit validation') + ); + + expect(commitlint).toHaveBeenCalledTimes(2); + }); + + afterAll(() => { + teardownTestDirectory(options.cwd as string); + }); + + it("Successfully completes commit validation with a detached head", async () => { + execSync(`git checkout --detach`, options); + addValidCommit(); + + const event = new PullRequest({ + base_ref: 'master', + head_ref: 'other', + target: getNthCommitBack(1) + }, options); + + await expect(event.validateCommits()).resolves.not.toThrow(); + + [getNthCommitBack(4), event.target].forEach(from => { + expect(commitlint).toHaveBeenCalledWith( + expect.objectContaining({ from: `${from}^` })) + }); + }); + + it("Successfully completes commit validation between two branches", async () => { + const event = new PullRequest({ + base_ref: 'master', + head_ref: 'other', + target: getNthCommitBack(1) + }, options); + + await expect(event.validateCommits()).resolves.not.toThrow(); + + [getNthCommitBack(3), event.target].forEach(from => { + expect(commitlint).toHaveBeenCalledWith( + expect.objectContaining({ from: `${from}^` })) + }); + }); + + it("Fails when there's an invalid commit between two branches", async () => { + addInvalidCommit(); + addValidCommit(); + + const event = new PullRequest({ + base_ref: 'master', + head_ref: 'other', + target: getNthCommitBack(1), + }, options); + + await expect(event.validateCommits()).rejects.toThrow(); + + expect(commitlint).toHaveBeenCalledWith( + expect.objectContaining({ from: `${getNthCommitBack(5)}^` }) + ); + }); +}); \ No newline at end of file diff --git a/src/events/pullRequest.ts b/src/events/pullRequest.ts new file mode 100644 index 00000000..48c4288d --- /dev/null +++ b/src/events/pullRequest.ts @@ -0,0 +1,45 @@ +import { ExecSyncOptions, execSync } from "child_process"; +import GitEvent from "./gitEvent"; + +/** + * Represents a Git Pull Request event. + */ +export default class PullRequest extends GitEvent { + #args: PullRequestEventArgs; + + constructor(args: PullRequestEventArgs, options?: ExecSyncOptions) { + const { target } = args; + super({ target }, options); + this.#args = args; + } + + /** + * Provide a list containing the base, head, and target refs for the pull request. + * @returns A list of refs + */ + getRefsToCheckout(): string[] { + const { base_ref, head_ref, target } = this.#args; + return [ base_ref, head_ref, target ]; + } + + /** + * Provide a list containing the initial commit of the pull request, and the target ref, which may be detached from the pull request head. + * @returns A list of commit hashes + */ + getFromCommits(): string[] { + const { head_ref, base_ref } = this.#args; + const { options, target } = this; + + const [commit, ] = execSync(`git rev-list --no-merges --first-parent ${base_ref}..'${head_ref}'`, options) + .toString() + .trim() + .split('\n') + .reverse(); + + if (!commit) { + throw new Error('Failed to get initial commit in the given range.'); + } + + return [commit, target]; + } +} \ No newline at end of file diff --git a/src/events/push.test.ts b/src/events/push.test.ts new file mode 100644 index 00000000..1c5c0dbc --- /dev/null +++ b/src/events/push.test.ts @@ -0,0 +1,130 @@ +import { execSync } from "child_process"; +import * as actions from "@actions/core"; +import TestUtils from "../test/util"; +import preinstall from "../preinstall"; +import * as commitlintExec from "../commitlint"; +import Push from "./push"; + +describe("src/events/push", () => { + const commitlint = jest.spyOn(commitlintExec, 'default'); + const actionsInfo = jest.spyOn(actions, "info"); + + const { + createTempDirectory, + intializeGitRepo, + getNthCommitBack, + teardownGitRepo, + teardownTestDirectory, + addInvalidCommit, + addValidCommit, + setupTestDirectory, + options, + } = new TestUtils(); + + beforeAll(() => { + options.cwd = createTempDirectory(); + setupTestDirectory(options.cwd); + preinstall('@joberstein12/commitlint-config', options); + }); + + beforeEach(() => { + intializeGitRepo(); + + execSync("git checkout -qb other", options); + [ ...Array(3).keys() ].forEach(addValidCommit); + }); + + afterEach(() => { + teardownGitRepo(); + }); + + afterAll(() => { + teardownTestDirectory(options.cwd as string); + }); + + describe("Validating push events to branches", () => { + afterEach(() => { + expect(actionsInfo).toHaveBeenCalled(); + expect(actionsInfo).not.toHaveBeenCalledWith( + expect.stringContaining('Skipping commit validation') + ); + + expect(commitlint).toHaveBeenCalledTimes(1); + }); + + it("Successfully validates commits on a branch", async () => { + const event = new Push({ + ref: 'refs/heads/other', + ref_type: 'branch', + target: getNthCommitBack(1), + }, options); + + await expect(event.validateCommits()).resolves.not.toThrow(); + + expect(commitlint).toHaveBeenCalledWith( + expect.objectContaining({ from: `${getNthCommitBack(3)}^` }) + ); + }); + + it("Successfully validates commits since a merge", async () => { + execSync([ + 'git checkout master', + 'git merge --no-ff other' + ].join(" && "), options); + + addValidCommit(); + + const event = new Push({ + ref: 'refs/heads/master', + ref_type: 'branch', + target: getNthCommitBack(1), + }, options); + + await expect(event.validateCommits()).resolves.not.toThrow(); + + expect(commitlint).toHaveBeenCalledWith( + expect.objectContaining({ from: `${getNthCommitBack(1)}^` }) + ); + }); + + it("Fails when there's an invalid commit on a branch", async () => { + addInvalidCommit(); + addValidCommit(); + + const event = new Push({ + ref: 'refs/heads/other', + ref_type: 'branch', + target: getNthCommitBack(1), + }, options); + + await expect(event.validateCommits()).rejects.toThrow(); + + expect(commitlint).toHaveBeenCalledWith( + expect.objectContaining({ from: `${getNthCommitBack(5)}^` }) + ); + }); + }); + + describe("Validating push events for other ref types", () => { + afterEach(() => { + expect(actionsInfo).toHaveBeenCalled(); + expect(actionsInfo).toHaveBeenCalledWith( + expect.stringContaining('Skipping commit validation') + ); + + expect(commitlint).not.toHaveBeenCalled(); + }); + + ['tag', ''].forEach(refType => { + it(`Skips validation for ref type: '${refType}' pushes`, async () => { + const event = new Push({ + ref: 'refs/tags/someTag', + ref_type: refType, + target: '', + }, options); + + await expect(event.validateCommits()).resolves.not.toThrow(); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/events/push.ts b/src/events/push.ts new file mode 100644 index 00000000..9b9c6897 --- /dev/null +++ b/src/events/push.ts @@ -0,0 +1,65 @@ +import { ExecSyncOptions, execSync } from "child_process"; +import GitEvent from "./gitEvent"; + +/** + * Represents a Git Push event. + */ +export default class Push extends GitEvent { + #args: PushEventArgs; + + constructor(args: PushEventArgs, options?: ExecSyncOptions) { + const { target } = args; + super({ target }, options); + this.#args = args; + } + + /** + * Provide a list of refs containing the ref that was pushed to. + * @returns A list of refs + */ + getRefsToCheckout(): string[] { + const { ref_type, ref } = this.#args; + + return ref_type === 'branch' ? [this.#args.ref] : []; + } + + /** + * Skip validation for tag and non-branch pushes. + * @returns A reason for skipping validation for non-branch pushes. + */ + getSkipValidationReason(): string | undefined { + const { ref_type, ref } = this.#args; + + if (ref_type !== 'branch') { + return `Pushes for ref type: '${ref_type}' are not supported, regarding: '${ref}'.`; + } + } + + /** + * Provide a list containing the initial commit on the ref that was pushed to. + * @returns A list of commit hashes + */ + getFromCommits(): string[] { + const { ref } = this.#args; + const { options } = this; + + const refsToExclude = execSync(`git for-each-ref --format="%(refname)" refs/heads`, options) + .toString() + .trim() + .split('\n') + .filter(seenRef => ref !== seenRef) + .join(' '); + + const [commit, ] = execSync(`git rev-list --no-merges '${ref}' --not ${refsToExclude}`, options) + .toString() + .trim() + .split('\n') + .reverse(); + + if (!commit) { + throw new Error(`Failed to get initial commit: '${ref}'.`); + } + + return [commit]; + } +} \ No newline at end of file diff --git a/src/index.test.ts b/src/index.test.ts index ca76078b..e498a93d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,13 +1,17 @@ import { execSync } from "child_process"; import * as actions from "@actions/core"; import TestUtils from "./test/util"; +import * as commitlintExec from "./commitlint"; import run from "./index"; +jest.setTimeout(20_000); + describe("src/index", () => { let tmpDir: string; const actionsInfo = jest.spyOn(actions, "info"); const setFailed = jest.spyOn(actions, "setFailed"); + const commitlint = jest.spyOn(commitlintExec, 'default'); const { createTempDirectory, @@ -38,33 +42,98 @@ describe("src/index", () => { delete process.env.INPUT_BASE_REF; delete process.env.INPUT_HEAD_REF; delete process.env.INPUT_EXTRA_CONFIG; + delete process.env.INPUT_REF; + delete process.env.INPUT_REF_TYPE; expect(actionsInfo).toHaveBeenCalled(); }); it("Successfully validates a target commit", async () => { await run(); + + expect(setFailed).not.toHaveBeenCalled(); + expect(commitlint).toHaveBeenCalledTimes(1); + expect(commitlint).toHaveBeenCalledWith({ from: `${process.env.INPUT_TARGET_REF}^` }); + }); + + it("Successfully validates commits for a branch push", async () => { + const branch = '#3'; + + execSync(`git checkout -qb '${branch}'`, options); + [ ...Array(3).keys() ].forEach(addValidCommit); + + process.env.INPUT_TARGET_REF = getNthCommitBack(1); + process.env.INPUT_REF = `refs/heads/${branch}`; + process.env.INPUT_REF_TYPE = 'branch'; + + await run(); + expect(setFailed).not.toHaveBeenCalled(); + expect(commitlint).toHaveBeenCalledTimes(1); + expect(commitlint).toHaveBeenCalledWith({ from: `${getNthCommitBack(3)}^` }); }); - it("Successfully validates a range of commits", async () => { + it("Successfully validates commits for a pull request", async () => { process.env.INPUT_BASE_REF = "master"; - process.env.INPUT_HEAD_REF = "#3"; + process.env.INPUT_HEAD_REF = '#3'; execSync(`git checkout -qb '${process.env.INPUT_HEAD_REF}'`, options); [ ...Array(3).keys() ].forEach(addValidCommit); process.env.INPUT_TARGET_REF = getNthCommitBack(1); await run(); + expect(setFailed).not.toHaveBeenCalled(); + expect(commitlint).toHaveBeenCalledTimes(2); + + [getNthCommitBack(3), process.env.INPUT_TARGET_REF] + .forEach(commit => + expect(commitlint).toHaveBeenCalledWith({ from: `${commit}^` }) + ); }); + it("Successfully validates commits for a pull request with a detached head", async () => { + process.env.INPUT_BASE_REF = "master"; + process.env.INPUT_HEAD_REF = '#3'; + + execSync(`git checkout -qb '${process.env.INPUT_HEAD_REF}'`, options); + [ ...Array(3).keys() ].forEach(addValidCommit); + const fromCommit = getNthCommitBack(3); + + execSync(`git checkout --detach`, options); + addValidCommit(); + + process.env.INPUT_TARGET_REF = getNthCommitBack(1); + + await run(); + + expect(setFailed).not.toHaveBeenCalled(); + expect(commitlint).toHaveBeenCalledTimes(2); + + [fromCommit, process.env.INPUT_TARGET_REF] + .forEach(commit => + expect(commitlint).toHaveBeenCalledWith({ from: `${commit}^` }) + ); + }); + + it("Skips commit validation for a tag push", async () => { + process.env.INPUT_TARGET_REF = getNthCommitBack(1); + process.env.INPUT_REF = `refs/tags/someTag`; + process.env.INPUT_REF_TYPE = 'tag'; + + await run(); + + expect(setFailed).not.toHaveBeenCalled(); + expect(commitlint).not.toHaveBeenCalled(); + }); it("Fails validation for an invalid commit", async () => { addInvalidCommit(); process.env.INPUT_TARGET_REF = getNthCommitBack(1); await run(); + + expect(commitlint).toHaveBeenCalledTimes(1); expect(setFailed).toHaveBeenCalledWith('Commit validation failed.'); }); }); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1ba36016..2d6f32ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,32 +1,30 @@ -import { execSync } from 'child_process'; -import validateCommits from './validateCommits'; -import preinstall from './preinstall'; import { setFailed } from '@actions/core'; +import GitEvent from './events/gitEvent'; +import PullRequest from './events/pullRequest'; +import Push from './events/push'; +import preinstall from './preinstall'; +import { execSync } from 'child_process'; export default async () => { const { - INPUT_BASE_REF: source, - INPUT_HEAD_REF: destination, - INPUT_TARGET_REF: target, + INPUT_BASE_REF: base_ref, + INPUT_HEAD_REF: head_ref, + INPUT_REF: ref, + INPUT_REF_TYPE: ref_type, + INPUT_TARGET_REF: target = '', INPUT_EXTRA_CONFIG: extraConfig, } = process.env; + + const event = base_ref && head_ref + ? new PullRequest({ base_ref, head_ref, target }) + : ref + ? new Push({ ref, ref_type, target }) + : new GitEvent({ target }); try { - preinstall(extraConfig); - - if (source) { - execSync(`git checkout '${source}'`); - } - - if (destination) { - execSync(`git checkout '${destination}'`); - } - - await validateCommits({ - source, - destination, - target, - }); + event.performCheckouts(); + preinstall(extraConfig); + await event.validateCommits(); } catch (e) { setFailed((e as Error).message); } diff --git a/src/test/util.ts b/src/test/util.ts index 52975dce..26345ff7 100644 --- a/src/test/util.ts +++ b/src/test/util.ts @@ -15,8 +15,9 @@ export default class TestUtils { addValidCommit = () => { const commands = [ - "echo invalid >> src/test.txt", - "git add src", + "echo valid >> src/test.txt", + "echo ' ' >> package.json", + "git add --all", `git commit -m "chore(ci): Add valid commit."`, ]; @@ -37,8 +38,12 @@ export default class TestUtils { execSync("mktemp -d", { encoding: this.encoding }).trim(); setupTestDirectory = (tmpDir: string) => { - execSync(`cp ${process.cwd()}/.commitlintrc.json ${tmpDir}`); - execSync(`cp ${process.cwd()}/package*.json ${tmpDir}`); + execSync([ + `cp ${process.cwd()}/.commitlintrc.json ${tmpDir}`, + `cp ${process.cwd()}/package*.json ${tmpDir}`, + `cp ${process.cwd()}/.gitignore ${tmpDir}`, + ].join(' && ')); + process.chdir(tmpDir); execSync('npm install --frozen-lockfile', this.options); }; @@ -46,9 +51,12 @@ export default class TestUtils { intializeGitRepo = () => { execSync([ "git init", + "git config advice.detachedHead false", "rm -rf .git/hooks", "mkdir src", "touch src/test.txt", + "git add --all", + "git commit -m 'chore: Add initial commit.'", ].join(" && "), this.options); [ ...Array(2).keys() ].forEach(this.addValidCommit); @@ -56,8 +64,8 @@ export default class TestUtils { teardownGitRepo = () => { execSync([ - "rm -rf .git", - "rm -rf src" + "rm -rf .git/*", + "rm -rf src", ].join(' && '), this.options); } diff --git a/src/types.d.ts b/src/types.d.ts index bba4bb09..94885b1d 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,19 +1,15 @@ -interface CommitRange { - source?: string; - destination?: string; +interface PushEventArgs extends GitEventArgs { + ref: string; + ref_type?: string; } -interface CommitlintArgs { - config?: string; - verbose?: boolean; - from?: string; +interface PullRequestEventArgs extends GitEventArgs { + base_ref: string; + head_ref: string; } -interface BuildCommitlintArgs { - config?: string; - source?: string; - destination?: string; - target?: string; +interface GitEventArgs { + target: string; } // Copied from '@commitlint/read' diff --git a/src/validateCommits.test.ts b/src/validateCommits.test.ts deleted file mode 100644 index 4e51d6f6..00000000 --- a/src/validateCommits.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { execSync } from "child_process"; -import * as actions from "@actions/core"; -import TestUtils from "./test/util"; -import validateCommits from "./validateCommits"; -import preinstall from "./preinstall"; -import * as commitlintExec from "./commitlint"; - -describe("src/validateCommits", () => { - const commitlint = jest.spyOn(commitlintExec, 'default'); - const actionsInfo = jest.spyOn(actions, "info"); - - const { - createTempDirectory, - intializeGitRepo, - getNthCommitBack, - teardownGitRepo, - teardownTestDirectory, - addInvalidCommit, - addValidCommit, - setupTestDirectory, - options, - } = new TestUtils(); - - beforeAll(() => { - options.cwd = createTempDirectory(); - setupTestDirectory(options.cwd); - preinstall('@joberstein12/commitlint-config', options); - }); - - beforeEach(() => { - intializeGitRepo(); - }); - - afterEach(() => { - teardownGitRepo(); - }); - - afterAll(() => { - teardownTestDirectory(options.cwd as string); - }); - - it("Validates all commits with empty args", async () => { - await expect(validateCommits({}, options)).resolves.not.toThrow(); - expect(actionsInfo).toHaveBeenCalled(); - expect(commitlint).toHaveBeenCalledWith( - expect.objectContaining({ from: undefined }) - ); - }); - - describe("Validating the target commit", () => { - it("Successfully validates the target commit", async () => { - const target = getNthCommitBack(1); - const args = { target }; - - await expect(validateCommits(args, options)).resolves.not.toThrow(); - expect(actionsInfo).toHaveBeenCalled(); - expect(commitlint).toHaveBeenCalledWith( - expect.objectContaining({ from: `${target}^` }) - ); - }); - - it("Fails when the target commit is invalid", async () => { - addInvalidCommit(); - - const target = getNthCommitBack(1); - const args = { target }; - - await expect(validateCommits(args, options)).rejects.toThrow(); - expect(actionsInfo).toHaveBeenCalled(); - expect(commitlint).toHaveBeenCalledWith( - expect.objectContaining({ from: `${target}^` }) - ); - }); - }); - - describe("Validating a range of commits", () => { - beforeEach(() => { - execSync("git checkout -qb other", options); - [ ...Array(3).keys() ].forEach(addValidCommit); - }); - - it("Successfully completes commit validation", async () => { - const target = getNthCommitBack(3); - const args = { - source: "master", - destination: "other", - target, - }; - - await expect(validateCommits(args, options)).resolves.not.toThrow(); - expect(actionsInfo).toHaveBeenCalled(); - expect(commitlint).toHaveBeenCalledWith( - expect.objectContaining({ from: `${target}^` }) - ); - }); - - it("Fails when the commit range is invalid", async () => { - addInvalidCommit(); - addValidCommit(); - - const target = getNthCommitBack(5); - const args = { - source: "master", - destination: "other", - target, - }; - - await expect(validateCommits(args, options)).rejects.toThrow(); - expect(actionsInfo).toHaveBeenCalled(); - expect(commitlint).toHaveBeenCalledWith( - expect.objectContaining({ from: `${target}^` }) - ); - }); - - it("Validates the target commit when the source is not known", async () => { - const target = getNthCommitBack(1); - const args = { - source: "error", - destination: "master", - target, - }; - - await expect(validateCommits(args, options)).rejects.toThrow(); - expect(actionsInfo).not.toHaveBeenCalled(); - expect(commitlint).not.toHaveBeenCalled(); - }); - - it("Validates the target commit when the destination is not known", async () => { - const target = getNthCommitBack(1); - const args = { - source: "other", - destination: "error", - target, - }; - - await expect(validateCommits(args, options)).rejects.toThrow(); - expect(actionsInfo).not.toHaveBeenCalled(); - expect(commitlint).not.toHaveBeenCalled(); - }); - - it("Successfully validates merge commits", async () => { - execSync([ - 'git checkout master', - 'git merge --no-ff other' - ].join(" && "), options); - - const target = getNthCommitBack(1); - const args = { target }; - - await expect(validateCommits(args, options)).resolves.not.toThrow(); - expect(actionsInfo).toHaveBeenCalled(); - expect(commitlint).toHaveBeenCalledWith( - expect.objectContaining({ from: `${target}^` }) - ); - }); - }); -}); \ No newline at end of file diff --git a/src/validateCommits.ts b/src/validateCommits.ts deleted file mode 100644 index ec255f91..00000000 --- a/src/validateCommits.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ExecSyncOptions, execSync } from 'child_process'; -import commitlint from './commitlint'; -import { setFailed } from '@actions/core'; - -/** - * Validate all the commits between two refs if possible. If not, validate a target - * commit instead. If the target commit is also absent, validate all commits. - */ -export default async ( - { target, source, destination }: BuildCommitlintArgs, - options?: ExecSyncOptions -) => { - const fromCommit = source && destination - ? getCommitFromRange({ source, destination }, options) - : target; - - await commitlint({ - from: fromCommit ? `${fromCommit}^` : undefined, - cwd: options?.cwd?.toString() || undefined, - }); -} - -/** - * Get the initial commit from two refs. - * @throws if there was an error getting the commit. - */ -const getCommitFromRange = ( - { source, destination }: CommitRange, - options?: ExecSyncOptions -): string => { - const result = execSync(`git rev-list ${source}..${destination} | tail -n 1`, options) - .toString() - .trim(); - - if (!result) { - throw new Error('Failed to get initial commit in the given range.'); - } - - return result; -} \ No newline at end of file