Files
dc/.gitea/workflows/build.yaml
T
dc e1c3c6e872
Lighthouse Checks / lighthouse (pull_request) Successful in 9m45s
Build / Build-and-ng-test (pull_request) Failing after 15m6s
Build / Build-and-test-development (pull_request) Skipped
fix(mocks): answer the deploy screen's DIRLIST and WRITECHECK actions
Staging now opens on the deploy screen, and its folder picker asks
makedata for a listing.  The mock ignored ACTION entirely: every request
seeded the control library, deleted makedata from the drive and replied
with the HTML success page.  So the first BROWSE deployed the site and
closed the wizard the visitor was standing on, behind an "invalid Json
string" error from the adapter, and VALIDATE and SUBMIT then answered
500 "Program not found in drive".

The mock now dispatches on ACTION the way the SAS service does:

  DIRLIST     the folders below PARENT, one level at a time, with the
              dirinfo/dirlist shape mpe_dirlist emits
  WRITECHECK  the checkwrite row mpe_writecheck emits, probed by creating
              and removing a file
  MAKEDATA    the existing seeding path, unchanged

ACTION is required and refused when absent or unknown, and the two browse
actions are refused once the settings file exists, mirroring browse_gate.
Both input routes are accepted - the screen's fromjs table and a CLI
seed's SASControlTable - because %webout(FETCH) does not care which one
arrived; deploy/makedata.json now carries ACTION=MAKEDATA as the real
service requires.

sas/mocks/test/makedata-actions.test.js runs the service file itself
against a temporary estate and is wired into the build workflow.
2026-11-04 14:59:19 +00:00

474 lines
22 KiB
YAML

