Files
ever-gauzy/.github/workflows/build.yml
T
Ruslan KonviserandClaude Opus 5.5 271b43e24e ci(desktop): fail win/linux releases whose dist package.json lacks the per-arch update channel
Gauzy Server's Windows and Linux release jobs published channel-less
update manifests (latest.yml, latest-linux*.yml) from v107 to v111.44.45.
Its :isolated scripts ran `pack --arch` after the build had already copied
apps/server/src/package.json into dist. electron-builder reads
dist/apps/<x>/package.json, never saw build.publish[].channel and fell
back to `latest`. The updater only requests `latest-${process.arch}`, so
installs stopped auto-updating, and nothing caught it for 4.5 months.
#10299 fixed that app. This change makes the whole class fail loudly.

Add .scripts/electron-package-utils/assert-publish-channel.js, a Node
script with no dependencies. It takes --project <dist dir> --arch
<x64|arm64> and requires every build.publish[] entry in
<dist dir>/package.json to have channel latest-<arch>. Otherwise it prints
a GitHub `::error` annotation naming the file and the expected and actual
channels, plus a hint, and exits 1. A missing file, unreadable JSON or bad
arguments also exit 1.

Run it immediately before electron-builder in the 28 root scripts that
publish for --windows/--linux and stamp the channel with `pack --arch`.
The list was found from the scripts, not typed by hand. Each guard's
--project and --arch are asserted equal to that script's electron-builder
--project, its --x64/--arm64 flag and its pack --arch. A bad channel now
stops the job before anything is published. No other script changes.
The 24 win/linux publishing scripts without `pack --arch` have no callers
in any workflow and are left alone.

Add assert-publish-channel.test.js (node --test, no dependencies) as
`yarn test:publish-channel` and run it in build.yml next to
test:postinstall. It covers the pass, fail and misuse cases. It also
checks that every per-arch win/linux publishing script keeps the guard on
the same arch and dist dir, so a new or edited release script cannot drop
it unnoticed. That check fails against the current develop package.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 00:42:07 +02:00

532 lines
30 KiB
YAML

