From 05c60a4b24afd8ea17449bc2cd14e89e0fda6311 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Sun, 1 Mar 2026 22:05:37 -0500 Subject: [PATCH 1/4] feat(penpal): add CI release workflow for tag-triggered builds New GitHub Actions workflow (.github/workflows/penpal-release.yml) triggered by penpal/v* tags. Builds Tauri app for both arm64 and x86_64 via matrix strategy, creates a GitHub Release with both zips, and triggers the cask bump workflow on block/homebrew-tap. Also updates package.sh to accept an optional target triple argument for cross-compiled builds where Tauri outputs to a target-specific directory. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/penpal-release.yml | 137 +++++++++++++++++++++++++++ apps/penpal/scripts/package.sh | 9 +- 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/penpal-release.yml diff --git a/.github/workflows/penpal-release.yml b/.github/workflows/penpal-release.yml new file mode 100644 index 00000000..c06cddb7 --- /dev/null +++ b/.github/workflows/penpal-release.yml @@ -0,0 +1,137 @@ +name: Penpal Release + +on: + push: + tags: ['penpal/v*'] + workflow_dispatch: + +permissions: + contents: write + actions: write + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: Build (${{ matrix.arch }}) + runs-on: macos-latest + strategy: + matrix: + include: + - arch: arm64 + target: aarch64-apple-darwin + goarch: arm64 + - arch: x86_64 + target: x86_64-apple-darwin + goarch: amd64 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + # Install hermit (manages node, rust, just, go) + - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 + + # Cache Cargo dependencies + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 + with: + workspaces: apps/penpal/frontend/src-tauri + key: ${{ matrix.target }} + + # Add cross-compilation target if needed + - name: Add Rust target + if: matrix.arch == 'x86_64' + run: rustup target add ${{ matrix.target }} + + # Enable pnpm via corepack + - run: corepack enable pnpm + + # Install frontend dependencies + - name: Install dependencies + run: | + pnpm install --frozen-lockfile + cd apps/penpal/frontend/src-tauri && cargo fetch + + # Build Go sidecar binaries for the target architecture + - name: Build Go sidecar + working-directory: apps/penpal + run: | + mkdir -p frontend/src-tauri/binaries + GOOS=darwin GOARCH=${{ matrix.goarch }} go build \ + -o "frontend/src-tauri/binaries/penpal-server-${{ matrix.target }}" \ + ./cmd/penpal-server + GOOS=darwin GOARCH=${{ matrix.goarch }} go build \ + -o "frontend/src-tauri/binaries/penpal-cli-${{ matrix.target }}" \ + ./cmd/penpal-cli + + # Build frontend (architecture-independent) + - name: Build frontend + working-directory: apps/penpal/frontend + run: pnpm install && VITE_BASE=/ VITE_API_URL=http://localhost:8080 pnpm run build + + # Build Tauri app for the target architecture + - name: Build Tauri app + working-directory: apps/penpal/frontend + run: pnpm run tauri:build -- --target ${{ matrix.target }} + + # Package into distributable zip + - name: Package + working-directory: apps/penpal + run: ./scripts/package.sh ${{ matrix.arch }} ${{ matrix.target }} + + # Upload artifact for the release job + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: penpal-${{ matrix.arch }} + path: apps/penpal/dist/*.zip + + release: + name: Create Release + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + fetch-tags: true + + # Download all build artifacts + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + path: artifacts + merge-multiple: true + + # Extract version from tag + - name: Extract version + id: version + run: | + TAG="${GITHUB_REF#refs/tags/penpal/v}" + echo "version=$TAG" >> "$GITHUB_OUTPUT" + + # Create GitHub Release with both zips + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + PREV_TAG=$(git describe --tags --match 'penpal/v*' --abbrev=0 HEAD^ 2>/dev/null || echo "") + NOTES_ARGS="--generate-notes" + if [ -n "$PREV_TAG" ]; then + NOTES_ARGS="$NOTES_ARGS --notes-start-tag $PREV_TAG" + fi + gh release create "${{ github.ref_name }}" \ + --title "Penpal v${{ steps.version.outputs.version }}" \ + $NOTES_ARGS \ + artifacts/*.zip + + # Trigger cask bump on block/homebrew-tap + - name: Trigger Homebrew cask bump + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + BASE_URL="https://github.com/block/builderbot/releases/download/penpal/v${VERSION}" + gh workflow run bump-cask.yaml \ + -R block/homebrew-tap \ + -f cask=penpal \ + -f tag="penpal/v${VERSION}" \ + -f arm64_url="${BASE_URL}/Penpal-${VERSION}-arm64.zip" \ + -f intel_url="${BASE_URL}/Penpal-${VERSION}-x86_64.zip" diff --git a/apps/penpal/scripts/package.sh b/apps/penpal/scripts/package.sh index 304d6930..7718f154 100755 --- a/apps/penpal/scripts/package.sh +++ b/apps/penpal/scripts/package.sh @@ -16,7 +16,14 @@ case "$ARCH" in *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; esac -APP_SRC="$ROOT_DIR/frontend/src-tauri/target/release/bundle/macos/Penpal.app" +# Optional Cargo build target triple (e.g. aarch64-apple-darwin). +# When set, Tauri outputs to target//release/ instead of target/release/. +TARGET_TRIPLE="${2:-}" +if [ -n "$TARGET_TRIPLE" ]; then + APP_SRC="$ROOT_DIR/frontend/src-tauri/target/$TARGET_TRIPLE/release/bundle/macos/Penpal.app" +else + APP_SRC="$ROOT_DIR/frontend/src-tauri/target/release/bundle/macos/Penpal.app" +fi if [ ! -d "$APP_SRC" ]; then echo "Error: $APP_SRC not found. Run 'just build' first." >&2 exit 1 From 65bc00089562fda6a1721446eeda12b1ee450620 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Sun, 1 Mar 2026 22:07:40 -0500 Subject: [PATCH 2/4] fix(penpal): address review feedback on release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use macos-13 (Intel) runner for x86_64 builds instead of cross-compiling on arm64 — avoids Tauri native linking issues - Remove workflow_dispatch (no tag = broken version extraction) - Scope permissions per-job (build: read, release: write) - Add fail-fast: false so both arch builds complete independently - Remove duplicate pnpm install in frontend build step - Add comment explaining cross-repo GITHUB_TOKEN pattern Co-Authored-By: Claude Opus 4.6 --- .github/workflows/penpal-release.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/penpal-release.yml b/.github/workflows/penpal-release.yml index c06cddb7..5798f700 100644 --- a/.github/workflows/penpal-release.yml +++ b/.github/workflows/penpal-release.yml @@ -3,11 +3,6 @@ name: Penpal Release on: push: tags: ['penpal/v*'] - workflow_dispatch: - -permissions: - contents: write - actions: write env: CARGO_TERM_COLOR: always @@ -15,16 +10,21 @@ env: jobs: build: name: Build (${{ matrix.arch }}) - runs-on: macos-latest + runs-on: ${{ matrix.runner }} + permissions: + contents: read strategy: + fail-fast: false matrix: include: - arch: arm64 target: aarch64-apple-darwin goarch: arm64 + runner: macos-latest - arch: x86_64 target: x86_64-apple-darwin goarch: amd64 + runner: macos-13 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -37,15 +37,10 @@ jobs: workspaces: apps/penpal/frontend/src-tauri key: ${{ matrix.target }} - # Add cross-compilation target if needed - - name: Add Rust target - if: matrix.arch == 'x86_64' - run: rustup target add ${{ matrix.target }} - # Enable pnpm via corepack - run: corepack enable pnpm - # Install frontend dependencies + # Install dependencies - name: Install dependencies run: | pnpm install --frozen-lockfile @@ -66,7 +61,7 @@ jobs: # Build frontend (architecture-independent) - name: Build frontend working-directory: apps/penpal/frontend - run: pnpm install && VITE_BASE=/ VITE_API_URL=http://localhost:8080 pnpm run build + run: VITE_BASE=/ VITE_API_URL=http://localhost:8080 pnpm run build # Build Tauri app for the target architecture - name: Build Tauri app @@ -88,6 +83,9 @@ jobs: name: Create Release needs: build runs-on: ubuntu-latest + permissions: + contents: write + actions: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: @@ -122,7 +120,9 @@ jobs: $NOTES_ARGS \ artifacts/*.zip - # Trigger cask bump on block/homebrew-tap + # Trigger cask bump on block/homebrew-tap. + # Uses GITHUB_TOKEN with actions:write — block org allows cross-repo + # workflow_dispatch (same pattern as block/qrgo → block/homebrew-tap). - name: Trigger Homebrew cask bump env: GH_TOKEN: ${{ github.token }} From 0dd1db08d57e2b64efa1353c1476072bf7350f72 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 2 Mar 2026 08:03:54 -0500 Subject: [PATCH 3/4] fix(penpal): use env vars to avoid shell injection in release workflow Pass version and tag name through env: instead of interpolating ${{ }} expressions directly in run: blocks. Fixes GitHub Advanced Security shell injection warnings. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/penpal-release.yml | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/penpal-release.yml b/.github/workflows/penpal-release.yml index 5798f700..826928bc 100644 --- a/.github/workflows/penpal-release.yml +++ b/.github/workflows/penpal-release.yml @@ -28,6 +28,17 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + # Validate that the git tag version matches Cargo.toml to prevent + # mismatched artifact names vs Homebrew URLs (see package.sh line 9). + - name: Validate tag matches Cargo.toml version + run: | + TAG_VERSION="${GITHUB_REF#refs/tags/penpal/v}" + CARGO_VERSION=$(grep '^version' apps/penpal/frontend/src-tauri/Cargo.toml | head -1 | sed 's/version = "//;s/"//') + if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then + echo "::error::Tag version ($TAG_VERSION) does not match Cargo.toml version ($CARGO_VERSION)" + exit 1 + fi + # Install hermit (manages node, rust, just, go) - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 @@ -98,25 +109,31 @@ jobs: path: artifacts merge-multiple: true - # Extract version from tag + # Extract version from tag and validate semver format - name: Extract version id: version run: | TAG="${GITHUB_REF#refs/tags/penpal/v}" + if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "::error::Tag version '$TAG' does not match semver format" + exit 1 + fi echo "version=$TAG" >> "$GITHUB_OUTPUT" # Create GitHub Release with both zips - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} + TAG_NAME: penpal/v${{ steps.version.outputs.version }} run: | PREV_TAG=$(git describe --tags --match 'penpal/v*' --abbrev=0 HEAD^ 2>/dev/null || echo "") NOTES_ARGS="--generate-notes" if [ -n "$PREV_TAG" ]; then NOTES_ARGS="$NOTES_ARGS --notes-start-tag $PREV_TAG" fi - gh release create "${{ github.ref_name }}" \ - --title "Penpal v${{ steps.version.outputs.version }}" \ + gh release create "$TAG_NAME" \ + --title "Penpal v${VERSION}" \ $NOTES_ARGS \ artifacts/*.zip @@ -126,8 +143,8 @@ jobs: - name: Trigger Homebrew cask bump env: GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} run: | - VERSION="${{ steps.version.outputs.version }}" BASE_URL="https://github.com/block/builderbot/releases/download/penpal/v${VERSION}" gh workflow run bump-cask.yaml \ -R block/homebrew-tap \ From fefd70fd0e92fb0fa4698694faaa65c5cbe92ee8 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 2 Mar 2026 11:04:37 -0500 Subject: [PATCH 4/4] fix(penpal): actionlint fixes and add dry-run workflow_dispatch - Replace macos-13 with macos-15-intel (macos-13 deprecated) - Fix SC2086: use bash array instead of unquoted string for args - Add workflow_dispatch with dry_run input for build-only validation - Skip version validation and release job on manual dispatch Co-Authored-By: Claude Opus 4.6 --- .github/workflows/penpal-release.yml | 16 ++++++++++++---- apps/penpal/justfile | 6 ++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/penpal-release.yml b/.github/workflows/penpal-release.yml index 826928bc..cce64f9c 100644 --- a/.github/workflows/penpal-release.yml +++ b/.github/workflows/penpal-release.yml @@ -3,6 +3,12 @@ name: Penpal Release on: push: tags: ['penpal/v*'] + workflow_dispatch: + inputs: + dry_run: + description: 'Build only — skip release creation and Homebrew trigger' + type: boolean + default: true env: CARGO_TERM_COLOR: always @@ -24,13 +30,14 @@ jobs: - arch: x86_64 target: x86_64-apple-darwin goarch: amd64 - runner: macos-13 + runner: macos-15-intel steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # Validate that the git tag version matches Cargo.toml to prevent # mismatched artifact names vs Homebrew URLs (see package.sh line 9). - name: Validate tag matches Cargo.toml version + if: github.event_name == 'push' run: | TAG_VERSION="${GITHUB_REF#refs/tags/penpal/v}" CARGO_VERSION=$(grep '^version' apps/penpal/frontend/src-tauri/Cargo.toml | head -1 | sed 's/version = "//;s/"//') @@ -93,6 +100,7 @@ jobs: release: name: Create Release needs: build + if: github.event_name == 'push' runs-on: ubuntu-latest permissions: contents: write @@ -128,13 +136,13 @@ jobs: TAG_NAME: penpal/v${{ steps.version.outputs.version }} run: | PREV_TAG=$(git describe --tags --match 'penpal/v*' --abbrev=0 HEAD^ 2>/dev/null || echo "") - NOTES_ARGS="--generate-notes" + ARGS=(--generate-notes) if [ -n "$PREV_TAG" ]; then - NOTES_ARGS="$NOTES_ARGS --notes-start-tag $PREV_TAG" + ARGS+=(--notes-start-tag "$PREV_TAG") fi gh release create "$TAG_NAME" \ --title "Penpal v${VERSION}" \ - $NOTES_ARGS \ + "${ARGS[@]}" \ artifacts/*.zip # Trigger cask bump on block/homebrew-tap. diff --git a/apps/penpal/justfile b/apps/penpal/justfile index ce0ecbd7..58a14dec 100644 --- a/apps/penpal/justfile +++ b/apps/penpal/justfile @@ -172,6 +172,12 @@ release version: #!/usr/bin/env bash set -euo pipefail + current_branch=$(git rev-parse --abbrev-ref HEAD) + if [ "$current_branch" != "main" ]; then + echo "Error: release must be run from the main branch (currently on '$current_branch')" >&2 + exit 1 + fi + git add frontend/src-tauri/Cargo.toml frontend/package.json CHANGELOG.md git commit -m "release: penpal v{{version}}" git tag "penpal/v{{version}}"