Skip to content

Commit 3a2f299

Browse files
committed
Add prebuilt release build script and clean-machine E2E release workflow
1 parent 1813112 commit 3a2f299

2 files changed

Lines changed: 263 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
name: Release
2+
3+
# Builds a real `mutantkit` release binary, packages it exactly as a user
4+
# downloads it (tarball + SHA256SUMS, no repo checkout required to use it),
5+
# then proves the whole quick-start path works on a machine that never
6+
# cloned this repo: extract -> doctor -> init -> plan -> run, against a
7+
# freshly generated SwiftPM fixture. Only a macOS arm64 binary for now —
8+
# no universal binary, no notarization, no auto-update; see
9+
# Scripts/release-build.sh.
10+
#
11+
# workflow_dispatch lets this whole pipeline be validated before any real
12+
# tag exists. A `v*` tag push additionally publishes a GitHub Release.
13+
14+
on:
15+
workflow_dispatch:
16+
push:
17+
tags:
18+
- "v*"
19+
20+
permissions:
21+
contents: write
22+
23+
jobs:
24+
build:
25+
runs-on: macos-15
26+
outputs:
27+
version: ${{ steps.version.outputs.version }}
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- name: Toolchain
32+
run: swift --version
33+
34+
- name: Determine version
35+
id: version
36+
run: |
37+
set -euo pipefail
38+
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
39+
version="${GITHUB_REF_NAME#v}"
40+
else
41+
version="0.0.0-dispatch.$(git rev-parse --short HEAD)"
42+
fi
43+
echo "version=$version" >> "$GITHUB_OUTPUT"
44+
45+
- name: Build and package
46+
run: Scripts/release-build.sh "${{ steps.version.outputs.version }}" dist
47+
48+
- name: Upload build artifact
49+
uses: actions/upload-artifact@v4
50+
with:
51+
name: mutantkit-macos-arm64
52+
path: |
53+
dist/mutantkit-macos-arm64.tar.gz
54+
dist/SHA256SUMS
55+
retention-days: 30
56+
57+
clean-machine-e2e:
58+
needs: build
59+
runs-on: macos-15
60+
steps:
61+
# Deliberately no actions/checkout here — the point of this job is to
62+
# prove the quick-start path works with no repo present at all, the
63+
# way a real user experiences it.
64+
- name: Download the packaged binary only
65+
uses: actions/download-artifact@v4
66+
with:
67+
name: mutantkit-macos-arm64
68+
path: download
69+
70+
- name: Verify checksum
71+
working-directory: download
72+
run: shasum -a 256 -c SHA256SUMS
73+
74+
- name: Extract
75+
working-directory: download
76+
run: tar xzf mutantkit-macos-arm64.tar.gz
77+
78+
- name: mutantkit --version
79+
run: download/mutantkit-macos-arm64/mutantkit --version
80+
81+
- name: mutantkit doctor (no project yet — must report that honestly)
82+
working-directory: download
83+
run: |
84+
set -euo pipefail
85+
./mutantkit-macos-arm64/mutantkit doctor || true
86+
87+
- name: Generate a minimal SwiftPM fixture project (no clone of this repo)
88+
run: |
89+
set -euo pipefail
90+
mkdir -p fixture/Sources/Calc fixture/Tests/CalcTests
91+
cat > fixture/Package.swift <<'EOF'
92+
// swift-tools-version:6.0
93+
import PackageDescription
94+
95+
let package = Package(
96+
name: "Calc",
97+
targets: [
98+
.target(name: "Calc"),
99+
.testTarget(name: "CalcTests", dependencies: ["Calc"])
100+
]
101+
)
102+
EOF
103+
cat > fixture/Sources/Calc/Calc.swift <<'EOF'
104+
public func calcAdd(_ a: Int, _ b: Int) -> Int {
105+
a + b
106+
}
107+
108+
public func isPositive(_ x: Int) -> Bool {
109+
x > 0
110+
}
111+
EOF
112+
cat > fixture/Tests/CalcTests/CalcTests.swift <<'EOF'
113+
import XCTest
114+
@testable import Calc
115+
116+
final class CalcTests: XCTestCase {
117+
func testCalcAdd() {
118+
XCTAssertEqual(calcAdd(2, 3), 5)
119+
}
120+
func testIsPositive() {
121+
XCTAssertTrue(isPositive(1))
122+
XCTAssertFalse(isPositive(-1))
123+
}
124+
}
125+
EOF
126+
127+
- name: mutantkit doctor (fixture present)
128+
working-directory: fixture
129+
run: ../download/mutantkit-macos-arm64/mutantkit doctor
130+
131+
- name: mutantkit init
132+
working-directory: fixture
133+
run: |
134+
set -euo pipefail
135+
../download/mutantkit-macos-arm64/mutantkit init
136+
python3 -c "
137+
import re
138+
with open('mutantkit.yml') as f:
139+
c = f.read()
140+
c = c.replace(
141+
\" # No test target detected. Add yours here, or run \`mutantkit doctor\`.\n []\",
142+
' - CalcTests'
143+
)
144+
with open('mutantkit.yml', 'w') as f:
145+
f.write(c)
146+
"
147+
grep -A2 '^tests:' mutantkit.yml
148+
149+
- name: mutantkit plan
150+
working-directory: fixture
151+
run: ../download/mutantkit-macos-arm64/mutantkit plan
152+
153+
- name: mutantkit run
154+
working-directory: fixture
155+
run: |
156+
set -euo pipefail
157+
../download/mutantkit-macos-arm64/mutantkit run --plan plan.json --output report.json
158+
python3 -c "
159+
import json
160+
report = json.load(open('report.json'))
161+
assert len(report['integrity']['violations']) == 0, report['integrity']['violations']
162+
assert len(report['results']) == 2, report['results']
163+
print('clean-machine E2E gate: PASSED')
164+
"
165+
166+
publish-release:
167+
if: github.ref_type == 'tag'
168+
needs: [build, clean-machine-e2e]
169+
runs-on: ubuntu-latest
170+
steps:
171+
- uses: actions/download-artifact@v4
172+
with:
173+
name: mutantkit-macos-arm64
174+
path: dist
175+
176+
- name: Create GitHub Release
177+
env:
178+
GH_TOKEN: ${{ github.token }}
179+
run: |
180+
set -euo pipefail
181+
gh release create "${{ github.ref_name }}" \
182+
dist/mutantkit-macos-arm64.tar.gz dist/SHA256SUMS \
183+
--repo "${{ github.repository }}" \
184+
--title "mutantkit ${{ needs.build.outputs.version }}" \
185+
--generate-notes

