ci: stop paying for the same build, run and audit three times per PR
Measured over the last 150 workflow runs on this forge: a pull request costs 56 runner-minutes (Build-and-ng-test 5.8m, Build-and-test-development 31.2m, lighthouse 19.3m), and when three pull requests are in flight the first job queues 47-69 minutes before it starts. - Build-and-ng-test publishes client/dist as an artifact and the Cypress job downloads it instead of running a second cold `npm run build` (1.6m each). Both jobs test the same bytes. - lighthouse: numberOfRuns 1 (lhci's default of 3 tripled the 13.6m audit to steady a median that only has to clear a fixed threshold), and the duplicate /view/usernav/groups entry is removed (17 unique URLs, not 18). - Cypress records no video in CI and takes screenshots on failure instead (`--config video=false,screenshotOnRunFailure=true`, CLI only so local runs and the demo recorder still record); the failure artifact carries screenshots as well as any video. - Both workflows detect whether client/ or sas/mocks/ changed and skip their expensive steps when neither did - a sas-only or docs-only pull request no longer pays 45 minutes for suites that cannot observe it. The gate is `!= 'false'`, so a missing step output runs the suite rather than quietly reporting green.
This commit is contained in:
1 parent
f24a02c47e
commit
3aae36f94d
3 files changed
+122
-25
No files matched your search
+75
-23
@@ -130,11 +130,16 @@ jobs:
|
||||
cd client
|
||||
npm run build
|
||||
|
||||
|
||||
# Cypress shard 1 of 4. The setup repeats in every shard because each
|
||||
# one gets its own runner and its own SASjs server; that fixed cost is what
|
||||
# limits how far sharding helps. The specs it runs come from
|
||||
# .gitea/scripts/cypress-specs.sh.
|
||||
- name: Save the production build for the Cypress job
|
||||
# The Cypress job needs client/dist: the server-ci target streams it as
|
||||
# the 'clickme' web app (see CONTRIBUTING.md). Handing it the build this
|
||||
# job has already made saves a second cold `npm run build` (1.6 minutes,
|
||||
# measured over thirteen runs) and guarantees both jobs test the same
|
||||
# bytes.
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: client-dist
|
||||
path: client/dist
|
||||
|
||||
- name: Remove registry config
|
||||
# written earlier in this job; nothing deletes it on the way out
|
||||
@@ -152,16 +157,46 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Detect client changes
|
||||
id: changes
|
||||
# This job runs the mocked estate (sas/mocks) against the client, so a
|
||||
# pull request that touches neither client/ nor sas/mocks cannot change
|
||||
# its outcome: a sas/ service change is not deployed here, and neither
|
||||
# is a documentation change. Skipping saves the twenty-six minute suite
|
||||
# plus its installs on those pull requests.
|
||||
#
|
||||
# The condition on every gated step is `!= 'false'`, not `== 'true'`: if
|
||||
# this step ever fails to publish its output the suite still runs. A CI
|
||||
# change whose worst case is "slow" is safe; one whose worst case is
|
||||
# "silently tests nothing" is not.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE="${{ github.base_ref }}"
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "no base ref (not a pull request) - running the suite"
|
||||
exit 0
|
||||
fi
|
||||
git fetch --no-tags --depth=1 origin "+refs/heads/${BASE}:refs/remotes/origin/${BASE}"
|
||||
CHANGED="$(git diff --name-only "refs/remotes/origin/${BASE}" HEAD)"
|
||||
if printf '%s\n' "$CHANGED" | grep -qE '^(client/|sas/mocks/|package-lock\.json|\.gitea/workflows/build\.yaml)'; then
|
||||
echo "client=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "client=false" >> "$GITHUB_OUTPUT"
|
||||
printf 'Skipping the Cypress suite - nothing under client/ or sas/mocks/ changed:\n%s\n' "$CHANGED"
|
||||
fi
|
||||
- name: Check runner size
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
# Each Cypress job compiles the frontend and runs a browser on top; a
|
||||
# runner too small for that fails as a flaky-looking disconnect - see
|
||||
# the script.
|
||||
run: bash .gitea/scripts/check-runner-memory.sh
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Write .npmrc file
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
# the secret arrives through the environment, not interpolated into
|
||||
# this script: a quote or $( in the value would otherwise break the
|
||||
@@ -172,6 +207,7 @@ jobs:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- name: Install system dependencies
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
apt-get update
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
@@ -182,12 +218,14 @@ jobs:
|
||||
update-locale LANG=en_GB.UTF-8 LC_ALL=en_GB.UTF-8
|
||||
|
||||
- name: Write cypress credentials
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||
shell: bash
|
||||
env:
|
||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
cd client
|
||||
# Decrypt and Install sheet
|
||||
@@ -200,16 +238,21 @@ jobs:
|
||||
# policy skips, so it is installed explicitly here
|
||||
npx cypress install
|
||||
|
||||
- name: Build frontend for web streaming
|
||||
- name: Fetch the production build
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
# The server-ci target streams client/dist as the 'clickme' web app (see
|
||||
# CONTRIBUTING.md), so the production build has to exist before the mock
|
||||
# services are deployed. Without it the deploy logs
|
||||
# "webSourcePath: .../client/dist ... doesn't exist" and streams no frontend.
|
||||
run: |
|
||||
cd client
|
||||
npm run build
|
||||
# "webSourcePath: .../client/dist ... doesn't exist" and streams no
|
||||
# frontend. It is the artefact Build-and-ng-test published, so this job
|
||||
# does not compile the frontend a second time.
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: client-dist
|
||||
path: client/dist
|
||||
|
||||
- name: Cache the npm downloads
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
# The server is installed from npm at a pinned version, so the tarball is
|
||||
# immutable and the key never needs invalidating by hand: a hit skips
|
||||
# the 10.7MB download, a miss costs exactly what the zip cost before.
|
||||
@@ -222,6 +265,7 @@ jobs:
|
||||
key: npm-sasjs-server-${{ env.SASJS_SERVER_VERSION }}
|
||||
|
||||
- name: Setup and start SASjs server
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
npm i -g pm2
|
||||
touch .env
|
||||
@@ -252,6 +296,7 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Deploy mocked services
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
cd ./sas/mocks/sasjs
|
||||
npm install -g @sasjs/cli
|
||||
@@ -268,6 +313,7 @@ jobs:
|
||||
sasjs request services/admin/makedata -t server-ci -d deploy/makedata.json -o ./makedata_out.json || true
|
||||
|
||||
- name: Prepare and run frontend and cypress
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
# One job, one SASjs server, one ng serve, one Chrome: four of each on one
|
||||
# runner starved the app under test and every failure in that run was a
|
||||
# timeout, not an assertion.
|
||||
@@ -294,28 +340,34 @@ jobs:
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
# timeout 2700: SIGTERM after 45 min so Cypress can flush video/screenshots
|
||||
# timeout 2700: SIGTERM after 45 min so Cypress can flush screenshots
|
||||
# before the outer timeout-minutes hard-kills the step (avoids silent multi-hour hangs)
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 2700 npx cypress run --browser chrome --spec "$SPECS"
|
||||
#
|
||||
# video=false, screenshotOnRunFailure=true: recording twenty-four
|
||||
# specs costs encode time on every run, and the recording is only ever
|
||||
# opened after a failure. A screenshot of the failing step carries the
|
||||
# same information for a fraction of the cost - and cypress.config.ts
|
||||
# sets screenshotOnRunFailure false by default, so both are passed
|
||||
# here, as CLI overrides, leaving local runs (and the demo recorder,
|
||||
# which sets RECORD_WINDOW_SIZE) on video.
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 2700 npx cypress run --browser chrome --config video=false,screenshotOnRunFailure=true --spec "$SPECS"
|
||||
|
||||
- name: Zip Cypress videos
|
||||
# Only on a failure: a green run has nothing to watch, and zipping and
|
||||
# uploading every run's videos is the part of the video cost that lands
|
||||
# on the clock.
|
||||
if: failure()
|
||||
- name: Zip Cypress failure evidence
|
||||
# Only on a failure: a green run has nothing to look at.
|
||||
if: failure() && steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
mkdir -p ./client/cypress/videos
|
||||
zip -r cypress-videos ./client/cypress/videos
|
||||
mkdir -p ./client/cypress/screenshots ./client/cypress/videos
|
||||
zip -r cypress-artifacts.zip ./client/cypress/screenshots ./client/cypress/videos
|
||||
|
||||
- name: Add cypress videos artifacts
|
||||
if: failure()
|
||||
- name: Add cypress failure evidence as an artifact
|
||||
if: failure() && steps.changes.outputs.client != 'false'
|
||||
# v3, not v4: this runner reports "GHESNotSupportedError:
|
||||
# @actions/artifact v2.0.0+, upload-artifact@v4+ ... not supported on
|
||||
# GHES", so the current major cannot upload here at all.
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-videos.zip
|
||||
path: cypress-videos.zip
|
||||
name: cypress-artifacts.zip
|
||||
path: cypress-artifacts.zip
|
||||
|
||||
- name: Remove registry config
|
||||
# written earlier in this job; nothing deletes it on the way out
|
||||
|
||||
@@ -14,27 +14,55 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Detect client changes
|
||||
id: changes
|
||||
# The audit measures the client, served from the mocked estate. A pull
|
||||
# request that changes neither cannot move a score, so it does not need
|
||||
# nineteen minutes of runner (measured median) to prove it. As in
|
||||
# build.yaml the gate is `!= 'false'`, so a missing output runs the job
|
||||
# rather than silently passing it.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE="${{ github.base_ref }}"
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "no base ref (not a pull request) - running the audit"
|
||||
exit 0
|
||||
fi
|
||||
git fetch --no-tags --depth=1 origin "+refs/heads/${BASE}:refs/remotes/origin/${BASE}"
|
||||
CHANGED="$(git diff --name-only "refs/remotes/origin/${BASE}" HEAD)"
|
||||
if printf '%s\n' "$CHANGED" | grep -qE '^(client/|sas/mocks/|package-lock\.json|\.gitea/workflows/lighthouse\.yaml)'; then
|
||||
echo "client=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "client=false" >> "$GITHUB_OUTPUT"
|
||||
printf 'Skipping the audit - nothing under client/ or sas/mocks/ changed:\n%s\n' "$CHANGED"
|
||||
fi
|
||||
|
||||
- name: Check runner size
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
# This job runs a production build, then a SASjs server, a dev server
|
||||
# and Chrome. An undersized runner dies inside the build with exitcode
|
||||
# 137; fail here in seconds instead - see the script.
|
||||
run: bash .gitea/scripts/check-runner-memory.sh
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Google Chrome
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
apt-get update
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
apt install -y ./google-chrome*.deb
|
||||
|
||||
- name: Install global packages
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: npm i -g pm2 @sasjs/cli wait-on
|
||||
|
||||
- name: Cache the npm downloads
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
# The server is installed from npm at a pinned version, so the tarball is
|
||||
# immutable and the key never needs invalidating by hand: a hit skips
|
||||
# the 10.7MB download, a miss costs exactly what the zip cost before.
|
||||
@@ -47,6 +75,7 @@ jobs:
|
||||
key: npm-sasjs-server-${{ env.SASJS_SERVER_VERSION }}
|
||||
|
||||
- name: Setup and start SASjs server
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
touch .env
|
||||
echo RUN_TIMES=js >> .env
|
||||
@@ -76,6 +105,7 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Write .npmrc file
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
# the registry secret stays outside the repo: client/.npmrc is
|
||||
# tracked now, and one git add -A would commit a live token
|
||||
@@ -86,6 +116,7 @@ jobs:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- name: Install npm dependencies
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
cd client
|
||||
# Decrypt and Install sheet
|
||||
@@ -100,34 +131,44 @@ jobs:
|
||||
npm install -g replace-in-files-cli
|
||||
|
||||
- name: Update appLoc in index.html
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
cd client
|
||||
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/proj/sasjs/genesis-mocks"' ./src/index.html
|
||||
|
||||
- name: Build Frontend
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
cd client
|
||||
npm run build
|
||||
|
||||
- name: Deploy JS mocked services and frontend to the local SASjs Server instance
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
cd sas/mocks
|
||||
npm ci
|
||||
sasjs cbd -t server-ci
|
||||
|
||||
- name: Start frontend server
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
cd client
|
||||
npx ng serve --host 0.0.0.0 --port 4200 &
|
||||
wait-on http://localhost:4200
|
||||
|
||||
- name: Run Lighthouse CI
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
# One run per URL: the assertions aggregate by median, so three runs
|
||||
# bought a steadier median at three times the navigations (13.6 minutes
|
||||
# of this job, measured). A pull request gate only needs to know the
|
||||
# page still clears the thresholds it cleared before; a regression that
|
||||
# only shows in a median of three is a flake to chase separately.
|
||||
run: |
|
||||
cd client
|
||||
npx lhci autorun
|
||||
|
||||
- name: Lighthouse Result Artifacts
|
||||
if: always()
|
||||
if: always() && steps.changes.outputs.client != 'false'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Lighthouse results
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
module.exports = {
|
||||
ci: {
|
||||
collect: {
|
||||
// One pass per URL. lhci defaults to three and takes the median, which
|
||||
// triples the wall clock of the audit (13.6 minutes measured across
|
||||
// seventeen runs) to steady a number that only has to clear a fixed
|
||||
// threshold. Raise this if a score starts flapping.
|
||||
numberOfRuns: 1,
|
||||
settings: {
|
||||
preset: 'desktop',
|
||||
chromeFlags: '--no-sandbox --disable-dev-shm-usage'
|
||||
@@ -12,7 +17,6 @@ module.exports = {
|
||||
'http://localhost:5000/AppStream/clickme/#/view/data/DC996664',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/data/DC996664.MPE_X_TEST',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups/1',
|
||||
'http://localhost:5000/AppStream/clickme/#/view/usernav/users/1',
|
||||
'http://localhost:5000/AppStream/clickme/#/home/excel-maps',
|
||||
|
||||
Reference in new issue
Block a user