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
82 changes: 45 additions & 37 deletions .github/workflows/release-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,24 +181,40 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
# 2026-10-04: Linux and macOS lanes run on self-hosted runners that
# reach git.terraphim.cloud via the Tailscale /etc/hosts mapping, so
# cargo needs no special auth against the Gitea cargo registry
# (Caddy treats tailnet sources as on-network). GitHub-hosted runners
# cannot reach the registry: off-tailnet cargo access is
# Bearer-only (Gitea #341) and cargo omits Bearer on .crate
# downloads when the registry advertises auth-required:false.
#
# Host preconditions on the release-linux runner (bigbox):
# passwordless sudo, docker (for cross), qemu-user-static,
# binutils (readelf), rustup on PATH. On the release-macos runner
# (Apple Silicon): rustup on PATH, Rosetta (provisioned by a
# guarded step below for the x86_64 lane).
- os: [self-hosted, Linux, X64, release-linux]
target: x86_64-unknown-linux-gnu
use_cross: false
- os: ubuntu-22.04
- os: [self-hosted, Linux, X64, release-linux]
target: x86_64-unknown-linux-musl
use_cross: true
- os: ubuntu-22.04
- os: [self-hosted, Linux, X64, release-linux]
target: aarch64-unknown-linux-musl
use_cross: true
- os: macos-15-intel
# Both macOS targets build on the Apple Silicon runner; x86_64 is a
# cross-compile from the arm64 host.
- os: [self-hosted, macOS, ARM64, release-macos]
target: x86_64-apple-darwin
use_cross: false
- os: macos-15
- os: [self-hosted, macOS, ARM64, release-macos]
target: aarch64-apple-darwin
use_cross: false
- os: windows-latest
target: x86_64-pc-windows-msvc
use_cross: false
# x86_64-pc-windows-msvc lane: temporarily removed. GitHub-hosted
# Windows runners cannot authenticate to the private Gitea cargo
# registry; restore this lane once the terraphim crates are
# published to crates.io.
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Expand All @@ -222,6 +238,17 @@ jobs:
- name: Install QEMU for supported foreign execution
if: matrix.target == 'aarch64-unknown-linux-musl'
run: sudo apt-get update -qq && sudo apt-get install -y -qq qemu-user-static
- name: Prove Rosetta for x86_64 thin verification on Apple Silicon
# The release-macos runner is an Apple Silicon host that verifies the
# cross-compiled x86_64 binary by executing it under Rosetta. The
# install is a no-op when Rosetta is already present; sudo is
# passwordless on the dedicated runner.
if: runner.os == 'macOS' && matrix.target == 'x86_64-apple-darwin'
run: |
set -euo pipefail
arch -x86_64 /usr/bin/true 2>/dev/null || \
sudo softwareupdate --install-rosetta --agree-to-license
arch -x86_64 /usr/bin/true
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.1
with:
key: immutable-clients-v2-${{ matrix.target }}
Expand All @@ -232,10 +259,6 @@ jobs:
CARGO_REGISTRIES_TERRAPHIM_TOKEN: ${{ secrets.CARGO_REGISTRIES_TERRAPHIM_TOKEN }}
run: |
set -euo pipefail
echo "DEBUG: token length=${#CARGO_REGISTRIES_TERRAPHIM_TOKEN}"
echo "DEBUG: token prefix=${CARGO_REGISTRIES_TERRAPHIM_TOKEN:0:7}"
echo "DEBUG: CARGO_REGISTRY_TOKEN set=${CARGO_REGISTRY_TOKEN:+yes}"
rustup run 1.96.0 cargo config get registries.terraphim 2>&1 || true
if [ "${{ matrix.use_cross }}" = "true" ]; then
build=(rustup run 1.96.0 cross)
else
Expand Down Expand Up @@ -265,12 +288,10 @@ jobs:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
extension=""
[ "$TARGET" != "x86_64-pc-windows-msvc" ] || extension=".exe"
for binary in terraphim-agent terraphim-cli terraphim-grep; do
path="target/$TARGET/release/${binary}${extension}"
path="target/$TARGET/release/$binary"
test -s "$path"
test -x "$path" || [ -n "$extension" ]
test -x "$path"
scripts/validate_release_binary.py "$TARGET" "$path"
case "$TARGET" in
aarch64-unknown-linux-musl) output="$(qemu-aarch64-static "$path" --version)" ;;
Expand All @@ -286,10 +307,8 @@ jobs:
run: |
set -euo pipefail
mkdir raw
extension=""
[ "${{ matrix.target }}" != "x86_64-pc-windows-msvc" ] || extension=".exe"
for binary in terraphim-agent terraphim-cli terraphim-grep; do
cp "target/${{ matrix.target }}/release/${binary}${extension}" "raw/${binary}-${{ matrix.target }}${extension}"
cp "target/${{ matrix.target }}/release/$binary" "raw/${binary}-${{ matrix.target }}"
done
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
Expand Down Expand Up @@ -665,10 +684,11 @@ jobs:
run: |
set -euo pipefail
(cd canonical-binaries && sha256sum -c ../BINARY_SHA256SUMS)
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: raw-client-binaries-x86_64-pc-windows-msvc
path: raw
# 2026-10-04: the Windows lane is removed while the private crates are
# unpublished (GitHub-hosted runners cannot authenticate to the
# off-tailnet cargo registry). Its download-artifact step and the zip
# assembly block below were removed atomically with the matrix lane;
# restore both when the crates reach crates.io.
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: signed-client-binaries-apple-darwin
Expand Down Expand Up @@ -712,23 +732,11 @@ jobs:
-C "$root" -cf - "$binary" LICENSE-Apache-2.0 LICENSE-MIT | gzip -n -9 > "release-assets/$archive"
printf '%s\n' "$archive" >> expected-assets.txt
done
target=x86_64-pc-windows-msvc
root="package-root/$binary-$target"
mkdir -p "$root"
cp "raw/$binary-$target.exe" "$root/$binary.exe"
cp LICENSE-Apache-2.0 LICENSE-MIT "$root/"
chmod 755 "$root/$binary.exe"
chmod 644 "$root/LICENSE-Apache-2.0" "$root/LICENSE-MIT"
touch -d "@$SOURCE_DATE_EPOCH" "$root/$binary.exe" "$root/LICENSE-Apache-2.0" "$root/LICENSE-MIT"
archive="$binary-$VERSION-$target.zip"
scripts/create-deterministic-zip.py "$SOURCE_DATE_EPOCH" "$root" \
"release-assets/$archive" "$binary.exe" LICENSE-Apache-2.0 LICENSE-MIT
printf '%s\n' "$archive" >> expected-assets.txt
done
LC_ALL=C sort -o expected-assets.txt expected-assets.txt
find release-assets -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort > actual-assets.txt
diff -u expected-assets.txt actual-assets.txt
test "$(wc -l < expected-assets.txt | tr -d ' ')" = 20
test "$(wc -l < expected-assets.txt | tr -d ' ')" = 17
- name: Install archive signer
run: cargo install zipsign --version 0.2.1 --locked
- name: Sign every final archive
Expand Down Expand Up @@ -780,7 +788,7 @@ jobs:
LC_ALL=C sha256sum "${sealed_assets[@]}" > ../SHA256SUMS
)
(cd release-assets && sha256sum -c ../SHA256SUMS)
test "$(wc -l < SHA256SUMS | tr -d ' ')" = 20
test "$(wc -l < SHA256SUMS | tr -d ' ')" = 17
for binary in terraphim-agent terraphim-cli terraphim-grep; do
scripts/build-manifest.sh "$VERSION" "$binary" release-assets "manifests/$binary.v2.candidate.json"
scripts/build-legacy-manifest.py "manifests/$binary.v2.candidate.json" "manifests/$binary.v1.candidate.json"
Expand Down
5 changes: 5 additions & 0 deletions tests/test_pkgbuild_release_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@

