Skip to content

Harden the release procedure before 0.1.0 #402

Description

@Bronek

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 from release, 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:
gh api repos/libfn/functional/commits/<sha>/check-runs \
  --jq '.check_runs[] | select(.conclusion != "success") | .name + " " + .conclusion'

6. Provenance and release notes

  • 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)

Assisted-by: Claude:claude-opus-5

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

    documentationImprovements or additions to documentationrelease-0.1Planned for release 0.1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions