Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
name: Release

on:
push:
tags:
- 'v*'

env:
CARGO_TERM_COLOR: always

jobs:
# ── Build: macOS (arm64 + x64, native cross-compile) ──────────────────────
build-macos:
name: Build macOS
runs-on: macos-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-apple-darwin,aarch64-apple-darwin

- uses: Swatinem/rust-cache@v2

- name: Build darwin-arm64
run: cargo build --release -p o8v --target aarch64-apple-darwin

- name: Build darwin-x64
run: cargo build --release -p o8v --target x86_64-apple-darwin

- name: Import Developer ID certificate
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
run: |
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain"
KEYCHAIN_PWD=$(openssl rand -base64 32)

echo "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/cert.p12"

security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
security import "$RUNNER_TEMP/cert.p12" -P "$MACOS_CERTIFICATE_PWD" \
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PWD" \
"$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')

- name: Sign macOS binaries
run: |
IDENTITY=$(security find-identity -v -p codesigning \
| grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}')
codesign --sign "$IDENTITY" --options runtime --timestamp \
target/aarch64-apple-darwin/release/8v
codesign --sign "$IDENTITY" --options runtime --timestamp \
target/x86_64-apple-darwin/release/8v

- name: Decode Apple API key
env:
APPLE_API_KEY_B64: ${{ secrets.APPLE_API_KEY }}
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
run: |
mkdir -p "$RUNNER_TEMP/apple"
echo "$APPLE_API_KEY_B64" | base64 --decode \
> "$RUNNER_TEMP/apple/AuthKey_${APPLE_KEY_ID}.p8"
echo "APPLE_API_KEY=$RUNNER_TEMP/apple/AuthKey_${APPLE_KEY_ID}.p8" >> "$GITHUB_ENV"

- name: Notarize darwin-arm64
env:
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
run: |
cd target/aarch64-apple-darwin/release
zip -q 8v-darwin-arm64.zip 8v
xcrun notarytool submit 8v-darwin-arm64.zip \
--key "$APPLE_API_KEY" \
--key-id "$APPLE_KEY_ID" \
--issuer "$APPLE_ISSUER_ID" \
--wait
rm 8v-darwin-arm64.zip

- name: Notarize darwin-x64
env:
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
run: |
cd target/x86_64-apple-darwin/release
zip -q 8v-darwin-x64.zip 8v
xcrun notarytool submit 8v-darwin-x64.zip \
--key "$APPLE_API_KEY" \
--key-id "$APPLE_KEY_ID" \
--issuer "$APPLE_ISSUER_ID" \
--wait
rm 8v-darwin-x64.zip

- name: Stage binaries
run: |
mkdir -p dist
cp target/aarch64-apple-darwin/release/8v dist/8v-darwin-arm64
cp target/x86_64-apple-darwin/release/8v dist/8v-darwin-x64

- uses: actions/upload-artifact@v4
with:
name: binaries-macos
path: dist/

# ── Build: Linux (x64 + arm64, musl via cargo-zigbuild) ───────────────────
build-linux:
name: Build Linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl

- uses: Swatinem/rust-cache@v2

- name: Install zig + cargo-zigbuild
run: |
sudo snap install zig --classic --beta || pip3 install ziglang
cargo install cargo-zigbuild

- name: Build linux-x64
run: cargo zigbuild --release -p o8v --target x86_64-unknown-linux-musl

- name: Build linux-arm64
run: cargo zigbuild --release -p o8v --target aarch64-unknown-linux-musl

- name: Stage binaries
run: |
mkdir -p dist
cp target/x86_64-unknown-linux-musl/release/8v dist/8v-linux-x64
cp target/aarch64-unknown-linux-musl/release/8v dist/8v-linux-arm64

- uses: actions/upload-artifact@v4
with:
name: binaries-linux
path: dist/

# ── Release: collect binaries, generate checksums, publish ────────────────
release:
name: Publish release
needs: [build-macos, build-linux]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: binaries-macos
path: dist/

- uses: actions/download-artifact@v4
with:
name: binaries-linux
path: dist/

- name: Generate checksums
run: |
cd dist
sha256sum 8v-* > checksums.txt
cat checksums.txt

- name: Publish GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${GITHUB_REF_NAME}"
gh release create "$TAG" \
--title "$TAG" \
--generate-notes \
dist/8v-darwin-arm64 \
dist/8v-darwin-x64 \
dist/8v-linux-x64 \
dist/8v-linux-arm64 \
dist/checksums.txt
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ dist/
**/node_modules/
**/.ruff_cache/
**/.mypy_cache/
o8v/tests/fixtures/build-go/buildtest

# Local state
.8v/
.claude/
.wrangler/

# Logs and crashes
*.log
Expand Down
Binary file removed o8v/tests/fixtures/build-go/buildtest
Binary file not shown.
99 changes: 30 additions & 69 deletions o8v/tests/unit_release_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,13 @@ fn validate_base_url(url: &str) -> bool {
|| url.starts_with("http://127.0.0.1")
}

