Skip to content

fix(bridge): validate/preflight carry the render's target scope - v1.… #51

fix(bridge): validate/preflight carry the render's target scope - v1.…

fix(bridge): validate/preflight carry the render's target scope - v1.… #51

Workflow file for this run

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') }}