Skip to content

release: one flow, from merge to published release - #1

Merged
MihaiNueleanu merged 2 commits into
mainfrom
chore/faster-ci
Oct 26, 2026
Merged

MihaiNueleanu merged 2 commits into
mainfrom
chore/faster-ci

Conversation

@MihaiNueleanu

@MihaiNueleanu MihaiNueleanu commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

One pipeline: merge to main → checks → version from conventional commits → release build → GitHub release with the binary attached and notes generated.

ci.yaml is deleted; release.yaml replaces it. Pull requests run the checks only.

How a release happens

  1. check: fmt, clippy -D warnings, tests (--locked).
  2. release (main only): reads the current version from Cargo.toml, then .github/scripts/next-release.py looks at the commit subjects since the last v* tag and decides the bump.
  3. The version is written into Cargo.toml before the build, so binary and tag agree.
  4. cargo build --release, packaged as kubetitan-linux-x86_64.tar.gz with LICENSE, README.md and a SHA256SUMS.
  5. The bump is committed as chore(release): vX.Y.Z [skip ci] (the [skip ci] is what stops the push restarting the workflow), then gh release create tags that commit and attaches the archives.

Version rules (and why)

Commits since last tag Result
feat:, or feat!:, fix!: while 0.x minor bump (0.1.0 → 0.2.0)
fix:, perf:, anything unreleased patch bump (0.1.0 → 0.1.1)
only chore:, docs:, ci:, test: no release
no tag yet first release at the version in Cargo.toml

Pre-1.0, breaking and feature both bump the minor on purpose: 0.x promises no compatibility, so burning 1.0.0 in week two just means arriving at 7.0.0 with no users.

Verified by rehearsal, not by reading YAML

  • Nine version cases in throwaway repos, all passing: no tags, chores-only, fix, feat, breaking pre-1.0, breaking at 1.x, feat at 1.x, fix at 1.x, mixed unreleased.
  • Notes grouping checked on a mixed release (Features, Fixes, Chores).
  • The packaging steps run against a stub binary: archive contains kubetitan, LICENSE, README.md; SHA256SUMS matches.

Scope note

Crate renamed kubetitan-mvp → kubetitan, because the artifact would otherwise be a binary called kubetitan-mvp and "mvp" is not a promise you want in a download.

Linux only for now: macOS runners cost 10× minutes on a private repo, and Windows needs config.rs fixed first (it falls back to $HOME/.config, unset there, so state would land in the working directory). Adding a platform is one matrix line once that is done.

The first run took over 25 minutes and the release build was most of it, still
going when clippy (2.5 min) and tests (8.7 min) had finished.

Two reasons, both by design and both wrong for a pull request:

- `[profile.release]` sets `lto = "thin"` and `codegen-units = 1`. One codegen
  unit means the compiler cannot split a crate into parallel units, and thin LTO
  then analyses the whole crate graph at link time. That is the correct trade for
  a binary people download; it is 13 minutes for a smoke check.
- Three compiles of the same dependencies in one job: clippy needs them for
  metadata, tests need a debug build, and release needs a third, entirely
  separate set of artifacts. Nothing is shared between profiles.

So CI keeps fmt, clippy and tests, and the release build moves to where it
belongs: a release workflow on a tag, which can afford to be slow. The debug
build that replaces it costs seconds after `cargo test` and still catches a
binary that does not link.

Also `--locked` (no silent lockfile or registry drift mid-run) and
`save-if: main` so pull requests read the cache instead of each writing one.
Replaces the separate check-only workflow with a single pipeline: on every push
to main the checks run, the next version is worked out from the conventional
commit messages since the last tag, that version is built, and a GitHub release
is created with the binary attached and notes grouped by commit type. Pull
requests run the checks and nothing else.

Decisions worth spelling out:

- **Version from commit messages, not a bot PR.** Squash merging keeps the PR
  title as the commit subject, so `feat:`, `fix:` and `feat!:` are already the
  history. `.github/scripts/next-release.py` reads them and applies the rules:
  pre-1.0 a breaking change and a feature both bump the minor (0.x promises no
  compatibility, so 1.0.0 on week two is a trap), fixes bump the patch, and a
  merge that only contains chores or docs produces no release at all.
- **The bump is committed with `[skip ci]`.** The tag has to agree with
  Cargo.toml, so the version is written in, committed and pushed; `[skip ci]`
  stops that push from starting the workflow again.
- **The version is written before the build**, so the binary and the tag cannot
  disagree.
- **Linux only for now.** macOS runners cost ten times the minutes on a private
  repository and Windows would need `config.rs` fixed first: it falls back to
  `$HOME/.config`, which is unset there, so state would land in the current
  directory. Adding a platform is one matrix line when those are sorted.
- **`--locked` stays on the checks**, but the release build runs unlocked: the
  version bump rewrites the crate's own entry in Cargo.lock.

Also renames the crate from `kubetitan-mvp` to `kubetitan`. The artifact is
`kubetitan-linux-x86_64.tar.gz` containing a binary called `kubetitan`, and
"mvp" in a released binary name is a promise nobody wants to keep.

Verified by rehearsal, not by reading the YAML: nine version cases pass
(`release-tests.sh`), the notes group correctly, and the packaging steps run
against a stub binary producing the archive and its SHA256SUMS.
@MihaiNueleanu MihaiNueleanu changed the title ci: stop building release binaries on every push release: one flow, from merge to published release Oct 26, 2026
@MihaiNueleanu

Copy link
Copy Markdown
Contributor Author

Verification on the renamed crate, locally, with the same flags CI uses:

cargo check --locked --all-targets   → Finished in 1m 38s   (Checking kubetitan v0.1.0)
cargo test  --locked                 → 51 passed, 0 failed

So --locked accepts the updated Cargo.lock after the rename, and the binary the packaging step copies is target/release/kubetitan.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant