diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..32e7905 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,129 @@ +--- +name: release +description: Release workflow for docs-theme-extras — the consumer repos that pin the module and get bumped on every release, how beta tags are cut and pushed (the one push Claude may do itself), and the CHANGELOG section ordering. Use when cutting a beta tag, releasing a version, bumping the extras pin in consumer repos, or writing a changelog entry. +--- + +# docs-theme-extras release workflow + +This skill documents the recurring release chores for `docs-theme-extras`: cutting +beta tags, bumping the module pin across the consumer repos, and ordering the +CHANGELOG. All repos below are sibling clones under `~/Documents/GitHub`. + +## Consumer repos that pin the module + +These repos import `github.com/solo-io/docs-theme-extras` and get their pin bumped +on essentially every release. Update **all six** together unless told otherwise. + +| Nickname | +|----------| +| agw oss | +| agr oss | +| ambientmesh | +| docs (Solo.io hub) | +| kgw oss | +| kagent oss (note the `docs-site/` subdir, not repo root) | + +The pin appears as `require github.com/solo-io/docs-theme-extras vX.Y.Z // indirect`. +Consumer bumps are deliberate, one version at a time — never a floating ref. + +### Bumping the pin + +Run from inside each consumer repo, using the version-pinned Hugo binary +(`hugo160`, not bare `hugo`): + +```bash +hugo160 mod get github.com/solo-io/docs-theme-extras@vX.Y.Z +``` + +This updates both `go.mod` and `go.sum` with the correct hash. Then verify: + +```bash +grep "docs-theme-extras vX.Y.Z " go.mod +grep "docs-theme-extras vX.Y.Z h1:" go.sum # hash should match across all repos +``` + +Notes: +- `hugo mod get` leaves the **old version's lines in `go.sum`**. They're harmless + (go.sum keeps historical hashes) and the build resolves fine. If a clean go.sum + matters, remove the two stale `vX.Y.(Z-1)` lines **by hand** — do NOT reach for + `tidy`. +- **Never run `hugo160 mod tidy` (or `go mod tidy`) to prune the pin.** In the + `docs` hub these modules (`docs-theme-extras`, `kgateway.dev`, `ambientmesh.io`) + are declared as **Hugo module imports in the hugo config, not Go source + imports**, so the Go tooling treats them as unused and `tidy` **deletes the + entire `require` block** — not just churn, it drops all three pins and breaks the + build. Same trap applies to any consumer whose extras pin is `// indirect`. If + you run it by reflex, `git checkout go.mod go.sum` and redo with `hugo mod get` + only. +- Leave the changes local. Do **not** commit or push consumer-repo changes + automatically — that's a user action (see the push rule below). + +## Beta tags (and the one push Claude may do itself) + +Tag convention: `vX.Y.Z-beta.N`. Betas increment `N` (`-beta.1`, `-beta.2`, …); +the final release drops the suffix (`vX.Y.Z`). Both beta and final tags are cut +the same way, from the current release branch (e.g. `release-next`). + +**Pushing a tag is the ONLY push Claude is allowed to do on its own.** Everything +else (branch commits, `git push` of branches, opening PRs) waits for an explicit +user instruction — see `feedback_no_auto_push`. + +Steps: + +```bash +# 1. Confirm the working tree is clean and the branch is in sync with origin +git status -sb # want a clean tree, no ahead/behind on the branch + +# 2. Pick the next tag by looking at existing tags +git tag --sort=-creatordate | head + +# 3. Create an annotated tag on HEAD and push just that tag +git tag -a vX.Y.Z-beta.N -m "vX.Y.Z-beta.N" +git push origin vX.Y.Z-beta.N +``` + +There is no `make` release target; tagging is manual. After the tag lands on +GitHub, the Go module proxy can fetch it, and the consumer-repo bump above will +resolve. + +## CHANGELOG ordering and entry rules + +`CHANGELOG.md` lists **newest version first** (`## [vX.Y.Z] — YYYY-MM-DD`). + +### Section order within a version + +Order the `###` sections by type, most disruptive first: + +1. **Breaking** — anything that requires content edits in consumer repos +2. **Add** / **Feature** — new shortcode, partial, or capability +3. **Fix** — non-breaking fix +4. **Test harness** / chore — last + +(Recent versions drifted into author/commit order — e.g. v0.1.22 lists Fixes +before its Add. Re-sort to the order above when editing; don't copy the drift.) + +### Writing an entry + +The rules are also stated at the top of `CHANGELOG.md` — follow them: + +- **Lead with *why*** — the bug, missing behavior, or failure mode — not just + what changed, so a reader gets the motivation without the diff. +- **Link a production page** that shows the bug or the fix. For additive features + with no single defect page, link a representative page where the new behavior is + observable and say how to verify it (view-source, a validator, etc.). + This link is required on every entry (see `feedback_changelog_production_link`). +- **State how it was verified** — the local build, the Playwright spec added, the + fixture exercised. +- Note that the change takes effect **when a consumer bumps its extras pin** (or, + for the `docs` hub where a local override shadows a module file, that a pin bump + alone won't change hub output). + +## Typical end-to-end flow + +1. Land the change on the release branch; write the CHANGELOG entry (correct + section order + production link + verification). +2. Cut a beta tag (`vX.Y.Z-beta.N`) and push it — allowed. +3. Bump the beta pin in the six consumer repos to smoke-test. +4. When ready, cut the final `vX.Y.Z` tag and push it — allowed. +5. Bump the six consumer repos to the final `vX.Y.Z`. Leave those edits local for + the user to commit/PR. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 865b2dd..a674daf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,21 @@ on: branches: [main] workflow_dispatch: +# Supersede in-progress runs on the same PR: a new push to a PR branch cancels +# its own older, still-running run instead of stacking a second one. Keyed on +# github.ref so each PR cancels only its own runs, never another PR's. A hung +# run (see the install-step timeout below) is likewise cleared by the next push +# rather than lingering to the job timeout. +# +# Deliberately NOT applied to push:main. Cancellation there is keyed on the +# branch, not the commit, so two merges in quick succession would cancel the +# first one's run — leaving a merged commit on main with no completed sweep. +# Main is also the only place some coverage is guaranteed to have run, so its +# runs must be allowed to finish. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read @@ -20,10 +35,36 @@ env: NODE_VERSION: '20' jobs: - test-all: + brand: runs-on: ubuntu-latest timeout-minutes: 15 + # The two brand layers are independent, so run them as parallel matrix + # jobs instead of sequentially in one `make test-all`. Wall-clock ~halves + # at the cost of a second runner. fail-fast off so an OSS failure doesn't + # cancel the enterprise job (and vice versa) — you want both reports. + strategy: + fail-fast: false + matrix: + brand: [oss, enterprise] + + env: + # All three engines run on PRs, not just on main. cross-browser.spec.ts is + # a single 11-test file, the binaries come from the actions/cache below, so + # the marginal cost next to a ~1600-test suite is small — and a PR is where + # someone actually reads a failure. Restricting it to push:main would make + # main the only place an engine-specific regression can surface, i.e. after + # merge. If PR wall-clock needs trimming later, take it from the install + # step, not from dropping engines. + PW_BROWSERS: 'chromium firefox webkit' + # ubuntu-latest is a 4-core runner for public repos. The brand matrix above + # already halves wall-clock by giving each brand its own runner; also + # raising the worker count oversubscribes those cores, and firefox is the + # engine that shows it first (observed locally as page.goto timeouts on the + # everything page under high parallelism, clean at low). Keep this at or + # below the core count. + PW_WORKERS: '4' + steps: - uses: actions/checkout@v7 @@ -59,6 +100,10 @@ jobs: - name: Install npm dependencies run: npm ci || npm install + # Every run installs the same three engines now, so the key needs no + # browser-set component (the PW_BROWSER_TAG split existed only to keep a + # PR's chromium-only cache from being restored by a main run that needed + # all three). - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v6 @@ -66,26 +111,32 @@ jobs: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} + # timeout-minutes guards the `sudo apt-get` that --with-deps/install-deps + # runs: on a bad runner it can hang on a dpkg lock or a stalled mirror. + # The browser download + apt normally finishes in <1min, so 5 is generous + # — a genuine hang fails the step fast (and is re-runnable) instead of + # burning the whole 15-min job budget. - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' - run: npx playwright install --with-deps chromium firefox webkit + timeout-minutes: 5 + run: npx playwright install --with-deps ${{ env.PW_BROWSERS }} - name: Install Playwright OS deps (cache-hit path) if: steps.playwright-cache.outputs.cache-hit == 'true' - run: npx playwright install-deps + timeout-minutes: 5 + run: npx playwright install-deps ${{ env.PW_BROWSERS }} # Override HUGO=hugo160 (the local convention) with HUGO=hugo (what - # peaceiris/actions-hugo installs the binary as). Run both brand - # variants. test-all chains build-oss + test-oss + build-enterprise - # + test-enterprise. - - name: Run test-all - run: HUGO=hugo make test-all + # peaceiris/actions-hugo installs the binary as). Each matrix job runs a + # single brand: test- chains build- + test-. + - name: Run test-${{ matrix.brand }} + run: HUGO=hugo make test-${{ matrix.brand }} - name: Upload Playwright HTML report on failure if: failure() uses: actions/upload-artifact@v7 with: - name: playwright-report + name: playwright-report-${{ matrix.brand }} path: playwright-report retention-days: 7 @@ -93,9 +144,28 @@ jobs: if: failure() uses: actions/upload-artifact@v7 with: - name: hugo-build-logs + name: hugo-build-logs-${{ matrix.brand }} path: | .build-oss.log .build-enterprise.log retention-days: 7 if-no-files-found: ignore + + # Single stable status context for branch protection. The matrix above reports + # as "brand (oss)" / "brand (enterprise)", so requiring those directly would + # mean re-pointing branch protection every time the matrix changes shape. This + # job collapses them into one required check named `test-all` — the name the + # rule already used before the matrix split. + # + # `if: always()` so this runs even when a matrix leg fails; the step then + # asserts the aggregate result, and `cancelled`/`skipped` fail too (only a + # literal success passes). + test-all: + needs: [brand] + if: always() + runs-on: ubuntu-latest + steps: + - name: Assert every brand leg succeeded + run: | + echo "brand result: ${{ needs.brand.result }}" + [ "${{ needs.brand.result }}" = "success" ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fcaddf..bd21589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,82 @@ how to verify it, e.g. view-source or a validator). State how the change was ver --- +## [v0.1.25] — 2026-08-03 + +### Change — `docTabs` moves from a front-matter `tab` model to a content-directory (`id`) model (`layouts/_partials/docs-tabs.html`, `layouts/partials/sidebar.html`, `hugo-*.toml`, `USAGE.md`) + +- **Why.** The v0.1.21 `docTabs` prototype grouped pages by a front-matter `tab: ""` key, re-derived the grouping by filtering the whole tree on every render, matched the active tab by URL string (which drifts between the OSS, enterprise, and local-dev URL shapes), and — because a tab was a virtual grouping, not a real container — linked each tab to its *first item's* page instead of a stable landing. +- **What changed.** A tab is now a real content directory: config declares each tab with an `id` (a top-level directory under the version root) plus its `name`, the sidebar roots the left-nav tree *inside* that directory, the active tab resolves by page ancestry (`.IsDescendant`) so every URL shape agrees, and each tab links to its directory `_index`. The enable gate becomes "2+ present tab directories in this version"; below that the band is suppressed and the full tree renders as before. Two supporting fixes: a tab holding only its `_index` now emits a link to its own landing (an empty pre-rendered panel made the mobile chip a dead end), opt-in per call site so non-tab sidebars are untouched; and the band centers its tab row in the same `utils/page-width` container as the content column, so tabs align with the sidebar/content left edge at every `page.width`. +- **Consumer contract change (opt-in feature only).** `[[params.docTabs]]` now requires an `id` per tab, and enabling `docTabs` means partitioning that version's content into one top-level directory per tab; the `tab:` front-matter key is no longer read. **No production consumer has enabled `docTabs`**, so nothing in production changes — this supersedes the prototype before its first real use. `USAGE.md` is rewritten to the directory model. +- No production page — the feature stays available-but-dormant. Observable on the theme fixture: `make server-oss`, then compare `/test/v3/api/authentication/` (the `v3` version partitions into `documentation/`, `api/`, `changelog/`, so three tabs render and the nav is rooted in the active tab's directory) with `/test/main/everything/` (no tab dirs → no band, full tree). Verified with `hugo160` OSS + enterprise builds; the rewritten `tests/docs-tabs.spec.ts` pins both the enabled and disabled states plus the page-width wrapper, 9/9 on both brands. Takes effect when a consumer sets `params.docTabs` with `id`s, partitions its content, and bumps its extras pin. + +### Add — per-tab `hideSidebar` drops the left nav on a `docTabs` tab, desktop only (`layouts/_partials/docs-tabs.html`, `layouts/partials/sidebar.html`, `assets/css/docs-theme-extras.css`, `hugo-*.toml`, `tests/docs-tabs-sidebar.spec.ts`, `USAGE.md`) + +- **Why.** With `docTabs`, the left nav is rooted inside the active tab's directory — so a tab that owns a single page (a one-page changelog, one generated API reference) renders a one-item nav whose only link is the page the reader is already on, spending a 16rem column to say nothing. There was no way to say "this tab has no tree"; the choice was a full nav on every tab or no tabs at all. +- **What changed.** `[[params.docTabs]]` takes an optional `hideSidebar = true`. `docs-tabs.html` resolves it for the ACTIVE tab (off the present-tab list, so a tab whose directory is missing in this version can't suppress a nav for pages it doesn't own) and puts it on the page store; `sidebar.html` turns it into a `sidebar-desktop-hidden` class on the `