From 773fa430edb2da92a4dcf74330539ccb95425168 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:06:22 +0000 Subject: [PATCH 1/2] feat: improve GitHub release workflow with auto-versioning, changelog gen, and streamlined notarization Agent-Logs-Url: https://github.com/farfromrefug/SimulatorCamera/sessions/e627bbd1-ffdd-4170-ad1d-a13adef14223 Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com> --- .github/workflows/release.yml | 222 ++++++++++++++++++++++++++++------ RELEASING.md | 175 ++++++++++++++------------- scripts/build-release.sh | 16 +-- 3 files changed, 284 insertions(+), 129 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e1379d..991c6f2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,33 +1,159 @@ name: Release on: - push: - tags: - - "v*.*.*" workflow_dispatch: inputs: - version: - description: "Version to release (e.g. 0.2.0)" + bump_type: + description: "Version bump type" required: true + type: choice + options: + - patch + - minor + - major + default: patch + create_release: + description: "Create GitHub release and tag" + required: false + type: boolean + default: true jobs: - build-mac: - name: Build & notarize Mac app + test-spm: + name: Swift Package tests runs-on: macos-14 - timeout-minutes: 60 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_15.4.app + - name: swift test + run: swift test - env: - VERSION: ${{ github.event.inputs.version || github.ref_name }} + build-and-release: + name: Build, notarize & release + runs-on: macos-14 + needs: test-spm + timeout-minutes: 60 + permissions: + contents: write steps: - uses: actions/checkout@v4 - - - name: Normalize VERSION (strip leading v) - run: echo "VERSION=${VERSION#v}" >> "$GITHUB_ENV" + with: + fetch-depth: 0 - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode_15.4.app + - name: Compute new version + id: version + run: | + # Latest semver tag, falling back to most recent version in CHANGELOG.md + CURRENT=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 | sed 's/^v//') + if [[ -z "$CURRENT" ]]; then + CURRENT=$(grep -oE '\[([0-9]+\.[0-9]+\.[0-9]+)\]' CHANGELOG.md | head -1 | tr -d '[]') + fi + # Falls back to 0.0.0 when no tag or CHANGELOG entry exists (first release from scratch) + [[ -z "$CURRENT" ]] && CURRENT="0.0.0" + + IFS='.' read -r MAJ MIN PAT <<< "$CURRENT" + case "${{ inputs.bump_type }}" in + major) MAJ=$((MAJ+1)); MIN=0; PAT=0 ;; + minor) MIN=$((MIN+1)); PAT=0 ;; + *) PAT=$((PAT+1)) ;; + esac + + NEW="${MAJ}.${MIN}.${PAT}" + echo "current=$CURRENT" >> "$GITHUB_OUTPUT" + echo "new=$NEW" >> "$GITHUB_OUTPUT" + echo "tag=v$NEW" >> "$GITHUB_OUTPUT" + + - name: Generate release notes + id: notes + run: | + CURRENT_TAG="v${{ steps.version.outputs.current }}" + NEW_VERSION="${{ steps.version.outputs.new }}" + NOTES_FILE="$RUNNER_TEMP/release_notes.md" + + if git rev-parse "$CURRENT_TAG" >/dev/null 2>&1; then + LOG=$(git log "${CURRENT_TAG}..HEAD" --format="%s" 2>/dev/null || true) + else + LOG=$(git log --format="%s" 2>/dev/null || true) + fi + + BREAKING=$(printf '%s\n' "$LOG" | grep -E '^(feat|fix|refactor|perf)(\([^)]+\))?!:' || true) + FEATURES=$(printf '%s\n' "$LOG" | grep -E '^feat(\([^)]+\))?[!]?:' | grep -v '!:' || true) + FIXES=$(printf '%s\n' "$LOG" | grep -E '^fix(\([^)]+\))?[!]?:' | grep -v '!:' || true) + OTHERS=$(printf '%s\n' "$LOG" | grep -E '^(chore|docs|style|refactor|perf|test|ci)(\([^)]+\))?:' || true) + + { + if [[ -n "$BREAKING" ]]; then + printf '### ⚠️ Breaking Changes\n\n' + printf '%s\n' "$BREAKING" | sed 's/^/- /' + printf '\n' + fi + if [[ -n "$FEATURES" ]]; then + printf '### ✨ Features\n\n' + printf '%s\n' "$FEATURES" | sed 's/^/- /' + printf '\n' + fi + if [[ -n "$FIXES" ]]; then + printf '### 🐛 Bug Fixes\n\n' + printf '%s\n' "$FIXES" | sed 's/^/- /' + printf '\n' + fi + if [[ -n "$OTHERS" ]]; then + printf '### 🔧 Other Changes\n\n' + printf '%s\n' "$OTHERS" | sed 's/^/- /' + printf '\n' + fi + if [[ -z "$BREAKING$FEATURES$FIXES$OTHERS" ]]; then + printf '\n\n' "$CURRENT_TAG" + fi + printf '\n**Full Changelog**: https://github.com/%s/compare/%s...%s\n' \ + "${{ github.repository }}" "$CURRENT_TAG" "v${NEW_VERSION}" + } > "$NOTES_FILE" + + echo "file=$NOTES_FILE" >> "$GITHUB_OUTPUT" + + - name: Update CHANGELOG.md + run: | + cat > /tmp/update_changelog.py << 'PYEOF' + import sys, re + + new_version = sys.argv[1] + today = sys.argv[2] + notes_path = sys.argv[3] + + notes = open(notes_path).read().strip() + changelog = open("CHANGELOG.md").read() + + new_entry = "## [{}] — {}\n\n{}\n\n".format(new_version, today, notes) + updated = re.sub( + r"(## \[Unreleased\]\n)", + r"\1\n" + new_entry, + changelog, + count=1, + ) + open("CHANGELOG.md", "w").write(updated) + PYEOF + + python3 /tmp/update_changelog.py \ + "${{ steps.version.outputs.new }}" \ + "$(date +%Y-%m-%d)" \ + "${{ steps.notes.outputs.file }}" + + - name: Update cask version + run: | + NEW_VERSION="${{ steps.version.outputs.new }}" + sed -i '' "s/ version \"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\"/ version \"${NEW_VERSION}\"/" \ + Casks/simulatorcamera.rb + # Reset sha256 to a placeholder until we have the real value after the build + sed -i '' 's/ sha256 "[^"]*"/ sha256 "PENDING"/' \ + Casks/simulatorcamera.rb + - name: Install xcpretty run: gem install xcpretty --no-document @@ -49,38 +175,60 @@ jobs: -s -k "$KEYCHAIN_PASSWORD" build.keychain rm cert.p12 - - name: Build, sign, notarize, package + - name: Store notarytool credentials in keychain + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + xcrun notarytool store-credentials "SimulatorCameraNotary" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" + + - name: Build, sign, notarize & package env: - APPLE_DEVELOPER_ID: ${{ secrets.APPLE_DEVELOPER_ID }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + VERSION: ${{ steps.version.outputs.new }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + KEYCHAIN_PROFILE: SimulatorCameraNotary run: ./scripts/build-release.sh + - name: Update cask sha256 + run: | + NEW_VERSION="${{ steps.version.outputs.new }}" + SHA256=$(awk "/SimulatorCamera-${NEW_VERSION}\.dmg/ { print \$1 }" \ + "dist/SimulatorCamera-${NEW_VERSION}.sha256") + if [[ -z "$SHA256" ]]; then + echo "::error::Could not find sha256 for SimulatorCamera-${NEW_VERSION}.dmg" >&2 + exit 1 + fi + sed -i '' "s/ sha256 \"PENDING\"/ sha256 \"${SHA256}\"/" \ + Casks/simulatorcamera.rb + - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: mac-release-${{ env.VERSION }} + name: mac-release-${{ steps.version.outputs.new }} path: dist/* - - name: Create/Update GitHub Release + - name: Commit version bump + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add CHANGELOG.md Casks/simulatorcamera.rb + git commit -m "release: v${{ steps.version.outputs.new }}" + git push + + - name: Create tag & GitHub release + if: inputs.create_release uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/') with: - name: SimulatorCamera v${{ env.VERSION }} - body_path: docs/RELEASE_NOTES_v${{ env.VERSION }}.md - draft: true + tag_name: ${{ steps.version.outputs.tag }} + target_commitish: ${{ github.ref_name }} + name: "SimulatorCamera ${{ steps.version.outputs.tag }}" + body_path: ${{ steps.notes.outputs.file }} + draft: false files: | - dist/SimulatorCamera-${{ env.VERSION }}.dmg - dist/SimulatorCamera-${{ env.VERSION }}.zip - dist/SimulatorCamera-${{ env.VERSION }}.sha256 - - test-spm: - name: Swift Package tests - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_15.4.app - - name: swift test - run: swift test + dist/SimulatorCamera-${{ steps.version.outputs.new }}.dmg + dist/SimulatorCamera-${{ steps.version.outputs.new }}.zip + dist/SimulatorCamera-${{ steps.version.outputs.new }}.sha256 diff --git a/RELEASING.md b/RELEASING.md index ab5205e..3994a24 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -3,91 +3,96 @@ This runbook cuts a signed, notarized, Homebrew-installable release of the Mac companion app and the iOS Swift Package. -## Prereqs (one-time per machine) - -- Xcode 15.4+ with a configured Apple ID that has Developer ID signing privileges. -- A stored `notarytool` profile so CI and local builds don't need to handle secrets inline: - ``` - xcrun notarytool store-credentials "SimulatorCameraNotary" \ - --apple-id "you@example.com" \ - --team-id "ABCDE12345" \ - --password "app-specific-password" - ``` -- GitHub secrets configured on the repo: - - `MAC_CERTIFICATE_P12_BASE64`, `MAC_CERTIFICATE_PASSWORD`, `KEYCHAIN_PASSWORD` - - `APPLE_DEVELOPER_ID`, `APPLE_ID`, `APPLE_APP_PASSWORD`, `APPLE_TEAM_ID` - -## Cut a release - -1. **Pick a version** (`MAJOR.MINOR.PATCH`). For 0.x we bump MINOR for new - features, PATCH for fixes. Source of truth: `CHANGELOG.md`. - -2. **Prep the repo** - ``` - git checkout main - git pull - ``` - - Move `## [Unreleased]` content into a new `## [X.Y.Z] — YYYY-MM-DD` - section in `CHANGELOG.md`. - - Write `docs/RELEASE_NOTES_vX.Y.Z.md` (user-facing, not a dupe of the - changelog — lead with headlines, link back to the changelog for detail). - - Bump the cask version in `Casks/simulatorcamera.rb` and leave the - `sha256` as a placeholder (step 5 fills it in). - -3. **Local dry run** - ``` - SKIP_NOTARIZE=1 VERSION=X.Y.Z ./scripts/build-release.sh - open dist/ - ``` - Sanity-check the `.app` launches from the `.dmg`. - -4. **Tag and push** - ``` - git commit -am "release: vX.Y.Z" - git tag -s vX.Y.Z -m "SimulatorCamera vX.Y.Z" - git push origin main vX.Y.Z - ``` - The `Release` workflow picks up the tag, builds, notarizes, and uploads - a **draft** GitHub Release with the `.dmg`, `.zip`, and checksums. - -5. **Update the Homebrew cask** - - Grab the DMG sha256 from `dist/SimulatorCamera-X.Y.Z.sha256` (or the - Release page). - - Commit the updated `Casks/simulatorcamera.rb` to the tap repo - (`dautovri/homebrew-tap`): - ``` - brew bump-cask-pr \ - --version X.Y.Z \ - --sha256 \ - dautovri/tap/simulatorcamera - ``` - -6. **Publish the draft Release** - - Double-check the release notes render. - - Un-draft. - -7. **Smoke test** - ``` - brew update - brew upgrade --cask simulatorcamera - open -a SimulatorCameraServer - ``` - Then in a throwaway iOS app: - ```swift - import SimulatorCameraClient - - SimulatorCamera.configure() - SimulatorCamera.start() - ``` - Verify frames arrive at 25–30 FPS. - -8. **Announce** - - Tweet / LinkedIn / /r/iOSProgramming post linking to the Release. - - Update `docs/ROADMAP.md` by moving the just-shipped bullets into the - "Shipped" section and drafting the next milestone. +## Prereqs (one-time per repo) + +GitHub secrets must be configured on the repo: + +| Secret | Purpose | +|---|---| +| `MAC_CERTIFICATE_P12_BASE64` | Developer ID certificate (base64-encoded .p12) | +| `MAC_CERTIFICATE_PASSWORD` | Password for the .p12 file | +| `KEYCHAIN_PASSWORD` | Ephemeral CI keychain password | +| `APPLE_ID` | Apple ID used for notarization | +| `APPLE_APP_PASSWORD` | App-specific password for `notarytool` | +| `APPLE_TEAM_ID` | 10-character Apple team ID | + +## Cut a release (automated) + +The `Release` workflow in `.github/workflows/release.yml` handles the full +release lifecycle. Trigger it from the GitHub Actions tab: + +1. **Go to** Actions → Release → **Run workflow**. +2. **Choose the bump type**: `patch`, `minor`, or `major`. + The workflow reads the latest semver tag (or falls back to `CHANGELOG.md`) + and computes the new version automatically. +3. **Toggle "Create GitHub release and tag"** (default: on). + Turn this off for a dry-run that builds and uploads artifacts without + publishing a release. +4. **Click Run workflow**. + +### What the workflow does + +| Step | Description | +|---|---| +| Swift Package tests | Runs `swift test` as a gate before the build. | +| Compute version | Determines the current version from git tags and bumps it. | +| Generate release notes | Parses conventional commits since the last tag into Breaking / Features / Fixes / Other sections. | +| Update `CHANGELOG.md` | Inserts a new versioned section below `## [Unreleased]`. | +| Update cask | Bumps `version` in `Casks/simulatorcamera.rb`; fills in the real `sha256` after the build. | +| Import certificate | Imports the Developer ID cert into an ephemeral keychain. | +| Store notarytool profile | Stores Apple credentials under the `SimulatorCameraNotary` keychain profile once, so the build step never handles raw secrets. | +| Build, sign, notarize & package | Runs `scripts/build-release.sh` using the stored keychain profile. | +| Commit version bump | Commits `CHANGELOG.md` and `Casks/simulatorcamera.rb` and pushes to the triggering branch. | +| Create release | Creates the git tag and publishes the GitHub Release with the generated notes and `.dmg` / `.zip` / `.sha256` attachments (when `create_release` is true). | + +## Local dry run + +```sh +SKIP_NOTARIZE=1 VERSION=X.Y.Z ./scripts/build-release.sh +open dist/ +``` + +Sanity-check that the `.app` launches from the `.dmg` before triggering +the full workflow. + +## Update the Homebrew tap + +After a successful release the `Casks/simulatorcamera.rb` in **this repo** +is updated automatically. You still need to update the **tap repo** +(`dautovri/homebrew-tap`) separately: + +```sh +brew bump-cask-pr \ + --version X.Y.Z \ + --sha256 \ + dautovri/tap/simulatorcamera +``` + +## Smoke test + +```sh +brew update +brew upgrade --cask simulatorcamera +open -a SimulatorCameraServer +``` + +Then in a throwaway iOS app: +```swift +import SimulatorCameraClient + +SimulatorCamera.configure() +SimulatorCamera.start() +``` +Verify frames arrive at 25–30 FPS. ## If something goes wrong -- **Notarization stuck.** `xcrun notarytool log --keychain-profile SimulatorCameraNotary` — Apple tells you exactly which rule tripped. -- **CI artifacts wrong version.** The workflow normalizes `v0.2.0 → 0.2.0`; if you see mismatched filenames the tag was probably pushed without a `v` prefix. -- **Need to yank a release.** Delete the tag, delete the Release, revert the cask commit. Existing users stay on whatever they have; Homebrew won't downgrade by default. +- **Notarization stuck.** Check the log: + `xcrun notarytool log --keychain-profile SimulatorCameraNotary` + Apple tells you exactly which rule tripped. +- **CI artifacts wrong version.** The workflow derives the version from git + tags; if filenames look wrong, check whether an unexpected semver tag + exists in the repo (`git tag --sort=-v:refname | head -5`). +- **Need to yank a release.** Delete the tag, delete the GitHub Release, and + revert the version-bump commit. Existing users stay on whatever they have; + Homebrew won't downgrade by default. diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 03ba587..3e8a4ed 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -10,15 +10,17 @@ # # Required env vars: # VERSION e.g. 0.2.0 (else read from git tag) -# APPLE_DEVELOPER_ID "Developer ID Application: Your Name (TEAMID)" -# APPLE_ID Apple ID used for notarization -# APPLE_APP_PASSWORD app-specific password for notarytool -# APPLE_TEAM_ID 10-char team ID +# APPLE_TEAM_ID 10-char team ID (used in exportOptions.plist) +# +# Notarization — choose one: +# KEYCHAIN_PROFILE name of a stored notarytool keychain profile +# (preferred; avoids passing credentials inline) +# APPLE_ID + APPLE_APP_PASSWORD + APPLE_TEAM_ID +# inline credentials used when KEYCHAIN_PROFILE +# is not set # # Optional env vars: -# KEYCHAIN_PROFILE reuse a stored notarytool profile instead of -# ID+password (takes precedence if set) -# SKIP_NOTARIZE=1 for local/dev builds +# SKIP_NOTARIZE=1 skip notarization entirely (local/dev builds) # set -euo pipefail From b685e6f2a04e1e449df62065294cba8794490660 Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Tue, 28 Apr 2026 21:05:01 +0200 Subject: [PATCH 2/2] chore: ci --- .github/workflows/release.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 991c6f2..b4bd824 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,6 +48,7 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_15.4.app - name: Compute new version + if: inputs.create_release id: version run: | # Latest semver tag, falling back to most recent version in CHANGELOG.md @@ -71,6 +72,7 @@ jobs: echo "tag=v$NEW" >> "$GITHUB_OUTPUT" - name: Generate release notes + if: inputs.create_release id: notes run: | CURRENT_TAG="v${{ steps.version.outputs.current }}" @@ -146,6 +148,7 @@ jobs: "${{ steps.notes.outputs.file }}" - name: Update cask version + if: inputs.create_release run: | NEW_VERSION="${{ steps.version.outputs.new }}" sed -i '' "s/ version \"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\"/ version \"${NEW_VERSION}\"/" \ @@ -159,9 +162,9 @@ jobs: - name: Import Developer ID certificate env: - MAC_CERTIFICATE_P12_BASE64: ${{ secrets.MAC_CERTIFICATE_P12_BASE64 }} - MAC_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + MAC_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE }} + MAC_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} run: | echo "$MAC_CERTIFICATE_P12_BASE64" | base64 --decode > cert.p12 security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain @@ -194,6 +197,7 @@ jobs: run: ./scripts/build-release.sh - name: Update cask sha256 + if: inputs.create_release run: | NEW_VERSION="${{ steps.version.outputs.new }}" SHA256=$(awk "/SimulatorCamera-${NEW_VERSION}\.dmg/ { print \$1 }" \ @@ -212,6 +216,7 @@ jobs: path: dist/* - name: Commit version bump + if: inputs.create_release run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com"