Repository navigation
v0.1.0
Added
-
GitHub Action and
pharn-ci checkCLI. -
Diff coverage: the share of lines added in the change that the coverage report (lcov or
Cobertura XML) marks as executed, with an optional, inclusive threshold gate (thresholdinput,
--threshold,coverage.threshold). -
Agent setup inventory: the tracked agent setup files of Claude Code, Cursor, GitHub Copilot,
Codex, Gemini, Windsurf, Cline and MCP, listed by kind and never read. The kinds areclaude-md,
claude-local-md,agents-md,gemini-md,claude-settings,claude-settings-local,
claude-skill,claude-hook,claude-agent,claude-command,cursor-rules,cursor-mcp,
copilot-instructions,copilot-instructions-scoped,copilot-agents,copilot-prompts,
vscode-mcp,codex-config,windsurf-rules,cline-rulesandmcp-config, plus, for Aider,
Continue, Roo Code, Amazon Q Developer, Junie, Zed, Augment, Kiro, Goose, OpenCode and Gemini CLI
and for.claude/rules/,claude-rule,aider-config,conventions-md,continue-rules,
roo-rules,amazonq-rules,junie-guidelines,zed-rules,augment-rules,kiro-steering,
goosehints,opencode-configandgemini-settings..mcp.json,.claude/files and
.cursor/rules/are found in nested directories, and the more specific directory rule wins over
a file-name rule. -
Report v1: optional
agentSetup.summary(counts per kind and the tools involved), and an
Agent setup: N files · toolsline in the markdown, with the table collapsed above 25 files. -
Report v1 (
schema/report.v1.json), the stable JSON output, with message codes
DIFF_COVERAGE_BELOW_THRESHOLD,DIFF_COVERAGE_UNGATED,NO_COVERABLE_CHANGES,
COVERAGE_FILE_MISSING,COVERAGE_FILE_EMPTY,COVERAGE_FORMAT_UNSUPPORTED,
DIFF_UNAVAILABLEandNO_AGENT_SETUP_FILES. -
One sticky PR comment and a job summary; on fork PRs, where the token cannot comment, the job
summary only. -
.pharn/ci.ymlconfig file, overridden by Action inputs and CLI flags. -
Coverage exclusions: glob patterns (
*,**,?) fromcoverage.exclude, the CLI's repeatable
--exclude <glob>and the Action'sexcludeinput, merged (their union applies), leave matching
changed files out of diff coverage. The report gains the optionaldiffCoverage.excludedFiles
field, and the comment an "Excluded" summary listing them. -
Cobertura XML coverage parsing in core, read by the CLI and the Action.
-
Coverage format selection: the
coverage-formatAction input, the--coverage-formatCLI flag
andcoverage.formatin.pharn/ci.ymltakelcov,coberturaorauto. The flag or input
wins over the config file. -
autois the default: the format is detected from the first 1 KiB of the file, and the CLI
(stderr) and the Action (log) print which format was used, for example
coverage: cobertura (auto-detected). Clover and JaCoCo native XML are not supported. -
Message code
COVERAGE_FORMAT_UNSUPPORTED: underauto, a coverage file that is neither lcov
nor Cobertura XML is an error with a threshold, a warning without. -
Report v1: optional
diffCoverage.format(lcovorcobertura), the format the report was
read as. -
COVERAGE_LINES_SKIPPED(info): malformed lcovDArecords and Cobertura<line>elements that were skipped while reading the coverage files are now counted, and the report says how many (a count only, no line numbers or file names). The verdict and the diff coverage numbers do not change, and reports that end before diff coverage is computed do not carry the message. -
Coverage auto-discovery: with no
coverage-fileinput, no--coverageflag and nocoverage.file, the Action and the CLI look for the coverage report atcoverage/lcov.info,lcov.info,coverage.xml,coverage/cobertura-coverage.xmland**/coverage.cobertura.xml, and printcoverage: <path> (auto-discovered)for each file they read. More than one kind found is an error asking you to set the path; none found is aCOVERAGE_FILE_MISSINGmessage (previously a usage error). Complete workflows for vitest, jest, pytest, .NET and cargo are inexamples/workflows/. -
Expected agent tools:
setup.expectin.pharn/ci.yml, theexpect-agent-setupAction input and the CLI's--expect-agent-setupflag (repeatable) name agent tools (for exampleclaude-code,cursor) that should have a setup file. Any tool no setup file configures is listed in the newagentSetup.missingfield and reported asAGENT_SETUP_EXPECTED_MISSING(warn, or error when agent setup is required). A repo that expects nothing sees no change. -
AGENT_SETUP_FILE_EMPTY(warn): a listed agent setup file that is empty (size 0 inHEAD, fromgit ls-tree -r -l) is reported with the first path, and stays in the inventory. Whengit ls-treefails, none is reported (the CLI notes it on stderr, the Action warns). Because it is a warning, the verdict of a repo with an emptyCLAUDE.mdchanges frompasstowarn. -
TEST_FILES_DELETED(info): the change deletes a test file, or renames one to a name that is not a test file
(F-21). The message names up to five of them; the verdict is unchanged. Test files are recognized by name
(.test./.spec.in the basename, or atest,testsor__tests__directory). -
Agent setup inventory covers more tools (F-11): 13 new setup kinds,
claude-rule(.claude/rules/),aider-config,conventions-md,continue-rules,roo-rules,amazonq-rules,junie-guidelines,zed-rules,augment-rules,kiro-steering,goosehints,opencode-configandgemini-settings, so the files of Aider, Continue, Roo Code, Amazon Q Developer, Junie, Zed, Augment, Kiro, Goose, OpenCode and Gemini CLI are listed.agents-mdis now also credited to amp, jules, opencode and zed, andagentSetup.summary.toolsin Report v1 accepts new tool names within v1 (a lowercase name pattern replaces the closed list). -
PR-number mode: the
pr-numberandhead-shaAction inputs let pharn-ci measure and comment on a
pull request from aworkflow_runorworkflow_dispatchrun, with no PR code checked out. The PR,
its diff and its file list come from the GitHub API (two new read-only calls: get a pull request,
get a git tree),pr-numberis validated, andhead-shais checked against the PR head. The README
documents the two-workflow pattern for comments on fork PRs. -
Three warn-level messages for a change that touches the inputs of its own grade:
CONFIG_CHANGED_IN_DIFF(the diff edits the pharn-ci config in use or a file under.github/workflows/),COVERAGE_FILE_TRACKED(the coverage file is a tracked file, so the change can supply its own numbers) andDIFF_BINARY_COVERED_FILE(the diff shows a covered source file as binary, e.g. through-diffin.gitattributes, so its added lines were not measured). They lift apasstowarnand never fail the check. -
Several coverage files and globs: the
coverage-fileAction input takes one entry per line, the CLI's--coverageflag is repeatable andcoverage.filein.pharn/ci.ymlcan be a list. Each entry is a path or a glob (*,**,?, relative to the workspace, the current directory or the repo root), soTestResults/**/coverage.cobertura.xmlandpackages/*/coverage/lcov.infowork. The files are merged with hit counts summed;autodetects the format of each file. A glob that finds nothing isCOVERAGE_FILE_MISSING; a walk stops at 1,000 matched files or 100,000 visited entries, never follows a symbolic link and skips.gitandnode_modulesunless named. -
Require agent setup: the
require-agent-setupAction input, the CLI's--require-agent-setupflag andsetup.requirein.pharn/ci.yml(off by default) makeNO_AGENT_SETUP_FILESan error and the verdictfailwhen the repo has no agent setup files. -
DIFF_COVERAGE_UNGATED(info): diff coverage was measured, with its percent in the text, but no
threshold gates it (none is set, or it is 0). Before, an ungated run passed with no message. -
Data handling section in
SECURITY.md, linked from the README's Safety model: what the Action reads, what it sends to your GitHub API, what lands in the PR comment, job summary, step log andreport.json, and that nothing is sent to Pharn. -
The rendered report (PR comment, job summary, CLI markdown) adds a one-line hint under each message
saying how to fix it, and says what is missing (typicallyAGENTS.mdorCLAUDE.md) when no agent
setup files are found. Message text inreport.jsonis unchanged. -
coverage.excludein.pharn/ci.ymlcan be written as a one-line flow list, e.g.
exclude: ["**", "dist/**"], which used to stop the check with exit code 2. -
Coverage root:
coverage.rootin.pharn/ci.yml, thecoverage-rootAction input and the CLI's--coverage-rootflag name the repo-relative directory that relative coverage paths start from, so a package-relative lcov or Cobertura report in a monorepo is measured at the right files. Absolute paths are never remapped. -
COVERAGE_PATHS_PARTIALLY_UNMATCHED(info): some of the coverage report's file paths are not tracked files, so their coverage is not used. The verdict is unchanged. Thefixtures/goldenandfixtures/golden-coberturareports gain this message.
Changed
- When more than one coverage file is read (a glob, a list, or several
--coverageflags), thecoverage: <format> (<how>)log line names its file:coverage: coverage/a.info: lcov (auto-detected). With one file the line is unchanged, and so is the(auto-discovered)line. - The PR comment, job summary and CLI markdown now show the overview sentence in a GitHub alert under the verdict heading:
[!CAUTION]for fail,[!WARNING]for warn,[!TIP]for pass. Report data and message text are unchanged. - The PR comment, job summary and CLI markdown now show the pharn logo (a 48×48 image from the repository's raw
mainURL) above the verdict heading. Report data and message text are unchanged. - The PR comment, job summary and CLI markdown have a new look. They open with a branded header (a verdict heading, a one-line overview and a summary table), use emoji section headings with counts, and collapse the changed-files and agent setup tables in
<details>blocks (the changed-files block is open when diff coverage is below the threshold). They end with a footer naming the tool version, the report schema and the coverage format. Report data and message text are unchanged. - Internal refactor: the CLI and the Action share their Node I/O (git, file reads, directory listing) and their coverage-file reading through a private workspace package,
@pharn-ci/node-io. No user-visible change. - Large inputs: the diff and lcov coverage are read line by line (F-15), so a diff is no longer limited by a 256 MiB buffer and lcov no longer by the longest string the engine can build (only one line that long still fails). The CLI and the Action stream git's output and the coverage file instead of reading them whole. Cobertura coverage and the config stay string-based. The limits of each input and what you see when one is passed are in
docs/LIMITS.md§8 (Size limits). - Versioning policy for 0.x (
docs/RELEASING.md): Action inputs and outputs, CLI flags and message
codes are never removed before 1.0; a renamed input keeps working under its old name, with a
warning, for at least one minor release; a changed default ships only in a minor release and is
listed here. The README status note anddocs/LIMITS.mdsay the same. - Agent setup inventory quality (F-10):
.mcp.json,.claude/settings.jsonand.claude/settings.local.jsoncount at the repo root only (a nested copy in a package or a test fixture is no longer listed); a dot-file or README under a directory kind (such as.claude/skills/x/.gitkeepor.github/agents/README.md) is not listed; and a path with anode_modulesdirectory is never listed. Repos with such files see fewer files inagentSetup.files. - A coverage entry (the
coverage-fileinput,--coverageorcoverage.file) containing[,],{or}is
rejected as unsupported glob syntax, even without*or?. A literal coverage path with one of those characters
is now a usage error. - The rendered report lists messages before the agent setup inventory, and a file's uncovered-lines cell shows
at most 20 ranges followed by…and N more. The Action fits an oversized PR comment (65,000 characters) or
job summary (1 MiB) by rendering fewer table rows instead of cutting the end, so the verdict and messages
stay visible;report.jsonkeeps the full report. - The diff coverage table lists files by coverage, lowest first (then by path), so the files that
need tests stay visible when the table is cut at 50 rows; a cut table now points to the full list
inreport.json(the Action'sreport-pathoutput, or--format json). .pharn/ci.ymlerrors for unsupported YAML now name the supported form (for example "use a block
list").- A coverage report whose paths match no tracked file is now
COVERAGE_PATHS_UNMATCHED: an error, and
the verdictfail, when a threshold is set (a warning without one), with the suggested coverage
root when one can be found. Before, it passed asNO_COVERABLE_CHANGES. If your coverage paths are
relative to a package directory, setcoverage.root. DIFF_UNAVAILABLEis now an error, and the verdictfail, when a threshold is set (a warning
without one), like a missing, empty or unrecognized coverage file. If a shallow checkout makes
the diff unavailable, useactions/checkoutwithfetch-depth: 0or rely on the GitHub API
fallback.- The diff coverage threshold is compared exactly against covered / coverable lines instead of the
percent rounded to two decimals: 15,999 of 20,000 lines (79.995%) now fails a threshold of 80.
A failing coverage that would round up to the threshold is shown rounded down (79.99%). - Threshold 0 is documented as gating no percentage (reported with
DIFF_COVERAGE_UNGATED); it
still fails closed when coverage cannot be measured. - The repository no longer tracks the maintainer's PHARN workflow install (
pharn/,
pharn.config.json,pharn.records.jsonand PHARN's own rootLIMITS.mdandTHREAT-MODEL.md),
souses: pharn-dev/pharn-ci@...downloads about 5 MB less. pharn-ci's own threat model and
limits aredocs/THREAT-MODEL.mdanddocs/LIMITS.md.
Fixed
- CLI: a flag value that starts with
-(for example--threshold -1or--base -x) no longer fails with the misleading "Did you forget to specify the option argument?". The error (exit code 2, unchanged) now says the value must be written with=, for example--threshold=-1. - Thresholds (
--threshold, thethresholdinput andcoverage.threshold) accept only plain decimal numbers from 0 to 100, such as80or72.5. Exponent (1e2), hex (0x10), a sign, a bare or trailing dot (.5,80.),Infinity,%and inner spaces were read as JavaScript numbers before and are now refused with the existing "must be a number between 0 and 100" error (exit 2). - The README quick start no longer aborts for vitest: it says
@vitest/coverage-v8must be a dev dependency, and a
new "Coverage per ecosystem" table shows the report andcoverage-filefor vitest, jest, pytest-cov, coverlet,
cargo-llvm-cov, Go, and JaCoCo/Kover through a Cobertura converter. - An lcov file whose
SFrecords carry noDAlines (a crashed or truncated reporter) is now
COVERAGE_FILE_EMPTY, as the Cobertura equivalent already was, instead of passing as
NO_COVERABLE_CHANGES. lcov files without a validDAline are left out of the coverage map. - Docs: the quick start pins
actions/checkoutto a commit SHA; the Action reference gives the
realthresholdandbase-refdefaults; the docs name Cobertura as untrusted input, the
GET /usercall, and the bundled transitive dependencies. - The Action no longer crashes with a stack trace before running when
github-tokenis empty. The
token is used only when an API call is needed, socomment: falsewith a local diff works
without one; otherwise the compare fallback reportsDIFF_UNAVAILABLEand the comment step
warns, as for any API failure. Thegithub-tokeninput now sayspull-requests: writeis
needed only for the comment. - Concurrent runs on one PR no longer leave two sticky comments: after creating its comment, the
Action deletes newer duplicates of its own marker comment (only ones its own login wrote) and
keeps updating the oldest. The README quick start now sets a per-PRconcurrencygroup. - Merge queue runs (
merge_group) diff againstmerge_group.base_shainstead of always reporting
DIFF_UNAVAILABLE. - An explicit
base-reffound neither in the checkout nor through the GitHub API now fails the
step with an error naming the ref, instead of reportingDIFF_UNAVAILABLE. - A
configinput naming a file that does not exist now fails the step with "config file not
found", like the CLI's--config, instead of running with no config (and so without a threshold
held only in the config). The input no longer has a default inaction.yml; left empty, it reads
.pharn/ci.ymlwhen present, as before. - The CLI lists tracked files from the repository root when run from a subdirectory, so coverage
paths are compared with the same names wherever it runs. The agent setup inventory of a
subdirectory run now covers the whole repo. - With a GitHub App token, the sticky comment is now found and updated instead of re-posted on every
run. Each workflow and job owns its own comment, marked by a<!-- pharn-ci scope: ... -->line
under the marker, so sibling jobs no longer overwrite each other. Comments from earlier versions
keep being updated, but a PR whose single comment was shared by two jobs gains one comment for
the second job after the upgrade. - When the Action falls back to the GitHub compare API and the response has no
charset=utf-8(it
arrives as bytes) the diff is decoded as UTF-8, and a body that is not diff text (such as JSON)
degrades toDIFF_UNAVAILABLE. Before, the step crashed with "text.split is not a function" and
wrote no summary, report or outputs. - A hung GitHub API or
gitcall no longer stalls the Action for minutes per stage. Each API request
is aborted after 30 s, and a 5xx, a network error or a rate limit asking for at most 60 s is retried
once (honouringretry-afterandx-ratelimit-reset);gitcalls in the Action and the CLI are
killed after 2 minutes. The README example setstimeout-minutes: 5on the step. - Docs: the
github-tokeninput says the token is always used inpr-numbermode; the README
quick start'sconcurrencygroup falls back togithub.ref, so merge-queue entries no longer
cancel each other; the token-footprint bullet listsGET /userand thepr-numberreads; and
SECURITY.md's data handling names every message code that quotes a path, including the coverage
report path quoted byCOVERAGE_PATHS_UNMATCHED.
Security
- The PR comment cannot be altered through file names or message text: control characters in
paths are replaced and HTML in messages is escaped. - The sticky comment is matched by its author as well as its marker, so a comment someone else
posted is never taken over. - A base ref starting with
-is rejected, and revisions reach git after--end-of-options. - The config parser runs in linear time on hostile input.
- An empty or unparseable coverage file reports
COVERAGE_FILE_EMPTYinstead of passing. - A
pull_request_targetrun that checked out the base branch reportsDIFF_UNAVAILABLEinstead
of passing. - PR comments are capped below GitHub's size limit; the job summary keeps the full report.
- Repository protection: signed, linear, squash-merged
mainbehind six required checks;
immutablev*tags; every third-party action pinned to a commit SHA. SECURITY.mdanddocs/THREAT-MODEL.mdstate that pharn-ci has one maintainer: pull requests
are merged with themainruleset's admin bypass and releases are self-approved, so no second
person reviews a change; they list what still enforces integrity and the plan for when a second
maintainer joins. The threat model no longer saysmaintakes only reviewed pull requests (F-18).- A
pull_request_targetrun that checked out the base branch no longer tells users to check out the
PR head, which would run fork code with a write token.DIFF_UNAVAILABLEnow says it cannot be
measured safely and points topull_requestandSECURITY.md; the safeworkflow_runpattern for
comments on fork PRs is documented in the README. - Cobertura path resolution no longer slows down quadratically on a crafted report with thousands
of<source>entries and thousands of filenames (F-24). Results are unchanged, except that a
relativefilenamestarting with..is now joined to at most the first 16 distinct sources. - PR-controlled text in messages (the coverage path from
.pharn/ci.yml, the base ref) no longer
renders as links, images, emphasis or @mentions in the PR comment and job summary: markdown
characters are escaped,@cannot mention, and bare URLs are not autolinked. The report JSON
keeps the message text unchanged. - The Action's
git rev-parse --verifylookup of the base ref now passes the revision after
--end-of-optionstoo, so every git call that takes a revision separates it from options. - Release tags are documented as unsigned lightweight tags, with pinning the full commit SHA as the
integrity control (README.md,SECURITY.md,docs/RELEASING.md). Thepackages/action/dist
diff is no longer collapsed in pull requests (.gitattributesno longer marks it
linguist-generated), and CI writes the changed dist files and the bundle's sha256 to the job
summary of every pull request so reviewers see the shipped artifact change.