From 4d77127f6f3dd755aa7533b26a6c4ecb58efef39 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 20 Aug 2026 17:06:14 +0100 Subject: [PATCH 1/5] docs: publish the command reference to GitHub Pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI's help text is the only complete description of its 61 commands, and it was only readable from a terminal. Render it as a website so it is linkable, searchable, and indexed. The site is nothing but the reference. Installing and getting started are already documented in the README and on docs.flagsmith.com, and a third copy here would be the one that goes stale — so the root command's own page is the home page and there is no hand-written prose anywhere on the site. Install and Guides are navbar links out to those two. cmd/docgen renders each page with cobra's markdown generator, so a page cannot drift from the binary: nothing about a command is written by hand. It walks the tree itself rather than using cobra's GenMarkdownTree, because that flattens every page into one directory — which gives a sidebar listing six commands all called "create", and URLs like /flagsmith_feature_variant_create/. Mirroring the command tree as a directory tree instead gives /feature/variant/create/ and navigation that nests the way the CLI does. Cross-references are rewritten relative to the page holding them, so they survive the /flagsmith-cli/ prefix a project site is served under. Hidden commands and hidden flags are skipped by cobra. The auto-generated date footer is disabled so the build is reproducible, and cobra's opening heading is dropped because the theme already renders the title. Only the newest release is published — GitHub Pages serves one artifact, and versioned docs would be machinery serving nobody while a single line is supported. But which release that is has to be visible, or a page found in a search result looks current to someone running an older CLI. A badge beside the navbar title names it where nobody has to scroll for it, and the footer repeats it with the context that explains it. The version is a literal in hugo.yaml that release-please bumps like the ones in install.sh and the README, so a local preview shows exactly what a deploy would and the workflow needs no step of its own to work it out. website/ therefore holds the Hugo config and three small templates: two are theme overrides Hextra gives no hook for — the navbar badge, and a home page that keeps its sidebar rather than hiding it — and one is Hextra's own footer extension point. Its go.mod is deliberately separate so the theme never becomes a dependency of the released binary. Docs deploy from release tags, so the published reference always describes a version you can install. A cheap generate-only job runs on pull requests to catch a broken generator before a release rather than during one. Importing cobra/doc adds three indirect dependencies (go-md2man, blackfriday, yaml/v3); they are build-time only for docgen and are not linked into the CLI. --- .github/workflows/docs.yml | 58 ++++++ .github/workflows/pull-request.yml | 14 ++ .gitignore | 6 + README.md | 2 + cmd/docgen/main.go | 198 +++++++++++++++++++ go.mod | 3 + go.sum | 4 + internal/cmd/root.go | 8 + release-please-config.json | 3 +- website/README.md | 55 ++++++ website/go.mod | 7 + website/go.sum | 4 + website/hugo.yaml | 68 +++++++ website/layouts/_partials/custom/footer.html | 19 ++ website/layouts/_partials/navbar-title.html | 39 ++++ website/layouts/home.html | 34 ++++ 16 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docs.yml create mode 100644 cmd/docgen/main.go create mode 100644 website/README.md create mode 100644 website/go.mod create mode 100644 website/go.sum create mode 100644 website/hugo.yaml create mode 100644 website/layouts/_partials/custom/footer.html create mode 100644 website/layouts/_partials/navbar-title.html create mode 100644 website/layouts/home.html diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..805ebb4 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,58 @@ +name: Docs + +# The published reference must describe a version people can install, so it is +# built from release tags rather than from main. workflow_dispatch is here for +# fixing the site itself (theme, landing page) without cutting a release. +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: read + +# One deployment at a time: GitHub Pages rejects concurrent deploys. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + # Hextra needs the extended build for its SCSS. + - uses: peaceiris/actions-hugo@2752ce1d29631191ea3f27c23495fa06139a5b78 # v3.2.1 + with: + hugo-version: "0.165.0" + extended: true + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + id: pages + - name: Generate command reference + run: go run ./cmd/docgen -out website/content + - name: Build site + working-directory: website + # configure-pages reports the correct base URL, which differs between a + # project site (/flagsmith-cli/) and a custom domain. + run: hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/" + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: website/public + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write # deploy to Pages + id-token: write # verify the deployment originates here + steps: + - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + id: deployment diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 993af2b..0073db9 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -47,6 +47,20 @@ jobs: with: extra-args: --all-files --hook-stage pre-push renovate-config-validator + # The site itself is only built on release tags, but generating the reference + # is cheap and catches a broken generator here rather than mid-release. + docs: + name: docs generate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + - run: go run ./cmd/docgen -out "$RUNNER_TEMP/reference" + install-ps1: name: install.ps1 lint runs-on: windows-latest diff --git a/.gitignore b/.gitignore index a94b49e..13bb2a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ /flagsmith /dist/ *.test + +# Generated command reference and Hugo build output (see website/README.md). +/website/content/ +/website/public/ +/website/resources/ +/website/.hugo_build.lock diff --git a/README.md b/README.md index 43335dc..f49c99d 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ flagsmith flag list # list the flags in the current environment ## Commands +Full reference for every command and flag: (generated from `--help`, so it matches the release). + Reading works against any Flagsmith instance. Changing flags — `flag update`, `flag enable`/`disable`, `flag reorder`, `flag delete` — needs Flagsmith 2.263.0 or newer, self-hosted or SaaS. - `flagsmith init` — bind the current directory to a project (writes `flagsmith.json`). diff --git a/cmd/docgen/main.go b/cmd/docgen/main.go new file mode 100644 index 0000000..2b1169c --- /dev/null +++ b/cmd/docgen/main.go @@ -0,0 +1,198 @@ +// Command docgen renders the CLI's own help into markdown for the website. +// +// Pages are cobra's own markdown output, so the published reference cannot +// drift from the binary: every page is the command's Short, Long, Example and +// flags as the terminal would print them. Hidden commands and hidden flags are +// skipped. +// +// The command tree is mirrored as a directory tree — `flagsmith flag update` +// becomes flag/update.md, served at /flag/update/ — so the site navigation +// nests the way the CLI does. cobra's own tree walker is not used because it +// flattens everything into one directory, which leaves a sidebar of six +// commands all called "create". +// +// The output is the whole site content: the root command's page is the home +// page, so there is nowhere for a hand-written description of the CLI to drift +// out of step with the CLI. +// +// Usage: +// +// go run ./cmd/docgen -out website/content +package main + +import ( + "bytes" + "flag" + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + + "github.com/Flagsmith/flagsmith-cli/v2/internal/cmd" +) + +func main() { + if err := run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(args []string) error { + flags := flag.NewFlagSet(filepath.Base(args[0]), flag.ContinueOnError) + out := flags.String("out", "", "directory to write markdown pages to (required)") + if err := flags.Parse(args[1:]); err != nil { + return err + } + if *out == "" { + flags.Usage() + return fmt.Errorf("-out not set") + } + + root := cmd.Root() + // Every page ends with cobra's "Auto generated ... on " footer + // otherwise, which makes the build non-reproducible. cobra only inherits + // this from a parent while rendering its "SEE ALSO" block, so set it on + // each command rather than trusting it to propagate from the root. + forEach(root, func(c *cobra.Command) { c.DisableAutoGenTag = true }) + + // The whole directory is generated, so clear it: a renamed or deleted + // command must not leave an orphan page behind. + if err := os.RemoveAll(*out); err != nil { + return err + } + return write(root, newSite(root), *out) +} + +// forEach applies fn to cmd and every command beneath it, skipping the ones +// cobra would not document. +func forEach(cmd *cobra.Command, fn func(*cobra.Command)) { + fn(cmd) + for _, sub := range cmd.Commands() { + if !sub.IsAvailableCommand() || sub.IsAdditionalHelpTopicCommand() { + continue + } + forEach(sub, fn) + } +} + +// site maps each documented command to where its page lives, and remembers the +// link names cobra will ask about while rendering cross-references. +type site struct { + // url is the served path of a command's page, e.g. "flag/update", relative + // to the site root. + url map[*cobra.Command]string + // byLink resolves the "flagsmith_flag_update.md" names cobra emits in its + // SEE ALSO block back to the command they point at. + byLink map[string]*cobra.Command +} + +func newSite(root *cobra.Command) *site { + s := &site{url: map[*cobra.Command]string{}, byLink: map[string]*cobra.Command{}} + forEach(root, func(c *cobra.Command) { + // path.Join returns "" for the root command; "." keeps it a valid path + // for filepath.Rel when links are computed. + s.url[c] = path.Join(".", strings.Join(segments(c), "/")) + s.byLink[strings.ReplaceAll(c.CommandPath(), " ", "_")+".md"] = c + }) + return s +} + +// segments is a command's path below the root, so `flagsmith flag update` +// yields [flag update] and the root itself yields nothing. +func segments(cmd *cobra.Command) []string { + return strings.Fields(cmd.CommandPath())[1:] +} + +// write renders cmd and its subcommands beneath dir. +func write(cmd *cobra.Command, s *site, dir string) error { + page, err := render(cmd, s) + if err != nil { + return err + } + + // A command with subcommands owns a directory, so its own page has to be + // that directory's index; a leaf is a plain page beside its siblings. + file := filepath.Join(dir, filepath.Join(segments(cmd)...)+".md") + if cmd.HasAvailableSubCommands() { + file = filepath.Join(dir, filepath.Join(segments(cmd)...), "_index.md") + } + if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil { + return err + } + if err := os.WriteFile(file, page, 0o644); err != nil { + return err + } + + for _, sub := range cmd.Commands() { + if !sub.IsAvailableCommand() || sub.IsAdditionalHelpTopicCommand() { + continue + } + if err := write(sub, s, dir); err != nil { + return err + } + } + return nil +} + +// render produces one page: Hugo front matter, then cobra's markdown. +func render(cmd *cobra.Command, s *site) ([]byte, error) { + var body bytes.Buffer + if err := doc.GenMarkdownCustom(cmd, &body, s.linker(cmd)); err != nil { + return nil, err + } + + var page bytes.Buffer + // Hextra renders the title as the page's heading, and cobra opens with the + // command path as a heading too. Keep the front matter one and drop + // cobra's, so the command name is not printed twice. + fmt.Fprintf(&page, "---\ntitle: %s\nlinkTitle: %s\ndescription: %s\n", + yamlString(cmd.CommandPath()), yamlString(cmd.Name()), yamlString(cmd.Short)) + if !cmd.HasParent() { + // Expand the top of the sidebar on arrival. + fmt.Fprint(&page, "sidebar:\n open: true\n") + } + fmt.Fprint(&page, "---\n\n") + page.Write(dropHeading(body.Bytes())) + return page.Bytes(), nil +} + +// linker resolves cobra's cross-reference filenames into links relative to the +// page being rendered, so they survive being served under a project-pages +// subpath such as /flagsmith-cli/. +func (s *site) linker(from *cobra.Command) func(string) string { + return func(link string) string { + to, ok := s.byLink[link] + if !ok { + return link + } + rel, err := filepath.Rel(s.url[from], s.url[to]) + if err != nil { + return link + } + return filepath.ToSlash(rel) + "/" + } +} + +// dropHeading removes the leading "## " line cobra writes, along +// with the blank line after it. +func dropHeading(md []byte) []byte { + if !bytes.HasPrefix(md, []byte("## ")) { + return md + } + _, rest, found := bytes.Cut(md, []byte("\n")) + if !found { + return md + } + return bytes.TrimLeft(rest, "\n") +} + +// yamlString quotes a value for a front matter scalar, since a Short can +// contain a colon or a quote. +func yamlString(s string) string { + return `"` + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(s) + `"` +} diff --git a/go.mod b/go.mod index 0ec789b..5c0398e 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect @@ -49,7 +50,9 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/ohler55/ojg v1.28.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.39.0 // indirect diff --git a/go.sum b/go.sum index e44eb7b..0c351cc 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,7 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -97,6 +98,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= @@ -112,6 +114,7 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= @@ -130,6 +133,7 @@ golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 82b6e14..1b50b74 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -174,6 +174,14 @@ func nudgeInit(cmd *cobra.Command) bool { return true } +// Root returns the fully wired command tree, for callers that need to inspect +// it rather than run it — documentation generation, shell completions. It never +// executes a command, so no credential or network access is involved. +func Root() *cobra.Command { + prepare() + return rootCmd +} + func Execute() { prepare() // ExecuteC returns the command that actually ran (or failed to parse), so a diff --git a/release-please-config.json b/release-please-config.json index b652d56..511667a 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -9,7 +9,8 @@ "extra-files": [ "install.sh", "install.ps1", - "README.md" + "README.md", + "website/hugo.yaml" ] } }, diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..9f0c5f0 --- /dev/null +++ b/website/README.md @@ -0,0 +1,55 @@ +# Command reference site + +The [Hugo](https://gohugo.io) site published to + by +[`.github/workflows/docs.yml`](../.github/workflows/docs.yml) on every release +tag. + +The site *is* the command reference: all of `content/` is generated from the +CLI's own help text by [`cmd/docgen`](../cmd/docgen) and is gitignored. Nothing +about a command is written here by hand — fix the `Short`, `Long` or `Example` +on the command itself in `internal/cmd/` and the page follows. There is +deliberately no landing page: installing and getting started are documented in +the [README](../README.md#install) and on +[docs.flagsmith.com](https://docs.flagsmith.com/integrating-with-flagsmith/CLI), +and a third copy here would be the one that goes stale. + +The command tree becomes a directory tree, so `flagsmith flag update` is served +at `/flag/update/`, the `flagsmith` root command is the home page, and the +sidebar nests the way the CLI does. + +## Preview locally + +Requires [Hugo extended](https://gohugo.io/installation/) (`brew install hugo`) +and Go. + +```sh +go run ./cmd/docgen -out website/content # from the repository root +cd website && hugo server +``` + +Then open . Re-run `docgen` after changing help text. + +## What is hand-written + +| Path | Purpose | +| --- | --- | +| `hugo.yaml` | Site config, navigation, theme | +| `layouts/_partials/navbar-title.html` | Theme override: version badge | +| `layouts/_partials/custom/footer.html` | Names the release being documented | +| `layouts/home.html` | Theme override, see below | + +Only the newest release is published: Pages serves a single artifact, so there +are no `/vX.Y.Z/` URLs. The version is therefore shown twice — as a badge beside +the navbar title, where it is seen without scrolling, and once in the footer with +the context that explains it. Both read `params.version` in `hugo.yaml`, which +release-please bumps in its release pull request (`website/hugo.yaml` is one of +its `extra-files`), so a plain `hugo server` shows exactly what a deploy would. + +Hextra's home layout hides the sidebar, which would leave the front page — the +root command's own page — with no navigation at all. `layouts/home.html` is the +theme's ordinary reference-page layout, copied so the home page gets the command +tree too. Re-check it against the theme when bumping Hextra. + +`go.mod` here is a Hugo module file and is intentionally separate from the CLI's +own `go.mod`, so the theme never becomes a dependency of the released binary. diff --git a/website/go.mod b/website/go.mod new file mode 100644 index 0000000..9ed82a6 --- /dev/null +++ b/website/go.mod @@ -0,0 +1,7 @@ +// Hugo module for the documentation site. Deliberately a separate module from +// the CLI: theme dependencies must not reach the released binary's go.mod. +module github.com/Flagsmith/flagsmith-cli/website + +go 1.26 + +require github.com/imfing/hextra v0.12.3 // indirect diff --git a/website/go.sum b/website/go.sum new file mode 100644 index 0000000..90d1e03 --- /dev/null +++ b/website/go.sum @@ -0,0 +1,4 @@ +github.com/imfing/hextra v0.12.0 h1:f6y35hW/WDJEcx9S0dOmbICOBxYE0PmP6IJFsTUgVyY= +github.com/imfing/hextra v0.12.0/go.mod h1:YAv8XRNSmcqjieFwI7fVQK1AoY2Do+45DO9HGqxSGu4= +github.com/imfing/hextra v0.12.3 h1:DZHY2rUWYteyzjlHi9r4n7Bb5e2Q+6LXe4C1Dqn0ZjM= +github.com/imfing/hextra v0.12.3/go.mod h1:vi+yhpq8YPp/aghvJlNKVnJKcPJ/VyAEcfC1BSV9ARo= diff --git a/website/hugo.yaml b/website/hugo.yaml new file mode 100644 index 0000000..7a432b1 --- /dev/null +++ b/website/hugo.yaml @@ -0,0 +1,68 @@ +# Hugo config for the generated command reference at +# https://flagsmith.github.io/flagsmith-cli/ +# +# The whole of content/ is generated by `go run ./cmd/docgen` and is not +# committed; see website/README.md. +baseURL: https://flagsmith.github.io/flagsmith-cli/ +title: Flagsmith CLI +locale: en-gb + +module: + imports: + - path: github.com/imfing/hextra + +enableRobotsTXT: true +# Generated pages have no git history of their own, so there is no +# "last modified" worth showing. +enableGitInfo: false + +markup: + highlight: + noClasses: false + goldmark: + renderer: + # Required by Hextra's shortcodes, which emit HTML. + unsafe: true + +menu: + main: + # Installing is documented in the README and on docs.flagsmith.com. Link to + # one of them rather than keeping a third copy in step. + - name: Install + url: https://github.com/Flagsmith/flagsmith-cli#install + weight: 1 + - name: Guides + url: https://docs.flagsmith.com/integrating-with-flagsmith/CLI + weight: 2 + - name: Search + weight: 3 + params: + type: search + - name: GitHub + weight: 4 + url: https://github.com/Flagsmith/flagsmith-cli + params: + icon: github + +params: + # The release this build documents, shown beside the navbar title and in the + # footer. Bumped by release-please, which is why website/hugo.yaml is one of + # its extra-files. + version: "v2.0.0-beta.3" # x-release-please-version + description: The Flagsmith command-line interface — manage flags, segments and environments from your terminal. + navbar: + displayTitle: true + displayLogo: false + footer: + displayCopyright: false + displayPoweredBy: false + # The reference is generated, so "edit this page" would point at a file that + # does not exist in the repository. + editURL: + enable: false + page: + width: normal + # Every page is a command, so the sidebar is the command tree and should be + # visible from the home page down. + sidebar: + displayTitle: false diff --git a/website/layouts/_partials/custom/footer.html b/website/layouts/_partials/custom/footer.html new file mode 100644 index 0000000..803c669 --- /dev/null +++ b/website/layouts/_partials/custom/footer.html @@ -0,0 +1,19 @@ +{{/* + Hextra's own extension point for footer content, so unlike layouts/home.html + this copies no theme markup and cannot drift from it. + + The site always describes the newest release, which is invisible to someone + who arrives from a search result while running an older CLI. Name the version, + so a stale page looks stale. HUGO_PARAMS_VERSION is set by the docs workflow; + it is empty for a local preview, which renders nothing. +*/}} +{{- with site.Params.version -}} +
+ Documenting + {{ . }}, the latest release. Run flagsmith --version to check yours. +
+{{- end -}} diff --git a/website/layouts/_partials/navbar-title.html b/website/layouts/_partials/navbar-title.html new file mode 100644 index 0000000..1040b3f --- /dev/null +++ b/website/layouts/_partials/navbar-title.html @@ -0,0 +1,39 @@ +{{/* + Hextra has no hook for navbar content, so this is the theme's own + navbar-title.html with a version badge added beside the title. The version + belongs where it is seen without scrolling: a reader arriving from a search + result on an older CLI should not have to reach the footer to find out which + release the page describes. + + The auto margin that pushes the menu to the right moves onto the badge when + there is one, so the badge sits beside the title rather than beside the menu. + + Copied from hextra@v0.12.3 layouts/_partials/navbar-title.html; re-check when + bumping the theme. +*/}} +{{- $logoPath := .Site.Params.navbar.logo.path | default "images/logo.svg" -}} +{{- $logoLink := .Site.Params.navbar.logo.link | default .Site.Home.RelPermalink -}} +{{- $logoWidth := .Site.Params.navbar.logo.width | default "20" -}} +{{- $logoHeight := .Site.Params.navbar.logo.height | default "20" -}} +{{- $logoDarkPath := .Site.Params.navbar.logo.dark | default $logoPath -}} +{{- $version := .Site.Params.version -}} +{{- $push := cond (eq $version "") "hx:ltr:mr-auto hx:rtl:ml-auto" "" -}} + + + {{- $displayTitle := (.Site.Params.navbar.displayTitle | default true) }} + {{- if (.Site.Params.navbar.displayLogo | default true) }} + {{ cond $displayTitle `Logo` .Site.Title }} + {{ cond $displayTitle `Dark Logo` .Site.Title }} + {{- end }} + {{- if $displayTitle }} + {{- .Site.Title -}} + {{- end }} + +{{- with $version }} + {{ . }} +{{- end }} diff --git a/website/layouts/home.html b/website/layouts/home.html new file mode 100644 index 0000000..589d3c4 --- /dev/null +++ b/website/layouts/home.html @@ -0,0 +1,34 @@ +{{/* + The site is the command reference, so the home page is the `flagsmith` root + command's own page and needs the command tree beside it. Hextra's home layout + passes disableSidebar, which would leave the front page with no navigation at + all, so use the layout the theme gives any other reference page. + + Copied from hextra@v0.12.3 layouts/docs/list.html; re-check when bumping the + theme. +*/}} +{{ define "main" }} +
+ {{ partial "sidebar.html" (dict "context" .) }} + {{ partial "toc.html" . }} +
+
+ {{ partial "breadcrumb.html" (dict "page" . "enable" true) }} +
+ {{ if .Title }} +
+

