Compare commits

...
Author SHA1 Message Date
dc 3aae36f94d ci: stop paying for the same build, run and audit three times per PR
Build / Build-and-ng-test (pull_request) Successful in 6m43s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m58s
Build / Build-and-test-development (pull_request) Successful in 27m23s
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.
2026-11-03 14:03:58 +00:00
3 changed files with 122 additions and 25 deletions

No files matched your search

+75 -23
View File
@@ -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
+42 -1
View File
@@ -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
+5 -1
View File
@@ -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',