Hand Cypress E2E deps to workers as a run-scoped artifact (#38253)

prep-deps wrote node_modules + the Cypress binary to a shared Actions
cache keyed on the cypress lockfile, and the 40 workers read it back
within the same run.

The repo is pinned at the 10 GB Actions cache ceiling (100 entries,
9.99 GB), so LRU eviction runs continuously. Every distinct cypress
lockfile a run happens to build costs a ~275 MB entry in that budget —
four are live today, 1.10 GB total — and they evict entries other jobs
depend on: the webapp node_modules cache (3.7 GB), the Playwright deps
cache, and the E2E npm registry cache.

Build the deps once in prep-deps and upload them as a run-scoped
artifact instead. The handoff is unchanged in shape — one producer, N
consumers, within a run — but it costs the shared cache nothing, and it
drops the fail-on-cache-miss cliff the workers previously depended on.

The artifact name is scoped by edition: e2e-tests-ci.yml invokes this
template twice per workflow run (default and fips) and artifact names
are run-scoped, so the two invocations would otherwise collide.

prep-deps loses its cache-hit shortcut and now always installs, so it
restores the master-warmed ~/.npm registry cache read-only to keep that
install short, and its timeout goes 10 -> 15 minutes.

Playwright still uses the cache handoff; it is a larger payload with a
churnier key and follows separately.
This commit is contained in:
Jesse Hallam
2026-09-02 11:44:15 -03:00
committed by GitHub
parent cfab036615
commit 235bddbae0
3 changed files with 87 additions and 23 deletions
@@ -0,0 +1,40 @@
name: "Pack Cypress deps artifact"
description: >
Tar the Cypress node_modules and binary into a run-scoped artifact for the
workers in e2e-tests-cypress-template.yml. The payload must stay in sync with
unpack-cypress-deps.
inputs:
fips:
description: >
Whether this is a FIPS-edition run. e2e-tests-ci.yml invokes the cypress
template twice per workflow run — once per edition — and artifact names are
run-scoped, so the two invocations would otherwise collide. Must match the
unpack step.
required: false
default: "false"
runs:
using: "composite"
steps:
# -C so the archives hold paths relative to their extraction root, letting
# unpack extract straight into the workspace and HOME.
- name: ci/pack-cypress-deps
shell: bash
run: |
set -euo pipefail
tar -czf "${RUNNER_TEMP}/cypress-node-modules.tgz" -C "${GITHUB_WORKSPACE}" e2e-tests/cypress/node_modules
tar -czf "${RUNNER_TEMP}/cypress-binary.tgz" -C "${HOME}" .cache/Cypress
- name: ci/upload-cypress-deps
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: e2e-cypress-deps${{ inputs.fips == 'true' && '-fips' || '' }}
path: |
${{ runner.temp }}/cypress-node-modules.tgz
${{ runner.temp }}/cypress-binary.tgz
# Only ever consumed by workers in the same run.
retention-days: 1
if-no-files-found: error
# The tarballs are already gzipped; re-compressing them in the zip
# wrapper costs CPU for no gain.
compression-level: 0
@@ -0,0 +1,27 @@
name: "Unpack Cypress deps artifact"
description: >
Download and extract the run-scoped artifact built by pack-cypress-deps,
restoring the Cypress node_modules and binary into place.
inputs:
fips:
description: >
Whether this is a FIPS-edition run. Must match the pack step so the correct
artifact is downloaded.
required: false
default: "false"
runs:
using: "composite"
steps:
- name: ci/download-cypress-deps
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: e2e-cypress-deps${{ inputs.fips == 'true' && '-fips' || '' }}
path: ${{ runner.temp }}
- name: ci/unpack-cypress-deps
shell: bash
run: |
set -euo pipefail
tar -xzf "${RUNNER_TEMP}/cypress-node-modules.tgz" -C "${GITHUB_WORKSPACE}"
tar -xzf "${RUNNER_TEMP}/cypress-binary.tgz" -C "${HOME}"
@@ -198,13 +198,12 @@ jobs:
echo "workers=$(jq -nc --argjson n "${INPUT_WORKERS}" '[range(1; $n+1)]')" >> $GITHUB_OUTPUT
echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
# Install webapp node_modules once via the shared webapp-setup action, then
# workers restore the same stable cache. The node_modules cache is keyed only
# on webapp/package-lock.json and is shared with webapp-ci.yml jobs.
# Build the Cypress deps once and hand them to the workers as a run-scoped
# artifact.
prep-deps:
name: prep-deps
runs-on: ubuntu-24.04
timeout-minutes: 10
timeout-minutes: 15
permissions:
contents: read
steps:
@@ -214,23 +213,23 @@ jobs:
persist-credentials: false
ref: ${{ inputs.commit_sha }}
fetch-depth: 1
# Provides the @mattermost/eslint-plugin target the cypress install
# symlinks to.
- name: ci/setup-webapp-node-modules
uses: ./.github/actions/webapp-setup
- name: ci/cache-cypress-deps
# node_modules + the cypress binary (downloaded to ~/.cache/Cypress by
# cypress's postinstall, not into node_modules). Both must be cached;
# otherwise workers see "cypress npm package installed but binary missing".
id: cache-cypress
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
e2e-tests/cypress/node_modules
~/.cache/Cypress
key: e2e-cypress-deps-${{ runner.os }}-${{ hashFiles('e2e-tests/cypress/package-lock.json') }}
# Read-only restore of the registry cache warmed daily from master, so the
# install below refetches as little as possible.
- name: ci/restore-npm-cache
uses: ./.github/actions/restore-e2e-npm-cache
# The postinstall downloads the Cypress binary to ~/.cache/Cypress rather
# than into node_modules, which is why the artifact carries both.
- name: ci/install-cypress-deps
if: steps.cache-cypress.outputs.cache-hit != 'true'
working-directory: e2e-tests/cypress
run: npm ci
- name: ci/pack-cypress-deps
uses: ./.github/actions/pack-cypress-deps
with:
fips: ${{ inputs.server_edition == 'fips' }}
# Register the Test System IO run AFTER prep-deps so workers reach
# dispatch-run within Test System IO's inactivity window.
@@ -317,14 +316,12 @@ jobs:
uses: ./.github/actions/webapp-setup
with:
read-only: "true"
- name: ci/restore-cypress-deps
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
# prep-deps built these; it is an ancestor of this job via dispatch-begin,
# so the artifact is always present by now.
- name: ci/reuse-cypress-deps
uses: ./.github/actions/unpack-cypress-deps
with:
path: |
e2e-tests/cypress/node_modules
~/.cache/Cypress
key: e2e-cypress-deps-${{ runner.os }}-${{ hashFiles('e2e-tests/cypress/package-lock.json') }}
fail-on-cache-miss: true
fips: ${{ inputs.server_edition == 'fips' }}
- name: ci/cloud-init
working-directory: e2e-tests
run: make cloud-init