From 7a6aa5ccd7291b3de1e24c0f2f995f27c9830c0f Mon Sep 17 00:00:00 2001 From: Paddo <653385+paddo@users.noreply.github.com> Date: Tue, 12 May 2026 10:48:48 +1000 Subject: [PATCH] build: migrate release.yml to ushr + fastlane match for Developer ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the persistent self-hosted runner with ushr's ephemeral macOS VMs (runs-on: [self-hosted, macOS, ARM64]). The Developer ID Application certificate is no longer pre-installed on the runner — instead it lives encrypted in paddo-tech/fastlane-match and is installed into a fresh temporary keychain per build. Adds: - Gemfile pulling in fastlane - fastlane/Appfile (apple_id + team_id from env) - fastlane/Fastfile with two lanes: sync_developer_id - create temp keychain, fetch cert via match cleanup_keychain - delete the temp keychain Pattern mirrors trivialis but scoped to a single Developer ID cert. Workflow changes: - bundle install + sync_developer_id steps before existing build/sign/notarize - MATCH_GIT_URL overridden inline to use HTTPS+token form. Org-level MATCH_GIT_URL is SSH (works on home-server which has the key); ushr ephemeral VMs don't, so we need a token-bearing URL. Requires a new repo or org secret FASTLANE_MATCH_TOKEN (PAT with Contents:Read on paddo-tech/fastlane-match). - Always-run cleanup step destroys the temp keychain + API key file. Prerequisites before this can run successfully: 1. fastlane match developer_id has been run once locally to populate certs/developer_id/ in the match repo (Apple will create a new Developer ID cert if the existing one's private key isn't available). 2. FASTLANE_MATCH_TOKEN secret is set on tether-cli (or paddo-tech org). The other org-level secrets (APPLE_ID, APPLE_TEAM_ID, APPLE_APP_PASSWORD, APP_STORE_CONNECT_API_KEY_CONTENT, MATCH_PASSWORD, KEYCHAIN_PASSWORD) are already present and inherited from paddo-tech. --- .github/workflows/release.yml | 54 ++++++++++++++++++++++++++--------- Gemfile | 3 ++ fastlane/Appfile | 2 ++ fastlane/Fastfile | 39 +++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 Gemfile create mode 100644 fastlane/Appfile create mode 100644 fastlane/Fastfile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 374120e..c21ac48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,15 +19,45 @@ permissions: contents: write env: - # Signing identity - must match certificate installed on runner + # Signing identity - codesign matches by name; cert is installed at runtime + # via fastlane match into a temporary keychain (see fastlane/Fastfile). SIGNING_IDENTITY: "Developer ID Application: Paddo Tech PTY LTD (D9Q57P9D3L)" jobs: build: - runs-on: self-hosted + runs-on: [self-hosted, macOS, ARM64] steps: - uses: actions/checkout@v4 + - name: Install fastlane via bundler + run: | + export GEM_HOME="$HOME/.gem" + export PATH="$HOME/.gem/bin:$PATH" + gem list bundler -i || gem install bundler --user-install + bundle config set --local path "$HOME/.gem" + bundle install --jobs 4 + + - name: Stash App Store Connect API key on disk + run: | + mkdir -p "$RUNNER_TEMP/fastlane" + echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' \ + > "$RUNNER_TEMP/fastlane/asc_api_key.json" + + - name: Sync Developer ID certificate (fastlane match) + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APP_STORE_CONNECT_API_KEY_PATH: ${{ runner.temp }}/fastlane/asc_api_key.json + # Override org-level MATCH_GIT_URL (likely SSH) with HTTPS+token — + # ephemeral VMs don't have an SSH key for the match repo. + MATCH_GIT_URL: https://x-access-token:${{ secrets.FASTLANE_MATCH_TOKEN }}@github.com/paddo-tech/fastlane-match.git + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + MATCH_KEYCHAIN_NAME: tether_match + MATCH_KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + export PATH="$HOME/.gem/bin:$PATH" + bundle exec fastlane mac sync_developer_id + - name: Build release (Apple Silicon) run: cargo build --release --target aarch64-apple-darwin @@ -54,7 +84,6 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} run: | - # Notarize Apple Silicon binary cd target/aarch64-apple-darwin/release zip tether-arm64.zip tether xcrun notarytool submit tether-arm64.zip \ @@ -65,7 +94,6 @@ jobs: rm tether-arm64.zip cd - - # Notarize Intel binary cd target/x86_64-apple-darwin/release zip tether-x64.zip tether xcrun notarytool submit tether-x64.zip \ @@ -91,7 +119,6 @@ jobs: run: | VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') echo "version=$VERSION" >> $GITHUB_OUTPUT - # Detect prerelease (contains -alpha, -beta, -rc) if [[ "$VERSION" =~ -alpha|-beta|-rc ]]; then echo "prerelease=true" >> $GITHUB_OUTPUT else @@ -113,7 +140,6 @@ jobs: - name: Create tag if: steps.release_check.outputs.exists == 'false' && github.event.inputs.dry_run != 'true' run: | - # Only create tag if it doesn't exist if ! git ls-remote --tags origin | grep -q "refs/tags/v${{ steps.version.outputs.version }}$"; then git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -146,25 +172,18 @@ jobs: SHA_ARM=$(cat target/aarch64-apple-darwin/release/tether-aarch64-apple-darwin.tar.gz.sha256 | awk '{print $1}') SHA_X86=$(cat target/x86_64-apple-darwin/release/tether-x86_64-apple-darwin.tar.gz.sha256 | awk '{print $1}') - # Clone the tap repo rm -rf /tmp/homebrew-tap git clone https://x-access-token:${COMMITTER_TOKEN}@github.com/paddo-tech/homebrew-tap.git /tmp/homebrew-tap cd /tmp/homebrew-tap - # Determine formula file and class name if [ "$IS_PRERELEASE" = "true" ]; then - # Versioned formula for prereleases: tether-cli@1.1.0-beta.1.rb FORMULA_FILE="Formula/tether-cli@${VERSION}.rb" - # Convert version to valid Ruby class name: 1.1.0-beta.1 -> 110Beta1 - # Remove dots, capitalize after hyphens, remove hyphens CLASS_NAME="TetherCliAT$(echo ${VERSION} | perl -pe 's/\.//g; s/-(.)/\u$1/g')" else - # Main formula for stable releases FORMULA_FILE="Formula/tether-cli.rb" CLASS_NAME="TetherCli" fi - # Generate formula printf '%s\n' \ "class ${CLASS_NAME} < Formula" \ ' desc "Sync dotfiles and packages across machines"' \ @@ -201,10 +220,17 @@ jobs: ' end' \ 'end' > "$FORMULA_FILE" - # Commit and push git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add "$FORMULA_FILE" git commit -m "tether-cli ${VERSION}" git push + - name: Cleanup match keychain + if: always() + env: + MATCH_KEYCHAIN_NAME: tether_match + run: | + export PATH="$HOME/.gem/bin:$PATH" + bundle exec fastlane mac cleanup_keychain || true + rm -f "$RUNNER_TEMP/fastlane/asc_api_key.json" || true diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..1e5dd95 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +apple_id(ENV["APPLE_ID"]) +team_id(ENV["APPLE_TEAM_ID"]) diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..b2e2174 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,39 @@ +default_platform(:mac) + +# tether is signed with a Developer ID Application certificate that lives in +# fastlane match. Each release run installs the cert into a fresh temporary +# keychain, codesign signs against it, then the keychain is destroyed. Fits +# ephemeral runners (ushr's Tart VMs) where state must not persist. + +KEYCHAIN_NAME = ENV["MATCH_KEYCHAIN_NAME"] || "tether_match" +KEYCHAIN_PASSWORD = ENV["MATCH_KEYCHAIN_PASSWORD"] || "tether_match_password" +APP_IDENTIFIER = ENV["MATCH_APP_IDENTIFIER"] || "dev.paddo.tether-cli" + +platform :mac do + desc "Install the Developer ID Application certificate via match into a temp keychain" + lane :sync_developer_id do + create_keychain( + name: KEYCHAIN_NAME, + password: KEYCHAIN_PASSWORD, + default_keychain: true, + unlock: true, + timeout: 3600, + lock_when_sleeps: false, + ) + + match( + type: "developer_id", + app_identifier: APP_IDENTIFIER, + git_url: ENV["MATCH_GIT_URL"], + keychain_name: KEYCHAIN_NAME, + keychain_password: KEYCHAIN_PASSWORD, + readonly: true, + api_key_path: ENV["APP_STORE_CONNECT_API_KEY_PATH"], + ) + end + + desc "Delete the temporary match keychain" + lane :cleanup_keychain do + delete_keychain(name: KEYCHAIN_NAME) rescue UI.message("keychain already gone") + end +end