Skip to content

Commit 7d867b5

Browse files
committed
Add deterministic release automation
1 parent 8f814a5 commit 7d867b5

5 files changed

Lines changed: 289 additions & 32 deletions

File tree

.github/workflows/release.yml

Lines changed: 112 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,126 @@
11
name: Release
22

33
on:
4-
workflow_dispatch:
5-
inputs:
6-
tag:
7-
description: Release tag, for example v0.1.0
8-
required: true
4+
push:
5+
tags:
6+
- "v*"
97

10-
permissions:
11-
contents: write
8+
permissions: {}
9+
10+
concurrency:
11+
group: release-${{ github.ref }}
12+
cancel-in-progress: false
1213

1314
jobs:
14-
build:
15-
name: Build release artifacts
16-
runs-on: ubuntu-latest
15+
create-release:
16+
name: Create draft release
17+
runs-on: ubuntu-24.04
18+
permissions:
19+
contents: write
1720
steps:
1821
- name: Checkout
19-
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
22+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
2023
with:
2124
persist-credentials: false
22-
- name: Install Rust
23-
run: rustup toolchain install 1.85.0 --profile minimal
24-
- name: Test
25-
run: cargo +1.85.0 test --workspace --all-targets --all-features
26-
- name: Build
27-
run: cargo +1.85.0 build --release --locked
28-
- name: Checksums
29-
run: shasum -a 256 target/release/model-routing > SHA256SUMS
30-
- name: Upload draft release
25+
- name: Verify release contract
26+
env:
27+
TAG: ${{ github.ref_name }}
28+
run: |
29+
crate_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)"
30+
package_version="$(jq -r .version package.json)"
31+
test "v$crate_version" = "$TAG"
32+
test "$package_version" = "$crate_version"
33+
grep -q "^## \[$crate_version\]" CHANGELOG.md
34+
- name: Create draft release
3135
env:
3236
GH_TOKEN: ${{ github.token }}
33-
RELEASE_TAG: ${{ inputs.tag }}
37+
TAG: ${{ github.ref_name }}
3438
run: |
35-
gh release create "$RELEASE_TAG" \
39+
prerelease_flag=""
40+
case "$TAG" in *-*) prerelease_flag="--prerelease" ;; esac
41+
gh release create "$TAG" \
42+
--repo "$GITHUB_REPOSITORY" \
3643
--draft \
37-
--title "$RELEASE_TAG" \
38-
target/release/model-routing \
39-
SHA256SUMS
44+
$prerelease_flag \
45+
--title "Switchloom $TAG" \
46+
--generate-notes
47+
48+
build:
49+
name: Build ${{ matrix.target }}
50+
needs: create-release
51+
strategy:
52+
fail-fast: false
53+
matrix:
54+
include:
55+
- target: darwin-arm64
56+
rust_target: aarch64-apple-darwin
57+
runner: macos-14
58+
- target: darwin-x86_64
59+
rust_target: x86_64-apple-darwin
60+
runner: macos-14
61+
- target: linux-x86_64
62+
rust_target: x86_64-unknown-linux-gnu
63+
runner: ubuntu-24.04
64+
- target: linux-arm64
65+
rust_target: aarch64-unknown-linux-gnu
66+
runner: ubuntu-24.04-arm
67+
runs-on: ${{ matrix.runner }}
68+
permissions:
69+
contents: write
70+
steps:
71+
- name: Checkout
72+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
73+
with:
74+
persist-credentials: false
75+
- name: Install Rust target
76+
run: rustup target add ${{ matrix.rust_target }}
77+
- name: Build release archive
78+
env:
79+
SWITCHLOOM_TARGET: ${{ matrix.target }}
80+
SWITCHLOOM_CARGO_TARGET: ${{ matrix.rust_target }}
81+
run: scripts/build-release.sh
82+
- name: Smoke-test native binary
83+
if: matrix.target != 'darwin-x86_64'
84+
run: ./target/${{ matrix.rust_target }}/release/model-routing --version
85+
- name: Upload release archive
86+
env:
87+
GH_TOKEN: ${{ github.token }}
88+
TAG: ${{ github.ref_name }}
89+
run: |
90+
gh release upload "$TAG" \
91+
--repo "$GITHUB_REPOSITORY" \
92+
--clobber \
93+
"dist/switchloom-${{ matrix.target }}.tar.gz"
94+
95+
finalize:
96+
name: Publish checksums and release
97+
needs: build
98+
runs-on: ubuntu-24.04
99+
permissions:
100+
contents: write
101+
steps:
102+
- name: Download release archives
103+
env:
104+
GH_TOKEN: ${{ github.token }}
105+
TAG: ${{ github.ref_name }}
106+
run: |
107+
mkdir assets
108+
gh release download "$TAG" \
109+
--repo "$GITHUB_REPOSITORY" \
110+
--pattern 'switchloom-*.tar.gz' \
111+
--dir assets
112+
- name: Generate aggregate checksums
113+
run: |
114+
cd assets
115+
sha256sum switchloom-*.tar.gz > SHA256SUMS
116+
cat SHA256SUMS
117+
- name: Publish release
118+
env:
119+
GH_TOKEN: ${{ github.token }}
120+
TAG: ${{ github.ref_name }}
121+
run: |
122+
gh release upload "$TAG" \
123+
--repo "$GITHUB_REPOSITORY" \
124+
--clobber \
125+
assets/SHA256SUMS
126+
gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false

