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: 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 cat .env pm2 start api-linux --wait-ready - 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. # 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 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 - 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 cd .. zip -r frontend.zip ./client/dist - name: Release Typedoc run: | cd client npm -g install cloudron-surfer npm run compodoc:build surfer put --token ${{ secrets.TSDOC_TOKEN }} --server webdoc.datacontroller.io documentation/* / - name: Release code.datacontroller.io run: | cd sas sasjs doc surfer put --token ${{ secrets.CODE_DATACONTROLLER_IO }} --server code.datacontroller.io sasjsbuild/sasdocs/* / - 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 -k --fail-with-body -sS -H "$AUTH_HEADER" "$BASE/releases/latest") RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id') RELEASE_BODY=$(echo "$RELEASE_JSON" | jq -r '.body') # Update body (also confirms the token has contents:write on this repo) curl -k --fail-with-body -sS -X PATCH \ -H "$AUTH_HEADER" \ -H 'Content-Type: application/json' \ --data "$(jq -n --arg body "$RELEASE_BODY"$'\n\nFor installation instructions, please visit https://docs.datacontroller.io/' \ '{draft:false, body:$body}')" \ "$BASE/releases/$RELEASE_ID" # Upload assets URL="$BASE/releases/$RELEASE_ID/assets" for f in 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; do echo "Uploading $f ..." curl -k --fail-with-body -sS -H "$AUTH_HEADER" "$URL" -F "attachment=@$f" done