name: Build
# Replaces the CircleCI `build` workflow, which could not complete on that plan: a cold
# `yarn install` for this monorepo takes ~46 minutes and saving the node_modules cache another ~9,
# which exceeds the 60-minute job cap — so the cache was never written and every run started cold.
# Self-hosted runners have no such cap.
#
# LATENCY IS A CORRECTNESS PROPERTY OF THIS GATE. The 2026-08-13 template-error incident was not
# a coverage gap — build-web caught the error — but its verdict landed 74 minutes AFTER the PR
# had been merged on a still-pending check (~3h19m push-to-verdict). Two structural causes fixed
# here:
# 1. `build-libs` is its own job: `build:package:all` is where every LIBRARY's strict template
# check runs (each lib's tsconfig sets strictTemplates — the app tsconfig is lax in every
# configuration), so the verdict that catches template errors now reports as its own named
# check ~install+~20min after push instead of at the end of the longest job. Treat a
# pending/cancelled `build-libs`/`build-web` as a red: NEVER merge on yellow — the PR
# concurrency group cancels superseded runs, so "no failure" often just means "never ran".
# 2. The `needs: build-monorepo-root` edges are BACK, because that job now produces something the
# others consume. They were removed when it produced nothing and the edge was pure serial
# latency; five jobs then each ran their own `yarn install` of the same tree CONCURRENTLY and
# fought each other for I/O. Measured on run 32358690732, same commit: build-api installed in
# 1h49m and passed, build-libs and build-web took 2h40m and were KILLED at the 180-minute
# ceiling with their build steps never started. build-monorepo-root now installs once and
# publishes the tree; the other four restore it in minutes. On the warm path (~most PRs) they
# report EARLIER than before, because the install in front of them is a cache lookup.
# They carry `if: !cancelled()` so a dead producer still yields a real red — a `needs:` edge
# alone would SKIP them, and a skipped required check does not block a merge, which is the same
# "never ran" hazard as (1).
# The dev-config "Build packages" pre-step was removed from build-api/build-web: the app builds
# rebuild their dependency libraries themselves (Nx `dependsOn: ^build`, production config), and
# the strict library verdict lives in `build-libs`.
on:
pull_request:
push:
branches:
- develop
- stage
- master
# Unique to this workflow and ref. Deliberately NOT a group shared with other workflows: a shared
# group serializes unrelated builds and silently cancels them (see the image-build starvation
# incident). Within this workflow+ref, only the newest commit is built.
concurrency:
# Keyed on the HEAD REF, not on `github.ref`, so the two events that fire for one commit land in
# the same group. `pull_request:` above is unfiltered, so while a release-cascade PR is open whose
# head is a build branch (today: #10257, `stage` -> `stage-apps`), a single push to `stage` starts
# BOTH a push run on `refs/heads/stage` AND a pull_request run on `refs/pull/10257/merge`. Under
# `github.ref` those are different strings, so neither cancelled the other and the fleet compiled
# the same tree twice — two 3-hour builds per push, for as long as the cascade PR stays open.
#
# The trade-off, stated plainly: the pull_request run builds the MERGE commit and the push run
# builds the branch head. For a fast-forward cascade those are the same tree and the second build
# is pure waste; if the base has diverged they differ, and collapsing them means only the newer
# event's view is compiled. That is acceptable here because no branch declares a required status
# check, so nothing is gated on the verdict that loses — but it IS a real narrowing, not a free win.
#
# Qualified by the head REPOSITORY as well as the ref. Fork pull requests are skipped at the job
# level, but a skipped job still creates a run that claims the group first — so without the repo
# in the key, a fork branch named `develop` could cancel a genuine `develop` build and then skip.
group: >-
build-${{ github.workflow }}-${{ github.event_name == 'pull_request'
&& format('{0}@{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.head.ref)
|| format('{0}@{1}', github.repository, github.ref_name) }}
# Was `github.event_name == 'pull_request'`, which left branch pushes uncancelled:
# four merges to develop in an hour meant four full 3-h compiles of commits that were
# already superseded. This job only compiles — nothing outside the run depends on a
# half-finished one — so the newest commit always wins.
cancel-in-progress: true
env:
# Mirrors the CircleCI `defaults` block. The `ng:*` scripts already set the heap themselves via
# cross-env, so this covers everything outside them.
NODE_OPTIONS: --max-old-space-size=12288
NG_CLI_ANALYTICS: false
# Nx Cloud is disabled for this org; without this `nx run-many` hard-fails with
# "Nx Cloud: Workspace is unable to be authorized. Exiting run."
NX_NO_CLOUD: true
# The dependency tree travels between jobs as this one compressed file. Workspace-relative so the
# same string works for `tar` in a run step and for actions/cache.
NODE_MODULES_ARCHIVE: node-modules.tar.zst
# Least-privilege scope for the automatic GITHUB_TOKEN.
# This workflow only builds/tests/deploys from a checkout — read access is sufficient.
permissions:
contents: read
jobs:
build-monorepo-root:
name: build-monorepo-root
# Moved off the 4-core pool: this is no longer one of five equals, it is the SERIAL CRITICAL PATH
# for the whole gate, and both halves of its work (yarn install, zstd -T0) scale with cores.
# Never run jobs for a pull request from a fork (owner decision 2026-09-16); branch PRs and pushes still run.
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ${{ vars.RUNNER_LINUX_X64_8 || 'ubuntu-latest' }}
# 360, not 180. This job now carries the install for the ENTIRE gate, alone, and a cold install
# here has no yarn tarball cache behind it: test_playwright.yml's equivalent deps job measured the
# same work at 88 min, 3h20m and 3h46m. At 180 a cold miss would time out, and because the four
# build jobs `needs:` this one, that single timeout would take the whole gate with it. The ceiling
# costs nothing on the warm path, which is a lookup and an exit.
timeout-minutes: 360
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Test native rebuild setup
run: yarn test:postinstall
- name: Test release update-channel guard
run: yarn test:publish-channel
- name: Restore node_modules archive
id: cache
uses: actions/cache/restore@v4
with:
# NOT lookup-only. It was, and that made every warm run fail: on a cache HIT the archive
# is never written to disk, so `Archive node_modules` is skipped along with the other
# producer steps, and the UNGUARDED `Upload node_modules archive` below then trips its
# `if-no-files-found: error`. The cold run that first writes the key passes; the NEXT run,
# the one the cache exists for, is the one that breaks - the worst place to put a failure.
# Downloading ~2.9 GB here costs a few minutes against the ~90-minute install it replaces,
# and it is what guarantees the run-scoped artifact handoff below actually has a file.
path: ${{ env.NODE_MODULES_ARCHIVE }}
key: ${{ runner.os }}-${{ runner.arch }}-node-modules-${{ hashFiles('yarn.lock', 'package.json', 'patches/**', '.scripts/postinstall.js') }}
# Route the install through the internal Verdaccio cache. Placed AFTER the cache restore on
# purpose: this action rewrites yarn.lock's resolved URLs, and hashFiles() in a cache key
# evaluates at step runtime - configuring the registry before the restore would move the key
# and bifurcate the cache namespace by VIP reachability (the reason #10025 skipped this file).
# Measured cost of NOT having it here: the bootstrap fallback pulled from the public registry
# and blew through the 180-minute job ceiling twice on stage run 32513912629.
- name: Configure Registry
if: steps.cache.outputs.cache-hit != 'true'
uses: ever-co/ever-gauzy/.github/actions/configure-registry@aa4ee19926fabcf820aa1294385a76aec6bdb548
with:
verdaccio-registry: ${{ vars.VERDACCIO_REGISTRY }}
verdaccio-token: ${{ secrets.VERDACCIO_TOKEN }}
force-public: ${{ vars.VERDACCIO_FORCE_PUBLIC }}
# Not contains(matrix.os, ...): these jobs define no matrix.os, so that expression is
# always false and would silently disable the in-network VIP retry and warning.
expect-vip: ${{ vars.RUNNER_LINUX_X64_8 != '' }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: yarn install --network-timeout 1000000 --frozen-lockfile --ignore-scripts
- name: Run postinstall manually
if: steps.cache.outputs.cache-hit != 'true'
run: yarn postinstall.manual
- name: Archive node_modules
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: .github/scripts/archive-node-modules.sh
- name: Save node_modules archive
if: steps.cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
continue-on-error: true
# Explicit save so a FAILED install can never publish a half-built tree. BEST EFFORT, and only
# a CROSS-run optimization — see the upload below for why it cannot be the handoff.
with:
path: ${{ env.NODE_MODULES_ARCHIVE }}
# Pinned to the key the RESTORE computed, not re-derived: Configure Registry rewrites
# yarn.lock after the restore, and hashFiles() here evaluates at save time - re-deriving
# would write the archive under a different key than the next run looks up.
key: ${{ steps.cache.outputs.cache-primary-key }}
- name: Upload node_modules archive
uses: actions/upload-artifact@v7
# THE HANDOFF. The cache above cannot be relied on for it: an Actions cache is a REPOSITORY
# resource on a 10 GB budget, so a build on any other ref can evict this entry between the
# save and the consumer's restore. That is not hypothetical — it happened on the first run of
# this design (run 32377379196): the producer logged "Cache saved with key: …2b58b651…",
# build-libs asked for the byte-identical key 32 seconds later and got "Failed to restore
# cache entry", because three 4.66 GB setup-node yarn caches written by develop, stage and
# PR #10014 had pushed the repo to 14.03 GB and LRU took the newest entry.
# An artifact is scoped to THIS RUN and no other workflow can evict it. The cleanup job
# deletes it when the run ends, so it costs storage only while the run needs it.
with:
name: build-node-modules
path: ${{ env.NODE_MODULES_ARCHIVE }}
retention-days: 1
compression-level: 0 # already zstd-compressed
if-no-files-found: error
overwrite: true
# The strict library verdict, as its own early-reporting check: this is the task set whose
# per-library `strictTemplates` tsconfigs catch template type errors (the class that broke the
# demo webapp image). It has no `needs` edge and no app build behind it, so it reports as soon
# as install + the 71 library builds finish.
build-libs:
name: build-libs
needs: build-monorepo-root
# `needs:` alone would make a producer failure SKIP this job, and a skipped required check does
# not block a merge — the exact "no failure just means never ran" trap this file's header warns
# about. `!cancelled()` keeps the ordering but still runs on producer failure, where the restore
# fails loudly and this reports a real red instead of vanishing.
# Never run jobs for a pull request from a fork (owner decision 2026-09-16); branch PRs and pushes still run.
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
runs-on: ${{ vars.RUNNER_LINUX_X64_8 || 'ubuntu-latest' }}
timeout-minutes: 180
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Download node_modules archive
id: artifact
uses: actions/download-artifact@v8
# Primary handoff — run-scoped, so no other workflow can evict it between jobs.
continue-on-error: true
with:
name: build-node-modules
- name: Restore node_modules archive (cache fallback)
# Gated, because it was NOT before and the comment below claimed otherwise. With no `if:`
# this ran even on a successful artifact download, and since the producer saves the same
# key each cold run the lookup HITS - so every consumer pulled the same ~2.9 GB twice.
# Measured on run 32418672320: artifact download 1015s/1028s/1102s, then this step still
# running past 209s on top. ~17 min of pure waste per consumer, ~68 min per run.
if: steps.artifact.outcome != 'success'
uses: actions/cache/restore@v4
continue-on-error: true
with:
# Only reached if the artifact was unavailable. Deliberately NOT fail-on-cache-miss: the
# cache is best effort here, and the script below still has the install fallback.
path: ${{ env.NODE_MODULES_ARCHIVE }}
key: ${{ runner.os }}-${{ runner.arch }}-node-modules-${{ hashFiles('yarn.lock', 'package.json', 'patches/**', '.scripts/postinstall.js') }}
# Route the install through the internal Verdaccio cache. Placed AFTER the cache restore on
# purpose: this action rewrites yarn.lock's resolved URLs, and hashFiles() in a cache key
# evaluates at step runtime - configuring the registry before the restore would move the key
# and bifurcate the cache namespace by VIP reachability (the reason #10025 skipped this file).
# Measured cost of NOT having it here: the bootstrap fallback pulled from the public registry
# and blew through the 180-minute job ceiling twice on stage run 32513912629.
- name: Configure Registry
uses: ever-co/ever-gauzy/.github/actions/configure-registry@aa4ee19926fabcf820aa1294385a76aec6bdb548
with:
verdaccio-registry: ${{ vars.VERDACCIO_REGISTRY }}
verdaccio-token: ${{ secrets.VERDACCIO_TOKEN }}
force-public: ${{ vars.VERDACCIO_FORCE_PUBLIC }}
# Not contains(matrix.os, ...): these jobs define no matrix.os, so that expression is
# always false and would silently disable the in-network VIP retry and warning.
expect-vip: ${{ vars.RUNNER_LINUX_X64_8 != '' }}
- name: Restore node_modules
shell: bash
# Unpacks the tree build-monorepo-root published, or installs from scratch if the cache is
# unavailable. One step, so there is no `if:` on a previous step's outcome to get wrong.
run: .github/scripts/restore-node-modules.sh
- name: Build packages
run: yarn build:package:all
build-api:
name: build-api
needs: build-monorepo-root
# `needs:` alone would make a producer failure SKIP this job, and a skipped required check does
# not block a merge — the exact "no failure just means never ran" trap this file's header warns
# about. `!cancelled()` keeps the ordering but still runs on producer failure, where the restore
# fails loudly and this reports a real red instead of vanishing.
# Never run jobs for a pull request from a fork (owner decision 2026-09-16); branch PRs and pushes still run.
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
runs-on: ${{ vars.RUNNER_LINUX_X64_8 || 'ubuntu-latest' }}
timeout-minutes: 180
services:
postgres:
image: postgres:18-alpine@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
env:
POSTGRES_USER: migration_test
POSTGRES_PASSWORD: migration_test
POSTGRES_DB: migration_test
ports:
- 5432/tcp
options: >-
--health-cmd "pg_isready -U migration_test -d migration_test"
--health-interval 5s --health-timeout 5s --health-retries 12
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Download node_modules archive
id: artifact
uses: actions/download-artifact@v8
# Primary handoff — run-scoped, so no other workflow can evict it between jobs.
continue-on-error: true
with:
name: build-node-modules
- name: Restore node_modules archive (cache fallback)
# Gated, because it was NOT before and the comment below claimed otherwise. With no `if:`
# this ran even on a successful artifact download, and since the producer saves the same
# key each cold run the lookup HITS - so every consumer pulled the same ~2.9 GB twice.
# Measured on run 32418672320: artifact download 1015s/1028s/1102s, then this step still
# running past 209s on top. ~17 min of pure waste per consumer, ~68 min per run.
if: steps.artifact.outcome != 'success'
uses: actions/cache/restore@v4
continue-on-error: true
with:
# Only reached if the artifact was unavailable. Deliberately NOT fail-on-cache-miss: the
# cache is best effort here, and the script below still has the install fallback.
path: ${{ env.NODE_MODULES_ARCHIVE }}
key: ${{ runner.os }}-${{ runner.arch }}-node-modules-${{ hashFiles('yarn.lock', 'package.json', 'patches/**', '.scripts/postinstall.js') }}
# Route the install through the internal Verdaccio cache. Placed AFTER the cache restore on
# purpose: this action rewrites yarn.lock's resolved URLs, and hashFiles() in a cache key
# evaluates at step runtime - configuring the registry before the restore would move the key
# and bifurcate the cache namespace by VIP reachability (the reason #10025 skipped this file).
# Measured cost of NOT having it here: the bootstrap fallback pulled from the public registry
# and blew through the 180-minute job ceiling twice on stage run 32513912629.
- name: Configure Registry
uses: ever-co/ever-gauzy/.github/actions/configure-registry@aa4ee19926fabcf820aa1294385a76aec6bdb548
with:
verdaccio-registry: ${{ vars.VERDACCIO_REGISTRY }}
verdaccio-token: ${{ secrets.VERDACCIO_TOKEN }}
force-public: ${{ vars.VERDACCIO_FORCE_PUBLIC }}
# Not contains(matrix.os, ...): these jobs define no matrix.os, so that expression is
# always false and would silently disable the in-network VIP retry and warning.
expect-vip: ${{ vars.RUNNER_LINUX_X64_8 != '' }}
- name: Restore node_modules
shell: bash
# Unpacks the tree build-monorepo-root published, or installs from scratch if the cache is
# unavailable. One step, so there is no `if:` on a previous step's outcome to get wrong.
run: .github/scripts/restore-node-modules.sh
- name: Test PostgreSQL migrations
# cspell:words PGHOST PGPORT PGDATABASE
env:
PGHOST: 127.0.0.1
PGPORT: ${{ job.services.postgres.ports[5432] }}
PGUSER: migration_test
PGPASSWORD: migration_test
PGDATABASE: migration_test
run: yarn nx run core:test-postgres-migrations
- name: Test Ever Async integration boundary
run: yarn nx run plugin-integration-ever-async:test-integration
- name: Build API
run: yarn build:api:prod:ci
build-web:
name: build-web
needs: build-monorepo-root
# `needs:` alone would make a producer failure SKIP this job, and a skipped required check does
# not block a merge — the exact "no failure just means never ran" trap this file's header warns
# about. `!cancelled()` keeps the ordering but still runs on producer failure, where the restore
# fails loudly and this reports a real red instead of vanishing.
# Never run jobs for a pull request from a fork (owner decision 2026-09-16); branch PRs and pushes still run.
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
runs-on: ${{ vars.RUNNER_LINUX_X64_8 || 'ubuntu-latest' }}
timeout-minutes: 180
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Download node_modules archive
id: artifact
uses: actions/download-artifact@v8
# Primary handoff — run-scoped, so no other workflow can evict it between jobs.
continue-on-error: true
with:
name: build-node-modules
- name: Restore node_modules archive (cache fallback)
# Gated, because it was NOT before and the comment below claimed otherwise. With no `if:`
# this ran even on a successful artifact download, and since the producer saves the same
# key each cold run the lookup HITS - so every consumer pulled the same ~2.9 GB twice.
# Measured on run 32418672320: artifact download 1015s/1028s/1102s, then this step still
# running past 209s on top. ~17 min of pure waste per consumer, ~68 min per run.
if: steps.artifact.outcome != 'success'
uses: actions/cache/restore@v4
continue-on-error: true
with:
# Only reached if the artifact was unavailable. Deliberately NOT fail-on-cache-miss: the
# cache is best effort here, and the script below still has the install fallback.
path: ${{ env.NODE_MODULES_ARCHIVE }}
key: ${{ runner.os }}-${{ runner.arch }}-node-modules-${{ hashFiles('yarn.lock', 'package.json', 'patches/**', '.scripts/postinstall.js') }}
# Route the install through the internal Verdaccio cache. Placed AFTER the cache restore on
# purpose: this action rewrites yarn.lock's resolved URLs, and hashFiles() in a cache key
# evaluates at step runtime - configuring the registry before the restore would move the key
# and bifurcate the cache namespace by VIP reachability (the reason #10025 skipped this file).
# Measured cost of NOT having it here: the bootstrap fallback pulled from the public registry
# and blew through the 180-minute job ceiling twice on stage run 32513912629.
- name: Configure Registry
uses: ever-co/ever-gauzy/.github/actions/configure-registry@aa4ee19926fabcf820aa1294385a76aec6bdb548
with:
verdaccio-registry: ${{ vars.VERDACCIO_REGISTRY }}
verdaccio-token: ${{ secrets.VERDACCIO_TOKEN }}
force-public: ${{ vars.VERDACCIO_FORCE_PUBLIC }}
# Not contains(matrix.os, ...): these jobs define no matrix.os, so that expression is
# always false and would silently disable the in-network VIP retry and warning.
expect-vip: ${{ vars.RUNNER_LINUX_X64_8 != '' }}
- name: Restore node_modules
shell: bash
# Unpacks the tree build-monorepo-root published, or installs from scratch if the cache is
# unavailable. One step, so there is no `if:` on a previous step's outcome to get wrong.
run: .github/scripts/restore-node-modules.sh
- name: Build web
run: yarn build:gauzy:prod:ci
build-desktop:
name: build-desktop
needs: build-monorepo-root
# `needs:` alone would make a producer failure SKIP this job, and a skipped required check does
# not block a merge — the exact "no failure just means never ran" trap this file's header warns
# about. `!cancelled()` keeps the ordering but still runs on producer failure, where the restore
# fails loudly and this reports a real red instead of vanishing.
# The second half matches the CircleCI branch filter: desktop was never built on ordinary PR
# branches. Both conditions live in ONE expression — two `if:` keys is a duplicate mapping key and
# the file will not parse.
if: >-
${{ !cancelled() && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/stage' ||
github.ref == 'refs/heads/master') }}
runs-on: ${{ vars.RUNNER_LINUX_X64_8 || 'ubuntu-latest' }}
timeout-minutes: 180
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Install system dependencies for Electron
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential icnsutils graphicsmagick binutils libappindicator3-1 || true
- name: Download node_modules archive
id: artifact
uses: actions/download-artifact@v8
# Primary handoff — run-scoped, so no other workflow can evict it between jobs.
continue-on-error: true
with:
name: build-node-modules
- name: Restore node_modules archive (cache fallback)
# Gated, because it was NOT before and the comment below claimed otherwise. With no `if:`
# this ran even on a successful artifact download, and since the producer saves the same
# key each cold run the lookup HITS - so every consumer pulled the same ~2.9 GB twice.
# Measured on run 32418672320: artifact download 1015s/1028s/1102s, then this step still
# running past 209s on top. ~17 min of pure waste per consumer, ~68 min per run.
if: steps.artifact.outcome != 'success'
uses: actions/cache/restore@v4
continue-on-error: true
with:
# Only reached if the artifact was unavailable. Deliberately NOT fail-on-cache-miss: the
# cache is best effort here, and the script below still has the install fallback.
path: ${{ env.NODE_MODULES_ARCHIVE }}
key: ${{ runner.os }}-${{ runner.arch }}-node-modules-${{ hashFiles('yarn.lock', 'package.json', 'patches/**', '.scripts/postinstall.js') }}
# Route the install through the internal Verdaccio cache. Placed AFTER the cache restore on
# purpose: this action rewrites yarn.lock's resolved URLs, and hashFiles() in a cache key
# evaluates at step runtime - configuring the registry before the restore would move the key
# and bifurcate the cache namespace by VIP reachability (the reason #10025 skipped this file).
# Measured cost of NOT having it here: the bootstrap fallback pulled from the public registry
# and blew through the 180-minute job ceiling twice on stage run 32513912629.
- name: Configure Registry
uses: ever-co/ever-gauzy/.github/actions/configure-registry@aa4ee19926fabcf820aa1294385a76aec6bdb548
with:
verdaccio-registry: ${{ vars.VERDACCIO_REGISTRY }}
verdaccio-token: ${{ secrets.VERDACCIO_TOKEN }}
force-public: ${{ vars.VERDACCIO_FORCE_PUBLIC }}
# Not contains(matrix.os, ...): these jobs define no matrix.os, so that expression is
# always false and would silently disable the in-network VIP retry and warning.
expect-vip: ${{ vars.RUNNER_LINUX_X64_8 != '' }}
- name: Restore node_modules
shell: bash
# Unpacks the tree build-monorepo-root published, or installs from scratch if the cache is
# unavailable. One step, so there is no `if:` on a previous step's outcome to get wrong.
run: .github/scripts/restore-node-modules.sh
- name: Build desktop
run: yarn build:desktop
# ---------------------------------------------------------------------------------------------
# cleanup — the dependency archive exists only to cross job boundaries; delete it when the run ends.
# ---------------------------------------------------------------------------------------------
cleanup:
name: Delete node_modules artifact
needs: [build-monorepo-root, build-libs, build-api, build-web, build-desktop]
# Delete only when nothing failed. This used to be a bare `always()` (rationale then: red-run
# leftovers once filled the org's Actions storage quota and stopped uploads) - but the artifact
# is this run's ONLY dependency handoff, so deleting it on a red run breaks "Re-run failed
# jobs": every rerun consumer falls into the 1-3h bootstrap fallback. Measured on stage run
# 32513912629 (2026-08-21): a seconds-long DNS blip failed one restore, cleanup deleted the
# artifact, and the rerun cost build-libs 178m and pushed build-api into the 180m ceiling.
# The quota concern is now bounded instead of ignored: red Build runs are rare since the gate
# rework, and retention-days: 1 ages a kept artifact out within a day regardless.
# `skipped` (build-desktop on PR runs) still allows deletion - only failure/cancelled block it.
# Never run jobs for a pull request from a fork (owner decision 2026-09-16); branch PRs and pushes still run.
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
actions: write # deletes this run's node_modules artifact
steps:
- name: Delete build-node-modules
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
ids=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts" --jq '.artifacts[] | select(.name=="build-node-modules") | .id')
if [ -z "$ids" ]; then echo "nothing to delete"; exit 0; fi
for id in $ids; do
if gh api --method DELETE "repos/$GITHUB_REPOSITORY/actions/artifacts/$id" >/dev/null 2>&1; then
echo "deleted artifact $id"
else
echo "::warning::could not delete artifact $id — already gone?"
fi
done