CHANGELOG.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22

33
All notable changes to Switchloom are recorded here.
44

5-
## Unreleased
5+
## [0.1.0] - 2026-07-17
66

7-
- Established the independent repository baseline, package metadata, CI/release scaffolding, local-state policy, and Planr v1.5.0 extraction inventory.
8-
- Transferred the public website, catalog regeneration, and Alchemy/Cloudflare test publication stack to standalone Switchloom with explicit optional Planr integration controls.
9-
10-
## 0.1.0
11-
12-
- Initial standalone release target.
7+
- Established the independent repository baseline, package metadata, CI and release automation, local-state policy, and Planr v1.5.0 extraction inventory.
8+
- Transferred the public website, catalog regeneration, and Alchemy/Cloudflare publication stack to standalone Switchloom with explicit optional Planr integration controls.
9+
- Added deterministic repository-safe lifecycle management, host bindings, signed catalog metadata, security guardrails, and release packaging.

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,18 @@ secret, vulnerability, and misconfiguration scan before publishing:
4949
pnpm hooks:install
5050
pnpm security:check
5151
```
52+
53+
## Releases
54+
55+
Releases are created only through the repository-owned script. Prepare and
56+
commit a bracketed changelog section such as `## [0.1.0]`, then run:
57+
58+
```sh
59+
RELEASE_DRY_RUN=1 scripts/release.sh 0.1.0 "Initial standalone release"
60+
scripts/release.sh 0.1.0 "Initial standalone release"
61+
```
62+
63+
The script requires a clean, synchronized `main`, runs the complete local
64+
quality and security gates, creates an annotated tag, and pushes it. The tag
65+
workflow builds macOS and Linux archives, publishes aggregate SHA-256
66+
checksums, and makes the GitHub release public only after every build succeeds.

scripts/build-release.sh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env sh
2+
set -eu
3+
4+
cd "$(dirname "$0")/.."
5+
6+
version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n 1)"
7+
8+
detect_target() {
9+
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
10+
arch="$(uname -m)"
11+
12+
case "$os" in
13+
darwin) os="darwin" ;;
14+
linux) os="linux" ;;
15+
*)
16+
echo "unsupported OS: $os" >&2
17+
exit 1
18+
;;
19+
esac
20+
21+
case "$arch" in
22+
arm64 | aarch64) arch="arm64" ;;
23+
x86_64 | amd64) arch="x86_64" ;;
24+
*)
25+
echo "unsupported architecture: $arch" >&2
26+
exit 1
27+
;;
28+
esac
29+
30+
echo "$os-$arch"
31+
}
32+
33+
sha256_tool() {
34+
if command -v shasum >/dev/null 2>&1; then
35+
shasum -a 256 "$@"
36+
elif command -v sha256sum >/dev/null 2>&1; then
37+
sha256sum "$@"
38+
else
39+
echo "shasum or sha256sum is required" >&2
40+
exit 1
41+
fi
42+
}
43+
44+
target="${SWITCHLOOM_TARGET:-$(detect_target)}"
45+
cargo_target="${SWITCHLOOM_CARGO_TARGET:-}"
46+
target_dir="dist/switchloom-$version"
47+
asset="switchloom-$target.tar.gz"
48+
49+
rm -rf "$target_dir" "dist/$asset"
50+
mkdir -p "$target_dir"
51+
52+
if [ -n "$cargo_target" ]; then
53+
cargo build --release --locked --target "$cargo_target"
54+
built_bin="target/$cargo_target/release/model-routing"
55+
else
56+
cargo build --release --locked
57+
built_bin="target/release/model-routing"
58+
fi
59+
60+
cp "$built_bin" "$target_dir/model-routing"
61+
cp README.md LICENSE "$target_dir/"
62+
63+
(
64+
cd "$target_dir"
65+
sha256_tool model-routing README.md LICENSE > SHA256SUMS
66+
)
67+
68+
(
69+
cd "$target_dir"
70+
tar -czf "../$asset" model-routing README.md LICENSE SHA256SUMS
71+
)
72+
73+
echo "release artifact: dist/$asset"

