docs: add doc.go and rewrite exported godoc (#11) #7
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| # Skip main-branch CI for commits that only touch the CLA signatures | |
| # file or the auto-generated contributors list. Those pushes come | |
| # from the CLA Assistant workflow and the contributors regenerator | |
| # respectively; they cannot affect library behaviour. Normal PR | |
| # events continue to run the full suite (no paths filter below). | |
| paths-ignore: | |
| - "signatures/**" | |
| - "CONTRIBUTORS.md" | |
| pull_request: | |
| branches: [main] | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| makefile-targets-guard: | |
| name: Makefile targets documented | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| - name: Verify make help lists every required target | |
| run: | | |
| set -euo pipefail | |
| # Every target documented as part of the project contract must | |
| # appear in `make help` output. This guard fails the build if | |
| # a future Makefile edit silently drops a target. The expected | |
| # list is pinned here and mirrors the "Makefile Targets" | |
| # section of CLAUDE.md. If a new target is genuinely added to | |
| # the contract, update this list AND the CLAUDE.md section | |
| # together. | |
| expected=( | |
| check | |
| test | |
| test-bdd | |
| lint | |
| vet | |
| fmt | |
| fmt-check | |
| bench | |
| coverage | |
| tidy | |
| tidy-check | |
| security | |
| release-check | |
| clean | |
| help | |
| ) | |
| # Strip ANSI colour escapes so the grep matches the plain target | |
| # name as it appears after Makefile rendering. | |
| help_output=$(make help | sed $'s/\x1b\\[[0-9;]*m//g') | |
| missing=() | |
| for target in "${expected[@]}"; do | |
| if ! grep -qE "^[[:space:]]*${target}[[:space:]]" <<<"$help_output"; then | |
| missing+=("$target") | |
| fi | |
| done | |
| if [ "${#missing[@]}" -gt 0 ]; then | |
| echo "::error::make help is missing required target(s): ${missing[*]}" | |
| echo "-- full output of 'make help' --" | |
| echo "$help_output" | |
| exit 1 | |
| fi | |
| echo "All ${#expected[@]} required Makefile targets are documented in 'make help'." | |
| apache-header-guard: | |
| name: Apache 2.0 header on every Go file | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| - name: Verify Apache 2.0 header | |
| run: | | |
| set -euo pipefail | |
| # Every tracked *.go file MUST carry the Apache 2.0 header in its | |
| # first ~14 lines. The tell-tale phrase is stable across every | |
| # file in the repo; grep -L lists files missing it. | |
| missing=$(git ls-files '*.go' \ | |
| | xargs grep -L 'Licensed under the Apache License, Version 2.0' \ | |
| || true) | |
| if [ -n "$missing" ]; then | |
| echo "::error::Go files missing Apache 2.0 header:" | |
| printf '%s\n' "$missing" | |
| exit 1 | |
| fi | |
| echo "All tracked .go files carry the Apache 2.0 header." | |
| local-paths-guard: | |
| name: No local paths or replace directives | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| - name: Scan for local-filesystem references and replace directives | |
| run: | | |
| set -euo pipefail | |
| # A Go library MUST NOT ship with references to the maintainer's | |
| # local filesystem. Downstream consumers fetching the module via | |
| # `go get` can't satisfy /Users/johnny/... paths — the module is | |
| # broken on their machine. This guard catches two failure modes | |
| # before they reach main: | |
| # | |
| # 1. `replace` directives in go.mod — commonly used during local | |
| # development against a checkout of a sibling package, but | |
| # fatal for downstream consumers when committed. We block ALL | |
| # replace directives. If a legitimate need arises (a known | |
| # upstream bug, a fork hosted elsewhere), add an exception | |
| # here with a linked GitHub issue. | |
| # | |
| # 2. Absolute local paths anywhere in tracked files: Linux/macOS | |
| # /home/<user>/..., macOS /Users/..., Windows C:\Users\...\. | |
| # Covers source, docs, workflows, and anything else committed. | |
| failed=0 | |
| echo "::group::go.mod replace directives" | |
| if grep -nE "^[[:space:]]*replace[[:space:]]" go.mod; then | |
| echo "::error file=go.mod::replace directives are forbidden in a published library — see local-paths-guard." | |
| failed=1 | |
| else | |
| echo "go.mod: no replace directives" | |
| fi | |
| echo "::endgroup::" | |
| echo "::group::Absolute local paths in tracked files" | |
| # Exclusions: | |
| # - CLAUDE.md and .claude/ are dev-local (gitignored, but paranoid). | |
| # - This workflow file itself describes the forbidden patterns. | |
| pattern='(/Users/[A-Za-z0-9._-]+/|/home/[A-Za-z0-9._-]+/|[A-Z]:\\\\Users\\\\)' | |
| hits=$(git ls-files \ | |
| | grep -Ev '^(CLAUDE\.md|\.claude/|\.github/workflows/ci\.yml)$' \ | |
| | xargs -I{} sh -c 'grep -EHn "$1" "$2" 2>/dev/null || true' _ "$pattern" {} \ | |
| || true) | |
| if [ -n "$hits" ]; then | |
| echo "::error::Absolute local paths found in tracked files:" | |
| printf '%s\n' "$hits" | |
| failed=1 | |
| else | |
| echo "no local paths found" | |
| fi | |
| echo "::endgroup::" | |
| exit "$failed" | |
| attribution-guard: | |
| name: No AI attribution | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Scan tracked files for AI-tool references | |
| run: | | |
| set -euo pipefail | |
| # Forbidden tokens (case-insensitive). Exceptions: | |
| # - CLAUDE.md as a literal filename reference (the dev-local | |
| # project guide). | |
| # - llms.txt / llms-full.txt (file names for the LLM tooling | |
| # ecosystem — product names, not attribution). | |
| # - The CONTRIBUTING.md rule block that enumerates the | |
| # forbidden tokens as part of the policy itself. | |
| # | |
| # Commit messages are no longer scanned: they are transient and | |
| # controlled by the commit-message-reviewer gate at author time. | |
| # Scanning every historical commit message trips on any PR that | |
| # discusses the guard itself, and this is a masking library, not | |
| # a compliance audit trail. | |
| PATTERN='claude|anthropic|copilot|[^a-z]gpt[^a-z]?|\bllm\b|ai-generated|ai-assisted|generated by ai|co-authored-by: claude' | |
| echo "::group::Tracked files" | |
| # Exclusions: | |
| # - CLAUDE.md and .claude/ — dev-local Claude Code config (gitignored). | |
| # - CONTRIBUTING.md, SECURITY.md, this workflow, the requirements doc — | |
| # contain the forbidden tokens as part of the policy statement itself. | |
| # - llms.txt / llms-full.txt — the AI-assistant documentation bundle. | |
| # llms-full.txt is a generated concatenation of the above policy- | |
| # bearing files, so it inherits the same exclusion by construction; | |
| # llms.txt names mistakes AI assistants should avoid and cites tool | |
| # names in the "common mistakes" prose. | |
| files=$(git ls-files \ | |
| | grep -Ev '^(CLAUDE\.md|\.claude/|\.github/workflows/ci\.yml|CONTRIBUTING\.md|SECURITY\.md|docs/v1\.0\.0-requirements\.md|\.gitignore|llms\.txt|llms-full\.txt)$' \ | |
| | grep -Ev '\.(png|jpg|jpeg|gif|ico|webp|svg|pdf|woff2?|ttf|otf|mp3|mp4|wav|zip|gz|tgz)$' \ | |
| || true) | |
| hits="" | |
| # grep -I skips any file detected as binary — safety net in case the | |
| # extension blocklist above misses something. | |
| for f in $files; do | |
| if grep -IiEn "$PATTERN" "$f" >/dev/null 2>&1; then | |
| # Strip benign path references: llms.txt / llms-full.txt product | |
| # filenames, the CLAUDE.md filename token, and .claude/ path | |
| # segments in config files. | |
| match=$(grep -IiEn "$PATTERN" "$f" | grep -viE 'llms(-full)?\.txt|CLAUDE\.md|\.claude/') | |
| if [ -n "$match" ]; then | |
| hits="${hits}${f}:\n${match}\n\n" | |
| fi | |
| fi | |
| done | |
| if [ -n "$hits" ]; then | |
| printf '::error::AI-tool references found in tracked files:\n%b' "$hits" | |
| exit 1 | |
| fi | |
| echo "No AI-tool references found." | |
| echo "::endgroup::" | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| - uses: actions/setup-go@v6.4.0 | |
| with: | |
| go-version: "1.26" | |
| cache: true | |
| - name: Install goimports | |
| run: go install golang.org/x/tools/cmd/goimports@v0.28.0 | |
| - name: Format check | |
| run: make fmt-check | |
| - name: Vet | |
| run: make vet | |
| - name: golangci-lint | |
| uses: golangci/golangci-lint-action@v9.2.0 | |
| with: | |
| version: v2.11.4 | |
| install-mode: goinstall | |
| - name: Module tidy check | |
| run: make tidy-check | |
| test: | |
| name: Test | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| - uses: actions/setup-go@v6.4.0 | |
| with: | |
| go-version: "1.26" | |
| cache: true | |
| - name: Unit tests | |
| run: make test | |
| - name: BDD tests | |
| run: make test-bdd | |
| - name: Coverage | |
| if: matrix.os == 'ubuntu-latest' | |
| run: make coverage | |
| - name: Enforce coverage threshold | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| total=$(go tool cover -func=coverage.out | awk '/^total:/ {print $3}' | tr -d '%') | |
| threshold=95.0 | |
| echo "Total coverage: ${total}% (threshold ${threshold}%)" | |
| awk -v t="$total" -v th="$threshold" 'BEGIN { if (t+0 < th+0) { exit 1 } }' \ | |
| || { echo "::error::coverage ${total}% < threshold ${threshold}%"; exit 1; } | |
| - name: Upload coverage | |
| if: matrix.os == 'ubuntu-latest' | |
| uses: actions/upload-artifact@v7.0.1 | |
| with: | |
| name: coverage | |
| path: coverage.out | |
| build: | |
| name: Build ${{ matrix.goos }}/${{ matrix.goarch }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - goos: linux | |
| goarch: amd64 | |
| - goos: darwin | |
| goarch: arm64 | |
| - goos: windows | |
| goarch: amd64 | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| - uses: actions/setup-go@v6.4.0 | |
| with: | |
| go-version: "1.26" | |
| cache: true | |
| - name: Build | |
| env: | |
| GOOS: ${{ matrix.goos }} | |
| GOARCH: ${{ matrix.goarch }} | |
| CGO_ENABLED: "0" | |
| run: go build ./... | |
| security: | |
| name: Security scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| - uses: actions/setup-go@v6.4.0 | |
| with: | |
| go-version: "1.26" | |
| cache: true | |
| - name: govulncheck | |
| run: | | |
| go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 | |
| govulncheck ./... | |
| release-check: | |
| name: GoReleaser config check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| - uses: actions/setup-go@v6.4.0 | |
| with: | |
| go-version: "1.26" | |
| cache: true | |
| - uses: goreleaser/goreleaser-action@v7.0.0 | |
| with: | |
| distribution: goreleaser | |
| version: v2.7.0 | |
| args: check |