name: Build
run-name: Running Lint Check and Licence checker on Pull Request
on: [pull_request]
# One run per pull request: a newer push supersedes the one in flight for the
# older head. Both runs test the same thing over the same twenty-six minute
# suite, the older head can no longer be what gets merged, and this workflow
# shares two runners with every other pull request in the repo - so leaving the
# old run queued only delays the head that matters. The group is the base build
# only: the release workflow is never cancelled by something it does not
# supersede.
concurrency:
group: build-pr-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
# the server the mocked services and app tests run against - bump deliberately.
# 3.12.0 is the floor: /ddc-host carries `timing=early` from there, which
# client/cypress/e2e/va-ddc-host.cy.ts needs. 3.12.2 also carries the
# packaged-executable fix for the same page, so the version the tests run
# against is the one that ships.
#
# 3.14.0 ships @sasjs/lint 5.0.0 (the studio reports OS commands) and is the
# release the estate runs. It still declares @sasjs/utils ^4.7.0, so the
# envelope *formats* (SYSCC as text, DATE7, TIME5, E8601DT26.6 from utils 4.8)
# are only guaranteed once the next server release raises that floor - a fresh
# install resolves 4.8.0 today, a cached tree need not. Bump again when it lands.
#
# 3.13.0 is the one that matters for licensing: @sasjs/utils 4.7 reports
# SYSSITE, which %webout takes from &syssite. Before it the prelude wrote an
# empty string, so the client - which routes expired and wrong-organisation
# licence keys on that value - never reached those pages on the mock estate.
#
# 3.12.3 is on top of that for a different reason: it is the first release whose
# package.json declares @sasjs/utils ^4.6.0, which is what gives webout() its
# options argument. getdata, viewdata and getcolvals pass { showmeta }, and a
# 4.5 install drops it without a word - so the pin is what makes the metadata
# members guaranteed here rather than whatever a fresh install happens to
# resolve. requireWeboutOptions in sasjs/mocks/sasjs/services/dcMockUtils.js
# refuses a runtime without it.
SASJS_SERVER_VERSION: '3.14.0'
NODE_VERSION: '24.15.0'
jobs:
Build-and-ng-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check runner size
# An undersized runner dies inside `ng build` (exitcode 137) or as a
# 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
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Scan for secrets (gitleaks)
# The local pre-commit hook is suppressed entirely when .npmrc sets
# ignore-scripts=true (the prepare script that would activate the
# hooks never runs), so CI is the enforcement point: a leaked
# credential must fail the build, not just a commit.
# Uses the same @nogoo9/gitleaks binary the pre-commit hook pins - the
# gitleaks-action requires a license key for organization repos.
# Runs before the registry config is written, so the scan cannot see
# the token this job injects.
# --no-git scans the working tree, which is what a pull request gate
# is about.
run: |
npm ci
node_modules/.bin/gitleaks detect --no-git --source . --redact --no-banner -v
- name: Install Google Chrome
run: |
apt-get update
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome*.deb
- name: Write .npmrc file
run: |
# the registry secret stays outside the repo: client/.npmrc is
# tracked now, and one git add -A would commit a live token
echo "$NPMRC" > "$RUNNER_TEMP/npmrc"
echo "NPM_CONFIG_USERCONFIG=$RUNNER_TEMP/npmrc" >> "$GITHUB_ENV"
shell: bash
env:
NPMRC: ${{ secrets.NPMRC}}
- name: Install dependencies
env:
SHEET_PWD: ${{ secrets.SHEET_PWD }}
run: |
cd client
# Decrypt and Install sheet
printf '%s\n' "$SHEET_PWD" | \
gpg --batch --yes --passphrase-fd 0 \
--output ./libraries/sheet-crypto.tgz \
--decrypt ./libraries/sheet-crypto.tgz.gpg
# dependency hooks stay off (client/.npmrc); the client's own
# 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
- name: Check audit
# Audit should fail and stop the CI on any vulnerability in root and sas, and on low+ in client
run: |
npm audit --omit=dev
cd ./sas
npm audit --omit=dev
cd ../client
npm audit --omit=dev
- name: Check archiver directory walk
# the readdir-glob minimatch override in client/package.json is a major
# bump; archiver's directory() is the only runtime caller of that path,
# so exercise it rather than trusting that the install succeeded
run: |
cd client
npm run check:archiver
- name: Lint check
run: npm run lint:check
- name: SAS lint check
# sas/.sasjslint promotes each rule to "error" as its findings are
# cleared, and only errors change the exit code - so this gate fails on
# a regression in a cleared rule and passes while other rules still
# carry warnings.
run: |
cd sas
npm ci --ignore-scripts
npm run lint:sas
- name: Licence checker
run: |
cd client
npm run license-checker
- name: Mock estate service tests
# The deploy screen's questions (ACTION=DIRLIST / WRITECHECK) are
# answered by the mock makedata service on the browser-hosted sites.
# These run that service against a temporary estate, so a browse that
# seeds the site and deletes the program fails here instead of on the
# staging site, where it only shows as a wizard that cannot be used.
run: node --test sas/mocks/test/*.test.js
- name: Angular Tests
run: |
cd client
npm run test:headless
- name: Angular Tests in other timezones
# The VA filter clauses are built from local calendar dates, so a
# UTC-only run cannot see a date that slips a day west of UTC or east of
# it. Only the VA specs carry that property, so both scripts run that
# subset rather than the whole suite a second and third time.
run: |
cd client
npm run test:tz-west
npm run test:tz-east
- 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: |
cd client
npm run build
- 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.
#
# if-no-files-found: error, because an empty artifact is worse than a
# failed upload: the Cypress job would download nothing, deploy a service
# pack with no frontend, and the symptom would reappear there as the
# "webSourcePath ... doesn't exist" line this artefact exists to remove.
#
# 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 - the download in
# Build-and-test-development is @v3 for the same reason.
uses: actions/upload-artifact@v3
with:
name: client-dist
path: client/dist
if-no-files-found: error
- name: Remove registry config
# written earlier in this job; nothing deletes it on the way out
if: always()
run: rm -f "$RUNNER_TEMP/npmrc"
Build-and-test-development:
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
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 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.
env:
BASE_REF: ${{ github.base_ref }}
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
- name: Cypress suite not run - no client changes
# A named step rather than a run of greyed-out ones, so the job summary
# says why it took a minute: this job reports success having tested
# nothing, which is the intended behaviour for a pull request that
# changes no client code. If this job is a required check, "green" for
# such a pull request means "not applicable", and this step is the
# 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)."
- 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
# step or execute
printf '%s\n' "$NPMRC" > "$RUNNER_TEMP/npmrc"
echo "NPM_CONFIG_USERCONFIG=$RUNNER_TEMP/npmrc" >> "$GITHUB_ENV"
env:
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
apt install -y ./google-chrome*.deb
apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip locales
# Generate the en_GB.UTF-8 locale referenced by LANG/LC_ALL
locale-gen en_GB.UTF-8
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'
env:
SHEET_PWD: ${{ secrets.SHEET_PWD }}
run: |
cd client
# Decrypt and Install sheet
printf '%s\n' "$SHEET_PWD" | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
# dependency hooks stay off (client/.npmrc); the client's own
# 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
- 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. 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.
# continue-on-error, because a runner without cache support should still
# run the pipeline - this step only ever saves time.
continue-on-error: true
uses: actions/cache@v4
with:
path: ~/.npm
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@7.1.2
touch .env
echo RUN_TIMES=js >> .env
echo NODE_PATH=node >> .env
echo CORS=enable >> .env
echo WHITELIST=http://localhost:4200 >> .env
echo HTTP_PORT=5000 >> .env
# /ddc-host is a testing tool, so the server answers 404 for it unless
# this is set. va-ddc-host.cy.ts drives the app through that page with
# timing=early.
echo DDC_HOST_ENABLED=true >> .env
# The server refuses to run as root unless RUN_AS names an account
# (a security change in the recent releases). This job runs as root
# in a throwaway container, so opt in explicitly.
echo RUN_AS=root >> .env
cat .env
pm2 start npx --name sasjs-server -- --yes @sasjs/server@${{ env.SASJS_SERVER_VERSION }}
# `pm2 start` returns before the app is listening, and the next step
# hits :5000 immediately. Wait for the server to answer, and dump its
# log if it never does.
for i in $(seq 1 60); do
if curl -sf -o /dev/null http://localhost:5000/; then
echo "SASjs server answered on attempt ${i}"
exit 0
fi
sleep 2
done
echo "SASjs server did not come up within 120s"
pm2 list
pm2 logs sasjs-server --lines 100 --nostream
exit 1
- name: Deploy mocked services
if: steps.changes.outputs.client != 'false'
run: |
cd ./sas/mocks/sasjs
# The CLI comes from the version sas/package.json pins, as docs.yaml and
# release-assets.yaml already do: an unpinned global install runs
# whatever release is current, unreviewed.
CLI_VERSION=$(node -p "require('../../package.json').dependencies['@sasjs/cli']")
npm install -g "@sasjs/cli@${CLI_VERSION}"
npm install -g replace-in-files-cli@4.0.0
# Remove any previous deployment (drive folder delete API) so the
# deploy and makedata start from a clean appLoc
curl -sS -X DELETE "http://localhost:5000/SASjsApi/drive/folder/?_folderPath=/Public/app/dc"
sasjs cbd -t server-ci
# Seed the DC database (mock data files on the drive).
# makedata replies with HTML (it is normally called as a URL redirect),
# which the CLI reports as "invalid Json string" - ignore that.
# NOTE: -d paths resolve from the sasjs project root (sas/mocks),
# not the cwd (sas/mocks/sasjs).
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.
timeout-minutes: 50
env:
CYPRESS_USERNAME: ${{ secrets.CYPRESS_USERNAME_SASJS }}
CYPRESS_PWD: ${{ secrets.CYPRESS_PWD_SASJS }}
run: |
cd ./client
# 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 "the spec list resolved to nothing"
exit 1
fi
echo "specs: $SPECS"
mv ./cypress.env.example.json ./cypress.env.json
replace-in-files --regex='"username".*' --replacement='"username":"'"$CYPRESS_USERNAME"'",' ./cypress.env.json
replace-in-files --regex='"password".*' --replacement='"password":"'"$CYPRESS_PWD"'" ' ./cypress.env.json
cat ./cypress.env.json
npm run postinstall
# Prepare index.html to SASJS local
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/dc"' ./src/index.html
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 cypress
# 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)
#
# 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 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/screenshots ./client/cypress/videos
zip -r cypress-artifacts.zip ./client/cypress/screenshots ./client/cypress/videos
- 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-artifacts.zip
path: cypress-artifacts.zip
- name: Remove registry config
# written earlier in this job; nothing deletes it on the way out
if: always()
run: rm -f "$RUNNER_TEMP/npmrc"