Compare commits

...
2 Commits
Author SHA1 Message Date
allan b8eea5063c Merge pull request 'ci(build): run the Cypress suite in one job again, keeping the spec-list guard' (#394) from ci/unshard-cypress into release/7.20
Sites / deploy (push) Successful in 3m46s
Build / Build-and-ng-test (pull_request) Successful in 6m18s
Lighthouse Checks / lighthouse (pull_request) Successful in 19m57s
Build / Build-and-test-development (pull_request) Failing after 37m40s
Reviewed-on: #394
2026-10-28 19:38:08 +00:00
dc d84a1d50f4 ci(build): run the Cypress suite in one job again, keeping the spec-list guard
Build / Build-and-ng-test (pull_request) Successful in 6m49s
Lighthouse Checks / lighthouse (pull_request) Canceled after 1m17s
Build / Build-and-test-development (pull_request) Canceled after 0s
The four shards made the release-branch run red, and the reasons are in the
logs rather than in the theory:

  - every failure was a timeout, not an assertion - "cy.type() failed because
    the center of this element is hidden from view", "Expected
    <div.app-loading> not to exist ... continuously found".  Four jobs on one
    runner means four SASjs servers, four ng serve and four Chromes competing
    for the same CPU, and the app under test starves;
  - upload-artifact@v4 does not work on this runner at all:
    "GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+ ...
    not supported on GHES", so the v3 -> v4 move I made on review advice broke
    the artifact step outright.

Measured, from the one sharded run that did complete: 20m34s wall against 33-35
minutes for the single job, with peak concurrency 3 - so a third off, not the
four-fold the split suggests.  That is not worth a flaky pipeline while a release
sits on it.

Kept from the sharding work, because those parts were the real gain:

  - the spec list has one home, .gitea/scripts/cypress-specs.sh;
  - Build-and-ng-test fails when a spec on disk is not listed, so the five that
    had never run in CI cannot go missing again - and they do run now;
  - videos are zipped and uploaded only on failure;
  - one job means one server, which is also what the guards and the balance
    arguments were protecting.

The shard and fan-in jobs are gone with the branch that carried them.
2026-10-28 19:27:51 +00:00
3 changed files with 56 additions and 157 deletions

No files matched your search

-65
View File
@@ -1,65 +0,0 @@
#!/usr/bin/env bash
# Which Cypress specs run in which CI shard.
#
# One list, because the workflow used to spell the specs out in a single
# --spec string and five files quietly fell out of it - including the VA embed
# suite. The guard step in Build-and-ng-test compares this against the specs on
# disk and fails when a file belongs to no shard.
#
# Balance is on test count AND spec count: each spec pays a few seconds of
# startup, so seventeen one-test files cost more than their test count suggests.
set -euo pipefail
shard_specs() {
case "$1" in
1) echo "editor csv metanav" ;;
2) echo "excel filtering download browser-info excel-multi-load hook-programs" ;;
3) echo "embed-va viewbox licensing filter-panel liveness" ;;
4) echo "viewer-labels row-cell-limits derived-variables ddl-export coltype-delete-record submitted-details startupservice-launcher stage full-table-search csv-limited" ;;
*) echo "unknown shard: $1" >&2; return 1 ;;
esac
}
all_specs() {
local shard
for shard in 1 2 3 4; do shard_specs "$shard"; done
}
spec_paths() {
local name
for name in $1; do printf 'cypress/e2e/%s.cy.ts,' "$name"; done
}
case "${1:-}" in
list)
# comma separated, the form `cypress run --spec` takes
spec_paths "$(shard_specs "$2")" | sed 's/,$//'
;;
all)
spec_paths "$(all_specs)" | sed 's/,$//'
;;
coverage)
# every spec on disk must appear exactly once across the shards
on_disk=$(cd "$2/client" && ls cypress/e2e/*.cy.ts | xargs -n1 basename | sed 's/\.cy\.ts$//' | sort)
assigned=$(printf '%s\n' $(all_specs) | sort)
missing=$(comm -23 <(echo "$on_disk") <(echo "$assigned"))
duplicated=$(echo "$assigned" | uniq -d)
if [ -n "$missing" ]; then
echo "Specs that belong to no shard:"
echo "$missing"
fi
if [ -n "$duplicated" ]; then
echo "Specs assigned to more than one shard:"
echo "$duplicated"
fi
if [ -n "$missing$duplicated" ]; then
echo "Add them to a shard in .gitea/scripts/cypress-shards.sh"
exit 1
fi
echo "All $(echo "$on_disk" | wc -l) specs are assigned to exactly one shard."
;;
*)
echo "usage: $0 {list <shard>|all|coverage <repo-root>}" >&2
exit 2
;;
esac
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# The Cypress specs CI runs.
#
# One list, because the workflow used to spell the specs out in a single --spec
# string and five files quietly fell out of it - including the VA embed suite.
# `coverage` fails when a spec on disk is not listed here, so the next new spec
# cannot go missing in silence.
set -euo pipefail
SPECS="browser-info coltype-delete-record csv csv-limited ddl-export derived-variables download editor embed-va excel excel-multi-load filter-panel filtering full-table-search hook-programs licensing liveness metanav row-cell-limits stage startupservice-launcher submitted-details viewbox viewer-labels"
case "${1:-}" in
all)
# comma separated, the form `cypress run --spec` takes
for name in $SPECS; do printf 'cypress/e2e/%s.cy.ts,' "$name"; done | sed 's/,$//'
;;
coverage)
on_disk=$(cd "$2/client" && ls cypress/e2e/*.cy.ts | xargs -n1 basename | sed 's/\.cy\.ts$//' | sort)
listed=$(printf '%s\n' $SPECS | sort)
missing=$(comm -23 <(echo "$on_disk") <(echo "$listed"))
if [ -n "$missing" ]; then
echo "Specs that are not in the list:"
echo "$missing"
echo "Add them to SPECS in .gitea/scripts/cypress-specs.sh"
exit 1
fi
echo "All $(echo "$on_disk" | wc -l) specs are listed."
;;
*)
echo "usage: $0 {all|coverage <repo-root>}" >&2
exit 2
;;
esac
+23 -92
View File
@@ -85,11 +85,11 @@ jobs:
npm run test:tz-west
npm run test:tz-east
- name: Every Cypress spec belongs to a shard
# The spec lists used to be spelled out in one --spec string, and five
# files quietly fell out of it - among them the VA embed suite. Fails
# when a spec on disk appears in no shard, or in more than one.
run: bash .gitea/scripts/cypress-shards.sh coverage .
- name: Every Cypress spec is in the CI list
# The spec list used to be spelled out inline in --spec, and five files
# quietly fell out of it - among them the VA embed suite. Fails when a
# spec on disk is not listed.
run: bash .gitea/scripts/cypress-specs.sh coverage .
- name: Production Build
run: |
@@ -100,8 +100,9 @@ jobs:
# 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-shards.sh.
cypress-shard-1:
# .gitea/scripts/cypress-specs.sh.
Build-and-test-development:
runs-on: ubuntu-latest
needs: Build-and-ng-test
env:
@@ -109,9 +110,8 @@ jobs:
# Pin OS locale
LANG: en_GB.UTF-8
LC_ALL: en_GB.UTF-8
SHARD: '1'
steps: &dev-steps
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
@@ -204,17 +204,20 @@ 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
# 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.
timeout-minutes: 50
run: |
cd ./client
# The spec list lives in one place, and a shard that resolves to nothing
# is a broken workflow rather than a green run with no tests.
SPECS="$(bash ../.gitea/scripts/cypress-shards.sh list "$SHARD")"
# The spec list lives in one place, and an empty list is a broken
# workflow rather than a green run with no tests.
SPECS="$(bash ../.gitea/scripts/cypress-specs.sh all)"
if [ -z "$SPECS" ]; then
echo "shard $SHARD resolved to no specs"
echo "the spec list resolved to nothing"
exit 1
fi
echo "shard $SHARD: $SPECS"
echo "specs: $SPECS"
mv ./cypress.env.example.json ./cypress.env.json
replace-in-files --regex='"username".*' --replacement='"username":"'${{ secrets.CYPRESS_USERNAME_SASJS }}'",' ./cypress.env.json
replace-in-files --regex='"password".*' --replacement='"password":"'${{ secrets.CYPRESS_PWD_SASJS }}'" ' ./cypress.env.json
@@ -226,7 +229,7 @@ jobs:
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts
# Start frontend and run this shard's specs
# Start frontend and run cypress
# timeout 2700: SIGTERM after 45 min so Cypress can flush video/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"
@@ -242,82 +245,10 @@ jobs:
- name: Add cypress videos artifacts
if: failure()
uses: actions/upload-artifact@v4
# 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-${{ env.SHARD }}
name: cypress-videos.zip
path: cypress-videos.zip
# Cypress shard 2 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-shards.sh.
cypress-shard-2:
runs-on: ubuntu-latest
needs: Build-and-ng-test
env:
CHROME_BIN: /usr/bin/google-chrome
# Pin OS locale
LANG: en_GB.UTF-8
LC_ALL: en_GB.UTF-8
SHARD: '2'
steps: *dev-steps
# Cypress shard 3 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-shards.sh.
cypress-shard-3:
runs-on: ubuntu-latest
needs: Build-and-ng-test
env:
CHROME_BIN: /usr/bin/google-chrome
# Pin OS locale
LANG: en_GB.UTF-8
LC_ALL: en_GB.UTF-8
SHARD: '3'
steps: *dev-steps
# Cypress shard 4 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-shards.sh.
cypress-shard-4:
runs-on: ubuntu-latest
needs: Build-and-ng-test
env:
CHROME_BIN: /usr/bin/google-chrome
# Pin OS locale
LANG: en_GB.UTF-8
LC_ALL: en_GB.UTF-8
SHARD: '4'
steps: *dev-steps
# The status check that branch protection requires keeps its old name, but it
# is produced here now, so a red shard cannot hide behind a green one.
#
# if: always() is what makes that true: without it a failed dependency skips
# this job, and a skipped job is not a failure on the one check that is
# watched - the gate would be silent in exactly the case it exists for.
Build-and-test-development:
runs-on: ubuntu-latest
needs: [cypress-shard-1, cypress-shard-2, cypress-shard-3, cypress-shard-4]
if: always()
steps:
- name: Every Cypress shard passed
run: |
results="${{ needs.cypress-shard-1.result }} ${{ needs.cypress-shard-2.result }} ${{ needs.cypress-shard-3.result }} ${{ needs.cypress-shard-4.result }}"
echo "shard results: $results"
set -- $results
if [ "$#" -ne 4 ]; then
echo "expected 4 shard results, got $# - the needs context did not resolve" >&2
exit 1
fi
for result in "$@"; do
if [ "$result" != "success" ]; then
echo "a Cypress shard did not succeed: $results" >&2
exit 1
fi
done
echo "all 4 shards passed"