4eef81fdfc
Build and push images to the private registry, then deploy to the VPS by commit SHA with health check and rollback.
119 lines
3.7 KiB
YAML
119 lines
3.7 KiB
YAML
name: Deploy
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
|
|
# Two overlapping deploys would race on the same container. Queue instead.
|
|
concurrency:
|
|
group: deploy-production
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
IMAGE: git.ksan.dev/ksan/ksan.dev
|
|
SITE_DIR: /home/deploy/websites/ksan.dev
|
|
DOMAIN: ksan.dev
|
|
|
|
jobs:
|
|
check:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
- run: npm ci
|
|
- run: npm run typecheck
|
|
- run: npm run lint
|
|
|
|
build:
|
|
needs: check
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
tag: ${{ steps.meta.outputs.tag }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- id: meta
|
|
run: echo "tag=$(echo "${{ github.sha }}" | cut -c1-12)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Log in to the Gitea registry
|
|
env:
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: echo "$REGISTRY_TOKEN" | docker login git.ksan.dev -u "$REGISTRY_USER" --password-stdin
|
|
|
|
# PLANS_PAGES is a build arg: the codes are baked into the image via
|
|
# generateStaticParams, so this image must only ever go to the private
|
|
# package. Never push this repo's image to a public registry.
|
|
- name: Build and push
|
|
env:
|
|
PLANS_PAGES: ${{ secrets.PLANS_PAGES }}
|
|
TAG: ${{ steps.meta.outputs.tag }}
|
|
run: |
|
|
docker build \
|
|
--build-arg PLANS_PAGES="$PLANS_PAGES" \
|
|
-t "$IMAGE:$TAG" \
|
|
-t "$IMAGE:latest" \
|
|
.
|
|
docker push "$IMAGE:$TAG"
|
|
docker push "$IMAGE:latest"
|
|
|
|
deploy:
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Configure SSH
|
|
env:
|
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
|
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
|
|
run: |
|
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
|
|
chmod 600 ~/.ssh/id_ed25519
|
|
# Pinned host key — never StrictHostKeyChecking=no on a deploy path.
|
|
printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
|
chmod 600 ~/.ssh/known_hosts
|
|
|
|
- name: Ship compose file and deploy scripts
|
|
env:
|
|
SSH_HOST: ${{ secrets.SSH_HOST }}
|
|
SSH_USER: ${{ secrets.SSH_USER }}
|
|
run: |
|
|
scp -i ~/.ssh/id_ed25519 \
|
|
docker-compose.yaml deploy/deploy.sh deploy/rollback.sh \
|
|
"$SSH_USER@$SSH_HOST:$SITE_DIR/"
|
|
ssh -i ~/.ssh/id_ed25519 "$SSH_USER@$SSH_HOST" \
|
|
"chmod +x $SITE_DIR/deploy.sh $SITE_DIR/rollback.sh"
|
|
|
|
- name: Deploy
|
|
env:
|
|
SSH_HOST: ${{ secrets.SSH_HOST }}
|
|
SSH_USER: ${{ secrets.SSH_USER }}
|
|
TAG: ${{ needs.build.outputs.tag }}
|
|
run: |
|
|
ssh -i ~/.ssh/id_ed25519 "$SSH_USER@$SSH_HOST" \
|
|
"IMAGE_TAG=$TAG $SITE_DIR/deploy.sh"
|
|
|
|
# End-to-end, from outside the VPS: proves the container, the Traefik
|
|
# router labels, the frontend network attachment and the cert all work.
|
|
# A localhost probe on the VPS would miss a broken router rule.
|
|
- name: Verify the site is serving
|
|
run: |
|
|
curl -fsS --retry 10 --retry-delay 3 --retry-connrefused \
|
|
-o /dev/null "https://$DOMAIN/en"
|
|
|
|
- name: Roll back
|
|
if: failure()
|
|
env:
|
|
SSH_HOST: ${{ secrets.SSH_HOST }}
|
|
SSH_USER: ${{ secrets.SSH_USER }}
|
|
run: |
|
|
ssh -i ~/.ssh/id_ed25519 "$SSH_USER@$SSH_HOST" "$SITE_DIR/rollback.sh"
|
|
echo "::error::Deploy failed — rolled back to the last healthy build."
|
|
exit 1
|