mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
faba302437
* feat(release): alert through the other Feishu bot when the prerelease card goes silent The progressive card is now the only prerelease notification, and every way it breaks is silent: the pipeline stays green, the packages ship, and the channel hears nothing. The watcher catches Feishu failures on purpose — one hiccup must not end a two-hour watch — so expired application-bot credentials look exactly like a healthy run. Adds a last-resort notice delivered through the custom-bot webhook, which is a different bot with a different secret, so a credential failure on the card side cannot take the alert down with it. It fires only when the card did not reach the group: the watcher now reports `card_delivered` for a card whose FINAL state is in the chat, and the gate reads that rather than the job result. Two callers, mutually exclusive so a release can never draw both: release-prerelease-card.yml's `fallback_notice` job covers a watcher that died or never delivered; steps at the end of release-prerelease.yml's `dispatch_validation` cover a card that was never dispatched. The latter are steps, not a job, because nothing may appear in a `needs:` on the dispatchers. * fix(release): keep the fallback notice reachable past the dispatch it reports The two steps that actually deliver the notice carried no status function, so GitHub's implicit success() skipped them whenever anything earlier in the job had failed. `dispatch-validation.sh` exits non-zero when it cannot dispatch the card on any ref — which marks the job failed — so on the one path the dispatch-side fallback exists for, it was silent. This PR was written to close the implicit-success() trap at the job level and walked into its step-level twin. Both consumers now carry `always() && steps.notice.outcome == 'success'`: the status function so a failed dispatch still reaches them, and the outcome check so neither a crashed nor a skipped composer can hand feishu-notice.ts an empty body. The composer writes `alert` last, after every field a poster reads, so `alert=true` in an append-only $GITHUB_OUTPUT means the whole notice is on disk. Adds the step-level reachability rule as a spec beside the existing job-level one, and puts the dispatched prerelease lanes into that job-level sweep — release-prerelease-card.yml was outside it, so `fallback_notice`, whose `if` hand-checks `needs.card.result`, was uncovered by the rule it has to obey.
1782 lines
90 KiB
YAML
1782 lines
90 KiB
YAML
name: release-prerelease
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: "Optional git ref to build. Empty builds the dispatched ref."
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
release_version:
|
|
description: "Base version (x.y.z). Required when ref is not release/vX.Y.Z."
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
amr_profile:
|
|
description: "Optional AMR profile baked into the build (test or feature-test point the packaged app at that profile's Vela backend and enable the workspace-team transport). Empty leaves the default (prod)."
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
enable_mac_x64:
|
|
description: "Build and publish macOS x64 (Intel) artifacts. ON by default: stable ships Intel and stable is gated on validated prerelease artifacts, so an Intel-less prerelease cannot be promoted. The switch stays so a run that is explicitly not about Intel can buy back the ~12 min it adds to the critical path (~33 min on the macos-15-intel fleet against ~21 min everywhere else); turning it off costs that build its promotion eligibility."
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
enable_smoke:
|
|
description: "Run the packaged smoke suites (mac arm64, mac Intel, Windows). ON by default. Smoke no longer runs inside the build jobs and now costs this pipeline nothing but a dispatch: release-prerelease-smoke.yml picks it up after the packages are in R2 and re-downloads them from R2, so it exercises the artifact a user actually receives. Turn it off only to skip those runners entirely."
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
enable_tests:
|
|
description: "Dispatch the code test suites (P0 Functional E2E, E2E Vitest, daemon shards, verify) into release-prerelease-tests.yml. ON by default. They run against this build's commit in a workflow of their own, gate nothing here, and cost this pipeline only the seconds the dispatch takes."
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
mac_sign_mode:
|
|
description: "macOS signing depth for both mac lanes. 'sign-only' (default) codesigns with the Apple certificate but skips the Apple notary round-trip, which is minutes of waiting on Apple that no prerelease consumer needs. 'notarize' also notarizes and staples — use it for the last prerelease before a stable promotion, since stable ships notarized. 'no' skips signing entirely and will FAIL the stable promotion gate, which requires platforms.mac.signed == true."
|
|
required: true
|
|
type: choice
|
|
options:
|
|
- "no"
|
|
- "sign-only"
|
|
- "notarize"
|
|
default: "sign-only"
|
|
win_x64_smoke_mode:
|
|
description: "Windows x64 smoke coverage, applied only when enable_smoke is on. enable_smoke decides WHETHER packaged smoke runs; this input decides HOW DEEP the Windows one goes."
|
|
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:
|
|
ref:
|
|
description: "Optional git ref to build. Empty builds the caller's ref."
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
release_version:
|
|
description: "Base version (x.y.z). Required when ref is not release/vX.Y.Z."
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
amr_profile:
|
|
description: "Optional AMR profile baked into the build (test or feature-test enable the workspace-team transport by pointing at that profile's Vela backend). Empty leaves the default (prod)."
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
enable_mac_x64:
|
|
description: "Build and publish macOS x64 (Intel) artifacts. ON by default: stable ships Intel and stable is gated on validated prerelease artifacts, so an Intel-less prerelease cannot be promoted. The switch stays so a run that is explicitly not about Intel can buy back the ~12 min it adds to the critical path; turning it off costs that build its promotion eligibility."
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
enable_smoke:
|
|
description: "Run the packaged smoke suites (mac arm64, mac Intel, Windows). ON by default. Smoke no longer runs inside the build jobs: release-prerelease-smoke.yml picks it up after the packages are in R2 and re-downloads them from R2, so it exercises the artifact a user actually receives."
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
enable_tests:
|
|
description: "Dispatch the code test suites (P0 Functional E2E, E2E Vitest, daemon shards, verify) into release-prerelease-tests.yml. ON by default. They run against this build's commit in a workflow of their own and gate nothing here."
|
|
required: false
|
|
type: boolean
|
|
default: true
|
|
mac_sign_mode:
|
|
description: "macOS signing depth for both mac lanes: no / sign-only / notarize. Defaults to sign-only — codesigned but not sent to Apple's notary service. Use notarize for the last prerelease before a stable promotion. 'no' fails the stable promotion gate, which requires platforms.mac.signed == true."
|
|
required: false
|
|
type: string
|
|
default: "sign-only"
|
|
win_x64_smoke_mode:
|
|
description: "Windows x64 smoke coverage, applied only when enable_smoke is on: 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 prerelease version that was built."
|
|
value: ${{ jobs.metadata.outputs.release_version }}
|
|
channel:
|
|
description: "The prerelease 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 }}
|
|
previous_commit:
|
|
description: "Commit SHA of the previously published prerelease; empty on cold start."
|
|
value: ${{ jobs.metadata.outputs.previous_commit }}
|
|
# --- Advisory signals ------------------------------------------------
|
|
# Nothing below gates publication.
|
|
#
|
|
# The code tests and the packaged mac/Windows smoke no longer run in this
|
|
# workflow at all: they are dispatched into release-prerelease-tests.yml
|
|
# and release-prerelease-smoke.yml, which have concurrency groups of their
|
|
# own. That is the whole point — a validation job that outlives `publish`
|
|
# used to hold this workflow open, and this workflow's concurrency group
|
|
# is a single repository-wide string, so the next prerelease could not
|
|
# even START until the last one's tests finished. Their results reach a
|
|
# human through the progressive Feishu card (release-prerelease-card.yml),
|
|
# which watches all three runs, not through outputs here.
|
|
#
|
|
# Linux keeps its in-job smoke because the whole Linux lane is opt-in
|
|
# behind vars.ENABLE_STABLE_LINUX and is not on anyone's critical path.
|
|
linux_x64_smoke_result:
|
|
description: "Linux AppImage smoke outcome. Advisory; `skipped` unless vars.ENABLE_STABLE_LINUX is on."
|
|
value: ${{ jobs.build_linux.outputs.smoke_result }}
|
|
version_metadata_url:
|
|
description: "Public R2 URL of this build's version metadata.json. This is the authoritative \"did a package actually ship?\" signal — it is non-empty only when the publish job wrote metadata. Do NOT read the overall workflow conclusion for that: it now goes red whenever an advisory test job fails, even though the package shipped."
|
|
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 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."
|
|
value: ${{ jobs.publish.outputs.linux_url }}
|
|
permissions:
|
|
actions: write
|
|
contents: read
|
|
concurrency:
|
|
group: open-design-release-prerelease
|
|
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 }}
|
|
# Workspace Team gate for packaged prerelease builds. tools-pack reads
|
|
# OPEN_DESIGN_AMR_PROFILE and bakes OD_VELA_WEB_URL into open-design-config.json;
|
|
# both are required for the packaged daemon to enable the workspace-team
|
|
# transport (see apps/packaged/src/workspace-team.ts).
|
|
#
|
|
# Every profile now resolves an origin, production included: prod's Vela
|
|
# backend serves the feature (WORKSPACE_TEAM_ENABLED plus a configured
|
|
# resource-hub blob store), so a prod build ships the transports enabled.
|
|
# An empty amr_profile still yields no origin, which the packaged gate treats
|
|
# as dormant — a build that forgets the profile degrades to local-only rather
|
|
# than pointing at an unknown backend.
|
|
#
|
|
# Origins stay in repository secrets rather than literals: the non-prod AMR
|
|
# environments are internal deployments and this repository is public.
|
|
# Secrets read here: VELA_WEB_URL_FEATURE_TEST, VELA_WEB_URL_TEST,
|
|
# VELA_WEB_URL_PROD.
|
|
OPEN_DESIGN_AMR_PROFILE: ${{ inputs.amr_profile }}
|
|
OD_VELA_WEB_URL: ${{ inputs.amr_profile == 'feature-test' && secrets.VELA_WEB_URL_FEATURE_TEST || inputs.amr_profile == 'test' && secrets.VELA_WEB_URL_TEST || inputs.amr_profile == 'prod' && secrets.VELA_WEB_URL_PROD || '' }}
|
|
OD_VELA_WEB_URL_FEATURE_TEST: ${{ secrets.VELA_WEB_URL_FEATURE_TEST }}
|
|
OD_VELA_WEB_URL_TEST: ${{ secrets.VELA_WEB_URL_TEST }}
|
|
OD_VELA_WEB_URL_PROD: ${{ secrets.VELA_WEB_URL_PROD }}
|
|
jobs:
|
|
metadata:
|
|
name: Prepare prerelease metadata
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
OPEN_DESIGN_PRERELEASE_METADATA_URL: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}/prerelease/latest/metadata.json
|
|
OPEN_DESIGN_RELEASE_CHANNEL: prerelease
|
|
OPEN_DESIGN_STABLE_VERSION: ${{ inputs.release_version }}
|
|
outputs:
|
|
base_version: ${{ steps.prerelease.outputs.base_version }}
|
|
branch: ${{ steps.prerelease.outputs.branch }}
|
|
channel: ${{ steps.prerelease.outputs.channel }}
|
|
commit: ${{ steps.prerelease.outputs.commit }}
|
|
github_release_enabled: ${{ steps.prerelease.outputs.github_release_enabled }}
|
|
latest_stable: ${{ steps.prerelease.outputs.latest_stable }}
|
|
previous_commit: ${{ steps.prev.outputs.previous_commit }}
|
|
prerelease_number: ${{ steps.prerelease.outputs.prerelease_number }}
|
|
prerelease_version: ${{ steps.prerelease.outputs.prerelease_version }}
|
|
release_number: ${{ steps.prerelease.outputs.release_number }}
|
|
release_name: ${{ steps.prerelease.outputs.release_name }}
|
|
release_version: ${{ steps.prerelease.outputs.release_version }}
|
|
state_source: ${{ steps.prerelease.outputs.state_source }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
ref: ${{ inputs.ref != '' && inputs.ref || github.ref }}
|
|
# Shallow. Nothing in this workflow reads git history: the built
|
|
# commit arrives as an explicit input/env (RELEASE_COMMIT), `pnpm
|
|
# guard` uses `git ls-files` (index, not history), and tools-pack /
|
|
# tools-release never shell out to git. A full clone of this
|
|
# repository is a 2.2 GiB pack and cost 44-102s per job.
|
|
fetch-depth: 1
|
|
|
|
- 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: Resolve built commit
|
|
run: echo "BUILT_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
|
|
|
- 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:-<none>}"
|
|
echo "previous_commit=$prev" >> "$GITHUB_OUTPUT"
|
|
- name: Prepare prerelease release metadata
|
|
id: prerelease
|
|
env:
|
|
GITHUB_SHA: ${{ env.BUILT_SHA }}
|
|
GITHUB_REF_NAME: ${{ inputs.ref != '' && inputs.ref || github.ref_name }}
|
|
run: pnpm exec tools-release prepare prerelease
|
|
- name: Validate R2 prerelease access
|
|
env:
|
|
GITHUB_SHA: ${{ env.BUILT_SHA }}
|
|
R2_ACCESS_PROBE_NAME: release-prerelease
|
|
RELEASE_CHANNEL: prerelease
|
|
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
|
|
dispatch_validation:
|
|
name: Dispatch prerelease validation
|
|
# Fire-and-forget, and deliberately absent from every other job's `needs`.
|
|
#
|
|
# This workflow holds ONE repository-wide concurrency group
|
|
# (`open-design-release-prerelease`, cancel-in-progress: false), so any job
|
|
# that outlives `publish` keeps that group held and stops the NEXT
|
|
# prerelease from STARTING — not from publishing, from starting. The test
|
|
# suites were doing exactly that. Handing them to workflows of their own
|
|
# costs this pipeline the seconds it takes to POST a dispatch.
|
|
#
|
|
# A dispatch failure is loud (this job goes red) and harmless: nothing
|
|
# depends on this job, so `publish` cannot see it.
|
|
#
|
|
# Red is not, however, a channel anyone watches. The progressive card is the
|
|
# ONLY prerelease notification left, so a card that was never dispatched is
|
|
# a release the team never hears about. The last steps below are the ping
|
|
# for that, and they live in this job rather than a job of their own
|
|
# precisely because nothing may depend on the dispatchers.
|
|
needs: metadata
|
|
if: ${{ needs.metadata.result == 'success' }}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
# Read only by the last step, which alerts when the card was not
|
|
# dispatched. Deliberately the OTHER bot: the card posts through the
|
|
# Feishu application bot below, so an application credential that expired
|
|
# (or was emptied, which silently skips the dispatch step) must not take
|
|
# the alert down with it.
|
|
FEISHU_SIGN_SECRET: ${{ secrets.FEISHU_RELEASE_SIGN_SECRET }}
|
|
FEISHU_WEBHOOK: ${{ secrets.FEISHU_RELEASE_WEBHOOK }}
|
|
GH_REPO: ${{ github.repository }}
|
|
PRIMARY_REF: ${{ needs.metadata.outputs.branch }}
|
|
FALLBACK_REF: ${{ github.event.repository.default_branch || 'main' }}
|
|
# `secrets` is not a context an `if:` may read, so the card dispatch
|
|
# checks for its own credentials through env instead. A fork, or a
|
|
# repository without the Feishu app configured, simply skips the card.
|
|
FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }}
|
|
FEISHU_RELEASE_CHAT_ID: ${{ secrets.FEISHU_RELEASE_CHAT_ID }}
|
|
steps:
|
|
- name: Checkout dispatch helper
|
|
id: checkout
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
# Deliberately the ref this workflow was loaded from, NOT the built
|
|
# commit. The helper is infrastructure that ships with this file, so
|
|
# the two are always in step; pinning it to the built commit would
|
|
# break the moment a release branch carried an older .github tree.
|
|
fetch-depth: 1
|
|
- name: Dispatch prerelease code tests
|
|
id: tests_dispatch
|
|
if: ${{ inputs.enable_tests }}
|
|
env:
|
|
COMMIT: ${{ needs.metadata.outputs.commit }}
|
|
VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
run: |
|
|
bash .github/scripts/release/dispatch-validation.sh release-prerelease-tests.yml \
|
|
-f "commit=$COMMIT" \
|
|
-f "version=$VERSION" \
|
|
-f "origin_run_id=$GITHUB_RUN_ID"
|
|
- name: Dispatch progressive Feishu card
|
|
id: card_dispatch
|
|
if: ${{ env.FEISHU_APP_ID != '' && env.FEISHU_RELEASE_CHAT_ID != '' }}
|
|
env:
|
|
BRANCH: ${{ needs.metadata.outputs.branch }}
|
|
COMMIT: ${{ needs.metadata.outputs.commit }}
|
|
EXPECT_SMOKE: ${{ inputs.enable_smoke }}
|
|
EXPECT_TESTS: ${{ inputs.enable_tests }}
|
|
PREVIOUS_COMMIT: ${{ needs.metadata.outputs.previous_commit }}
|
|
VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
run: |
|
|
bash .github/scripts/release/dispatch-validation.sh release-prerelease-card.yml \
|
|
-f "version=$VERSION" \
|
|
-f "commit=$COMMIT" \
|
|
-f "branch=$BRANCH" \
|
|
-f "previous_commit=$PREVIOUS_COMMIT" \
|
|
-f "origin_run_id=$GITHUB_RUN_ID" \
|
|
-f "expect_tests=$EXPECT_TESTS" \
|
|
-f "expect_smoke=$EXPECT_SMOKE"
|
|
# Only reached when the dispatch above actually succeeded — the helper
|
|
# exits non-zero on every ref it could not dispatch on, and `run:`
|
|
# blocks stop at the first failure. Empty therefore covers every way
|
|
# the card fails to start: the helper could not dispatch on any ref,
|
|
# an earlier step in this job died, or this step was skipped because
|
|
# the application bot's credentials are missing.
|
|
echo "dispatched=true" >> "$GITHUB_OUTPUT"
|
|
|
|
# ---- Fallback when the card never started --------------------------
|
|
# `always()` so a failed dispatch above still reaches these; gated on the
|
|
# checkout because both scripts come from the working tree. They run on
|
|
# every prerelease and stay silent on the healthy ones — the composer
|
|
# refuses to emit `alert=true` when the card was dispatched, which is the
|
|
# same rule this repository cannot afford to get wrong: a fallback that
|
|
# also posts on good days is just a second card.
|
|
#
|
|
# This ping is necessarily early. It fires beside the build, before any
|
|
# package exists, so it reports where the version metadata WILL appear
|
|
# instead of claiming a publication verdict it cannot have yet. Once the
|
|
# card workflow IS running, release-prerelease-card.yml's own
|
|
# `fallback_notice` job owns the rest of the lane — the two are mutually
|
|
# exclusive, so a release can never draw both.
|
|
- name: Setup Node.js for the fallback notifier
|
|
if: >-
|
|
${{
|
|
always() && github.repository == 'nexu-io/open-design' &&
|
|
steps.checkout.outcome == 'success'
|
|
}}
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
- name: Compose the fallback notice
|
|
id: notice
|
|
if: >-
|
|
${{
|
|
always() && github.repository == 'nexu-io/open-design' &&
|
|
steps.checkout.outcome == 'success'
|
|
}}
|
|
env:
|
|
BRANCH: ${{ needs.metadata.outputs.branch }}
|
|
CARD_DISPATCHED: ${{ steps.card_dispatch.outputs.dispatched }}
|
|
CHANNEL_LABEL: Prerelease
|
|
COMMIT: ${{ needs.metadata.outputs.commit }}
|
|
LANE_SUMMARY: "card dispatch=${{ steps.card_dispatch.outcome }} · tests dispatch=${{ steps.tests_dispatch.outcome }}"
|
|
ORIGIN_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
# This job runs beside the build, so the packages are still coming.
|
|
# A probe would be worse than useless here: a job cannot ask the API
|
|
# whether its own run has finished.
|
|
PIPELINE_PROGRESS: running
|
|
RELEASE_CHANNEL: prerelease
|
|
RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}
|
|
STAGE: dispatch
|
|
VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
run: node --experimental-strip-types tools/release/src/notifications/prerelease-fallback-notice.ts
|
|
- name: Fail loudly when the fallback bot is itself unconfigured
|
|
# Both notification paths down at once is an incident of its own, and a
|
|
# red job is the only channel left to say so. Only reachable on this
|
|
# repository: the composer above is scoped to it, so a fork — which has
|
|
# neither bot — leaves `alert` empty and never goes red for this.
|
|
# `always()` because the whole point is to speak after something
|
|
# broke — without it GitHub's implicit success() skips this step in
|
|
# every run where an earlier step already failed. The outcome check
|
|
# is the other half: never act on a composer that did not finish.
|
|
if: >-
|
|
${{
|
|
always() && steps.notice.outcome == 'success' &&
|
|
steps.notice.outputs.alert == 'true' && env.FEISHU_WEBHOOK == ''
|
|
}}
|
|
run: |
|
|
echo "::error::the prerelease card was never dispatched and FEISHU_RELEASE_WEBHOOK is unset, so no fallback notice could be delivered"
|
|
exit 1
|
|
- name: Post the fallback notice
|
|
if: >-
|
|
${{
|
|
always() && steps.notice.outcome == 'success' &&
|
|
steps.notice.outputs.alert == 'true' && env.FEISHU_WEBHOOK != ''
|
|
}}
|
|
env:
|
|
NOTICE_BODY: ${{ steps.notice.outputs.body }}
|
|
NOTICE_TEMPLATE: ${{ steps.notice.outputs.template }}
|
|
NOTICE_TITLE: ${{ steps.notice.outputs.title }}
|
|
RUN_URL: ${{ steps.notice.outputs.run_url }}
|
|
run: node --experimental-strip-types tools/release/src/notifications/feishu-notice.ts
|
|
build_mac:
|
|
name: Build prerelease mac arm64
|
|
# Deliberately NOT gated on any test suite, and neither is `publish`. A
|
|
# prerelease exists so a person can install it and test it by hand;
|
|
# automated CI is a parallel opinion about the same commit, not a valve on
|
|
# delivery. Chaining the two meant a single flaky suite cost the team a
|
|
# whole day's package — the expensive failure mode, and the one this
|
|
# pipeline kept hitting. The suites still run on every prerelease, in
|
|
# release-prerelease-tests.yml, where they can take as long as they like
|
|
# without holding this workflow's concurrency group.
|
|
needs: metadata
|
|
runs-on: macos-14
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
ref: ${{ needs.metadata.outputs.commit }}
|
|
# Shallow — see the metadata job's checkout for why.
|
|
fetch-depth: 1
|
|
- 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: Compute mac_arm64 tools-pack cache key
|
|
id: mac_arm64_tools_pack_cache_key
|
|
env:
|
|
MAC_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-prerelease.yml', '.github/scripts/release/cache/mac.sh') }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "$MAC_TOOLS_PACK_ORIGIN_KEY" ]; then
|
|
echo "mac_arm64 tools-pack cache origin key is empty" >&2
|
|
exit 1
|
|
fi
|
|
prefix="tools-pack-mac-v1-prerelease-${RUNNER_OS}-arm64-"
|
|
{
|
|
echo "origin=$MAC_TOOLS_PACK_ORIGIN_KEY"
|
|
echo "prefix=$prefix"
|
|
echo "key=$prefix$MAC_TOOLS_PACK_ORIGIN_KEY"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Restore mac_arm64 tools-pack cache
|
|
id: mac_arm64_tools_pack_cache_restore
|
|
uses: actions/cache/restore@v5
|
|
continue-on-error: true
|
|
with:
|
|
path: ${{ runner.temp }}/tools-pack-cache
|
|
key: ${{ steps.mac_arm64_tools_pack_cache_key.outputs.key }}
|
|
restore-keys: |
|
|
${{ steps.mac_arm64_tools_pack_cache_key.outputs.prefix }}
|
|
|
|
- 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
|
|
if: ${{ inputs.mac_sign_mode != 'no' }}
|
|
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 prerelease mac artifacts
|
|
id: mac_arm64_tools_pack_build
|
|
continue-on-error: true
|
|
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
|
|
mkdir -p "$RUNNER_TEMP/release-build/mac_arm64"
|
|
rm -rf "$RUNNER_TEMP/tools-pack"
|
|
: > "$RUNNER_TEMP/release-build/mac_arm64/build.log"
|
|
build_args=(
|
|
exec tools-pack mac build
|
|
--dir "$RUNNER_TEMP/tools-pack"
|
|
--cache-dir "$RUNNER_TEMP/tools-pack-cache"
|
|
--namespace release-prerelease
|
|
--portable
|
|
--app-version "${{ needs.metadata.outputs.release_version }}"
|
|
--mac-compression normal
|
|
--to all
|
|
--json
|
|
)
|
|
# Signing depth is a dispatch input (default sign-only), mirroring
|
|
# release-beta.yml. Prerelease keeps --signed on every mode except an
|
|
# explicit 'no', because tools/release/src/metadata/prepare-stable.ts
|
|
# requires platforms.mac.signed == true (and platforms.macIntel.signed
|
|
# == true) in the prerelease metadata it promotes; an unsigned
|
|
# prerelease cannot become a stable release. Notarization is what the
|
|
# default drops: it is a round-trip to Apple's notary service that
|
|
# buys nothing for an internal channel, and metadata carries no
|
|
# notarized field for the stable gate to check.
|
|
case "${{ inputs.mac_sign_mode }}" in
|
|
no) ;;
|
|
sign-only) build_args+=(--signed) ;;
|
|
notarize) build_args+=(--signed --notarize) ;;
|
|
*) echo "unsupported mac_sign_mode: ${{ inputs.mac_sign_mode }}" >&2; exit 1 ;;
|
|
esac
|
|
if build_output="$(pnpm "${build_args[@]}" 2> >(tee -a "$RUNNER_TEMP/release-build/mac_arm64/build.log" >&2))"; then
|
|
printf '%s\n' "$build_output" | tee "$RUNNER_TEMP/release-build/mac_arm64/build.json"
|
|
else
|
|
build_status=$?
|
|
printf '%s\n' "$build_output"
|
|
exit "$build_status"
|
|
fi
|
|
- name: Retry prerelease mac_arm64 without restored cache
|
|
if: ${{ steps.mac_arm64_tools_pack_build.outcome == 'failure' }}
|
|
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
|
|
rm -rf "$RUNNER_TEMP/tools-pack-cache"
|
|
mkdir -p "$RUNNER_TEMP/release-build/mac_arm64"
|
|
pnpm exec tools-pack mac cleanup --dir "$RUNNER_TEMP/tools-pack" --namespace release-prerelease --json
|
|
: > "$RUNNER_TEMP/release-build/mac_arm64/build.log"
|
|
build_args=(
|
|
exec tools-pack mac build
|
|
--dir "$RUNNER_TEMP/tools-pack"
|
|
--cache-dir "$RUNNER_TEMP/tools-pack-cache"
|
|
--namespace release-prerelease
|
|
--portable
|
|
--app-version "${{ needs.metadata.outputs.release_version }}"
|
|
--mac-compression normal
|
|
--to all
|
|
--json
|
|
)
|
|
# Signing depth follows mac_sign_mode — see the primary mac arm64 build.
|
|
case "${{ inputs.mac_sign_mode }}" in
|
|
no) ;;
|
|
sign-only) build_args+=(--signed) ;;
|
|
notarize) build_args+=(--signed --notarize) ;;
|
|
*) echo "unsupported mac_sign_mode: ${{ inputs.mac_sign_mode }}" >&2; exit 1 ;;
|
|
esac
|
|
if build_output="$(pnpm "${build_args[@]}" 2> >(tee -a "$RUNNER_TEMP/release-build/mac_arm64/build.log" >&2))"; then
|
|
printf '%s\n' "$build_output" | tee "$RUNNER_TEMP/release-build/mac_arm64/build.json"
|
|
else
|
|
build_status=$?
|
|
printf '%s\n' "$build_output"
|
|
exit "$build_status"
|
|
fi
|
|
|
|
- name: Delete failed mac_arm64 tools-pack cache
|
|
if: ${{ steps.mac_arm64_tools_pack_build.outcome == 'failure' && steps.mac_arm64_tools_pack_cache_restore.outputs.cache-matched-key != '' }}
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
matched_key="${{ steps.mac_arm64_tools_pack_cache_restore.outputs.cache-matched-key }}"
|
|
cache_ids="$(gh cache list --key "$matched_key" --limit 100 --json id,key,ref --jq ".[] | select(.key == \"$matched_key\") | .id")"
|
|
count=0
|
|
while IFS= read -r cache_id; do
|
|
if [ -n "$cache_id" ]; then
|
|
gh cache delete "$cache_id"
|
|
count=$((count + 1))
|
|
fi
|
|
done <<< "$cache_ids"
|
|
echo "deletedFailedCacheKey=$matched_key count=$count"
|
|
|
|
- name: Verify prerelease mac_arm64 signature and notarization
|
|
if: ${{ inputs.mac_sign_mode != 'no' }}
|
|
run: |
|
|
set -euo pipefail
|
|
build_json="$RUNNER_TEMP/release-build/mac_arm64/build.json"
|
|
dmg_path="$(node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")).dmgPath; if (typeof value !== "string" || value.length === 0) process.exit(1); process.stdout.write(value);' "$build_json")"
|
|
if [ ! -f "$dmg_path" ]; then
|
|
echo "prerelease mac_arm64 DMG not found: $dmg_path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mount_point="$(mktemp -d "$RUNNER_TEMP/prerelease-mac-arm64.XXXXXX")"
|
|
cleanup() {
|
|
hdiutil detach "$mount_point" >/dev/null 2>&1 || true
|
|
rmdir "$mount_point" >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
hdiutil attach "$dmg_path" -nobrowse -readonly -mountpoint "$mount_point"
|
|
candidate_app="$mount_point/Open Design Prerelease.app"
|
|
if [ ! -d "$candidate_app" ]; then
|
|
echo "prerelease app not found in DMG: $candidate_app" >&2
|
|
exit 1
|
|
fi
|
|
# The signature is checked on every signed build, because
|
|
# platforms.mac[Intel].signed == true is what the stable promotion
|
|
# gate reads. The stapled-ticket and Gatekeeper assessments only pass
|
|
# for a notarized build, so they follow mac_sign_mode rather than
|
|
# failing the job for doing exactly what it was asked to do.
|
|
codesign --verify --deep --strict "$candidate_app"
|
|
if [ "${{ inputs.mac_sign_mode }}" = "notarize" ]; then
|
|
xcrun stapler validate "$candidate_app"
|
|
spctl --assess --type execute --verbose=4 "$candidate_app"
|
|
else
|
|
echo "mac_sign_mode=${{ inputs.mac_sign_mode }}: signature verified; skipping stapler/spctl, which require a notarization ticket this build did not request."
|
|
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)"
|
|
built_framework="$RUNNER_TEMP/tools-pack/out/mac/namespaces/release-prerelease/builder/mac-arm64/Open Design Prerelease.app/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-prerelease-mac-build-diagnostics
|
|
path: |
|
|
${{ runner.temp }}/release-build/mac_arm64/build.log
|
|
${{ runner.temp }}/release-build/mac_arm64/build.json
|
|
${{ runner.temp }}/mac-framework-diagnostics.txt
|
|
if-no-files-found: warn
|
|
- name: Write mac_arm64 release report
|
|
if: ${{ always() }}
|
|
env:
|
|
BUILD_JSON_PATH: ${{ runner.temp }}/release-build/mac_arm64/build.json
|
|
BUILD_LOG_PATH: ${{ runner.temp }}/release-build/mac_arm64/build.log
|
|
RELEASE_BUILD_TARGET: all
|
|
RELEASE_CHANNEL: prerelease
|
|
RELEASE_NAMESPACE: release-prerelease
|
|
RELEASE_REPORT_DIR: ${{ runner.temp }}/release-report/mac_arm64
|
|
RELEASE_REPORT_JSON_PATH: ${{ runner.temp }}/release-report/mac_arm64/report.json
|
|
RELEASE_REPORT_SUMMARY_PATH: ${{ runner.temp }}/release-report/mac_arm64/summary.md
|
|
# The build jobs no longer smoke; the packaged smoke lives in
|
|
# release-prerelease-smoke.yml and writes its own report there.
|
|
RELEASE_SMOKE_MODE: skip
|
|
RELEASE_TARGET: mac_arm64
|
|
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
REPORT_TITLE: mac_arm64 prerelease build
|
|
run: pnpm exec tools-release write-report
|
|
|
|
- name: Publish mac_arm64 report summary
|
|
if: ${{ always() }}
|
|
run: cat "${{ runner.temp }}/release-report/mac_arm64/summary.md" >> "$GITHUB_STEP_SUMMARY"
|
|
- name: Prune mac_arm64 tools-pack cache
|
|
continue-on-error: true
|
|
run: bash .github/scripts/release/cache/mac.sh
|
|
|
|
- name: Save mac_arm64 tools-pack cache
|
|
if: ${{ success() && (steps.mac_arm64_tools_pack_cache_restore.outputs.cache-hit != 'true' || steps.mac_arm64_tools_pack_build.outcome == 'failure') }}
|
|
uses: actions/cache/save@v5
|
|
continue-on-error: true
|
|
with:
|
|
path: ${{ runner.temp }}/tools-pack-cache
|
|
key: ${{ steps.mac_arm64_tools_pack_cache_key.outputs.key }}
|
|
|
|
- name: Retain recent mac_arm64 tools-pack caches
|
|
if: ${{ success() }}
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
prefix="${{ steps.mac_arm64_tools_pack_cache_key.outputs.prefix }}"
|
|
keep=1
|
|
cache_ids="$(gh cache list --key "$prefix" --sort created_at --order desc --limit 100 --json id,key,createdAt --jq ".[$keep:] | .[].id")"
|
|
count=0
|
|
while IFS= read -r cache_id; do
|
|
if [ -n "$cache_id" ]; then
|
|
gh cache delete "$cache_id"
|
|
count=$((count + 1))
|
|
fi
|
|
done <<< "$cache_ids"
|
|
echo "actionsCachePrefix=$prefix deleted=$count"
|
|
|
|
- name: Prepare mac prerelease assets
|
|
env:
|
|
RELEASE_ARTIFACT_MODE: all
|
|
RELEASE_ASSET_SUFFIX: ""
|
|
RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets/mac_arm64
|
|
RELEASE_CHANNEL: prerelease
|
|
RELEASE_NAMESPACE: release-prerelease
|
|
RELEASE_NOTES: Open Design Prerelease ${{ 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 prerelease platform
|
|
env:
|
|
RELEASE_ARTIFACT_MODE: all
|
|
RELEASE_ASSET_SUFFIX: ""
|
|
RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets/mac_arm64
|
|
RELEASE_CHANNEL: prerelease
|
|
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_arm64
|
|
RELEASE_REPORT_ZIP_PATH: ${{ runner.temp }}/release-report/mac_arm64-report.zip
|
|
# platforms.mac[Intel].signed in the published metadata comes from
|
|
# this value via the platform manifest, and the stable promotion gate
|
|
# reads it. It must describe what was actually done, not a constant.
|
|
RELEASE_SIGNED: ${{ inputs.mac_sign_mode != 'no' && 'true' || '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: mac_arm64
|
|
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
run: pnpm exec tools-release publish-platform
|
|
- name: Upload mac prerelease publish manifest
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: open-design-prerelease-mac-arm64-publish-manifest
|
|
path: ${{ runner.temp }}/release-platform-manifests/mac_arm64.json
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
# The packaged smoke used to run here, after the publish manifest. It now
|
|
# runs in release-prerelease-smoke.yml against the DMG re-downloaded from
|
|
# R2 — closer to what a user installs, and off this workflow's
|
|
# concurrency group entirely.
|
|
build_mac_intel:
|
|
name: Build prerelease mac intel x64
|
|
# Tests never gate delivery on this channel — see build_mac.
|
|
needs: metadata
|
|
# On by default, and switchable. Intel is this pipeline's critical path: it
|
|
# runs on the slower macos-15-intel fleet and takes ~33 min against ~21 min
|
|
# for every other platform. It still builds on every run, because the stable
|
|
# promotion gate reads platforms.macIntel out of the prerelease metadata and
|
|
# an Intel-less prerelease simply cannot be promoted — see the
|
|
# enable_mac_x64 input description and AGENTS.md "Release channel model".
|
|
if: ${{ inputs.enable_mac_x64 }}
|
|
runs-on: macos-15-intel
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
ref: ${{ needs.metadata.outputs.commit }}
|
|
# Shallow — see the metadata job's checkout for why.
|
|
fetch-depth: 1
|
|
- 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: Compute mac_x64 tools-pack cache key
|
|
id: mac_x64_tools_pack_cache_key
|
|
env:
|
|
MAC_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-prerelease.yml', '.github/scripts/release/cache/mac.sh') }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "$MAC_TOOLS_PACK_ORIGIN_KEY" ]; then
|
|
echo "mac_x64 tools-pack cache origin key is empty" >&2
|
|
exit 1
|
|
fi
|
|
prefix="tools-pack-mac-v1-prerelease-${RUNNER_OS}-x64-"
|
|
{
|
|
echo "origin=$MAC_TOOLS_PACK_ORIGIN_KEY"
|
|
echo "prefix=$prefix"
|
|
echo "key=$prefix$MAC_TOOLS_PACK_ORIGIN_KEY"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Restore mac_x64 tools-pack cache
|
|
id: mac_x64_tools_pack_cache_restore
|
|
uses: actions/cache/restore@v5
|
|
continue-on-error: true
|
|
with:
|
|
path: ${{ runner.temp }}/tools-pack-cache
|
|
key: ${{ steps.mac_x64_tools_pack_cache_key.outputs.key }}
|
|
restore-keys: |
|
|
${{ steps.mac_x64_tools_pack_cache_key.outputs.prefix }}
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- name: Prepare Apple signing certificate
|
|
if: ${{ inputs.mac_sign_mode != 'no' }}
|
|
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 prerelease mac intel artifacts
|
|
id: mac_x64_tools_pack_build
|
|
continue-on-error: true
|
|
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
|
|
mkdir -p "$RUNNER_TEMP/release-build/mac_x64"
|
|
rm -rf "$RUNNER_TEMP/tools-pack"
|
|
: > "$RUNNER_TEMP/release-build/mac_x64/build.log"
|
|
build_args=(
|
|
exec tools-pack mac build
|
|
--dir "$RUNNER_TEMP/tools-pack"
|
|
--cache-dir "$RUNNER_TEMP/tools-pack-cache"
|
|
--namespace release-prerelease-intel
|
|
--portable
|
|
--app-version "${{ needs.metadata.outputs.release_version }}"
|
|
--mac-compression normal
|
|
--to all
|
|
--json
|
|
)
|
|
# Signing depth follows mac_sign_mode — see the primary mac arm64 build.
|
|
case "${{ inputs.mac_sign_mode }}" in
|
|
no) ;;
|
|
sign-only) build_args+=(--signed) ;;
|
|
notarize) build_args+=(--signed --notarize) ;;
|
|
*) echo "unsupported mac_sign_mode: ${{ inputs.mac_sign_mode }}" >&2; exit 1 ;;
|
|
esac
|
|
if build_output="$(pnpm "${build_args[@]}" 2> >(tee -a "$RUNNER_TEMP/release-build/mac_x64/build.log" >&2))"; then
|
|
printf '%s\n' "$build_output" | tee "$RUNNER_TEMP/release-build/mac_x64/build.json"
|
|
else
|
|
build_status=$?
|
|
printf '%s\n' "$build_output"
|
|
exit "$build_status"
|
|
fi
|
|
|
|
- name: Retry prerelease mac_x64 without restored cache
|
|
if: ${{ steps.mac_x64_tools_pack_build.outcome == 'failure' }}
|
|
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
|
|
rm -rf "$RUNNER_TEMP/tools-pack-cache"
|
|
mkdir -p "$RUNNER_TEMP/release-build/mac_x64"
|
|
pnpm exec tools-pack mac cleanup --dir "$RUNNER_TEMP/tools-pack" --namespace release-prerelease-intel --json
|
|
: > "$RUNNER_TEMP/release-build/mac_x64/build.log"
|
|
build_args=(
|
|
exec tools-pack mac build
|
|
--dir "$RUNNER_TEMP/tools-pack"
|
|
--cache-dir "$RUNNER_TEMP/tools-pack-cache"
|
|
--namespace release-prerelease-intel
|
|
--portable
|
|
--app-version "${{ needs.metadata.outputs.release_version }}"
|
|
--mac-compression normal
|
|
--to all
|
|
--json
|
|
)
|
|
# Signing depth follows mac_sign_mode — see the primary mac arm64 build.
|
|
case "${{ inputs.mac_sign_mode }}" in
|
|
no) ;;
|
|
sign-only) build_args+=(--signed) ;;
|
|
notarize) build_args+=(--signed --notarize) ;;
|
|
*) echo "unsupported mac_sign_mode: ${{ inputs.mac_sign_mode }}" >&2; exit 1 ;;
|
|
esac
|
|
if build_output="$(pnpm "${build_args[@]}" 2> >(tee -a "$RUNNER_TEMP/release-build/mac_x64/build.log" >&2))"; then
|
|
printf '%s\n' "$build_output" | tee "$RUNNER_TEMP/release-build/mac_x64/build.json"
|
|
else
|
|
build_status=$?
|
|
printf '%s\n' "$build_output"
|
|
exit "$build_status"
|
|
fi
|
|
|
|
- name: Delete failed mac_x64 tools-pack cache
|
|
if: ${{ steps.mac_x64_tools_pack_build.outcome == 'failure' && steps.mac_x64_tools_pack_cache_restore.outputs.cache-matched-key != '' }}
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
matched_key="${{ steps.mac_x64_tools_pack_cache_restore.outputs.cache-matched-key }}"
|
|
cache_ids="$(gh cache list --key "$matched_key" --limit 100 --json id,key,ref --jq ".[] | select(.key == \"$matched_key\") | .id")"
|
|
count=0
|
|
while IFS= read -r cache_id; do
|
|
if [ -n "$cache_id" ]; then
|
|
gh cache delete "$cache_id"
|
|
count=$((count + 1))
|
|
fi
|
|
done <<< "$cache_ids"
|
|
echo "deletedFailedCacheKey=$matched_key count=$count"
|
|
|
|
- name: Verify prerelease mac_x64 signature and notarization
|
|
if: ${{ inputs.mac_sign_mode != 'no' }}
|
|
run: |
|
|
set -euo pipefail
|
|
build_json="$RUNNER_TEMP/release-build/mac_x64/build.json"
|
|
dmg_path="$(node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")).dmgPath; if (typeof value !== "string" || value.length === 0) process.exit(1); process.stdout.write(value);' "$build_json")"
|
|
if [ ! -f "$dmg_path" ]; then
|
|
echo "prerelease mac_x64 DMG not found: $dmg_path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mount_point="$(mktemp -d "$RUNNER_TEMP/prerelease-mac-x64.XXXXXX")"
|
|
cleanup() {
|
|
hdiutil detach "$mount_point" >/dev/null 2>&1 || true
|
|
rmdir "$mount_point" >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
hdiutil attach "$dmg_path" -nobrowse -readonly -mountpoint "$mount_point"
|
|
candidate_app="$mount_point/Open Design Prerelease.app"
|
|
if [ ! -d "$candidate_app" ]; then
|
|
echo "prerelease app not found in DMG: $candidate_app" >&2
|
|
exit 1
|
|
fi
|
|
# The signature is checked on every signed build, because
|
|
# platforms.mac[Intel].signed == true is what the stable promotion
|
|
# gate reads. The stapled-ticket and Gatekeeper assessments only pass
|
|
# for a notarized build, so they follow mac_sign_mode rather than
|
|
# failing the job for doing exactly what it was asked to do.
|
|
codesign --verify --deep --strict "$candidate_app"
|
|
if [ "${{ inputs.mac_sign_mode }}" = "notarize" ]; then
|
|
xcrun stapler validate "$candidate_app"
|
|
spctl --assess --type execute --verbose=4 "$candidate_app"
|
|
else
|
|
echo "mac_sign_mode=${{ inputs.mac_sign_mode }}: signature verified; skipping stapler/spctl, which require a notarization ticket this build did not request."
|
|
fi
|
|
|
|
- name: Write mac_x64 release report
|
|
if: ${{ always() }}
|
|
env:
|
|
BUILD_JSON_PATH: ${{ runner.temp }}/release-build/mac_x64/build.json
|
|
BUILD_LOG_PATH: ${{ runner.temp }}/release-build/mac_x64/build.log
|
|
RELEASE_BUILD_TARGET: all
|
|
RELEASE_CHANNEL: prerelease
|
|
RELEASE_NAMESPACE: release-prerelease-intel
|
|
RELEASE_REPORT_DIR: ${{ runner.temp }}/release-report/mac_x64
|
|
RELEASE_REPORT_JSON_PATH: ${{ runner.temp }}/release-report/mac_x64/report.json
|
|
RELEASE_REPORT_SUMMARY_PATH: ${{ runner.temp }}/release-report/mac_x64/summary.md
|
|
# The build jobs no longer smoke; the packaged smoke lives in
|
|
# release-prerelease-smoke.yml and writes its own report there.
|
|
RELEASE_SMOKE_MODE: skip
|
|
RELEASE_TARGET: mac_x64
|
|
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
REPORT_TITLE: mac_x64 prerelease build
|
|
run: pnpm exec tools-release write-report
|
|
|
|
- name: Publish mac_x64 report summary
|
|
if: ${{ always() }}
|
|
run: cat "${{ runner.temp }}/release-report/mac_x64/summary.md" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Prune mac_x64 tools-pack cache
|
|
continue-on-error: true
|
|
run: bash .github/scripts/release/cache/mac.sh
|
|
|
|
- name: Save mac_x64 tools-pack cache
|
|
if: ${{ success() && (steps.mac_x64_tools_pack_cache_restore.outputs.cache-hit != 'true' || steps.mac_x64_tools_pack_build.outcome == 'failure') }}
|
|
uses: actions/cache/save@v5
|
|
continue-on-error: true
|
|
with:
|
|
path: ${{ runner.temp }}/tools-pack-cache
|
|
key: ${{ steps.mac_x64_tools_pack_cache_key.outputs.key }}
|
|
|
|
- name: Retain recent mac_x64 tools-pack caches
|
|
if: ${{ success() }}
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
prefix="${{ steps.mac_x64_tools_pack_cache_key.outputs.prefix }}"
|
|
keep=1
|
|
cache_ids="$(gh cache list --key "$prefix" --sort created_at --order desc --limit 100 --json id,key,createdAt --jq ".[$keep:] | .[].id")"
|
|
count=0
|
|
while IFS= read -r cache_id; do
|
|
if [ -n "$cache_id" ]; then
|
|
gh cache delete "$cache_id"
|
|
count=$((count + 1))
|
|
fi
|
|
done <<< "$cache_ids"
|
|
echo "actionsCachePrefix=$prefix deleted=$count"
|
|
|
|
- name: Prepare mac intel prerelease assets
|
|
env:
|
|
RELEASE_ARTIFACT_MODE: all
|
|
RELEASE_ASSET_SUFFIX: ""
|
|
RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets/mac_x64
|
|
RELEASE_CHANNEL: prerelease
|
|
RELEASE_NAMESPACE: release-prerelease-intel
|
|
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 prerelease platform
|
|
env:
|
|
RELEASE_ARTIFACT_MODE: all
|
|
RELEASE_ASSET_SUFFIX: ""
|
|
RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets/mac_x64
|
|
RELEASE_CHANNEL: prerelease
|
|
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_REPORT_DIR: ${{ runner.temp }}/release-report/mac_x64
|
|
RELEASE_REPORT_ZIP_PATH: ${{ runner.temp }}/release-report/mac_x64-report.zip
|
|
# platforms.mac[Intel].signed in the published metadata comes from
|
|
# this value via the platform manifest, and the stable promotion gate
|
|
# reads it. It must describe what was actually done, not a constant.
|
|
RELEASE_SIGNED: ${{ inputs.mac_sign_mode != 'no' && 'true' || '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: mac_x64
|
|
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
run: pnpm exec tools-release publish-platform
|
|
- name: Upload mac intel prerelease publish manifest
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: open-design-prerelease-mac-x64-publish-manifest
|
|
path: ${{ runner.temp }}/release-platform-manifests/mac_x64.json
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
# Packaged smoke moved to release-prerelease-smoke.yml — see build_mac.
|
|
build_win:
|
|
name: Build prerelease win x64
|
|
# Tests never gate delivery on this channel — see build_mac.
|
|
needs: metadata
|
|
runs-on: windows-latest
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
ref: ${{ needs.metadata.outputs.commit }}
|
|
# Shallow — see the metadata job's checkout for why.
|
|
fetch-depth: 1
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v5
|
|
with:
|
|
version: 10.33.2
|
|
# No `cache: pnpm`. It caches the pnpm STORE, and on this repository that
|
|
# store never paid for itself on Windows. Measured across four consecutive
|
|
# prerelease runs: on a miss (3 of 4) it cost 100s / 66s / 43s in the
|
|
# "Post Setup Node.js" save with no install speedup, and on the one hit it
|
|
# traded a 32s restore for a 36s faster install — a wash. Worse, it
|
|
# competes for the repository's 10 GB Actions cache budget against the
|
|
# per-lane tools-pack caches, which are worth ~11 min of build time each.
|
|
# This is now the only release build job without it, which is exactly the
|
|
# shape every other build job (mac arm64, mac Intel, linux) already had.
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
- 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-prerelease.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-v1-prerelease-$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: |
|
|
# windows-latest no longer guarantees a preinstalled NSIS, and the
|
|
# Chocolatey community feed flakes: prerelease.4 hit a silent
|
|
# "Unable to find package 'nsis'" fetch failure that left this step
|
|
# green (pwsh -command does not propagate choco's exit code) and let
|
|
# the build die on makensis half an hour later. Retry the install
|
|
# and hard-fail HERE, with a clear message, if makensis still cannot
|
|
# be found — a red Setup NSIS step is diagnosable; a mid-build
|
|
# makensis error is not.
|
|
function Test-Makensis {
|
|
[bool]((Get-Command makensis.exe -ErrorAction SilentlyContinue) -or (Test-Path "C:\Program Files (x86)\NSIS\makensis.exe"))
|
|
}
|
|
if (Test-Makensis) { Write-Host "makensis already present"; exit 0 }
|
|
foreach ($attempt in 1..4) {
|
|
Write-Host "choco install nsis (attempt $attempt)"
|
|
choco install nsis -y --no-progress
|
|
if (Test-Makensis) { break }
|
|
Start-Sleep -Seconds (15 * $attempt)
|
|
}
|
|
if (-not (Test-Makensis)) {
|
|
Write-Error "NSIS install failed after 4 attempts: makensis absent from PATH and C:\Program Files (x86)\NSIS"
|
|
exit 1
|
|
}
|
|
Write-Host "NSIS ready"
|
|
- name: Build prerelease 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 "release-build\win_x64\build.json"
|
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $buildJsonPath) | Out-Null
|
|
pnpm.cmd exec tools-pack win cleanup --dir $toolsPackDir --namespace release-prerelease-win --json
|
|
$buildArgs = @(
|
|
"exec", "tools-pack", "win", "build",
|
|
"--dir", $toolsPackDir,
|
|
"--cache-dir", $cacheDir,
|
|
"--namespace", "release-prerelease-win",
|
|
"--portable",
|
|
"--app-version", "${{ needs.metadata.outputs.release_version }}",
|
|
"--to", "all",
|
|
"--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 release-prerelease-win --json
|
|
$buildOutput = pnpm exec tools-pack win build `
|
|
--dir $toolsPackDir `
|
|
--cache-dir $cacheDir `
|
|
--namespace release-prerelease-win `
|
|
--portable `
|
|
--app-version "${{ needs.metadata.outputs.release_version }}" `
|
|
--to all `
|
|
--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 release-prerelease-win --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: Write win_x64 release report
|
|
if: ${{ always() }}
|
|
env:
|
|
BUILD_JSON_PATH: ${{ runner.temp }}\release-build\win_x64\build.json
|
|
RELEASE_BUILD_TARGET: all
|
|
RELEASE_CHANNEL: prerelease
|
|
RELEASE_NAMESPACE: release-prerelease-win
|
|
RELEASE_REPORT_DIR: ${{ runner.temp }}\release-report\win_x64
|
|
RELEASE_REPORT_JSON_PATH: ${{ runner.temp }}\release-report\win_x64\report.json
|
|
RELEASE_REPORT_SUMMARY_PATH: ${{ runner.temp }}\release-report\win_x64\summary.md
|
|
# See the mac lanes: packaged smoke moved out of this job.
|
|
RELEASE_SMOKE_MODE: skip
|
|
RELEASE_TARGET: win_x64
|
|
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
REPORT_TITLE: win_x64 prerelease build
|
|
run: pnpm exec tools-release write-report
|
|
|
|
- name: Publish win_x64 report summary
|
|
if: ${{ always() }}
|
|
shell: pwsh
|
|
run: Get-Content "${{ runner.temp }}\release-report\win_x64\summary.md" | Add-Content $env:GITHUB_STEP_SUMMARY
|
|
- 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 = 1
|
|
$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 prerelease assets
|
|
shell: pwsh
|
|
run: |
|
|
tools\release\scripts\prepare-platform-assets.ps1 `
|
|
-ReleaseTarget win_x64 `
|
|
-ReleaseAssetsDir "${{ runner.temp }}\release-assets\win_x64" `
|
|
-BuildJsonPath "${{ runner.temp }}\release-build\win_x64\build.json" `
|
|
-ReleaseNamespace release-prerelease-win `
|
|
-ReleaseVersion "${{ needs.metadata.outputs.release_version }}" `
|
|
-ReleaseAssetSuffix "" `
|
|
-ReleaseChannel prerelease `
|
|
-ReleasePublicOrigin "${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}" `
|
|
-ReleaseNotes "Open Design Prerelease ${{ needs.metadata.outputs.release_version }}" `
|
|
-IncludeZip $true
|
|
- name: Publish windows prerelease platform
|
|
env:
|
|
RELEASE_ASSET_SUFFIX: ""
|
|
RELEASE_ASSETS_DIR: ${{ runner.temp }}\release-assets\win_x64
|
|
RELEASE_CHANNEL: prerelease
|
|
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_x64
|
|
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 prerelease publish manifest
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: open-design-prerelease-win-x64-publish-manifest
|
|
path: ${{ runner.temp }}\release-platform-manifests\win_x64.json
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
# The Windows update fixture and the packaged Windows smoke both moved
|
|
# to release-prerelease-smoke.yml, which rebuilds the fixture there when
|
|
# `full` coverage is requested and installs the setup.exe re-downloaded
|
|
# from R2 — see build_mac.
|
|
build_linux:
|
|
name: Build prerelease linux x64
|
|
# Tests never gate delivery on this channel — see build_mac.
|
|
needs: metadata
|
|
if: ${{ vars.ENABLE_STABLE_LINUX == 'true' }}
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
smoke_result: ${{ steps.linux_smoke.outcome }}
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
ref: ${{ needs.metadata.outputs.commit }}
|
|
# Shallow — see the metadata job's checkout for why.
|
|
fetch-depth: 1
|
|
- 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: Build prerelease linux artifacts
|
|
run: |
|
|
set -euo pipefail
|
|
report_dir="$RUNNER_TEMP/release-report/linux_x64"
|
|
mkdir -p "$report_dir"
|
|
: > "$report_dir/tools-pack.log"
|
|
build_args=(
|
|
exec tools-pack linux build
|
|
--dir "$RUNNER_TEMP/tools-pack"
|
|
--namespace release-prerelease-linux
|
|
--portable
|
|
--app-version "${{ needs.metadata.outputs.release_version }}"
|
|
--to appimage
|
|
--containerized
|
|
--json
|
|
)
|
|
if build_output="$(pnpm "${build_args[@]}" 2> >(tee -a "$report_dir/tools-pack.log" >&2))"; then
|
|
printf '%s\n' "$build_output" | tee "$report_dir/tools-pack.json"
|
|
node -e 'const fs = require("node:fs"); JSON.parse(fs.readFileSync(process.argv[1], "utf8"));' "$report_dir/tools-pack.json"
|
|
else
|
|
build_status=$?
|
|
printf '%s\n' "$build_output" | tee "$report_dir/tools-pack.json"
|
|
exit "$build_status"
|
|
fi
|
|
- name: Smoke prerelease linux AppImage runtime
|
|
id: linux_smoke
|
|
continue-on-error: true
|
|
working-directory: e2e
|
|
env:
|
|
OD_PACKAGED_E2E_LINUX_APPIMAGE: "1"
|
|
OD_PACKAGED_E2E_NAMESPACE: release-prerelease-linux
|
|
OD_PACKAGED_E2E_SCREENSHOT_PATH: ${{ runner.temp }}/release-report/linux_x64/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_x64"
|
|
mkdir -p "$report_dir/screenshots"
|
|
cat > "$report_dir/manifest.json" <<EOF
|
|
{
|
|
"channel": "prerelease",
|
|
"platform": "linux",
|
|
"releaseVersion": "${{ needs.metadata.outputs.release_version }}",
|
|
"spec": "specs/linux.spec.ts",
|
|
"namespace": "release-prerelease-linux",
|
|
"screenshot": "screenshots/open-design-linux-smoke.png",
|
|
"githubRunId": "$GITHUB_RUN_ID",
|
|
"githubRunAttempt": "$GITHUB_RUN_ATTEMPT",
|
|
"commit": "${{ needs.metadata.outputs.commit }}"
|
|
}
|
|
EOF
|
|
sudo apt-get update 2>&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-prerelease-linux-e2e-report
|
|
path: ${{ runner.temp }}/release-report/linux_x64
|
|
if-no-files-found: warn
|
|
- name: Prepare linux prerelease assets
|
|
env:
|
|
RELEASE_ASSET_SUFFIX: ""
|
|
RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets/linux_x64
|
|
RELEASE_CHANNEL: prerelease
|
|
RELEASE_NAMESPACE: release-prerelease-linux
|
|
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 prerelease platform
|
|
env:
|
|
RELEASE_ASSET_SUFFIX: ""
|
|
RELEASE_ASSETS_DIR: ${{ runner.temp }}/release-assets/linux_x64
|
|
RELEASE_CHANNEL: prerelease
|
|
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_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 prerelease publish manifest
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: open-design-prerelease-linux-x64-publish-manifest
|
|
path: ${{ runner.temp }}/release-platform-manifests/linux_x64.json
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
publish:
|
|
name: Publish prerelease release
|
|
# No test suite appears in `needs` or `if`, and none may be added.
|
|
# prerelease is the internal channel: its job is to put a package in a
|
|
# colleague's hands so a human can test it, and machine CI is a second,
|
|
# parallel signal about the same commit — not a valve on the first one. A
|
|
# flaky suite used to mean nobody got a build that day, which is a worse
|
|
# outcome than shipping an internal package alongside a red test. The
|
|
# suites now live in release-prerelease-tests.yml and the packaged smoke in
|
|
# release-prerelease-smoke.yml; the progressive Feishu card is what carries
|
|
# their verdict to a human. Stable is unaffected: its promotion gate reads
|
|
# the prerelease metadata through tools/release/src/metadata/prepare-stable.ts
|
|
# and runs its own checks.
|
|
needs:
|
|
- metadata
|
|
- build_mac
|
|
- build_mac_intel
|
|
- build_win
|
|
- build_linux
|
|
if: >-
|
|
${{
|
|
always() &&
|
|
!cancelled() &&
|
|
needs.metadata.result == 'success' &&
|
|
needs.build_mac.result == 'success' &&
|
|
(needs.build_mac_intel.result == 'success' || needs.build_mac_intel.result == 'skipped') &&
|
|
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 }}
|
|
RELEASE_COMMIT: ${{ needs.metadata.outputs.commit }}
|
|
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_WIN: "true"
|
|
GITHUB_RELEASE_ENABLED: "false"
|
|
RELEASE_CHANNEL: prerelease
|
|
RELEASE_NAME: ${{ needs.metadata.outputs.release_name }}
|
|
RELEASE_SIGNED: ${{ inputs.mac_sign_mode != 'no' && 'true' || 'false' }}
|
|
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
REPORT_MODE: zip
|
|
STATE_SOURCE: ${{ needs.metadata.outputs.state_source }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
ref: ${{ needs.metadata.outputs.commit }}
|
|
# Shallow — see the metadata job's checkout for why.
|
|
fetch-depth: 1
|
|
# The artifact downloads below run on the runner's own Node, and the pnpm
|
|
# toolchain this job actually uses is set up further down, right before
|
|
# `pnpm install`. The Setup Node.js that used to sit here was a second,
|
|
# redundant copy of that one — it paid for its own post-job step and
|
|
# bought nothing.
|
|
- name: Download mac prerelease publish manifest
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: open-design-prerelease-mac-arm64-publish-manifest
|
|
path: ${{ runner.temp }}/release-platform-manifests
|
|
- name: Download mac intel prerelease publish manifest
|
|
if: ${{ needs.build_mac_intel.result == 'success' }}
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: open-design-prerelease-mac-x64-publish-manifest
|
|
path: ${{ runner.temp }}/release-platform-manifests
|
|
- name: Download windows prerelease publish manifest
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: open-design-prerelease-win-x64-publish-manifest
|
|
path: ${{ runner.temp }}/release-platform-manifests
|
|
- name: Download linux prerelease publish manifest
|
|
if: ${{ needs.build_linux.result == 'success' }}
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: open-design-prerelease-linux-x64-publish-manifest
|
|
path: ${{ runner.temp }}/release-platform-manifests
|
|
|
|
- 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 and publish prerelease release notes
|
|
env:
|
|
RELEASE_CHANNEL: prerelease
|
|
RELEASE_NOTE_MANIFEST_PATH: ${{ runner.temp }}/release-metadata/release-note-publication.json
|
|
RELEASE_NOTE_PLAN_PATH: ${{ runner.temp }}/release-metadata/release-note-plan.json
|
|
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 }}
|
|
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
run: |
|
|
pnpm exec tools-release prepare-release-note
|
|
pnpm exec tools-release publish-release-note
|
|
pnpm exec tools-release verify-release-note
|
|
|
|
- name: Publish prerelease metadata
|
|
env:
|
|
BASE_VERSION: ${{ needs.metadata.outputs.base_version }}
|
|
ENABLE_LINUX_X64: ${{ needs.build_linux.result == 'success' }}
|
|
ENABLE_MAC_ARM64: "true"
|
|
ENABLE_MAC_X64: ${{ needs.build_mac_intel.result == 'success' }}
|
|
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_CHANNEL: prerelease
|
|
RELEASE_LAUNCHER_VERSION_MIN_PRERELEASE: ${{ vars.RELEASE_LAUNCHER_VERSION_MIN_PRERELEASE }}
|
|
RELEASE_LAUNCHER_VERSION_MIN_STABLE: ${{ vars.RELEASE_LAUNCHER_VERSION_MIN_STABLE }}
|
|
RELEASE_LAUNCHER_VERSION_MIN_URL_PRERELEASE: ${{ vars.RELEASE_LAUNCHER_VERSION_MIN_URL_PRERELEASE }}
|
|
RELEASE_LAUNCHER_VERSION_MIN_URL_STABLE: ${{ vars.RELEASE_LAUNCHER_VERSION_MIN_URL_STABLE }}
|
|
RELEASE_MANIFEST_DIR: ${{ runner.temp }}/release-platform-manifests
|
|
RELEASE_METADATA_DIR: ${{ runner.temp }}/release-metadata
|
|
RELEASE_NOTE_MANIFEST_PATH: ${{ runner.temp }}/release-metadata/release-note-publication.json
|
|
RELEASE_OUTPUTS_PATH: ${{ runner.temp }}/release-metadata/outputs.json
|
|
RELEASE_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}
|
|
RELEASE_SIGNED: ${{ inputs.mac_sign_mode != 'no' && 'true' || '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_VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
STATE_SOURCE: ${{ needs.metadata.outputs.state_source }}
|
|
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 prerelease metadata
|
|
env:
|
|
ENABLE_LINUX_X64: ${{ needs.build_linux.result == 'success' }}
|
|
ENABLE_MAC_ARM64: "true"
|
|
ENABLE_MAC_X64: ${{ needs.build_mac_intel.result == 'success' }}
|
|
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_CHANNEL: prerelease
|
|
RELEASE_LAUNCHER_VERSION_MIN_PRERELEASE: ${{ vars.RELEASE_LAUNCHER_VERSION_MIN_PRERELEASE }}
|
|
RELEASE_LAUNCHER_VERSION_MIN_STABLE: ${{ vars.RELEASE_LAUNCHER_VERSION_MIN_STABLE }}
|
|
RELEASE_LAUNCHER_VERSION_MIN_URL_PRERELEASE: ${{ vars.RELEASE_LAUNCHER_VERSION_MIN_URL_PRERELEASE }}
|
|
RELEASE_LAUNCHER_VERSION_MIN_URL_STABLE: ${{ vars.RELEASE_LAUNCHER_VERSION_MIN_URL_STABLE }}
|
|
RELEASE_METADATA_URL: ${{ steps.outputs.outputs.version_metadata_url }}
|
|
RELEASE_NOTE_MANIFEST_PATH: ${{ runner.temp }}/release-metadata/release-note-publication.json
|
|
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
WIN_X64_RESULT: ${{ needs.build_win.result }}
|
|
run: pnpm exec tools-release verify-metadata
|
|
- name: Write prerelease metadata summary
|
|
env:
|
|
RELEASE_CHANNEL: prerelease
|
|
RELEASE_METADATA_URL: ${{ steps.outputs.outputs.version_metadata_url }}
|
|
RELEASE_SUMMARY_PATH: ${{ runner.temp }}/release-metadata/summary.md
|
|
run: pnpm exec tools-release summary-metadata
|
|
- name: Publish summary
|
|
run: 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
|
|
dispatch_smoke:
|
|
name: Dispatch packaged smoke
|
|
# Packaged smoke needs a package, so unlike dispatch_validation this one
|
|
# waits for `publish` — but it only waits long enough to POST the dispatch.
|
|
# The smoke itself downloads the artifacts back out of R2 in
|
|
# release-prerelease-smoke.yml, which is both closer to what a user installs
|
|
# and completely off this workflow's concurrency group.
|
|
needs:
|
|
- metadata
|
|
- publish
|
|
# `always() && !cancelled()` is load-bearing, not decoration.
|
|
#
|
|
# `build_linux` is skipped on every run (the repository has no
|
|
# ENABLE_STABLE_LINUX variable), and GitHub applies a skip "to all jobs in
|
|
# the dependency chain from the point of failure or skip onwards" unless a
|
|
# job uses a conditional expression that causes it to continue. That break
|
|
# is per job and is NOT inherited: `publish` carries the same prefix and so
|
|
# runs, but its own dependents are still downstream of build_linux's skip.
|
|
# Without a break of its own this job never evaluates the conditions below
|
|
# at all — GitHub's implicit `success()` skips it first.
|
|
#
|
|
# That is how release-prerelease-smoke.yml went 0-for-N. In run
|
|
# 34149795952 publish succeeded, version_metadata_url was published, and
|
|
# enable_smoke arrived as true; the job was still skipped with zero steps.
|
|
# The explicit publish gate below is what actually decides whether smoke is
|
|
# dispatched, and it is unchanged.
|
|
if: >-
|
|
${{
|
|
always() && !cancelled() &&
|
|
inputs.enable_smoke &&
|
|
needs.publish.result == 'success' &&
|
|
needs.publish.outputs.version_metadata_url != ''
|
|
}}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
GH_REPO: ${{ github.repository }}
|
|
PRIMARY_REF: ${{ needs.metadata.outputs.branch }}
|
|
FALLBACK_REF: ${{ github.event.repository.default_branch || 'main' }}
|
|
steps:
|
|
- name: Checkout dispatch helper
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
# Deliberately the ref this workflow was loaded from, NOT the built
|
|
# commit. The helper is infrastructure that ships with this file, so
|
|
# the two are always in step; pinning it to the built commit would
|
|
# break the moment a release branch carried an older .github tree.
|
|
fetch-depth: 1
|
|
- name: Dispatch packaged smoke
|
|
env:
|
|
COMMIT: ${{ needs.metadata.outputs.commit }}
|
|
VERSION: ${{ needs.metadata.outputs.release_version }}
|
|
VERSION_METADATA_URL: ${{ needs.publish.outputs.version_metadata_url }}
|
|
WIN_X64_SMOKE_MODE: ${{ inputs.win_x64_smoke_mode }}
|
|
WIN_X64_UPDATE_METADATA_URL: ${{ inputs.win_x64_update_metadata_url }}
|
|
WIN_X64_UPDATE_TARGET_VERSION: ${{ inputs.win_x64_update_target_version }}
|
|
run: |
|
|
bash .github/scripts/release/dispatch-validation.sh release-prerelease-smoke.yml \
|
|
-f "commit=$COMMIT" \
|
|
-f "version=$VERSION" \
|
|
-f "origin_run_id=$GITHUB_RUN_ID" \
|
|
-f "version_metadata_url=$VERSION_METADATA_URL" \
|
|
-f "win_x64_smoke_mode=$WIN_X64_SMOKE_MODE" \
|
|
-f "win_x64_update_metadata_url=$WIN_X64_UPDATE_METADATA_URL" \
|
|
-f "win_x64_update_target_version=$WIN_X64_UPDATE_TARGET_VERSION"
|
|
cleanup_partial_release_assets:
|
|
name: Cleanup unpublished prerelease asset artifacts
|
|
needs:
|
|
- metadata
|
|
- build_mac
|
|
- build_mac_intel
|
|
- build_win
|
|
- build_linux
|
|
- publish
|
|
if: ${{ always() && needs.publish.result != 'success' }}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
ref: ${{ needs.metadata.outputs.commit || inputs.ref || github.ref }}
|
|
|
|
- name: Delete unpublished prerelease asset artifacts
|
|
env:
|
|
ARTIFACT_CLEANUP_DESCRIPTION: intermediate prerelease 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
|