Rayforce uses a tag-driven release flow. The git tag is the single source of truth for the version — no version literal is ever hand-edited in source.
dev— integration upstream and the repository default branch. Feature/fix branches merge here; all contributions targetdev(see CONTRIBUTING.md). CI runs on every push and PR.master— protected, stable releases only. The only way in is a merged release PR. A PR opened againstmasterthat isn't thedev -> masterrelease PR is automatically moved todevby the Retarget stray PRs workflow.
- Make sure
devis green and ready. - Open a pull request
dev→masterwith the title set to the new version:vX.Y.Z(e.g.v2.1.3).- The Version Guard check validates the title: it must be a clean
vX.Y.Z, must not already be tagged, and must be strictly greater than the latest release.
- The Version Guard check validates the title: it must be a clean
- Once CI and Version Guard are green, merge the PR (or enable auto-merge so it merges itself when checks pass).
- On merge, the Release workflow automatically:
- creates the tag
vX.Y.Zat the merge commit, - builds the optimized binary for Linux and macOS with the version injected
at compile time (
make dist RAY_VERSION=X.Y.Z), - packages
rayforce-X.Y.Z-<os>-<arch>.tar.gz+ a.sha256checksum, - publishes a GitHub Release with a feature-oriented changelog and the artifacts.
- creates the tag
That's the whole ritual. Never edit the version in source to make a release — the tag is authoritative.
The Release workflow also has a workflow_dispatch trigger. Run it from the
Actions tab (or gh workflow run release.yml -f version=X.Y.Z) to re-drive a
release that half-finished, or to seed the very first release before the
automatic pull_request: closed path can fire. It is idempotent: if a draft
vX.Y.Z already exists it is reused and built from its own target commit; a
manual dispatch with no prior draft tags the current master HEAD.
Why the build checks out a commit, not the tag: a draft GitHub release does not create its git tag — the tag ref only appears when the release is published (
--draft=false). Sobuildchecks out the release's target commit; thepublishjob is what creates the tagvX.Y.Z. Checking outref: vX.Y.Zduring build would fail (the tag does not exist yet).
Notes are a feature changelog, not a contributor ledger. The prepare job
runs .github/release-notes.sh, which groups
conventional-commit subjects since the previous release tag into sections —
✨ Features (feat), 🐛 Bug fixes (fix), ⚡ Performance (perf),
📚 Documentation (docs) — with everything else collapsed under
Maintenance & internal. Author names and merge-commit noise are dropped; a
Full changelog compare link at the bottom is the exhaustive per-commit view.
Because notes are scoped to previousTag..thisRelease, they only read well once
there is a previous release tag to diff against — the first release (v2.1.0)
spans the entire v2 line and was given a hand-written highlights summary instead.
The Makefile resolves the version (CI override RAY_VERSION= > latest git tag >
0.0.0 dev default) and injects it at compile time via -D, exactly like it
already does for the git commit and build date. ray_version_string(), the REPL
banner, and .sys.build all report that value. The literals in
include/rayforce.h are only #ifndef-guarded 0.0.0 fallbacks for untagged
builds and for downstream code that includes the header without our -D flags.
A plain local make on an untagged commit reports 0.0.0; with tags present it
reports the latest via git describe. To build a specific version locally:
make dist RAY_VERSION=2.1.3- Default branch =
dev: sogit cloneand the "New pull request" button steer contributors at the integration branch, notmaster. This is the primary nudge; Version Guard is the backstop, and Retarget stray PRs (.github/workflows/retarget-prs.yml) catches the few that still land onmasterand moves them todev. Because it is apull_request*workflow, it only governsmasteronce the file is present onmaster— i.e. after the first release PR that carries it merges. - Branch protection / ruleset on
master: require a pull request before merging; require these two status checks to pass, and disallow direct pushes:ci-success— a single aggregator job inci.ymlthat goes green only when every CI matrix leg (ubuntu/macOS × debug/release) passed. Require this one instead of the four matrix names so renaming the matrix can't silently drop the gate.check-version— the Version Guard job that validates the release version in the PR title.
- No release secret or bypass token is needed — the default
GITHUB_TOKEN(contents: write) creates the tag and the release. The release workflow never pushes a commit tomaster, only a tag ref. - Optional — Zulip announcement: add a repository secret
ZULIP_API_KEY(the API key of thereleases-bot@rayforcedb.zulipchat.combot). Theannouncejob then posts each published release — title + changelog highlights + link — to #Announcements under the Rayforce topic. Without the secret the job is skipped, and a Zulip error never fails the release (continue-on-error). If Zulip rejects the post, the bot may need to be a Generic bot (not just an incoming-webhook) and subscribed to the Announcements channel. - Optional — Homebrew tap: create an empty repo
RayforceDB/homebrew-tapand add a repository secretHOMEBREW_TAP_TOKEN(a PAT — fine-grained or classic — with contents: write onhomebrew-tap; the defaultGITHUB_TOKENcan't push to a different repo). Thehomebrewjob then rewritesFormula/rayforce.rbin the tap on every release. Without the secret the job is skipped. Users install withbrew install rayforcedb/tap/rayforce.
Each release publishes, in addition to the source:
- Tarballs —
rayforce-X.Y.Z-linux-x86_64.tar.gzand…-darwin-arm64.tar.gz(+.sha256). The Linux one is portable (RAY_MARCH=x86-64-v3, AVX2/~2013+) so it can't SIGILL on a different/older CPU. macOS stays-march=native: the runner is the oldest Apple-Silicon class, so it's already a safe floor (arm64 has no x86-style optional-ISA traps, and baselining toarmv8-awould only cost M1 tuning). .deb(rayforce_X.Y.Z_amd64.deb) — the same portable Linux binary, packaged with nfpm (packaging/nfpm.yaml). No setup (usesGITHUB_TOKEN).- Homebrew (
rayforcedb/tap) — a build-from-source formula (packaging/homebrew-formula.rb.tmpl), auto-bumped by thehomebrewjob. Compiling on the user's machine means all Mac arches work and there's no redistribution-portability footgun.
Want maximum per-CPU performance instead of a portable binary? Build from source —
makeand Homebrew both default to-march=native. Only the distributed x86-64 artifacts use thex86-64-v3baseline.
Linux and macOS binaries are published today. Windows is not build-ready yet
(IOCP backend is a stub, main.c/heap.c have unguarded POSIX calls, and the
Makefile has no Windows toolchain path); once ported, add a windows-latest row
to the build matrix in .github/workflows/release.yml.