Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ make test # Run all tests
make lint # Run golangci-lint (.golangci.yml; fails if not installed)
make check # fmt + lint + test (mutating: fmt rewrites files)
make verify # Non-mutating, credential-free gate for read-only reviewers: tidy diff, vet, gofmt -l, exact-pin lint, non-E2E tests
make ci # Credential-free gate matching CI's fail-fast order (verify-static, then coverage, E2E, race, cross-build)
make ci # Credential-free gate matching CI's fail-fast order (verify-static, docs integrity, then coverage, E2E, race, cross-build)
make test-cover # Tests with HTML coverage report
make coverage-check # Enforce the absolute floor and committed coverage ratchet
make test-coverage-check # Self-test scripts/check-coverage.sh with fixture profiles
Expand All @@ -81,7 +81,9 @@ Run a single test: `go test -v -run TestName ./updex/`
Build workflow: make code changes → `make fmt` → `make build` → smoke-test
with `./build/updex --help`. Use `make check` for the quick development loop.
Run `make ci` before opening a pull request. It checks module tidiness, vet,
formatting, lint, non-E2E unit and race tests, the separate black-box E2E
formatting, lint, docs integrity (the same `make test-docs-check` and
`node scripts/check-docs.mjs` the `docs-gate` CI job runs, so it needs Node.js
too), non-E2E unit and race tests, the separate black-box E2E
Comment on lines +84 to +86

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and the finding is correct on both counts at head 5ac9b99:

  • docs/design/overview.md "CI and Releases" (lines 583-592) enumerates the make ci stages as verify-static -> coverage -> E2E -> race -> cross-build. grep -n 'docs-integrity\|check-docs' docs/design/overview.md returns nothing, so the file never mentions the two steps this PR inserts.
  • docs/specs/pr-review-rubric.md:24 ("Build gate green") likewise enumerates make ci as tidy, vet, gofmt, golangci-lint, non-E2E unit tests, coverage gate, race, cross-build. The separate "Docs-integrity gate green" row names node scripts/check-docs.mjs but not as part of make ci, so the enumeration is now incomplete.

This is a patch change, not a mechanical cure, so it cannot be pushed under the pr-cure item that surfaced this thread (Snowcat refuses a cure whose patch identity moved). I have queued a bounded pr-cure-change proposal against this same branch to update both passages to match the ci: recipe in the Makefile — nothing else in either file. Leaving this thread unresolved until that lands.

suite, and Linux amd64/arm64 builds, and requires `golangci-lint`. The lint
step first runs `make lint-version-check`, which fails unless the installed
`golangci-lint` matches the pin in `mise.toml` (currently 2.13.1) and was
Expand Down Expand Up @@ -648,8 +650,8 @@ their canonical targets, never the aliases. `docs/review-rubric.md`,
2. Keep changes focused; unrelated fixes belong in a separate PR.
3. Use Conventional Commits for commit messages **and the pull request
title** (see [Commits & Pull Requests](#commits--pull-requests)).
4. Run `make fmt`, `make ci`, and `node scripts/check-docs.mjs` and make
sure they pass.
4. Run `make fmt` and `make ci` (which now includes the docs-integrity
gate) and make sure they pass.
5. Update the documentation and add tests for your change.
6. Classify the change using the [risk tier guide](docs/risk-tiers.md) and
include the tier rationale in the pull request
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,12 @@ verify: verify-static
@echo "==> unit tests"
$(GO) test $$($(GO) list ./... | grep -v '/tests/e2e$$')

## ci: Run the credential-free CI gate (verify-static, then coverage, race, and cross-build)
## ci: Run the credential-free CI gate (verify-static, docs integrity, then coverage, race, and cross-build)
ci: verify-static
@echo "==> docs-integrity checker self-test"
$(MAKE) test-docs-check
@echo "==> docs integrity"
node scripts/check-docs.mjs
@echo "==> unit tests with coverage"
$(GO) test -v $$($(GO) list ./... | grep -v '/tests/e2e$$') -coverprofile=coverage.out -covermode=atomic
@echo "==> coverage floor"
Expand Down
71 changes: 71 additions & 0 deletions updex/makefile_ci_docs_gate_contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package updex

import (
"os"
"regexp"
"strings"
"testing"
)

// docsGateInvocations are the two commands the `docs-gate` job in
// .github/workflows/test.yml runs. `make ci` claims to be the credential-free
// local equivalent of that workflow, so it must run both as well.
var docsGateInvocations = []struct {
name string
pattern *regexp.Regexp
}{
{
name: "make test-docs-check",
pattern: regexp.MustCompile(`(?m)^\t@?(?:\$\(MAKE\)|make)(?:\s+--no-print-directory)?\s+test-docs-check\s*$`),
},
{
name: "node scripts/check-docs.mjs",
pattern: regexp.MustCompile(`(?m)^\t@?node\s+scripts/check-docs\.mjs\s*$`),
},
}

// TestMakefileCIRecipeRunsDocsIntegrityGate pins the docs-integrity checks
// into `make ci`. GitHub CI runs `make test-docs-check` and
// `node scripts/check-docs.mjs` in its `docs-gate` job; before this guard the
// local `ci` recipe ran neither, so a broken docs index, a dead relative link,
// or a broken conformance alias passed the local gate and only failed after a
// pull request was already open. The test fails if either invocation is
// dropped from the recipe, is moved after the final "CI gate passed" line, or
// has its failure swallowed.
func TestMakefileCIRecipeRunsDocsIntegrityGate(t *testing.T) {
data, err := os.ReadFile("../Makefile")
if err != nil {
t.Fatalf("read Makefile: %v", err)
}

recipe := extractRecipe(t, string(data), "ci:")

passIdx := strings.Index(recipe, "CI gate passed")
if passIdx < 0 {
t.Fatalf("Makefile ci recipe no longer prints \"CI gate passed\"; got:\n%s", recipe)
}

for _, invocation := range docsGateInvocations {
loc := invocation.pattern.FindStringIndex(recipe)
if loc == nil {
t.Errorf("Makefile ci recipe must invoke `%s` (the docs-gate CI job runs it); got:\n%s", invocation.name, recipe)
continue
}
if loc[0] > passIdx {
t.Errorf("Makefile ci recipe invokes `%s` after printing \"CI gate passed\"; move it before the success line", invocation.name)
}
}

for _, line := range strings.Split(recipe, "\n") {
trimmed := strings.TrimPrefix(line, "\t")
if !strings.Contains(trimmed, "test-docs-check") && !strings.Contains(trimmed, "check-docs.mjs") {
continue
}
if strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "@-") {
t.Errorf("Makefile ci recipe ignores the exit status of a docs-integrity invocation: %q", line)
}
if strings.Contains(trimmed, "||") {
t.Errorf("Makefile ci recipe swallows a docs-integrity failure with a `||` fallback: %q", line)
}
}
}