From 70700d6559b5de00aff33c90ceb725649b53f92f Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:20:34 +0530 Subject: [PATCH] ci(release): require release tags to match the crate version Package versions come from the release tag. Reject any tag other than v, a plain MAJOR.MINOR.PATCH, before packaging starts; prerelease suffixes are not valid in every package format. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 2 +- AGENTS.md | 2 +- xtask/src/cli.rs | 44 +++++++++++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ade7e81..1682a83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Release -# Tag-driven. Push a tag like `v1.2.3` to build per-OS/arch binaries and attach them +# Tag-driven. Bump apps/backend/Cargo.toml, then push the matching tag (e.g. `v1.2.3`) to build per-OS/arch binaries and attach them # to a GitHub Release. The release binary embeds apps/dashboard/dist via rust-embed, # so the dashboard MUST be built before `cargo build --release`. # diff --git a/AGENTS.md b/AGENTS.md index 5d74540..38773fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,7 +102,7 @@ The server applies migrations on every startup; there is no separate migrate com - Keep the release workflow thin: build the dashboard, run native build/package jobs, assemble artifacts, attest, and publish. - Ordinary CI validates packaging templates, `xtask` tests, and deterministic dry-runs. It must not install or mutate host services. - Release artifacts include portable archives, Debian, RPM, MSI, and macOS PKG packages. Arch is maintained as a package recipe consuming published Linux archives; render it with `xtask arch-render`, not as a fake release archive. -- The package version comes from the release tag with its leading `v` removed. Package formats differ in what they accept, so check prerelease versions against each format before tagging one. +- Release tags are `v` plus the `apps/backend/Cargo.toml` version, a plain `MAJOR.MINOR.PATCH` (no prerelease suffix). Bump `Cargo.toml` first; `xtask` rejects any other tag before packaging starts. - The stable native service identifier is `vcms`; its human-facing display name is **Velopulent CMS**. Fresh Linux and macOS package installs register/enable the service but do not auto-start it; the Windows MSI installs the service for automatic startup and starts it immediately. ## Configuration diff --git a/xtask/src/cli.rs b/xtask/src/cli.rs index 99f26a8..152074f 100644 --- a/xtask/src/cli.rs +++ b/xtask/src/cli.rs @@ -76,10 +76,7 @@ impl Args { let mut output = Self { command, kind: PackageKind::Host, - version: match env::var("GITHUB_REF_NAME").ok() { - Some(value) => value.strip_prefix('v').unwrap_or(&value).to_owned(), - None => backend_version()?, - }, + version: default_version()?, target_os: TargetOs::parse(env::consts::OS)?, arch: Architecture::parse(env::consts::ARCH)?, dry_run: false, @@ -118,6 +115,36 @@ fn need_value(name: &str, value: Option) -> Result { value.ok_or_else(|| format!("{name} needs a value").into()) } +/// Version to package when `--version` is not given: the release tag on tag +/// builds, otherwise the backend crate version. +fn default_version() -> Result { + let crate_version = backend_version()?; + if env::var("GITHUB_REF_TYPE").as_deref() == Ok("tag") { + let tag = env::var("GITHUB_REF_NAME").map_err(|_| "GITHUB_REF_NAME is not set for a tag build")?; + return release_version(&tag, &crate_version); + } + Ok(crate_version) +} + +/// A release tag must be `v` followed by the backend crate version, which must +/// be a plain `MAJOR.MINOR.PATCH`: every package format accepts that form. +fn release_version(tag: &str, crate_version: &str) -> Result { + let plain = |version: &str| { + let parts = version.split('.').collect::>(); + parts.len() == 3 + && parts + .iter() + .all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit())) + }; + if !plain(crate_version) { + return Err(format!("apps/backend/Cargo.toml version {crate_version} is not MAJOR.MINOR.PATCH").into()); + } + match tag.strip_prefix('v') { + Some(version) if version == crate_version => Ok(version.to_owned()), + _ => Err(format!("release tag {tag} must be v{crate_version} to match apps/backend/Cargo.toml").into()), + } +} + fn backend_version() -> Result { let root = package::repo_root()?; let cargo = fs::read_to_string(root.join("apps/backend/Cargo.toml"))?; @@ -153,4 +180,13 @@ mod tests { assert_eq!(args.arch, Architecture::Arm64); assert!(args.dry_run); } + + #[test] + fn release_tags_must_match_the_plain_crate_version() { + assert_eq!(release_version("v0.1.0", "0.1.0").unwrap(), "0.1.0"); + for tag in ["0.1.0", "v0.1.1", "v0.1.0-beta.1"] { + assert!(release_version(tag, "0.1.0").is_err(), "{tag}"); + } + assert!(release_version("v0.1.0-beta.1", "0.1.0-beta.1").is_err()); + } }