Skip to content

Commit a2464cc

Browse files
committed
fix(skilldoc): make card check EOL-insensitive (Windows CI) + satisfy errcheck
Two CI failures on PR #56, both real: - build (windows-latest): `skilldoc check` reported all 19 cards stale. Root cause: the fence freshness check compares the card's fence block byte-for-byte against a generated (LF) render, but Windows git checks the cards out with CRLF, so every comparison mismatched. Fix the root cause — normalize CRLF→LF on read (the generated fence is canonical LF) so the tool is insensitive to checkout settings — and add a .gitattributes pinning the cards to LF so the committed files stay canonical. ubuntu/macos were already green. - lint (golangci-lint / errcheck): two unchecked fmt.Fprint* return values in main.go (the 'cards OK' line and the per-issue print loop). Propagate both errors. New regression test TestRunCheck_CRLFCardIsClean. Local: go test green, golangci-lint 0 issues, check-cards OK.
1 parent af922ce commit a2464cc

3 files changed

Lines changed: 40 additions & 5 deletions

File tree

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Skill command-cards are compared byte-for-byte against a generated (LF) fence,
2+
# so they must stay LF on every platform regardless of git's autocrlf setting.
3+
# (The skilldoc tool also normalizes EOL on read; this keeps the committed files
4+
# canonical so Windows checkouts don't materialize them as CRLF.)
5+
skills/flashduty/**/*.md text eol=lf

internal/cmd/skilldoc/main.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ func checkCmd() *cobra.Command {
7272
if n > 0 {
7373
return fmt.Errorf("%d card issue(s) found", n)
7474
}
75-
fmt.Fprintln(cmd.OutOrStdout(), "skilldoc: cards OK")
76-
return nil
75+
_, err = fmt.Fprintln(cmd.OutOrStdout(), "skilldoc: cards OK")
76+
return err
7777
},
7878
}
7979
}
@@ -90,7 +90,7 @@ func runGen(d skilldoc.Dump, base, group string) error {
9090
if err != nil {
9191
return fmt.Errorf("read card: %w", err)
9292
}
93-
body := string(raw)
93+
body := normalizeEOL(string(raw))
9494

9595
start, end := skilldoc.FenceStart(group), skilldoc.FenceEnd(group)
9696
si := strings.Index(body, start)
@@ -156,7 +156,9 @@ func runCheck(d skilldoc.Dump, base string, w io.Writer) (int, error) {
156156
return issues[i].Kind < issues[j].Kind
157157
})
158158
for _, is := range issues {
159-
fmt.Fprintf(w, "%s:%d %s %s\n", is.Doc, is.Line, is.Kind, is.Detail)
159+
if _, err := fmt.Fprintf(w, "%s:%d %s %s\n", is.Doc, is.Line, is.Kind, is.Detail); err != nil {
160+
return 0, err
161+
}
160162
}
161163
return len(issues), nil
162164
}
@@ -191,7 +193,7 @@ func loadDocs(base string) ([]skilldoc.Doc, error) {
191193
if err != nil {
192194
rel = path
193195
}
194-
docs = append(docs, skilldoc.Doc{Path: rel, Body: string(raw)})
196+
docs = append(docs, skilldoc.Doc{Path: rel, Body: normalizeEOL(string(raw))})
195197
return nil
196198
})
197199
if err != nil {
@@ -200,6 +202,12 @@ func loadDocs(base string) ([]skilldoc.Doc, error) {
200202
return docs, nil
201203
}
202204

205+
// normalizeEOL collapses Windows CRLF to LF so the byte-exact fence comparison
206+
// and the line-based harvester are insensitive to how git checked the cards out
207+
// (Windows autocrlf would otherwise make every fence look stale). The generated
208+
// fence is always LF, so LF is the canonical form to compare against.
209+
func normalizeEOL(s string) string { return strings.ReplaceAll(s, "\r\n", "\n") }
210+
203211
// cardBase resolves <repoRoot>/skills/flashduty by walking up from the cwd to
204212
// the directory containing go.mod.
205213
func cardBase() (string, error) {

internal/cmd/skilldoc/main_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,28 @@ func TestRunCheck_CleanDirIsZero(t *testing.T) {
8787
}
8888
}
8989

90+
// A card checked out with Windows CRLF line endings must still validate clean:
91+
// the fence comparison and harvester normalize EOL, so freshness does not depend
92+
// on how git materialized the file (regression test for the Windows CI failure).
93+
func TestRunCheck_CRLFCardIsClean(t *testing.T) {
94+
dir := t.TempDir()
95+
d := fixtureDump()
96+
body := "# status-page\n\n" +
97+
"```bash\nfduty status-page change-create --type incident\n```\n\n" +
98+
skilldoc.GenerateFence(d, "status-page") + "\n"
99+
crlf := strings.ReplaceAll(body, "\n", "\r\n")
100+
writeFile(t, filepath.Join(dir, "reference", "status-page.md"), crlf)
101+
102+
var out bytes.Buffer
103+
n, err := runCheck(d, dir, &out)
104+
if err != nil {
105+
t.Fatalf("runCheck: %v", err)
106+
}
107+
if n != 0 {
108+
t.Errorf("CRLF card should validate clean, got %d issue(s):\n%s", n, out.String())
109+
}
110+
}
111+
90112
func TestRunCheck_MissingDirIsZero(t *testing.T) {
91113
d := fixtureDump()
92114
var out bytes.Buffer

0 commit comments

Comments
 (0)