diff --git a/CLAUDE.md b/CLAUDE.md index a3ffbce..40f249f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,7 +87,8 @@ Module `github.com/mozilla/markfluence` (`go 1.25`). `main.go` is a shim to `cmd - `internal/testenv` — `RunIsolated`, which every package whose code calls `client.Resolve` (and `internal/client` itself) calls from `TestMain`: it points `HOME`/`XDG_CONFIG_HOME` at an empty temp directory and unsets `CONFLUENCE_*`, so a developer's real credentials file or an exported cloud ID cannot reach a test. A package of its own, importing nothing from this module, because `internal/client`'s tests are `package client` and `internal/clienttest` imports `client`; it repeats the four variable names for the same reason. Called for packages whose tests do not reach `Resolve` yet, so a later test cannot forget it. - `internal/schematest` — the `--json` drift guard, and the reason the schema can't fall behind the code. `ValidateEnvelope`/`ValidateError` validate an emitted document against the embedded schema **closed in memory** (`closeObjects`), which sets `additionalProperties: false` on every node declaring `"type": "object"` with `properties` — the published file is open for consumers, and the guard needs the opposite. The `type` test is what keeps it off the envelope's `if`/`then` branches, which declare none: a closed `then` would forbid every envelope key but `results`/`summary`. Three tests keep this honest: `TestPublishedSchemaIsOpen` keeps the file open, `TestEveryPropertiesNodeIsTyped` keeps every other node listing properties typed (an untyped one would silently escape the guard), and the walker skips instance data (`const`, `enum`...) and steps *through* name-keyed maps (`properties`, `$defs`) — `infoResult` has a property literally named `properties`; `document.go` checks the schema *document* instead (`Commands`, plus tests that every name in the `command` enum has an `if/then` branch that constrains `results.items` **and** `summary`). That last one matters because outside a branch the schema says only "results is an array": a command added to the enum without a branch is completely unvalidated, and adding just the enum entry is exactly how a new command's conformance test goes green. `cmd`'s `TestCommandEnumMatchesRegisteredCommands` closes the loop from the other side — every registered subcommand is in the enum or in that test's `noJSONEnvelope` list. Two rules keep all this working: **every result field lives on a typed struct and nothing uses `omitempty`** (so every field always marshals and the closed schema/`required` catch an added, renamed, or removed one no matter what a fixture sets — never build a result as a `map[string]any`), and **a conformance test builds its document with the command's own builder** (`failEnvelope`, `jsonResult`) rather than a hand-copied literal, or it validates a copy while the real output drifts. - `internal/completion` — the shell-completion functions the commands share (`MarkdownFiles` for a FILE/PAGE argument, `PageThenFiles`/`PageThenNames`, `Directories`, `Values`, and `RegisterFlag`). Cobra's built-in `completion` command generates the scripts; this only decides what they offer. Nothing here may call Confluence — completion runs on every keystroke — so a server-side value (an attachment name) completes to nothing instead of stalling the shell. A subcommand with no `ValidArgsFunction` fails `TestSubcommandsCompleteArgs` in `cmd`. -- `tools/gendocs` — the only thing outside `cmd`/`internal`/`schema`: a `main` that renders `docs/commands/`. Not built into the binary and not run by it; see **Documentation** above. +- `tools/gendocs` — the only Go code outside `cmd`/`internal`/`schema`: a `main` that renders `docs/commands/`. Not built into the binary and not run by it; see **Documentation** above. +- `scripts/` — shell scripts a maintainer runs by hand, never by `make` or CI. `check-release.sh vX.Y.Z` is step 6 of [docs/releasing.md](docs/releasing.md): it downloads the published `darwin_arm64` archive, verifies it against `checksums.txt`, and fails unless the binary in it reports the tag's version. Not in `bin/`, which is gitignored and which `make clean` deletes. - `internal/buildinfo` — `Version` (set via ldflags), `CommitDate` (from the `vcs.time` build setting), and `Stamp`. - `internal/ui` — lipgloss output helpers (colored `Header`/`Success`/`Warn`/`Error`, errors to stderr, `NO_COLOR`/piped detection) and the `ErrSilent` sentinel. `Info` writes to stdout and `Hint` to stderr, which is the whole difference between them: a hint explains output that may be on its way into a pipe. `InfoStderr`/`SuccessStderr` are `Info`/`Success` on stderr, for `credentials-init`, whose whole output is a conversation beside prompts on stderr. Every helper is a no-op under `--json` — the content belongs in the payload instead — and `Debug` is the only exception, being flag-gated. That rule survived the permission warning (#136) rather than bending for it: the first attempt was a JSON-exempt `SecurityWarn`, on the false premise that stderr is not part of the JSON contract. It is — `#/$defs/errorObject` is published, and `cmd/children`'s own test validates *all* of stderr as one document — so a human line printed ahead of it breaks a consumer the schema invites. The warning travels in `jsonout`'s top-level `warnings` array instead, and `cmd/root.go`'s `reportSecurityWarning` feeds both paths at once. diff --git a/docs/releasing.md b/docs/releasing.md index 8193356..26440c9 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -56,10 +56,11 @@ tells you which place: about what the configuration makes. Thus do the rehearsal, and not that check. - Then run `rm -rf dist completions`. git ignores both, but stale copies are - confusing. Run `git status` too. If `go mod tidy` changed `go.mod` or - `go.sum`, the tree was not tidy: commit that change in a normal pull - request, and do not release until it merges. + Then run `make clean`. + + Run `git status` too. If `go mod tidy` changed `go.mod` or `go.sum`, the + tree was not tidy: commit that change in a normal pull request, and do not + release until it merges. 3. (Laptop) **Make the tag and push it.** @@ -109,27 +110,21 @@ tells you which place: still gives the earlier version. 6. (Laptop) **Make sure that the correct thing shipped.** Download a real - archive and run the binary from it. Run this from the root of the - markfluence git repository. These commands are for macOS: + archive and run the binary from it. The script is for macOS on Apple + silicon: ```sh - # create a temp dir, download the release, untar it, check the version - mkdir tmp - pushd tmp - gh release download v1.2.3 -p 'markfluence_*_darwin_arm64.tar.gz' -p checksums.txt && - shasum -a 256 --check --ignore-missing checksums.txt && - tar -xzf markfluence_*_darwin_arm64.tar.gz markfluence && - ./markfluence --version - - # --- verify the version --- - - # clean up - popd - rm -rf tmp + scripts/check-release.sh v1.2.3 ``` - It must print the version of the tag with no leading `v`, for example - `markfluence 1.2.3 (...)`. Run `./markfluence`, and not `markfluence`. The + The script downloads the `darwin_arm64` archive and `checksums.txt` into a + temporary directory, and verifies the archive against the checksum. It then + runs the binary from the archive, and makes sure that it prints the version + of the tag with no leading `v`, for example `markfluence 1.2.3 (...)`. It + prints `OK: v1.2.3` and exits 0 only if all of that is correct. It removes + the temporary directory in all cases. + + The script runs the binary from the archive, and not `markfluence`. The plain `markfluence` runs the binary on your `PATH`. For a maintainer, that is usually the `make install` build with the stamp `dev`, so it does not test the release at all. @@ -142,15 +137,57 @@ tells you which place: "$(brew --prefix)/bin/markfluence" --version ``` -7. (Laptop) **Read the published release notes, and correct them if they are - bad.** +7. (Laptop) **Write the release notes.** goreleaser publishes a list of + commits. Replace it with release notes that a user can read. You can give + this prompt to Claude Code, from the root of the repository. It finds the + release itself, so you can paste it as it is: + + ```text + Write the release notes for the latest release of markfluence. + + 1. Run git fetch --tags. Find the tag of the latest published release: + gh release view --json tagName --jq .tagName + Call it TAG, and the tag before it (git describe --tags --abbrev=0 + TAG^) PREVIOUS. Print both, so that I can see which release the notes + are for. + 2. Download the published notes of TAG into notes.md: + gh release view TAG --json body --jq .body > notes.md + They are goreleaser's list of commits since PREVIOUS. + 3. Read the full message of each commit in PREVIOUS..TAG, and the issues + and pull requests that they name. Use gh for the issues and pull + requests. + 4. Replace the contents of notes.md with release notes in this structure. + Leave out a section that has no entries: + + ## What's new + ### Features + ### Bugs fixed + ## Backwards incompatible changes + ## Security issues fixed + + - One bullet for each change that a user can see, as a high-level + summary. Put the issue number as a link at the end of the bullet. + - Combine the commits of one change into one bullet. Leave out changes + that only affect documentation, tests, plans, or the build, unless a + user sees the result, for example a platform that is no longer built. + - For each backwards incompatible change, say what a user must do. If + the change needs more than one sentence of instructions, add a + subsection under "Backwards incompatible changes" for it. + - Make sure that each statement is true for this release: read the code + and the docs, and use `git tag --contains` to find the release that + first had a change. + - Do not wrap paragraphs. In GitHub release notes, a newline is a line + break. + - End with: **Full changelog:** + https://github.com/mozilla/markfluence/compare/PREVIOUS...TAG + 5. Do not publish the notes. I will edit notes.md. + ``` + + Read `notes.md`, and correct it. Then publish it, and delete the file: ```sh - gh release view v1.2.3 - # if it reads badly: - gh release view v1.2.3 --json body --jq .body > notes.md - # ...edit notes.md... gh release edit v1.2.3 --notes-file notes.md + rm notes.md ``` GitHub Releases supports full GFM, but a newline is a line break. Do not diff --git a/scripts/check-release.sh b/scripts/check-release.sh new file mode 100755 index 0000000..10c2dc8 --- /dev/null +++ b/scripts/check-release.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Check a published release (docs/releasing.md, step 6): download the +# darwin/arm64 archive, verify it against checksums.txt, and check that the +# binary in it reports the tag's version. +# +# Usage: scripts/check-release.sh vX.Y.Z +set -eu + +if [ $# -ne 1 ]; then + echo "usage: $0 vX.Y.Z" >&2 + exit 2 +fi +tag=$1 +case $tag in + v[0-9]*.[0-9]*.[0-9]*) ;; + *) echo "$0: $tag is not a vX.Y.Z tag" >&2; exit 2 ;; +esac + +dir=$(mktemp -d) +trap 'rm -rf "$dir"' EXIT +cd "$dir" + +gh release download "$tag" -R mozilla/markfluence \ + -p 'markfluence_*_darwin_arm64.tar.gz' -p checksums.txt +shasum -a 256 --check --ignore-missing checksums.txt +tar -xzf markfluence_*_darwin_arm64.tar.gz markfluence + +# ./markfluence, not markfluence: the one on PATH is usually a dev build. +out=$(./markfluence --version) +echo "$out" +case $out in + "markfluence ${tag#v} "*) echo "OK: $tag" ;; + *) echo "$0: expected markfluence ${tag#v}, got: $out" >&2; exit 1 ;; +esac