Skip to content
Merged
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
286 changes: 286 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
name: Release

# Release variant: serialize per-tag, never cancel. A cancelled release
# mid-publish leaves the GitHub Release page, build attestations, and
# per-target binary archives in an inconsistent state — better to queue
# than abort. Mirrors the pulseengine/synth and pulseengine/rivet
# release workflows.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

on:
push:
tags:
- "v*"
# Manual re-run for a tag whose initial run failed partway. The tag
# must already exist; this does not create tags.
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to (re)build (e.g. v0.1.0)"
required: true
type: string

permissions:
contents: read

env:
CARGO_TERM_COLOR: always
# wohl-hub depends on the relay sibling via path = "../relay/...".
# Pin the relay commit explicitly so release builds are reproducible
# against a known sibling state. Keep this SHA in lock-step with
# ci.yml's RELAY_REF — bump them together via PR. fetch-depth: 0
# on the checkout below covers force-pushes of relay (sibling repo
# is routinely rebased; see ci.yml comment + issue #8).
RELAY_REF: 178ffd479ad863c91ece4f580379a9207c36a530

jobs:
# ── Cross-platform binary builds ──────────────────────────────────────
# wohl-hub builds the home-supervision orchestrator: it wires the six
# verified Wohl monitors and the Relay engines into a single binary
# that runs on the hub host (Pi / MiniPC / dev Mac).
build-binaries:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
# Native arm64 runner — avoids cross-compilation's friction
# with wohl's `path = "../relay/..."` sibling-path deps
# (cross's docker image doesn't mount the parent dir, and
# patching CROSS_CONTAINER_OPTS is more fragile than just
# using a native runner).
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04-arm
archive: tar.gz
# macOS targets for dev convenience — wohl-hub on a Mac is
# not the production deployment shape, but reviewers and
# developers want a runnable binary.
- target: x86_64-apple-darwin
os: macos-14
archive: tar.gz
- target: aarch64-apple-darwin
os: macos-latest
archive: tar.gz
steps:
- uses: actions/checkout@v4
with:
path: wohl
ref: ${{ inputs.tag || github.ref }}

# relay sibling checkout: wohl's Cargo.toml has path-deps like
# `relay-lc = { path = "../relay/crates/relay-lc" }`. The build
# needs relay co-located at the workspace's parent dir.
- uses: actions/checkout@v4
with:
repository: pulseengine/relay
path: relay
ref: ${{ env.RELAY_REF }}
fetch-depth: 0

- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- uses: Swatinem/rust-cache@v2
with:
workspaces: wohl
key: release-${{ matrix.target }}

- name: Build wohl-hub
working-directory: wohl
run: cargo build --release --target ${{ matrix.target }} -p wohl-hub

- name: Strip binary
# Strip on the native targets; skip on cross or unusual setups.
if: runner.os == 'Linux' || runner.os == 'macOS'
working-directory: wohl
run: strip "target/${{ matrix.target }}/release/wohl-hub" 2>/dev/null || true

- name: Package archive
env:
TARGET: ${{ matrix.target }}
# Resolve the version once: tag push -> refs/tags/vX.Y.Z;
# workflow_dispatch -> the user-supplied tag input. Bound via
# env: and dereferenced as $VERSION below — never expand
# ${{ ... }} directly inside run: (command-injection vector).
INPUT_TAG: ${{ inputs.tag }}
working-directory: wohl
run: |
set -euo pipefail
VERSION="${INPUT_TAG:-${GITHUB_REF#refs/tags/}}"
ARCHIVE="wohl-hub-${VERSION}-${TARGET}.tar.gz"
mkdir -p staging
cp "target/${TARGET}/release/wohl-hub" staging/
cp README.md LICENSE staging/ 2>/dev/null || true
tar -czf "$ARCHIVE" -C staging .
echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV"

- uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: wohl/${{ env.ARCHIVE }}
retention-days: 7

# ── Create the GitHub Release: checksums, provenance, signing ─────────
create-release:
name: Create GitHub Release
needs: [build-binaries]
runs-on: ubuntu-latest
permissions:
# Keyless signing + provenance need an OIDC token; release-asset
# upload needs contents: write; build provenance attestation
# needs attestations: write. This block mirrors the permissions
# set used by the pulseengine/synth and pulseengine/sigil
# release workflows.
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
with:
path: wohl
ref: ${{ inputs.tag || github.ref }}

# relay sibling needed because cargo-cyclonedx walks workspace
# path-deps when generating the SBOM.
- uses: actions/checkout@v4
with:
repository: pulseengine/relay
path: relay
ref: ${{ env.RELAY_REF }}
fetch-depth: 0

- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
path: artifacts

