From e622b29629b4a6940ca80baf0663e5fc1f734811 Mon Sep 17 00:00:00 2001 From: Bart Van Bos Date: Sat, 7 Nov 2026 10:30:39 +0100 Subject: [PATCH] ci: docsync v0.2.0 docs check (DOC-008), engineering policy 1.4 Adds the docsync manifest test (cmd/docsync_manifest_test.go, root NewRootCmd(nil)), .otfabric/docsync.json, the docs-check make target and docs.yml (docs-check.yml@v3.5.0, docsync v0.2.0, 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 ++++- cmd/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 cmd/docsync_manifest_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16b5be4..05027c7 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 @@ -16,6 +18,7 @@ jobs: uses: otfabric/.github/.github/workflows/go-ci.yml@v3.3.1 with: run-govulncheck: true + policy-version: v1.4.1 fuzz-time: 10s coverage-min: "90" codecov-name: profinetctl 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..6892527 --- /dev/null +++ b/.otfabric/docsync.json @@ -0,0 +1,9 @@ +{ + "clis": [ + { + "name": "profinetctl", + "manifest": "./cmd", + "exit_codes": "internal/errs" + } + ] +} 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 14ab527..88c442e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ Command-line tool for PROFINET IO built on [go-profinet](https://github.com/otfa ## 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 53c1f23..8b7301a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help all run test coverage cover compile cross lint lint-ci fmt fmt-check vet vuln build build-nocheck build-all release-all install clean pnet-smoke examples-doc release-notes-check e2e coverage-pkg-check fuzz +.PHONY: help all run test coverage cover compile cross lint lint-ci fmt fmt-check vet vuln build build-nocheck build-all release-all install clean pnet-smoke examples-doc release-notes-check e2e coverage-pkg-check fuzz docs-check help: ## This help @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -35,6 +35,8 @@ COVERAGE_MIN ?= 90 # Per-package floor (TEST-012), from the coverage.out of `make coverage`. COVER_PKG_MIN := 85 FUZZTIME ?= 10s +# docsync release of docs-check (DOC-008). +DOCSYNC_VERSION := v0.2.0 all: test build build-all ## Test and build the application @@ -42,7 +44,7 @@ run: ## Run the application @echo "Running $(APP_NAME)" @go run $(APP_SRC) -check: release-notes-check fmt fmt-check lint lint-ci vet cross vuln test coverage coverage-pkg-check ## Run all checks (format, lint, vet, 32-bit build, vulnerability scan, test, coverage) +check: release-notes-check fmt fmt-check lint lint-ci vet cross vuln test coverage coverage-pkg-check docs-check ## Run all checks (format, lint, vet, 32-bit build, vulnerability scan, test, coverage) test: ## Run unit tests with race detector @echo "Running unit tests (race detector)" @@ -186,5 +188,9 @@ clean: ## Clean the build artifacts @rm -rf $(BIN_DIR) coverage.out @rm -f $(RELEASE_DIR)/* +# 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 . + release-notes-check: ## Fail when RELEASE.md is not in the otfabric release-notes format (## vX.Y.Z sections ending with ---) @scripts/release-notes.sh --check diff --git a/cmd/docsync_manifest_test.go b/cmd/docsync_manifest_test.go new file mode 100644 index 0000000..ce5925e --- /dev/null +++ b/cmd/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 cmd + +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(nil) // 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) + } +}