mirror of
https://github.com/mattermost/mattermost.git
synced 2026-11-03 12:48:37 -06:00
Warm the Cypress deps cache from master and prefer it over the artifact (#38261)
* Warm the Cypress deps cache from master and prefer it over the artifact The parent commit hands the Cypress deps to the workers as a run-scoped artifact, which costs the shared Actions cache nothing but makes prep-deps install on every run. A baseline run measured the cache path it replaced at 6s per worker with prep-deps skipping the install entirely, so paying an install plus a ~275 MB upload on every run is a poor trade for the common case. Restore the fast path without restoring the churn. A new scheduled workflow becomes the sole writer of the deps key, warming it from master daily and on any push that changes the cypress lockfile. prep-deps then probes the key with lookup-only — a metadata call, not a download — and publishes the result as a job output. Workers restore the warmed entry on a hit and unpack the artifact only on a miss, so exactly one of the two paths runs. The net effect on the cache budget is one warm entry per master lockfile instead of one per lockfile any run happens to build; the four live entries today total 1.10 GB. PR and merge runs still never write. restore-cypress-deps centralises the key so the warm workflow, the probe and the workers cannot drift apart. Its payload must stay identical to pack/unpack-cypress-deps, since the cache entry and the artifact are interchangeable by construction. A separate binary-only cache is deliberately omitted: keyed on the same lockfile hash, it would invalidate in lockstep with the deps key and earn its keep only when one entry is evicted and the other survives. Keying it on the resolved cypress version instead would be a real win and can follow on its own. * Spell out which transport each Cypress deps action provides restore-cypress-deps and unpack-cypress-deps both leave a worker with node_modules and the Cypress binary, differing only in where they fetch them from, and nothing in their descriptions said so. Name the transport each one implements, when it is the one that runs, and the invariant that lets the workers accept either. No behaviour change; description metadata only. * Warm the cache only from master, even on manual dispatch * Report the Cypress deps cache outcome to the job summary Which transport a run took was only legible by opening prep-deps and reading which steps the probe skipped, and whether the warm workflow actually saved anything was just as buried. Report both to GITHUB_STEP_SUMMARY: prep-deps prints hit or miss, and the warm job prints saved or already warm. Both print the computed key alongside the verdict, which is what makes a miss diagnosable — a lockfile or path drift reads as an unexpected key rather than an unexplained miss. The workers are deliberately left alone. Their summary would be twenty near-identical lines per run, and the hit path already runs with fail-on-cache-miss, so a worker either restores or fails loudly.
This commit is contained in:
1 parent
bbd00c4f35
commit
1cf3c87173
5 files changed
+204
-10
No files matched your search
@@ -1,8 +1,14 @@
|
||||
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.
|
||||
The producing half of the artifact transport for the Cypress deps: tars the
|
||||
node_modules and Cypress binary just installed by prep-deps and uploads them
|
||||
as a run-scoped artifact for this run's workers to unpack.
|
||||
|
||||
This is the fallback path, reached only when restore-cypress-deps found the
|
||||
cache key cold. It is not a cache — the artifact is scoped to one run, costs
|
||||
the shared Actions cache nothing, and is retained for a day. It must produce
|
||||
the payload restore-cypress-deps would otherwise have restored, since the
|
||||
workers accept either.
|
||||
|
||||
inputs:
|
||||
fips:
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: "Restore Cypress deps cache"
|
||||
description: >
|
||||
The Actions-cache transport for the Cypress deps — node_modules plus the
|
||||
Cypress binary — keyed on the cypress lockfile.
|
||||
|
||||
This is the fast path, taken whenever the key is warm: prep-deps asks whether
|
||||
it exists with lookup-only, and the workers do the real restore. Read-only, as
|
||||
e2e-ci-cache-warm-cypress-deps.yml is the only writer of the key, so PR and
|
||||
merge runs never write to the Actions cache.
|
||||
|
||||
When the key is cold the template falls back to the artifact transport instead
|
||||
— pack-cypress-deps into unpack-cypress-deps — which must land the same
|
||||
payload in the same paths, so the two transports are interchangeable.
|
||||
|
||||
inputs:
|
||||
lookup-only:
|
||||
description: "Probe whether the key exists without downloading the entry."
|
||||
required: false
|
||||
default: "false"
|
||||
fail-on-cache-miss:
|
||||
description: "Fail the step when the exact key is absent."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
outputs:
|
||||
cache-hit:
|
||||
description: "true on an exact-key hit"
|
||||
value: ${{ steps.restore.outputs.cache-hit }}
|
||||
key:
|
||||
description: "The computed cache key"
|
||||
value: ${{ steps.key.outputs.key }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: ci/compute-cypress-deps-key
|
||||
id: key
|
||||
shell: bash
|
||||
run: echo "key=e2e-cypress-deps-${{ runner.os }}-${{ hashFiles('e2e-tests/cypress/package-lock.json') }}" >> "${GITHUB_OUTPUT}"
|
||||
- name: ci/restore-cypress-deps
|
||||
id: restore
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
# No restore-keys: an older node_modules is not a usable substitute for
|
||||
# this lockfile, and a partial hit would silently diverge from `npm ci`.
|
||||
with:
|
||||
path: |
|
||||
e2e-tests/cypress/node_modules
|
||||
~/.cache/Cypress
|
||||
key: ${{ steps.key.outputs.key }}
|
||||
lookup-only: ${{ inputs.lookup-only }}
|
||||
fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }}
|
||||
@@ -1,7 +1,12 @@
|
||||
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.
|
||||
The consuming half of the artifact transport for the Cypress deps: downloads
|
||||
this run's artifact from pack-cypress-deps and extracts the node_modules and
|
||||
Cypress binary into place.
|
||||
|
||||
Runs on a worker only when restore-cypress-deps found the cache key cold, and
|
||||
must leave the same paths populated that a cache restore would have, so the
|
||||
steps that follow cannot tell which transport ran.
|
||||
|
||||
inputs:
|
||||
fips:
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Warms the Cypress deps cache (node_modules + the Cypress binary) from master.
|
||||
# This workflow is the only writer of the key, so PR and merge runs never write
|
||||
# to the Actions cache; they restore it read-only via
|
||||
# .github/actions/restore-cypress-deps.
|
||||
#
|
||||
# Until this workflow has warmed a given key, runs on that tree fall back to the
|
||||
# run-scoped artifact built by prep-deps in e2e-tests-cypress-template.yml.
|
||||
name: "E2E Cache Warm: Cypress deps"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 3 * * *" # Daily 3am UTC — after the webapp node_modules and E2E npm registry caches warm at 1am
|
||||
# Warm as soon as the key's only input lands on master, so the merge run and
|
||||
# subsequent PRs hit the cache instead of paying for the artifact fallback all
|
||||
# day. The schedule is the backstop.
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "e2e-tests/cypress/package-lock.json"
|
||||
workflow_dispatch:
|
||||
|
||||
# Keep manual, scheduled and push-triggered runs from racing to save the same key.
|
||||
concurrency:
|
||||
group: e2e-ci-cache-warm-cypress-deps
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write # required to save the Actions cache
|
||||
|
||||
jobs:
|
||||
warm:
|
||||
name: Warm Cypress deps cache
|
||||
runs-on: ubuntu-24.04
|
||||
# workflow_dispatch can target any branch. Warming from one would write a
|
||||
# branch-scoped entry keyed on that branch's lockfile — quota spent on a key
|
||||
# no other branch can read, which is the churn this workflow exists to avoid.
|
||||
if: github.ref == 'refs/heads/master'
|
||||
# Bound a hung run so it can't hold the concurrency group for the default
|
||||
# 6-hour timeout and block subsequent warm runs.
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout mattermost project
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: ci/probe-cypress-deps
|
||||
id: deps
|
||||
uses: ./.github/actions/restore-cypress-deps
|
||||
with:
|
||||
lookup-only: "true"
|
||||
# Ahead of webapp-setup so its install benefits too, and so cache/restore
|
||||
# never untars ~/.npm on top of entries npm has already written this job.
|
||||
- name: ci/restore-npm-cache
|
||||
if: steps.deps.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/restore-e2e-npm-cache
|
||||
# Provides the @mattermost/eslint-plugin target the cypress install
|
||||
# symlinks to.
|
||||
- name: ci/setup-webapp-node-modules
|
||||
if: steps.deps.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/webapp-setup
|
||||
with:
|
||||
read-only: "true"
|
||||
# The postinstall downloads the Cypress binary to ~/.cache/Cypress rather
|
||||
# than into node_modules, which is why the save below covers both.
|
||||
- name: ci/install-cypress-deps
|
||||
if: steps.deps.outputs.cache-hit != 'true'
|
||||
working-directory: e2e-tests/cypress
|
||||
run: npm ci
|
||||
# Must stay in sync with restore-cypress-deps and pack-cypress-deps.
|
||||
- name: ci/save-cypress-deps
|
||||
if: steps.deps.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: |
|
||||
e2e-tests/cypress/node_modules
|
||||
~/.cache/Cypress
|
||||
key: ${{ steps.deps.outputs.key }}
|
||||
# This job is the sole writer of the key, so its summary is where "is
|
||||
# warming actually working?" gets answered.
|
||||
- name: ci/report-warm-outcome
|
||||
env:
|
||||
CACHE_HIT: ${{ steps.deps.outputs.cache-hit }}
|
||||
CACHE_KEY: ${{ steps.deps.outputs.key }}
|
||||
run: |
|
||||
if [ "${CACHE_HIT}" = "true" ]; then
|
||||
echo "Cypress deps: \`${CACHE_KEY}\` already warm; nothing new to save." >> "${GITHUB_STEP_SUMMARY}"
|
||||
else
|
||||
echo "Cypress deps: saved \`${CACHE_KEY}\`." >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
@@ -198,14 +198,20 @@ jobs:
|
||||
echo "workers=$(jq -nc --argjson n "${INPUT_WORKERS}" '[range(1; $n+1)]')" >> $GITHUB_OUTPUT
|
||||
echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
|
||||
# Build the Cypress deps once and hand them to the workers as a run-scoped
|
||||
# artifact.
|
||||
# Decide how the workers get their deps, and bridge the gap when they can't use
|
||||
# the cache. On a hit this job is a checkout and a metadata lookup, and the
|
||||
# workers restore the warmed entry directly. On a miss — a PR that changes the
|
||||
# cypress lockfile, or a master commit that landed before the warm workflow ran
|
||||
# — it builds the deps once and ships them as a run-scoped artifact, until the
|
||||
# next warm populates the key.
|
||||
prep-deps:
|
||||
name: prep-deps
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
cache-hit: ${{ steps.probe.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -213,20 +219,47 @@ jobs:
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.commit_sha }}
|
||||
fetch-depth: 1
|
||||
# lookup-only: asking whether the key exists costs a metadata call rather
|
||||
# than a ~275 MB download. The workers do the real restore.
|
||||
- name: ci/probe-cypress-deps
|
||||
id: probe
|
||||
uses: ./.github/actions/restore-cypress-deps
|
||||
with:
|
||||
lookup-only: "true"
|
||||
# Which transport a run took is otherwise only legible from which steps
|
||||
# below were skipped. Reporting the key alongside the verdict is what makes
|
||||
# a miss diagnosable: a lockfile or path drift reads as an unexpected key
|
||||
# rather than an unexplained miss.
|
||||
- name: ci/report-cypress-deps-transport
|
||||
env:
|
||||
CACHE_HIT: ${{ steps.probe.outputs.cache-hit }}
|
||||
CACHE_KEY: ${{ steps.probe.outputs.key }}
|
||||
run: |
|
||||
if [ "${CACHE_HIT}" = "true" ]; then
|
||||
echo "Cypress deps: cache **hit** on \`${CACHE_KEY}\` — workers restore it directly from the Actions cache." >> "${GITHUB_STEP_SUMMARY}"
|
||||
else
|
||||
echo "Cypress deps: cache **miss** on \`${CACHE_KEY}\` — installing once and shipping the run-scoped artifact." >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
# Everything below is the cache-miss path.
|
||||
#
|
||||
# Provides the @mattermost/eslint-plugin target the cypress install
|
||||
# symlinks to.
|
||||
- name: ci/setup-webapp-node-modules
|
||||
if: steps.probe.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/webapp-setup
|
||||
# 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
|
||||
if: steps.probe.outputs.cache-hit != 'true'
|
||||
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.probe.outputs.cache-hit != 'true'
|
||||
working-directory: e2e-tests/cypress
|
||||
run: npm ci
|
||||
- name: ci/pack-cypress-deps
|
||||
if: steps.probe.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/pack-cypress-deps
|
||||
with:
|
||||
fips: ${{ inputs.server_edition == 'fips' }}
|
||||
@@ -271,7 +304,9 @@ jobs:
|
||||
name: dispatch-run-${{ matrix.worker_index }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
needs: [prepare-run, dispatch-begin]
|
||||
# prep-deps is already an ancestor via dispatch-begin; naming it directly
|
||||
# makes its cache-hit output readable here.
|
||||
needs: [prepare-run, prep-deps, dispatch-begin]
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
@@ -316,9 +351,15 @@ jobs:
|
||||
uses: ./.github/actions/webapp-setup
|
||||
with:
|
||||
read-only: "true"
|
||||
# prep-deps built these; it is an ancestor of this job via dispatch-begin,
|
||||
# so the artifact is always present by now.
|
||||
# Exactly one of the next two steps runs, per prep-deps' probe. The warmed
|
||||
# cache is the normal path; the artifact exists only when the probe missed.
|
||||
- name: ci/restore-cypress-deps
|
||||
if: needs.prep-deps.outputs.cache-hit == 'true'
|
||||
uses: ./.github/actions/restore-cypress-deps
|
||||
with:
|
||||
fail-on-cache-miss: "true"
|
||||
- name: ci/reuse-cypress-deps
|
||||
if: needs.prep-deps.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/unpack-cypress-deps
|
||||
with:
|
||||
fips: ${{ inputs.server_edition == 'fips' }}
|
||||
|
||||
Reference in new issue
Block a user