Skip to content

release v0.1.2

release v0.1.2 #6

Workflow file for this run

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)
echo "tag=$want pyproject=$py project.yml=$mv"
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; }
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
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.' }}