Files
nexu-io__open-design/.github/workflows/release-prerelease-tests.yml
lefarcen a07dc736fd perf(ci): take prerelease validation off the release pipeline, and report it on one live card (#7842)
* perf(ci): make prerelease mac Intel opt-in

build_mac_intel is the prerelease critical path: ~33 min on the
macos-15-intel fleet against ~21 min for mac arm64 and Windows, which
stretches every release-branch push to ~56 min end to end.

Gate it behind a new `enable_mac_x64` input that defaults to false, the
same shape build_linux already uses, and let publish tolerate a skipped
Intel job. Push-triggered prereleases now finish in ~21 min; a manual
dispatch can still turn Intel back on.

The stable promotion gate is unchanged and still hard-requires
platforms.macIntel in the promoted prerelease metadata, so an Intel-less
prerelease fails promotion loudly rather than shipping unvalidated.

* perf(ci): stop the prerelease pipeline waiting on work it does not need

Run 34102146769 took 56m, 42m of which survived turning macOS Intel off.
Four things in that 42m were wall clock nobody was buying anything with.

Tests no longer sit in front of packaging. functional_e2e, e2e_vitest,
daemon_unit_tests and verify take ~11 min; packaging takes ~21 min, and the
build jobs needed all four before they could start. They now need only
metadata and run alongside the tests. Nothing about the safety of a release
changes: `publish` is what writes metadata.json, and it already required all
four gates to be `success` — a red suite still cannot ship. What it costs is
CI minutes, since a failing gate now stops builds that have already run.

The Feishu card stops cloning the repository to read one `git log`. That
checkout was `fetch-depth: 0` against a 2.2 GiB pack and took 4m09s to
produce a `PREV..CUR` commit list. The list now comes from the compare API,
reproducing `git log --no-merges --pretty=format:'%s (%h)'` line for line —
verified against 35 commits of real history, where the only difference was a
leading space that feishu.ts trims anyway. Every fallback the old script had
(no baseline, unreachable commit) still produces an empty changelog rather
than a failed notification.

Packaged smoke and Apple notarization become opt-in, like Intel already is.
Smoke is advisory — `continue-on-error`, so it has never blocked publication
— and costs ~2.5 min on the Windows critical path. Notarization is a
round-trip to Apple that no prerelease consumer needs. Signing is NOT
optional and stays on: prepare-stable.ts requires platforms.mac.signed ==
true, so an unsigned prerelease could never be promoted. `mac_sign_mode: no`
is offered for parity with beta and now correctly reports signed=false
rather than lying in the metadata.

Windows drops setup-node's pnpm store cache and every job checks out
shallow. Measured across four prerelease runs the store cache missed 3 times
out of 4, costing 100s/66s/43s to save with no install speedup, and on its
one hit traded a 32s restore for a 36s faster install — while competing for
the 10 GB cache budget against tools-pack caches worth ~11 min each.
Nothing in this workflow reads git history.

* perf(ci): let a prerelease ship on the build's schedule, not the tests'

prerelease is the internal channel. Its job is to put a package in a
colleague's hands so a human can test it; automated CI is a second, parallel
opinion about the same commit. Wiring the two in series meant a single flaky
suite cost the team a whole day's package — the expensive failure, and the
one this pipeline kept producing.

The four test jobs (functional_e2e, e2e_vitest, daemon_unit_tests, verify)
now gate nothing. They are absent from `build_*` and from `publish`. They
still run on every prerelease and their results leave the workflow as
outputs — functional_e2e_result and friends, plus a tests_all_green /
failed_test_jobs rollup — so the notification layer can raise the alarm
without holding the package hostage. Stable is untouched: its promotion gate
reads the prerelease metadata and runs its own checks.

The packaged smoke suites move for the same reason. They were already
`continue-on-error`, so a smoke failure never failed the job — but they sat
between the build and the R2 upload, which meant the package waited on them
and a hung smoke could still eat the job. Every smoke now runs AFTER
"Publish <platform> platform" and "Upload <platform> publish manifest", and
every step after that manifest upload is continue-on-error, so nothing
post-delivery can fail a build job and retroactively block publish.

The results travel through a `test_signals` job rather than the
`jobs.<job_id>.result` expression GitHub documents for workflow_call outputs,
because actionlint has rejected that property since 2023
(rhysd/actionlint#343) and ci.yml runs actionlint at a pinned version.

One consequence to hand to the notification work: a failed advisory job now
makes the overall workflow conclusion red even though the package shipped, so
`needs.build.result` is no longer the right thing to colour a card by.
`version_metadata_url` is the authoritative "did a package ship?" signal and
is documented as such.

* perf(ci): move prerelease validation out of the pipeline it validates

release-prerelease.yml holds one repository-wide concurrency group with
cancel-in-progress: false, so any job that outlives `publish` keeps that
group held and stops the NEXT prerelease from starting. Making the test
jobs advisory did not fix that: they still held the group for 45 minutes
after the package had shipped.

The pipeline is now metadata -> build_* -> publish plus two dispatcher
jobs that nothing depends on. The four code suites, the packaged
mac/Windows smoke, and the release card each run in a workflow of their
own, keyed on the origin run.

The packaged smoke no longer installs electron-builder's output
directory on the machine that produced it. It reads the published
version metadata, downloads the DMG / setup.exe out of R2, verifies its
sha256 sidecar, and writes it to the single path
`tools-pack <platform> install` reads - which is why the specs need no
changes. That is both a better test and what lets the smoke live on a
different runner at all.

The release card becomes progressive. A Feishu application bot can PATCH
a message it sent, so one card is created when the first platform lands
in R2 and edited in place as the rest of the platforms, the code tests,
and the smoke report in. A PATCH replaces the whole card, so exactly one
job may write it: the watcher owns the message and polls the three runs.
The card is withheld until a package exists, because a "building..."
card posted at dispatch time becomes a permanent lie when a build dies -
with total build failure as the one exception, where silence would hide
an incident.

macOS Intel goes back to on-by-default: stable ships Intel and stable's
gate is a validated prerelease artifact, so an Intel-less prerelease
cannot be promoted at all. The switch stays, forwarded as
`github.event_name != 'workflow_dispatch' || inputs.enable_mac_x64` -
`inputs.enable_mac_x64 || true` would be unturnoffable, because a
dispatch with the box unchecked also yields true.

Also fixes the signal batch 2 left wrong: a reusable workflow reports
failure if any job inside it failed, including work that runs after
publication, so `needs.build.result` painted the download card red while
its own buttons worked. `version_metadata_url` is non-empty only when
publish wrote metadata, and now drives both the header colour and the
smoke annotations.

Two topology tests were already red on this branch - batch 2 changed the
workflow without updating them. Both now assert the new invariant.

The existing webhook card stays until the new one has carried a couple
of real releases; a gap in release notification is worse than a
duplicate.
2026-09-07 17:38:33 +00:00

155 lines
5.7 KiB
YAML

name: release-prerelease-tests
# The code test suites for one prerelease build, run OUTSIDE the pipeline that
# produced it.
#
# These four jobs used to live in release-prerelease.yml. They never gated
# delivery there — build and publish deliberately ignore them — but they still
# held the workflow open, and release-prerelease.yml's concurrency group is one
# repository-wide string with cancel-in-progress: false. So a 45-minute test
# suite did not delay one prerelease, it delayed the NEXT one from starting.
# Moving them here removes that coupling entirely: this workflow has a
# concurrency group scoped to the origin run, so two prereleases can be
# validated at once and neither can queue a build.
#
# Nothing dispatches this on a schedule or a push. release-prerelease.yml's
# `dispatch_validation` job POSTs it the moment the build commit is resolved,
# via .github/scripts/release/dispatch-validation.sh. Results reach a human
# through the progressive Feishu card (release-prerelease-card.yml), which
# watches this run's jobs; they are not workflow outputs, because nothing in the
# release path is allowed to consume them as a gate.
on:
workflow_dispatch:
inputs:
commit:
description: "Exact commit SHA to test. This is the commit release-prerelease actually built, not a branch head."
required: true
type: string
version:
description: "Prerelease version these suites are validating, e.g. 0.21.1-prerelease.3. Display only."
required: false
type: string
default: ""
origin_run_id:
description: "Run id of the release-prerelease pipeline that dispatched this. Carried in the run name so the Feishu card and a human can walk between the two."
required: false
type: string
default: ""
run-name: prerelease tests · ${{ inputs.version || inputs.commit }} · origin-run ${{ inputs.origin_run_id }}
permissions:
contents: read
concurrency:
# Scoped to the origin run, never repository-wide: this workflow must not be
# able to queue behind — or ahead of — anything. Two prereleases in flight get
# two independent test runs.
group: release-prerelease-tests-${{ inputs.origin_run_id || inputs.commit }}
cancel-in-progress: false
jobs:
functional_e2e:
name: P0 Functional E2E
uses: ./.github/workflows/ui-extended-main.yml
with:
ref: ${{ inputs.commit }}
suite: p0
e2e_vitest:
name: E2E Vitest
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
ref: ${{ inputs.commit }}
- 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: Setup Playwright
run: pnpm -C e2e exec playwright install --with-deps chromium
# Ubuntu 24.04 restricts unprivileged user namespaces through AppArmor.
# The daemon launches the installed Chromium directly instead of through
# Playwright. Restore user namespaces on this ephemeral runner so the real
# Chromium sandbox remains enabled rather than disabling that protection.
- name: Allow Chromium user namespaces on GitHub Ubuntu
shell: bash
run: |
set -euo pipefail
if sysctl kernel.apparmor_restrict_unprivileged_userns >/dev/null 2>&1; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- name: Prebuild workspace type declarations
run: |
pnpm --filter @open-design/daemon build
pnpm --filter @open-design/desktop build
pnpm --filter @open-design/web build:sidecar
- name: Run E2E Vitest
run: pnpm --filter @open-design/e2e test
daemon_unit_tests:
name: Daemon tests (${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
ref: ${{ inputs.commit }}
- 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: Run daemon test shard
env:
OD_WATCHER_TEST_DEBUG: "1"
run: pnpm --filter @open-design/daemon test --shard=${{ matrix.shard }}/4
verify:
name: Verify build (typecheck + tests)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
ref: ${{ inputs.commit }}
# Shallow: nothing here reads git history. `pnpm guard` uses
# `git ls-files` (the index, not history), and a full clone of this
# repository is a 2.2 GiB pack.
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 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