Desktop Release (macOS) #8
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: Desktop Release (macOS) | |
| # Builds, signs, notarizes, and uploads the desktop app to an existing GitHub | |
| # release. Stable releases live in this source repository; dev and staging | |
| # releases live in simstudioai/sim-desktop-releases. Ordering is load-bearing: | |
| # scripts/create-single-release.ts skips creation when the stable tag already | |
| # exists, so this workflow must never create a release itself. | |
| on: | |
| workflow_call: | |
| inputs: | |
| version: | |
| description: Release tag (vX.Y.Z) to attach desktop artifacts to | |
| required: true | |
| type: string | |
| publish: | |
| description: Upload artifacts to the GitHub release | |
| required: false | |
| type: boolean | |
| default: true | |
| sign: | |
| description: Sign and notarize with the Apple Developer identity. Unsigned | |
| builds are workflow artifacts only and cannot be published. | |
| required: false | |
| type: boolean | |
| default: true | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: Release tag (vX.Y.Z) to attach desktop artifacts to | |
| required: true | |
| type: string | |
| publish: | |
| description: Upload artifacts to the GitHub release | |
| required: false | |
| type: boolean | |
| default: false | |
| sign: | |
| description: Sign and notarize with the Apple Developer identity | |
| required: false | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: write | |
| jobs: | |
| build-sign-notarize: | |
| name: Build, Sign, Notarize | |
| runs-on: macos-26 | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| ref: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.sha }} | |
| # Prerelease versions carry their environment in the tag: -dev.N is a | |
| # dev build, -staging.N a staging build. Legacy -alpha/-beta tags remain | |
| # accepted while already-published builds age out. The channel decides the app's | |
| # identity (name/bundle id — a separate app per environment, installable | |
| # side by side) and the default origin baked into the bundle, which in | |
| # turn selects the update feed the installed app polls. | |
| - name: Resolve channel identity | |
| id: channel | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| case "$VERSION" in | |
| *-dev.*|*-alpha.*) | |
| NAME='Sim Dev'; APP_ID=ai.sim.desktop.dev; ORIGIN=https://www.dev.sim.ai; RELEASE_REPOSITORY=simstudioai/sim-desktop-releases; TOKEN_KIND=prerelease ;; | |
| *-staging.*|*-beta.*) | |
| NAME='Sim Staging'; APP_ID=ai.sim.desktop.staging; ORIGIN=https://www.staging.sim.ai; RELEASE_REPOSITORY=simstudioai/sim-desktop-releases; TOKEN_KIND=prerelease ;; | |
| *) | |
| NAME='Sim'; APP_ID=ai.sim.desktop; ORIGIN=''; RELEASE_REPOSITORY="$GITHUB_REPOSITORY"; TOKEN_KIND=stable ;; | |
| esac | |
| { | |
| echo "name=$NAME" | |
| echo "app_id=$APP_ID" | |
| echo "origin=$ORIGIN" | |
| echo "release_repository=$RELEASE_REPOSITORY" | |
| echo "token_kind=$TOKEN_KIND" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Building $NAME ($APP_ID) for $RELEASE_REPOSITORY; default origin: ${ORIGIN:-production}" | |
| - name: Validate release source | |
| env: | |
| PUBLISH: ${{ inputs.publish }} | |
| SIGN: ${{ inputs.sign }} | |
| TOKEN_KIND: ${{ steps.channel.outputs.token_kind }} | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then | |
| echo "::error::Refusing to build: '$VERSION' is not a vX.Y.Z release tag." | |
| exit 1 | |
| fi | |
| if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ] && [ "$TOKEN_KIND" != stable ]; then | |
| echo "::error::Manual desktop releases must use a stable source-repository tag." | |
| exit 1 | |
| fi | |
| if [ "$PUBLISH" = true ] && [ "$SIGN" != true ]; then | |
| echo "::error::Desktop releases must be signed before publication." | |
| exit 1 | |
| fi | |
| if [ "$TOKEN_KIND" = stable ]; then | |
| TAG_COMMIT="$(git rev-parse "refs/tags/${VERSION}^{commit}")" | |
| HEAD_COMMIT="$(git rev-parse HEAD)" | |
| if [ "$TAG_COMMIT" != "$HEAD_COMMIT" ]; then | |
| echo "::error::Requested tag $VERSION points to $TAG_COMMIT, but the checkout is $HEAD_COMMIT." | |
| exit 1 | |
| fi | |
| fi | |
| - name: Validate release authentication | |
| if: ${{ inputs.publish }} | |
| env: | |
| DESKTOP_RELEASE_TOKEN: ${{ secrets.DESKTOP_RELEASE_TOKEN }} | |
| RELEASE_REPOSITORY: ${{ steps.channel.outputs.release_repository }} | |
| TOKEN_KIND: ${{ steps.channel.outputs.token_kind }} | |
| run: | | |
| if [ "$TOKEN_KIND" = prerelease ] && [ -z "$DESKTOP_RELEASE_TOKEN" ]; then | |
| echo "::error::DESKTOP_RELEASE_TOKEN is required to publish prereleases to $RELEASE_REPOSITORY." | |
| exit 1 | |
| fi | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: 1.3.14 | |
| - name: Setup Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 22 | |
| - name: Cache Electron binaries | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: | | |
| ~/Library/Caches/electron | |
| ~/Library/Caches/electron-builder | |
| key: electron-cache-${{ runner.os }}-${{ hashFiles('apps/desktop/package.json') }} | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Inject release version | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| SEMVER="${VERSION#v}" | |
| cd apps/desktop | |
| bun pm pkg set version="$SEMVER" | |
| cd ../.. | |
| INJECTED="$(node -p "require('./apps/desktop/package.json').version")" | |
| if [ "$INJECTED" != "$SEMVER" ]; then | |
| echo "Version injection mismatch: wanted $SEMVER got $INJECTED" >&2 | |
| exit 1 | |
| fi | |
| - name: Verify desktop source | |
| run: | | |
| bun run --cwd apps/desktop lint:check | |
| bun run --cwd apps/desktop type-check | |
| bun run --cwd apps/desktop test | |
| - name: Bundle main and preload | |
| working-directory: apps/desktop | |
| env: | |
| SIM_DESKTOP_DEFAULT_ORIGIN: ${{ steps.channel.outputs.origin }} | |
| run: bun run build | |
| - name: Run Electron smoke tests | |
| working-directory: apps/desktop | |
| env: | |
| SIM_DESKTOP_DEFAULT_ORIGIN: ${{ steps.channel.outputs.origin }} | |
| run: bun run test:e2e | |
| - name: Write App Store Connect API key | |
| if: ${{ inputs.sign }} | |
| env: | |
| APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/appstoreconnect" | |
| printf '%s' "$APPLE_API_KEY_P8" > "$RUNNER_TEMP/appstoreconnect/AuthKey.p8" | |
| chmod 600 "$RUNNER_TEMP/appstoreconnect/AuthKey.p8" | |
| - name: Import Apple signing certificate | |
| if: ${{ inputs.sign }} | |
| env: | |
| CSC_LINK: ${{ secrets.CSC_LINK }} | |
| CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} | |
| run: | | |
| CERTIFICATE_PATH="$RUNNER_TEMP/desktop-signing.p12" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/desktop-signing.keychain-db" | |
| KEYCHAIN_PASSWORD="$(openssl rand -base64 32)" | |
| printf '%s' "$CSC_LINK" | base64 --decode > "$CERTIFICATE_PATH" | |
| chmod 600 "$CERTIFICATE_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 "$CERTIFICATE_PATH" -k "$KEYCHAIN_PATH" -P "$CSC_KEY_PASSWORD" \ | |
| -T /usr/bin/codesign -T /usr/bin/productbuild | |
| security set-key-partition-list -S apple-tool:,apple: -s \ | |
| -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" > /dev/null | |
| EXISTING_KEYCHAINS=("$KEYCHAIN_PATH") | |
| while IFS= read -r EXISTING_KEYCHAIN; do | |
| EXISTING_KEYCHAIN="${EXISTING_KEYCHAIN#*\"}" | |
| EXISTING_KEYCHAIN="${EXISTING_KEYCHAIN%\"}" | |
| if [ "$EXISTING_KEYCHAIN" != "$KEYCHAIN_PATH" ]; then | |
| EXISTING_KEYCHAINS+=("$EXISTING_KEYCHAIN") | |
| fi | |
| done < <(security list-keychains -d user) | |
| security list-keychains -d user -s "${EXISTING_KEYCHAINS[@]}" | |
| SIGNING_IDENTITIES="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH")" | |
| if ! grep -q 'Developer ID Application' <<< "$SIGNING_IDENTITIES"; then | |
| echo '::error::The signing certificate does not contain a valid Developer ID Application identity.' | |
| exit 1 | |
| fi | |
| - name: Package, sign, and notarize | |
| if: ${{ inputs.sign }} | |
| working-directory: apps/desktop | |
| env: | |
| CSC_KEYCHAIN: ${{ runner.temp }}/desktop-signing.keychain-db | |
| # Absolute path — @electron/notarize reads this via Node fs, which | |
| # does not expand a leading '~'. | |
| APPLE_API_KEY: ${{ runner.temp }}/appstoreconnect/AuthKey.p8 | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| PRODUCT_NAME: ${{ steps.channel.outputs.name }} | |
| APP_ID: ${{ steps.channel.outputs.app_id }} | |
| run: > | |
| bunx electron-builder --mac --publish never | |
| -c.productName="$PRODUCT_NAME" -c.appId="$APP_ID" | |
| - name: Remove Apple signing credentials | |
| if: ${{ always() && inputs.sign }} | |
| run: | | |
| security delete-keychain "$RUNNER_TEMP/desktop-signing.keychain-db" || true | |
| rm -f "$RUNNER_TEMP/desktop-signing.p12" | |
| rm -f "$RUNNER_TEMP/appstoreconnect/AuthKey.p8" | |
| # Unsigned artifact-only path: no Developer ID or notarization. The bundle | |
| # is ad-hoc signed with Hardened Runtime off for local workflow testing and | |
| # must never be published. | |
| - name: Package unsigned | |
| if: ${{ !inputs.sign }} | |
| working-directory: apps/desktop | |
| env: | |
| CSC_IDENTITY_AUTO_DISCOVERY: 'false' | |
| PRODUCT_NAME: ${{ steps.channel.outputs.name }} | |
| APP_ID: ${{ steps.channel.outputs.app_id }} | |
| run: > | |
| bunx electron-builder --mac --publish never -c.mac.notarize=false | |
| -c.mac.identity=- -c.mac.hardenedRuntime=false | |
| -c.productName="$PRODUCT_NAME" -c.appId="$APP_ID" | |
| - name: Validate packaged artifacts | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| SEMVER="${VERSION#v}" | |
| RELEASE_DIR=apps/desktop/release | |
| YML="$(find "$RELEASE_DIR" -maxdepth 1 -name '*-mac.yml' -print)" | |
| if [ "$(printf '%s\n' "$YML" | sed '/^$/d' | wc -l | tr -d ' ')" != 1 ]; then | |
| echo "::error::Expected exactly one updater manifest in $RELEASE_DIR." | |
| exit 1 | |
| fi | |
| if [ "$(basename "$YML")" != latest-mac.yml ]; then | |
| mv "$YML" "$RELEASE_DIR/latest-mac.yml" | |
| fi | |
| ARTIFACTS=( | |
| "$RELEASE_DIR/Sim-${SEMVER}-universal.dmg" | |
| "$RELEASE_DIR/Sim-${SEMVER}-universal.dmg.blockmap" | |
| "$RELEASE_DIR/Sim-${SEMVER}-universal.zip" | |
| "$RELEASE_DIR/Sim-${SEMVER}-universal.zip.blockmap" | |
| "$RELEASE_DIR/latest-mac.yml" | |
| ) | |
| for ARTIFACT in "${ARTIFACTS[@]}"; do | |
| if [ ! -f "$ARTIFACT" ]; then | |
| echo "::error::Expected desktop artifact is missing: $ARTIFACT" | |
| exit 1 | |
| fi | |
| done | |
| if [ "$(find "$RELEASE_DIR" -maxdepth 1 \( -name '*.dmg' -o -name '*.zip' -o -name '*.blockmap' \) | wc -l | tr -d ' ')" != 4 ]; then | |
| echo "::error::Unexpected package artifacts were produced; refusing a wildcard upload." | |
| find "$RELEASE_DIR" -maxdepth 1 -type f -print | |
| exit 1 | |
| fi | |
| if ! grep -Fxq "version: $SEMVER" "$RELEASE_DIR/latest-mac.yml"; then | |
| echo "::error::Updater manifest version does not match $VERSION." | |
| exit 1 | |
| fi | |
| URLS="$(sed -nE 's/^[[:space:]]*(-[[:space:]]*)?url:[[:space:]]*([^[:space:]]+)[[:space:]]*$/\2/p' "$RELEASE_DIR/latest-mac.yml" | sort)" | |
| EXPECTED_URLS="$(printf '%s\n' "Sim-${SEMVER}-universal.zip" "Sim-${SEMVER}-universal.dmg" | sort)" | |
| if [ "$URLS" != "$EXPECTED_URLS" ]; then | |
| echo "::error::Updater manifest contains unexpected artifact URLs." | |
| exit 1 | |
| fi | |
| if ! grep -Fxq "path: Sim-${SEMVER}-universal.zip" "$RELEASE_DIR/latest-mac.yml"; then | |
| echo "::error::Updater manifest path does not reference the verified zip artifact." | |
| exit 1 | |
| fi | |
| hdiutil verify "$RELEASE_DIR/Sim-${SEMVER}-universal.dmg" | |
| unzip -tq "$RELEASE_DIR/Sim-${SEMVER}-universal.zip" | |
| - name: Validate signature and notarization | |
| if: ${{ inputs.sign }} | |
| env: | |
| APP_ID: ${{ steps.channel.outputs.app_id }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| PRODUCT_NAME: ${{ steps.channel.outputs.name }} | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| SEMVER="${VERSION#v}" | |
| DMG="apps/desktop/release/Sim-${SEMVER}-universal.dmg" | |
| ZIP="apps/desktop/release/Sim-${SEMVER}-universal.zip" | |
| MOUNT_POINT="$RUNNER_TEMP/sim-dmg" | |
| ZIP_DIR="$(mktemp -d "$RUNNER_TEMP/sim-zip.XXXXXX")" | |
| validate_identity() { | |
| local APP_BUNDLE="$1" | |
| local ACTUAL_APP_ID ACTUAL_NAME SIGNATURE | |
| ACTUAL_APP_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_BUNDLE/Contents/Info.plist")" | |
| ACTUAL_NAME="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleName' "$APP_BUNDLE/Contents/Info.plist")" | |
| SIGNATURE="$(codesign -dv --verbose=4 "$APP_BUNDLE" 2>&1)" | |
| if [ "$ACTUAL_APP_ID" != "$APP_ID" ] || [ "$ACTUAL_NAME" != "$PRODUCT_NAME" ]; then | |
| echo "::error::Unexpected packaged identity: $ACTUAL_NAME ($ACTUAL_APP_ID)." | |
| exit 1 | |
| fi | |
| if ! grep -Fxq "TeamIdentifier=$APPLE_TEAM_ID" <<< "$SIGNATURE"; then | |
| echo "::error::The app was not signed by the expected Apple team." | |
| exit 1 | |
| fi | |
| if ! grep -Eq 'flags=.*runtime' <<< "$SIGNATURE"; then | |
| echo "::error::The app was not signed with Hardened Runtime." | |
| exit 1 | |
| fi | |
| } | |
| mkdir -p "$MOUNT_POINT" | |
| hdiutil attach "$DMG" -mountpoint "$MOUNT_POINT" -nobrowse -quiet | |
| trap 'hdiutil detach "$MOUNT_POINT" -quiet || true; rm -rf "$ZIP_DIR"' EXIT | |
| APP_BUNDLE="$(find "$MOUNT_POINT" -maxdepth 1 -name '*.app' -print -quit)" | |
| if [ -z "$APP_BUNDLE" ]; then | |
| echo "::error::The signed DMG does not contain an app bundle." | |
| exit 1 | |
| fi | |
| xcrun stapler validate "$APP_BUNDLE" | |
| spctl --assess --type execute --verbose "$APP_BUNDLE" | |
| codesign --verify --deep --strict "$APP_BUNDLE" | |
| validate_identity "$APP_BUNDLE" | |
| unzip -q "$ZIP" -d "$ZIP_DIR" | |
| ZIP_APP="$(find "$ZIP_DIR" -maxdepth 2 -name '*.app' -print -quit)" | |
| if [ -z "$ZIP_APP" ]; then | |
| echo "::error::The updater ZIP does not contain an app bundle." | |
| exit 1 | |
| fi | |
| xcrun stapler validate "$ZIP_APP" | |
| spctl --assess --type execute --verbose "$ZIP_APP" | |
| codesign --verify --deep --strict "$ZIP_APP" | |
| validate_identity "$ZIP_APP" | |
| hdiutil detach "$MOUNT_POINT" -quiet | |
| rm -rf "$ZIP_DIR" | |
| trap - EXIT | |
| - name: Run packaged Electron smoke suite | |
| working-directory: apps/desktop | |
| run: | | |
| APP_BUNDLE="$(find release -maxdepth 2 -name '*.app' -print -quit)" | |
| if [ -z "$APP_BUNDLE" ]; then | |
| echo "::error::Packaged app bundle was not found." | |
| exit 1 | |
| fi | |
| EXECUTABLE="$(find "$APP_BUNDLE/Contents/MacOS" -maxdepth 1 -type f -perm -111 -print -quit)" | |
| if [ -z "$EXECUTABLE" ]; then | |
| echo "::error::Packaged app executable was not found." | |
| exit 1 | |
| fi | |
| SIM_DESKTOP_EXECUTABLE="$EXECUTABLE" bunx playwright test e2e/packaged-smoke.spec.ts | |
| - name: Upload artifacts to the release | |
| if: ${{ inputs.publish }} | |
| env: | |
| DESKTOP_RELEASE_TOKEN: ${{ secrets.DESKTOP_RELEASE_TOKEN }} | |
| RELEASE_REPOSITORY: ${{ steps.channel.outputs.release_repository }} | |
| SOURCE_RELEASE_TOKEN: ${{ github.token }} | |
| TOKEN_KIND: ${{ steps.channel.outputs.token_kind }} | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| case "$TOKEN_KIND" in | |
| prerelease) GH_TOKEN="$DESKTOP_RELEASE_TOKEN" ;; | |
| stable) GH_TOKEN="$SOURCE_RELEASE_TOKEN" ;; | |
| *) | |
| echo "::error::Unknown desktop release token kind: $TOKEN_KIND" | |
| exit 1 ;; | |
| esac | |
| if [ -z "$GH_TOKEN" ]; then | |
| echo "::error::No GitHub token is available to publish to $RELEASE_REPOSITORY." | |
| exit 1 | |
| fi | |
| export GH_TOKEN | |
| if ! RELEASE_ID="$( | |
| gh release view "$VERSION" --repo "$RELEASE_REPOSITORY" \ | |
| --json databaseId --jq '.databaseId' | |
| )"; then | |
| echo "::error::Release $VERSION does not exist in $RELEASE_REPOSITORY." | |
| exit 1 | |
| fi | |
| if ! [[ "$RELEASE_ID" =~ ^[0-9]+$ ]]; then | |
| echo "::error::Release $VERSION returned an invalid database ID." | |
| exit 1 | |
| fi | |
| RELEASE_JSON="$(gh api "repos/${RELEASE_REPOSITORY}/releases/${RELEASE_ID}")" | |
| SEMVER="${VERSION#v}" | |
| ARTIFACTS=( | |
| "apps/desktop/release/Sim-${SEMVER}-universal.dmg" | |
| "apps/desktop/release/Sim-${SEMVER}-universal.dmg.blockmap" | |
| "apps/desktop/release/Sim-${SEMVER}-universal.zip" | |
| "apps/desktop/release/Sim-${SEMVER}-universal.zip.blockmap" | |
| "apps/desktop/release/latest-mac.yml" | |
| ) | |
| upload_or_verify() { | |
| local ARTIFACT="$1" | |
| local NAME SIZE DIGEST REMOTE REMOTE_SIZE REMOTE_DIGEST | |
| NAME="$(basename "$ARTIFACT")" | |
| SIZE="$(stat -f%z "$ARTIFACT")" | |
| DIGEST="sha256:$(shasum -a 256 "$ARTIFACT" | awk '{print $1}')" | |
| REMOTE="$(jq -c --arg name "$NAME" '.assets[] | select(.name == $name)' <<< "$RELEASE_JSON")" | |
| if [ -n "$REMOTE" ]; then | |
| REMOTE_SIZE="$(jq -r '.size' <<< "$REMOTE")" | |
| REMOTE_DIGEST="$(jq -r '.digest // empty' <<< "$REMOTE")" | |
| if [ "$REMOTE_SIZE" != "$SIZE" ] || [ "$REMOTE_DIGEST" != "$DIGEST" ]; then | |
| echo "::error::Existing release asset $NAME does not match this build." | |
| exit 1 | |
| fi | |
| echo "Verified existing release asset $NAME; skipping upload." | |
| return | |
| fi | |
| gh release upload "$VERSION" "$ARTIFACT" --repo "$RELEASE_REPOSITORY" | |
| } | |
| for ARTIFACT in "${ARTIFACTS[@]:0:4}"; do | |
| upload_or_verify "$ARTIFACT" | |
| done | |
| upload_or_verify "${ARTIFACTS[4]}" | |
| - name: Upload artifacts to the workflow run | |
| if: ${{ !inputs.publish }} | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: sim-desktop-${{ inputs.version }} | |
| path: | | |
| apps/desktop/release/*.dmg | |
| apps/desktop/release/*.zip | |
| apps/desktop/release/*.blockmap | |
| apps/desktop/release/latest-mac.yml | |
| retention-days: 7 |