ci: add frontend CD pipeline, server provisioning, TLS scripts
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
There was no CD: pushing to main deployed nothing, and deploys were a manual copy onto the server. This adds the missing half. - .github/workflows/deploy.yml - build, upload to a per-commit release directory, swap the symlink atomically, reload nginx, verify over HTTP. The swap only happens after the upload is verified to contain index.html, so a failed deploy leaves the previous release serving. - scripts/deploy/server-setup.sh - idempotent one-time provisioning: nginx, certbot, ufw, and a key-only deploy user whose sole sudo right is "systemctl reload nginx". - scripts/deploy/add-domain.sh - per-domain server block plus TLS issuance, run once a domain's A record resolves to the server. - docs/DEPLOYMENT.md - setup order, required CI secrets, rollback, limits. Also adds .gitattributes: the shell scripts were being checked out with CRLF endings, which makes bash fail on the shebang line on Linux. Host keys are pinned via DEPLOY_KNOWN_HOSTS rather than trusted on first use. No credentials are committed; all four deploy secrets are supplied by CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
134
.github/workflows/deploy.yml
vendored
Normal file
134
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
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: ubuntu-latest
|
||||
environment: production
|
||||
|
||||
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/<name>/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"
|
||||
rsync -az --delete \
|
||||
-e "$SSH" \
|
||||
"$SRC/" "$USER@$HOST:/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 <<EOSSH
|
||||
BASE=/srv/marketplaces
|
||||
REL="\$BASE/releases/$RELEASE"
|
||||
test -f "\$REL/frontend/index.html" || { echo "upload incomplete, refusing to swap" >&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
|
||||
Reference in New Issue
Block a user