From 32326905cab9d5ccbdc6176524173ede977c649d Mon Sep 17 00:00:00 2001 From: Bart Van Bos Date: Sat, 7 Nov 2026 10:28:57 +0100 Subject: [PATCH] ci: docsync v0.2.0 docs check (DOC-008), policy 1.4 Add the docsync manifest test (TestDocsyncManifest, skipped without DOCSYNC_MANIFEST), .otfabric/docsync.json, a docs-check make target and docs.yml (docsync v0.2.0, otfabric-policy v1.4.1). ci.yml skips Markdown-only changes and runs policy v1.4.1; policy pin 1.4. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 3 ++ .github/workflows/docs.yml | 19 +++++++++ .otfabric/docsync.json | 9 +++++ .otfabric/policy.yaml | 2 +- AGENTS.md | 2 +- Makefile | 10 ++++- docsync_manifest_test.go | 81 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 .otfabric/docsync.json create mode 100644 docsync_manifest_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1530172..fd08b36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,8 +4,10 @@ run-name: "CI (${{ github.event_name }}) on ${{ github.ref_name }} by @${{ githu on: push: branches: ["**"] + paths-ignore: ['**/*.md'] pull_request: branches: ["**"] + paths-ignore: ['**/*.md'] permissions: contents: read @@ -21,6 +23,7 @@ jobs: codecov-name: sunspecctl go-versions: '["1.26", "1.27"]' test-exclude-regex: "/example(s)?($|/)" + policy-version: v1.4.1 secrets: inherit # The per-package coverage floor (TEST-012). diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..a3ac5cb --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,19 @@ +# Documents against the code (DOC-008, standards/documentation.md#doc-sync). +# No path filter: this is the check that runs when ci.yml skips a +# Markdown-only change, so branch protection can require it. +name: Docs +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + docs: + uses: otfabric/.github/.github/workflows/docs-check.yml@v3.5.0 + with: + docsync-version: v0.2.0 + policy-version: v1.4.1 + secrets: inherit diff --git a/.otfabric/docsync.json b/.otfabric/docsync.json new file mode 100644 index 0000000..2cb79de --- /dev/null +++ b/.otfabric/docsync.json @@ -0,0 +1,9 @@ +{ + "clis": [ + { + "name": "sunspecctl", + "manifest": ".", + "exit_codes": "." + } + ] +} diff --git a/.otfabric/policy.yaml b/.otfabric/policy.yaml index dfc1df0..f9c7aa3 100644 --- a/.otfabric/policy.yaml +++ b/.otfabric/policy.yaml @@ -1,5 +1,5 @@ # OT Fabric engineering policy adoption (otfabric/engineering standards/README.md#adoption). -policy: "1.2" +policy: "1.4" profile: cli mode: enforce owner: bartvanbos diff --git a/AGENTS.md b/AGENTS.md index 71f81f8..d0c8d2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ Command-line tool for reading and simulating SunSpec devices over Modbus, built ## Organisation standards -This repository follows **otfabric/engineering policy 1.2**, profile **cli** (`.otfabric/policy.yaml`, mode `enforce`). Where this file and the organisation standards disagree, this file wins here — say so in your report. +This repository follows **otfabric/engineering policy 1.4**, profile **cli** (`.otfabric/policy.yaml`, mode `enforce`). Where this file and the organisation standards disagree, this file wins here — say so in your report. ## Where the truth lives diff --git a/Makefile b/Makefile index c397552..c06ec3d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help all build build-all release-all check test coverage coverage-html coverage-clean coverage-check cover fmt fmt-check vet lint lint-ci vuln install clean e2e fuzz coverage-pkg-check +.PHONY: help all build build-all release-all check test coverage coverage-html coverage-clean coverage-check cover fmt fmt-check vet lint lint-ci vuln install clean e2e fuzz coverage-pkg-check docs-check help: ## This help @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -31,6 +31,8 @@ TEST_PKGS := ./... COVERAGE_MIN := 90 # Per-package floor (TEST-012): what the weakest package measures now, rounded down. COVER_PKG_MIN := 90 +# docsync release of docs-check (DOC-008). +DOCSYNC_VERSION := v0.2.0 all: check build ## Run all checks and build the CLI @@ -39,7 +41,7 @@ coverage-pkg-check: coverage-check ## Fail when a package is below COVER_PKG_MIN END { for (k in n) { p = k; sub(/\/[^\/]*$$/, "", p); s[p] += n[k]; if (k in h) c[p] += n[k] } \ for (p in s) { v = 100 * c[p] / s[p]; if (v < m) { printf "%s: %.1f%% < %s%%\n", p, v, m; bad = 1 } } exit bad }' coverage.out -check: fmt fmt-check vet lint lint-ci vuln test coverage-pkg-check ## Run all checks (format, vet, lint, test, coverage) +check: fmt fmt-check vet lint lint-ci vuln test coverage-pkg-check docs-check ## Run all checks (format, vet, lint, test, coverage, docs) test: ## Run the tests with the race detector @echo "Running tests (race detector)" @@ -144,6 +146,10 @@ release-all: build-all ## Package CLI binaries into tar.gz (Unix) and zip (Windo fi; \ done +# The documents against the code (DOC-008): docsync, as docs-check.yml runs it. +docs-check: ## Check that the documents agree with the code (docsync) + @go run github.com/otfabric/docsync@$(DOCSYNC_VERSION) check . + FUZZTIME ?= 30s fuzz: ## Run every fuzz target for FUZZTIME each (default 30s) diff --git a/docsync_manifest_test.go b/docsync_manifest_test.go new file mode 100644 index 0000000..1eb6bd3 --- /dev/null +++ b/docsync_manifest_test.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT + +// docsync manifest v1 (template from github.com/otfabric/docsync; keep it as +// it is apart from the package clause and the line marked docsync:root). + +package main + +import ( + "encoding/json" + "os" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// TestDocsyncManifest writes the command tree to $DOCSYNC_MANIFEST for +// docsync, which checks it against the documents. Without the variable the +// test does nothing. +func TestDocsyncManifest(t *testing.T) { + out := os.Getenv("DOCSYNC_MANIFEST") + if out == "" { + t.Skip("DOCSYNC_MANIFEST is not set") + } + root := newRootCmd() // docsync:root + root.InitDefaultHelpCmd() + root.InitDefaultCompletionCmd() + type flag struct { + Name string `json:"name"` + Shorthand string `json:"shorthand,omitempty"` + Bool bool `json:"bool,omitempty"` + Required bool `json:"required,omitempty"` + Hidden bool `json:"hidden,omitempty"` + Local bool `json:"local,omitempty"` + } + type command struct { + Path string `json:"path"` + Aliases []string `json:"aliases,omitempty"` + Hidden bool `json:"hidden,omitempty"` + Runnable bool `json:"runnable"` + Subcommands []string `json:"subcommands,omitempty"` + Flags []flag `json:"flags,omitempty"` + } + var cmds []command + var walk func(c *cobra.Command) + walk = func(c *cobra.Command) { + c.InitDefaultHelpFlag() + c.InitDefaultVersionFlag() + path := strings.TrimSpace(strings.TrimPrefix(c.CommandPath(), root.Name())) + cmd := command{Path: path, Aliases: c.Aliases, Hidden: c.Hidden, Runnable: c.Runnable()} + local := map[string]bool{} + c.LocalNonPersistentFlags().VisitAll(func(f *pflag.Flag) { local[f.Name] = true }) + c.PersistentFlags().VisitAll(func(f *pflag.Flag) { local[f.Name] = true }) + add := func(f *pflag.Flag) { + req := false + if v, ok := f.Annotations[cobra.BashCompOneRequiredFlag]; ok && len(v) > 0 && v[0] == "true" { + req = true + } + cmd.Flags = append(cmd.Flags, flag{Name: f.Name, Shorthand: f.Shorthand, Bool: f.NoOptDefVal != "", + Required: req, Hidden: f.Hidden, Local: local[f.Name]}) + } + c.LocalFlags().VisitAll(add) + c.InheritedFlags().VisitAll(add) + for _, s := range c.Commands() { + cmd.Subcommands = append(cmd.Subcommands, s.Name()) + } + cmds = append(cmds, cmd) + for _, s := range c.Commands() { + walk(s) + } + } + walk(root) + data, err := json.Marshal(map[string]any{"version": 1, "name": root.Name(), "commands": cmds}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(out, data, 0o600); err != nil { + t.Fatal(err) + } +}