Skip to content

Commit 2cb6ec7

Browse files
feat(release): controlled release script + CI test gate
- scripts/release.sh: interactive release — runs fmt/clippy/full tests locally, prompts major/minor/patch (or current-as-is when untagged), bumps the workspace version + Cargo.lock, promotes CHANGELOG [Unreleased] -> version, shows the diff to confirm, commits + annotated tag, then asks to push. The site (site/package.json) is intentionally not version-bumped. - release.yml: add a test job (fmt + clippy + cargo test --workspace) and gate build/publish on it — tests must pass before artifacts publish - site: nav version chip now reads the runtime version from the workspace Cargo.toml at build time (vite define), not site/package.json
1 parent a5a42e5 commit 2cb6ec7

4 files changed

Lines changed: 172 additions & 5 deletions

File tree

.github/workflows/release.yml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Release artifacts for the `esrun` CLI.
22
#
3-
# Triggered by pushing a semver tag (e.g. `v0.1.0`). Builds the standalone
4-
# binary per target, packages it (tar.gz on Unix, zip on Windows) with a
5-
# SHA-256 checksum, and publishes them all to a GitHub Release.
3+
# Triggered by pushing a semver tag (e.g. `v0.1.0`). Runs the workspace tests
4+
# first; only if they pass does it build the standalone binary per target,
5+
# package it (tar.gz on Unix, zip on Windows) with a SHA-256 checksum, and
6+
# publish them all to a GitHub Release.
67
#
78
# The `v8` crate downloads a prebuilt static library at build time, so each
89
# runner needs network access. The toolchain is pinned by rust-toolchain.toml.
@@ -24,8 +25,22 @@ env:
2425
RUST_BACKTRACE: 1
2526

2627
jobs:
28+
test:
29+
name: test
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
- uses: Swatinem/rust-cache@v2
34+
- name: Format
35+
run: cargo fmt --all --check
36+
- name: Clippy
37+
run: cargo clippy --workspace --all-targets --locked -- -D warnings
38+
- name: Tests
39+
run: cargo test --workspace --locked
40+
2741
build:
2842
name: build (${{ matrix.target }})
43+
needs: test
2944
runs-on: ${{ matrix.os }}
3045
strategy:
3146
fail-fast: false

