You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tagging v0.1.0-rc1 exercised the release-branch flow end to end for the first time. It worked,
but only after clearing two repository settings, and it surfaced a set of gaps worth closing
before 0.1.0 final. Collected here as one checklist.
Fixed during rc1 — recorded for context, no action
github-pages environment: release added to the deployment-branch policy (previously main
and feature/doxygen only). Without it actions/deploy-pages rejects the deploy regardless of
what the workflow says.
release protection: cleared Require deployments to succeed before merging
(github-pages). Under the pre-Open the 0.1 release cycle at 0.1.0-rc1 #398 model the site deployed from main, so the rule read
"only content already published may enter release" — coherent. Open the 0.1 release cycle at 0.1.0-rc1 #398 inverted the arrow: the
site now deploys fromrelease, so no commit can be deployed until it is on release, and
none can enter release until deployed. A closed loop; the branch was unadvanceable by any
route. Note this setting is invisible in the REST branch-protection payload — only GraphQL
(requiresDeployments) exposes it.
release protection: cleared Require a pull request before merging. With linear history
required, GitHub offers only squash and rebase, and both mint new SHAs — a release PR would
orphan the tag and pin merge-base(main, release) permanently, so every later release PR would
render the full cumulative diff.
release advanced by fast-forward onto the tagged main commit rather than by a merge commit.
The tag is therefore reachable from both branches and git describe resolves on main and on
pull requests, not only on release.
1. Version cadence — open the next cycle immediately after tagging
Highest priority: the window is open right now.
VERSION feeds CMakeLists.txt, conanfile.py, MODULE.bazel, ports/libfn/vcpkg.json and include/libfn_version.hpp, where it becomes LIBFN_VERSION — the inline namespace. Between a
tag and the next bump, every main commit builds headers spelling the released namespace while
containing unreleased content. Two such builds link cleanly and are ODR-incompatible: the failure
mode is silent, which is the class of hazard the contributor guidance asks us to challenge.
Bump VERSION and include/libfn_version.hpp together in the commit immediately after the tag
Make that ordering explicit in the documented procedure, so it cannot drift later
2. Workflow hardening
Drop the paths: filter from the release push trigger in docs.yml. Path filters evaluate
the push's before..after diff; a release whose delta happens to miss the list would silently
not deploy. A release merge should always redeploy.
fetch-depth: 0 in docs.yml (currently fetch-tags: true at default depth 1). Exact-match git describe works only on the tagged commit itself; once release moves past the tag the
banner loses its -N-g<sha> form. single-header.yml already uses full depth.
Add concurrency: { group: pages, cancel-in-progress: false } to docs.yml — two release
pushes can currently race the Pages deploy.
Declare environment: { name: github-pages, url: ... } on deploy_docs. Surfaces the live
URL in the UI and turns a branch-policy rejection into a legible gate rather than a
mid-action error.
3. Packaging — the vcpkg port unblocks with the first tag
ports/libfn/portfile.cmake carries a TODO to replace the overlay-only path with vcpkg_from_github(... REF "v${VERSION}" SHA512 <pinned>) once a release is tagged.
Fill in the port
Decide what the SHA512 pins. GitHub's auto-generated tag archives are produced on demand; the
2023 compression change invalidated pinned hashes ecosystem-wide. Release assets are
immutable, so attaching our own tarball and pinning that is the cheaper hedge — single-header.yml's publish job already holds contents: write.
4. Docs versioning — decide while there is exactly one version
The site deploys to the root, so both the API reference and https://libfn.org/libfn.hpp mean
"latest release". When 0.2 ships, 0.1's documentation disappears. Cheap to decide now, expensive
once URLs are bookmarked.
Choose: version-scoped docs (/v0.1/ plus a latest alias) or deliberate single-version docs
Document the two distribution channels and their differing contracts — the moving https://libfn.org/libfn.hpp versus the immutable libfn-<tag>.hpp release asset — so
consumers pick deliberately
5. Gate the release on the candidate commit being green
docs.yml builds and runs ctest as a guardrail, but under a single compiler from the docs image.
The real breadth — build across the matrix, the four package-test-* lanes, licence — runs on main, and nothing verifies it passed before release advances.
Add a pre-tag check; empty output means safe to tag:
actions/attest-build-provenance on single-header.yml's publish job. For a library
distributed as one copy-pasteable header, signed provenance is unusually meaningful for the
effort (needs attestations: write, id-token: write).
Use gh release create --generate-notes for the merged-PR list and pair it with the
CHANGELOG section, rather than hand-writing both.
7. Protect release tags — after 0.1.0 ships, not before
There is no tag ruleset today, which is exactly why rc1 could be re-tagged when the first attempt
put it on a merge commit that linear history would never accept.
Once the procedure has proven itself, add a ruleset for v* blocking deletion and update, so
a shipped tag cannot move under a consumer who pinned it. Adding it removes the escape hatch
rc1 depended on, so it belongs after the release, not before.
8. Document the procedure
Nothing in CONTRIBUTING.md describes releasing today, and the sequence has at least three
non-obvious steps whose failure modes are silent.
Add a release section covering the sequence below and, for each step, why it is that way:
tag before pushing (the docs build derives the banner from git describe, so a tag created
afterwards never reaches the published artifact); --atomic so both refs land before any
workflow starts; fast-forward only (linear history is enforced); the environment policy; and
the check-runs gate.
# 0. verify the candidate commit is green (section 5)
git checkout release
git merge --ff-only <main-sha>
git tag -s v<version> -m 'libfn <version>'<main-sha>
git push --atomic origin release:release refs/tags/v<version># once docs is green, create the Release object — this is what fires# single-header.yml's publish job and attaches libfn-v<version>.hpp
gh release create v<version> --verify-tag --notes-file <notes># then immediately open the next cycle (section 1)
Tagging
v0.1.0-rc1exercised the release-branch flow end to end for the first time. It worked,but only after clearing two repository settings, and it surfaced a set of gaps worth closing
before 0.1.0 final. Collected here as one checklist.
Fixed during rc1 — recorded for context, no action
github-pagesenvironment:releaseadded to the deployment-branch policy (previouslymainand
feature/doxygenonly). Without itactions/deploy-pagesrejects the deploy regardless ofwhat the workflow says.
releaseprotection: cleared Require deployments to succeed before merging(
github-pages). Under the pre-Open the 0.1 release cycle at 0.1.0-rc1 #398 model the site deployed frommain, so the rule read"only content already published may enter release" — coherent. Open the 0.1 release cycle at 0.1.0-rc1 #398 inverted the arrow: the
site now deploys from
release, so no commit can be deployed until it is onrelease, andnone can enter
releaseuntil deployed. A closed loop; the branch was unadvanceable by anyroute. Note this setting is invisible in the REST branch-protection payload — only GraphQL
(
requiresDeployments) exposes it.releaseprotection: cleared Require a pull request before merging. With linear historyrequired, GitHub offers only squash and rebase, and both mint new SHAs — a release PR would
orphan the tag and pin
merge-base(main, release)permanently, so every later release PR wouldrender the full cumulative diff.
releaseadvanced by fast-forward onto the taggedmaincommit rather than by a merge commit.The tag is therefore reachable from both branches and
git describeresolves onmainand onpull requests, not only on
release.1. Version cadence — open the next cycle immediately after tagging
Highest priority: the window is open right now.
VERSIONfeedsCMakeLists.txt,conanfile.py,MODULE.bazel,ports/libfn/vcpkg.jsonandinclude/libfn_version.hpp, where it becomesLIBFN_VERSION— the inline namespace. Between atag and the next bump, every
maincommit builds headers spelling the released namespace whilecontaining unreleased content. Two such builds link cleanly and are ODR-incompatible: the failure
mode is silent, which is the class of hazard the contributor guidance asks us to challenge.
VERSIONandinclude/libfn_version.hpptogether in the commit immediately after the tag2. Workflow hardening
paths:filter from thereleasepush trigger indocs.yml. Path filters evaluatethe push's
before..afterdiff; a release whose delta happens to miss the list would silentlynot deploy. A release merge should always redeploy.
fetch-depth: 0indocs.yml(currentlyfetch-tags: trueat default depth 1). Exact-matchgit describeworks only on the tagged commit itself; oncereleasemoves past the tag thebanner loses its
-N-g<sha>form.single-header.ymlalready uses full depth.concurrency: { group: pages, cancel-in-progress: false }todocs.yml— two releasepushes can currently race the Pages deploy.
environment: { name: github-pages, url: ... }ondeploy_docs. Surfaces the liveURL in the UI and turns a branch-policy rejection into a legible gate rather than a
mid-action error.
3. Packaging — the vcpkg port unblocks with the first tag
ports/libfn/portfile.cmakecarries a TODO to replace the overlay-only path withvcpkg_from_github(... REF "v${VERSION}" SHA512 <pinned>)once a release is tagged.2023 compression change invalidated pinned hashes ecosystem-wide. Release assets are
immutable, so attaching our own tarball and pinning that is the cheaper hedge —
single-header.yml's publish job already holdscontents: write.4. Docs versioning — decide while there is exactly one version
The site deploys to the root, so both the API reference and
https://libfn.org/libfn.hppmean"latest release". When 0.2 ships, 0.1's documentation disappears. Cheap to decide now, expensive
once URLs are bookmarked.
/v0.1/plus alatestalias) or deliberate single-version docshttps://libfn.org/libfn.hppversus the immutablelibfn-<tag>.hpprelease asset — soconsumers pick deliberately
5. Gate the release on the candidate commit being green
docs.ymlbuilds and runs ctest as a guardrail, but under a single compiler from the docs image.The real breadth —
buildacross the matrix, the fourpackage-test-*lanes,licence— runs onmain, and nothing verifies it passed beforereleaseadvances.6. Provenance and release notes
actions/attest-build-provenanceonsingle-header.yml's publish job. For a librarydistributed as one copy-pasteable header, signed provenance is unusually meaningful for the
effort (needs
attestations: write,id-token: write).gh release create --generate-notesfor the merged-PR list and pair it with theCHANGELOG section, rather than hand-writing both.
7. Protect release tags — after 0.1.0 ships, not before
There is no tag ruleset today, which is exactly why rc1 could be re-tagged when the first attempt
put it on a merge commit that linear history would never accept.
v*blocking deletion and update, soa shipped tag cannot move under a consumer who pinned it. Adding it removes the escape hatch
rc1 depended on, so it belongs after the release, not before.
8. Document the procedure
Nothing in
CONTRIBUTING.mddescribes releasing today, and the sequence has at least threenon-obvious steps whose failure modes are silent.
tag before pushing (the docs build derives the banner from
git describe, so a tag createdafterwards never reaches the published artifact);
--atomicso both refs land before anyworkflow starts; fast-forward only (linear history is enforced); the environment policy; and
the check-runs gate.
Assisted-by: Claude:claude-opus-5