Merge pull request #1145 from shorepine/set_render_load_thresh #42
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: Tulip firmware release | |
| # Continuous Tulip release on push to main: build TULIP4_R11 firmware (and Tulip | |
| # Desktop for macOS) and publish to the rolling 'tulip' GitHub release. | |
| # | |
| # The rolling 'tulip' release is marked LATEST: it is what the releases page | |
| # badges and what the GitHub API serves at releases/latest. That matters because | |
| # firmware shipped in the monthly releases (<= v-jun-2026, before continuous | |
| # release) OTAs from releases/latest — those Tulips find tulip-firmware-*/ | |
| # tulip-sys.bin here, and legacy AMYboards find the amyboard-* bins that | |
| # amyboard-release.yml mirrors onto this release. Current firmware reads | |
| # releases/tags/tulip directly (see get_latest_release() in | |
| # tulip/shared/py/tulip.py). If a monthly release.sh release is ever cut again, | |
| # create it with --latest=false so it doesn't steal releases/latest back. | |
| # | |
| # Only TULIP4_R11 is shipped; TDECK/N16R8/N32R8 are developer-only (build them by | |
| # hand with `idf.py -DMICROPY_BOARD=<board> build`). | |
| # | |
| # Paths are firmware/desktop-only (NOT tulip/web/**) so a web-only change doesn't | |
| # bump the release date; the web app is deployed separately by tulip-web-release.yml. | |
| # | |
| # Publishing is gated on main so a workflow_dispatch from a branch is a safe | |
| # dry-run: everything builds, nothing is uploaded to the release. | |
| on: | |
| push: | |
| branches: [ "main" ] | |
| paths: | |
| - 'amy' | |
| - 'tulip/esp32s3/**' | |
| - 'tulip/shared/**' | |
| - 'tulip/fs/tulip/**' | |
| - 'tulip/macos/**' | |
| - '.github/workflows/tulip-release.yml' | |
| workflow_dispatch: | |
| concurrency: | |
| group: tulip-firmware-release # serialize releases; never cancel a half-done one | |
| cancel-in-progress: false | |
| jobs: | |
| firmware: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # create + clobber the 'tulip' release | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # amy + micropython (and nested submodules) are needed to build. | |
| submodules: recursive | |
| - name: Build TULIP4_R11 firmware + assemble images | |
| uses: espressif/esp-idf-ci-action@v1 | |
| with: | |
| esp_idf_version: v5.4.1 | |
| target: esp32s3 | |
| path: tulip/esp32s3 | |
| command: >- | |
| python -m pip install littlefs-python && | |
| idf.py -DMICROPY_BOARD=TULIP4_R11 build && | |
| cd .. && | |
| python fs_create.py tulip | |
| - name: Upload firmware artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: tulip-firmware | |
| if-no-files-found: error | |
| path: | | |
| tulip/esp32s3/dist/tulip-firmware-TULIP4_R11.bin | |
| tulip/esp32s3/dist/tulip-full-TULIP4_R11.bin | |
| tulip/esp32s3/dist/tulip-sys.bin | |
| - name: Publish to the rolling 'tulip' release | |
| if: github.ref == 'refs/heads/main' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # Create the rolling release once. It is kept marked LATEST (see the | |
| # header comment: legacy pre-jun-2026 devices OTA from releases/latest). | |
| if ! gh release view tulip >/dev/null 2>&1; then | |
| gh release create tulip \ | |
| --target "$GITHUB_SHA" \ | |
| --title "Tulip (rolling release)" \ | |
| --notes "Rolling Tulip release built from main (TULIP4_R11). OTA'd by tulip.upgrade(). Updated on every push to main; not a tagged version." | |
| fi | |
| # Stable asset names — consumed by tulip.upgrade(), the docs' direct | |
| # download links, and legacy releases/latest OTA. Do not rename. | |
| gh release upload --clobber tulip \ | |
| tulip/esp32s3/dist/tulip-firmware-TULIP4_R11.bin \ | |
| tulip/esp32s3/dist/tulip-full-TULIP4_R11.bin \ | |
| tulip/esp32s3/dist/tulip-sys.bin | |
| # Date-coded copy of the full image for manufacturing ("which build is | |
| # this file?"). One dated copy lives on the release at a time; older | |
| # date codes are pruned. Same-day rebuilds clobber the same name. | |
| # Copy into RUNNER_TEMP: dist/ is root-owned (created inside the | |
| # esp-idf-ci-action container), so this step can't create files there. | |
| DATE_CODE="$(date -u +%Y%m%d)" | |
| cp tulip/esp32s3/dist/tulip-full-TULIP4_R11.bin \ | |
| "${RUNNER_TEMP}/tulip-full-TULIP4_R11-${DATE_CODE}.bin" | |
| gh api "repos/${GITHUB_REPOSITORY}/releases/tags/tulip" --jq '.assets[].name' \ | |
| | { grep -E '^tulip-full-TULIP4_R11-[0-9]{8}\.bin$' || true; } \ | |
| | { grep -v "${DATE_CODE}" || true; } \ | |
| | while read -r stale; do gh release delete-asset tulip "$stale" --yes; done | |
| gh release upload --clobber tulip \ | |
| "${RUNNER_TEMP}/tulip-full-TULIP4_R11-${DATE_CODE}.bin" | |
| # Refresh the body with the exact released commit for provenance, and | |
| # (re)assert latest so releases/latest keeps serving legacy OTA. | |
| gh release edit tulip \ | |
| --latest \ | |
| --notes "Rolling Tulip release (TULIP4_R11), built from main @ ${GITHUB_SHA}. OTA'd by tulip.upgrade(). Updated on every push to main; not a tagged version." | |
| # Tulip Desktop for macOS (universal arm64+x86_64), mirroring tulip/macos/ | |
| # package.sh. Signing + notarization run only when the Apple credentials are | |
| # present as repo secrets; without them the app is still built (and uploaded | |
| # as a workflow artifact) but NOT published to the release, since an unsigned | |
| # zip would be blocked by Gatekeeper on download. | |
| # | |
| # Required secrets for publishing: | |
| # MACOS_SIGNING_CERT_P12 — base64 of the "Developer ID Application" | |
| # certificate + private key (.p12) | |
| # MACOS_SIGNING_CERT_PASSWORD — password for the .p12 | |
| # MACOS_NOTARY_API_KEY — App Store Connect API key file contents (.p8) | |
| # MACOS_NOTARY_API_KEY_ID — key ID for that key | |
| # MACOS_NOTARY_API_ISSUER_ID — issuer ID for that key | |
| desktop: | |
| runs-on: macos-14 | |
| permissions: | |
| contents: write # upload Tulip_Desktop.zip to the 'tulip' release | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Build Tulip Desktop (universal binary) | |
| run: | | |
| set -euo pipefail | |
| cd tulip/macos | |
| # Mirrors package.sh: build each arch, lipo, assemble the app bundle. | |
| make WHICH_ARCH=arm64 | |
| cp build-standard/tulip/obj/tulip.arm64 . | |
| rm -rf build-standard | |
| make WHICH_ARCH=x86_64 | |
| cp build-standard/tulip/obj/tulip.x86_64 . | |
| rm -rf build-standard | |
| lipo -create -output tulip tulip.x86_64 tulip.arm64 | |
| rm -rf dist | |
| mkdir -p "dist/Tulip Desktop.app/Contents/MacOS" \ | |
| "dist/Tulip Desktop.app/Contents/Resources" \ | |
| "dist/Tulip Desktop.app/Contents/Frameworks" \ | |
| "dist/Tulip Desktop.app/Contents/libs" | |
| cp tulip "dist/Tulip Desktop.app/Contents/MacOS/" | |
| cp Info.plist "dist/Tulip Desktop.app/Contents/" | |
| cp -rf ../fs "dist/Tulip Desktop.app/Contents/Resources/" | |
| cp -a SDL2.framework "dist/Tulip Desktop.app/Contents/Frameworks/" | |
| install_name_tool -add_rpath @executable_path/../Frameworks \ | |
| "dist/Tulip Desktop.app/Contents/MacOS/tulip" | |
| cp tulip.icns "dist/Tulip Desktop.app/Contents/Resources/" | |
| - name: Check for signing secrets | |
| id: signing | |
| env: | |
| CERT_P12: ${{ secrets.MACOS_SIGNING_CERT_P12 }} | |
| NOTARY_KEY: ${{ secrets.MACOS_NOTARY_API_KEY }} | |
| run: | | |
| if [ -n "$CERT_P12" ] && [ -n "$NOTARY_KEY" ]; then | |
| echo "enabled=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning::macOS signing/notarization secrets not set — building unsigned; Tulip_Desktop.zip will NOT be published to the release." | |
| fi | |
| - name: Sign, notarize, and staple | |
| if: steps.signing.outputs.enabled == 'true' | |
| env: | |
| CERT_P12: ${{ secrets.MACOS_SIGNING_CERT_P12 }} | |
| CERT_PASSWORD: ${{ secrets.MACOS_SIGNING_CERT_PASSWORD }} | |
| NOTARY_KEY: ${{ secrets.MACOS_NOTARY_API_KEY }} | |
| NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }} | |
| NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }} | |
| run: | | |
| set -euo pipefail | |
| cd tulip/macos | |
| # Import the Developer ID cert into a throwaway keychain. | |
| KEYCHAIN="$RUNNER_TEMP/tulip-signing.keychain-db" | |
| KEYCHAIN_PASSWORD="$(uuidgen)" | |
| echo "$CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" | |
| security import "$RUNNER_TEMP/cert.p12" -P "$CERT_PASSWORD" \ | |
| -A -t cert -f pkcs12 -k "$KEYCHAIN" | |
| security set-key-partition-list -S apple-tool:,apple: \ | |
| -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null | |
| security list-keychains -d user -s "$KEYCHAIN" login.keychain | |
| DEV_ID="$(security find-identity -v -p codesigning "$KEYCHAIN" \ | |
| | awk -F'"' '/Developer ID Application/ {print $2; exit}')" | |
| if [ -z "$DEV_ID" ]; then | |
| echo "::error::No 'Developer ID Application' identity found in the imported .p12"; exit 1 | |
| fi | |
| # Same signing order as package.sh: framework, binary, then the bundle | |
| # with hardened runtime (required for notarization). | |
| codesign -s "$DEV_ID" -f \ | |
| "dist/Tulip Desktop.app/Contents/Frameworks/SDL2.framework/Versions/A/SDL2" | |
| codesign -s "$DEV_ID" -f "dist/Tulip Desktop.app/Contents/MacOS/tulip" | |
| codesign --force --options runtime --timestamp --sign "$DEV_ID" \ | |
| "dist/Tulip Desktop.app" | |
| # Notarize with an App Store Connect API key, then staple. | |
| echo "$NOTARY_KEY" > "$RUNNER_TEMP/notary.p8" | |
| cd dist | |
| /usr/bin/ditto -c -k --sequesterRsrc --keepParent "Tulip Desktop.app" Tulip_Desktop.zip | |
| xcrun notarytool submit Tulip_Desktop.zip \ | |
| --key "$RUNNER_TEMP/notary.p8" \ | |
| --key-id "$NOTARY_KEY_ID" \ | |
| --issuer "$NOTARY_ISSUER_ID" \ | |
| --wait | |
| rm -rf "Tulip Desktop.app" | |
| unzip -q Tulip_Desktop.zip | |
| xcrun stapler staple "Tulip Desktop.app" | |
| /usr/bin/ditto -c -k --sequesterRsrc --keepParent "Tulip Desktop.app" Tulip_Desktop.zip | |
| - name: Zip unsigned app (no signing secrets) | |
| if: steps.signing.outputs.enabled != 'true' | |
| run: | | |
| set -euo pipefail | |
| cd tulip/macos/dist | |
| /usr/bin/ditto -c -k --sequesterRsrc --keepParent "Tulip Desktop.app" Tulip_Desktop.zip | |
| - name: Upload Tulip Desktop artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: tulip-desktop-macos | |
| if-no-files-found: error | |
| path: tulip/macos/dist/Tulip_Desktop.zip | |
| - name: Publish Tulip_Desktop.zip to the rolling 'tulip' release | |
| if: github.ref == 'refs/heads/main' && steps.signing.outputs.enabled == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if ! gh release view tulip >/dev/null 2>&1; then | |
| echo "::error::rolling 'tulip' release does not exist yet"; exit 1 | |
| fi | |
| gh release upload --clobber tulip tulip/macos/dist/Tulip_Desktop.zip |