name: Deploy Frontend # Multi-tenant: one bundle serves every customer domain, so a single deploy # updates all of them at once. There is no per-tenant build or per-tenant deploy. on: push: branches: - main workflow_dispatch: inputs: ref: description: Branch or SHA to deploy required: false default: main concurrency: group: deploy-frontend cancel-in-progress: false # never abandon a half-finished release swap jobs: deploy: runs-on: urgent-deploy environment: production defaults: run: shell: bash steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.ref || github.ref }} - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 cache: npm - name: Install dependencies run: npm ci - name: Enforce boundaries run: npm run arch:check - name: Build run: npm run build -- --configuration production - name: Resolve build output id: dist run: | set -euo pipefail # @angular/build:application emits into dist//browser. # Fall back to the flat layout so this survives a builder change. if [ -d dist/dexarmarket/browser ]; then DIR=dist/dexarmarket/browser elif [ -f dist/dexarmarket/index.html ]; then DIR=dist/dexarmarket else echo "no build output found under dist/dexarmarket" >&2 ls -R dist || true exit 1 fi test -f "$DIR/index.html" || { echo "$DIR has no index.html" >&2; exit 1; } echo "dir=$DIR" >> "$GITHUB_OUTPUT" echo "Deploying from $DIR ($(find "$DIR" -type f | wc -l) files)" - name: Configure SSH env: DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }} run: | set -euo pipefail test -n "$DEPLOY_SSH_KEY" || { echo "secret DEPLOY_SSH_KEY is empty" >&2; exit 1; } test -n "$DEPLOY_KNOWN_HOSTS" || { echo "secret DEPLOY_KNOWN_HOSTS is empty" >&2; exit 1; } mkdir -p ~/.ssh printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key # Pinned host key, so a MITM or a rebuilt server fails the deploy # instead of being trusted silently. printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts - name: Upload release env: HOST: ${{ secrets.DEPLOY_HOST }} USER: ${{ secrets.DEPLOY_USER }} SRC: ${{ steps.dist.outputs.dir }} run: | set -euo pipefail RELEASE="${GITHUB_SHA::12}" echo "RELEASE=$RELEASE" >> "$GITHUB_ENV" SSH="ssh -i ~/.ssh/deploy_key -o BatchMode=yes" $SSH "$USER@$HOST" "mkdir -p /srv/marketplaces/releases/$RELEASE/frontend" tar -C "$SRC" -czf - . | $SSH "$USER@$HOST" \ "tar -xzf - -C /srv/marketplaces/releases/$RELEASE/frontend" - name: Activate release env: HOST: ${{ secrets.DEPLOY_HOST }} USER: ${{ secrets.DEPLOY_USER }} run: | set -euo pipefail ssh -i ~/.ssh/deploy_key -o BatchMode=yes "$USER@$HOST" bash -euo pipefail <&2; exit 1; } # ln -T onto a temp name then mv: the swap is atomic, so no request # is ever served from a half-updated root. ln -sfnT "\$REL" "\$BASE/current.new" mv -Tf "\$BASE/current.new" "\$BASE/current" sudo /bin/systemctl reload nginx # Keep the last 5 releases so a rollback is a symlink change. ls -1dt "\$BASE"/releases/*/ | tail -n +6 | xargs -r rm -rf echo "active: \$(readlink -f \$BASE/current)" EOSSH - name: Verify env: HOST: ${{ secrets.DEPLOY_HOST }} USER: ${{ secrets.DEPLOY_USER }} run: | set -euo pipefail ssh -i ~/.ssh/deploy_key -o BatchMode=yes "$USER@$HOST" \ 'curl -fsS -o /dev/null -w "health=%{http_code}\n" http://127.0.0.1/health && curl -fsS -o /dev/null -w "index=%{http_code}\n" http://127.0.0.1/' - name: Report if: always() run: | if [ "${{ job.status }}" = "success" ]; then echo "Deployed ${GITHUB_SHA::12} to ${{ secrets.DEPLOY_HOST }}" >> "$GITHUB_STEP_SUMMARY" else echo "Deploy of ${GITHUB_SHA::12} FAILED. The previous release is still active — the symlink only moves after a successful upload." >> "$GITHUB_STEP_SUMMARY" fi