Merge pull request 'ci(build): skip the cold build when the Cypress job will not run' (#470) from ci/faster-build-job into release/7.22
Reviewed-on: #470
This commit was merged in pull request #470.
This commit is contained in:
commit
f0769b46aa
3 files changed
+172
-47
No files matched your search
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# Decide whether this pull request can change the Cypress job's outcome.
|
||||
#
|
||||
# The Cypress job runs the mocked estate (sas/mocks) against the client, so a
|
||||
# pull request that touches none of what it exercises cannot change its
|
||||
# result: a sas/ service change is not deployed there, a documentation change
|
||||
# is not read, and neither is lighthouse.yaml. The build job decides once and
|
||||
# publishes the answer as a job output, so its gate on the cold production
|
||||
# build and the artifact upload cannot disagree with the job that downloads
|
||||
# them - re-deciding in the second job could see a moved base branch and ask
|
||||
# for an artifact the first job never built.
|
||||
#
|
||||
# Writes client=true|false to $GITHUB_OUTPUT when a decision can be made, and
|
||||
# prints the files it saw. It exits 0 WITHOUT writing anything when it cannot
|
||||
# decide (no base ref, a failed fetch, a failed diff): every caller gates on
|
||||
# `!= 'false'`, so an undecidable pull request runs everything. A CI change
|
||||
# whose worst case is "slow" is safe; one whose worst case is "silently tests
|
||||
# nothing" is not, and one whose worst case is "every pull request goes red
|
||||
# because a fetch failed" is not acceptable either.
|
||||
#
|
||||
# .gitea/scripts/ is in the set because the spec list (cypress-specs.sh) and
|
||||
# the memory gate (check-runner-memory.sh) live there: a pull request that
|
||||
# changes what the suite runs, or the gate that lets it run, has to run it.
|
||||
# build.yaml is in the set for the same reason.
|
||||
#
|
||||
# sas/package.json is in the set because the job installs the CLI from the
|
||||
# version it pins (`CLI_VERSION=$(node -p "...dependencies['@sasjs/cli']")`
|
||||
# in "Deploy mocked services"): bumping that pin changes the toolchain the
|
||||
# suite runs, even though no file under sas/mocks/ changes. The sas lockfile
|
||||
# is not in the set - the job installs that CLI globally by version string,
|
||||
# so the lockfile cannot change what it resolves.
|
||||
#
|
||||
# Usage: BASE_REF=<base branch> bash .gitea/scripts/cypress-changes.sh
|
||||
set -uo pipefail
|
||||
|
||||
# The paths the Cypress job reads, and nothing else. The tests in
|
||||
# tests/cypress-changes.test.sh exist because this list is the whole gate: a
|
||||
# dropped alternative would silently stop the suite running on a class of
|
||||
# change, and the only symptom would be a green job that finished sooner.
|
||||
PATHS='client/|sas/mocks/|sas/package\.json|\.gitea/scripts/|package-lock\.json|\.gitea/workflows/build\.yaml'
|
||||
|
||||
# --match < file-list reads newline-separated paths on stdin and prints
|
||||
# true/false, so the matcher can be exercised with fixed lists and no
|
||||
# repository. It is what the test above drives.
|
||||
if [ "${1:-}" = "--match" ]; then
|
||||
if grep -qE "^(${PATHS})"; then echo true; else echo false; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BASE="${BASE_REF:-}"
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "no base ref (not a pull request) - running the suite"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! git fetch --no-tags --depth=1 origin \
|
||||
"+refs/heads/${BASE}:refs/remotes/origin/${BASE}"; then
|
||||
echo "::warning::could not fetch ${BASE} - running the suite rather than deciding"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! CHANGED="$(git diff --name-only "refs/remotes/origin/${BASE}" HEAD)"; then
|
||||
echo "::warning::could not diff against ${BASE} - running the suite rather than deciding"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if grep -qE "^(${PATHS})" <<<"$CHANGED"; then
|
||||
echo "client=true" >>"${GITHUB_OUTPUT:-/dev/null}"
|
||||
echo "the Cypress job's inputs changed:"
|
||||
else
|
||||
echo "client=false" >>"${GITHUB_OUTPUT:-/dev/null}"
|
||||
echo "::notice::Cypress suite skipped - nothing under client/, sas/mocks/ or .gitea/scripts/ changed"
|
||||
echo "Skipping the Cypress suite - nothing it exercises changed:"
|
||||
fi
|
||||
printf '%s\n' "$CHANGED"
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# The path list in cypress-changes.sh is the whole gate: drop an alternative
|
||||
# and the Cypress suite silently stops running for a class of change, with a
|
||||
# green job that just finished sooner. These cases pin the list to what the
|
||||
# suite actually reads, and run in the build job on every pull request.
|
||||
#
|
||||
# Run by hand: bash .gitea/scripts/tests/cypress-changes.test.sh
|
||||
set -uo pipefail
|
||||
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
matcher="$here/../cypress-changes.sh"
|
||||
fails=0
|
||||
|
||||
check() { # <expected> <path>...
|
||||
local want="$1" got
|
||||
shift
|
||||
got=$(printf '%s\n' "$@" | bash "$matcher" --match)
|
||||
if [ "$got" != "$want" ]; then
|
||||
echo "FAIL: expected $want for: $*"
|
||||
echo " got: $got"
|
||||
fails=$((fails + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Everything the Cypress job reads: the client it drives, the mock estate it
|
||||
# deploys, the CLI pin it installs, the scripts that decide what it runs, and
|
||||
# the dependency/workflow files that change the first two.
|
||||
check true client/src/app/app.component.ts
|
||||
check true sas/mocks/sasjs/services/makedata.js
|
||||
check true sas/package.json
|
||||
check true .gitea/scripts/cypress-specs.sh
|
||||
check true package-lock.json
|
||||
check true .gitea/workflows/build.yaml
|
||||
|
||||
# Not inputs: a sas/ service the mock estate does not deploy, documentation,
|
||||
# the Lighthouse workflow, an unrelated directory whose name starts with
|
||||
# "client", and the sas lockfile (the job installs the CLI by version string,
|
||||
# so the lockfile cannot change what it resolves).
|
||||
check false sas/macros/mp_abort.sas
|
||||
check false AGENTS.md
|
||||
check false docs/readme.md
|
||||
check false .gitea/workflows/lighthouse.yaml
|
||||
check false clientx/foo.ts
|
||||
check false sas/package-lock.json
|
||||
|
||||
if [ "$fails" -ne 0 ]; then
|
||||
echo "cypress-changes matcher: $fails case(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "cypress-changes matcher: all cases passed"
|
||||
+47
-47
@@ -44,6 +44,13 @@ env:
|
||||
jobs:
|
||||
Build-and-ng-test:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
# The Cypress job consumes this instead of deciding again: the base
|
||||
# branch can move between the two jobs, and a second answer could say
|
||||
# "run" after this job had already skipped the build and the upload,
|
||||
# which would fail there as a missing artifact. Empty (the decision
|
||||
# could not be made) means the Cypress job runs everything.
|
||||
client: ${{ steps.changes.outputs.client }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -52,6 +59,21 @@ jobs:
|
||||
# Karma disconnect twenty minutes in. Fail here instead, in seconds,
|
||||
# with the measured numbers in the log - see the script.
|
||||
run: bash .gitea/scripts/check-runner-memory.sh
|
||||
- name: Detect the Cypress job's inputs
|
||||
id: changes
|
||||
# Decides whether the Cypress job will run at all, so the production
|
||||
# build and its artifact upload can be skipped when nothing the suite
|
||||
# exercises has changed. Same script the Cypress job itself calls, so
|
||||
# the two jobs cannot disagree about which paths matter.
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: bash .gitea/scripts/cypress-changes.sh
|
||||
- name: Test the changed-path matcher
|
||||
# The path list is the whole gate: a dropped alternative silently stops
|
||||
# the suite running for a class of change, and the only symptom is a
|
||||
# green job that finished sooner. The cases pin the list to what the
|
||||
# suite reads.
|
||||
run: bash .gitea/scripts/tests/cypress-changes.test.sh
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
@@ -101,9 +123,9 @@ jobs:
|
||||
# generator runs explicitly, because the app imports the file it makes
|
||||
npm ci --ignore-scripts
|
||||
npm run postinstall
|
||||
# cypress downloads its binary in a dependency install hook, which the
|
||||
# policy skips, so it is installed explicitly here
|
||||
npx cypress install
|
||||
# cypress's binary is installed in the Cypress job, where it is
|
||||
# actually used; nothing in this job starts it, so downloading and
|
||||
# unzipping it here only cost time (11s, measured).
|
||||
|
||||
- name: Check audit
|
||||
# Audit should fail and stop the CI on any vulnerability in root and sas, and on low+ in client
|
||||
@@ -170,11 +192,19 @@ jobs:
|
||||
run: bash .gitea/scripts/cypress-specs.sh coverage .
|
||||
|
||||
- name: Production Build
|
||||
# The build exists to give the Cypress job the client/dist it streams
|
||||
# as the mock estate's frontend. When nothing that job exercises has
|
||||
# changed, that job skips and nobody downloads the artifact, so the
|
||||
# cold build (109s, measured on run 3132) would be spent on no one.
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
run: |
|
||||
cd client
|
||||
npm run build
|
||||
|
||||
- name: Save the production build for the Cypress job
|
||||
# Gated with the build it packages: when the Cypress job skips, this
|
||||
# artifact has no consumer.
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
# 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,
|
||||
@@ -212,51 +242,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Detect client changes
|
||||
- name: Take the changed-path decision from the build job
|
||||
id: changes
|
||||
# This job runs the mocked estate (sas/mocks) against the client, so a
|
||||
# pull request that touches none of what it exercises cannot change its
|
||||
# outcome: a sas/ service change is not deployed here, a documentation
|
||||
# change is not read, and neither is .gitea/workflows/lighthouse.yaml.
|
||||
# Skipping saves the twenty-six minute suite plus its installs.
|
||||
#
|
||||
# The condition on every gated step is `!= 'false'`, not `== 'true'`, and
|
||||
# every failure below exits 0 without writing `client=false`: if the
|
||||
# decision cannot be made, the suite runs. A CI change whose worst case
|
||||
# is "slow" is safe; one whose worst case is "silently tests nothing" is
|
||||
# not, and one whose worst case is "every pull request goes red because a
|
||||
# fetch failed" is not acceptable either.
|
||||
# The build job made this decision before it built (or skipped) the
|
||||
# client-dist this job downloads, and published it as a job output.
|
||||
# Deciding again here could disagree if the base branch moved in
|
||||
# between, and the disagreement would surface as a missing artifact.
|
||||
# An empty value means that job could not decide, so the suite runs.
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
CLIENT: ${{ needs['Build-and-ng-test'].outputs.client }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE="${BASE_REF:-}"
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "no base ref (not a pull request) - running the suite"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! git fetch --no-tags --depth=1 origin "+refs/heads/${BASE}:refs/remotes/origin/${BASE}"; then
|
||||
echo "::warning::could not fetch ${BASE} - running the suite rather than deciding"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! CHANGED="$(git diff --name-only "refs/remotes/origin/${BASE}" HEAD)"; then
|
||||
echo "::warning::could not diff against ${BASE} - running the suite rather than deciding"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# .gitea/scripts/ is in the set because the suite's spec list
|
||||
# (cypress-specs.sh) and the memory gate (check-runner-memory.sh) live
|
||||
# there: a pull request that changes what the suite runs, or the gate
|
||||
# that lets it run, has to run it.
|
||||
if printf '%s\n' "$CHANGED" | grep -qE '^(client/|sas/mocks/|\.gitea/scripts/|package-lock\.json|\.gitea/workflows/build\.yaml)'; then
|
||||
echo "client=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "client=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Cypress suite skipped - nothing under client/, sas/mocks/ or .gitea/scripts/ changed"
|
||||
printf 'Skipping the Cypress suite - nothing it exercises changed:\n%s\n' "$CHANGED"
|
||||
fi
|
||||
case "${CLIENT:-}" in
|
||||
true) echo "client=true" >> "$GITHUB_OUTPUT" ;;
|
||||
false) echo "client=false" >> "$GITHUB_OUTPUT" ;;
|
||||
*) echo "the build job published no decision - running the suite" ;;
|
||||
esac
|
||||
|
||||
- name: Cypress suite not run - no client changes
|
||||
# A named step rather than a run of greyed-out ones, so the job summary
|
||||
@@ -267,8 +267,8 @@ jobs:
|
||||
# evidence for it.
|
||||
if: steps.changes.outputs.client == 'false'
|
||||
run: |
|
||||
echo "No client, sas/mocks or .gitea/scripts change in this pull request - the Cypress suite was skipped by design."
|
||||
echo "The detection step above lists the files it saw, and warns instead if it could not decide (in which case the suite runs)."
|
||||
echo "No client, sas/mocks, sas/package.json or .gitea/scripts change in this pull request - the Cypress suite was skipped by design."
|
||||
echo "The build job's 'Detect the Cypress job's inputs' step lists the files it saw, and warns instead if it could not decide (in which case the suite runs)."
|
||||
- name: Check runner size
|
||||
if: steps.changes.outputs.client != 'false'
|
||||
# Each Cypress job compiles the frontend and runs a browser on top; a
|
||||
|
||||
Reference in new issue
Block a user