Scripts/release-build.sh

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env bash
2+
#
3+
# release-build.sh — build a release `mutantkit` binary with a real version
4+
# and commit SHA embedded, and package it exactly as an end user downloads
5+
# it: a tarball plus a SHA256SUMS file, no repo checkout required to use it.
6+
#
7+
# Sources/CLI/Version.swift documents this itself: "The placeholders are
8+
# substituted by the release script" — this is that script. It edits
9+
# Version.swift in place, builds, packages, then reverts the edit so the
10+
# working tree is never left with a stamped version.
11+
#
12+
# Usage:
13+
# Scripts/release-build.sh <version> [output-dir]
14+
#
15+
# <version> — e.g. "0.1.0". Do not include a leading "v".
16+
# [output-dir] — defaults to ./dist
17+
#
18+
# Requires: swift toolchain, tar, shasum (macOS default) or sha256sum.
19+
20+
set -euo pipefail
21+
22+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
23+
cd "$REPO_ROOT"
24+
25+
VERSION="${1:?usage: scripts/release-build.sh <version> [output-dir]}"
26+
OUTPUT_DIR="${2:-dist}"
27+
COMMIT_SHA="$(git rev-parse HEAD)"
28+
VERSION_FILE="Sources/CLI/Version.swift"
29+
30+
if [[ "$VERSION" == v* ]]; then
31+
echo "error: pass the version without a leading 'v' (got '$VERSION')" >&2
32+
exit 1
33+
fi
34+
35+
if [[ -n "$(git status --porcelain "$VERSION_FILE")" ]]; then
36+
echo "error: $VERSION_FILE already has uncommitted changes; refusing to stamp over them" >&2
37+
exit 1
38+
fi
39+
40+
restore_version_file() {
41+
git checkout -- "$VERSION_FILE"
42+
}
43+
trap restore_version_file EXIT
44+
45+
echo "Stamping $VERSION_FILE: version=$VERSION commit=$COMMIT_SHA"
46+
sed -i '' \
47+
-e "s/public static let version = \".*\"/public static let version = \"${VERSION}\"/" \
48+
-e "s/public static let commitSHA: String? = nil/public static let commitSHA: String? = \"${COMMIT_SHA}\"/" \
49+
"$VERSION_FILE"
50+
51+
echo "swift build -c release --product mutantkit"
52+
swift build -c release --product mutantkit
53+
54+
rm -rf "$OUTPUT_DIR"
55+
mkdir -p "$OUTPUT_DIR"
56+
57+
PACKAGE_NAME="mutantkit-macos-arm64"
58+
PACKAGE_DIR="$(mktemp -d)/$PACKAGE_NAME"
59+
mkdir -p "$PACKAGE_DIR"
60+
cp .build/release/mutantkit "$PACKAGE_DIR/mutantkit"
61+
cp LICENSE "$PACKAGE_DIR/LICENSE"
62+
63+
TARBALL="$OUTPUT_DIR/${PACKAGE_NAME}.tar.gz"
64+
tar czf "$TARBALL" -C "$(dirname "$PACKAGE_DIR")" "$PACKAGE_NAME"
65+
66+
(
67+
cd "$OUTPUT_DIR"
68+
if command -v shasum >/dev/null 2>&1; then
69+
shasum -a 256 "${PACKAGE_NAME}.tar.gz" > SHA256SUMS
70+
else
71+
sha256sum "${PACKAGE_NAME}.tar.gz" > SHA256SUMS
72+
fi
73+
)
74+
75+
echo
76+
echo "Built: $TARBALL"
77+
echo "Checksums:"
78+
cat "$OUTPUT_DIR/SHA256SUMS"

0 commit comments

Comments
 (0)