ci: require docs lint when docs change (#25608)

Move docs linting into the required CI umbrella and reuse the existing
`changes` job so docs lint runs when docs or CI files change, plus on
`main` as a backstop.

This is motivated by the docs lint failures on #25601. That PR touched
`.claude/docs/TESTING.md`; the standalone `Docs CI` workflow picked it
up because `docs-ci.yaml` used broad `**.md` matching, but local `pnpm
lint-docs` and `make lint` did not catch the same file because they only
scanned `docs/**` plus root `*.md`. The first failed Docs CI run
reported markdownlint errors in `.claude/docs/TESTING.md` (`MD040` and
`MD031`), and the next run reported a markdown table formatter failure
in the same file.

That mismatch is why this PR exists: prevent unrelated PRs from being
surprised by stale `.claude/docs/**` lint drift only after they happen
to touch one of those files. The local docs scripts now include
`.claude/docs/**`, and the old standalone `Docs CI` workflow is removed
so we do not maintain separate path-filter logic outside the required CI
workflow.

> Generated by mux, but reviewed by a human
This commit is contained in:
Ethan
2026-05-27 12:30:05 +10:00
committed by GitHub
parent 20b50dd4b8
commit e99f7171e4
6 changed files with 115 additions and 160 deletions
+17 -17
View File
@@ -8,14 +8,14 @@ not add new readiness or debug endpoints for these workflows.
`scripts/develop/main.go` defines these base defaults:
| Resource | Base default | Override |
|----------|--------------|----------|
| API server | `3000` | `--port`, `CODER_DEV_PORT` |
| Frontend dev server | `8080` | `--web-port`, `CODER_DEV_WEB_PORT` |
| Workspace proxy | `3010` | `--proxy-port`, `CODER_DEV_PROXY_PORT` |
| Coder Prometheus metrics | `2114` | `--prometheus-port`, `CODER_DEV_PROMETHEUS_PORT` |
| Embedded Prometheus UI | `9090` | Fixed in `scripts/develop/main.go` |
| Delve debugger | `12345` | Fixed when `--debug` is used |
| Resource | Base default | Override |
|--------------------------|--------------|--------------------------------------------------|
| API server | `3000` | `--port`, `CODER_DEV_PORT` |
| Frontend dev server | `8080` | `--web-port`, `CODER_DEV_WEB_PORT` |
| Workspace proxy | `3010` | `--proxy-port`, `CODER_DEV_PROXY_PORT` |
| Coder Prometheus metrics | `2114` | `--prometheus-port`, `CODER_DEV_PROMETHEUS_PORT` |
| Embedded Prometheus UI | `9090` | Fixed in `scripts/develop/main.go` |
| Delve debugger | `12345` | Fixed when `--debug` is used |
By default, plain `./scripts/develop.sh` uses the base defaults exactly:
`3000`, `8080`, `3010`, and `2114` for Coder Prometheus metrics. Set
@@ -37,15 +37,15 @@ Linux. The Prometheus UI port `9090` and Delve port `12345` remain hardcoded.
The develop script also supports these existing flags and environment
variables:
| Purpose | Flag | Environment variable |
|---------|------|----------------------|
| Per-worktree port offset | `--port-offset` | `CODER_DEV_PORT_OFFSET` |
| Access URL | `--access-url` | `CODER_DEV_ACCESS_URL` |
| Admin password | `--password` | `CODER_DEV_ADMIN_PASSWORD` |
| Starter template | `--starter-template` | `CODER_DEV_STARTER_TEMPLATE` |
| Roll back missing migrations | `--db-rollback` | `CODER_DEV_DB_ROLLBACK` |
| Reset the development database | `--db-reset` | `CODER_DEV_DB_RESET` |
| Accept changed migration tracking | `--db-continue` | `CODER_DEV_DB_CONTINUE` |
| Purpose | Flag | Environment variable |
|-----------------------------------|----------------------|------------------------------|
| Per-worktree port offset | `--port-offset` | `CODER_DEV_PORT_OFFSET` |
| Access URL | `--access-url` | `CODER_DEV_ACCESS_URL` |
| Admin password | `--password` | `CODER_DEV_ADMIN_PASSWORD` |
| Starter template | `--starter-template` | `CODER_DEV_STARTER_TEMPLATE` |
| Roll back missing migrations | `--db-rollback` | `CODER_DEV_DB_ROLLBACK` |
| Reset the development database | `--db-reset` | `CODER_DEV_DB_RESET` |
| Accept changed migration tracking | `--db-continue` | `CODER_DEV_DB_CONTINUE` |
Extra `coder server` flags can be passed after `--`. For example,
`./scripts/develop.sh -- --trace` passes `--trace` to the API server.
+57 -57
View File
@@ -92,69 +92,69 @@ The left column reflects common patterns from pre-1.22 Go. Write the
right column instead. The "Since" column tells you the minimum `go`
directive version required in `go.mod`.
| Old pattern | Modern replacement | Since |
|---|---|---|
| `interface{}` | `any` | 1.18 |
| `v := v` inside loops | remove it | 1.22 |
| `for i := 0; i < n; i++` | `for i := range n` | 1.22 |
| `for i := 0; i < b.N; i++` (benchmarks) | `for b.Loop()` (correct timing, future-proof) | 1.24 |
| `sort.Slice(s, func(i,j int) bool{…})` | `slices.SortFunc(s, cmpFn)` | 1.21 |
| `wg.Add(1); go func(){ defer wg.Done(); … }()` | `wg.Go(func(){…})` | 1.25 |
| `func ptr[T any](v T) *T { return &v }` | `new(expr)` e.g. `new(time.Now())` | 1.26 |
| `var target *E; errors.As(err, &target)` | `t, ok := errors.AsType[*E](err)` | 1.26 |
| Custom multi-error type | `errors.Join(err1, err2, …)` | 1.20 |
| Single `%w` for multiple causes | `fmt.Errorf("…: %w, %w", e1, e2)` | 1.20 |
| `rand.Seed(time.Now().UnixNano())` | delete it (auto-seeded); prefer `math/rand/v2` | 1.20/1.22 |
| `sync.Once` + captured variable | `sync.OnceValue(func() T {…})` / `OnceValues` | 1.21 |
| Custom `min`/`max` helpers | `min(a, b)` / `max(a, b)` builtins (any ordered type) | 1.21 |
| `for k := range m { delete(m, k) }` | `clear(m)` (also zeroes slices) | 1.21 |
| Index+slice or `SplitN(s, sep, 2)` | `strings.Cut(s, sep)` / `bytes.Cut` | 1.18 |
| `TrimPrefix` + check if anything was trimmed | `strings.CutPrefix` / `CutSuffix` (returns ok bool) | 1.20 |
| `strings.Split` + loop when no slice is needed | `strings.SplitSeq` / `Lines` / `FieldsSeq` (iterator, no alloc) | 1.24 |
| `"2006-01-02"` / `"2006-01-02 15:04:05"` / `"15:04:05"` | `time.DateOnly` / `time.DateTime` / `time.TimeOnly` | 1.20 |
| Manual `Before`/`After`/`Equal` chains for comparison | `time.Time.Compare` (returns -1/0/+1; works with `slices.SortFunc`) | 1.20 |
| Loop collecting map keys into slice | `slices.Sorted(maps.Keys(m))` | 1.23 |
| `fmt.Sprintf` + append to `[]byte` | `fmt.Appendf(buf, …)` (also `Append`, `Appendln`) | 1.18 |
| `reflect.TypeOf((*T)(nil)).Elem()` | `reflect.TypeFor[T]()` | 1.22 |
| `*(*[4]byte)(slice)` unsafe cast | `[4]byte(slice)` direct conversion | 1.20 |
| `atomic.LoadInt64` / `AddInt64` / `StoreInt64` etc. | `atomic.Int64` (also `Int32`, `Uint32`, `Uint64`, `Bool`, `Pointer[T]`) | 1.19 |
| `crypto/rand.Read(buf)` + hex/base64 encode | `crypto/rand.Text()` (one call) | 1.24 |
| Checking `crypto/rand.Read` error | don't: return is always nil | 1.24 |
| `time.Sleep` in tests | `testing/synctest` (deterministic fake clock) | 1.24/1.25 |
| `json:",omitempty"` on zero-value structs like `time.Time{}` | `json:",omitzero"` (uses `IsZero()` method) | 1.24 |
| `strings.Title` | `golang.org/x/text/cases` | 1.18 |
| `net.IP` in new code | `net/netip.Addr` (immutable, comparable, lighter) | 1.18 |
| `tools.go` with blank imports | `tool` directive in `go.mod` | 1.24 |
| `runtime.SetFinalizer` | `runtime.AddCleanup` (multiple per object, no pointer cycles) | 1.24 |
| `httputil.ReverseProxy.Director` | `.Rewrite` hook + `ProxyRequest` (Director deprecated in 1.26) | 1.20 |
| `sql.NullString`, `sql.NullInt64`, etc. | `sql.Null[T]` | 1.22 |
| Manual `ctx, cancel := context.WithCancel(…)` + `t.Cleanup(cancel)` | `t.Context()` (auto-canceled when test ends) | 1.24 |
| `if d < 0 { d = -d }` on durations | `d.Abs()` (handles `math.MinInt64`) | 1.19 |
| Implement only `TextMarshaler` | also implement `TextAppender` for alloc-free marshaling | 1.24 |
| Custom `Unwrap() error` on multi-cause errors | `Unwrap() []error` (slice form; required for tree traversal) | 1.20 |
| Old pattern | Modern replacement | Since |
|---------------------------------------------------------------------|-------------------------------------------------------------------------|-----------|
| `interface{}` | `any` | 1.18 |
| `v := v` inside loops | remove it | 1.22 |
| `for i := 0; i < n; i++` | `for i := range n` | 1.22 |
| `for i := 0; i < b.N; i++` (benchmarks) | `for b.Loop()` (correct timing, future-proof) | 1.24 |
| `sort.Slice(s, func(i,j int) bool{…})` | `slices.SortFunc(s, cmpFn)` | 1.21 |
| `wg.Add(1); go func(){ defer wg.Done(); … }()` | `wg.Go(func(){…})` | 1.25 |
| `func ptr[T any](v T) *T { return &v }` | `new(expr)` e.g. `new(time.Now())` | 1.26 |
| `var target *E; errors.As(err, &target)` | `t, ok := errors.AsType[*E](err)` | 1.26 |
| Custom multi-error type | `errors.Join(err1, err2, …)` | 1.20 |
| Single `%w` for multiple causes | `fmt.Errorf("…: %w, %w", e1, e2)` | 1.20 |
| `rand.Seed(time.Now().UnixNano())` | delete it (auto-seeded); prefer `math/rand/v2` | 1.20/1.22 |
| `sync.Once` + captured variable | `sync.OnceValue(func() T {…})` / `OnceValues` | 1.21 |
| Custom `min`/`max` helpers | `min(a, b)` / `max(a, b)` builtins (any ordered type) | 1.21 |
| `for k := range m { delete(m, k) }` | `clear(m)` (also zeroes slices) | 1.21 |
| Index+slice or `SplitN(s, sep, 2)` | `strings.Cut(s, sep)` / `bytes.Cut` | 1.18 |
| `TrimPrefix` + check if anything was trimmed | `strings.CutPrefix` / `CutSuffix` (returns ok bool) | 1.20 |
| `strings.Split` + loop when no slice is needed | `strings.SplitSeq` / `Lines` / `FieldsSeq` (iterator, no alloc) | 1.24 |
| `"2006-01-02"` / `"2006-01-02 15:04:05"` / `"15:04:05"` | `time.DateOnly` / `time.DateTime` / `time.TimeOnly` | 1.20 |
| Manual `Before`/`After`/`Equal` chains for comparison | `time.Time.Compare` (returns -1/0/+1; works with `slices.SortFunc`) | 1.20 |
| Loop collecting map keys into slice | `slices.Sorted(maps.Keys(m))` | 1.23 |
| `fmt.Sprintf` + append to `[]byte` | `fmt.Appendf(buf, …)` (also `Append`, `Appendln`) | 1.18 |
| `reflect.TypeOf((*T)(nil)).Elem()` | `reflect.TypeFor[T]()` | 1.22 |
| `*(*[4]byte)(slice)` unsafe cast | `[4]byte(slice)` direct conversion | 1.20 |
| `atomic.LoadInt64` / `AddInt64` / `StoreInt64` etc. | `atomic.Int64` (also `Int32`, `Uint32`, `Uint64`, `Bool`, `Pointer[T]`) | 1.19 |
| `crypto/rand.Read(buf)` + hex/base64 encode | `crypto/rand.Text()` (one call) | 1.24 |
| Checking `crypto/rand.Read` error | don't: return is always nil | 1.24 |
| `time.Sleep` in tests | `testing/synctest` (deterministic fake clock) | 1.24/1.25 |
| `json:",omitempty"` on zero-value structs like `time.Time{}` | `json:",omitzero"` (uses `IsZero()` method) | 1.24 |
| `strings.Title` | `golang.org/x/text/cases` | 1.18 |
| `net.IP` in new code | `net/netip.Addr` (immutable, comparable, lighter) | 1.18 |
| `tools.go` with blank imports | `tool` directive in `go.mod` | 1.24 |
| `runtime.SetFinalizer` | `runtime.AddCleanup` (multiple per object, no pointer cycles) | 1.24 |
| `httputil.ReverseProxy.Director` | `.Rewrite` hook + `ProxyRequest` (Director deprecated in 1.26) | 1.20 |
| `sql.NullString`, `sql.NullInt64`, etc. | `sql.Null[T]` | 1.22 |
| Manual `ctx, cancel := context.WithCancel(…)` + `t.Cleanup(cancel)` | `t.Context()` (auto-canceled when test ends) | 1.24 |
| `if d < 0 { d = -d }` on durations | `d.Abs()` (handles `math.MinInt64`) | 1.19 |
| Implement only `TextMarshaler` | also implement `TextAppender` for alloc-free marshaling | 1.24 |
| Custom `Unwrap() error` on multi-cause errors | `Unwrap() []error` (slice form; required for tree traversal) | 1.20 |
## New capabilities
These enable things that weren't practical before. Reach for them in the
described situations.
| What | Since | When to use it |
|---|---|---|
| `cmp.Or(a, b, c)` | 1.22 | Defaults/fallback chains: returns first non-zero value. Replaces verbose `if a != "" { return a }` cascades. |
| `context.WithoutCancel(ctx)` | 1.21 | Background work that must outlive the request (e.g. async cleanup after HTTP response). Derived context keeps parent's values but ignores cancellation. |
| `context.AfterFunc(ctx, fn)` | 1.21 | Register cleanup that fires on context cancellation without spawning a goroutine that blocks on `<-ctx.Done()`. |
| `context.WithCancelCause` / `Cause` | 1.20 | When callers need to know WHY a context was canceled, not just that it was. Retrieve cause with `context.Cause(ctx)`. |
| `context.WithDeadlineCause` / `WithTimeoutCause` | 1.21 | Attach a domain-specific error to deadline/timeout expiry (e.g. distinguish "DB query timed out" from "HTTP request timed out"). |
| `errors.ErrUnsupported` | 1.21 | Standard sentinel for "not supported." Use instead of per-package custom sentinels. Check with `errors.Is`. |
| `http.ResponseController` | 1.20 | Per-request flush, hijack, and deadline control without type-asserting `ResponseWriter` to `http.Flusher` or `http.Hijacker`. |
| Enhanced `ServeMux` routing | 1.22 | `"GET /items/{id}"` patterns in `http.ServeMux`. Access with `r.PathValue("id")`. Wildcards: `{name}`, catch-all: `{path...}`, exact: `{$}`. Eliminates many third-party router dependencies. |
| `os.Root` / `OpenRoot` | 1.24 | Confined directory access that prevents symlink escape. 1.25 adds `MkdirAll`, `ReadFile`, `WriteFile` for real use. |
| `os.CopyFS` | 1.23 | Copy an entire `fs.FS` to local filesystem in one call. |
| `os/signal.NotifyContext` with cause | 1.26 | Cancellation cause identifies which signal (SIGTERM vs SIGINT) triggered shutdown. |
| `io/fs.SkipAll` / `filepath.SkipAll` | 1.20 | Return from `WalkDir` callback to stop walking entirely. Cleaner than a sentinel error. |
| `GOMEMLIMIT` env / `debug.SetMemoryLimit` | 1.19 | Soft memory limit for GC. Use alongside or instead of `GOGC` in memory-constrained containers. |
| `net/url.JoinPath` | 1.19 | Join URL path segments correctly. Replaces error-prone string concatenation. |
| `go test -skip` | 1.20 | Skip tests matching a pattern. Useful when running a subset of a large test suite. |
| What | Since | When to use it |
|--------------------------------------------------|-------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `cmp.Or(a, b, c)` | 1.22 | Defaults/fallback chains: returns first non-zero value. Replaces verbose `if a != "" { return a }` cascades. |
| `context.WithoutCancel(ctx)` | 1.21 | Background work that must outlive the request (e.g. async cleanup after HTTP response). Derived context keeps parent's values but ignores cancellation. |
| `context.AfterFunc(ctx, fn)` | 1.21 | Register cleanup that fires on context cancellation without spawning a goroutine that blocks on `<-ctx.Done()`. |
| `context.WithCancelCause` / `Cause` | 1.20 | When callers need to know WHY a context was canceled, not just that it was. Retrieve cause with `context.Cause(ctx)`. |
| `context.WithDeadlineCause` / `WithTimeoutCause` | 1.21 | Attach a domain-specific error to deadline/timeout expiry (e.g. distinguish "DB query timed out" from "HTTP request timed out"). |
| `errors.ErrUnsupported` | 1.21 | Standard sentinel for "not supported." Use instead of per-package custom sentinels. Check with `errors.Is`. |
| `http.ResponseController` | 1.20 | Per-request flush, hijack, and deadline control without type-asserting `ResponseWriter` to `http.Flusher` or `http.Hijacker`. |
| Enhanced `ServeMux` routing | 1.22 | `"GET /items/{id}"` patterns in `http.ServeMux`. Access with `r.PathValue("id")`. Wildcards: `{name}`, catch-all: `{path...}`, exact: `{$}`. Eliminates many third-party router dependencies. |
| `os.Root` / `OpenRoot` | 1.24 | Confined directory access that prevents symlink escape. 1.25 adds `MkdirAll`, `ReadFile`, `WriteFile` for real use. |
| `os.CopyFS` | 1.23 | Copy an entire `fs.FS` to local filesystem in one call. |
| `os/signal.NotifyContext` with cause | 1.26 | Cancellation cause identifies which signal (SIGTERM vs SIGINT) triggered shutdown. |
| `io/fs.SkipAll` / `filepath.SkipAll` | 1.20 | Return from `WalkDir` callback to stop walking entirely. Cleaner than a sentinel error. |
| `GOMEMLIMIT` env / `debug.SetMemoryLimit` | 1.19 | Soft memory limit for GC. Use alongside or instead of `GOGC` in memory-constrained containers. |
| `net/url.JoinPath` | 1.19 | Join URL path segments correctly. Replaces error-prone string concatenation. |
| `go test -skip` | 1.20 | Skip tests matching a pattern. Useful when running a subset of a large test suite. |
## Key packages
+12 -12
View File
@@ -23,48 +23,48 @@
### Testing Issues
3. **"package should be X_test"**
1. **"package should be X_test"**
- **Solution**: Use `package_test` naming for test files
- Example: `identityprovider_test` for black-box testing
4. **Race conditions in tests**
2. **Race conditions in tests**
- **Solution**: Use unique identifiers instead of hardcoded names
- Example: `fmt.Sprintf("test-client-%s-%d", t.Name(), time.Now().UnixNano())`
- Never use hardcoded names in concurrent tests
5. **Missing newlines**
3. **Missing newlines**
- **Solution**: Ensure files end with newline character
- Most editors can be configured to add this automatically
### OAuth2 Issues
6. **OAuth2 endpoints returning wrong error format**
1. **OAuth2 endpoints returning wrong error format**
- **Solution**: Ensure OAuth2 endpoints return RFC 6749 compliant errors
- Use standard error codes: `invalid_client`, `invalid_grant`, `invalid_request`
- Format: `{"error": "code", "error_description": "details"}`
7. **Resource indicator validation failing**
2. **Resource indicator validation failing**
- **Solution**: Ensure database stores and retrieves resource parameters correctly
- Check both authorization code storage and token exchange handling
8. **PKCE tests failing**
3. **PKCE tests failing**
- **Solution**: Verify both authorization code storage and token exchange handle PKCE fields
- Check `CodeChallenge` and `CodeChallengeMethod` field handling
### RFC Compliance Issues
9. **RFC compliance failures**
1. **RFC compliance failures**
- **Solution**: Verify against actual RFC specifications, not assumptions
- Use WebFetch tool to get current RFC content for compliance verification
- Read the actual RFC specifications before implementation
10. **Default value mismatches**
2. **Default value mismatches**
- **Solution**: Ensure database migrations match application code defaults
- Example: RFC 7591 specifies `client_secret_basic` as default, not `client_secret_post`
### Authorization Issues
11. **Authorization context errors in public endpoints**
1. **Authorization context errors in public endpoints**
- **Solution**: Use `dbauthz.AsSystemRestricted(ctx)` pattern
- Example:
@@ -75,17 +75,17 @@
### Authentication Issues
12. **Bearer token authentication issues**
1. **Bearer token authentication issues**
- **Solution**: Check token extraction precedence and format validation
- Ensure proper RFC 6750 Bearer Token Support implementation
13. **URI validation failures**
2. **URI validation failures**
- **Solution**: Support both standard schemes and custom schemes per protocol requirements
- Native OAuth2 apps may use custom schemes
### General Development Issues
14. **Log message formatting errors**
1. **Log message formatting errors**
- **Solution**: Use lowercase, descriptive messages without special characters
- Follow Go logging conventions
+26 -1
View File
@@ -53,7 +53,8 @@ jobs:
- "**"
docs:
- "docs/**"
- "README.md"
- ".claude/docs/**"
- "*.md"
- "examples/web-server/**"
- "examples/monitoring/**"
- "examples/lima/**"
@@ -120,6 +121,28 @@ jobs:
env:
FILTER_JSON: ${{ toJSON(steps.filter.outputs) }}
lint-docs:
needs: changes
if: needs.changes.outputs.docs == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
persist-credentials: false
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Check docs
run: pnpm check-docs
# Disabled due to instability. See: https://github.com/coder/coder/issues/14553
# Re-enable once the flake hash calculation is stable.
# update-flake:
@@ -1132,6 +1155,7 @@ jobs:
- changes
- fmt
- lint
- lint-docs
- lint-actions
- gen
- test-go-pg
@@ -1157,6 +1181,7 @@ jobs:
echo "- changes: ${{ needs.changes.result }}"
echo "- fmt: ${{ needs.fmt.result }}"
echo "- lint: ${{ needs.lint.result }}"
echo "- lint-docs: ${{ needs.lint-docs.result }}"
echo "- lint-actions: ${{ needs.lint-actions.result }}"
echo "- gen: ${{ needs.gen.result }}"
echo "- test-go-pg: ${{ needs.test-go-pg.result }}"
-71
View File
@@ -1,71 +0,0 @@
name: Docs CI
on:
push:
branches:
- main
# Self-reference removed from both push and pull_request: the `lint`
# and `fmt` steps gate on `tj-actions/changed-files` matching
# `docs/**` or `**.md`, so a workflow-only edit produced an empty
# run. `actionlint` and `make lint/actions` catch YAML problems
# before merge regardless. See DOCS-129.
paths:
- "docs/**"
- "**.md"
pull_request:
# Self-reference removed; see comment under `push:` above.
paths:
- "docs/**"
- "**.md"
permissions:
contents: read
jobs:
docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node
uses: ./.github/actions/setup-node
# Per-tool changed-files filters. Each tool gets its own `changed-*`
# step scoped to the files it processes, keeping workflow-level `paths:`
# broad. Adding a tool (e.g. image linter) only needs a new step pair.
- uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v45.0.7
id: changed-md
with:
files: |
**.md
separator: ","
# Both downstream tools take file paths as argv. `tj-actions/changed-files`
# joins paths with `separator: ","`, which the shell does not split on, so
# run the output through `tr ',' '\n' | xargs -d '\n'` to hand each path to
# the tool as a distinct argument. This tolerates filenames containing
# spaces and prevents silent fallbacks: `markdownlint-cli2` would treat a
# comma-joined string as a single non-matching glob, and
# `markdown-table-formatter` would fall back to scanning every `.md` in
# the working tree when invoked with no positional args.
#
# `printf '%s\n'` is used instead of `echo` so a hypothetical leading
# `-e` or `-n` in a path is treated as data, not a bash builtin flag.
- name: lint
if: steps.changed-md.outputs.any_changed == 'true'
run: |
printf '%s\n' "$ALL_CHANGED_FILES" | tr ',' '\n' | xargs -d '\n' pnpm exec markdownlint-cli2
env:
ALL_CHANGED_FILES: ${{ steps.changed-md.outputs.all_changed_files }}
- name: fmt
if: steps.changed-md.outputs.any_changed == 'true'
run: |
printf '%s\n' "$ALL_CHANGED_FILES" | tr ',' '\n' | xargs -d '\n' pnpm exec markdown-table-formatter --check
env:
ALL_CHANGED_FILES: ${{ steps.changed-md.outputs.all_changed_files }}
+3 -2
View File
@@ -4,8 +4,9 @@
"version": "0.0.0",
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
"scripts": {
"format-docs": "markdown-table-formatter $(find docs -name '*.md') *.md",
"lint-docs": "markdownlint-cli2 --fix $(find docs -name '*.md') *.md",
"format-docs": "markdown-table-formatter $(find docs .claude/docs examples/web-server examples/monitoring examples/lima -name '*.md' 2>/dev/null) *.md",
"lint-docs": "markdownlint-cli2 --fix $(find docs .claude/docs examples/web-server examples/monitoring examples/lima -name '*.md' 2>/dev/null) *.md",
"check-docs": "markdownlint-cli2 $(find docs .claude/docs examples/web-server examples/monitoring examples/lima -name '*.md' 2>/dev/null) *.md && markdown-table-formatter --check $(find docs .claude/docs examples/web-server examples/monitoring examples/lima -name '*.md' 2>/dev/null) *.md",
"storybook": "pnpm run -C site/ storybook"
},
"devDependencies": {