Release #56
Workflow file for this run
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: | |
| tags: | |
| - 'v*' | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| runs-on: macos-26 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Run tests | |
| run: swift test | |
| - name: Build CLI release binaries | |
| run: swift build -c release | |
| - name: Package CLI tarball | |
| run: | | |
| mkdir -p staging/agents | |
| cp .build/release/Engram staging/memory | |
| cp .build/release/EngramHooks staging/memory-hooks | |
| cp -R .build/release/Engram_EngramKit.bundle staging/ | |
| cp -R .build/release/swift-transformers_Hub.bundle staging/ | |
| cp -R .build/release/SwiftLM_SwiftLM.bundle staging/ | |
| cp agents/*.md staging/agents/ | |
| cd staging && tar czf ../engram-macos-arm64.tar.gz * | |
| - name: Import signing certificate | |
| if: env.DEVELOPER_ID_CERT_BASE64 != '' | |
| env: | |
| DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }} | |
| DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }} | |
| run: | | |
| KEYCHAIN_PATH=$RUNNER_TEMP/signing.keychain-db | |
| KEYCHAIN_PASSWORD=$(openssl rand -base64 32) | |
| # Create temporary keychain | |
| 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 | |
| echo "$DEVELOPER_ID_CERT_BASE64" | base64 --decode > $RUNNER_TEMP/cert.p12 | |
| security import $RUNNER_TEMP/cert.p12 -P "$DEVELOPER_ID_CERT_PASSWORD" \ | |
| -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Add to search list | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') | |
| - name: Archive app with xcodebuild | |
| if: env.DEVELOPER_ID_CERT_BASE64 != '' | |
| env: | |
| DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| VERSION=${GITHUB_REF_NAME#v} | |
| PREV_BUILD=$(sed -n 's/.*<sparkle:version>\([0-9]*\)<.*/\1/p' appcast.xml | head -1) | |
| BUILD_NUMBER=$(( ${PREV_BUILD:-0} + 1 )) | |
| xcodebuild archive \ | |
| -project Engram.xcodeproj \ | |
| -scheme Engram \ | |
| -archivePath build/Engram.xcarchive \ | |
| -configuration Release \ | |
| -skipMacroValidation \ | |
| -skipPackagePluginValidation \ | |
| ARCHS=arm64 \ | |
| CODE_SIGN_IDENTITY="Developer ID Application" \ | |
| CODE_SIGN_STYLE=Manual \ | |
| DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \ | |
| MARKETING_VERSION="$VERSION" \ | |
| CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \ | |
| OTHER_CODE_SIGN_FLAGS="--keychain $RUNNER_TEMP/signing.keychain-db" | |
| - name: Export archive to .app | |
| if: env.DEVELOPER_ID_CERT_BASE64 != '' | |
| env: | |
| DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| # Inject team ID into ExportOptions.plist | |
| sed -i '' "s|</dict>| <key>teamID</key><string>$APPLE_TEAM_ID</string></dict>|" ExportOptions.plist | |
| xcodebuild -exportArchive \ | |
| -archivePath build/Engram.xcarchive \ | |
| -exportPath build/export \ | |
| -exportOptionsPlist ExportOptions.plist | |
| - name: Bundle CLI binaries into .app | |
| if: env.DEVELOPER_ID_CERT_BASE64 != '' | |
| env: | |
| DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| APP="build/export/Engram.app" | |
| CLI_DIR="$APP/Contents/Resources/cli" | |
| AGENTS_DIR="$CLI_DIR/agents" | |
| mkdir -p "$CLI_DIR" "$AGENTS_DIR" | |
| cp .build/release/Engram "$CLI_DIR/memory" | |
| cp .build/release/EngramHooks "$CLI_DIR/memory-hooks" | |
| cp -R .build/release/Engram_EngramKit.bundle "$CLI_DIR/" | |
| cp -R .build/release/swift-transformers_Hub.bundle "$CLI_DIR/" | |
| cp -R .build/release/SwiftLM_SwiftLM.bundle "$CLI_DIR/" | |
| cp agents/*.md "$AGENTS_DIR/" | |
| sign() { | |
| codesign --force --sign "Developer ID Application" \ | |
| --keychain "$RUNNER_TEMP/signing.keychain-db" \ | |
| --options runtime --timestamp "$1" | |
| } | |
| # Sign CLI binaries | |
| for bin in "$CLI_DIR/memory" "$CLI_DIR/memory-hooks"; do | |
| sign "$bin" | |
| done | |
| # Sign Sparkle components bottom-up (--deep is deprecated and unreliable | |
| # for nested XPC services — causes TeamIdentifier=not set on Installer.xpc, | |
| # which triggers the App Management permission prompt on macOS Ventura+) | |
| SPARKLE="$APP/Contents/Frameworks/Sparkle.framework" | |
| for xpc in "$SPARKLE/Versions/B/XPCServices/"*.xpc; do | |
| sign "$xpc" | |
| done | |
| sign "$SPARKLE/Versions/B/Autoupdate" | |
| sign "$SPARKLE" | |
| # Re-codesign the .app (no --deep) | |
| sign "$APP" | |
| - name: Create DMG | |
| if: env.DEVELOPER_ID_CERT_BASE64 != '' | |
| env: | |
| DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }} | |
| run: | | |
| brew install create-dmg | |
| VERSION=${GITHUB_REF_NAME#v} | |
| create-dmg \ | |
| --volname "Engram $VERSION" \ | |
| --window-pos 200 120 \ | |
| --window-size 600 400 \ | |
| --icon-size 100 \ | |
| --icon "Engram.app" 150 190 \ | |
| --app-drop-link 450 190 \ | |
| --no-internet-enable \ | |
| "Engram-${VERSION}.dmg" \ | |
| "build/export/Engram.app" | |
| - name: Notarize DMG | |
| if: env.DEVELOPER_ID_CERT_BASE64 != '' | |
| env: | |
| DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }} | |
| 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} | |
| DMG="Engram-${VERSION}.dmg" | |
| SUBMIT_OUT=$(xcrun notarytool submit "$DMG" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --wait 2>&1) || true | |
| echo "$SUBMIT_OUT" | |
| # Extract submission ID and fetch log on failure | |
| SUB_ID=$(echo "$SUBMIT_OUT" | grep "id:" | head -1 | awk '{print $2}') | |
| if echo "$SUBMIT_OUT" | grep -q "status: Invalid"; then | |
| echo "--- Notarization log ---" | |
| xcrun notarytool log "$SUB_ID" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" || true | |
| exit 1 | |
| fi | |
| xcrun stapler staple "$DMG" | |
| - name: Generate Sparkle appcast | |
| if: env.SPARKLE_PRIVATE_KEY != '' | |
| env: | |
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | |
| run: | | |
| VERSION=${GITHUB_REF_NAME#v} | |
| DMG="Engram-${VERSION}.dmg" | |
| # Download Sparkle tools | |
| SPARKLE_VERSION="2.8.1" | |
| mkdir -p /tmp/sparkle-tools | |
| curl -sL "https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz" | tar xJ -C /tmp/sparkle-tools bin/ | |
| # Move DMG to a staging dir for generate_appcast | |
| mkdir -p appcast_staging | |
| cp "$DMG" appcast_staging/ | |
| # Generate appcast from the DMG | |
| echo "$SPARKLE_PRIVATE_KEY" | /tmp/sparkle-tools/bin/generate_appcast \ | |
| --ed-key-file - \ | |
| --download-url-prefix "https://github.com/jsflax/Engram/releases/download/${GITHUB_REF_NAME}/" \ | |
| appcast_staging | |
| # Use generated appcast | |
| cp appcast_staging/appcast.xml appcast.xml | |
| - name: Extract changelog for release | |
| id: changelog | |
| run: | | |
| VERSION=${GITHUB_REF_NAME#v} | |
| BODY=$(awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" CHANGELOG.md) | |
| # Write to file for multi-line handling | |
| echo "$BODY" > /tmp/release-body.md | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| engram-macos-arm64.tar.gz | |
| Engram-*.dmg | |
| body_path: /tmp/release-body.md | |
| - name: Commit appcast.xml to main | |
| if: env.SPARKLE_PRIVATE_KEY != '' | |
| env: | |
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git fetch origin main | |
| git checkout main | |
| git add appcast.xml | |
| git diff --cached --quiet || (git commit -m "Update appcast.xml for ${GITHUB_REF_NAME}" && git push origin main) | |
| - name: Update Homebrew formula | |
| if: env.TAP_TOKEN != '' | |
| env: | |
| TAP_TOKEN: ${{ secrets.TAP_TOKEN }} | |
| run: | | |
| SHA=$(shasum -a 256 engram-macos-arm64.tar.gz | awk '{print $1}') | |
| VERSION=${GITHUB_REF_NAME#v} | |
| git clone https://x-access-token:${TAP_TOKEN}@github.com/jsflax/homebrew-tap.git tap | |
| cd tap | |
| # Update version and sha256 in existing formula | |
| sed -i '' "s/version \".*\"/version \"${VERSION}\"/" Formula/engram.rb | |
| sed -i '' "s/sha256 \".*\"/sha256 \"${SHA}\"/" Formula/engram.rb | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add Formula/engram.rb | |
| git diff --cached --quiet || (git commit -m "Update engram to ${VERSION}" && git push) | |
| - name: Notify Slack | |
| if: success() && env.SLACK_WEBHOOK_URL != '' | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| run: | | |
| TAG="${GITHUB_REF_NAME}" | |
| VERSION="${TAG#v}" | |
| REPO="${{ github.repository }}" | |
| URL="https://github.com/${REPO}/releases/tag/${TAG}" | |
| # Extract changelog section and convert Markdown → Slack mrkdwn | |
| CHANGES=$(awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" CHANGELOG.md \ | |
| | sed '/^$/d' \ | |
| | sed 's/^### \(.*\)/*\1*/' \ | |
| | sed 's/\*\*\([^*]*\)\*\*/*\1*/g' \ | |
| | sed 's/^- /• /' \ | |
| | sed 's/`\([^`]*\)`/`\1`/g' \ | |
| | head -20) | |
| # Escape for JSON | |
| CHANGES_ESCAPED=$(echo "$CHANGES" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read())[1:-1])') | |
| curl -s -X POST "$SLACK_WEBHOOK_URL" \ | |
| -H 'Content-Type: application/json' \ | |
| -d "{ | |
| \"blocks\": [ | |
| { | |
| \"type\": \"header\", | |
| \"text\": {\"type\": \"plain_text\", \"text\": \"✅ Engram ${TAG} released\"} | |
| }, | |
| { | |
| \"type\": \"section\", | |
| \"text\": {\"type\": \"mrkdwn\", \"text\": \"${CHANGES_ESCAPED}\"}, | |
| \"accessory\": { | |
| \"type\": \"button\", | |
| \"text\": {\"type\": \"plain_text\", \"text\": \"View Release\"}, | |
| \"url\": \"${URL}\" | |
| } | |
| } | |
| ] | |
| }" | |
| - name: Cleanup keychain | |
| if: always() | |
| run: | | |
| if [ -f "$RUNNER_TEMP/signing.keychain-db" ]; then | |
| security delete-keychain "$RUNNER_TEMP/signing.keychain-db" 2>/dev/null || true | |
| fi |