scripts/release.sh

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env sh
2+
# The supported release path. It validates one version across manifests, runs
3+
# all local release gates, commits a version bump when needed, tags, and pushes.
4+
# The tag-triggered GitHub workflow builds and publishes platform artifacts.
5+
#
6+
# Usage: scripts/release.sh <x.y.z[-alpha.N|-beta.N|-rc.N]> "release summary"
7+
# Dry run: RELEASE_DRY_RUN=1 scripts/release.sh <current-version> "summary"
8+
set -eu
9+
10+
cd "$(dirname "$0")/.."
11+
12+
version="${1:-}"
13+
summary="${2:-}"
14+
if ! echo "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?$'; then
15+
echo "usage: scripts/release.sh <x.y.z[-alpha.N|-beta.N|-rc.N]> \"release summary\"" >&2
16+
exit 1
17+
fi
18+
if [ -z "$summary" ]; then
19+
echo "release summary must not be empty" >&2
20+
exit 1
21+
fi
22+
23+
branch="$(git rev-parse --abbrev-ref HEAD)"
24+
if [ "$branch" != "main" ]; then
25+
echo "release must run on main (current: $branch)" >&2
26+
exit 1
27+
fi
28+
if [ -n "$(git status --porcelain)" ]; then
29+
echo "worktree is dirty; commit or stash before releasing" >&2
30+
exit 1
31+
fi
32+
33+
git fetch origin main --tags
34+
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then
35+
echo "local main must exactly match origin/main before releasing" >&2
36+
exit 1
37+
fi
38+
if git rev-parse "v$version" >/dev/null 2>&1; then
39+
echo "tag v$version already exists" >&2
40+
exit 1
41+
fi
42+
if ! grep -q "^## \[$version\]" CHANGELOG.md; then
43+
echo "CHANGELOG.md has no '## [$version]' section" >&2
44+
exit 1
45+
fi
46+
47+
current_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n 1)"
48+
if [ "${RELEASE_DRY_RUN:-0}" = "1" ] && [ "$version" != "$current_version" ]; then
49+
echo "dry runs require the current manifest version ($current_version)" >&2
50+
exit 1
51+
fi
52+
53+
replace() {
54+
file="$1"
55+
pattern="$2"
56+
sed "$pattern" "$file" > "$file.release-tmp"
57+
mv "$file.release-tmp" "$file"
58+
}
59+
60+
replace Cargo.toml "s/^version = \".*\"/version = \"$version\"/"
61+
replace package.json "s/\"version\": \".*\"/\"version\": \"$version\"/"
62+
cargo check --locked --quiet
63+
64+
cargo fmt --all -- --check
65+
cargo clippy --workspace --all-targets --all-features -- -D warnings
66+
cargo test --workspace --all-targets --all-features
67+
pnpm install --frozen-lockfile
68+
pnpm site:check
69+
cargo package --locked --allow-dirty --no-verify
70+
scripts/secleak-check.sh
71+
scripts/build-release.sh
72+
73+
if [ "${RELEASE_DRY_RUN:-0}" = "1" ]; then
74+
echo "release dry run passed for v$version"
75+
exit 0
76+
fi
77+
78+
git add -- Cargo.toml Cargo.lock package.json
79+
if ! git diff --cached --quiet; then
80+
git commit -m "release $version: $summary"
81+
fi
82+
git tag -a "v$version" -m "Switchloom v$version: $summary"
83+
git push origin HEAD "v$version"
84+
85+
echo "released v$version; the Release workflow will publish signed checksums and platform archives"

0 commit comments

Comments
 (0)