Refresh rolling release metadata and versioning #30
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: Publish Main Build | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| build-and-release-mac-downloads: | |
| runs-on: macos-latest | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Capture previous rolling release commit | |
| run: | | |
| set -euo pipefail | |
| git fetch --force --tags origin | |
| previous_sha="$(git rev-list -n 1 refs/tags/main-build 2>/dev/null || true)" | |
| echo "PREVIOUS_MAIN_BUILD_SHA=$previous_sha" >> "$GITHUB_ENV" | |
| - name: Validate release signing secrets | |
| env: | |
| MACOS_DEVELOPER_ID_CERT_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12_BASE64 }} | |
| MACOS_DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_CERT_PASSWORD }} | |
| MACOS_DEVELOPER_ID_IDENTITY: ${{ secrets.MACOS_DEVELOPER_ID_IDENTITY }} | |
| MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} | |
| APPLE_NOTARY_API_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }} | |
| APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }} | |
| APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} | |
| run: | | |
| set -euo pipefail | |
| missing=0 | |
| for name in \ | |
| MACOS_DEVELOPER_ID_CERT_P12_BASE64 \ | |
| MACOS_DEVELOPER_ID_CERT_PASSWORD \ | |
| MACOS_DEVELOPER_ID_IDENTITY \ | |
| MACOS_KEYCHAIN_PASSWORD \ | |
| APPLE_NOTARY_API_KEY_P8_BASE64 \ | |
| APPLE_NOTARY_ISSUER_ID \ | |
| APPLE_NOTARY_KEY_ID | |
| do | |
| if [ -z "${!name:-}" ]; then | |
| echo "Missing required secret: $name" >&2 | |
| missing=1 | |
| fi | |
| done | |
| if [ "$missing" -ne 0 ]; then | |
| echo "Refusing to publish unsigned or unnotarized release artifacts." >&2 | |
| exit 1 | |
| fi | |
| - name: Extract release metadata | |
| run: | | |
| set -euo pipefail | |
| version="$(sed -n 's/^DMG_VERSION=\"\([^\"]*\)\"$/\1/p' MacBackend/build_dmg.sh | head -n 1)" | |
| if [ -z "$version" ]; then | |
| echo "Could not determine DMG_VERSION from MacBackend/build_dmg.sh" >&2 | |
| exit 1 | |
| fi | |
| { | |
| echo "RELEASE_VERSION=$version" | |
| echo "RELEASE_MIN_MACOS=12+" | |
| } >> "$GITHUB_ENV" | |
| - name: Generate rolling release summary | |
| run: | | |
| set -euo pipefail | |
| collect_subjects() { | |
| local range="$1" | |
| if [ -n "$range" ]; then | |
| git log --no-merges --pretty=format:'%s' "$range" | |
| else | |
| git log --no-merges --pretty=format:'%s' -n 12 | |
| fi | |
| } | |
| build_notes() { | |
| local range="$1" | |
| local -a notes=() | |
| local seen=$'\n' | |
| local subject="" | |
| while IFS= read -r subject; do | |
| [ -n "$subject" ] || continue | |
| case "$seen" in | |
| *$'\n'"$subject"$'\n'*) continue ;; | |
| esac | |
| notes+=("- $subject") | |
| seen+="$subject"$'\n' | |
| [ "${#notes[@]}" -ge 4 ] && break | |
| done < <(collect_subjects "$range") | |
| if [ "${#notes[@]}" -lt 3 ]; then | |
| while IFS= read -r subject; do | |
| [ -n "$subject" ] || continue | |
| case "$seen" in | |
| *$'\n'"$subject"$'\n'*) continue ;; | |
| esac | |
| notes+=("- $subject") | |
| seen+="$subject"$'\n' | |
| [ "${#notes[@]}" -ge 4 ] && break | |
| done < <(collect_subjects "") | |
| fi | |
| if [ "${#notes[@]}" -eq 0 ]; then | |
| notes=( | |
| "- Refreshed the rolling main build" | |
| "- Updated signed and notarized Mac downloads" | |
| "- Synced the website with the newest release metadata" | |
| ) | |
| fi | |
| printf '%s\n' "${notes[@]}" | |
| } | |
| range="" | |
| if [ -n "${PREVIOUS_MAIN_BUILD_SHA:-}" ] && git cat-file -e "${PREVIOUS_MAIN_BUILD_SHA}^{commit}" 2>/dev/null; then | |
| range="${PREVIOUS_MAIN_BUILD_SHA}..${GITHUB_SHA}" | |
| fi | |
| { | |
| echo "RELEASE_NOTES<<EOF" | |
| build_notes "$range" | |
| echo "EOF" | |
| } >> "$GITHUB_ENV" | |
| - name: Decode release signing assets | |
| env: | |
| MACOS_DEVELOPER_ID_CERT_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12_BASE64 }} | |
| APPLE_NOTARY_API_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }} | |
| APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} | |
| run: | | |
| set -euo pipefail | |
| CERT_PATH="$RUNNER_TEMP/developer-id-application.p12" | |
| API_KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_NOTARY_KEY_ID}.p8" | |
| decode_base64() { | |
| if base64 --help 2>&1 | grep -q -- '--decode'; then | |
| base64 --decode | |
| else | |
| base64 -D | |
| fi | |
| } | |
| echo "$MACOS_DEVELOPER_ID_CERT_P12_BASE64" | decode_base64 > "$CERT_PATH" | |
| echo "$APPLE_NOTARY_API_KEY_P8_BASE64" | decode_base64 > "$API_KEY_PATH" | |
| { | |
| echo "MACOS_CERT_PATH=$CERT_PATH" | |
| echo "MACOS_NOTARY_API_KEY_PATH=$API_KEY_PATH" | |
| } >> "$GITHUB_ENV" | |
| - name: Build macOS app and DMG | |
| working-directory: MacBackend | |
| env: | |
| SIGN_APP: "0" | |
| NOTARIZE_DMG: "0" | |
| REQUIRE_SIGNED_RELEASE: "0" | |
| run: bash build_dmg.sh | |
| - name: Install rcodesign | |
| run: cargo install apple-codesign --locked | |
| - name: Sign and notarize macOS app and DMG | |
| working-directory: MacBackend | |
| env: | |
| MACOS_CERT_PATH: ${{ env.MACOS_CERT_PATH }} | |
| MACOS_DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_CERT_PASSWORD }} | |
| APPLE_NOTARY_API_KEY_PATH: ${{ env.MACOS_NOTARY_API_KEY_PATH }} | |
| APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }} | |
| APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} | |
| run: | | |
| set -euo pipefail | |
| APP_NAME="iCodex-Connect" | |
| BUILD_DIR="build" | |
| STAGING_APP="$BUILD_DIR/dmg-staging/${APP_NAME}.app" | |
| STANDALONE_APP="$BUILD_DIR/${APP_NAME}.app" | |
| API_JSON="$RUNNER_TEMP/appstoreconnect-api-key.json" | |
| DMG_PATH="$(find "$BUILD_DIR" -maxdepth 1 -name "${APP_NAME}-*.dmg" | head -n 1)" | |
| if [ ! -d "$STAGING_APP" ] || [ ! -d "$STANDALONE_APP" ] || [ -z "$DMG_PATH" ]; then | |
| echo "Expected unsigned app bundle outputs were not created." >&2 | |
| exit 1 | |
| fi | |
| rcodesign encode-app-store-connect-api-key \ | |
| --output-path "$API_JSON" \ | |
| "$APPLE_NOTARY_ISSUER_ID" \ | |
| "$APPLE_NOTARY_KEY_ID" \ | |
| "$APPLE_NOTARY_API_KEY_PATH" | |
| rcodesign sign \ | |
| --p12-file "$MACOS_CERT_PATH" \ | |
| --p12-password "$MACOS_DEVELOPER_ID_CERT_PASSWORD" \ | |
| --for-notarization \ | |
| "$STAGING_APP" | |
| rcodesign sign \ | |
| --p12-file "$MACOS_CERT_PATH" \ | |
| --p12-password "$MACOS_DEVELOPER_ID_CERT_PASSWORD" \ | |
| --for-notarization \ | |
| "$STANDALONE_APP" | |
| rm -f "$DMG_PATH" | |
| hdiutil create \ | |
| -volname "$APP_NAME" \ | |
| -srcfolder "$BUILD_DIR/dmg-staging" \ | |
| -ov \ | |
| -format UDZO \ | |
| -imagekey zlib-level=9 \ | |
| "$DMG_PATH" | |
| rcodesign sign \ | |
| --p12-file "$MACOS_CERT_PATH" \ | |
| --p12-password "$MACOS_DEVELOPER_ID_CERT_PASSWORD" \ | |
| --for-notarization \ | |
| "$DMG_PATH" | |
| rcodesign notary-submit \ | |
| --api-key-file "$API_JSON" \ | |
| --wait \ | |
| --staple \ | |
| "$DMG_PATH" | |
| xcrun stapler validate "$DMG_PATH" | |
| MOUNT_DIR="$RUNNER_TEMP/icodex-release-check" | |
| rm -rf "$MOUNT_DIR" | |
| mkdir -p "$MOUNT_DIR" | |
| hdiutil attach "$DMG_PATH" -nobrowse -readonly -mountpoint "$MOUNT_DIR" >/dev/null | |
| spctl --assess -vv "$MOUNT_DIR/${APP_NAME}.app" | |
| hdiutil detach "$MOUNT_DIR" >/dev/null | |
| - name: Prepare stable release assets | |
| working-directory: MacBackend | |
| run: | | |
| set -euo pipefail | |
| APP_NAME="iCodex-Connect" | |
| BUILD_DIR="build" | |
| RELEASE_DIR="release" | |
| mkdir -p "$RELEASE_DIR" | |
| DMG_PATH="$(find "$BUILD_DIR" -maxdepth 1 -name "${APP_NAME}-*.dmg" | head -n 1)" | |
| if [ -z "$DMG_PATH" ]; then | |
| echo "Could not find DMG output." >&2 | |
| exit 1 | |
| fi | |
| if [ ! -d "$BUILD_DIR/${APP_NAME}.app" ]; then | |
| echo "Could not find app bundle output." >&2 | |
| exit 1 | |
| fi | |
| cp "$DMG_PATH" "$RELEASE_DIR/${APP_NAME}.dmg" | |
| ditto -c -k --sequesterRsrc --keepParent \ | |
| "$BUILD_DIR/${APP_NAME}.app" \ | |
| "$RELEASE_DIR/${APP_NAME}.app.zip" | |
| shasum -a 256 \ | |
| "$RELEASE_DIR/${APP_NAME}.dmg" \ | |
| "$RELEASE_DIR/${APP_NAME}.app.zip" \ | |
| > "$RELEASE_DIR/SHA256SUMS.txt" | |
| - name: Move rolling release tag to this commit | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git tag -f main-build "${GITHUB_SHA}" | |
| git push origin refs/tags/main-build --force | |
| - name: Recreate rolling release so timestamps stay fresh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| if gh release view main-build >/dev/null 2>&1; then | |
| gh release delete main-build --yes | |
| fi | |
| - name: Publish rolling release | |
| uses: ncipollo/release-action@v1 | |
| with: | |
| tag: main-build | |
| name: Main Build · v${{ env.RELEASE_VERSION }} | |
| allowUpdates: true | |
| removeArtifacts: true | |
| replacesArtifacts: true | |
| prerelease: false | |
| makeLatest: true | |
| artifacts: | | |
| MacBackend/release/iCodex-Connect.dmg | |
| MacBackend/release/iCodex-Connect.app.zip | |
| MacBackend/release/SHA256SUMS.txt | |
| body: | | |
| What's new: | |
| ${{ env.RELEASE_NOTES }} | |
| Version: `${{ env.RELEASE_VERSION }}` | |
| Minimum macOS: `${{ env.RELEASE_MIN_MACOS }}` | |
| Commit: `${{ github.sha }}` | |
| This release is Developer ID signed and notarized before publication. | |
| Stable asset URLs: | |
| - `https://github.com/${{ github.repository }}/releases/download/main-build/iCodex-Connect.dmg` | |
| - `https://github.com/${{ github.repository }}/releases/download/main-build/iCodex-Connect.app.zip` | |
| - name: Clean up decoded signing assets | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| rm -f "${MACOS_CERT_PATH:-}" "${MACOS_NOTARY_API_KEY_PATH:-}" || true |