scripts/release.sh

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Controlled release for ES-Runtime.
4+
#
5+
# scripts/release.sh
6+
#
7+
# Asks for a major / minor / patch bump, then:
8+
# 1. bumps the version (workspace Cargo.toml + Cargo.lock). The site
9+
# (site/package.json) is versioned independently — docs change far more
10+
# often than the runtime — so it is intentionally left alone.
11+
# 2. promotes CHANGELOG.md's [Unreleased] section to the new version + date,
12+
# 3. shows the diff and asks you to confirm,
13+
# 4. commits + creates an annotated git tag,
14+
# 5. asks whether to push the branch and tag.
15+
#
16+
# Pushing the tag triggers .github/workflows/release.yml, which runs the tests
17+
# and only then builds and publishes the artifacts. Nothing here is automatic —
18+
# every step waits for you.
19+
set -euo pipefail
20+
21+
ROOT="$(git rev-parse --show-toplevel)"
22+
cd "$ROOT"
23+
24+
CARGO_TOML="Cargo.toml"
25+
CHANGELOG="CHANGELOG.md"
26+
27+
red() { printf '\033[31m%s\033[0m\n' "$*" >&2; }
28+
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
29+
err() { red "error: $*"; exit 1; }
30+
31+
# --- preconditions ----------------------------------------------------------
32+
[ -f "$CARGO_TOML" ] || err "must run inside the ES-Runtime repo"
33+
git diff --quiet && git diff --cached --quiet || err "working tree is dirty — commit or stash first"
34+
35+
# --- local quality gate (before touching anything) -------------------------
36+
# CI re-runs all of this after the tag is pushed, but verify locally first so a
37+
# release is never started on code that doesn't format, lint, or test clean.
38+
# `cargo test --workspace` includes the CLI end-to-end test
39+
# (crates/runtime-cli/tests) that spawns the real `esrun` binary.
40+
# Reuses the existing target/ cache (no clean) — incremental on a warm tree.
41+
# A cold tree is slow once (downloads/links the prebuilt V8 static lib).
42+
bold "Running fmt, clippy, and the full test suite…"
43+
cargo fmt --all --check || err "formatting check failed — run 'cargo fmt' and retry"
44+
cargo clippy --workspace --all-targets --locked -- -D warnings ||
45+
err "clippy failed — fix the warnings and retry"
46+
cargo test --workspace --locked || err "tests failed — fix them and retry"
47+
48+
branch="$(git rev-parse --abbrev-ref HEAD)"
49+
50+
# Current version = the workspace.package.version (first `version = "x"` line).
51+
current="$(grep -m1 -E '^version = "' "$CARGO_TOML" | sed -E 's/^version = "([^"]+)".*/\1/')"
52+
[ -n "$current" ] || err "could not read the workspace version from $CARGO_TOML"
53+
IFS=. read -r MA MI PA <<<"${current%%-*}"
54+
55+
bold "ES-Runtime release"
56+
echo " current version: $current"
57+
echo " branch: $branch"
58+
echo
59+
60+
# --- choose the bump --------------------------------------------------------
61+
patch="$MA.$MI.$((PA + 1))"
62+
minor="$MA.$((MI + 1)).0"
63+
major="$((MA + 1)).0.0"
64+
65+
echo "Release type:"
66+
echo " 1) patch -> $patch"
67+
echo " 2) minor -> $minor"
68+
echo " 3) major -> $major"
69+
asis=""
70+
if ! git rev-parse -q --verify "refs/tags/v$current" >/dev/null; then
71+
asis="$current"
72+
echo " 0) current -> $current (release the current version as-is; not yet tagged)"
73+
fi
74+
printf "Choice: "
75+
read -r choice
76+
case "$choice" in
77+
1) new="$patch" ;;
78+
2) new="$minor" ;;
79+
3) new="$major" ;;
80+
0) [ -n "$asis" ] && new="$asis" || err "invalid choice" ;;
81+
*) err "invalid choice" ;;
82+
esac
83+
84+
tag="v$new"
85+
git rev-parse -q --verify "refs/tags/$tag" >/dev/null && err "tag $tag already exists"
86+
date="$(date -u +%Y-%m-%d)"
87+
echo
88+
bold "Preparing $tag ($date)"
89+
90+
# --- 1. bump versions -------------------------------------------------------
91+
# workspace.package.version — the first `version = "..."` line in the root manifest.
92+
NEW="$new" perl -0777 -i -pe 'BEGIN{$v=$ENV{NEW}} s/^(version = ")[^"]+(")/${1}$v${2}/m' "$CARGO_TOML"
93+
# Cargo.lock — refresh only the workspace members' recorded versions (no build).
94+
cargo update --workspace --offline >/dev/null 2>&1 ||
95+
cargo update --workspace >/dev/null 2>&1 ||
96+
err "could not update Cargo.lock (run 'cargo update --workspace' and retry)"
97+
98+
# --- 2. changelog: promote [Unreleased] -> [new] - date, keep a fresh one ---
99+
grep -q '^## \[Unreleased\]' "$CHANGELOG" || err "no '## [Unreleased]' section in $CHANGELOG"
100+
NEW="$new" DATE="$date" perl -0777 -i -pe \
101+
'BEGIN{$v=$ENV{NEW};$d=$ENV{DATE}} s/^## \[Unreleased\]/## [Unreleased]\n\n## [$v] - $d/m' \
102+
"$CHANGELOG"
103+
104+
# --- 3. review + confirm ----------------------------------------------------
105+
echo
106+
bold "Changes for $tag:"
107+
git --no-pager diff -- "$CARGO_TOML" "$CHANGELOG"
108+
echo
109+
printf "Commit these and tag %s? [y/N] " "$tag"
110+
read -r confirm
111+
case "$confirm" in
112+
y | Y) ;;
113+
*)
114+
bold "Aborted — reverting changes."
115+
git checkout -- "$CARGO_TOML" "$CHANGELOG" Cargo.lock 2>/dev/null || true
116+
exit 1
117+
;;
118+
esac
119+
120+
# --- 4. commit + tag --------------------------------------------------------
121+
git add "$CARGO_TOML" "$CHANGELOG" Cargo.lock
122+
git commit -m "release: $tag"
123+
git tag -a "$tag" -m "$tag"
124+
bold "Committed and tagged $tag."
125+
126+
# --- 5. push ----------------------------------------------------------------
127+
echo
128+
printf "Push '%s' and tag '%s' to origin now? [y/N] " "$branch" "$tag"
129+
read -r dopush
130+
case "$dopush" in
131+
y | Y)
132+
git push origin "$branch" "$tag"
133+
bold "Pushed. The Release workflow will run tests, build, and publish $tag."
134+
;;
135+
*)
136+
bold "Not pushed. When ready:"
137+
echo " git push origin $branch $tag"
138+
;;
139+
esac

site/components/Nav.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { router } from "@opentf/web/router";
2-
import pkg from "../package.json";
32

43
const GITHUB = "https://github.com/Open-Tech-Foundation/ES-Runtime";
54

5+
// Injected from the workspace Cargo.toml at build time (see vite.config.js) —
6+
// the released runtime version, not this site's package.json.
7+
const VERSION = __RUNTIME_VERSION__;
8+
69
const LINKS = [
710
{ href: "/", label: "Home" },
811
{ href: "/docs", label: "Docs" },
@@ -26,7 +29,7 @@ export default function Nav() {
2629
ES <span className="text-brand-600">Runtime</span>
2730
</span>
2831
<span className="rounded-full border border-zinc-200 px-2 py-0.5 text-[11px] font-medium tabular-nums text-zinc-500">
29-
v{pkg.version}
32+
v{VERSION}
3033
</span>
3134
</a>
3235

site/vite.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import { defineConfig } from 'vite'
22
import { babel } from '@rollup/plugin-babel'
33
import tailwindcss from '@tailwindcss/vite'
4+
import { readFileSync } from 'node:fs'
5+
6+
// The displayed runtime version comes from the workspace Cargo.toml (the single
7+
// source of truth, bumped by scripts/release.sh). This site's own package.json
8+
// version is unrelated — docs change far more often than the runtime.
9+
const cargoToml = readFileSync(new URL('../Cargo.toml', import.meta.url), 'utf8')
10+
const runtimeVersion = cargoToml.match(/^version = "([^"]+)"/m)?.[1] ?? '0.0.0'
411

512
export default defineConfig({
13+
define: {
14+
__RUNTIME_VERSION__: JSON.stringify(runtimeVersion),
15+
},
616
esbuild: {
717
jsx: 'preserve'
818
},

0 commit comments

Comments
 (0)