Skip to content

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 03 Nov 21:22
Immutable release. Only release title and notes can be modified.
cb87afd

The fixes of the 2026-11-01 review. Test integrity in block fails closed when it cannot see the change, TI03 blocks
the ways a skip was written to slip past it, unreadable agent settings and MCP files are errors, and the upload to
pharn.dev is bounded in time and never sent from a pull_request_target run. The Action input fail-on-threshold is
renamed fail-on-verdict; the old name still works, with a warning. Some warnings become errors on purpose: see
Changed and Security. No schema field or message code was added, removed or renamed.

Added

  • The Action input fail-on-verdict, the new name of fail-on-threshold (review 2026-11-01, finding 27): it gates on
    the verdict, whatever made it fail, not only on the coverage threshold. fail-on-threshold still works when
    fail-on-verdict is not set, with a deprecation warning, and is ignored (with a warning) when both are; both stay
    declared in action.yml, which no longer gives either a default (the default, true, is applied in code, so a set
    old name is never shadowed).

Changed

  • The Action applies two things the pharn.dev app contract already named (review finding 15 f and g). No schema change,
    no new input, no new message code and no new host.

    • The app's settings.messageMaxBytes now lowers the upload's per-message cap: the cap is the smaller of 2048 bytes
      and that setting, taken only from a policy response that validated whole in the same run (never under
      policy: never or a file: policy). The app's other settings (uploadJunit, redactMessages) and its
      quarantine list are validated and still not applied.
    • meta.mergeBase is recorded for a run with a local diff: the merge base of the resolved base and meta.headSha
      (the pull request's head, so on a pull_request run the fork point, not the base tip), one extra local
      git merge-base per run at most. Every other run, and a git merge-base that fails or prints no object id (a
      shallow checkout), records null and changes nothing else. Matrix legs of one job remain a documented limit
      (docs/LIMITS.md).
  • The Action's packages/action/dist/index.js shrinks from about 11.7 MB to about 1.7 MB: the TypeScript parser no
    longer sits inside it. scripts/bundle.mjs copies typescript@5.9.3's lib/typescript.js byte for byte beside the
    bundle as packages/action/dist/typescript.js (9.1 MB, the dist's second committed file), and the Action loads it
    only when a pull request makes it parse a test file or test config, after checking its sha256 against a pin in
    packages/action/src/vendored-typescript-pin.ts. A file that does not match the pin fails the step with an error
    naming the file and both hashes, and nothing is reported. A run that parses no test file never reads it. The CLI
    still inlines the parser. No schema change and no new message code. See docs/THREAT-MODEL.md, G1.

  • One source for three text rules in @pharn-ci/core (text.ts): the code-unit comparator byCodeUnit (about 50
    copies, in core, node-io and the tests, are gone), the surrogate-safe cut cutCodeUnits (the … inside the limit,
    an option for a cut without it, never inside a surrogate pair) and utf8ByteLength. No limit number, message code
    or schema changes. Two byte-level effects: a TI09 value (base/head) is now cut to at most 80 code units, …
    included (it was 81), and a key text to at most 100 (it was 101); and the three cuts that could leave a lone
    surrogate (TI01 and TI06 details, the 500-unit test name) no longer do.

  • The Action's run.ts items of the 2026-11-01 review. No schema change and no new message code.

    • A PR comment or job summary cut as a last resort (the messages alone over the limit) now closes every collapsed
      <details> section it cuts into before the truncation note, so the note is no longer hidden inside it (37).
    • Fewer GitHub API calls (47): a pull request is read once per run (in pr-number mode it was read up to three
      times: the target, the agent PR detection and the accept label), and the comments and the token's login are
      listed once for the upload's comment link, the pointer and the sticky comment (they were listed twice when an
      upload was tried, including when the app answered 404). A failed listing is still asked again by the next
      reader. The config and base-ref inputs are read once, and the coverage file is read from the inputs after the
      org policy, as the CLI reads it.
    • The base is decided once for the diff and for report.json (45): meta.baseSha is now the base the diff was taken
      against, recorded only when it is a full commit SHA. With a base-ref branch name, a pull_request run used to
      record the PR base and a push run the previous commit; both now record null. A 64-character (SHA-256) commit
      SHA as base-ref is no longer also tried as origin/<sha>.
    • The warning for a truncated PR-head file list in pr-number mode names what may miss files: the agent setup scan,
      the secret scan and the org policy's forbidden-file rule (P08) (48). docs/THREAT-MODEL.md residual 36 records
      why a bot's marker comment still counts as pharn-ci's own when the token's login is unknown: no API names an
      installation token's own login.
    • An unexpected exception in the upload is still failed with the reason internal and never changes the check,
      and its log line now names the error class (never its message); with debug logging (RUNNER_DEBUG=1) it then
      fails the step with the message, so it can be found (28). A 413 from the app is still read without its body:
      the app's API defines none, so maxBytes stays null (29, docs/LIMITS.md).
    • Not changed: finding 39 (the two test-sources tests vi.mock("@pharn-ci/core")) is dropped. The mock is a pinned
      spy-through to the real check(); replacing it would delete the test that pins it, which TI02 correctly blocks.
  • Docs and metadata (second review, 2026-11-01): README says nothing phones home only without id-token: write, the
    quick precedence sentence no longer claims .pharn/ci.yml "can only tighten" (only the test-integrity mode is
    merged; an input replaces the config's threshold, file, root, format and setup.require), the token-footprint
    bullet names the accept-label reads, and the AI-agnostic and "fully stateless" sentences name P11 and the opt-in
    upload. SECURITY.md best practices name what id-token: write turns on. docs/THREAT-MODEL.md §1 names the
    pharn.dev app, its responses, app-url and the OIDC token, and §3 gains item 37 (the threshold comes from the PR).
    docs/LIMITS.md §1 points at TI02, TI05 and TI06 instead of "not detected yet", gains "What P01 misses", and says
    fail-on-threshold: false un-gates every cause of a fail verdict; action.yml says so too, and its
    github-token text names the commits read and its fail-closed agent-PR detection. AGENTS.md describes Report v2,
    the schemas, the Python index subprocess and the opt-in pharn.dev calls. docs/MARKETPLACE.md no longer says the
    listing is refreshed on each release: the Action is not listed yet. CONTRIBUTING.md says why secret-pattern
    tests build tokens at test time.

  • Release verify refuses a run whose HEAD is not the chore: release X.Y.Z (#N) merge commit, so a tag can no
    longer land on a later PR (v0.2.0 was tagged four PRs after its release PR).

  • The skylos-nine fixture pins now cover fixtures/skylos-nine-pytest/ and fixtures/skylos-nine-sources/ too, and
    name the commits and reasons of the two change.diff edits made before the pins existed (docs/PRECISION.md §3).

  • Test-only, recorded late: 0.4.0 (#111) moved the growth tests to CPU time and raised four vitest timeouts to 300 s
    (ac-ti0205-growth, ac-ti06-ti12-linear, ac-py-index-growth, limits); the growth ceilings themselves did not
    change.

  • An org policy that requires a deny rule or a hook (setup.required_permissions_deny or setup.required_hooks,
    with P03 or P06 at block) now fails the check when the agent settings files could not be read, instead of warning
    (second review, HIGH 4). The files are unread whenever the diff came from the compare API (a shallow checkout, the
    actions/checkout default; pr-number mode; a failed local diff), so before this a default checkout turned the
    policy's block into a warning and the step passed with the requirement unchecked. POLICY_SETTINGS_INVALID is now
    error there, as it already was under policy.required; use fetch-depth: 0. A rule the policy sets to warn or
    off keeps the warning.

  • TI10 reports a test script replaced by a lone true or : ("test": "true", or true || vitest run) as forcing a
    successful exit, like exit 0 (second review, HIGH 5). docs/LIMITS.md lists what TI10 still misses:
    node -e "process.exit(0)", a positional path filter, --dir and --project.

  • .pharn/ci.yml refuses a __proto__ key, bare, quoted or nested: it set the prototype of the parsed map, so a
    coverage block under it escaped the unknown-key check (second review, finding 26).

  • The PR comment shows the accepted line once: TEST_INTEGRITY_ACCEPTED is no longer repeated under Messages when the
    Test integrity section shows the accepted line, and its hint moves under that line (finding 32).

  • TEST_INTEGRITY_MODE_IGNORED names the config in use when the config input points elsewhere than
    .pharn/ci.yml; the upload bundle redacts secrets in object keys too; action.yml states the python3
    requirement; the comment renderer uses the shared unsafe-character set, and the Action reads logins with core's
    LOGIN_PATTERN (findings 45, 46 and 48, cleanups). No schema change.

  • Tests (findings 12, 35, 41): the uploaded run's report equals the offline one apart from meta.upload; every
    declared input and output must be in the never-removed list (test-integrity and accept-label were missing); the
    policy request treats 409, 413 and 422 as unexpected statuses.

  • The PR comment no longer hides a test-integrity finding id whose findings all fall past the 20 it lists: that
    id's TEST_INTEGRITY_<id> message now stays under Messages (review finding 10). No schema change.

  • No hint tells the reader to loosen the gate (review finding 11): the POLICY_UNAVAILABLE hint (also under a P10
    violation) no longer offers policy: never, and the AGENT_SETUP_EXPECTED_MISSING hint (also under a P01
    violation) asks whoever owns setup.expect to change it instead of suggesting removing the tool from it.

  • A file: policy's looser test_integrity.mode is now named in POLICY_OVERRIDE_IGNORED when neither the
    test-integrity input nor .pharn/ci.yml sets a mode (review finding 14). The mode in force is unchanged.

  • Docs (review findings 17 and 18): README and SECURITY.md name the pull request commits read made on every
    pull_request run for agent-PR detection (with an empty or under-scoped token every pull request counts as an
    agent PR), the accept-label reads and every git subcommand; the quick start's id-token: write comment and
    the token-footprint bullet say the permission also turns the upload to pharn.dev on.

  • Docs (review findings 28 and 30): docs/LIMITS.md gains "What P08 misses"; docs/RELEASING.md says the
    release PR is the last change merged before the dispatch and that schemaVersion is the one v1 field whose
    type changed in Report v2; CONTRIBUTING.md names the fixture naming rule and its two historical exceptions.

  • TI09 no longer lets a test-runner config made unreadable at HEAD turn a block into a warning (review finding 22):
    a modified or renamed config whose settings were read at the merge base and that is not found at HEAD (an IIFE or
    other opaque wrapper) or cannot be read there (padded past the size cap, nested too deep, a syntax error, dropped)
    is now one (config) finding at the mode's level (block in test-integrity: block; it was a warn, or no
    finding for a refused head). An added config and a config already unreadable at base are unchanged. No schema
    change and no new message code.

  • report.json and the upload now neutralize the same invisible and bidirectional characters the PR comment does
    (U+200B to U+200F, U+202A to U+202E, U+2066 to U+2069 and U+FEFF), besides the control characters: in message
    text, signal details, test names, test-config values and policy violation details each becomes a space or U+FFFD,
    so hidden or reordered text from a pull request cannot reach a consumer of the report (review finding 23). The
    DIFF_UNAVAILABLE detail, the coverage file path in COVERAGE_FILE_MISSING, COVERAGE_FILE_EMPTY and
    COVERAGE_FORMAT_UNSUPPORTED, and the reason in POLICY_SETTINGS_INVALID are now cleaned too (control characters
    included); text without such characters is unchanged. The accept-label input (and a policy's accept label) is
    now also refused when it holds a line or paragraph separator (U+2028, U+2029). No schema change and no new message
    code.

  • A pull_request_target run is never uploaded to pharn.dev: it asks for no upload token and sends nothing, and
    meta.upload.status is skipped (pharn-ci refuses to measure its base-branch checkout, so the app would record a
    run that measured nothing). The report, comment, summary, verdict and policy request are unchanged. No schema change.

  • The install hint (Install the Pharn GitHub App …) now also ends the PR comment and the job summary when the org
    policy request is answered 404, for example with upload: never; a run whose policy request and upload both get
    a 404 shows it once, and a 204 (no policy yet) shows none. No schema change.

  • The OIDC token request (for the org policy and the upload, only with id-token: write) now gives up after
    30 s, and the run goes on as if the runner offered no token; @actions/core alone retried it up to ten times
    with a 3-minute socket timeout each (review LOW 21). No schema change and no new message code.

  • meta.scope in report.json and in the upload is cut to 200 code points with control characters removed, like
    the other run metadata a pull request can influence: the workflow name: in it comes from the pull request's
    workflow file (review LOW). No schema change.

  • Test integrity in block now fails closed when it cannot see the change (review HIGH 4): DIFF_UNAVAILABLE and a
    TEST_INTEGRITY_INDEX_UNAVAILABLE for which TI02 and TI05, or TI09, did not run at all are now errors in
    test-integrity: block (they were warnings; the per-file form stays a warning). The Action now fails the step when
    a local git diff against a base that resolves fails, naming the base and fetch-depth: 0, instead of falling back
    to the GitHub compare API. In pr-number mode, block fails when a test file or a test-runner config is modified
    (use warn there). No schema change and no new message code.

  • TI03 now blocks an unconditional skip added to a test that exists at base however the change writes it (review
    B1): a test renamed to it.skip(…) or it.skipIf(true)(…) with the same body, a describe.skip wrapped around
    existing tests, an existing test moved into a skipped describe, and pytestmark = pytest.mark.skip(…) added at
    module level or in a test class. When the test index compared the files, TI03 now also reads the base-to-head test
    pairs TI05 reads and the describe, test-class and pytestmark modifiers around each test, giving one finding per
    line. The Python index and the line reader read pytestmark. Deliberate precision change: an unguarded in-body
    pytest.skip(), pytest.xfail() or self.skipTest() added to an existing test now blocks when the Python index ran
    (it warned). The line reader's sentence for a skip in a test the diff shows as new now says only what the diff
    shows: "A skip was added to a test that no removed line of this file opens, so the diff shows it as new." No schema
    change and no new message code.

  • Docs: the report.v2 schema now says DIFF_UNAVAILABLE is an error with a coverage threshold or in
    test-integrity: block, and TEST_INTEGRITY_INDEX_UNAVAILABLE is an error in block when TI02 and TI05, or TI09,
    did not run at all (the levels #126 shipped). docs/THREAT-MODEL.md §3 now renders residual numbers 8 to 35 as
    written, the numbers the docs and tests cite. No code change.

  • *.test.mts, *.spec.mts, *.test.cts and *.spec.cts files are test files, so TI01, TI03, TI04 and the other
    test-file signals read them, and TI13 no longer fires in them (review 2026-11-01, finding 13). The eight JS/TS
    extensions are one exported list, JS_TS_EXTENSIONS in @pharn-ci/core, that every site derives from.

  • After a successful upload to pharn.dev, the PR comment an earlier run of the same workflow and job left is updated
    to one line pointing to the run on pharn.dev, instead of keeping its old verdict beside the Pharn GitHub App's
    comment (review finding 7). Nothing is created or deleted; a failed update is a warning.

  • The upload to pharn.dev now ends within 90 s of its start (review finding 8): each request and the PUT is cut off
    at the time left, the retry happens only when enough time is left, and running out is a failed upload (reason
    …:deadline) that never fails the check. A slow pharn.dev could take about 6 minutes, past the recommended
    timeout-minutes: 5. No schema change.

  • A failed git ls-files (or, in PR-number mode, a failed listing of the PR head's tree) now fails the Action step,
    as it already failed the CLI. It used to warn and run the agent setup, secret and policy checks on an empty file
    list, which passed them and skipped the coverage path check (review finding 9).

  • A base that resolves to HEAD or a descendant of it (base-ref: HEAD, base-ref: ${{ github.sha }}, or the CLI's
    --base HEAD) is now DIFF_UNAVAILABLE, an error with a threshold or in test-integrity: block, instead of a
    NO_COVERABLE_CHANGES pass on an empty diff (review finding 13). Running the CLI with a threshold on a branch
    that equals its base now exits 1.

  • The PR comment and the job summary show a 20-cell bar of the diff-coverage percentage (▰ covered, ▱ not)
    above the "N% of changed lines covered" line. No schema change.

  • The TI03 hint leads with the fix, as the other block hints do: "Remove the skip so the test runs again, or say in
    the PR why it must stay", then the accept label (review 2026-11-01, A4-F5).

  • Test-only (review SPEC finding 29): the markdown render tests compare with committed files byte for byte
    (fixtures/markdown/, fixtures/coverage-paths/package-relative/) instead of snapshots that pnpm test -u could
    silently rewrite. No output changes.

Security

  • A pull request can no longer lower the coverage threshold past TI14 by putting a Unicode space before the comment
    (threshold: 50<U+00A0># lowered): the config reader opened a comment after any \s character and set the gate
    to 50, while TI14 read the line by YAML's rule (space or tab only), saw no number and stayed silent. Both now share
    one comment rule (findComment), and the config refuses a # that follows any other space, with an error naming
    the character. TI14 also fails closed when a removed threshold is replaced by a value it cannot read as a plain
    number, such as one padded past the 400-character kept-line cap. A config that relied on such a space before #
    now fails with that error; replace the character with a space.

  • The dependency audit in CI and in the release job runs at --audit-level=moderate instead of high (second
    review, HIGH 3): the Action bundles its production dependencies into packages/action/dist, and two of the five
    undici 6.29.0 advisories were moderate. A known moderate advisory in a production dependency now fails the build.

  • The undici bundled into the Action moves from 6.29.0 to 6.29.1 (review HIGH 6). It carries every GitHub API request
    and the OIDC token request, and 6.29.0 has three high and two moderate advisories (GHSA-xjj6-f33q-929c,
    GHSA-2m5v-pch6-74hx, GHSA-qrxm-fqhp-jwvm, GHSA-4qv8-2p9j-2685, GHSA-9fm3-97px-rjf6). It is a lockfile-only change
    with a rebuilt packages/action/dist: no source, input or output change. CI and the release job now run
    pnpm audit --prod --audit-level=high after install, so a high or critical advisory in a production dependency fails
    the build before it can ship.

  • Unparseable or binary agent settings and MCP files are now errors whatever the policy lists (review A3-F2). A
    settings file that is too deep, not JSON, not an object or binary used to be only a warning unless the policy set a
    required deny list or hook list, and such an MCP file was always only a warning, so a pull request could pad
    .claude/settings.json or .mcp.json past the parser's depth limit, keep it valid for Claude Code and loosen the
    policy without a block. Now POLICY_SETTINGS_INVALID is an error and the verdict fails, and P09 blocks an edit to
    such a file. This is a deliberate tightening: a repository with an already broken settings or MCP file under a policy
    that reads it now fails where it warned (P02 is in force under every policy, so this applies to any policy
    document). No schema change and no new message code.

  • SECURITY.md and the Marketplace listing text no longer say agent setup files are never read (review A7-F1). The
    Data handling table now says check A7 reads the machine-readable agent config files at HEAD for committed secrets
    and policy mode reads the settings, MCP and instruction files, and a new row says where that text goes: it stays in
    the job and never reaches the comment, the summary, the log, report.json or the upload. Documentation only.

  • SECURITY.md's Data handling table now names the run upload to pharn.dev (review HIGH 5). It said the org policy
    request was the only thing sent to Pharn and that nothing else was; a new row lists the run upload's two POST
    requests to the app-url origin and their body fields, the PUT of report.json and meta.json to the signed
    storage URL, every meta.json field (including commentUrl), how report.json is redacted and capped, the one
    retry, that the first request goes out before pharn-ci knows whether the Pharn GitHub App is installed, and how to
    turn it off. The README's security section now says that table lists what goes to pharn.dev too. Documentation
    only.

  • A bug in pharn-ci's own code no longer reads as bad input (review finding 28). The test-file index, the JS and JSON
    test-config readers and the Python output mapper caught every exception and reported it as parser-threw,
    too-deep, syntax-error, invalid-json or bad-output, and the policy validator and indexPythonTestSources
    turned it into an invalid policy or python-failed; a fault in them now escapes and fails the run (Action step
    failed, CLI exit 2), while a file the parser genuinely throws on is refused as before. Only the third-party call
    (createSourceFile, JSON.parse) stays inside a try. The unused couldNotValidate message is removed.