name: release-stable on: workflow_dispatch: inputs: prerelease_version: description: "Exact validated prerelease version to promote, for example 0.5.1-prerelease.3." required: true type: string dry_run: description: "Release mode. metadata stops after promotion metadata; prepublish runs build/smoke/report/plan without publishing; publish performs the stable release." required: false type: choice options: - metadata - prepublish - publish default: metadata ref: description: "Stable release branch to build, for example release/v0.5.1. Empty builds the dispatch ref, which must also be release/vX.Y.Z." required: false type: string default: "" win_x64_smoke_mode: description: "Windows x64 smoke coverage." required: true type: choice options: - skip - core - full default: core win_x64_update_metadata_url: description: "Optional external updater metadata URL for full Windows x64 payload smoke." required: false type: string default: "" win_x64_update_target_version: description: "Optional external updater target version expected by full Windows x64 payload smoke." required: false type: string default: "" workflow_call: inputs: prerelease_version: description: "Exact validated prerelease version to promote." required: true type: string dry_run: description: "Release mode: metadata, prepublish, or publish. The legacy false value is also accepted for callers." required: false type: string default: "metadata" ref: description: "Stable release branch to build. Empty builds the caller's ref, which must also be release/vX.Y.Z." required: false type: string default: "" win_x64_smoke_mode: description: "Windows x64 smoke coverage: skip, core, or full." required: false type: string default: "core" win_x64_update_metadata_url: description: "Optional external updater metadata URL for full Windows x64 payload smoke." required: false type: string default: "" win_x64_update_target_version: description: "Optional external updater target version expected by full Windows x64 payload smoke." required: false type: string default: "" outputs: release_version: description: "The stable version that was validated or published." value: ${{ jobs.metadata.outputs.release_version }} channel: description: "The stable release channel." value: ${{ jobs.metadata.outputs.channel }} branch: description: "The branch that was built." value: ${{ jobs.metadata.outputs.branch }} commit: description: "The commit SHA that was built." value: ${{ jobs.metadata.outputs.commit }} dry_run: description: "Whether this run is a dry-run." value: ${{ jobs.metadata.outputs.dry_run }} dry_run_mode: description: "The stable dry-run boundary: metadata or prepublish." value: ${{ jobs.metadata.outputs.dry_run_mode }} previous_commit: description: "Commit SHA of the previously published prerelease (changelog baseline); empty on cold start." value: ${{ jobs.metadata.outputs.previous_commit }} version_metadata_url: description: "Public R2 URL of this build's version metadata.json." value: ${{ jobs.publish.outputs.version_metadata_url }} mac_arm64_url: description: "macOS arm64 dmg download URL." value: ${{ jobs.publish.outputs.mac_arm64_url }} mac_intel_url: description: "macOS x64 (Intel) dmg download URL." value: ${{ jobs.publish.outputs.mac_intel_url }} win_url: description: "Windows x64 installer download URL." value: ${{ jobs.publish.outputs.win_url }} linux_url: description: "Linux x64 AppImage download URL (empty if linux disabled)." value: ${{ jobs.publish.outputs.linux_url }} permissions: actions: write contents: write packages: write concurrency: group: open-design-release-stable-${{ inputs.dry_run }} cancel-in-progress: false env: OPEN_DESIGN_TELEMETRY_RELAY_URL: ${{ vars.OPEN_DESIGN_TELEMETRY_RELAY_URL }} # PostHog product-analytics ingest. Defined as repository secret + var # so official builds ship with analytics enabled; PR builds and forks # without these run the daemon in no-op analytics mode (events never # leave the user's machine, /api/analytics/config returns enabled=false). POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} POSTHOG_HOST: ${{ vars.POSTHOG_HOST }} # PostHog Error tracking sourcemap upload. Personal API key (phx_...) and # project ID let tools-pack's web-sourcemaps step ship browser sourcemaps # to PostHog after `next build` and before the .map files are stripped # from the packaged bundle. Missing in PR/fork builds → upload is skipped # and the helper still strips .map to keep source out of the installer. POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} POSTHOG_CLI_PROJECT_ID: ${{ vars.POSTHOG_CLI_PROJECT_ID }} # Run/repo attribution for the published metadata + per-platform manifests, # set workflow-wide so the platform build jobs and the publish-metadata step # agree on github.runId — publish-metadata.ts refuses a platform manifest # whose github.{runId,commit} disagree with the publish step's. These are the # same for caller and callee under workflow_call, so they're safe here. Branch # is NOT: under workflow_call (or inputs.ref) github.ref_name is the caller's # ref, so RELEASE_BRANCH is passed per publish step as the resolved # needs.metadata.outputs.branch instead, matching what the metadata job built. RELEASE_REPOSITORY: ${{ github.repository }} RELEASE_RUN_ATTEMPT: ${{ github.run_attempt }} RELEASE_RUN_ID: ${{ github.run_id }} RELEASE_WORKFLOW: ${{ github.workflow }} jobs: metadata: name: Prepare release metadata if: github.repository == 'nexu-io/open-design' runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} GITHUB_REPOSITORY: ${{ github.repository }} OPEN_DESIGN_PRERELEASE_METADATA_URL: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}/prerelease/latest/metadata.json OPEN_DESIGN_RELEASE_DRY_RUN: ${{ inputs.dry_run == 'publish' && 'false' || inputs.dry_run }} OPEN_DESIGN_RELEASE_CHANNEL: stable OPEN_DESIGN_RELEASES_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} OPEN_DESIGN_STABLE_PRERELEASE_VERSION: ${{ inputs.prerelease_version }} outputs: base_version: ${{ steps.stable.outputs.base_version }} branch: ${{ steps.stable.outputs.branch }} channel: ${{ steps.stable.outputs.channel }} commit: ${{ steps.stable.outputs.commit }} dry_run: ${{ steps.stable.outputs.dry_run }} dry_run_mode: ${{ steps.stable.outputs.dry_run_mode }} github_release_enabled: ${{ steps.stable.outputs.github_release_enabled }} linux_namespace: ${{ steps.stable.outputs.linux_namespace }} mac_intel_namespace: ${{ steps.stable.outputs.mac_intel_namespace }} namespace: ${{ steps.stable.outputs.namespace }} publish_side_effects_enabled: ${{ steps.stable.outputs.publish_side_effects_enabled }} prerelease_number: ${{ steps.stable.outputs.prerelease_number }} previous_stable: ${{ steps.stable.outputs.previous_stable }} previous_commit: ${{ steps.prev.outputs.previous_commit }} release_name: ${{ steps.stable.outputs.release_name }} release_version: ${{ steps.stable.outputs.release_version }} run_prepublish_jobs: ${{ steps.stable.outputs.run_prepublish_jobs }} stable_version: ${{ steps.stable.outputs.stable_version }} state_source: ${{ steps.stable.outputs.state_source }} version_tag: ${{ steps.stable.outputs.version_tag }} win_namespace: ${{ steps.stable.outputs.win_namespace }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: ref: ${{ inputs.ref != '' && inputs.ref || github.ref }} fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: 10.33.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Install dependencies run: pnpm install --frozen-lockfile # Under workflow_call the runner's GITHUB_SHA/GITHUB_REF_NAME describe the # caller's ref (e.g. main for the scheduled daily build), not the ref we # actually checked out. Resolve the real built HEAD so the published # metadata.json records the branch we built, not the caller. - name: Resolve built commit run: echo "BUILT_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" # Capture the previously published prerelease commit BEFORE this run overwrites # prerelease/latest/metadata.json, so the notification can diff the changelog # against the last package. Empty on cold start (HTTP 404). - name: Capture previous prerelease commit id: prev run: | set -euo pipefail prev="$(curl -fsSL "$OPEN_DESIGN_PRERELEASE_METADATA_URL" | jq -r '.github.commit // ""' 2>/dev/null || true)" echo "previous prerelease commit: ${prev:-}" echo "previous_commit=$prev" >> "$GITHUB_OUTPUT" - name: Prepare release metadata id: stable env: GITHUB_SHA: ${{ env.BUILT_SHA }} GITHUB_REF_NAME: ${{ inputs.ref != '' && inputs.ref || github.ref_name }} run: pnpm exec tools-release prepare stable - name: Validate R2 release access if: ${{ steps.stable.outputs.publish_side_effects_enabled == 'true' }} env: GITHUB_SHA: ${{ env.BUILT_SHA }} R2_ACCESS_PROBE_NAME: ${{ steps.stable.outputs.namespace }} RELEASE_CHANNEL: stable RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} RELEASE_STORAGE_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }} RELEASE_STORAGE_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }} RELEASE_STORAGE_ENDPOINT: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }} RELEASE_STORAGE_REGION: auto RELEASE_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }} run: pnpm exec tools-release check-storage verify: name: Verify build (typecheck + tests) needs: metadata if: ${{ needs.metadata.outputs.run_prepublish_jobs == 'true' }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 with: ref: ${{ inputs.ref }} fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: 10.33.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Install dependencies run: pnpm install --frozen-lockfile # `scripts/postinstall.mjs` auto-builds `packages/*` and `tools/*`, but # `apps/daemon` and `apps/desktop` are not in that list. On a fresh clone # (every CI run), workspace typecheck fails because: # - packaged/runtime consumers resolve the daemon package export through # generated `apps/daemon/dist/*.d.ts` # - `apps/packaged/src/index.ts` dynamic-imports `@open-design/desktop/main` # which resolves to `apps/desktop/dist/main/index.d.ts` # Build them explicitly here. Keeps the root `typecheck` script untouched. - name: Build daemon and desktop (typecheck dependencies) run: | pnpm --filter @open-design/daemon build pnpm --filter @open-design/desktop build - name: Typecheck workspaces run: pnpm -r --workspace-concurrency=4 --if-present run typecheck - name: Check repository layout policies run: pnpm guard # Workspace tests are intentionally not gated here. apps/web's # i18n content-coverage tests assert that every locale carries # display metadata for every prompt template / skill / design # system. Those tests fail on `main` as of this writing because # PR #187 added two new prompt templates without translating # their metadata into the 9 ship-ready locales — an i18n drift # that's out of scope for the release infrastructure. Tracked as # a follow-up; revisit once locale metadata is back in sync. build_mac: name: Build release mac arm64 needs: [metadata, verify] if: ${{ needs.metadata.outputs.run_prepublish_jobs == 'true' }} runs-on: macos-14 env: GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: ref: ${{ inputs.ref }} fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: 10.33.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Inspect mac Electron framework symlinks run: | set -euo pipefail electron_dist="$(node -e 'const path = require("node:path"); const { createRequire } = require("node:module"); const requireFromDesktop = createRequire(path.join(process.cwd(), "apps/desktop/package.json")); const electron = requireFromDesktop.resolve("electron"); process.stdout.write(path.join(path.dirname(electron), "dist"));')" framework="$electron_dist/Electron.app/Contents/Frameworks/Electron Framework.framework" missing_links=0 for link in \ "$framework/Electron Framework" \ "$framework/Helpers" \ "$framework/Libraries" \ "$framework/Resources" \ "$framework/Versions/Current"; do if [ ! -L "$link" ]; then echo "::warning::Expected Electron framework symlink, got non-symlink: $link" missing_links=1 fi done if [ "$missing_links" -ne 0 ]; then ls -la "$framework" >&2 || true ls -la "$framework/Versions" >&2 || true echo "Continuing into tools-pack because electron-builder is the source of truth for whether packaging actually works." fi - name: Prepare Apple signing certificate env: APPLE_SIGNING_CERTIFICATE_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_BASE64 }} APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }} run: | set -euo pipefail cert_path="$RUNNER_TEMP/open-design-signing.p12" if ! printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 --decode > "$cert_path" 2>/dev/null; then printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 -D > "$cert_path" fi { echo "CSC_LINK=$cert_path" echo "CSC_KEY_PASSWORD=$APPLE_SIGNING_CERTIFICATE_PASSWORD" } >> "$GITHUB_ENV" - name: Build release mac artifacts env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | set -euo pipefail tools_pack_dir="$RUNNER_TEMP/tools-pack" build_json_path="$RUNNER_TEMP/mac-tools-pack-build.json" build_log_path="$RUNNER_TEMP/mac-tools-pack-build.log" rm -rf "$tools_pack_dir" : > "$build_log_path" build_args=( exec tools-pack mac build --dir "$tools_pack_dir" --namespace "${{ needs.metadata.outputs.namespace }}" --portable --app-version "${{ needs.metadata.outputs.release_version }}" --mac-compression normal --to all --require-vela-cli --json --signed --notarize ) if build_output="$(pnpm "${build_args[@]}" 2> >(tee -a "$build_log_path" >&2))"; then printf '%s\n' "$build_output" | tee "$build_json_path" else build_status=$? printf '%s\n' "$build_output" exit "$build_status" fi - name: Capture mac framework diagnostics if: ${{ failure() }} continue-on-error: true run: | set -euo pipefail output="$RUNNER_TEMP/mac-framework-diagnostics.txt" source_resolve_log="$RUNNER_TEMP/mac-framework-source-resolve.err" source_framework="$(node -e 'const path = require("node:path"); const { createRequire } = require("node:module"); const requireFromDesktop = createRequire(path.join(process.cwd(), "apps/desktop/package.json")); const electron = requireFromDesktop.resolve("electron"); process.stdout.write(path.join(path.dirname(electron), "dist", "Electron.app", "Contents", "Frameworks", "Electron Framework.framework"));' 2>"$source_resolve_log" || true)" app_name="Open Design.app" if [ "${{ needs.metadata.outputs.channel }}" = "prerelease" ]; then app_name="Open Design Prerelease.app" fi built_framework="$RUNNER_TEMP/tools-pack/out/mac/namespaces/${{ needs.metadata.outputs.namespace }}/builder/mac-arm64/$app_name/Contents/Frameworks/Electron Framework.framework" dump_framework() { local label="$1" local framework="$2" echo "## $label" echo "path=$framework" if [ ! -e "$framework" ] && [ ! -L "$framework" ]; then echo "missing" return 0 fi echo "### top-level" ls -la "$framework" || true echo "### symlinks" find "$framework" -maxdepth 4 -type l -print0 | while IFS= read -r -d '' link; do printf '%s -> %s\n' "$link" "$(readlink "$link")" done || true echo "### selected stat" for path in \ "$framework" \ "$framework/Electron Framework" \ "$framework/Versions" \ "$framework/Versions/Current" \ "$framework/Versions/Current/Electron Framework" \ "$framework/Versions/A" \ "$framework/Versions/A/Electron Framework" \ "$framework/Resources" \ "$framework/Versions/A/Resources/Info.plist"; do if [ -e "$path" ] || [ -L "$path" ]; then stat -f '%Sp %HT %N' "$path" || true else echo "missing: $path" fi done echo "### plist" plutil -p "$framework/Versions/A/Resources/Info.plist" 2>&1 || true echo "### codesign display" codesign --display --verbose=4 "$framework/Electron Framework" 2>&1 || true codesign --display --verbose=4 "$framework/Versions/Current/Electron Framework" 2>&1 || true codesign --display --verbose=4 "$framework/Versions/A/Electron Framework" 2>&1 || true codesign --display --verbose=4 "$framework" 2>&1 || true } { date -u if [ -n "$source_framework" ]; then dump_framework "source Electron Framework" "$source_framework" else echo "## source Electron Framework" echo "resolve failed" cat "$source_resolve_log" || true fi dump_framework "built Electron Framework" "$built_framework" } > "$output" cat "$output" - name: Upload mac build diagnostics if: ${{ always() }} uses: actions/upload-artifact@v7 with: name: open-design-stable-mac-build-diagnostics path: | ${{ runner.temp }}/mac-tools-pack-build.log ${{ runner.temp }}/mac-tools-pack-build.json ${{ runner.temp }}/mac-framework-diagnostics.txt if-no-files-found: warn - name: Smoke release mac packaged runtime working-directory: e2e env: OD_PACKAGED_E2E_BUILD_JSON_PATH: ${{ runner.temp }}/mac-tools-pack-build.json OD_PACKAGED_E2E_BUILD_LOG_PATH: ${{ runner.temp }}/mac-tools-pack-build.log OD_PACKAGED_E2E_MAC: "1" OD_PACKAGED_E2E_MAC_ONBOARDING_SMOKE: ${{ needs.metadata.outputs.channel == 'nightly' && '1' || '0' }} OD_PACKAGED_E2E_NAMESPACE: ${{ needs.metadata.outputs.namespace }} OD_PACKAGED_E2E_RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} OD_PACKAGED_E2E_RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} OD_PACKAGED_E2E_REPORT_DIR: ${{ runner.temp }}/release-report/mac OD_PACKAGED_E2E_TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack run: | set -euo pipefail pnpm exec tsx scripts/release-smoke.ts mac specs/mac.spec.ts - name: Upload mac e2e spec report if: ${{ always() }} uses: actions/upload-artifact@v7 with: name: open-design-release-mac-e2e-report path: ${{ runner.temp }}/release-report/mac if-no-files-found: warn - name: Prepare mac release assets id: assets env: RELEASE_ARTIFACT_MODE: all RELEASE_ASSET_SUFFIX: "" RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets RELEASE_BRANCH: ${{ needs.metadata.outputs.branch }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_NAMESPACE: ${{ needs.metadata.outputs.namespace }} RELEASE_NOTES: Open Design ${{ needs.metadata.outputs.release_version }} RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} RELEASE_TARGET: mac_arm64 RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack run: bash tools/release/scripts/prepare-platform-assets.sh - name: Publish mac platform env: RELEASE_ARTIFACT_MODE: all RELEASE_ASSET_SUFFIX: "" RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets RELEASE_BRANCH: ${{ needs.metadata.outputs.branch }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }} RELEASE_DRY_RUN_MODE: ${{ needs.metadata.outputs.dry_run_mode }} RELEASE_PUBLISH_SIDE_EFFECTS: ${{ needs.metadata.outputs.publish_side_effects_enabled }} RELEASE_MANIFEST_DIR: ${{ runner.temp }}/release-platform-manifests RELEASE_OUTPUTS_PATH: ${{ runner.temp }}/release-platform-outputs/mac_arm64.json RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} RELEASE_REPORT_DIR: ${{ runner.temp }}/release-report/mac RELEASE_REPORT_ZIP_PATH: ${{ runner.temp }}/release-report/mac-report.zip RELEASE_SIGNED: "true" RELEASE_STORAGE_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }} RELEASE_STORAGE_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }} RELEASE_STORAGE_ENDPOINT: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }} RELEASE_STORAGE_REGION: auto RELEASE_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }} RELEASE_TARGET: mac_arm64 RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} run: pnpm exec tools-release publish-platform - name: Upload mac publish manifest uses: actions/upload-artifact@v7 with: name: open-design-release-mac-arm64-publish-manifest path: ${{ runner.temp }}/release-platform-manifests/mac_arm64.json if-no-files-found: error retention-days: 1 - name: Upload mac release bundle uses: actions/upload-artifact@v7 with: name: open-design-release-mac-release-assets path: ${{ runner.temp }}/release-assets retention-days: 1 build_mac_intel: name: Build release mac intel x64 needs: [metadata, verify] if: ${{ needs.metadata.outputs.run_prepublish_jobs == 'true' }} runs-on: macos-15-intel env: GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: ref: ${{ inputs.ref }} fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: 10.33.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Prepare Apple signing certificate env: APPLE_SIGNING_CERTIFICATE_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_BASE64 }} APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }} run: | set -euo pipefail cert_path="$RUNNER_TEMP/open-design-signing.p12" if ! printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 --decode > "$cert_path" 2>/dev/null; then printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 -D > "$cert_path" fi { echo "CSC_LINK=$cert_path" echo "CSC_KEY_PASSWORD=$APPLE_SIGNING_CERTIFICATE_PASSWORD" } >> "$GITHUB_ENV" - name: Build release mac intel artifacts env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | set -euo pipefail pnpm exec tools-pack mac build \ --dir "$RUNNER_TEMP/tools-pack" \ --namespace "${{ needs.metadata.outputs.mac_intel_namespace }}" \ --portable \ --app-version "${{ needs.metadata.outputs.release_version }}" \ --mac-compression normal \ --to all \ --require-vela-cli \ --json \ --signed \ --notarize - name: Prepare mac intel release assets id: assets env: RELEASE_ARTIFACT_MODE: all RELEASE_ASSET_SUFFIX: "" RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets RELEASE_BRANCH: ${{ needs.metadata.outputs.branch }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_NAMESPACE: ${{ needs.metadata.outputs.mac_intel_namespace }} RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} RELEASE_TARGET: mac_x64 RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack run: bash tools/release/scripts/prepare-platform-assets.sh - name: Publish mac intel platform env: RELEASE_ARTIFACT_MODE: all RELEASE_ASSET_SUFFIX: "" RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets RELEASE_BRANCH: ${{ needs.metadata.outputs.branch }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }} RELEASE_DRY_RUN_MODE: ${{ needs.metadata.outputs.dry_run_mode }} RELEASE_PUBLISH_SIDE_EFFECTS: ${{ needs.metadata.outputs.publish_side_effects_enabled }} RELEASE_MANIFEST_DIR: ${{ runner.temp }}/release-platform-manifests RELEASE_OUTPUTS_PATH: ${{ runner.temp }}/release-platform-outputs/mac_x64.json RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} RELEASE_SIGNED: "true" RELEASE_STORAGE_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }} RELEASE_STORAGE_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }} RELEASE_STORAGE_ENDPOINT: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }} RELEASE_STORAGE_REGION: auto RELEASE_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }} RELEASE_TARGET: mac_x64 RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} run: pnpm exec tools-release publish-platform - name: Upload mac intel publish manifest uses: actions/upload-artifact@v7 with: name: open-design-release-mac-x64-publish-manifest path: ${{ runner.temp }}/release-platform-manifests/mac_x64.json if-no-files-found: error retention-days: 1 - name: Upload mac intel release bundle uses: actions/upload-artifact@v7 with: name: open-design-release-mac-intel-release-assets path: ${{ runner.temp }}/release-assets retention-days: 1 build_win: name: Build release win x64 needs: [metadata, verify] if: ${{ needs.metadata.outputs.run_prepublish_jobs == 'true' }} runs-on: windows-latest env: GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: ref: ${{ inputs.ref }} fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: 10.33.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 cache: pnpm cache-dependency-path: pnpm-lock.yaml - name: Compute Windows tools-pack cache key id: win_tools_pack_cache_key shell: pwsh env: WIN_TOOLS_PACK_ORIGIN_KEY: ${{ hashFiles('package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'apps/daemon/**', 'apps/web/**', 'apps/desktop/**', 'apps/packaged/**', 'packages/agui-adapter/**', 'packages/contracts/**', 'packages/plugin-runtime/**', 'packages/sidecar-proto/**', 'packages/sidecar/**', 'packages/platform/**', 'tools/pack/bin/**', 'tools/pack/package.json', 'tools/pack/resources/**', 'tools/pack/src/**', 'tools/pack/tsconfig.json', 'assets/community-pets/**', 'assets/frames/**', 'craft/**', 'design-systems/**', 'design-templates/**', 'plugins/_official/**', 'plugins/registry/**', 'prompt-templates/**', 'skills/**', '.github/workflows/release-stable.yml', '.github/scripts/release/cache/win.ps1') }} run: | if ([string]::IsNullOrWhiteSpace($env:WIN_TOOLS_PACK_ORIGIN_KEY)) { throw "Windows tools-pack cache origin key is empty" } $prefix = "tools-pack-win-v7-stable-$env:RUNNER_OS-" "origin=$env:WIN_TOOLS_PACK_ORIGIN_KEY" | Out-File -FilePath $env:GITHUB_OUTPUT -Append "prefix=$prefix" | Out-File -FilePath $env:GITHUB_OUTPUT -Append "key=$prefix$env:WIN_TOOLS_PACK_ORIGIN_KEY" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - name: Restore Windows tools-pack cache id: win_tools_pack_cache_restore uses: actions/cache/restore@v5 continue-on-error: true with: path: ${{ runner.temp }}/tools-pack-cache key: ${{ steps.win_tools_pack_cache_key.outputs.key }} restore-keys: | ${{ steps.win_tools_pack_cache_key.outputs.prefix }} - name: Install dependencies run: pnpm install --frozen-lockfile - name: Setup NSIS shell: pwsh run: | if ((Get-Command makensis.exe -ErrorAction SilentlyContinue) -or (Test-Path "C:\Program Files (x86)\NSIS\makensis.exe")) { exit 0 } choco install nsis -y --no-progress - name: Build release windows artifacts id: win_tools_pack_build shell: pwsh run: | $ErrorActionPreference = "Stop" $toolsPackDir = "${{ runner.temp }}/tools-pack" $cacheDir = "${{ runner.temp }}/tools-pack-cache" $buildJsonPath = Join-Path $env:RUNNER_TEMP "windows-tools-pack-build.json" pnpm.cmd exec tools-pack win cleanup --dir $toolsPackDir --namespace "${{ needs.metadata.outputs.win_namespace }}" --json $buildArgs = @( "exec", "tools-pack", "win", "build", "--dir", $toolsPackDir, "--cache-dir", $cacheDir, "--namespace", "${{ needs.metadata.outputs.win_namespace }}", "--portable", "--app-version", "${{ needs.metadata.outputs.release_version }}", "--to", "all", "--require-vela-cli", "--json" ) "cache_failed=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append try { $buildOutput = pnpm @buildArgs if ($LASTEXITCODE -ne 0) { throw "Windows tools-pack cached build exited with code $LASTEXITCODE" } } catch { Write-Warning "Windows tools-pack cached build failed; removing restored cache and retrying without cache." "cache_failed=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $cacheDir pnpm.cmd exec tools-pack win cleanup --dir $toolsPackDir --namespace "${{ needs.metadata.outputs.win_namespace }}" --json $buildOutput = pnpm exec tools-pack win build ` --dir $toolsPackDir ` --cache-dir $cacheDir ` --namespace "${{ needs.metadata.outputs.win_namespace }}" ` --portable ` --app-version "${{ needs.metadata.outputs.release_version }}" ` --to all ` --require-vela-cli ` --json if ($LASTEXITCODE -ne 0) { throw "Windows tools-pack uncached fallback build exited with code $LASTEXITCODE" } } $buildOutput | Set-Content -Path $buildJsonPath $build = $buildOutput | ConvertFrom-Json pnpm.cmd exec tools-pack win validate-payload --namespace "${{ needs.metadata.outputs.win_namespace }}" --payload-path $build.payloadPath --expected-version "${{ needs.metadata.outputs.release_version }}" --json $buildOutput - name: Delete failed Windows tools-pack cache if: ${{ steps.win_tools_pack_build.outputs.cache_failed == 'true' && steps.win_tools_pack_cache_restore.outputs.cache-matched-key != '' }} shell: pwsh continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: | $matchedKey = "${{ steps.win_tools_pack_cache_restore.outputs.cache-matched-key }}" $caches = @(gh cache list --key $matchedKey --limit 100 --json id,key,ref | ConvertFrom-Json | Where-Object { $_.key -eq $matchedKey }) foreach ($cache in $caches) { gh cache delete $cache.id } "deletedFailedCacheKey=$matchedKey count=$($caches.Count)" - name: Build stable win_x64 update fixture if: ${{ inputs.win_x64_smoke_mode == 'full' && inputs.win_x64_update_metadata_url == '' && inputs.win_x64_update_target_version == '' }} shell: pwsh run: | $ErrorActionPreference = "Stop" $match = [regex]::Match("${{ needs.metadata.outputs.release_version }}", "^(?\d+)\.(?\d+)\.(?\d+)$") if (-not $match.Success) { throw "full Windows stable smoke requires stable version x.y.z; got ${{ needs.metadata.outputs.release_version }}" } $updateVersion = "{0}.{1}.{2}" -f $match.Groups["major"].Value, $match.Groups["minor"].Value, ([int]$match.Groups["patch"].Value + 1) $buildArgs = @("exec", "tools-pack", "win", "build", "--dir", "${{ runner.temp }}\release-work\win_x64\tools-pack-update-fixture", "--cache-dir", "${{ runner.temp }}\tools-pack-cache", "--namespace", "${{ needs.metadata.outputs.win_namespace }}", "--app-version", $updateVersion, "--to", "nsis", "--require-vela-cli", "--json") $updateOutput = pnpm.cmd @buildArgs if ($LASTEXITCODE -ne 0) { throw "tools-pack win update fixture failed with exit code $LASTEXITCODE" } $updateOutput | Set-Content -Path "${{ runner.temp }}\windows-tools-pack-update-build.json" -Encoding utf8 $updateBuild = $updateOutput | ConvertFrom-Json pnpm.cmd exec tools-pack win validate-payload --namespace "${{ needs.metadata.outputs.win_namespace }}" --payload-path $updateBuild.payloadPath --expected-version $updateVersion --json - name: Smoke release windows packaged runtime if: ${{ inputs.win_x64_smoke_mode != 'skip' }} working-directory: e2e env: OD_PACKAGED_E2E_BUILD_JSON_PATH: ${{ runner.temp }}/windows-tools-pack-build.json OD_PACKAGED_E2E_WIN: "1" OD_PACKAGED_E2E_WIN_ONBOARDING_SMOKE: ${{ needs.metadata.outputs.channel == 'nightly' && '1' || '0' }} OD_PACKAGED_E2E_NAMESPACE: ${{ needs.metadata.outputs.win_namespace }} OD_PACKAGED_E2E_RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} OD_PACKAGED_E2E_RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} OD_PACKAGED_E2E_REPORT_DIR: ${{ runner.temp }}/release-report/win OD_PACKAGED_E2E_TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack OD_PACKAGED_E2E_WIN_SMOKE_PROFILE: ${{ inputs.win_x64_smoke_mode }} OD_PACKAGED_E2E_WIN_UPDATE_FIXTURE: ${{ inputs.win_x64_smoke_mode == 'full' && inputs.win_x64_update_metadata_url == '' && inputs.win_x64_update_target_version == '' && 'tools-serve' || '' }} OD_PACKAGED_E2E_WIN_UPDATE_METADATA_URL: ${{ inputs.win_x64_update_metadata_url }} OD_PACKAGED_E2E_WIN_UPDATE_VERSION: ${{ inputs.win_x64_update_target_version }} run: | $ErrorActionPreference = "Stop" pnpm exec tsx scripts/release-smoke.ts win specs/win.spec.ts if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Upload windows e2e spec report if: ${{ always() }} uses: actions/upload-artifact@v7 with: name: open-design-release-win-e2e-report path: ${{ runner.temp }}/release-report/win if-no-files-found: warn - name: Prune Windows tools-pack cache shell: pwsh continue-on-error: true run: ./.github/scripts/release/cache/win.ps1 - name: Save Windows tools-pack cache if: ${{ success() && (steps.win_tools_pack_cache_restore.outputs.cache-hit != 'true' || steps.win_tools_pack_build.outputs.cache_failed == 'true') }} uses: actions/cache/save@v5 continue-on-error: true with: path: ${{ runner.temp }}/tools-pack-cache key: ${{ steps.win_tools_pack_cache_key.outputs.key }} - name: Retain recent Windows tools-pack caches if: ${{ success() }} shell: pwsh continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: | $prefix = "${{ steps.win_tools_pack_cache_key.outputs.prefix }}" $keep = 3 $caches = @(gh cache list --key $prefix --sort created_at --order desc --limit 100 --json id,key,createdAt | ConvertFrom-Json) $stale = @($caches | Select-Object -Skip $keep) foreach ($cache in $stale) { gh cache delete $cache.id } "actionsCachePrefix=$prefix kept=$([Math]::Min($caches.Count, $keep)) deleted=$($stale.Count)" - name: Prepare windows release assets shell: pwsh run: | tools\release\scripts\prepare-platform-assets.ps1 ` -ReleaseTarget win_x64 ` -ReleaseAssetsDir "${{ runner.temp }}\release-assets" ` -BuildJsonPath "${{ runner.temp }}\windows-tools-pack-build.json" ` -ReleaseNamespace "${{ needs.metadata.outputs.win_namespace }}" ` -ReleaseVersion "${{ needs.metadata.outputs.release_version }}" ` -ReleaseAssetSuffix "" ` -ReleaseChannel "${{ needs.metadata.outputs.channel }}" ` -ReleasePublicOrigin "${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}" ` -ReleaseNotes "Open Design ${{ needs.metadata.outputs.release_version }}" ` -IncludeZip $true - name: Publish windows platform env: RELEASE_ASSET_SUFFIX: "" RELEASE_ASSETS_DIR: ${{ runner.temp }}\release-assets RELEASE_BRANCH: ${{ needs.metadata.outputs.branch }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }} RELEASE_DRY_RUN_MODE: ${{ needs.metadata.outputs.dry_run_mode }} RELEASE_PUBLISH_SIDE_EFFECTS: ${{ needs.metadata.outputs.publish_side_effects_enabled }} RELEASE_MANIFEST_DIR: ${{ runner.temp }}\release-platform-manifests RELEASE_OUTPUTS_PATH: ${{ runner.temp }}\release-platform-outputs\win_x64.json RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} RELEASE_REPORT_DIR: ${{ runner.temp }}\release-report\win RELEASE_SIGNED: "false" RELEASE_STORAGE_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }} RELEASE_STORAGE_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }} RELEASE_STORAGE_ENDPOINT: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }} RELEASE_STORAGE_REGION: auto RELEASE_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }} RELEASE_TARGET: win_x64 RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} WIN_INCLUDE_ZIP: "true" run: pnpm exec tools-release publish-platform - name: Upload windows publish manifest uses: actions/upload-artifact@v7 with: name: open-design-release-win-x64-publish-manifest path: ${{ runner.temp }}\release-platform-manifests\win_x64.json if-no-files-found: error retention-days: 1 - name: Upload windows release bundle uses: actions/upload-artifact@v7 with: name: open-design-release-win-release-assets path: ${{ runner.temp }}/release-assets retention-days: 1 build_linux: name: Build release linux x64 needs: [metadata, verify] # Linux AppImage packaging is temporarily excluded from stable releases. # Keep the job definition in place so the Linux lane can be re-enabled once # the containerized pnpm bootstrap is fixed and reviewed. if: ${{ needs.metadata.outputs.run_prepublish_jobs == 'true' && vars.ENABLE_STABLE_LINUX == 'true' }} runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: ref: ${{ inputs.ref }} fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: 10.33.2 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Install dependencies run: pnpm install --frozen-lockfile # `--containerized` builds the AppImage inside the electronuserland/builder # Docker image (glibc 2.27 baseline) so the resulting binary runs on older # distros than ubuntu-latest's glibc 2.39. Docker is preinstalled on the # GitHub-hosted ubuntu-latest runner, so no extra setup is required. - name: Build release linux artifacts run: | set -euo pipefail tools_pack_dir="$RUNNER_TEMP/tools-pack" report_dir="$RUNNER_TEMP/release-report/linux" build_json_path="$report_dir/tools-pack.json" build_log_path="$report_dir/tools-pack.log" rm -rf "$tools_pack_dir" mkdir -p "$report_dir" : > "$build_log_path" build_args=( exec tools-pack linux build --dir "$tools_pack_dir" --namespace "${{ needs.metadata.outputs.linux_namespace }}" --portable --app-version "${{ needs.metadata.outputs.release_version }}" --to appimage --containerized --json ) if build_output="$(pnpm "${build_args[@]}" 2> >(tee -a "$build_log_path" >&2))"; then printf '%s\n' "$build_output" | tee "$build_json_path" node -e 'const fs = require("node:fs"); JSON.parse(fs.readFileSync(process.argv[1], "utf8"));' "$build_json_path" else build_status=$? printf '%s\n' "$build_output" | tee "$build_json_path" exit "$build_status" fi - name: Smoke release linux AppImage runtime working-directory: e2e env: OD_PACKAGED_E2E_LINUX_APPIMAGE: "1" OD_PACKAGED_E2E_NAMESPACE: ${{ needs.metadata.outputs.linux_namespace }} OD_PACKAGED_E2E_SCREENSHOT_PATH: ${{ runner.temp }}/release-report/linux/screenshots/open-design-linux-smoke.png OD_PACKAGED_E2E_TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack run: | set -euo pipefail report_dir="$RUNNER_TEMP/release-report/linux" mkdir -p "$report_dir/screenshots" cat > "$report_dir/manifest.json" <&1 | tee "$report_dir/apt-get-update.log" sudo apt-get install -y xvfb 2>&1 | tee "$report_dir/apt-get-install-xvfb.log" xvfb-run -a pnpm test specs/linux.spec.ts 2>&1 | tee "$report_dir/vitest.log" - name: Upload linux e2e spec report if: ${{ always() }} uses: actions/upload-artifact@v7 with: name: open-design-release-linux-e2e-report path: ${{ runner.temp }}/release-report/linux if-no-files-found: warn - name: Prepare linux release assets env: RELEASE_ASSET_SUFFIX: "" RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets RELEASE_BRANCH: ${{ needs.metadata.outputs.branch }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_NAMESPACE: ${{ needs.metadata.outputs.linux_namespace }} RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} RELEASE_TARGET: linux_x64 RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack run: bash tools/release/scripts/prepare-platform-assets.sh - name: Publish linux platform env: RELEASE_ASSET_SUFFIX: "" RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets RELEASE_BRANCH: ${{ needs.metadata.outputs.branch }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }} RELEASE_DRY_RUN_MODE: ${{ needs.metadata.outputs.dry_run_mode }} RELEASE_PUBLISH_SIDE_EFFECTS: ${{ needs.metadata.outputs.publish_side_effects_enabled }} RELEASE_MANIFEST_DIR: ${{ runner.temp }}/release-platform-manifests RELEASE_OUTPUTS_PATH: ${{ runner.temp }}/release-platform-outputs/linux_x64.json RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} RELEASE_REPORT_DIR: ${{ runner.temp }}/release-report/linux RELEASE_SIGNED: "false" RELEASE_STORAGE_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }} RELEASE_STORAGE_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }} RELEASE_STORAGE_ENDPOINT: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }} RELEASE_STORAGE_REGION: auto RELEASE_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }} RELEASE_TARGET: linux_x64 RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} run: pnpm exec tools-release publish-platform - name: Upload linux publish manifest uses: actions/upload-artifact@v7 with: name: open-design-release-linux-x64-publish-manifest path: ${{ runner.temp }}/release-platform-manifests/linux_x64.json if-no-files-found: error retention-days: 1 - name: Upload linux release bundle uses: actions/upload-artifact@v7 with: name: open-design-release-linux-release-assets path: ${{ runner.temp }}/release-assets retention-days: 1 publish_docker_image: name: Publish stable Docker image needs: - metadata - publish if: >- ${{ always() && !cancelled() && needs.metadata.outputs.dry_run != 'true' && needs.metadata.outputs.github_release_enabled == 'true' && needs.metadata.result == 'success' && needs.publish.result == 'success' }} permissions: contents: read packages: write id-token: write uses: ./.github/workflows/docker-image.yml secrets: inherit with: ref: ${{ needs.metadata.outputs.commit }} release_version: ${{ needs.metadata.outputs.stable_version }} publish_latest: true publish: name: Finalize ${{ needs.metadata.outputs.channel }} release plan needs: - metadata - verify - build_mac - build_mac_intel - build_win - build_linux if: >- ${{ always() && !cancelled() && needs.metadata.outputs.run_prepublish_jobs == 'true' && needs.metadata.result == 'success' && needs.verify.result == 'success' && needs.build_mac.result == 'success' && needs.build_mac_intel.result == 'success' && needs.build_win.result == 'success' && (needs.build_linux.result == 'success' || needs.build_linux.result == 'skipped') }} runs-on: ubuntu-latest outputs: version_metadata_url: ${{ steps.outputs.outputs.version_metadata_url }} mac_arm64_url: ${{ steps.outputs.outputs.mac_arm64_dmg_url }} mac_intel_url: ${{ steps.outputs.outputs.mac_x64_dmg_url }} win_url: ${{ steps.outputs.outputs.win_x64_installer_url }} linux_url: ${{ steps.outputs.outputs.linux_x64_appImage_url }} env: GH_TOKEN: ${{ github.token }} # Record the real built commit in metadata.json. Under workflow_call the # runner's GITHUB_SHA is the caller's ref, so pin it to the resolved build # commit; the next run reads this back as its changelog baseline. GITHUB_SHA: ${{ needs.metadata.outputs.commit }} BASE_VERSION: ${{ needs.metadata.outputs.base_version }} BRANCH_NAME: ${{ needs.metadata.outputs.branch }} ENABLE_LINUX: ${{ needs.build_linux.result == 'success' }} ENABLE_MAC: "true" ENABLE_MAC_INTEL: "true" ENABLE_WIN: "true" GITHUB_RELEASE_ENABLED: ${{ needs.metadata.outputs.github_release_enabled }} MAC_INTEL_SIGNED: "true" PRERELEASE_NUMBER: ${{ needs.metadata.outputs.prerelease_number }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_NAME: ${{ needs.metadata.outputs.release_name }} RELEASE_SIGNED: "true" RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} REPORT_MODE: zip STATE_SOURCE: ${{ needs.metadata.outputs.state_source }} VERSION_TAG: ${{ needs.metadata.outputs.version_tag }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: ref: ${{ inputs.ref }} fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: 10.33.2 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Download mac publish manifest uses: actions/download-artifact@v8 with: name: open-design-release-mac-arm64-publish-manifest path: ${{ runner.temp }}/release-platform-manifests - name: Download mac intel publish manifest uses: actions/download-artifact@v8 with: name: open-design-release-mac-x64-publish-manifest path: ${{ runner.temp }}/release-platform-manifests - name: Download windows publish manifest uses: actions/download-artifact@v8 with: name: open-design-release-win-x64-publish-manifest path: ${{ runner.temp }}/release-platform-manifests - name: Download linux publish manifest if: ${{ needs.build_linux.result == 'success' }} uses: actions/download-artifact@v8 with: name: open-design-release-linux-x64-publish-manifest path: ${{ runner.temp }}/release-platform-manifests - name: Pre-flight tag/release check if: ${{ needs.metadata.outputs.github_release_enabled == 'true' }} run: | set -euo pipefail if git ls-remote --exit-code --tags origin "refs/tags/$VERSION_TAG" >/dev/null 2>&1; then echo "tag $VERSION_TAG already exists on origin; aborting" >&2 exit 1 fi if gh release view "$VERSION_TAG" >/dev/null 2>&1; then echo "release $VERSION_TAG already exists; aborting" >&2 exit 1 fi - name: Download mac release bundle uses: actions/download-artifact@v8 with: name: open-design-release-mac-release-assets path: ${{ runner.temp }}/release-assets/mac - name: Download mac intel release bundle uses: actions/download-artifact@v8 with: name: open-design-release-mac-intel-release-assets path: ${{ runner.temp }}/release-assets/mac-intel - name: Download windows release bundle uses: actions/download-artifact@v8 with: name: open-design-release-win-release-assets path: ${{ runner.temp }}/release-assets/win - name: Download linux release bundle if: ${{ needs.build_linux.result == 'success' }} uses: actions/download-artifact@v8 with: name: open-design-release-linux-release-assets path: ${{ runner.temp }}/release-assets/linux - name: Download linux e2e spec report if: ${{ needs.build_linux.result == 'success' }} uses: actions/download-artifact@v8 with: name: open-design-release-linux-e2e-report path: ${{ runner.temp }}/release-report/linux - name: Download mac e2e spec report uses: actions/download-artifact@v8 with: name: open-design-release-mac-e2e-report path: ${{ runner.temp }}/release-report/mac - name: Download windows e2e spec report uses: actions/download-artifact@v8 with: name: open-design-release-win-e2e-report path: ${{ runner.temp }}/release-report/win - name: Prepare stable GitHub Release asset plan env: ENABLE_LINUX_X64: ${{ needs.build_linux.result == 'success' }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_GITHUB_ASSETS_DIR: ${{ runner.temp }}/github-release-assets RELEASE_GITHUB_ASSETS_SOURCE_DIR: ${{ runner.temp }}/release-assets RELEASE_OUTPUTS_PATH: ${{ runner.temp }}/github-release-assets-plan/outputs.json RELEASE_SUMMARY_PATH: ${{ runner.temp }}/github-release-assets-plan/summary.md RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} run: pnpm exec tools-release prepare-github-assets - name: Publish GitHub Release asset plan summary run: cat "${{ runner.temp }}/github-release-assets-plan/summary.md" >> "$GITHUB_STEP_SUMMARY" - name: Write stable release notes id: notes if: ${{ needs.metadata.outputs.github_release_enabled == 'true' }} env: RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} run: bash .github/scripts/release/github/stable-notes.sh - name: Create draft release with tag id: create_release if: ${{ needs.metadata.outputs.github_release_enabled == 'true' }} run: | set -euo pipefail # gh release create creates the tag at $GITHUB_SHA atomically with the release. # Using --draft keeps the release invisible until R2 publish and asset upload verify successfully. gh release create "$VERSION_TAG" \ --target "$GITHUB_SHA" \ --title "$RELEASE_NAME" \ --notes-file "${{ steps.notes.outputs.notes_file }}" \ --draft - name: Upload assets to draft release if: ${{ needs.metadata.outputs.github_release_enabled == 'true' }} run: | set -euo pipefail gh release upload "$VERSION_TAG" "$RUNNER_TEMP/github-release-assets"/* - name: Prepare release metadata env: BASE_VERSION: ${{ needs.metadata.outputs.base_version }} ENABLE_LINUX_X64: ${{ needs.build_linux.result == 'success' }} ENABLE_MAC_ARM64: "true" ENABLE_MAC_X64: "true" ENABLE_WIN_X64: "true" LINUX_X64_RESULT: ${{ needs.build_linux.result }} MAC_ARM64_RESULT: ${{ needs.build_mac.result }} MAC_X64_RESULT: ${{ needs.build_mac_intel.result }} RELEASE_ASSET_SUFFIX: "" RELEASE_BRANCH: ${{ needs.metadata.outputs.branch }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }} RELEASE_DRY_RUN_MODE: ${{ needs.metadata.outputs.dry_run_mode }} RELEASE_PUBLISH_SIDE_EFFECTS: ${{ needs.metadata.outputs.publish_side_effects_enabled }} RELEASE_MANIFEST_DIR: ${{ runner.temp }}/release-platform-manifests RELEASE_METADATA_DIR: ${{ runner.temp }}/release-metadata RELEASE_OUTPUTS_PATH: ${{ runner.temp }}/release-metadata/outputs.json RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }} RELEASE_REPOSITORY: ${{ github.repository }} RELEASE_RUN_ATTEMPT: ${{ github.run_attempt }} RELEASE_RUN_ID: ${{ github.run_id }} RELEASE_SIGNED: "true" RELEASE_STORAGE_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }} RELEASE_STORAGE_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }} RELEASE_STORAGE_ENDPOINT: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }} RELEASE_STORAGE_REGION: auto RELEASE_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }} RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} RELEASE_WORKFLOW: ${{ github.workflow }} STABLE_VERSION: ${{ needs.metadata.outputs.stable_version }} STATE_SOURCE: ${{ needs.metadata.outputs.state_source }} VERSION_TAG: ${{ needs.metadata.outputs.version_tag }} WIN_X64_RESULT: ${{ needs.build_win.result }} run: pnpm exec tools-release publish-metadata - name: Read metadata outputs id: outputs uses: actions/github-script@v8 with: script: | const fs = require("fs"); const outputs = JSON.parse(fs.readFileSync(`${process.env.RUNNER_TEMP}/release-metadata/outputs.json`, "utf8")); for (const [key, value] of Object.entries(outputs)) core.setOutput(key, String(value)); - name: Verify release metadata env: ENABLE_LINUX_X64: ${{ needs.build_linux.result == 'success' }} ENABLE_MAC_ARM64: "true" ENABLE_MAC_X64: "true" ENABLE_WIN_X64: "true" LINUX_X64_RESULT: ${{ needs.build_linux.result }} MAC_ARM64_RESULT: ${{ needs.build_mac.result }} MAC_X64_RESULT: ${{ needs.build_mac_intel.result }} RELEASE_DRY_RUN_MODE: ${{ needs.metadata.outputs.dry_run_mode }} RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_METADATA_URL: ${{ steps.outputs.outputs.version_metadata_url }} RELEASE_METADATA_PATH: ${{ needs.metadata.outputs.publish_side_effects_enabled == 'true' && '' || format('{0}/release-metadata/metadata.json', runner.temp) }} RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }} WIN_X64_RESULT: ${{ needs.build_win.result }} run: pnpm exec tools-release verify-metadata - name: Promote draft to published latest if: ${{ needs.metadata.outputs.github_release_enabled == 'true' }} run: | set -euo pipefail gh release edit "$VERSION_TAG" \ --draft=false \ --latest - name: Cleanup release + tag on failure if: ${{ failure() && needs.metadata.outputs.github_release_enabled == 'true' && steps.create_release.outcome == 'success' }} run: | set +e echo "publish failed after release was created; rolling back release and tag" gh release delete "$VERSION_TAG" --cleanup-tag --yes # belt-and-suspenders: ensure remote tag is gone even if --cleanup-tag missed git push origin --delete "refs/tags/$VERSION_TAG" || true - name: Publish summary env: RELEASE_CHANNEL: ${{ needs.metadata.outputs.channel }} RELEASE_METADATA_URL: ${{ steps.outputs.outputs.version_metadata_url }} RELEASE_METADATA_PATH: ${{ needs.metadata.outputs.publish_side_effects_enabled == 'true' && '' || format('{0}/release-metadata/metadata.json', runner.temp) }} RELEASE_SUMMARY_PATH: ${{ runner.temp }}/release-metadata/summary.md run: | pnpm exec tools-release summary-metadata cat "${{ runner.temp }}/release-metadata/summary.md" >> "$GITHUB_STEP_SUMMARY" - name: Cleanup workflow artifacts if: ${{ success() }} run: bash .github/scripts/release/github/cleanup-artifacts.sh cleanup_partial_release_assets: name: Cleanup unpublished release asset artifacts needs: - metadata - build_mac - build_mac_intel - build_win - build_linux - publish if: ${{ always() && needs.metadata.outputs.dry_run != 'true' && needs.publish.result != 'success' }} runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v6.0.2 with: ref: ${{ inputs.ref }} - name: Delete unpublished release asset artifacts env: ARTIFACT_CLEANUP_DESCRIPTION: intermediate release asset Actions artifacts from this unpublished run. Canonical manual downloads are only the R2 links in a successful publish summary ARTIFACT_NAME_REGEX: "-release-assets$" run: bash .github/scripts/release/github/cleanup-artifacts.sh