/// Verify the version-bump logic lives in bump-version.sh and is referenced
/// from release.sh. release.sh delegates — bump-version.sh is the single
/// source of truth so CI-visible drift is impossible.
fn bump_version_script() -> &'static str {
include_str!("../../scripts/bump-version.sh")
}

fn release_sh_delegates_to_bump_version() -> bool {
let release_sh = include_str!("../../scripts/release.sh");
release_sh.contains("bump-version.sh")
}

/// Extract binary names from release.sh build section.
fn extract_binary_names_from_release_sh() -> Vec<String> {
let release_sh = include_str!("../../scripts/release.sh");
/// Extract binary names from the release workflow file.
fn extract_binary_names_from_workflow() -> Vec<String> {
let workflow = include_str!("../../.github/workflows/release.yml");
let mut binaries = Vec::new();

for line in release_sh.lines() {
if line.contains("cp target") && line.contains("dist/8v-") {
if let Some(start) = line.find("dist/") {
for line in workflow.lines() {
if line.trim_start().starts_with("cp ") && line.contains("dist/8v-") {
if let Some(start) = line.find("dist/8v-") {
let rest = &line[start + 5..];
let binary = rest.split_whitespace().next().unwrap_or("");
if !binary.is_empty() {
Expand All @@ -69,24 +56,31 @@ fn workspace_cargo_toml_has_version_field() {
);
assert!(
content.contains("version = \""),
"Workspace Cargo.toml missing version field — release.sh sed would silently skip it"
"Workspace Cargo.toml missing version field — bump the version in [workspace.package]"
);
}

#[test]
fn release_sh_version_bump_targets_workspace_root() {
assert!(
release_sh_delegates_to_bump_version(),
"release.sh must delegate to scripts/bump-version.sh for version bumping"
);
let bump = bump_version_script();
fn workflow_targets_workspace_package_version() {
let root = workspace_root();
let cargo_toml = root.join("Cargo.toml");
let content = fs::read_to_string(&cargo_toml).expect("read workspace Cargo.toml");
// The workflow releases whatever version is in [workspace.package].
// Verify the section and version key are present so a tag push reflects
// the correct version.
assert!(
bump.contains("[workspace.package]"),
"bump-version.sh does not target the [workspace.package] section"
content.contains("[workspace.package]"),
"[workspace.package] section missing — workflow release would use wrong version"
);
let in_section = content
.lines()
.skip_while(|l| !l.trim().starts_with("[workspace.package]"))
.skip(1)
.take_while(|l| !l.trim_start().starts_with('['))
.any(|l| l.trim_start().starts_with("version = \""));
assert!(
bump.contains("^version = "),
"bump-version.sh does not anchor on ^version = (may match dependency versions)"
in_section,
"version field not found under [workspace.package] — CI release would be unversioned"
);
}

Expand Down Expand Up @@ -187,14 +181,17 @@ fn checksum_format_parseable() {

#[test]
fn binary_names_match_install_platforms() {
let binaries = extract_binary_names_from_release_sh();
assert!(!binaries.is_empty(), "Failed to extract binary names");
let binaries = extract_binary_names_from_workflow();
assert!(
!binaries.is_empty(),
"Failed to extract binary names from workflow"
);

let expected = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"];
for expected_name in expected {
assert!(
binaries.iter().any(|b| b.contains(expected_name)),
"Binary name {} not found in release.sh",
"Binary name {} not found in release workflow",
expected_name
);
}
Expand Down Expand Up @@ -226,42 +223,6 @@ fn version_txt_has_no_whitespace() {
);
}

#[test]
fn changelog_sed_preserves_unreleased_and_adds_version() {
let project = TempProject::empty();
let changelog_path = project.path().join("CHANGELOG.md");

let original = r#"# Changelog

## [Unreleased]

## [1.0.0] - 2026-01-01

### Added

- Some feature
"#;
project
.write_file("CHANGELOG.md", original.as_bytes())
.expect("write CHANGELOG");

let content = fs::read_to_string(&changelog_path).expect("read CHANGELOG");

// Simulate sed: insert new version after [Unreleased]
let updated = content.replace(
"## [Unreleased]",
"## [Unreleased]\n\n## [2.0.0] - 2026-04-07",
);
project
.write_file("CHANGELOG.md", updated.as_bytes())
.expect("write updated CHANGELOG");

let result = fs::read_to_string(&changelog_path).expect("read updated");
assert!(result.contains("## [Unreleased]"));
assert!(result.contains("## [2.0.0] - 2026-04-07"));
assert!(result.contains("## [1.0.0] - 2026-01-01"));
}

#[test]
fn semver_regex_accepts_valid_versions() {
let valid = ["0.1.0", "1.0.0", "10.20.30", "1.2.3"];
Expand Down
45 changes: 0 additions & 45 deletions scripts/bump-version.sh

This file was deleted.

Loading
Loading