diff --git a/.github/scripts/app-release/stage-binaries.sh b/.github/scripts/app-release/stage-binaries.sh new file mode 100755 index 0000000..321ead7 --- /dev/null +++ b/.github/scripts/app-release/stage-binaries.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Stage the binaries produced by tauri-action into a flat directory so they +# can be uploaded as GitHub Actions build artifacts and attested. +# +# Env: ARTIFACT_PATHS (JSON array from tauri-action's artifactPaths output). +# Output: dir= appended to $GITHUB_OUTPUT. +set -euo pipefail + +: "${ARTIFACT_PATHS:?}" + +staging="${GITHUB_WORKSPACE:-$PWD}/dist/release" +rm -rf "$staging" +mkdir -p "$staging" + +while IFS= read -r src; do + # Strip stray CR — on windows-latest the env var can arrive CRLF-terminated. + src="${src%$'\r'}" + [ -z "$src" ] && continue + # tauri-action lists the .app bundle (a directory) alongside the .dmg on + # macOS; skip anything that isn't a regular file. + if [ ! -f "$src" ]; then + echo "Skipping non-file artifact: $src" >&2 + continue + fi + cp "$src" "$staging/" +done < <(printf '%s' "$ARTIFACT_PATHS" | jq -r '.[]') + +count=$(find "$staging" -maxdepth 1 -type f | wc -l | tr -d ' ') +if [ "$count" -eq 0 ]; then + echo "No binary artifacts produced by tauri-action." >&2 + exit 1 +fi + +echo "dir=$staging" >> "$GITHUB_OUTPUT" +echo "Staged $count file(s) in $staging" diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml index 6348c25..f8312b9 100644 --- a/.github/workflows/app-release.yml +++ b/.github/workflows/app-release.yml @@ -9,6 +9,11 @@ name: App Release # # Builds are currently unsigned. See doc/guides/releasing.md for how to obtain # Apple and Windows code-signing credentials and wire them back in here. +# +# After the build, each platform job also uploads its bundles as GitHub +# Actions build artifacts and generates a signed SLSA provenance attestation +# (actions/attest-build-provenance). The attestation is what surfaces the +# binaries on the org's linked-artifacts view via the artifact metadata API. on: push: @@ -22,6 +27,8 @@ on: permissions: contents: write + id-token: write + attestations: write jobs: build: @@ -30,12 +37,15 @@ jobs: matrix: include: - platform: macos-latest + slug: macos args: "--target universal-apple-darwin" rust-targets: "aarch64-apple-darwin,x86_64-apple-darwin" - platform: ubuntu-22.04 + slug: linux args: "" rust-targets: "" - platform: windows-latest + slug: windows args: "" rust-targets: "" @@ -74,6 +84,7 @@ jobs: run: bun install --frozen-lockfile - name: Build and release + id: tauri uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -91,3 +102,22 @@ jobs: releaseDraft: true prerelease: false args: ${{ matrix.args }} + + - name: Stage binaries + id: stage + shell: bash + env: + ARTIFACT_PATHS: ${{ steps.tauri.outputs.artifactPaths }} + run: bash .github/scripts/app-release/stage-binaries.sh + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: pengine-${{ matrix.slug }} + path: ${{ steps.stage.outputs.dir }} + if-no-files-found: error + + - name: Attest build provenance + uses: actions/attest-build-provenance@v2 + with: + subject-path: ${{ steps.stage.outputs.dir }}/* diff --git a/doc/guides/releasing.md b/doc/guides/releasing.md index b6328b1..886a714 100644 --- a/doc/guides/releasing.md +++ b/doc/guides/releasing.md @@ -3,7 +3,20 @@ Pengine ships installers for macOS, Windows, and Linux via the [`App Release`](../../.github/workflows/app-release.yml) GitHub Actions workflow. Pushing a tag matching `v*` (e.g. `v1.0.1`) triggers a build on each -platform and uploads the installers as assets on a **draft** GitHub Release. +platform and publishes the installers in two places: + +- as assets on a **draft** GitHub Release (for humans to download), and +- as GitHub Actions build artifacts (`pengine-macos`, `pengine-linux`, + `pengine-windows`) with a signed SLSA build-provenance attestation. The + attestations surface on the org's + [linked artifacts page](https://github.com/orgs/pengine-ai/artifacts) via + the artifact metadata API — GHCR is reserved for Docker images (tools). + +Verify a downloaded binary's provenance with the GitHub CLI: + +```bash +gh attestation verify ./pengine_1.0.1_amd64.AppImage --owner pengine-ai +``` ```bash git tag v1.0.1