From 7ec75e27a49c42577546383a88ed6825b2e1f032 Mon Sep 17 00:00:00 2001 From: Filip Seman Date: Mon, 9 Feb 2026 13:53:24 +0100 Subject: [PATCH] feat: support workflow_run trigger with automatic PR detection - Resolve PR number from workflow_run event or fallback to GitHub API - Add pull-request-number and show-commit-link inputs - Include commit SHA link in coverage comments - Scope cache keys correctly for workflow_run context --- .github/workflows/coverage.yml | 2 +- .github/workflows/quality.yml | 2 +- action.yml | 11 + src/context.ts | 123 +++++++++++ src/context_test.ts | 207 ++++++++++++++++++ src/{diff.test.ts => diff_test.ts} | 0 src/{go.test.ts => go_test.ts} | 0 src/index.ts | 46 ++-- ...ntegration.test.ts => integration_test.ts} | 37 ---- src/{lcov.test.ts => lcov_test.ts} | 0 src/render.ts | 18 +- src/{render.test.ts => render_test.ts} | 29 +++ 12 files changed, 408 insertions(+), 67 deletions(-) create mode 100644 src/context.ts create mode 100644 src/context_test.ts rename src/{diff.test.ts => diff_test.ts} (100%) rename src/{go.test.ts => go_test.ts} (100%) rename src/{integration.test.ts => integration_test.ts} (86%) rename src/{lcov.test.ts => lcov_test.ts} (100%) rename src/{render.test.ts => render_test.ts} (73%) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b2b7711..bba5a82 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v6 - name: Download coverage artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: coverage path: coverage diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 01eb3c0..a9cf3d9 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -46,7 +46,7 @@ jobs: - run: bun test --coverage - name: Upload coverage artifact if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: coverage path: coverage/lcov.info diff --git a/action.yml b/action.yml index 2ff5949..8b72a3f 100644 --- a/action.yml +++ b/action.yml @@ -12,6 +12,17 @@ inputs: Newline- or comma-separated list of coverage artifact paths. Each entry is `:`, e.g. `bun:coverage/lcov.info` or `go:coverage.out`. required: true + pull-request-number: + description: > + PR number to comment on. Auto-detected from event context + when omitted. Use this when running under workflow_run + with fork PRs where auto-detection may fail. + required: false + default: "" + show-commit-link: + description: "Include a commit link at the top of the comment (on/off)." + required: false + default: "on" base-branch: description: "Target branch for comparison (e.g. main). Defaults to the PR base ref." required: false diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 0000000..8225087 --- /dev/null +++ b/src/context.ts @@ -0,0 +1,123 @@ +import * as github from "@actions/github"; + +type Context = typeof github.context; + +/** + * Resolve the PR number from the event context using a priority chain: + * + * 1. Explicit `pull-request-number` input (manual override) + * 2. `pull_request` / `pull_request_target` event payload + * 3. `workflow_run` event — first PR in `pull_requests` array + * 4. API fallback — search open PRs by head SHA + * + * Returns `undefined` when no PR can be found. + */ +export async function resolvePrNumber( + inputOverride: string, + token: string, + context: Context = github.context, +): Promise { + // 1. Manual override + if (inputOverride) { + const n = parseInt(inputOverride, 10); + if (Number.isFinite(n) && n > 0) return n; + } + + // 2. Direct PR trigger + if (context.eventName === "pull_request" || context.eventName === "pull_request_target") { + const num = context.payload.pull_request?.number; + if (typeof num === "number") return num; + } + + // 3. workflow_run trigger — PR was the original event + if (context.eventName === "workflow_run") { + const prs: unknown[] | undefined = context.payload.workflow_run?.pull_requests; + if (Array.isArray(prs) && prs.length > 0) { + const first = prs[0] as { number?: number; }; + if (typeof first.number === "number") return first.number; + } + + // 4. API fallback — find PR by head branch + if (token) { + const headBranch: string | undefined = context.payload.workflow_run?.head_branch; + const headRepoOwner: string | undefined = context.payload.workflow_run?.head_repository + ?.owner?.login; + if (headBranch) { + try { + const octokit = github.getOctokit(token); + const { owner, repo } = context.repo; + const head = headRepoOwner && headRepoOwner !== owner + ? `${headRepoOwner}:${headBranch}` + : `${owner}:${headBranch}`; + const { data: prs } = await octokit.rest.pulls.list({ + owner, + repo, + head, + state: "open", + }); + if (prs.length > 0) return prs[0].number; + } catch { + // Swallowed — caller will get undefined + } + } + } + } + + return undefined; +} + +/** + * Resolve the head commit SHA from the event context. + * + * Under `workflow_run` the SHA of the triggering run is more accurate than + * `context.sha` (which points at the merge commit on the default branch). + */ +export function resolveHeadSha(context: Context = github.context): string { + if (context.eventName === "workflow_run") { + return context.payload.workflow_run?.head_sha ?? context.sha; + } + return context.sha; +} + +/** + * Resolve the base branch for cache key scoping. + * + * Under `workflow_run`, `context.ref` points to the *default* branch, not the + * PR base. Use `head_branch` from the triggering run instead. Falls back to the + * explicit `base-branch` input or `main`. + */ +export function resolveBaseBranch( + inputBaseBranch: string, + context: Context = github.context, +): string { + if (inputBaseBranch) return inputBaseBranch; + + if (context.eventName === "pull_request" || context.eventName === "pull_request_target") { + return context.payload.pull_request?.base?.ref ?? "main"; + } + + if (context.eventName === "workflow_run") { + return context.payload.workflow_run?.head_branch ?? "main"; + } + + return context.ref.replace("refs/heads/", "") || "main"; +} + +/** + * Resolve the current (head) branch for cache saving. + * + * Under `workflow_run`, the head branch comes from the triggering workflow. + */ +export function resolveCurrentBranch(context: Context = github.context): string { + if (context.eventName === "pull_request" || context.eventName === "pull_request_target") { + return context.payload.pull_request?.head?.ref + ?? context.ref.replace("refs/heads/", ""); + } + + if (context.eventName === "workflow_run") { + return context.payload.workflow_run?.head_branch + ?? context.ref.replace("refs/heads/", ""); + } + + return context.ref.replace("refs/heads/", ""); +} diff --git a/src/context_test.ts b/src/context_test.ts new file mode 100644 index 0000000..e977b4d --- /dev/null +++ b/src/context_test.ts @@ -0,0 +1,207 @@ +import { + describe, + expect, + test, +} from "bun:test"; + +import { + resolveBaseBranch, + resolveCurrentBranch, + resolveHeadSha, + resolvePrNumber, +} from "./context"; + +function makeContext(overrides: { + eventName?: string; + sha?: string; + ref?: string; + payload?: Record; + repo?: { owner: string; repo: string; }; +}) { + return { + eventName: overrides.eventName ?? "push", + sha: overrides.sha ?? "aaa111", + ref: overrides.ref ?? "refs/heads/main", + payload: overrides.payload ?? {}, + repo: overrides.repo ?? { owner: "test-owner", repo: "test-repo" }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; +} + +describe("resolvePrNumber", () => { + test("returns input override when provided", async () => { + const ctx = makeContext({ eventName: "push" }); + expect(await resolvePrNumber("42", "", ctx)).toBe(42); + }); + + test("ignores invalid input override", async () => { + const ctx = makeContext({ eventName: "push" }); + expect(await resolvePrNumber("abc", "", ctx)).toBeUndefined(); + expect(await resolvePrNumber("0", "", ctx)).toBeUndefined(); + expect(await resolvePrNumber("-5", "", ctx)).toBeUndefined(); + }); + + test("resolves from pull_request event", async () => { + const ctx = makeContext({ + eventName: "pull_request", + payload: { pull_request: { number: 99 } }, + }); + expect(await resolvePrNumber("", "", ctx)).toBe(99); + }); + + test("resolves from pull_request_target event", async () => { + const ctx = makeContext({ + eventName: "pull_request_target", + payload: { pull_request: { number: 77 } }, + }); + expect(await resolvePrNumber("", "", ctx)).toBe(77); + }); + + test("resolves from workflow_run.pull_requests", async () => { + const ctx = makeContext({ + eventName: "workflow_run", + payload: { + workflow_run: { + pull_requests: [{ number: 55 }], + head_branch: "feature-x", + }, + }, + }); + expect(await resolvePrNumber("", "", ctx)).toBe(55); + }); + + test("falls back to API when workflow_run.pull_requests is empty", async () => { + // Mock the GitHub API call by providing a token that will trigger the + // API fallback. Since we can't actually call the API here, we test that + // undefined is returned when the API is unreachable. + const ctx = makeContext({ + eventName: "workflow_run", + payload: { + workflow_run: { + pull_requests: [], + head_branch: "feature-x", + head_repository: { owner: { login: "test-owner" } }, + }, + }, + }); + // No token → skips API fallback → undefined + expect(await resolvePrNumber("", "", ctx)).toBeUndefined(); + }); + + test("returns undefined when all fallbacks fail", async () => { + const ctx = makeContext({ eventName: "workflow_run", payload: { workflow_run: {} } }); + expect(await resolvePrNumber("", "", ctx)).toBeUndefined(); + }); + + test("returns undefined for push event without override", async () => { + const ctx = makeContext({ eventName: "push" }); + expect(await resolvePrNumber("", "", ctx)).toBeUndefined(); + }); + + test("input override takes precedence over event payload", async () => { + const ctx = makeContext({ + eventName: "pull_request", + payload: { pull_request: { number: 99 } }, + }); + expect(await resolvePrNumber("5", "", ctx)).toBe(5); + }); +}); + +describe("resolveHeadSha", () => { + test("returns context.sha for pull_request event", () => { + const ctx = makeContext({ eventName: "pull_request", sha: "pr-sha-123" }); + expect(resolveHeadSha(ctx)).toBe("pr-sha-123"); + }); + + test("returns workflow_run.head_sha for workflow_run event", () => { + const ctx = makeContext({ + eventName: "workflow_run", + sha: "default-sha", + payload: { workflow_run: { head_sha: "wr-sha-456" } }, + }); + expect(resolveHeadSha(ctx)).toBe("wr-sha-456"); + }); + + test("falls back to context.sha when workflow_run.head_sha missing", () => { + const ctx = makeContext({ + eventName: "workflow_run", + sha: "fallback-sha", + payload: { workflow_run: {} }, + }); + expect(resolveHeadSha(ctx)).toBe("fallback-sha"); + }); +}); + +describe("resolveCurrentBranch", () => { + test("returns pull_request.head.ref for PR event", () => { + const ctx = makeContext({ + eventName: "pull_request", + ref: "refs/heads/main", + payload: { pull_request: { head: { ref: "feature-branch" } } }, + }); + expect(resolveCurrentBranch(ctx)).toBe("feature-branch"); + }); + + test("returns workflow_run.head_branch for workflow_run event", () => { + const ctx = makeContext({ + eventName: "workflow_run", + ref: "refs/heads/main", + payload: { workflow_run: { head_branch: "wr-branch" } }, + }); + expect(resolveCurrentBranch(ctx)).toBe("wr-branch"); + }); + + test("strips refs/heads/ prefix for push event", () => { + const ctx = makeContext({ eventName: "push", ref: "refs/heads/develop" }); + expect(resolveCurrentBranch(ctx)).toBe("develop"); + }); +}); + +describe("resolveBaseBranch", () => { + test("returns input override when provided", () => { + const ctx = makeContext({ eventName: "pull_request" }); + expect(resolveBaseBranch("staging", ctx)).toBe("staging"); + }); + + test("returns pull_request.base.ref for PR event", () => { + const ctx = makeContext({ + eventName: "pull_request", + payload: { pull_request: { base: { ref: "develop" } } }, + }); + expect(resolveBaseBranch("", ctx)).toBe("develop"); + }); + + test("returns workflow_run.head_branch for workflow_run event", () => { + const ctx = makeContext({ + eventName: "workflow_run", + payload: { workflow_run: { head_branch: "wr-base" } }, + }); + expect(resolveBaseBranch("", ctx)).toBe("wr-base"); + }); + + test("returns main as default when no context available", () => { + const ctx = makeContext({ eventName: "push", ref: "" }); + expect(resolveBaseBranch("", ctx)).toBe("main"); + }); + + test("returns main when workflow_run has no head_branch", () => { + const ctx = makeContext({ + eventName: "workflow_run", + payload: { workflow_run: {} }, + }); + expect(resolveBaseBranch("", ctx)).toBe("main"); + }); + + test("input override takes precedence over PR payload", () => { + const ctx = makeContext({ + eventName: "pull_request", + payload: { pull_request: { base: { ref: "develop" } } }, + }); + expect(resolveBaseBranch("release", ctx)).toBe("release"); + }); + + test("strips refs/heads/ prefix for push event", () => { + const ctx = makeContext({ eventName: "push", ref: "refs/heads/develop" }); + expect(resolveBaseBranch("", ctx)).toBe("develop"); + }); +}); diff --git a/src/diff.test.ts b/src/diff_test.ts similarity index 100% rename from src/diff.test.ts rename to src/diff_test.ts diff --git a/src/go.test.ts b/src/go_test.ts similarity index 100% rename from src/go.test.ts rename to src/go_test.ts diff --git a/src/index.ts b/src/index.ts index 8c589d2..d11f5e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,12 @@ import { saveArtifact, } from "./cache.js"; import { upsertComment } from "./comment.js"; +import { + resolveBaseBranch, + resolveCurrentBranch, + resolveHeadSha, + resolvePrNumber, +} from "./context.js"; import { buildFullReport, buildToolReport, @@ -22,8 +28,6 @@ import type { FileCoverage, } from "./types.js"; -// ── Input parsing ─────────────────────────────────────────────────── - function parseArtifactInputs(raw: string): ArtifactInput[] { return raw .split(/[\n,]+/) @@ -84,25 +88,11 @@ function parseFile(tool: string, filePath: string): { files: FileCoverage[]; war } } -// ── Determine PR number ───────────────────────────────────────────── - -function getPrNumber(): number | null { - const pr = github.context.payload.pull_request; - if (pr) return pr.number; - // For push events on branches with open PRs, the caller must supply the PR number - // via the github context or we skip commenting. - return null; -} - -// ── Main ──────────────────────────────────────────────────────────── - async function run(): Promise { try { // Read inputs const artifactPathsRaw = core.getInput("coverage-artifact-paths", { required: true }); - const baseBranch = core.getInput("base-branch") - || github.context.payload.pull_request?.base?.ref - || "main"; + const inputBaseBranch = core.getInput("base-branch"); const cacheKeyPrefix = core.getInput("cache-key") || "coverage-reporter"; const marker = core.getInput("update-comment-marker") || ""; @@ -110,10 +100,13 @@ async function run(): Promise { const failOnDecrease = core.getInput("fail-on-decrease") === "true"; const threshold = parseFloat(core.getInput("coverage-threshold") || "0"); const token = core.getInput("github-token") || process.env.GITHUB_TOKEN || ""; + const prNumberInput = core.getInput("pull-request-number"); + const showCommitLink = core.getInput("show-commit-link") !== "off"; - const commitSha = github.context.sha; - const currentBranch = github.context.payload.pull_request?.head?.ref - || github.context.ref.replace("refs/heads/", ""); + // Resolve context-dependent values + const baseBranch = resolveBaseBranch(inputBaseBranch); + const commitSha = resolveHeadSha(); + const currentBranch = resolveCurrentBranch(); // Parse artifact inputs const inputs = parseArtifactInputs(artifactPathsRaw); @@ -168,14 +161,16 @@ async function run(): Promise { const fullReport = buildFullReport(toolReports); // Render markdown - const markdown = renderReport(fullReport, marker, colorize); + const { owner, repo } = github.context.repo; + const commitInfo = showCommitLink ? { sha: commitSha, owner, repo } : undefined; + const markdown = renderReport(fullReport, marker, colorize, commitInfo); // Set outputs core.setOutput("overall-coverage", fullReport.overall.percent.toFixed(2)); core.setOutput("coverage-decreased", anyDecrease ? "true" : "false"); // Post / update PR comment - const prNumber = getPrNumber(); + const prNumber = await resolvePrNumber(prNumberInput, token); if (prNumber && token) { try { core.info(`Upserting comment on PR #${prNumber}`); @@ -194,7 +189,12 @@ async function run(): Promise { core.info(markdown); } } else { - core.info("Not in a PR context or no token — skipping comment."); + if (!prNumber) { + core.warning( + "Could not determine PR number. Set the `pull-request-number` input " + + "or ensure the action runs on pull_request / workflow_run events.", + ); + } // Still output the rendered markdown to the action log core.info("--- Coverage Report ---"); core.info(markdown); diff --git a/src/integration.test.ts b/src/integration_test.ts similarity index 86% rename from src/integration.test.ts rename to src/integration_test.ts index 960a020..d5129f1 100644 --- a/src/integration.test.ts +++ b/src/integration_test.ts @@ -27,14 +27,9 @@ import type { FileCoverage, } from "./types"; -// ── Helpers ───────────────────────────────────────────────────────── - const FIXTURES = join(import.meta.dir, "fixtures"); const read = (name: string) => readFileSync(join(FIXTURES, name), "utf-8"); -// ── Pre-computed expectations ─────────────────────────────────────── -// Manually counted from the fixture files so the tests are deterministic. - /** * bun-head.lcov expected per-file results (DA-line counting): * Button.tsx : 18 covered / 30 total = 60% @@ -69,10 +64,6 @@ const BUN_BASE_EXPECTED: Record { let headFiles: FileCoverage[]; let baseFiles: FileCoverage[]; @@ -82,8 +73,6 @@ describe("Bun LCOV integration", () => { baseFiles = parseLcov(read("bun-base.lcov")); }); - // ── Parsing ───────────────────────────────────────────────────── - test("head: parses all 5 files", () => { expect(headFiles).toHaveLength(5); }); @@ -112,8 +101,6 @@ describe("Bun LCOV integration", () => { } }); - // ── Diffing ───────────────────────────────────────────────────── - test("diff: detects deleted file from base (legacy/deprecated.ts)", () => { const diffs = computeFileDiffs(headFiles, baseFiles); const deleted = diffs.find((d) => d.file === "src/legacy/deprecated.ts"); @@ -159,8 +146,6 @@ describe("Bun LCOV integration", () => { expect(store!.delta).toBe(0); }); - // ── Report building ───────────────────────────────────────────── - test("buildToolReport: summary aggregates correctly", () => { const baseArtifact: CoverageArtifact = { tool: "bun", @@ -199,10 +184,6 @@ describe("Bun LCOV integration", () => { }); }); -// ═════════════════════════════════════════════════════════════════════ -// §2 Go — parse real tool output -// ═════════════════════════════════════════════════════════════════════ - /** * go-head.out expected per-file (statement counting): * cmd/server/main.go : 13 covered / 21 total = 61.90% @@ -258,8 +239,6 @@ describe("Go cover profile integration", () => { baseFiles = parseGoCover(read("go-base.out")); }); - // ── Parsing ───────────────────────────────────────────────────── - test("head: parses all 8 files", () => { expect(headFiles).toHaveLength(8); }); @@ -294,8 +273,6 @@ describe("Go cover profile integration", () => { expect(paths).toEqual(sorted); }); - // ── Diffing ───────────────────────────────────────────────────── - test("diff: detects deleted file (deprecated/old.go)", () => { const diffs = computeFileDiffs(headFiles, baseFiles); const deleted = diffs.find((d) => @@ -345,8 +322,6 @@ describe("Go cover profile integration", () => { } }); - // ── Report building ───────────────────────────────────────────── - test("buildToolReport: summary aggregates correctly", () => { const baseArtifact: CoverageArtifact = { tool: "go", @@ -372,10 +347,6 @@ describe("Go cover profile integration", () => { }); }); -// ═════════════════════════════════════════════════════════════════════ -// §3 Multi-tool full pipeline (Bun + Go combined) -// ═════════════════════════════════════════════════════════════════════ - describe("Multi-tool full pipeline", () => { let bunHead: FileCoverage[]; let bunBase: FileCoverage[]; @@ -497,10 +468,6 @@ describe("Multi-tool full pipeline", () => { }); }); -// ═════════════════════════════════════════════════════════════════════ -// §4 Edge cases — malformed / partial tool outputs -// ═════════════════════════════════════════════════════════════════════ - describe("Edge cases with real-ish tool output", () => { test("LCOV: file with only branch data (no DA lines) uses LH/LF fallback", () => { // Some coverage tools emit BRDA lines but no DA lines @@ -663,10 +630,6 @@ github.com/myorg/app/dead.go:22.1,25.2 3 0 }); }); -// ═════════════════════════════════════════════════════════════════════ -// §5 Numeric precision & percentage calculations -// ═════════════════════════════════════════════════════════════════════ - describe("Numeric precision", () => { test("LCOV percentages are rounded to 2 decimal places", () => { // 7/11 = 63.636363...% should round to 63.64% diff --git a/src/lcov.test.ts b/src/lcov_test.ts similarity index 100% rename from src/lcov.test.ts rename to src/lcov_test.ts diff --git a/src/render.ts b/src/render.ts index af55487..b7562eb 100644 --- a/src/render.ts +++ b/src/render.ts @@ -3,7 +3,11 @@ import type { ToolCoverageReport, } from "./types.js"; -// ── Helpers ───────────────────────────────────────────────────────── +export interface CommitInfo { + sha: string; + owner: string; + repo: string; +} function pad(n: number, width: number): string { const s = String(n); @@ -20,8 +24,6 @@ function deltaStr(delta: number | null, colorize: boolean): string { return ""; } -// ── Per-tool section ──────────────────────────────────────────────── - function renderToolSection(report: ToolCoverageReport, colorize: boolean): string { const lines: string[] = []; const toolLabel = report.tool.charAt(0).toUpperCase() + report.tool.slice(1); @@ -57,18 +59,24 @@ function renderToolSection(report: ToolCoverageReport, colorize: boolean): strin return lines.join("\n"); } -// ── Full render ───────────────────────────────────────────────────── - export function renderReport( report: CoverageReport, marker: string, colorize: boolean, + commitInfo?: CommitInfo, ): string { const parts: string[] = []; parts.push(marker); parts.push("## Coverage Report\n"); + if (commitInfo) { + const short = commitInfo.sha.slice(0, 7); + const url = + `https://github.com/${commitInfo.owner}/${commitInfo.repo}/commit/${commitInfo.sha}`; + parts.push(`[Commit ${short}](${url})\n`); + } + for (const tool of report.tools) { parts.push("```"); parts.push(renderToolSection(tool, colorize)); diff --git a/src/render.test.ts b/src/render_test.ts similarity index 73% rename from src/render.test.ts rename to src/render_test.ts index e6f9ab4..5188fa4 100644 --- a/src/render.test.ts +++ b/src/render_test.ts @@ -9,6 +9,7 @@ import { buildToolReport, } from "./diff"; import { renderReport } from "./render"; +import type { CommitInfo } from "./render"; import type { CoverageArtifact, FileCoverage, @@ -89,4 +90,32 @@ describe("renderReport", () => { expect(md).toContain("Go Coverage: 80.00%"); expect(md).toContain("**Total Coverage: 65.00%**"); }); + + test("renders commit link when commitInfo is provided", () => { + const head: FileCoverage[] = [ + { file: "src/index.ts", coveredLines: 8, totalLines: 10, percent: 80 }, + ]; + const toolReport = buildToolReport("bun", head, null, []); + const fullReport = buildFullReport([toolReport]); + const commitInfo: CommitInfo = { + sha: "abc1234567890", + owner: "myorg", + repo: "myrepo", + }; + const md = renderReport(fullReport, "", true, commitInfo); + + expect(md).toContain("[Commit abc1234]"); + expect(md).toContain("https://github.com/myorg/myrepo/commit/abc1234567890"); + }); + + test("omits commit link when commitInfo is not provided", () => { + const head: FileCoverage[] = [ + { file: "src/index.ts", coveredLines: 8, totalLines: 10, percent: 80 }, + ]; + const toolReport = buildToolReport("bun", head, null, []); + const fullReport = buildFullReport([toolReport]); + const md = renderReport(fullReport, "", true); + + expect(md).not.toContain("[Commit"); + }); });