Files
Ruslan KonviserandClaude Opus 5.5 40826df61b ci: unpack the lint cache off the RAM disk; PRs into develop start no workflow
Static Checks / lint (x64-4 lane): every cache hit since the job moved to
this lane ran out of space. The 2.47 GB archive plus the tree it unpacks to
does not fit the 16Gi RAM-backed workspace, so each hit fell back to a
1.5-4 h cold install (16 of 16 runs cold since #10303). The Restore step
now moves the archive onto the disk-backed package-cache volume (the
parent of YARN_CACHE_FOLDER) before extracting, so only the tree lives in
RAM. Where YARN_CACHE_FOLDER is unset, or the move fails, it extracts in
place exactly as before. Two report-only df lines (after the restore and
at the top of Summarize) record workspace headroom and can never fail a
step.

Triggers: apply the owner decision of 2026-09-21 (already on draft #10254,
same text) directly to develop. A pull request INTO develop no longer
starts static-checks, typos (cspell), snyk-analysis (trigger removed,
restore lines kept in a comment), or build, secrets-analysis and the
external-uptime-monitor self-test (branches-ignore: develop, so PRs into
stage/master still run them). Every push trigger is unchanged, so all of
them still run when code lands on develop. develop has no required status
checks, so no PR is blocked by the missing runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:11:58 +02:00

537 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:
# Off by owner decision (2026-09-21): a pull request INTO develop does not start this workflow. The cost
# belonged to every commit on the branch being merged, and the run belongs to the merge - the push
# trigger below runs it when code lands on develop. PRs aimed at other branches still run it.
branches-ignore:
- develop
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