- name: Flatten release assets
run: |
set -euo pipefail
mkdir -p release-assets
find artifacts -type f -name "*.tar.gz" -exec cp {} release-assets/ \;
ls -la release-assets/

# CycloneDX SBOM for wohl-hub. Generated *before* SHA256SUMS so
# its digest is captured in the checksum manifest; the cosign
# signature over SHA256SUMS.txt transitively covers the SBOM.
- name: Install cargo-cyclonedx
run: cargo install --locked cargo-cyclonedx

- name: Generate wohl-hub SBOM (CycloneDX)
env:
INPUT_TAG: ${{ inputs.tag }}
working-directory: wohl
run: |
set -euo pipefail
VERSION="${INPUT_TAG:-${GITHUB_REF#refs/tags/}}"
BARE="${VERSION#v}"
# cargo-cyclonedx doesn't proxy cargo's `-p` flag — use
# --manifest-path to target wohl-hub's Cargo.toml directly.
cargo cyclonedx \
--manifest-path crates/wohl-hub/Cargo.toml \
--format json \
--spec-version 1.5
SBOM_SRC="crates/wohl-hub/wohl-hub.cdx.json"
if [ ! -f "$SBOM_SRC" ]; then
SBOM_SRC=$(find crates/wohl-hub -maxdepth 2 -name '*.cdx.json' | head -1)
fi
test -n "$SBOM_SRC" && test -f "$SBOM_SRC"
cp "$SBOM_SRC" "../release-assets/wohl-hub-${BARE}.cdx.json"
echo "::notice::wohl-hub SBOM written to release-assets/wohl-hub-${BARE}.cdx.json"
ls -la ../release-assets/

- name: Generate SHA256 checksums
run: |
set -euo pipefail
cd release-assets
sha256sum ./* > SHA256SUMS.txt
cat SHA256SUMS.txt

# ── SLSA build provenance (GitHub-native) ──────────────────────────
# actions/attest-build-provenance generates an in-toto SLSA v1
# provenance statement for every binary archive, signs it keyless
# via Sigstore (Fulcio cert bound to this workflow's OIDC identity),
# and records it in the Rekor transparency log. Consumers verify
# with `gh attestation verify <file> --repo pulseengine/wohl`.
- name: Generate SLSA build provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: "release-assets/*.tar.gz"

# ── Sigstore keyless signing (cosign) ──────────────────────────────
# Signs SHA256SUMS.txt so a consumer can verify the checksum file
# itself was produced by this workflow (closes the gap where an
# attacker who can replace a release asset could also replace the
# plain checksum file). Mirrors the pulseengine/synth and
# pulseengine/rivet cosign sign-blob pattern.
- name: Install cosign
uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.4.1'

- name: Sign SHA256SUMS with cosign (keyless OIDC)
run: |
set -euo pipefail
cd release-assets
cosign sign-blob \
--yes \
--bundle SHA256SUMS.txt.cosign.bundle \
--output-signature SHA256SUMS.txt.sig \
--output-certificate SHA256SUMS.txt.pem \
SHA256SUMS.txt
echo "::notice::SHA256SUMS signed via Sigstore keyless flow."
ls -la ./*

- name: Capture build environment
run: |
set -euo pipefail
{
echo "rustc: $(rustc --version)"
echo "cargo: $(cargo --version)"
echo "cosign: $(cosign version 2>&1 | head -1)"
echo "runner: $(uname -srm)"
} > release-assets/build-env.txt
cat release-assets/build-env.txt

- name: Create or update GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Untrusted-input safety: the tag name flows in via env: and is
# dereferenced through $VERSION, never expanded into the shell.
INPUT_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
VERSION="${INPUT_TAG:-${GITHUB_REF#refs/tags/}}"
# Verification one-liner for release notes — copy-paste-friendly:
#
# cosign verify-blob \
# --certificate-identity-regexp \
# 'https://github.com/pulseengine/wohl/.github/workflows/release.yml@.*' \
# --certificate-oidc-issuer \
# 'https://token.actions.githubusercontent.com' \
# --bundle SHA256SUMS.txt.cosign.bundle \
# SHA256SUMS.txt
# gh attestation verify wohl-hub-vX.Y.Z-<triple>.tar.gz \
# --repo pulseengine/wohl
#
# Idempotent: re-running the workflow for an existing release
# uploads/overwrites assets rather than failing. --clobber lets
# a re-run replace assets a previous partial run left behind.
if gh release view "$VERSION" >/dev/null 2>&1; then
echo "::notice::Release $VERSION exists; uploading assets"
gh release upload "$VERSION" --clobber release-assets/*
else
echo "::notice::Creating Release $VERSION with assets"
gh release create "$VERSION" \
--title "wohl $VERSION" \
--generate-notes \
release-assets/*
fi
Loading