Skip to content

Commit 0cc9a36

Browse files
Merge pull request #6 from 8Network/chore/release-pipeline-cleanup
chore: release pipeline cleanup + GitHub Actions release workflow
2 parents 2a4b9c1 + b26da3f commit 0cc9a36

8 files changed

Lines changed: 257 additions & 956 deletions

File tree

.github/workflows/release.yml

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
env:
9+
CARGO_TERM_COLOR: always
10+
11+
jobs:
12+
# ── Build: macOS (arm64 + x64, native cross-compile) ──────────────────────
13+
build-macos:
14+
name: Build macOS
15+
runs-on: macos-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: dtolnay/rust-toolchain@stable
20+
with:
21+
targets: x86_64-apple-darwin,aarch64-apple-darwin
22+
23+
- uses: Swatinem/rust-cache@v2
24+
25+
- name: Build darwin-arm64
26+
run: cargo build --release -p o8v --target aarch64-apple-darwin
27+
28+
- name: Build darwin-x64
29+
run: cargo build --release -p o8v --target x86_64-apple-darwin
30+
31+
- name: Import Developer ID certificate
32+
env:
33+
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
34+
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
35+
run: |
36+
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain"
37+
KEYCHAIN_PWD=$(openssl rand -base64 32)
38+
39+
echo "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/cert.p12"
40+
41+
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
42+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
43+
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
44+
security import "$RUNNER_TEMP/cert.p12" -P "$MACOS_CERTIFICATE_PWD" \
45+
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
46+
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PWD" \
47+
"$KEYCHAIN_PATH"
48+
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
49+
50+
- name: Sign macOS binaries
51+
run: |
52+
IDENTITY=$(security find-identity -v -p codesigning \
53+
| grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}')
54+
codesign --sign "$IDENTITY" --options runtime --timestamp \
55+
target/aarch64-apple-darwin/release/8v
56+
codesign --sign "$IDENTITY" --options runtime --timestamp \
57+
target/x86_64-apple-darwin/release/8v
58+
59+
- name: Decode Apple API key
60+
env:
61+
APPLE_API_KEY_B64: ${{ secrets.APPLE_API_KEY }}
62+
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
63+
run: |
64+
mkdir -p "$RUNNER_TEMP/apple"
65+
echo "$APPLE_API_KEY_B64" | base64 --decode \
66+
> "$RUNNER_TEMP/apple/AuthKey_${APPLE_KEY_ID}.p8"
67+
echo "APPLE_API_KEY=$RUNNER_TEMP/apple/AuthKey_${APPLE_KEY_ID}.p8" >> "$GITHUB_ENV"
68+
69+
- name: Notarize darwin-arm64
70+
env:
71+
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
72+
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
73+
run: |
74+
cd target/aarch64-apple-darwin/release
75+
zip -q 8v-darwin-arm64.zip 8v
76+
xcrun notarytool submit 8v-darwin-arm64.zip \
77+
--key "$APPLE_API_KEY" \
78+
--key-id "$APPLE_KEY_ID" \
79+
--issuer "$APPLE_ISSUER_ID" \
80+
--wait
81+
rm 8v-darwin-arm64.zip
82+
83+
- name: Notarize darwin-x64
84+
env:
85+
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
86+
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
87+
run: |
88+
cd target/x86_64-apple-darwin/release
89+
zip -q 8v-darwin-x64.zip 8v
90+
xcrun notarytool submit 8v-darwin-x64.zip \
91+
--key "$APPLE_API_KEY" \
92+
--key-id "$APPLE_KEY_ID" \
93+
--issuer "$APPLE_ISSUER_ID" \
94+
--wait
95+
rm 8v-darwin-x64.zip
96+
97+
- name: Stage binaries
98+
run: |
99+
mkdir -p dist
100+
cp target/aarch64-apple-darwin/release/8v dist/8v-darwin-arm64
101+
cp target/x86_64-apple-darwin/release/8v dist/8v-darwin-x64
102+
103+
- uses: actions/upload-artifact@v4
104+
with:
105+
name: binaries-macos
106+
path: dist/
107+
108+
# ── Build: Linux (x64 + arm64, musl via cargo-zigbuild) ───────────────────
109+
build-linux:
110+
name: Build Linux
111+
runs-on: ubuntu-latest
112+
steps:
113+
- uses: actions/checkout@v4
114+
115+
- uses: dtolnay/rust-toolchain@stable
116+
with:
117+
targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl
118+
119+
- uses: Swatinem/rust-cache@v2
120+
121+
- name: Install zig + cargo-zigbuild
122+
run: |
123+
sudo snap install zig --classic --beta || pip3 install ziglang
124+
cargo install cargo-zigbuild
125+
126+
- name: Build linux-x64
127+
run: cargo zigbuild --release -p o8v --target x86_64-unknown-linux-musl
128+
129+
- name: Build linux-arm64
130+
run: cargo zigbuild --release -p o8v --target aarch64-unknown-linux-musl
131+
132+
- name: Stage binaries
133+
run: |
134+
mkdir -p dist
135+
cp target/x86_64-unknown-linux-musl/release/8v dist/8v-linux-x64
136+
cp target/aarch64-unknown-linux-musl/release/8v dist/8v-linux-arm64
137+
138+
- uses: actions/upload-artifact@v4
139+
with:
140+
name: binaries-linux
141+
path: dist/
142+
143+
# ── Release: collect binaries, generate checksums, publish ────────────────
144+
release:
145+
name: Publish release
146+
needs: [build-macos, build-linux]
147+
runs-on: ubuntu-latest
148+
permissions:
149+
contents: write
150+
steps:
151+
- uses: actions/checkout@v4
152+
153+
- uses: actions/download-artifact@v4
154+
with:
155+
name: binaries-macos
156+
path: dist/
157+
158+
- uses: actions/download-artifact@v4
159+
with:
160+
name: binaries-linux
161+
path: dist/
162+
163+
- name: Generate checksums
164+
run: |
165+
cd dist
166+
sha256sum 8v-* > checksums.txt
167+
cat checksums.txt
168+
169+
- name: Publish GitHub release
170+
env:
171+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
172+
run: |
173+
TAG="${GITHUB_REF_NAME}"
174+
gh release create "$TAG" \
175+
--title "$TAG" \
176+
--generate-notes \
177+
dist/8v-darwin-arm64 \
178+
dist/8v-darwin-x64 \
179+
dist/8v-linux-x64 \
180+
dist/8v-linux-arm64 \
181+
dist/checksums.txt

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ dist/
88
**/node_modules/
99
**/.ruff_cache/
1010
**/.mypy_cache/
11+
o8v/tests/fixtures/build-go/buildtest
1112

