Skip to content

Merge commit from fork #507

Merge commit from fork

Merge commit from fork #507

Workflow file for this run

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