{{ .Title }}

+ {{ partial "components/page-context-menu" . }} +
+ {{ end }} + {{ .Content }} +
+ {{ partial "components/last-updated.html" . }} + {{- if (site.Params.page.displayPagination | default true) -}} + {{- partial "components/pager.html" . -}} + {{- end -}} + {{ partial "components/comments.html" . }} +
+
+
+{{ end }} From f87de346f2dac1f7967a6d648b3e621240dcc8d0 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 20 Aug 2026 17:58:12 +0100 Subject: [PATCH 2/5] docs: refresh the README install instructions Point installs at get.flagsmith.com rather than a raw.githubusercontent.com path, add the npm package, and open with what the CLI is for instead of calling it a work in progress. The pinned --version example is wrapped in release-please block markers so it bumps with every release like the ones in install.sh and install.ps1. It needs the block form rather than an inline annotation because the line sits inside a fenced code block, where the comment would render as part of the snippet. --- README.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f49c99d..5207790 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Flagsmith CLI -The next-generation Flagsmith command-line interface (work in progress). +The Flagsmith command-line interface helps you manage and roll out your feature flags without leaving your terminal. ## Install @@ -11,28 +11,38 @@ brew install Flagsmith/tap/flagsmith Or: ```sh -curl -fsSL https://raw.githubusercontent.com/Flagsmith/flagsmith-cli/main/install.sh | sh +curl -fsSL https://get.flagsmith.com | sh ``` Installs to `$HOME/.local/bin` and adds it to your `PATH`. Options: + + ```sh -curl -fsSL https://raw.githubusercontent.com/Flagsmith/flagsmith-cli/main/install.sh | sh -s -- --version v2.0.0 --bin-dir /usr/local/bin --no-modify-path -curl -fsSL https://raw.githubusercontent.com/Flagsmith/flagsmith-cli/main/install.sh | sh -s -- --help +curl -fsSL https://get.flagsmith.com | sh -s -- --version v2.0.0-beta.3 --bin-dir /usr/local/bin --no-modify-path +curl -fsSL https://get.flagsmith.com | sh -s -- --help ``` + + `FLAGSMITH_CLI_VERSION`, `FLAGSMITH_INSTALL_DIR` and `FLAGSMITH_NO_MODIFY_PATH` do the same if exported first. To pin the installer itself, fetch it at a commit you trust: `raw.githubusercontent.com/Flagsmith/flagsmith-cli//install.sh`. -Alternatively, `go install github.com/Flagsmith/flagsmith-cli/v2@v2.0.0-beta.3` (installs as `flagsmith-cli`), or grab an archive from [Releases](https://github.com/Flagsmith/flagsmith-cli/releases). - On Windows: ```powershell irm https://raw.githubusercontent.com/Flagsmith/flagsmith-cli/main/install.ps1 | iex ``` +Using npm: + +```sh +npm install -g @flagsmith/cli +``` + +Alternatively, `go install github.com/Flagsmith/flagsmith-cli/v2@v2.0.0-beta.3` (installs as `flagsmith-cli`), or grab an archive from [Releases](https://github.com/Flagsmith/flagsmith-cli/releases). + ## Build ```sh @@ -56,7 +66,7 @@ flagsmith flag list # list the flags in the current environment ## Commands -Full reference for every command and flag: (generated from `--help`, so it matches the release). +Full reference for every command and flag: . Reading works against any Flagsmith instance. Changing flags — `flag update`, `flag enable`/`disable`, `flag reorder`, `flag delete` — needs Flagsmith 2.263.0 or newer, self-hosted or SaaS. From 10aafeb146fc6a25e4cb83e6561f7c84b0580c2a Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 20 Aug 2026 18:09:23 +0100 Subject: [PATCH 3/5] fix(ci): Grant the docs build job Pages read permission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/configure-pages reads the site's base URL from the Pages API on every run, so with only contents: read the build job would have failed a tagged deployment with "Resource not accessible by integration". Read is all it needs: the action's enablement input defaults to false in v6, so it never tries to create the site — Pages still has to be pointed at Actions by hand. Deployment keeps pages: write. --- .github/workflows/docs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 805ebb4..5ee8dd5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,6 +19,12 @@ concurrency: jobs: build: runs-on: ubuntu-latest + permissions: + contents: read + # configure-pages reads the site's base URL from the Pages API. It never + # creates the site: enablement defaults to false, so Pages has to be set + # to build from Actions by hand, and read is all this job needs. + pages: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: From 939b5b99d207d03ec18c5adc1d4a8091c9944dc9 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 21 Aug 2026 10:40:46 +0100 Subject: [PATCH 4/5] deslop --- website/README.md | 45 ++++++++------------------------------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/website/README.md b/website/README.md index 9f0c5f0..39f5eda 100644 --- a/website/README.md +++ b/website/README.md @@ -1,22 +1,8 @@ # Command reference site -The [Hugo](https://gohugo.io) site published to - by -[`.github/workflows/docs.yml`](../.github/workflows/docs.yml) on every release -tag. - -The site *is* the command reference: all of `content/` is generated from the -CLI's own help text by [`cmd/docgen`](../cmd/docgen) and is gitignored. Nothing -about a command is written here by hand — fix the `Short`, `Long` or `Example` -on the command itself in `internal/cmd/` and the page follows. There is -deliberately no landing page: installing and getting started are documented in -the [README](../README.md#install) and on -[docs.flagsmith.com](https://docs.flagsmith.com/integrating-with-flagsmith/CLI), -and a third copy here would be the one that goes stale. - -The command tree becomes a directory tree, so `flagsmith flag update` is served -at `/flag/update/`, the `flagsmith` root command is the home page, and the -sidebar nests the way the CLI does. +The [Hugo](https://gohugo.io) site published to on every release. + +All of `content/` is generated from the CLI's own help text by [`cmd/docgen`](../cmd/docgen) and is gitignored. ## Preview locally @@ -32,24 +18,9 @@ Then open . Re-run `docgen` after changing help text. ## What is hand-written -| Path | Purpose | -| --- | --- | -| `hugo.yaml` | Site config, navigation, theme | -| `layouts/_partials/navbar-title.html` | Theme override: version badge | +| Path | Purpose | +| -------------------------------------- | ---------------------------------- | +| `hugo.yaml` | Site config, navigation, theme | +| `layouts/_partials/navbar-title.html` | Theme override: version badge | | `layouts/_partials/custom/footer.html` | Names the release being documented | -| `layouts/home.html` | Theme override, see below | - -Only the newest release is published: Pages serves a single artifact, so there -are no `/vX.Y.Z/` URLs. The version is therefore shown twice — as a badge beside -the navbar title, where it is seen without scrolling, and once in the footer with -the context that explains it. Both read `params.version` in `hugo.yaml`, which -release-please bumps in its release pull request (`website/hugo.yaml` is one of -its `extra-files`), so a plain `hugo server` shows exactly what a deploy would. - -Hextra's home layout hides the sidebar, which would leave the front page — the -root command's own page — with no navigation at all. `layouts/home.html` is the -theme's ordinary reference-page layout, copied so the home page gets the command -tree too. Re-check it against the theme when bumping Hextra. - -`go.mod` here is a Hugo module file and is intentionally separate from the CLI's -own `go.mod`, so the theme never becomes a dependency of the released binary. +| `layouts/home.html` | Theme override | From 3b2e95a82c8cb942c669682af0caabfb1debda78 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 21 Aug 2026 10:41:19 +0100 Subject: [PATCH 5/5] feat(ci): Attach the command reference to each release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The website only ever documents the newest release, so anyone staying on an older version has nothing to read. Attach the generated markdown to the release itself, which is also the copy to reach for offline. The build job already generates those pages, so it hands them to the archive job as an artifact and nothing is built or checked out twice. It uploads that artifact on every run, not only on tags, so a dispatched run leaves the pages somewhere they can be inspected. Archiving happens on the far side of the handover because an artifact is not a release asset and its zip needs a token to fetch, so the tarball has to be made somewhere; doing it there also avoids wrapping a tarball inside the artifact's own zip. Attaching stays a separate job because it needs a write token, and the build job runs the Hugo theme — third-party template code should not share a job with a token that can push to the repository. Only tag_name and files are passed to action-gh-release, which leaves the body, draft and prerelease fields of an existing release alone — release-please owns the notes. --- .github/workflows/docs.yml | 39 ++++++++++++++++++++++++++++---------- README.md | 2 +- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5ee8dd5..66c1440 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,8 +1,5 @@ name: Docs -# The published reference must describe a version people can install, so it is -# built from release tags rather than from main. workflow_dispatch is here for -# fixing the site itself (theme, landing page) without cutting a release. on: push: tags: ["v*"] @@ -11,7 +8,7 @@ on: permissions: contents: read -# One deployment at a time: GitHub Pages rejects concurrent deploys. +# GitHub Pages rejects concurrent deploys. concurrency: group: pages cancel-in-progress: false @@ -21,10 +18,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - # configure-pages reads the site's base URL from the Pages API. It never - # creates the site: enablement defaults to false, so Pages has to be set - # to build from Actions by hand, and read is all this job needs. - pages: read + pages: read # read the site's base URL from the Pages API steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -43,12 +37,17 @@ jobs: run: go run ./cmd/docgen -out website/content - name: Build site working-directory: website - # configure-pages reports the correct base URL, which differs between a - # project site (/flagsmith-cli/) and a custom domain. run: hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/" - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: website/public + # The pages the site was just built from, for the archive job to attach. + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: reference + path: website/content + if-no-files-found: error + retention-days: 1 deploy: needs: build @@ -62,3 +61,23 @@ jobs: steps: - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 id: deployment + + # The site only ever documents the newest release, so attach a copy to the + # release itself for anyone staying on an older version. + archive: + needs: build + if: github.ref_type == 'tag' + runs-on: ubuntu-latest + permissions: + contents: write # upload a release asset + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: reference + path: reference + - run: tar -czf "flagsmith_${GITHUB_REF_NAME}_reference.tar.gz" reference + - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 + with: + tag_name: ${{ github.ref_name }} + files: flagsmith_*_reference.tar.gz + fail_on_unmatched_files: true diff --git a/README.md b/README.md index 5207790..d756ad2 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ flagsmith flag list # list the flags in the current environment Full reference for every command and flag: . -Reading works against any Flagsmith instance. Changing flags — `flag update`, `flag enable`/`disable`, `flag reorder`, `flag delete` — needs Flagsmith 2.263.0 or newer, self-hosted or SaaS. +Reading works against any Flagsmith instance. Changing flags — `flag update`, `flag enable`/`disable`, `flag reorder`, `flag delete` — needs Flagsmith 2.263.0 or newer. - `flagsmith init` — bind the current directory to a project (writes `flagsmith.json`). - `flagsmith flag list` — list feature flags in the current environment.