The Sites workflow was the one place that still started the server without a listener, so it died with [error] Invalid environment variable(s) provided: - no listener is enabled - set HTTP_PORT and/or HTTPS_PORT (either may be used alone) and the following step's 120s poll timed out. build.yaml, release.yaml and lighthouse.yaml already carry the same line.
184 lines
7.7 KiB
YAML
184 lines
7.7 KiB
YAML
name: Sites
|
|
on:
|
|
push:
|
|
branches:
|
|
- 'release/**'
|
|
tags:
|
|
- 'v*'
|
|
paths-ignore:
|
|
- '**.md'
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
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: |
|
|
touch client/.npmrc
|
|
echo '${{ secrets.NPMRC}}' > client/.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
|
|
|
|
- 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: 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: |
|
|
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
|
unzip -q linux.zip
|
|
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 ./api-linux > 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
|