Merge commit from fork #507
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| jobs: | |
| release-please: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| outputs: | |
| release_created: ${{ steps.release.outputs.release_created }} | |
| version: ${{ steps.release.outputs.version }} | |
| tag_name: ${{ steps.release.outputs.tag_name }} | |
| steps: | |
| - uses: googleapis/release-please-action@v5 | |
| id: release | |
| with: | |
| config-file: .release-please-config.json | |
| manifest-file: .release-please-manifest.json | |
| build: | |
| needs: release-please | |
| if: ${{ needs.release-please.outputs.release_created }} | |
| runs-on: macos-15 | |
| permissions: | |
| contents: write | |
| actions: write | |
| env: | |
| SIGNING_IDENTITY: "Developer ID Application: ALL TUNER LABS S.L. (J5TAY75Q3F)" | |
| APP_NAME: "Factory Floor" | |
| SCHEME: "FactoryFloor" | |
| PROJECT: "FactoryFloor.xcodeproj" | |
| SPARKLE_VERSION: "2.9.0" | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| submodules: true | |
| - name: Log build environment | |
| run: | | |
| xcodebuild -version | |
| swift --version | |
| - name: Install XcodeGen | |
| run: brew install xcodegen | |
| - name: Install Zig | |
| uses: ./.github/actions/install-zig | |
| with: | |
| version: 0.15.2 | |
| - uses: oven-sh/setup-bun@v2 | |
| - name: Cache SPM packages | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| build/derived/SourcePackages | |
| key: spm-${{ hashFiles('project.yml') }} | |
| restore-keys: spm- | |
| - name: Cache Ghostty xcframework | |
| id: ghostty-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ghostty/macos/GhosttyKit.xcframework | |
| ghostty/zig-out/share | |
| key: ghostty-build-${{ hashFiles('ghostty/.git') }} | |
| - name: Build Ghostty xcframework | |
| if: steps.ghostty-cache.outputs.cache-hit != 'true' | |
| run: | | |
| cd ghostty | |
| zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=universal -Doptimize=ReleaseFast | |
| - name: Setup Sparkle | |
| uses: jozefizso/setup-sparkle@v2 | |
| with: | |
| version: ${{ env.SPARKLE_VERSION }} | |
| - name: Build Monaco editor | |
| run: ./scripts/build-editor.sh | |
| - name: Generate Xcode project | |
| run: xcodegen generate | |
| - name: Import signing certificate | |
| env: | |
| CERTIFICATE_P12_BASE64: ${{ secrets.CERTIFICATE_P12_BASE64 }} | |
| CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} | |
| run: | | |
| CERT_PATH=$RUNNER_TEMP/certificate.p12 | |
| KEYCHAIN_PATH=$RUNNER_TEMP/signing.keychain-db | |
| KEYCHAIN_PASSWORD=$(uuidgen) | |
| echo -n "$CERTIFICATE_P12_BASE64" | base64 --decode -o "$CERT_PATH" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security import "$CERT_PATH" -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain-db | |
| - name: Build release | |
| run: | | |
| xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \ | |
| -destination 'generic/platform=macOS' \ | |
| -derivedDataPath build/derived \ | |
| -clonedSourcePackagesDirPath build/derived/SourcePackages \ | |
| DEVELOPMENT_TEAM=J5TAY75Q3F \ | |
| CODE_SIGN_IDENTITY="$SIGNING_IDENTITY" \ | |
| CODE_SIGN_STYLE=Manual \ | |
| ENABLE_HARDENED_RUNTIME=YES \ | |
| CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO \ | |
| OTHER_CODE_SIGN_FLAGS="--timestamp --options=runtime" \ | |
| build | |
| - name: Upload debug symbols to Sentry | |
| continue-on-error: true | |
| env: | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| run: | | |
| if [ -z "$SENTRY_AUTH_TOKEN" ]; then | |
| echo "::warning::SENTRY_AUTH_TOKEN not set, skipping dSYM upload" | |
| exit 0 | |
| fi | |
| brew install getsentry/tools/sentry-cli | |
| sentry-cli debug-files upload \ | |
| --org all-tuner-labs \ | |
| --project factory-floor \ | |
| build/derived/Build/Products/Release/*.dSYM | |
| - name: Package and sign | |
| env: | |
| VERSION: ${{ needs.release-please.outputs.version }} | |
| run: | | |
| APP_BUILT=$(find build/derived -name "${APP_NAME}.app" -type d | head -1) | |
| if [ -z "$APP_BUILT" ]; then | |
| echo "Error: Built app not found" | |
| exit 1 | |
| fi | |
| mkdir -p build/release | |
| cp -R "$APP_BUILT" "build/release/${APP_NAME}.app" | |
| APP="build/release/${APP_NAME}.app" | |
| # Codesign Sparkle framework components (XPC services, helpers, then framework) | |
| codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options=runtime \ | |
| "$APP/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc" | |
| codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options=runtime \ | |
| "$APP/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc" | |
| codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options=runtime \ | |
| "$APP/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate" | |
| codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options=runtime \ | |
| "$APP/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app" | |
| codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options=runtime \ | |
| "$APP/Contents/Frameworks/Sparkle.framework" | |
| # Re-sign embedded frameworks with secure timestamp and hardened runtime | |
| find "$APP/Contents/Frameworks" -type f -perm +111 -o -name "*.dylib" | while read -r bin; do | |
| codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options=runtime "$bin" | |
| done | |
| # Re-sign helpers with hardened runtime and secure timestamp | |
| find "$APP/Contents/Helpers" -type f -perm +111 | while read -r bin; do | |
| codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options=runtime "$bin" | |
| done | |
| # Sign the main app binary (not --deep, nested code is already signed above) | |
| codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options=runtime \ | |
| --entitlements Resources/ff2.entitlements "$APP" | |
| codesign --verify --verbose=2 --deep --strict "$APP" | |
| DMG_NAME="${SCHEME}.dmg" | |
| brew install create-dmg | |
| DMG_STAGING="build/dmg-staging" | |
| mkdir -p "$DMG_STAGING" | |
| cp -R "$APP" "$DMG_STAGING/" | |
| # create-dmg exits non-zero when skipping deprecated internet-enable | |
| create-dmg \ | |
| --volname "$APP_NAME" \ | |
| --background "Resources/dmg-background@2x.png" \ | |
| --window-size 660 500 \ | |
| --icon-size 128 \ | |
| --icon "${APP_NAME}.app" 170 190 \ | |
| --app-drop-link 490 190 \ | |
| --no-internet-enable \ | |
| "build/release/$DMG_NAME" \ | |
| "$DMG_STAGING" || true | |
| if [ ! -f "build/release/$DMG_NAME" ]; then | |
| echo "Error: DMG was not created" | |
| exit 1 | |
| fi | |
| codesign --sign "$SIGNING_IDENTITY" --timestamp "build/release/$DMG_NAME" | |
| echo "DMG_NAME=$DMG_NAME" >> "$GITHUB_ENV" | |
| - name: Store notarization credentials | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} | |
| run: | | |
| xcrun notarytool store-credentials "notarize-profile" \ | |
| --apple-id "$APPLE_ID" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --password "$APPLE_APP_PASSWORD" \ | |
| --keychain "$RUNNER_TEMP/signing.keychain-db" | |
| - name: Notarize | |
| run: | | |
| SUBMIT_OUT=$(xcrun notarytool submit "build/release/$DMG_NAME" \ | |
| --keychain-profile "notarize-profile" \ | |
| --keychain "$RUNNER_TEMP/signing.keychain-db" \ | |
| --wait 2>&1) || true | |
| echo "$SUBMIT_OUT" | |
| SUBMISSION_ID=$(echo "$SUBMIT_OUT" | grep 'id:' | head -1 | awk '{print $2}') | |
| if echo "$SUBMIT_OUT" | grep -q "status: Invalid"; then | |
| echo "::error::Notarization failed. Fetching detailed log..." | |
| xcrun notarytool log "$SUBMISSION_ID" \ | |
| --keychain-profile "notarize-profile" \ | |
| --keychain "$RUNNER_TEMP/signing.keychain-db" | |
| exit 1 | |
| fi | |
| xcrun stapler staple "build/release/$DMG_NAME" | |
| - name: Generate appcast | |
| env: | |
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | |
| GH_TOKEN: ${{ github.token }} | |
| VERSION: ${{ needs.release-please.outputs.version }} | |
| TAG_NAME: ${{ needs.release-please.outputs.tag_name }} | |
| run: | | |
| echo "$SPARKLE_PRIVATE_KEY" > /tmp/sparkle_key | |
| SIGN_OUTPUT=$(sign_update -f /tmp/sparkle_key "build/release/$DMG_NAME") | |
| rm -f /tmp/sparkle_key | |
| # sign_update outputs: sparkle:edSignature="<sig>" length="<len>" | |
| SIGNATURE=$(echo "$SIGN_OUTPUT" | sed -n 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p') | |
| if [ -z "$SIGNATURE" ]; then | |
| echo "::error::Failed to extract Ed25519 signature from sign_update output" | |
| echo "sign_update output: $SIGN_OUTPUT" | |
| exit 1 | |
| fi | |
| # Download existing appcast from previous release to merge with (preserves items | |
| # for the Homebrew update popover). Cannot use --latest because release-please | |
| # already created the current (empty) release. | |
| PREV_TAG=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 10 --json tagName --jq "[.[] | select(.tagName != \"$TAG_NAME\")][0].tagName") | |
| if [ -n "$PREV_TAG" ]; then | |
| gh release download "$PREV_TAG" --repo "$GITHUB_REPOSITORY" --pattern "appcast.xml" --output existing-appcast.xml || true | |
| fi | |
| DMG_URL="https://github.com/alltuner/factoryfloor/releases/download/v${VERSION}/${DMG_NAME}" | |
| python3 scripts/generate_appcast.py \ | |
| --version "$VERSION" \ | |
| --signature "$SIGNATURE" \ | |
| --dmg-path "build/release/$DMG_NAME" \ | |
| --dmg-url "$DMG_URL" \ | |
| --changelog CHANGELOG.md \ | |
| --existing existing-appcast.xml \ | |
| --output appcast.xml | |
| - name: Upload to release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| VERSION: ${{ needs.release-please.outputs.version }} | |
| TAG_NAME: ${{ needs.release-please.outputs.tag_name }} | |
| run: | | |
| for attempt in 1 2 3 4 5; do | |
| if gh release upload --clobber "$TAG_NAME" "build/release/$DMG_NAME" appcast.xml; then | |
| exit 0 | |
| fi | |
| if [ "$attempt" -eq 5 ]; then | |
| echo "::error::Failed to upload release assets for $TAG_NAME after $attempt attempts" | |
| exit 1 | |
| fi | |
| sleep 10 | |
| done | |
| - name: Update Homebrew cask | |
| env: | |
| GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| VERSION: ${{ needs.release-please.outputs.version }} | |
| run: | | |
| DMG_URL="https://github.com/alltuner/factoryfloor/releases/download/v${VERSION}/${DMG_NAME}" | |
| SHA256=$(shasum -a 256 "build/release/$DMG_NAME" | awk '{print $1}') | |
| CASK_SHA=$(gh api repos/alltuner/homebrew-tap/contents/Casks/factoryfloor.rb \ | |
| --method GET -q '.sha') | |
| CASK_CONTENT=$(cat <<RUBY | |
| cask "factoryfloor" do | |
| version "${VERSION}" | |
| sha256 "${SHA256}" | |
| url "${DMG_URL}" | |
| name "Factory Floor" | |
| desc "AI-powered development workspace for macOS" | |
| homepage "https://factory-floor.com" | |
| depends_on macos: ">= :sonoma" | |
| app "Factory Floor.app" | |
| binary "#{appdir}/Factory Floor.app/Contents/Resources/ff", target: "ff" | |
| zap trash: [ | |
| "~/.config/factoryfloor", | |
| "~/.factoryfloor", | |
| "~/Library/Preferences/com.alltuner.factoryfloor.plist", | |
| ] | |
| end | |
| RUBY | |
| ) | |
| gh api repos/alltuner/homebrew-tap/contents/Casks/factoryfloor.rb \ | |
| --method PUT \ | |
| -f message="chore: update factoryfloor to v${VERSION}" \ | |
| -f content="$(echo "$CASK_CONTENT" | base64)" \ | |
| -f sha="$CASK_SHA" | |
| - name: Trigger website deploy | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh workflow run deploy-website.yml | |
| - name: Clean up keychain | |
| if: always() | |
| run: security delete-keychain $RUNNER_TEMP/signing.keychain-db 2>/dev/null || true |