Skip to content

Release

Release #4

Workflow file for this run

name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to build (e.g. v0.4.0)'
required: true
permissions:
contents: write
env:
NFPM_VERSION: 2.46.3
jobs:
test:
name: test + clippy
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- name: Install system build deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libssl-dev pkg-config clang cmake g++ curl
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: ci
- name: cargo test
run: cargo test --locked
- name: cargo clippy
run: cargo clippy --locked --all-targets -- -D warnings
build:
name: Build ${{ matrix.arch }}
needs: test
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
arch: amd64
rust_target: x86_64-unknown-linux-gnu
- runner: ubuntu-24.04-arm
arch: arm64
rust_target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.runner }}
steps:
- name: Resolve checkout ref
id: resolve_ref
env:
DISPATCH_TAG: ${{ inputs.tag }}
EVENT_REF: ${{ github.ref }}
run: |
if [ -n "$DISPATCH_TAG" ]; then
printf 'ref=%s\n' "$DISPATCH_TAG" >> "$GITHUB_OUTPUT"
else
printf 'ref=%s\n' "$EVENT_REF" >> "$GITHUB_OUTPUT"
fi
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ steps.resolve_ref.outputs.ref }}
- name: Install system build deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libssl-dev pkg-config clang cmake g++ curl
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust_target }}
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: release-${{ matrix.arch }}
- name: Build release binary
env:
RUST_TARGET: ${{ matrix.rust_target }}
run: cargo build --release --locked --target "$RUST_TARGET"
- name: Stage binary for nfpm
env:
RUST_TARGET: ${{ matrix.rust_target }}
run: |
mkdir -p target/release
cp "target/${RUST_TARGET}/release/utter" target/release/utter
- name: Install nfpm
env:
ARCH_DEB: ${{ matrix.arch }}
run: |
curl -sSfL \
"https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_${ARCH_DEB}.deb" \
-o /tmp/nfpm.deb
sudo dpkg -i /tmp/nfpm.deb
- name: Build .deb and .rpm
working-directory: packaging
env:
RAW_VERSION: ${{ github.ref_name }}
DISPATCH_TAG: ${{ inputs.tag }}
UTTER_ARCH: ${{ matrix.arch }}
run: |
TAG="${DISPATCH_TAG:-$RAW_VERSION}"
# Strip leading 'v' so package version is 0.4.0, not v0.4.0.
UTTER_VERSION="${TAG#v}"
export UTTER_VERSION UTTER_ARCH
mkdir -p ../dist
nfpm pkg --packager deb --target ../dist/
nfpm pkg --packager rpm --target ../dist/
- name: Build plain binary tarball
env:
RAW_VERSION: ${{ github.ref_name }}
DISPATCH_TAG: ${{ inputs.tag }}
UTTER_ARCH: ${{ matrix.arch }}
run: |
TAG="${DISPATCH_TAG:-$RAW_VERSION}"
VERSION="${TAG#v}"
STAGE="utter-${VERSION}-linux-${UTTER_ARCH}"
mkdir -p "dist/${STAGE}"
cp target/release/utter "dist/${STAGE}/"
cp README.md LICENSE NOTICE AGENTS.md CLAUDE.md BACKLOG.md "dist/${STAGE}/"
cp -r packaging "dist/${STAGE}/"
cp -r scripts "dist/${STAGE}/"
tar -C dist -czf "dist/${STAGE}.tar.gz" "${STAGE}"
rm -rf "dist/${STAGE}"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.arch }}
path: dist/*
if-no-files-found: error
macos-build:
name: Build macOS arm64 (signed + notarized DMG)
needs: test
runs-on: macos-14
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
UTTER_SIGN_IDENTITY: "Developer ID Application: Josh Guice (YKQ46WD7SL)"
RUST_TARGET: aarch64-apple-darwin
steps:
- name: Resolve checkout ref
id: resolve_ref
env:
DISPATCH_TAG: ${{ inputs.tag }}
EVENT_REF: ${{ github.ref }}
run: |
if [ -n "$DISPATCH_TAG" ]; then
printf 'ref=%s\n' "$DISPATCH_TAG" >> "$GITHUB_OUTPUT"
else
printf 'ref=%s\n' "$EVENT_REF" >> "$GITHUB_OUTPUT"
fi
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ steps.resolve_ref.outputs.ref }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ env.RUST_TARGET }}
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: release-macos-arm64
- name: Import Developer ID cert into temp keychain
env:
CERT_B64: ${{ secrets.APPLE_DEV_ID_CERT_BASE64 }}
CERT_PASS: ${{ secrets.APPLE_DEV_ID_CERT_PASSWORD }}
run: |
set -euo pipefail
KEYCHAIN_PASS=$(uuidgen)
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN_PATH"
printf '%s' "$CERT_B64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" \
-k "$KEYCHAIN_PATH" \
-P "$CERT_PASS" \
-T /usr/bin/codesign -T /usr/bin/security
# Put the temp keychain at the front of the search list so codesign finds it.
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g')
# Allow codesign to access the key without a GUI prompt.
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASS" "$KEYCHAIN_PATH"
rm -f "$RUNNER_TEMP/cert.p12"
# Sanity check: identity visible.
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
- name: Build release binary (arm64)
run: cargo build --release --locked --target "$RUST_TARGET"
- name: Stage binary for bundle
run: |
mkdir -p target/release
cp "target/${RUST_TARGET}/release/utter" target/release/utter
- name: Build + sign .app bundle
run: ./scripts/make-bundle.sh
- name: Notarize + staple app bundle
run: |
set -euo pipefail
ditto -c -k --keepParent target/release/utter.app "$RUNNER_TEMP/utter.zip"
xcrun notarytool submit "$RUNNER_TEMP/utter.zip" \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
xcrun stapler staple target/release/utter.app
xcrun stapler validate target/release/utter.app
- name: Create DMG
env:
RAW_VERSION: ${{ github.ref_name }}
DISPATCH_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
TAG="${DISPATCH_TAG:-$RAW_VERSION}"
VERSION="${TAG#v}"
DMG_NAME="utter-${VERSION}-macos-arm64.dmg"
mkdir -p dist
# Use a staging dir so the DMG layout is just the app (no stray files).
STAGE="$RUNNER_TEMP/dmg-stage"
mkdir -p "$STAGE"
cp -R target/release/utter.app "$STAGE/"
hdiutil create -volname "utter" \
-srcfolder "$STAGE" \
-ov -format UDZO \
"dist/${DMG_NAME}"
- name: Sign + notarize + staple DMG
run: |
set -euo pipefail
DMG=$(ls dist/utter-*-macos-arm64.dmg)
codesign --force --sign "$UTTER_SIGN_IDENTITY" --timestamp "$DMG"
xcrun notarytool submit "$DMG" \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
xcrun stapler staple "$DMG"
xcrun stapler validate "$DMG"
- name: Upload DMG artifact
uses: actions/upload-artifact@v4
with:
name: dist-macos-arm64
path: dist/utter-*-macos-arm64.dmg
if-no-files-found: error
release:
name: Publish release
needs: [build, macos-build]
runs-on: ubuntu-24.04
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: List artifacts
run: ls -la dist/
- name: Resolve release tag
id: resolve_tag
env:
DISPATCH_TAG: ${{ inputs.tag }}
EVENT_TAG: ${{ github.ref_name }}
run: |
if [ -n "$DISPATCH_TAG" ]; then
printf 'tag=%s\n' "$DISPATCH_TAG" >> "$GITHUB_OUTPUT"
else
printf 'tag=%s\n' "$EVENT_TAG" >> "$GITHUB_OUTPUT"
fi
- name: Attach artifacts to GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.resolve_tag.outputs.tag }}
files: |
dist/utter_*.deb
dist/utter-*.rpm
dist/utter-*.tar.gz
dist/utter-*-macos-*.dmg
# release-please creates the Release with CHANGELOG-derived notes
# before this workflow runs; we only need to attach artifacts.
generate_release_notes: false
fail_on_unmatched_files: true