Add Gitea Actions CI/CD pipeline
Deploy / check (push) Successful in 9m45s
Deploy / build (push) Failing after 4s
Deploy / deploy (push) Has been skipped

Build and push images to the private registry, then deploy to the VPS by commit SHA with health check and rollback.
This commit is contained in:
2026-08-20 15:16:37 +02:00
parent 541e5c26c1
commit 4eef81fdfc
12 changed files with 4650 additions and 87 deletions
+118
View File
@@ -0,0 +1,118 @@
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