Skip to content

No .gitattributes, so a Windows or WSL clone gets CRLF and a byte-exact doc test blames the action catalogue #342

Description

@vladimirrott

Re-measured 2026-09-07 at adab560, and half of this issue is done. The
committed CRLF artifact was renormalised by #361 on 3 September, before you
were pointed here. Nothing tracked is CRLF or mixed today:

$ git ls-files --eol | grep -E '^i/(crlf|mixed)'; echo "grep rc=$?"
grep rc=1
$ grep -c $'\r' tests/evidence/workspace-tests.json
0

What is left is the prevention half, which nobody has touched: the missing
.gitattributes, the guard, and the CONTRIBUTING.md paragraph. The trap
named under "Tests first" has inverted.
The guard now passes on its first
run, so a contributor who adds it and sees green has learned nothing about
whether it works. Prove it bites before you believe it.

Three line references below have moved and one figure is gone:

$ grep -n 'json.dumps' scripts/record_test_baseline.py
160:        json.dumps(doc, indent=2, sort_keys=True) + "\n", encoding="utf-8"
$ sed -n "154p;156p" crates/sysknife-daemon/tests/action_reference_doc.rs
    }
    let committed = std::fs::read_to_string(&path).unwrap_or_default();
$ wc -c tests/evidence/workspace-tests.json
193 tests/evidence/workspace-tests.json

So record_test_baseline.py:65 is now 159-160 (#393 moved it),
action_reference_doc.rs:154 is 162 (156 on 2026-09-07; #384 moved it again on 2026-09-09), and the "450 bytes of LF over 467 bytes
of CRLF" sentence describes a file that #393 replaced with a 193-byte schema.
The reasoning around each of them still holds; only the numbers moved.

The repository has no .gitattributes, and core.autocrlf is whatever the contributor's machine set. A Windows or WSL clone with autocrlf=true therefore gets CRLF in the working tree, and a commit back from that clone puts CRLF into the index. Nothing in the repo prevents either half, and nothing tells the contributor it happened.

Both halves have now happened. At 60af0ad:

$ git ls-files --eol | grep -E '^i/(crlf|mixed)'
i/crlf  w/crlf  attr/    tests/evidence/workspace-tests.json

That file arrived CRLF in 60af0ad, and it is LF in every earlier commit that touched it:

$ for c in 60af0ad 0640818 5867807 5a9450a; do
    echo "$c $(git show $c:tests/evidence/workspace-tests.json | grep -c $'\r')"
  done
60af0ad 17
0640818 0
5867807 0
5a9450a 0

scripts/record_test_baseline.py:65 writes the file with json.dumps(...) + "\n", so the generator never produces those bytes. They came from somebody's checkout.

Why it matters

It cost two contributors a day, in the same 24 hours.

crates/sysknife-daemon/tests/action_reference_doc.rs:154 compares the file on disk against a generated string byte for byte:

let committed = std::fs::read_to_string(&path).unwrap_or_default();
assert_eq!(committed, generated, "docs/action-reference.md is out of date ...");

The generator emits \n. A CRLF working tree fails on line endings alone, and the message blames the action catalogue. #341 reported it as a pre-existing failure on clean main, and it passes on Linux:

PASS [ 0.017s] sysknife-daemon::action_reference_doc action_reference_doc_is_current

The same PR carries three sed -i 's/\r$//' calls against files it copies out of the repo, which is the same symptom being worked around one file at a time.

Neither contributor did anything wrong. The repo let a mangled checkout look like a broken catalogue.

The committed artifact costs something separate, and quietly. scripts/check_evidence_claims.py still passes, because a carriage return is legal JSON whitespace:

$ python3 scripts/check_evidence_claims.py
Published figures match the evidence artifacts.

So no gate goes red. The next contributor who runs UPDATE_TEST_BASELINE=1 scripts/test_baseline.sh gets a whole-file diff instead of a one-line one, because the generator writes 450 bytes of LF over 467 bytes of CRLF.

Scope

  • Renormalise tests/evidence/workspace-tests.json to LF. Done in fix(evidence): restore LF line endings in workspace-tests.json #361 on 3 September; there is nothing left to renormalise.
  • Add .gitattributes with * text=auto eol=lf, plus -text for the binary paths already marked as such (icons, .ico, .png, cassette fixtures if any are binary).
  • Mark the executable shell scripts explicitly, since a CRLF shebang line makes bash report bad interpreter: /usr/bin/env bash^M, which is the next thing this bites.
  • Say in CONTRIBUTING.md what to run after cloning on Windows or WSL: git config core.autocrlf false, then git rm --cached -r . && git reset --hard to renormalise an already-mangled tree.

Tests first

git ls-files --eol is the assertion. A guard in tests/release/ that fails when any tracked file is i/crlf or i/mixed pins it, and it costs one git call:

git ls-files --eol | grep -E '^i/(crlf|mixed)' && exit 1

The trap in this issue has inverted since it was written. That grep matched tests/evidence/workspace-tests.json when this was filed; #361 renormalised the file, so the guard now goes green the moment you add it. A guard that has only ever been observed passing is the defect this tracker keeps meeting. Commit a CRLF file on a scratch branch, watch the guard go red, drop the branch, and say in the PR that you did.

Prove it bites by committing a CRLF file in a scratch branch, not by trusting the grep.

Difficulty

easy. The whole change is one new file, one docs paragraph and one guard.

Getting started

CONTRIBUTING.md has the build and test commands. No CLA and no copyright waiver. The project is MIT.

Activity

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

Metadata

Metadata

Assignees

Labels

claimedSomeone has said in the thread that they are working on thiseasyDifficulty: self-contained, no deep context neededenhancementNew feature or requestgood first issueGood for newcomers

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions