Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
91 changes: 64 additions & 27 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**

Expand Down Expand Up @@ -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.
Expand 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
Expand Down
34 changes: 34 additions & 0 deletions scripts/check-release.sh
Original file line number Diff line number Diff line change
@@ -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
Loading