D = chr(36) # dollar sign, kept out of template literals in the test source

# Tracks the last *published* release, not the workspace version: the PKGBUILD
# pins the release asset SHA-256 digests, which only exist once the release is
# sealed. During a release window (workspace bumped, assets not yet
# published) this contract intentionally fails until the follow-up PKGBUILD
# bump lands.
PKGVER = "1.21.16"
REPO = "terraphim/terraphim-clients"
RELEASE_BASE = f"https://github.com/{REPO}/releases/download/v{PKGVER}"
Expand Down
49 changes: 35 additions & 14 deletions tests/test_release_binaries_workflow_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def test_preflight_recursively_peels_to_exact_expected_sha(self) -> None:
def test_release_uses_checked_in_version_and_never_mutates_source(self) -> None:
text = workflow_text()
workspace = tomllib.loads((ROOT / "Cargo.toml").read_text())
self.assertEqual(workspace["workspace"]["package"]["version"], "1.21.16")
self.assertEqual(workspace["workspace"]["package"]["version"], "1.21.17")
for forbidden in (
"Set release version",
"set_section_version",
Expand Down Expand Up @@ -316,24 +316,27 @@ def test_client_packaging_ci_job_is_non_vacuous_and_mutation_sensitive(self) ->
with self.assertRaises((AssertionError, ValueError)):
self.assert_client_packaging_ci_contract(mutant)

def test_matrix_is_the_exact_six_lane_contract(self) -> None:
def test_matrix_is_the_exact_five_lane_self_hosted_contract(self) -> None:
text = workflow_text()
expected = {
("ubuntu-22.04", "x86_64-unknown-linux-gnu", "false"),
("ubuntu-22.04", "x86_64-unknown-linux-musl", "true"),
("ubuntu-22.04", "aarch64-unknown-linux-musl", "true"),
("macos-15-intel", "x86_64-apple-darwin", "false"),
("macos-15", "aarch64-apple-darwin", "false"),
("windows-latest", "x86_64-pc-windows-msvc", "false"),
("[self-hosted, Linux, X64, release-linux]", "x86_64-unknown-linux-gnu", "false"),
("[self-hosted, Linux, X64, release-linux]", "x86_64-unknown-linux-musl", "true"),
("[self-hosted, Linux, X64, release-linux]", "aarch64-unknown-linux-musl", "true"),
("[self-hosted, macOS, ARM64, release-macos]", "x86_64-apple-darwin", "false"),
("[self-hosted, macOS, ARM64, release-macos]", "aarch64-apple-darwin", "false"),
}
actual = set(
re.findall(
r"- os: ([^\n]+)\n\s+target: ([^\n]+)\n\s+use_cross: (true|false)",
r"- os: (\[[^\n]+\])\n\s+target: ([^\n]+)\n\s+use_cross: (true|false)",
text,
)
)
self.assertEqual(actual, expected)
self.assertIn("fail-fast: false", text)
# The Windows lane returns once the private crates are public on
# crates.io; GitHub-hosted runners cannot authenticate to the
# Bearer-only off-tailnet cargo registry (Gitea #341).
self.assertNotIn("windows-latest", text)

def test_builds_are_locked_and_grep_features_are_preserved(self) -> None:
block = job_block("build-binaries")
Expand Down Expand Up @@ -364,7 +367,10 @@ def test_omarchy_targets_are_required_for_agent_and_grep(self) -> None:
self.assertIn("aarch64-unknown-linux-musl", stage)
self.assertIn("for binary in terraphim-agent terraphim-cli terraphim-grep", stage)
self.assertIn('if [ "$binary" != "terraphim-cli" ]; then targets+=(universal-apple-darwin); fi', stage)
self.assertIn('test "$(wc -l < expected-assets.txt | tr -d \' \')" = 20', stage)
self.assertIn('test "$(wc -l < expected-assets.txt | tr -d \' \')" = 17', stage)
# Windows lane removed: 17 tar.gz assets (6 agent + 5 cli + 6 grep);
# restore 20 only when the Windows zip lane returns.
self.assertNotIn("x86_64-pc-windows-msvc", stage)

def test_macos_is_signed_before_deterministic_packaging(self) -> None:
text = workflow_text()
Expand All @@ -383,14 +389,16 @@ def test_archives_are_deterministic_and_have_exact_layout(self) -> None:
"tar --sort=name",
'--owner=0 --group=0 --numeric-owner',
"gzip -n -9",
"scripts/create-deterministic-zip.py",
"LICENSE-Apache-2.0",
"LICENSE-MIT",
"expected-assets.txt",
"diff -u expected-assets.txt actual-assets.txt",
"scripts/validate-release-archive.py",
):
self.assertIn(token, stage)
# create-deterministic-zip.py left with the Windows zip lane; it must
# return together with that lane (and the = 17 count becomes 20).
self.assertNotIn("create-deterministic-zip", stage)

