Build / Build-and-ng-test (pull_request) Canceled after 0s
Build / cypress-shard-1 (pull_request) Canceled after 0s
Build / cypress-shard-2 (pull_request) Canceled after 0s
Build / cypress-shard-3 (pull_request) Canceled after 0s
Build / cypress-shard-4 (pull_request) Canceled after 0s
Build / Build-and-test-development (pull_request) Canceled after 0s
Lighthouse Checks / lighthouse (pull_request) Canceled after 0s
semantic-release bumps the working-tree package.json before the release commit, so the tag is known in the job. Using it for the asset upload and both dispatches means an overlapping release cannot redirect them.
462 lines
19 KiB
YAML
462 lines
19 KiB
YAML
name: Release
|
|
run-name: Testing and Releasing DC
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
|
|
env:
|
|
NODE_VERSION: '24.5.0'
|
|
|
|
jobs:
|
|
Build-production-and-ng-test:
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
CHROME_BIN: /usr/bin/google-chrome
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- 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 Chrome for Angular tests
|
|
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 cypress credentials
|
|
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
|
shell: bash
|
|
env:
|
|
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
|
|
|
- name: Install 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: 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: Angular Tests
|
|
run: |
|
|
cd client
|
|
npm run test:headless
|
|
|
|
- name: Angular Production Build
|
|
run: |
|
|
cd client
|
|
npm run postinstall
|
|
npm run build
|
|
|
|
Build-and-test-development:
|
|
runs-on: ubuntu-latest
|
|
needs: Build-production-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
|
|
- 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 system dependencies
|
|
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
|
|
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
|
shell: bash
|
|
env:
|
|
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
|
|
|
- name: Install 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 for web streaming
|
|
# The server-ci target streams client/dist as the 'clickme' web app, so
|
|
# the production build has to exist before the mocked services are
|
|
# deployed. Without it `sasjs cbd` reports
|
|
# "webSourcePath: .../client/dist ... doesn't exist", streams no
|
|
# frontend and the deploy step fails.
|
|
run: |
|
|
cd client
|
|
npm run build
|
|
|
|
- name: Setup and start SASjs server
|
|
run: |
|
|
npm i -g pm2
|
|
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
|
unzip linux.zip
|
|
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
|
|
# 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 api-linux --wait-ready
|
|
# `pm2 start --wait-ready` can return while the app is still
|
|
# launching (it does not always wait for the ready signal), 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 api-linux --lines 100 --nostream
|
|
exit 1
|
|
|
|
- name: Deploy mocked services
|
|
run: |
|
|
cd ./sas/mocks/sasjs
|
|
npm install -g @sasjs/cli
|
|
npm install -g replace-in-files-cli
|
|
# 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
|
|
run: |
|
|
cd ./client
|
|
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
|
|
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
|
|
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts,cypress/e2e/viewbox.cy.ts,cypress/e2e/stage.cy.ts"
|
|
|
|
- name: Zip Cypress videos
|
|
if: always()
|
|
run: |
|
|
mkdir -p ./client/cypress/videos
|
|
zip -r cypress-videos ./client/cypress/videos
|
|
|
|
- name: Add cypress videos artifacts
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: cypress-videos.zip
|
|
path: cypress-videos.zip
|
|
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
needs: [Build-production-and-ng-test, Build-and-test-development]
|
|
# The semantic-release step below uses the per-job token injected by Gitea
|
|
# Actions (secrets.GITEA_TOKEN). That token is scoped to THIS repository
|
|
# and its permissions are derived from this block.
|
|
# contents: write -> push CHANGELOG.md / package.json commit + tag, and
|
|
# create the Release via the Gitea API.
|
|
# actions: write -> dispatch the Sites workflow for the new tag (that
|
|
# route is scoped to the actions unit, which contents
|
|
# does not grant).
|
|
# The TSDOC_TOKEN and CODE_DATACONTROLLER_IO secrets continue to be used
|
|
# because they target *different* servers (webdoc.datacontroller.io and
|
|
# code.datacontroller.io), not the Gitea API.
|
|
permissions:
|
|
contents: write
|
|
actions: write
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Write .npmrc file
|
|
run: |
|
|
echo "$NPMRC" > client/.npmrc
|
|
echo "legacy-peer-deps=true" >> client/.npmrc
|
|
shell: bash
|
|
env:
|
|
NPMRC: ${{ secrets.NPMRC}}
|
|
|
|
- name: Install system packages
|
|
run: |
|
|
apt-get update
|
|
apt-get install -y zip jq doxygen
|
|
npm i -g @sasjs/cli
|
|
|
|
- name: Frontend Preliminary Build
|
|
description: We want to prevent creating empty release if frontend fails
|
|
run: |
|
|
cd client
|
|
# Decrypt and Install sheet
|
|
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
|
npm ci
|
|
npm i webpack
|
|
npm run build
|
|
|
|
- name: Create Empty Release (assets are posted later)
|
|
env:
|
|
# Use the ephemeral per-job token injected by Gitea Actions instead
|
|
# of a manually-managed PAT. This token always reports push=true on
|
|
# its own repo via the Gitea API, which is what the
|
|
# @saithodev/semantic-release-gitea verifyConditions step checks.
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
GITEA_URL: ${{ gitea.server_url }}
|
|
run: |
|
|
set -euo pipefail
|
|
npm i
|
|
npm i -g semantic-release
|
|
|
|
# We do a semantic-release DRY RUN to make the job fail if there are
|
|
# no changes to release. The previous implementation piped --dry-run
|
|
# into `grep`, which swallowed semantic-release's non-zero exit codes
|
|
# (e.g. auth/verifyConditions failures) — the real error then only
|
|
# surfaced from the second invocation. Capture output and inspect it
|
|
# so authentication failures fail the step cleanly with context.
|
|
echo "::group::semantic-release --dry-run"
|
|
DRY_EXIT=0
|
|
DRY_OUTPUT=$(semantic-release --dry-run 2>&1) || DRY_EXIT=$?
|
|
echo "$DRY_OUTPUT"
|
|
echo "::endgroup::"
|
|
|
|
if [ "$DRY_EXIT" -ne 0 ]; then
|
|
echo "::error::semantic-release dry-run failed (exit $DRY_EXIT). See log above; common cause is the Gitea token lacking push permission on this repo (verifyConditions of @saithodev/semantic-release-gitea)."
|
|
exit "$DRY_EXIT"
|
|
fi
|
|
|
|
if echo "$DRY_OUTPUT" | grep -q "There are no relevant changes, so no new version is released."; then
|
|
echo "::error::No releasable changes since last tag - aborting release job."
|
|
exit 1
|
|
fi
|
|
|
|
semantic-release
|
|
|
|
# The tag this job cuts, for the steps that publish against it: a
|
|
# `releases/latest` lookup would pick whichever release landed last
|
|
# if two overlap.
|
|
echo "RELEASE_TAG=v$(node -p "require('./package.json').version")" >> "$GITHUB_ENV"
|
|
|
|
- name: Frontend Build
|
|
description: Must be created AFTER the release as the version (git tag) is used in the interface
|
|
run: |
|
|
cd client
|
|
npm run build
|
|
|
|
- name: Build SAS9 EBI Release
|
|
description: Compile SAS 9 services, remove tests & create deployment program
|
|
run: |
|
|
cd sas
|
|
npm i
|
|
sasjs c -t sas9
|
|
rm -rf sasjsbuild/tests
|
|
sasjs b -t sas9
|
|
cp sasjsbuild/mysas9deploy.sas ./demostream_sas9.sas
|
|
#
|
|
# remove streamed component and rebuild SAS 9 services
|
|
#
|
|
rm -rf sasjsbuild/services/web9
|
|
rm sasjsbuild/services/clickme.sas
|
|
sasjs b -t sas9
|
|
cp sasjsbuild/mysas9deploy.sas ./sas9.sas
|
|
|
|
- name: Build SASjs Server Release
|
|
description: Compile Base (SASjs) services, remove tests & create deployment JSON
|
|
run: |
|
|
cd sas
|
|
cp sasjs/utils/favicon.ico ../client/dist/favicon.ico
|
|
sasjs c -t server
|
|
rm -rf sasjsbuild/tests
|
|
server_apploc="/Public/app/dc"
|
|
sed -i "s|apploc=\"[^\"]*\"|apploc=\"${server_apploc}\"|g" sasjsbuild/services/web/index.html
|
|
sasjs b -t server
|
|
cp sasjsbuild/server.json.zip ./sasjs_server.json.zip
|
|
|
|
- name: Build Viya Release
|
|
description: compile Viya Streaming Deploy (without tests)
|
|
run: |
|
|
cd sas
|
|
sasjs c -t viya
|
|
rm -rf sasjsbuild/tests
|
|
sed -i -e 's/servertype="SASJS"/servertype="SASVIYA"/g' sasjsbuild/services/DC.html
|
|
sasjs b -t viya
|
|
cp sasjsbuild/viya.sas ./viya.sas
|
|
# compile Viya Full deploy (without web)
|
|
rm -rf sasjsbuild/services/web
|
|
rm sasjsbuild/services/DC.html
|
|
sasjs b -t viya
|
|
cp sasjsbuild/viya.sas ./viya_noweb.sas
|
|
cp sasjsbuild/viya.json ./viya_noweb.json
|
|
|
|
- name: Zip Frontend (including viya.json for full viya deploy)
|
|
run: |
|
|
cd sas
|
|
cp sasjsbuild/viya.json ../client/dist/viya.json
|
|
# Zip from *inside* dist so the archive holds the frontend files at its
|
|
# root rather than under a client/dist/ prefix (see #147).
|
|
cd ../client/dist
|
|
zip -r ../../frontend.zip .
|
|
|
|
- name: Upload assets to release
|
|
env:
|
|
# Use the same ephemeral per-job Gitea Actions token that the
|
|
# "Create Empty Release" step uses for semantic-release. It is scoped
|
|
# to this repo and is granted write access via the workflow's
|
|
# `permissions:` block at the top of the file (see contents: write).
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Send the token via Authorization header rather than ?access_token=
|
|
# (the query-string form is deprecated and leaks into access logs).
|
|
AUTH_HEADER="Authorization: token ${GITEA_TOKEN}"
|
|
BASE="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
|
|
|
RELEASE_JSON=$(curl --fail-with-body -sS -H "$AUTH_HEADER" "$BASE/releases/tags/${RELEASE_TAG}")
|
|
RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id')
|
|
RELEASE_BODY=$(echo "$RELEASE_JSON" | jq -r '.body')
|
|
|
|
# Generate SHA-256 checksums over the release assets, so downloads can
|
|
# be verified. The file uses basename-only paths so that a plain
|
|
# "sha256sum -c SHA256SUMS" works in the folder the assets were
|
|
# downloaded to (the upload below stores them under their basename).
|
|
ASSETS=(frontend.zip
|
|
sas/demostream_sas9.sas
|
|
sas/viya.sas
|
|
sas/sasjs_server.json.zip
|
|
sas/sas9.sas
|
|
sas/viya_noweb.sas
|
|
sas/viya_noweb.json)
|
|
|
|
: > SHA256SUMS
|
|
for f in "${ASSETS[@]}"; do
|
|
sum=$(sha256sum "$f" | cut -d' ' -f1)
|
|
printf '%s %s\n' "$sum" "${f##*/}" >> SHA256SUMS
|
|
done
|
|
cat SHA256SUMS
|
|
|
|
# Update body: keep the existing notes, add a verification section
|
|
# ahead of the installation footer. The section text lives in a
|
|
# dedicated script below (.gitea/scripts/verify-section.sh) so this
|
|
# YAML scalar stays clean.
|
|
# `jq -r` is required: without it jq emits the result as a JSON string
|
|
# literal (quoted, with \n escapes), and the PATCH below then encodes
|
|
# that literal a second time, so the release body renders as one
|
|
# escaped line instead of markdown.
|
|
VERIFY_SECTION=$(./.gitea/scripts/verify-section.sh)
|
|
NEW_BODY=$(jq -nr --arg body "$RELEASE_BODY" --arg verify "$VERIFY_SECTION" \
|
|
'$body + "\n" + $verify + "\n\nFor installation instructions, please visit https://docs.datacontroller.io/"')
|
|
curl --fail-with-body -sS -X PATCH \
|
|
-H "$AUTH_HEADER" \
|
|
-H 'Content-Type: application/json' \
|
|
--data "$(jq -n --arg body "$NEW_BODY" '{draft:false, body:$body}')" \
|
|
"$BASE/releases/$RELEASE_ID"
|
|
|
|
# Upload assets
|
|
URL="$BASE/releases/$RELEASE_ID/assets"
|
|
for f in "${ASSETS[@]}"; do
|
|
echo "Uploading $f ..."
|
|
curl --fail-with-body -sS -H "$AUTH_HEADER" "$URL" -F "attachment=@$f"
|
|
done
|
|
echo "Uploading SHA256SUMS ..."
|
|
curl --fail-with-body -sS -H "$AUTH_HEADER" "$URL" -F "attachment=@SHA256SUMS"
|
|
|
|
- name: Publish the code documentation
|
|
# Typedoc (webdoc) and the SAS docs (code) are published by docs.yaml,
|
|
# not inline here. Two reasons: the v6 `surfer put --token` flow is gone
|
|
# in Surfer 7, which takes a Cloudron username plus an app password; and
|
|
# a dispatch means a stale or failed docs deploy can be republished
|
|
# without re-running this job, which semantic-release refuses once the
|
|
# tag exists.
|
|
#
|
|
# This runs AFTER the release assets are uploaded: the documentation is
|
|
# a side channel, and a docs problem must not cost a cut release its
|
|
# artefacts.
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
AUTH_HEADER="Authorization: token $GITEA_TOKEN"
|
|
BASE="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
|
|
|
TAG="${RELEASE_TAG}"
|
|
echo "Dispatching code documentation for ${TAG}"
|
|
|
|
curl --fail-with-body -sS -X POST \
|
|
-H "$AUTH_HEADER" \
|
|
-H 'Content-Type: application/json' \
|
|
--data "$(jq -n --arg ref "refs/tags/$TAG" '{ref: $ref}')" \
|
|
"$BASE/actions/workflows/docs.yaml/dispatches"
|
|
|
|
- name: Publish the demo site for this release
|
|
# The tag is created by this job's semantic-release step, so it carries
|
|
# the Actions user as its author - and the forge suppresses ref events
|
|
# from that user, which means `on: push: tags` in sites.yaml never fires.
|
|
# Dispatch it instead: the API call is authenticated, and the run it
|
|
# starts checks out the tag the same way.
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
AUTH_HEADER="Authorization: token ${GITEA_TOKEN}"
|
|
BASE="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
|
|
|
TAG="${RELEASE_TAG}"
|
|
echo "Dispatching Sites for ${TAG}"
|
|
|
|
# the dispatch endpoint resolves a bare name as a branch; a tag needs
|
|
# the refs/tags/ prefix
|
|
curl --fail-with-body -sS -X POST \
|
|
-H "$AUTH_HEADER" \
|
|
-H 'Content-Type: application/json' \
|
|
--data "$(jq -n --arg ref "refs/tags/$TAG" '{ref: $ref}')" \
|
|
"$BASE/actions/workflows/sites.yaml/dispatches"
|