Skip to content

Establish the release process and cut v0.1.0 #175

Description

@willkg

Establish the release process, document it, and cut v0.1.0.

.goreleaser.yaml is fully configured and nothing has ever invoked it. There are no tags, no releases, and no release workflow — ci.yml is the only workflow in the repo. So the download path, the checksum file, the archive layout and the Homebrew cask are all things this project has never done once, and every one of them is a guess until a real tag has produced real assets.

This blocks #29 (ship markfluence as a GitHub Action), whose whole premise is fetching markfluence_<version>_<os>_<arch>.tar.gz from a release. It also unblocks the two release-shaped notes already in the tree: #1 (the Homebrew tap) and #95 (SECURITY.md's supported-versions table).

What the release process has to decide

The tag must be vX.Y.Z

Git does not care, but two things here do, and both are hard requirements rather than conventions:

  • Go modules. github.com/mozilla/markfluence is a module and docs/github-actions.md tells people to go install github.com/mozilla/markfluence@latest. The module proxy only recognises semver tags with the v prefix — a 1.2.3 or release-1 tag is invisible to it, so @v1.2.3 does not resolve and @latest degrades to a v0.0.0-2026…-<sha> pseudo-version.
  • goreleaser, which parses the tag and fails with not a valid semantic version otherwise.

A prerelease suffix is fine for both: v0.1.0-rc.1 is valid semver, valid for Go, and goreleaser marks the release a prerelease on its own. Worth knowing for a dry run against a throwaway tag.

Also worth writing down because it bites downstream: .Version strips the v, so tag v1.2.3 produces markfluence_1.2.3_linux_amd64.tar.gz. Anything that constructs an asset name from a tag has to account for it.

The trigger glob is v*.*.*, not v*

The obvious tags: ['v*'] will also match a moving major tag (v1), which #29 needs. That would re-enter the workflow on every release and fail the second run on a tag goreleaser rejects as non-semver. Actions tag filters are globs rather than regex, so v*.*.* is the available way to say "three components".

The Homebrew cask needs two permissions on the release job

Settled in #1: this repository is its own tap. The cask lands in ./Casks here rather than in a separate mozilla/homebrew-markfluence, which is how mozilla/mozcloud does it, and it matters for this issue because it decides the credential. GitHub Actions' automatic GITHUB_TOKEN is scoped to the repository the workflow runs in, so a separate tap repo would have needed a fine-grained PAT someone owns and rotates; a same-repo write needs nothing extra.

What release.yml has to grant is just contents: write — which the GitHub Release itself already needs. The cask is pushed to a goreleaser/cask-<tag> branch and the release stops there; opening the PR is a human step (gh pr create), because a PR opened by the automatic GITHUB_TOKEN never triggers ci and main's ruleset requires that check with nobody able to bypass it. #1 has the measurement.

So: no separate repo to create, no pull-requests: write, and nothing here blocks on it. The one thing this issue should carry is a line in docs/releasing.md saying that the cask PR is opened by hand after the release, or the first person to cut a release will assume it failed.

No changelog file is required

There is no CHANGELOG.md and none is needed: a Release body may be empty, and .goreleaser.yaml already sets changelog: {use: github, sort: asc}, so notes are generated from commit subjects — which Conventional Commits already makes readable here. Worth a deliberate decision rather than a default, since adding one later is cheap and removing one is not.

Which platforms ship

goreleaser builds darwin+linux × arm64+amd64 — four assets as of c03b1ff. darwin/amd64 had been in ignore on the grounds that the macos-13 runner is retiring, and that came back once the repo became its own Homebrew tap: the build matrix is then the install matrix, and the cask had no on_intel block, so brew install failed on an Intel Mac. Windows is still not built and is deliberately out of scope. Recorded here because #29 turns this into a support matrix too, so it is no longer a private goreleaser detail.

