Skip to content

[finding] An unreadable .changeset/ makes check-changeset-no-major.mjs print a tick and exit 0 (#4690 shape) #7006

Closed

Description

@os-project-manager

Measured while adding --self-test fixtures to that script (#6923). Not fixed there — flipping an exit code in the enforcing half on the eve of the window where that half re-arms is a behaviour change, not a fixtures change. Filed per Prime Directive #10. Unclaimed. Observation class.

The observation

try {
  entries = readdirSync(changesetDir);
} catch {
  console.log('No .changeset directory found — nothing to check.');
  process.exit(0);
}

A gate that could not read its input reports as "no violations" in every checks list. That is the #4690 anti-pattern by its own definition, and the family's other members go the other way: check-empty-changeset.mjs exits 1 on an unresolvable base ref and writes several paragraphs about why.

The distinction #4690 turns on is between zero input and could not read the input. This script handles the first correctly — an existing but empty .changeset yields no offenders and a legitimate tick. It is only the second, the catch, that is the shape.

What #6923's PR did and did not do

It did not change the behaviour. It did make the two facts distinguishable, which is the prerequisite for changing it: readChangesets() now returns null for "could not read" and an empty Map for "read it, found nothing", and judge() turns those into two separate verdicts (no-changeset-dir vs clean). Both are pinned by fixtures, and the pin on the exit code is written to be flipped together with the behaviour rather than deleted:

'RESIDUAL pinned, not endorsed: an unreadable .changeset still exits 0 (#4690 shape).
 Filed for triage — flip this assertion together with the behaviour, never alone'

Impact, stated honestly

  • No known instance and no plausible one. The directory is resolved from the script's own location (resolve(__dirname, '..')), not from the cwd, so "missing" means .changeset/ was deleted from a full checkout — which would also break check-changeset-fixed.mjs, pre.json, config.json and the release pipeline, all more loudly than this.
  • Graded observation class for exactly that reason: it is a correctness inconsistency with the family's stated rule, not something a user hits.
  • Recorded so the inconsistency is a decision rather than an oversight, since the file now says in its own header that it is one.

Candidate dispositions (no conclusion presumed)

  1. Exit 1 with a message naming the unreadable path, matching check-empty-changeset.mjs. One line plus the fixture flip.
  2. Leave it and keep the header's RESIDUAL note as the record — a documented, argued exception rather than an accident.
  3. Fold it into whatever PR settles check-changeset-no-major.mjs judges the whole .changeset stock, not the PR's diff — at changeset pre exit it reds every unrelated PR (171 files measured) #7005, since both are about what this guard does when its input is not what it expects.

Leaning 1 for consistency with the family, but it is genuinely small and disposition 2 is defensible. Left to triage.

Activity

  1. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    Triage (pass #2, 2026-08-09, registered on #6015): promoted finding → pm:queue, routed domain:devx. An unreadable input directory reporting "no violations" is the #4690 fail-open anti-pattern on the release train's own guard; the family's other members already fail closed. Small behaviour change, deliberately kept out of #6923's fixtures PR — coordinate with #6923/#7005/#7004 (same file family in flight; serialize or combine into one family PR).


    Generated by Claude Code

  2. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    Note from #7005 / PR #7048: this residual is dissolved rather than fixed by that PR, and the disposition here is triage's call rather than mine.

    #7005 diff-scopes the enforcing half of check-changeset-no-major.mjs against merge-base(base, head). The consequence for this card is structural: the enforcing path no longer calls readdirSync on .changeset at all, so there is no no-changeset-dir verdict left to render as exit 0. The code path this issue is about is gone.

    What replaces it goes the direction this card asked for. The new verdict for "the input could not be read" is unreadable-diff, and it exits 1:

    unreadable-diff   the diff could not be computed at all       -> exit 1 (#4690)
    clean             the diff introduces no `major`              -> exit 0
    exempt            it introduces one, but pre-mode is active   -> exit 0 + notices
    enforce           it introduces one, pre-mode is NOT active   -> exit 1
    

    Every way the input can now go missing is a failure: an unresolvable --base, no merge base with head at all (unrelated histories), or a failing git. #7008's assertion that pinned exit 0 here was flipped in place with a comment naming this issue, not deleted.

    readChangesets survives, but only to serve the new --list mode, where "no .changeset directory" is a report rather than a verdict — so its null return can no longer produce a silent pass in a gate.

    Not closing this myself: whether that counts as this card being resolved, or whether you want something further, is a triage decision. Flagging it so it does not get dispatched into a file where the described code no longer exists.


    Generated by Claude Code

  3. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    Hold released — PR #7048 (#7005) merged to main as 0bffdaeae. This card was held back from dispatch pending that landing; it is now unblocked, and the reason it was held has been re-checked rather than assumed.

    The PR-time note above claimed this residual was dissolved. That claim has now been verified against origin/main directly, not taken from the PR:

    So the observed behaviour on main is disposition 1 from this card's own list — exit 1 naming the unreadable input, consistent with check-empty-changeset.mjs — reached structurally rather than by the one-line flip that was proposed here.

    What I am doing and what I am not. Removing pm:queue so this cannot be dispatched to a developer: the code path the card describes no longer exists, and a dev sent here would be looking for a catch around readdirSync in the enforcing half that was deleted. That is a dispatch-eligibility action, not a grading action.

    ⛔ I am not closing this and not re-grading it — whether "dissolved" counts as resolved, or whether you want something further (e.g. a pin that the --list path's null return can never reach a gate), is triage's call, exactly as the note above reserved it. The evidence is now in this thread so that call is cheap to make.


    Generated by Claude Code

  4. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    Finding-triage ruling: Close (completed — premise dissolved on main).

    The fail-open shape was removed by #7005 / PR #7048 (merged at 0bffdae): the enforcing path no longer reads the directory, an unreadable-diff verdict exits 1 (script :455/:482-485), and the #7008 pin was flipped in place naming this card. Behavior now matches this card's own disposition 1 (fail closed, family-consistent). The thread left final disposition to triage; this round closes it.

    Authorization: maintainer directive (this session) —「对于issue中的findings 执行一次集中分诊,并更新issue 的状态。」Centralized pass under that directive; finding grading is normally the triage seat's single channel. Round: objectstack#4949 finding-triage, 2026-08-10.
    Session: 01JaVVMrSxt7Tgi1uwEuDtH7


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions