From f9d2a95d806a3c6b2a686196c33853292bda2089 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Fri, 25 Sep 2026 18:06:57 -0400 Subject: [PATCH 1/2] chore: a script for checking a published release Step 6 of docs/releasing.md was a block of commands pasted into a shell. It did not compare the version it printed, exited 0 when a step failed, checked the latest release when the tag was left off, and made and deleted a tmp/ directory in the working tree. scripts/check-release.sh does the same check and fails unless it passes: it requires a vX.Y.Z tag, works in a temporary directory it always removes, verifies the archive against checksums.txt, and compares the binary's version with the tag. Step 2's cleanup is now make clean. --- CLAUDE.md | 3 ++- docs/releasing.md | 37 ++++++++++++++++--------------------- scripts/check-release.sh | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 22 deletions(-) create mode 100755 scripts/check-release.sh 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..22b4e4a 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. 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 From 052a21a27db96c17eb71ec0a89514e266eeb2f70 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Fri, 25 Sep 2026 18:19:30 -0400 Subject: [PATCH 2/2] docs(releasing): a prompt that drafts the release notes goreleaser publishes a list of commits as the release notes. Step 7 now carries a prompt for Claude Code that finds the latest published release and the tag before it, reads the commits and the issues they name, and writes notes.md in the structure v0.2.0's notes settled on: What's new (features, bugs fixed), backwards incompatible changes with what a user must do, and security issues fixed. It checks each claim against the release, and leaves the notes unpublished for editing. --- docs/releasing.md | 54 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index 22b4e4a..26440c9 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -137,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