1213
# Local state
1314
.8v/
1415
.claude/
15-
.wrangler/
1616

1717
# Logs and crashes
1818
*.log
-2.36 MB
Binary file not shown.

o8v/tests/unit_release_pipeline.rs

Lines changed: 30 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,13 @@ fn validate_base_url(url: &str) -> bool {
2323
|| url.starts_with("http://127.0.0.1")
2424
}
2525

26-
/// Verify the version-bump logic lives in bump-version.sh and is referenced
27-
/// from release.sh. release.sh delegates — bump-version.sh is the single
28-
/// source of truth so CI-visible drift is impossible.
29-
fn bump_version_script() -> &'static str {
30-
include_str!("../../scripts/bump-version.sh")
31-
}
32-
33-
fn release_sh_delegates_to_bump_version() -> bool {
34-
let release_sh = include_str!("../../scripts/release.sh");
35-
release_sh.contains("bump-version.sh")
36-
}
37-
38-
/// Extract binary names from release.sh build section.
39-
fn extract_binary_names_from_release_sh() -> Vec<String> {
40-
let release_sh = include_str!("../../scripts/release.sh");
26+
/// Extract binary names from the release workflow file.
27+
fn extract_binary_names_from_workflow() -> Vec<String> {
28+
let workflow = include_str!("../../.github/workflows/release.yml");
4129
let mut binaries = Vec::new();
42-
43-
for line in release_sh.lines() {
44-
if line.contains("cp target") && line.contains("dist/8v-") {
45-
if let Some(start) = line.find("dist/") {
30+
for line in workflow.lines() {
31+
if line.trim_start().starts_with("cp ") && line.contains("dist/8v-") {
32+
if let Some(start) = line.find("dist/8v-") {
4633
let rest = &line[start + 5..];
4734
let binary = rest.split_whitespace().next().unwrap_or("");
4835
if !binary.is_empty() {
@@ -69,24 +56,31 @@ fn workspace_cargo_toml_has_version_field() {
6956
);
7057
assert!(
7158
content.contains("version = \""),
72-
"Workspace Cargo.toml missing version field — release.sh sed would silently skip it"
59+
"Workspace Cargo.toml missing version field — bump the version in [workspace.package]"
7360
);
7461
}
7562

7663
#[test]
77-
fn release_sh_version_bump_targets_workspace_root() {
78-
assert!(
79-
release_sh_delegates_to_bump_version(),
80-
"release.sh must delegate to scripts/bump-version.sh for version bumping"
81-
);
82-
let bump = bump_version_script();
64+
fn workflow_targets_workspace_package_version() {
65+
let root = workspace_root();
66+
let cargo_toml = root.join("Cargo.toml");
67+
let content = fs::read_to_string(&cargo_toml).expect("read workspace Cargo.toml");
68+
// The workflow releases whatever version is in [workspace.package].
69+
// Verify the section and version key are present so a tag push reflects
70+
// the correct version.
8371
assert!(
84-
bump.contains("[workspace.package]"),
85-
"bump-version.sh does not target the [workspace.package] section"
72+
content.contains("[workspace.package]"),
73+
"[workspace.package] section missing — workflow release would use wrong version"
8674
);
75+
let in_section = content
76+
.lines()
77+
.skip_while(|l| !l.trim().starts_with("[workspace.package]"))
78+
.skip(1)
79+
.take_while(|l| !l.trim_start().starts_with('['))
80+
.any(|l| l.trim_start().starts_with("version = \""));
8781
assert!(
88-
bump.contains("^version = "),
89-
"bump-version.sh does not anchor on ^version = (may match dependency versions)"
82+
in_section,
83+
"version field not found under [workspace.package] — CI release would be unversioned"
9084
);
9185
}
9286

@@ -187,14 +181,17 @@ fn checksum_format_parseable() {
187181

188182
#[test]
189183
fn binary_names_match_install_platforms() {
190-
let binaries = extract_binary_names_from_release_sh();
191-
assert!(!binaries.is_empty(), "Failed to extract binary names");
184+
let binaries = extract_binary_names_from_workflow();
185+
assert!(
186+
!binaries.is_empty(),
187+
"Failed to extract binary names from workflow"
188+
);
192189

193190
let expected = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"];
194191
for expected_name in expected {
195192
assert!(
196193
binaries.iter().any(|b| b.contains(expected_name)),
197-
"Binary name {} not found in release.sh",
194+
"Binary name {} not found in release workflow",
198195
expected_name
199196
);
200197
}
@@ -226,42 +223,6 @@ fn version_txt_has_no_whitespace() {
226223
);
227224
}
228225

229-
#[test]
230-
fn changelog_sed_preserves_unreleased_and_adds_version() {
231-
let project = TempProject::empty();
232-
let changelog_path = project.path().join("CHANGELOG.md");
233-
234-
let original = r#"# Changelog
235-
236-
## [Unreleased]
237-
238-
## [1.0.0] - 2026-01-01
239-
240-
### Added
241-
242-
- Some feature
243-
"#;
244-
project
245-
.write_file("CHANGELOG.md", original.as_bytes())
246-
.expect("write CHANGELOG");
247-
248-
let content = fs::read_to_string(&changelog_path).expect("read CHANGELOG");
249-
250-
// Simulate sed: insert new version after [Unreleased]
251-
let updated = content.replace(
252-
"## [Unreleased]",
253-
"## [Unreleased]\n\n## [2.0.0] - 2026-04-07",
254-
);
255-
project
256-
.write_file("CHANGELOG.md", updated.as_bytes())
257-
.expect("write updated CHANGELOG");
258-
259-
let result = fs::read_to_string(&changelog_path).expect("read updated");
260-
assert!(result.contains("## [Unreleased]"));
261-
assert!(result.contains("## [2.0.0] - 2026-04-07"));
262-
assert!(result.contains("## [1.0.0] - 2026-01-01"));
263-
}
264-
265226
#[test]
266227
fn semver_regex_accepts_valid_versions() {
267228
let valid = ["0.1.0", "1.0.0", "10.20.30", "1.2.3"];

scripts/bump-version.sh

Lines changed: 0 additions & 45 deletions
This file was deleted.

0 commit comments

Comments
 (0)