def test_final_bytes_are_signed_before_checksums_and_manifests(self) -> None:
stage = job_block("seal-release-stage")
Expand Down Expand Up @@ -708,7 +716,7 @@ def test_managed_inventory_is_version_bound_and_separate_from_20_archives(self)
self.assertIn("--managed-target aarch64-unknown-linux-musl", stage)
self.assertIn("managed-release-assets/*", stage)
self.assertLess(assemble, checksum)
self.assertIn('test "$(wc -l < SHA256SUMS | tr -d \' \')" = 20', stage)
self.assertIn('test "$(wc -l < SHA256SUMS | tr -d \' \')" = 17', stage)

def test_producer_is_stage_only_and_has_no_public_writer(self) -> None:
text = workflow_text()
Expand Down Expand Up @@ -904,8 +912,21 @@ def test_linux_canonical_bytes_are_stripped_before_all_qualification_and_hashing

def test_macos_thin_execution_has_deterministic_runner_semantics(self) -> None:
text = workflow_text()
self.assertIn("os: macos-15-intel\n target: x86_64-apple-darwin", text)
self.assertIn("os: macos-15\n target: aarch64-apple-darwin", text)
# 2026-10-04: both macOS thin builds run on the Apple Silicon
# self-hosted runner (x86_64 is a cross-compile from the arm64 host);
# the sign/notarize job stays on GitHub-hosted macos-15 because it
# needs sudo for Rosetta provisioning and never touches the cargo
# registry.
self.assertIn(
"os: [self-hosted, macOS, ARM64, release-macos]\n"
" target: x86_64-apple-darwin",
text,
)
self.assertIn(
"os: [self-hosted, macOS, ARM64, release-macos]\n"
" target: aarch64-apple-darwin",
text,
)
signing = job_block("sign-and-notarize-macos")
self.assertIn("runs-on: macos-15", signing)
provision = signing.index("softwareupdate --install-rosetta --agree-to-license")
Expand Down
Loading