Follow-ups from the review of #414. The credential guard now checks each staged .npmrc rather than only the client one: the root and sas/ files are tracked policy as well, so a token pasted into either was equally one `git add -A` away. Verified by staging a token in sas/.npmrc - the hook exits 1. $RUNNER_TEMP/npmrc is removed at the end of each job that writes it, with `if: always()` so a failed job cleans up too. The root `install` script is gone: the root .npmrc sets ignore-scripts, so npm never ran it. CONTRIBUTING already spells out the per-project commands, and is now the single source. The release workflows build the published client from the same tree, so they run `check:archiver` like build.yaml does.
209 lines
8.9 KiB
YAML
209 lines
8.9 KiB
YAML
name: Sites
|
|
on:
|
|
push:
|
|
branches:
|
|
- 'release/**'
|
|
tags:
|
|
- 'v*'
|
|
paths-ignore:
|
|
- '**.md'
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
# the server the mocked services and app tests run against - bump deliberately
|
|
SASJS_SERVER_VERSION: '3.6.2'
|
|
NODE_VERSION: '24.15.0'
|
|
APP_LOC: /Public/app/dc
|
|
SURFER_HOST: https://demo.datacontroller.io
|
|
# A release branch publishes the staging site (a named site on the instance,
|
|
# served at its alias domain); a release tag publishes the demo, which is the
|
|
# default site and so needs no site name.
|
|
STAGING_SITE: staging
|
|
STAGING_DOMAIN: staging.datacontroller.io
|
|
|
|
# One deploy per target site: a newer push supersedes the one in flight, and two
|
|
# release branches (which share the staging site) cannot interleave their swaps.
|
|
# The demo and staging deploys are independent, so they do not queue.
|
|
concurrency:
|
|
group: surfer-${{ startsWith(gitea.ref, 'refs/tags/') && 'demo' || 'staging' }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
# The tag is what the client's build stamp describes (src/version.ts
|
|
# runs git-describe), so the demo build needs the tags in the clone.
|
|
fetch-depth: 0
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Write .npmrc file
|
|
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 client dependencies
|
|
run: |
|
|
cd client
|
|
# Decrypt and Install sheet
|
|
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
|
npm ci
|
|
# client install scripts are off (client/.npmrc); the generator that
|
|
# creates src/environments/version.ts runs explicitly
|
|
npm run postinstall
|
|
|
|
- name: Build frontend
|
|
# The site serves this build. `sasjs build` rewrites its index.html to
|
|
# point the adapter at the in-browser backend.
|
|
run: |
|
|
cd client
|
|
npm run postinstall
|
|
npm run build
|
|
|
|
- name: Install the SASjs CLI
|
|
# Pinned in sas/package.json - the browser build needs CLI >= 6.1.0
|
|
run: |
|
|
cd sas
|
|
npm ci
|
|
|
|
- name: Cache the npm downloads
|
|
# 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: Start SASjs server
|
|
# Needed only to run makedata: the drive the site ships with is captured
|
|
# from a real server (see the snapshot step below).
|
|
run: |
|
|
touch .env
|
|
echo RUN_TIMES=js >> .env
|
|
echo NODE_PATH=node >> .env
|
|
echo HTTP_PORT=5000 >> .env
|
|
# The server refuses to run as root unless RUN_AS names an account
|
|
echo RUN_AS=root >> .env
|
|
nohup npx --yes @sasjs/server@${{ env.SASJS_SERVER_VERSION }} > server.log 2>&1 &
|
|
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"
|
|
cat server.log
|
|
exit 1
|
|
|
|
- name: Deploy the mocked services and seed the database
|
|
run: |
|
|
cd sas/mocks/sasjs
|
|
# Start from a clean appLoc so the snapshot below is this build alone,
|
|
# not a merge with whatever a previous run left on the drive.
|
|
curl -sS -X DELETE "http://localhost:5000/SASjsApi/drive/folder/?_folderPath=$APP_LOC"
|
|
../../node_modules/.bin/sasjs cbd -t server-ci
|
|
# makedata replies with HTML (it is normally called as a URL redirect),
|
|
# which the CLI reports as "invalid Json string" - that exit is
|
|
# expected. A real failure must not be published as an empty site, so
|
|
# the drive is checked before the snapshot is taken.
|
|
../../node_modules/.bin/sasjs request services/admin/makedata -t server-ci -d deploy/makedata.json -o ./makedata_out.json || true
|
|
|
|
DATA_DIR="../../../sasjs_root/drive/files$APP_LOC/data"
|
|
SEEDED=$(find "$DATA_DIR" -name '*.json' 2>/dev/null | wc -l)
|
|
echo "makedata seeded $SEEDED table(s) under $DATA_DIR"
|
|
if [ -z "$(find "$DATA_DIR" -name 'mpe_tables.json' 2>/dev/null | head -1)" ] || [ "$SEEDED" -lt 10 ]; then
|
|
echo "makedata did not seed the drive - refusing to publish a data-less site"
|
|
find ../../../sasjs_root/drive/files -maxdepth 3 2>/dev/null | head -20
|
|
exit 1
|
|
fi
|
|
|
|
- name: Build the browser-hosted site
|
|
run: |
|
|
# The published sites ship a working licence so they are usable beyond
|
|
# the free tier. The pair is committed in sas/mocks/mock-licence.json
|
|
# rather than minted here, so every deploy of both sites carries the
|
|
# same key; the CI estate stays unlicensed because that file is outside
|
|
# the deployed sync folder, so the specs still exercise the free tier.
|
|
# The pair must round trip through the same WebCrypto the app uses,
|
|
# because a committed pair can drift (one half regenerated, a value
|
|
# truncated on copy) and nothing else would notice before publishing.
|
|
# Identical values in the two fields is the HTTP-format marker the app
|
|
# refuses in a secure context, so refuse to publish one.
|
|
node -e "
|
|
const d = require('./sas/mocks/mock-licence.json')
|
|
const b = (s) => Buffer.from(String(s || ''), 'base64')
|
|
if (!d.licenceKey || !d.activationKey || d.licenceKey === d.activationKey) {
|
|
console.error('sas/mocks/mock-licence.json must hold an HTTPS-format key pair')
|
|
process.exit(1)
|
|
}
|
|
crypto.subtle
|
|
.importKey('pkcs8', b(d.activationKey), { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['decrypt'])
|
|
.then((key) => crypto.subtle.decrypt({ name: 'RSA-OAEP', hash: 'SHA-256' }, key, b(d.licenceKey)))
|
|
.then((plain) => {
|
|
const data = JSON.parse(new TextDecoder().decode(plain))
|
|
console.log('licence pair round trips: site', data.site_id, 'until', data.valid_until)
|
|
})
|
|
.catch((e) => {
|
|
console.error('sas/mocks/mock-licence.json does not decrypt:', e.message || e)
|
|
process.exit(1)
|
|
})
|
|
"
|
|
LICENCE_DIR="sasjs_root/drive/files$APP_LOC/mock-storage"
|
|
mkdir -p "$LICENCE_DIR"
|
|
cp sas/mocks/mock-licence.json "$LICENCE_DIR/licence.json"
|
|
|
|
# DC's data files exist only after makedata has run, so the drive is
|
|
# captured first and shipped as the site's starting drive. services/web9
|
|
# is the streamed frontend, which is already the site itself.
|
|
node sas/mocks/snapshot.js sasjs_root/drive "$APP_LOC" sas/mocks/sasjsbuild/mock-snapshot.json services/web9
|
|
cd sas/mocks/sasjs
|
|
../../node_modules/.bin/sasjs b -t server-ci
|
|
|
|
- name: Publish to Surfer
|
|
env:
|
|
SURFER_USER: allan
|
|
SURFER_PASSWORD: ${{ secrets.DEMO_DATACONTROLLER_IO }}
|
|
REF: ${{ gitea.ref }}
|
|
MESSAGE: Build from ${{ gitea.ref_name }} @ ${{ gitea.sha }}
|
|
run: |
|
|
# `deploy` uploads the folder as an archive and swaps the site in one
|
|
# step, so a failed upload leaves the current site serving.
|
|
if [ -z "$REF" ]; then
|
|
echo "REF is empty - cannot tell a branch push from a tag push"
|
|
exit 1
|
|
fi
|
|
|
|
SITE_ARGS=()
|
|
case "$REF" in
|
|
refs/tags/*) ;;
|
|
*) SITE_ARGS=(--site-name "$STAGING_SITE" --domain "$STAGING_DOMAIN") ;;
|
|
esac
|
|
|
|
npx --yes @cloudron/surfer@7.2.0 deploy \
|
|
--server "$SURFER_HOST" \
|
|
--username "$SURFER_USER" \
|
|
--password "$SURFER_PASSWORD" \
|
|
--message "$MESSAGE" \
|
|
"${SITE_ARGS[@]}" \
|
|
sas/mocks/sasjsbuild/mock
|
|
|
|
- name: Remove registry config
|
|
# written earlier in this job; nothing deletes it on the way out
|
|
if: always()
|
|
run: rm -f "$RUNNER_TEMP/npmrc"
|