Release #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |