From 8e53dee03caf746540469fbde666a15ac3d0e7d5 Mon Sep 17 00:00:00 2001 From: Mathias Beaulieu-Duncan Date: Tue, 30 Dec 2025 04:35:36 -0500 Subject: [PATCH] Add CI/CD pipeline and documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Gitea Actions workflow for automated releases - Builds release binary - Signs app with Developer ID - Creates and signs DMG - Notarizes with Apple - Uploads to release - Add documentation: - macos-runner-setup.md: Self-hosted runner setup guide - pipeline-configuration.md: Secrets and pipeline config guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitea/workflows/release.yml | 150 +++++++++++++++++ docs/macos-runner-setup.md | 187 +++++++++++++++++++++ docs/pipeline-configuration.md | 294 +++++++++++++++++++++++++++++++++ 3 files changed, 631 insertions(+) create mode 100644 .gitea/workflows/release.yml create mode 100644 docs/macos-runner-setup.md create mode 100644 docs/pipeline-configuration.md diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..2ca574b --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,150 @@ +name: Build and Release + +on: + release: + types: [created] + +jobs: + build: + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Swift + uses: swift-actions/setup-swift@v2 + with: + swift-version: "6.0" + + - name: Install Certificate + env: + APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + # Create temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + # Import certificate + CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12 + echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH" + + security import "$CERTIFICATE_PATH" \ + -P "$APPLE_CERTIFICATE_PASSWORD" \ + -A -t cert -f pkcs12 \ + -k "$KEYCHAIN_PATH" + + security list-keychain -d user -s "$KEYCHAIN_PATH" + + # Allow codesign to access keychain + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + - name: Build Release Binary + run: swift build -c release --product AppleIntelligenceApp + + - name: Create App Bundle + run: | + APP_NAME="Apple Intelligence Server" + VERSION="${GITHUB_REF_NAME#v}" + + mkdir -p "dist/$APP_NAME.app/Contents/MacOS" + mkdir -p "dist/$APP_NAME.app/Contents/Resources" + + cp .build/release/AppleIntelligenceApp "dist/$APP_NAME.app/Contents/MacOS/$APP_NAME" + + cat > "dist/$APP_NAME.app/Contents/Info.plist" << EOF + + + + + CFBundleExecutable + $APP_NAME + CFBundleIdentifier + com.svrnty.apple-intelligence-server + CFBundleName + $APP_NAME + CFBundlePackageType + APPL + CFBundleShortVersionString + $VERSION + CFBundleVersion + 1 + LSMinimumSystemVersion + 26.0 + LSUIElement + + NSHighResolutionCapable + + NSLocalNetworkUsageDescription + Apple Intelligence Server needs local network access to accept connections from other devices. + NSPrincipalClass + NSApplication + + + EOF + + echo -n "APPL????" > "dist/$APP_NAME.app/Contents/PkgInfo" + + - name: Sign App + run: | + codesign --deep --force --verify --verbose \ + --options runtime \ + --sign "Developer ID Application: Mathias Beaulieu-Duncan (LD76P8L42W)" \ + "dist/Apple Intelligence Server.app" + + - name: Create DMG + run: | + VERSION="${GITHUB_REF_NAME#v}" + + mkdir -p dist/dmg-temp + cp -R "dist/Apple Intelligence Server.app" dist/dmg-temp/ + ln -s /Applications dist/dmg-temp/Applications + + hdiutil create -volname "Apple Intelligence Server" \ + -srcfolder dist/dmg-temp \ + -ov -format UDRW dist/temp.dmg + + hdiutil convert dist/temp.dmg -format UDZO \ + -o "dist/AppleIntelligenceServer-$VERSION.dmg" + + rm -rf dist/dmg-temp dist/temp.dmg + + - name: Sign DMG + run: | + VERSION="${GITHUB_REF_NAME#v}" + codesign --force \ + --sign "Developer ID Application: Mathias Beaulieu-Duncan (LD76P8L42W)" \ + "dist/AppleIntelligenceServer-$VERSION.dmg" + + - name: Notarize DMG + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + VERSION="${GITHUB_REF_NAME#v}" + + xcrun notarytool submit "dist/AppleIntelligenceServer-$VERSION.dmg" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + + - name: Staple DMG + run: | + VERSION="${GITHUB_REF_NAME#v}" + xcrun stapler staple "dist/AppleIntelligenceServer-$VERSION.dmg" + + - name: Upload to Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${GITHUB_REF_NAME#v}" + gh release upload "$GITHUB_REF_NAME" \ + "dist/AppleIntelligenceServer-$VERSION.dmg" \ + --clobber diff --git a/docs/macos-runner-setup.md b/docs/macos-runner-setup.md new file mode 100644 index 0000000..94ac67a --- /dev/null +++ b/docs/macos-runner-setup.md @@ -0,0 +1,187 @@ +# Setting Up a macOS Self-Hosted Runner for Gitea Actions + +This guide explains how to set up a self-hosted Gitea Actions runner on macOS for building, signing, and notarizing the Apple Intelligence Server app. + +## Prerequisites + +- A Mac (Intel or Apple Silicon) running macOS 13+ +- Admin access to your Gitea instance +- Xcode Command Line Tools installed + +## Step 1: Install Xcode Command Line Tools + +```bash +xcode-select --install +``` + +## Step 2: Install Swift + +Ensure Swift 6.0+ is installed: + +```bash +swift --version +``` + +If not installed, download from [swift.org](https://swift.org/download/) or install via Xcode. + +## Step 3: Download Gitea Act Runner + +Download the latest release from [gitea/act_runner](https://gitea.com/gitea/act_runner/releases): + +```bash +# For Apple Silicon (M1/M2/M3) +curl -L -o act_runner https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-darwin-arm64 + +# For Intel Mac +curl -L -o act_runner https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-darwin-amd64 + +# Make executable +chmod +x act_runner +``` + +## Step 4: Get Registration Token + +1. Go to your Gitea repository +2. Navigate to **Settings** → **Actions** → **Runners** +3. Click **Create new Runner** +4. Copy the registration token + +## Step 5: Register the Runner + +```bash +./act_runner register \ + --instance https://your-gitea-instance.com \ + --token YOUR_REGISTRATION_TOKEN \ + --name "macos-runner" \ + --labels "macos,macos-latest,self-hosted" +``` + +When prompted: +- **Runner name:** `macos-runner` (or any name you prefer) +- **Labels:** `macos,macos-latest,self-hosted` + +This creates a `.runner` configuration file in the current directory. + +## Step 6: Run the Runner + +### Option A: Run in Foreground (for testing) + +```bash +./act_runner daemon +``` + +### Option B: Run as a Background Service (recommended) + +Create a LaunchAgent to run the runner automatically: + +```bash +mkdir -p ~/Library/LaunchAgents +``` + +Create the plist file: + +```bash +cat > ~/Library/LaunchAgents/com.gitea.act_runner.plist << 'EOF' + + + + + Label + com.gitea.act_runner + ProgramArguments + + /Users/YOUR_USERNAME/act_runner/act_runner + daemon + + WorkingDirectory + /Users/YOUR_USERNAME/act_runner + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/YOUR_USERNAME/act_runner/runner.log + StandardErrorPath + /Users/YOUR_USERNAME/act_runner/runner.error.log + + +EOF +``` + +**Important:** Replace `YOUR_USERNAME` with your actual macOS username. + +Load the service: + +```bash +launchctl load ~/Library/LaunchAgents/com.gitea.act_runner.plist +``` + +## Step 7: Verify Runner is Connected + +1. Go to your Gitea repository +2. Navigate to **Settings** → **Actions** → **Runners** +3. Your runner should appear as **Online** + +## Managing the Runner + +### Check Status + +```bash +launchctl list | grep act_runner +``` + +### Stop the Runner + +```bash +launchctl unload ~/Library/LaunchAgents/com.gitea.act_runner.plist +``` + +### Start the Runner + +```bash +launchctl load ~/Library/LaunchAgents/com.gitea.act_runner.plist +``` + +### View Logs + +```bash +tail -f ~/act_runner/runner.log +tail -f ~/act_runner/runner.error.log +``` + +## Security Considerations + +1. **Dedicated User:** Consider creating a dedicated macOS user for the runner +2. **Keychain Access:** The runner needs access to the keychain for code signing +3. **Network:** Ensure the Mac has reliable network access to your Gitea instance +4. **Updates:** Keep the runner updated to the latest version + +## Troubleshooting + +### Runner Not Connecting + +- Check firewall settings +- Verify the Gitea instance URL is correct +- Ensure the registration token hasn't expired + +### Code Signing Fails + +- Ensure certificates are installed in the login keychain +- Check that the runner has keychain access +- Verify certificate names match the workflow + +### Build Fails with Swift Errors + +- Ensure Xcode Command Line Tools are installed +- Check Swift version compatibility +- Clear the build cache: `swift package clean` + +## Recommended Directory Structure + +``` +~/act_runner/ +├── act_runner # The runner binary +├── .runner # Runner configuration +├── runner.log # Standard output log +└── runner.error.log # Error log +``` diff --git a/docs/pipeline-configuration.md b/docs/pipeline-configuration.md new file mode 100644 index 0000000..8a6540a --- /dev/null +++ b/docs/pipeline-configuration.md @@ -0,0 +1,294 @@ +# CI/CD Pipeline Configuration Guide + +This guide explains how to configure the Gitea Actions pipeline for automated building, signing, and notarizing the Apple Intelligence Server app. + +## Overview + +The pipeline automatically: +1. Builds the app when a release is created +2. Creates a signed `.app` bundle +3. Packages it into a DMG +4. Signs and notarizes the DMG with Apple +5. Uploads the DMG to the release + +## Prerequisites + +- A macOS self-hosted runner (see [macos-runner-setup.md](./macos-runner-setup.md)) +- Apple Developer Program membership ($99/year) +- Developer ID Application certificate + +## Step 1: Export Your Signing Certificate + +### Using Keychain Access (GUI) + +1. Open **Keychain Access** +2. Find "Developer ID Application: Your Name" +3. Right-click → **Export** +4. Save as `.p12` file +5. Set a strong password + +### Using Command Line + +```bash +security find-identity -v -p codesigning | grep "Developer ID" +# Note the certificate name + +security export -k ~/Library/Keychains/login.keychain-db \ + -t identities \ + -f pkcs12 \ + -P "YOUR_SECURE_PASSWORD" \ + -o ~/Desktop/developer-id.p12 +``` + +### Convert to Base64 + +```bash +base64 -i ~/Desktop/developer-id.p12 | tr -d '\n' > ~/Desktop/certificate-base64.txt +``` + +Copy the contents of `certificate-base64.txt` for the next step. + +## Step 2: Create an App-Specific Password + +Apple requires an app-specific password for notarization: + +1. Go to [appleid.apple.com](https://appleid.apple.com/account/manage) +2. Sign in with your Apple ID +3. Go to **Sign-In and Security** → **App-Specific Passwords** +4. Click **Generate an app-specific password** +5. Name it "Gitea CI" or similar +6. Copy the generated password (format: `xxxx-xxxx-xxxx-xxxx`) + +## Step 3: Configure Gitea Secrets + +Go to your Gitea repository → **Settings** → **Actions** → **Secrets** + +Add the following secrets: + +| Secret Name | Description | Example | +|-------------|-------------|---------| +| `APPLE_CERTIFICATE_BASE64` | Base64-encoded .p12 certificate | `MIIKYgIBAzCCCh...` | +| `APPLE_CERTIFICATE_PASSWORD` | Password for the .p12 file | `your-p12-password` | +| `APPLE_ID` | Your Apple ID email | `you@example.com` | +| `APPLE_APP_PASSWORD` | App-specific password | `xxxx-xxxx-xxxx-xxxx` | +| `APPLE_TEAM_ID` | Your Apple Developer Team ID | `LD76P8L42W` | + +### Finding Your Team ID + +Your Team ID is shown in: +- Apple Developer portal → Membership → Team ID +- Or in your certificate name: "Developer ID Application: Name (TEAM_ID)" + +## Step 4: Workflow File + +The workflow file is located at `.gitea/workflows/release.yml`. + +### Workflow Triggers + +The workflow triggers when a release is created: + +```yaml +on: + release: + types: [created] +``` + +### Key Steps Explained + +#### Install Certificate + +Creates a temporary keychain and imports the signing certificate: + +```yaml +- name: Install Certificate + env: + APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + # Creates temporary keychain + # Imports certificate + # Configures codesign access +``` + +#### Build Release Binary + +Compiles the Swift project in release mode: + +```yaml +- name: Build Release Binary + run: swift build -c release --product AppleIntelligenceApp +``` + +#### Sign App + +Signs the app bundle with hardened runtime: + +```yaml +- name: Sign App + run: | + codesign --deep --force --verify --verbose \ + --options runtime \ + --sign "Developer ID Application: Your Name (TEAM_ID)" \ + "dist/Apple Intelligence Server.app" +``` + +The `--options runtime` flag enables hardened runtime, required for notarization. + +#### Notarize DMG + +Submits the DMG to Apple for notarization: + +```yaml +- name: Notarize DMG + run: | + xcrun notarytool submit "dist/AppleIntelligenceServer-$VERSION.dmg" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait +``` + +The `--wait` flag makes the command wait until notarization completes. + +#### Staple DMG + +Attaches the notarization ticket to the DMG: + +```yaml +- name: Staple DMG + run: xcrun stapler staple "dist/AppleIntelligenceServer-$VERSION.dmg" +``` + +## Step 5: Creating a Release + +To trigger the pipeline: + +1. Go to your Gitea repository +2. Click **Releases** → **New Release** +3. Create a tag (e.g., `v1.0.0`) +4. Fill in release title and description +5. Click **Publish Release** + +The pipeline will automatically: +- Build the app +- Sign and notarize +- Upload the DMG to the release + +## Version Numbering + +The workflow extracts the version from the Git tag: + +```yaml +VERSION="${GITHUB_REF_NAME#v}" # v1.0.0 → 1.0.0 +``` + +Tag your releases as `v1.0.0`, `v1.1.0`, etc. + +## Monitoring Pipeline Runs + +1. Go to your Gitea repository +2. Click **Actions** +3. View the running or completed workflows +4. Click on a run to see detailed logs + +## Troubleshooting + +### Certificate Import Fails + +**Error:** `security: SecKeychainItemImport: The specified item already exists in the keychain` + +**Solution:** The temporary keychain may not be cleaning up. Check the workflow logs. + +### Notarization Fails + +**Error:** `Error: Unable to upload your app for notarization` + +**Solutions:** +- Verify Apple ID credentials are correct +- Ensure app-specific password is valid +- Check that the app is properly signed with hardened runtime + +### Notarization Rejected + +**Error:** `Package Invalid` + +**Solutions:** +- Run `xcrun notarytool log ` to see details +- Common issues: + - Missing hardened runtime (`--options runtime`) + - Unsigned nested code + - Invalid entitlements + +### Code Signing Fails + +**Error:** `No identity found` + +**Solutions:** +- Verify certificate is correctly base64-encoded +- Check certificate password is correct +- Ensure certificate hasn't expired + +## Security Best Practices + +1. **Rotate Secrets Regularly:** Update your app-specific password periodically +2. **Certificate Expiration:** Developer ID certificates expire after 5 years +3. **Limit Access:** Only admins should have access to repository secrets +4. **Audit Logs:** Monitor pipeline runs for unauthorized activity + +## Customizing the Pipeline + +### Adding Tests + +```yaml +- name: Run Tests + run: swift test +``` + +### Building for Multiple Architectures + +```yaml +- name: Build Universal Binary + run: | + swift build -c release --arch arm64 --arch x86_64 +``` + +### Custom DMG Background + +Add a background image to make the DMG look professional: + +```yaml +- name: Create DMG with Background + run: | + # Use create-dmg tool for custom styling + brew install create-dmg + create-dmg \ + --volname "Apple Intelligence Server" \ + --background "assets/dmg-background.png" \ + --window-pos 200 120 \ + --window-size 600 400 \ + --icon-size 100 \ + --icon "Apple Intelligence Server.app" 150 190 \ + --app-drop-link 450 185 \ + "dist/AppleIntelligenceServer-$VERSION.dmg" \ + "dist/Apple Intelligence Server.app" +``` + +## Quick Reference + +### Secrets Checklist + +- [ ] `APPLE_CERTIFICATE_BASE64` +- [ ] `APPLE_CERTIFICATE_PASSWORD` +- [ ] `APPLE_ID` +- [ ] `APPLE_APP_PASSWORD` +- [ ] `APPLE_TEAM_ID` + +### Release Checklist + +- [ ] Code is tested and ready +- [ ] Version number updated (if applicable) +- [ ] Create Git tag: `git tag v1.0.0` +- [ ] Push tag: `git push origin v1.0.0` +- [ ] Create release on Gitea +- [ ] Monitor pipeline completion +- [ ] Verify DMG is uploaded to release