Work

  • .github/workflows/release.yml — done in build: the release workflow, and per-platform install instructions #177. Also runs make check against the tagged commit, since a tag can be pushed at any commit. zizmor clean: actions pinned by SHA, persist-credentials: false, and the module cache disabled (cache: false has to be explicit — setup-go caches by default, so dropping cache: true left the finding standing).
  • contents: write only, no pull-requests: write — build: the release workflow, and per-platform install instructions #177.
  • Decided: draft: true plus a publish-last step — build: the release workflow, and per-platform install instructions #177. And prerelease: auto, which turned out to be required for the draft to mean anything: release.prerelease defaults to false, so an RC tag produced a non-prerelease draft that the publish step then published unconditionally, making v0.1.0-rc.1 become /releases/latest. Easy to miss, because the cask is correctly skipped for an RC (skip_upload: "auto" reads the parsed semver rather than this setting), so a rehearsal would have looked right while publishing a live release.
  • Dry-run against a prerelease tag — not done, and deliberately not going to be. This project does not cut prereleases. docs/releasing.md no longer recommends an RC as the rehearsal; the local snapshot build is, and it publishes nothing. release.prerelease: auto and the cask's skip_upload: "auto" stay as one-line insurance against an RC tag nobody intends to push, marked in the config as never having run (build: drop Intel macOS, and stop recommending prereleases #180).
  • docs/releasing.md — done as a "Cutting a release" section at the end of CONTRIBUTING.md (1f7a037), rather than a file of its own: release steps are maintainer documentation and that is where a maintainer already looks. Covers the tag format and both reasons for it, the snapshot rehearsal, the manual cask PR and why, verification, and how to recover from a failed release. It carries an IMPORTANT admonition saying the process is not usable until this issue lands — remove that admonition as part of closing this issue.
  • Decided: no make target. docs/releasing.md is the checklist, and a target would wrap two commands.
  • v0.1.0 is out. Verified: three archives (darwin_arm64, linux_arm64, linux_amd64 — Intel macOS was dropped in build: drop Intel macOS, and stop recommending prereleases #180), checksums.txt verifying, completions/ inside the archive, and the shipped binary reporting markfluence 0.1.0 (1d0a2d0, 2026-09-20) from the ldflags stamp. The cask PR merged, and brew install markfluence works.
  • README.md — build: the release workflow, and per-platform install instructions #177 replaced it with per-platform instructions: macOS via the cask, Linux via a release archive (no Homebrew, since Cask is macOS-only — which also settles the unverified Linux-cask question by not depending on it), and go install. The Linux snippet was verified by running it against real goreleaser archives, which caught an unqualified tar -xzf that overwrote the user's own README.md and LICENSE.
  • SECURITY.md — build: the release workflow, and per-platform install instructions #177 states the pre-1.0 policy (latest release only, no backports). The table is still SECURITY.md: replace the no-releases-yet note with a supported-versions table #95.
  • CLAUDE.md — done in fix(docs): what cutting v0.1.0 turned up #179: the trigger glob, make check against the tagged commit, draft-then-publish, prerelease: auto being required rather than default, the contents: write-only permission, and docs/releasing.md as the runbook.

Why v0.1.0 and not 1.0.0

#29 is in the 1.0.0 milestone, so the action cannot wait for the release it is meant to ship with — and the release pipeline has never run, so the first tag should be the cheap one to re-cut while it is being debugged. v0.1.0 gives #29 real assets to build against, and pre-1.0 means re-tagging costs nothing if the first attempt is wrong.

markfluence is unreleased, so there is no backwards-compatibility or migration story to design for here.

Not in scope


Status after #177: the workflow, the config and the documentation are all on main. What remains is the part that can only be done by doing it — a prerelease rehearsal, then v0.1.0 — plus the CLAUDE.md note above, and deleting the two "no release yet" admonitions (README.md's Install section and docs/releasing.md's) once a release exists.

Closing

Done. v0.1.0 shipped and the whole path is exercised: tag → workflow → draft → assets → publish → cask branch → hand-opened PR → brew install.

Four things the first real release turned up, all fixed in #179 and #180:

  • brew trust is required before brew install from a non-official tap. Nothing in the goreleaser config could have revealed that.
  • The verify step downloaded into the repository root and left the files behind, and gh release download sat outside its && chain — so a failed download silently verified whatever was already there. That misfired during testing against a stale archive, printing a version that was right only by luck.
  • Release notes must not be hard-wrapped: GitHub renders newlines in a release body as line breaks.
  • Intel macOS was dropped (build: drop Intel macOS, and stop recommending prereleases #180). macOS 26 Tahoe is Apple's last Intel release, and nobody here can test on one, so the build matrix — which for a self-tapping repo is the install matrix — no longer claims it.

Left open elsewhere, not blocking this:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions