fix(bridge): validate/preflight carry the render's target scope - v1.… #51
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: addon | |
| # Builds the per-platform Blender addon packages (self-contained bridge bundled), signs the macOS | |
| # bridge binary, and attaches everything to the GitHub Release. Triggered by an `addon-v*` tag; can | |
| # also be run manually. | |
| # | |
| # All three RIDs are cross-published from a single Linux runner. This is deliberate: the bridge weaves | |
| # aspects with AspectInjector at build time, and its osx-arm64 native tool gets SIGKILL'd by Gatekeeper | |
| # on the macOS runner. On Linux the (managed) weaving runs with the Linux tool and `dotnet publish -r | |
| # <rid>` still produces a working self-contained build for every target. Ships the SELF-CONTAINED | |
| # variant (no .NET prerequisite for artists). | |
| # | |
| # SIGNING (mirrors the WitCloud client-release pipeline; same secret names): | |
| # - macOS: a dedicated macos-latest job codesigns the bundled bridge Mach-O (Developer ID | |
| # Application, hardened runtime + .NET entitlements) and notarizes the addon zip with notarytool. | |
| # A bare executable cannot be STAPLED (only .app/.dmg/.pkg can) — Gatekeeper validates the | |
| # notarization ticket online, which is the standard path for CLI binaries shipped in zips. | |
| # Required repo secrets: APPLE_CERT_P12_BASE64, APPLE_CERT_PASSWORD, APPLE_ID, | |
| # APPLE_APP_PASSWORD, APPLE_TEAM_ID. The job degrades to a no-op (with a warning) until they | |
| # are configured, so unsigned debug releases keep working. | |
| # - Windows: a dedicated sign-windows job Authenticode-signs the bundled bridge .exe via SSL.com | |
| # eSigner (cloud CodeSignTool) — the same mechanism + secrets as the WitCloud client release. | |
| # Required repo secrets: ESIGNER_USERNAME, ESIGNER_PASSWORD, ESIGNER_CREDENTIAL_ID, | |
| # ESIGNER_TOTP_SECRET. The eSigner tier is quota-limited (~240/yr), so the job signs public tags | |
| # (-beta / stable / -rc) but NOT internal iteration tags (-dev/-test/-internal); it degrades to a | |
| # no-op (ships UNSIGNED, with a warning) when the secrets are absent. | |
| # - Linux: no per-binary signing (per the signing strategy); the release ships SHA256SUMS and a | |
| # detached GPG signature when GPG_PRIVATE_KEY/GPG_PASSPHRASE are configured. | |
| # | |
| # Not yet automated (see @Archive/docs/addon-packaging-and-distribution.md §4): regenerating + | |
| # publishing the extension repository index.json for in-Blender auto-update — that lands with the | |
| # portal (the index/zips will be hosted there). | |
| on: | |
| push: | |
| tags: | |
| - 'addon-v*' | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| rid: [win-x64, linux-x64, osx-arm64] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Build addon package (${{ matrix.rid }}, self-contained) | |
| shell: pwsh | |
| run: ./OutWit.Render.BlenderAddon/Build-BlenderAddonPackage.ps1 -RuntimeIdentifier ${{ matrix.rid }} -DeploymentMode SelfContained | |
| # Guards two packaging invariants: blender_manifest.toml must sit at the zip ROOT (Blender's | |
| # extension installer requirement), and unix bridges must carry the executable bit (native | |
| # zip path in the build script; Compress-Archive would silently drop it). | |
| - name: Verify package layout | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ZIP=$(ls OutWit.Render.BlenderAddon/dist/*.zip | head -1) | |
| unzip -l "$ZIP" | sed -n '1,12p' | |
| unzip -l "$ZIP" | awk '{print $4}' | grep -qx 'blender_manifest.toml' \ | |
| || { echo "::error::blender_manifest.toml is not at the zip root"; exit 1; } | |
| if [ "${{ matrix.rid }}" != "win-x64" ]; then | |
| INFO=$(zipinfo "$ZIP" | grep 'OutWit.Render.BlenderBridge$' || true) | |
| echo "$INFO" | |
| echo "$INFO" | grep -q '^-rwx' \ | |
| || { echo "::error::bridge binary is not executable inside the package"; exit 1; } | |
| fi | |
| - name: Upload build artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: addon-${{ matrix.rid }} | |
| path: OutWit.Render.BlenderAddon/dist/*.zip | |
| if-no-files-found: error | |
| sign-macos: | |
| name: sign + notarize macOS bridge | |
| needs: build | |
| runs-on: macos-latest | |
| steps: | |
| - name: Download macOS package | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: addon-osx-arm64 | |
| path: stage | |
| # Secrets are not available in job-level `if`, so gate inside the job: without the Apple | |
| # credentials this job is a no-op and the unsigned zip ships as-is (debug-friendly). | |
| - name: Check signing credentials | |
| id: gate | |
| env: | |
| APPLE_CERT_P12_BASE64: ${{ secrets.APPLE_CERT_P12_BASE64 }} | |
| run: | | |
| if [ -z "${APPLE_CERT_P12_BASE64:-}" ]; then | |
| echo "::warning::APPLE_CERT_P12_BASE64 not configured — shipping the macOS package UNSIGNED. Add APPLE_CERT_P12_BASE64/APPLE_CERT_PASSWORD/APPLE_ID/APPLE_APP_PASSWORD/APPLE_TEAM_ID to sign+notarize." | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "enabled=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Sign the bridge binary | |
| if: steps.gate.outputs.enabled == 'true' | |
| env: | |
| APPLE_CERT_P12_BASE64: ${{ secrets.APPLE_CERT_P12_BASE64 }} | |
| APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| ZIP=$(ls stage/*.zip | head -1) | |
| echo "Package: $ZIP" | |
| WORK="$RUNNER_TEMP/addon"; rm -rf "$WORK"; mkdir -p "$WORK" | |
| unzip -q "$ZIP" -d "$WORK" | |
| BRIDGE=$(find "$WORK" -type f -name 'OutWit.Render.BlenderBridge' -path '*osx-arm64*' | head -1) | |
| if [ -z "$BRIDGE" ]; then | |
| echo "::error::bridge binary not found inside the package"; exit 1 | |
| fi | |
| echo "Bridge binary: $BRIDGE" | |
| # Belt-and-braces: the build job stores the exec bit via native zip, but make sure the | |
| # re-packed archive always ships a runnable binary. | |
| chmod +x "$BRIDGE" | |
| # Temp keychain with the Developer ID Application cert (client-release pattern). | |
| echo "$APPLE_CERT_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/apple_cert.p12" | |
| KEYCHAIN="$RUNNER_TEMP/sign.keychain-db" | |
| security create-keychain -p ci "$KEYCHAIN" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN" | |
| security unlock-keychain -p ci "$KEYCHAIN" | |
| security import "$RUNNER_TEMP/apple_cert.p12" -k "$KEYCHAIN" -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple: -s -k ci "$KEYCHAIN" >/dev/null | |
| security list-keychains -d user -s "$KEYCHAIN" login.keychain-db | |
| DEVID=$(security find-identity -v -p codesigning "$KEYCHAIN" | awk '/Developer ID Application/{print $2; exit}') | |
| echo "Signing identity: $DEVID" | |
| # .NET hardened-runtime entitlements (the bridge JITs, talks to the cloud over HTTPS and | |
| # binds the loopback REST server). | |
| ENT="$RUNNER_TEMP/entitlements.plist" | |
| cat > "$ENT" <<'PLIST' | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"><dict> | |
| <key>com.apple.security.cs.allow-jit</key><true/> | |
| <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/> | |
| <key>com.apple.security.cs.disable-library-validation</key><true/> | |
| <key>com.apple.security.network.client</key><true/> | |
| <key>com.apple.security.network.server</key><true/> | |
| </dict></plist> | |
| PLIST | |
| codesign --force --timestamp --options runtime --entitlements "$ENT" --sign "$DEVID" "$BRIDGE" | |
| codesign --verify --strict --verbose=2 "$BRIDGE" | |
| # Re-zip with the original name and the original internal layout. | |
| NAME=$(basename "$ZIP") | |
| (cd "$WORK" && zip -qry "$RUNNER_TEMP/$NAME" .) | |
| mv "$RUNNER_TEMP/$NAME" "$ZIP" | |
| echo "ZIP_PATH=$ZIP" >> "$GITHUB_ENV" | |
| - name: Notarize the package | |
| if: steps.gate.outputs.enabled == 'true' | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| echo "Submitting $ZIP_PATH to the Apple notary service (waits for the result)..." | |
| SUBMIT_OUT=$(xcrun notarytool submit "$ZIP_PATH" \ | |
| --apple-id "$APPLE_ID" --password "$APPLE_APP_PASSWORD" --team-id "$APPLE_TEAM_ID" \ | |
| --wait 2>&1) || true | |
| echo "$SUBMIT_OUT" | |
| SUB_ID=$(echo "$SUBMIT_OUT" | awk '/id:/{print $2; exit}') | |
| if ! echo "$SUBMIT_OUT" | grep -q "status: Accepted"; then | |
| echo "::error::Notarization not accepted (id=$SUB_ID). Fetching the detailed log:" | |
| xcrun notarytool log "$SUB_ID" \ | |
| --apple-id "$APPLE_ID" --password "$APPLE_APP_PASSWORD" --team-id "$APPLE_TEAM_ID" || true | |
| exit 1 | |
| fi | |
| # A bare binary in a zip cannot be stapled — Gatekeeper validates the ticket online. | |
| - name: Upload signed package | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: addon-osx-arm64 | |
| path: stage/*.zip | |
| overwrite: true | |
| if-no-files-found: error | |
| sign-windows: | |
| name: sign Windows bridge (SSL.com eSigner) | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download Windows package | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: addon-win-x64 | |
| path: stage | |
| # Authenticode-sign the bundled bridge .exe via SSL.com eSigner (cloud) — the same mechanism the | |
| # WitCloud client release uses (ESIGNER_* secrets). The eSigner tier is quota-limited (~240/yr), so | |
| # sign every tag EXCEPT internal iteration tags (-dev/-test/-internal); the public `-beta`/stable | |
| # releases ARE signed. No-op (ships UNSIGNED) when the ESIGNER_* secrets are absent, so the release | |
| # still ships. macOS notarization + Linux GPG/SHA256SUMS are unaffected. | |
| - name: Decide Windows signing | |
| id: gate | |
| env: | |
| ESIGNER_USERNAME: ${{ secrets.ESIGNER_USERNAME }} | |
| run: | | |
| set -euo pipefail | |
| should=false | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then should=true; fi | |
| if [ "${{ github.ref_type }}" = "tag" ]; then | |
| case "${{ github.ref_name }}" in | |
| *-dev*|*-test*|*-internal*) : ;; # internal iteration tag → don't sign (save quota) | |
| *) should=true ;; # public release (-beta / stable / -rc) → sign | |
| esac | |
| fi | |
| if [ -z "${ESIGNER_USERNAME:-}" ]; then | |
| [ "$should" = "true" ] && echo "::warning::Windows signing requested but ESIGNER_* secrets are not set — shipping the Windows package UNSIGNED." | |
| should=false | |
| fi | |
| echo "enabled=$should" >> "$GITHUB_OUTPUT" | |
| echo "Windows signing: enabled=$should" | |
| - name: Unpack the addon package | |
| if: steps.gate.outputs.enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| ZIP=$(ls stage/*.zip | head -1) | |
| echo "ZIP_NAME=$(basename "$ZIP")" >> "$GITHUB_ENV" | |
| WORK="$RUNNER_TEMP/addon"; rm -rf "$WORK"; mkdir -p "$WORK" | |
| unzip -q "$ZIP" -d "$WORK" | |
| BRIDGE=$(find "$WORK" -type f -name 'OutWit.Render.BlenderBridge.exe' | head -1) | |
| if [ -z "$BRIDGE" ]; then | |
| echo "::error::bridge .exe not found inside the Windows package"; exit 1 | |
| fi | |
| echo "Bridge binary: $BRIDGE" | |
| echo "BRIDGE_PATH=$BRIDGE" >> "$GITHUB_ENV" | |
| echo "WORK_DIR=$WORK" >> "$GITHUB_ENV" | |
| # CodeSignTool copies the signed file into output_path (errors if it equals the input dir), so sign | |
| # into a separate dir and move it back. We sign only the bridge .exe — the native binary Windows / | |
| # SmartScreen evaluate — not the inner DLLs. | |
| - name: Sign the bridge (SSL.com eSigner) | |
| if: steps.gate.outputs.enabled == 'true' | |
| uses: sslcom/esigner-codesign@develop | |
| with: | |
| command: sign | |
| username: ${{ secrets.ESIGNER_USERNAME }} | |
| password: ${{ secrets.ESIGNER_PASSWORD }} | |
| credential_id: ${{ secrets.ESIGNER_CREDENTIAL_ID }} | |
| totp_secret: ${{ secrets.ESIGNER_TOTP_SECRET }} | |
| file_path: ${{ env.BRIDGE_PATH }} | |
| output_path: ${{ runner.temp }}/signed | |
| malware_block: 'false' | |
| environment_name: PROD | |
| - name: Repack with the signed bridge | |
| if: steps.gate.outputs.enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| mv -f "$RUNNER_TEMP/signed/OutWit.Render.BlenderBridge.exe" "$BRIDGE_PATH" | |
| (cd "$WORK_DIR" && zip -qry "$RUNNER_TEMP/$ZIP_NAME" .) | |
| rm -rf stage-signed; mkdir -p stage-signed | |
| mv -f "$RUNNER_TEMP/$ZIP_NAME" "stage-signed/$ZIP_NAME" | |
| echo "Repacked the Windows package with the signed bridge." | |
| - name: Upload signed package | |
| if: steps.gate.outputs.enabled == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: addon-win-x64 | |
| path: stage-signed/*.zip | |
| overwrite: true | |
| if-no-files-found: error | |
| release: | |
| needs: [build, sign-macos, sign-windows] | |
| if: startsWith(github.ref, 'refs/tags/addon-v') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| # A single SHA256SUMS over every package + a detached GPG signature when the key is | |
| # configured (same trust mechanism as the client release; verify with | |
| # `gpg --verify SHA256SUMS.asc SHA256SUMS && sha256sum -c SHA256SUMS`). | |
| - name: SHA256SUMS + GPG signature | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p release | |
| find artifacts -type f -name '*.zip' -exec cp {} release/ \; | |
| cd release | |
| sha256sum *.zip > SHA256SUMS | |
| echo "=== SHA256SUMS ==="; cat SHA256SUMS | |
| if [ -z "${GPG_PRIVATE_KEY:-}" ]; then | |
| echo "::warning::GPG_PRIVATE_KEY secret not set — shipping SHA256SUMS WITHOUT a signature." | |
| exit 0 | |
| fi | |
| echo "$GPG_PRIVATE_KEY" | gpg --batch --import | |
| gpg --batch --pinentry-mode loopback --passphrase "${GPG_PASSPHRASE:-}" \ | |
| --detach-sign --armor -o SHA256SUMS.asc SHA256SUMS | |
| echo "=== signed ==="; ls -la SHA256SUMS* | |
| - name: Attach addon packages to the release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: release/* | |
| prerelease: ${{ contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') || contains(github.ref_name, '-rc') }} |