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