release v0.2.0 #7
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 DMG | |
| # Builds the macOS .dmg when a version tag is pushed (e.g. `v0.1.0`) and attaches | |
| # it to the GitHub Release for that tag. Can also be run manually for a dry run. | |
| # | |
| # Signing + notarization are OPTIONAL and gated on repository secrets being set: | |
| # DEVELOPER_ID_APPLICATION "Developer ID Application: Your Name (TEAMID)" | |
| # DEVELOPER_ID_CERT_P12 base64 of the exported .p12 certificate | |
| # DEVELOPER_ID_CERT_PASSWORD password for that .p12 | |
| # APPLE_ID / APPLE_APP_PASSWORD / APPLE_TEAM_ID notarytool credentials | |
| # With those set, the DMG is signed with Developer ID + notarized + stapled. | |
| # Without them, the job still produces an ad-hoc-signed (unsigned) DMG for testing. | |
| # See docs/ci.md. | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Version tag to build (e.g. v0.1.0)" | |
| required: true | |
| permissions: | |
| contents: write # create/update the GitHub Release and upload the DMG | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| build: | |
| # Apple Silicon runner with Xcode 26+ (Swift 6.3) — required by MLX / MLX-Swift. | |
| runs-on: macos-15 | |
| timeout-minutes: 90 | |
| env: | |
| DEVELOPER_ID: ${{ secrets.DEVELOPER_ID_APPLICATION }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Resolve version | |
| id: version | |
| run: | | |
| REF="${{ github.event.inputs.tag || github.ref_name }}" | |
| echo "tag=$REF" >> "$GITHUB_OUTPUT" | |
| echo "version=${REF#v}" >> "$GITHUB_OUTPUT" | |
| echo "Building $REF (version ${REF#v})" | |
| # Guardrail: the in-repo version must match the tag, so a release can never | |
| # ship out of sync. Bump with scripts/release.sh, which keeps them aligned. | |
| # Skipped for workflow_dispatch dry runs (the checked-out tree may differ). | |
| - name: Verify in-repo version matches tag | |
| if: github.event_name == 'push' | |
| run: | | |
| want="${{ steps.version.outputs.version }}" | |
| py=$(sed -n -E 's/^version = "(.*)"/\1/p' backend/pyproject.toml | head -1) | |
| mv=$(sed -n -E 's/.*MARKETING_VERSION: "(.*)"/\1/p' App/project.yml | head -1) | |
| iv=$(sed -n -E 's/^__version__ = "(.*)"/\1/p' backend/tinyforge/__init__.py | head -1) | |
| echo "tag=$want pyproject=$py project.yml=$mv __init__=$iv" | |
| rc=0 | |
| [ "$py" = "$want" ] || { echo "::error::backend/pyproject.toml version '$py' != tag '$want'"; rc=1; } | |
| [ "$mv" = "$want" ] || { echo "::error::App/project.yml MARKETING_VERSION '$mv' != tag '$want'"; rc=1; } | |
| [ "$iv" = "$want" ] || { echo "::error::backend/tinyforge/__init__.py __version__ '$iv' != tag '$want'"; rc=1; } | |
| if [ "$rc" -ne 0 ]; then | |
| echo "Use scripts/release.sh $want to bump + tag in sync, then retry." >&2 | |
| exit 1 | |
| fi | |
| - name: Select Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: latest-stable | |
| - name: Install build tools (xcodegen, uv) | |
| run: | | |
| brew install xcodegen | |
| curl -LsSf https://astral.sh/uv/install.sh | sh | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| - name: Download Metal Toolchain (MLX-Swift shaders) | |
| run: xcodebuild -downloadComponent MetalToolchain | |
| - name: Cache bundled Python runtime | |
| uses: actions/cache@v4 | |
| with: | |
| path: build/python-runtime | |
| key: pyruntime-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('backend/uv.lock', 'backend/pyproject.toml', 'scripts/bundle_python.sh') }} | |
| - name: Configure signing | |
| id: signing | |
| run: | | |
| if [ -n "$DEVELOPER_ID" ]; then | |
| echo "Developer ID present — will sign + notarize." | |
| echo "identity=$DEVELOPER_ID" >> "$GITHUB_OUTPUT" | |
| echo "signed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "No Developer ID secret — building an ad-hoc (unsigned) DMG." | |
| echo "identity=-" >> "$GITHUB_OUTPUT" | |
| echo "signed=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Import Developer ID certificate | |
| if: steps.signing.outputs.signed == 'true' | |
| env: | |
| CERT_P12: ${{ secrets.DEVELOPER_ID_CERT_P12 }} | |
| CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }} | |
| KEYCHAIN_PASSWORD: tinyforge-ci-temp | |
| run: | | |
| CERT_PATH="$RUNNER_TEMP/cert.p12" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db" | |
| echo -n "$CERT_P12" | 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 "$CERT_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" | |
| rm -f "$CERT_PATH" | |
| - name: Build DMG | |
| env: | |
| MARKETING_VERSION: ${{ steps.version.outputs.version }} | |
| SIGN_IDENTITY: ${{ steps.signing.outputs.identity }} | |
| run: | | |
| scripts/build_release.sh | |
| DMG="build/TinyForge-${{ steps.version.outputs.tag }}.dmg" | |
| mv build/TinyForge.dmg "$DMG" | |
| echo "DMG=$DMG" >> "$GITHUB_ENV" | |
| - name: Notarize + staple | |
| if: steps.signing.outputs.signed == 'true' | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| xcrun notarytool submit "$DMG" \ | |
| --apple-id "$APPLE_ID" --password "$APPLE_APP_PASSWORD" --team-id "$APPLE_TEAM_ID" \ | |
| --wait | |
| xcrun stapler staple "$DMG" | |
| xcrun stapler validate "$DMG" | |
| - name: Upload workflow artifact | |
| if: ${{ !cancelled() && env.DMG != '' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: TinyForge-${{ steps.version.outputs.tag }}-dmg | |
| path: ${{ env.DMG }} | |
| if-no-files-found: error | |
| - name: Publish to GitHub Release | |
| if: github.event_name == 'push' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.version.outputs.tag }} | |
| name: TinyForge ${{ steps.version.outputs.tag }} | |
| files: ${{ env.DMG }} | |
| generate_release_notes: true | |
| append_body: true # keep hand-written release notes; append the DMG note + changelog | |
| fail_on_unmatched_files: true | |
| body: | | |
| macOS (Apple Silicon) build of TinyForge ${{ steps.version.outputs.tag }}. | |
| ${{ steps.signing.outputs.signed == 'true' && '✅ Signed with Developer ID and notarized.' || '⚠️ Unsigned (ad-hoc) build — right-click → Open, or sign locally. Configure signing secrets to ship a notarized DMG.' }} |