diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml new file mode 100644 index 0000000..23937b5 --- /dev/null +++ b/.gitea/workflows/deploy-staging.yml @@ -0,0 +1,160 @@ +name: Deploy — Staging + +on: + push: + branches: + - staging + +# STAGING_REGISTRY must include the owner segment, e.g. git.yourdomain.com:3000/myorg +# so images are correctly tagged as git.yourdomain.com:3000/myorg/storefront:staging +# (see: troubleshooting #8 — missing /owner causes a 500 from Gitea registry) +# +# Required secrets: +# STAGING_REGISTRY — host:port/owner (e.g. git.yourdomain.com:3000/myorg) +# STAGING_REGISTRY_USER — Gitea username +# STAGING_REGISTRY_TOKEN — Gitea personal access token (package:write scope) +# STAGING_SSH_HOST — use host.containers.internal, not the external IP +# (see: troubleshooting #13 — VPS firewall blocks ext IP) +# STAGING_SSH_USER — SSH user on the VPS +# STAGING_SSH_KEY — SSH private key (full PEM) +# STAGING_SSH_PORT — (optional) defaults to 22 +# +# The Dockerfiles are expected at: +# apps/storefront/Dockerfile +# apps/admin/Dockerfile +# Both receive ./out as build context (turbo prune output). + +env: + REGISTRY: ${{ secrets.STAGING_REGISTRY }} + +jobs: + # ── 1. CI ─────────────────────────────────────────────────────────────────── + + ci: + name: Lint, Typecheck & Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Typecheck + run: npm run type-check + + - name: Test + run: npm run test:once + + # ── 2. Build & push ───────────────────────────────────────────────────────── + # Runs storefront and admin in parallel via matrix. + # Each job prunes its own workspace so there is no out/ directory collision. + + build: + name: Build & push — ${{ matrix.app }} + needs: ci + runs-on: ubuntu-latest + strategy: + matrix: + app: [storefront, admin] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Prune workspace for ${{ matrix.app }} + run: npx turbo prune ${{ matrix.app }} --docker + + - name: Authenticate with registry + # docker login sends HTTPS even for HTTP-only (insecure) registries, so it + # fails before the daemon can handle it. Pre-populating config.json bypasses + # login entirely — docker push goes through the Podman daemon which correctly + # uses HTTP. (see: troubleshooting #7) + run: | + mkdir -p ~/.docker + AUTH=$(echo -n "${{ secrets.STAGING_REGISTRY_USER }}:${{ secrets.STAGING_REGISTRY_TOKEN }}" | base64 -w 0) + REGISTRY_HOST=$(echo "${{ env.REGISTRY }}" | cut -d'/' -f1) + echo "{\"auths\":{\"${REGISTRY_HOST}\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json + + - name: Build & push ${{ matrix.app }} + # Plain docker build — no docker/setup-buildx-action needed. + # The docker-container buildx driver spawns a privileged builder container + # which fails on rootless Podman without --privileged. (see: troubleshooting #5) + run: | + SHORT_SHA="${GITHUB_SHA::7}" + IMAGE="${{ env.REGISTRY }}/${{ matrix.app }}" + + docker build \ + -f apps/${{ matrix.app }}/Dockerfile \ + -t "${IMAGE}:staging" \ + -t "${IMAGE}:sha-${SHORT_SHA}" \ + ./out + + docker push "${IMAGE}:staging" + docker push "${IMAGE}:sha-${SHORT_SHA}" + + # ── 3. Deploy ─────────────────────────────────────────────────────────────── + + deploy: + name: Deploy to staging VPS + needs: build + runs-on: ubuntu-latest + + steps: + - name: Write SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.STAGING_SSH_KEY }}" > ~/.ssh/staging + chmod 600 ~/.ssh/staging + + - name: Pull & restart containers on VPS + env: + REGISTRY: ${{ secrets.STAGING_REGISTRY }} + REGISTRY_USER: ${{ secrets.STAGING_REGISTRY_USER }} + REGISTRY_TOKEN: ${{ secrets.STAGING_REGISTRY_TOKEN }} + SSH_HOST: ${{ secrets.STAGING_SSH_HOST }} + SSH_USER: ${{ secrets.STAGING_SSH_USER }} + SSH_PORT: ${{ secrets.STAGING_SSH_PORT }} + run: | + # Auth key is the hostname only — strip the /owner path + REGISTRY_HOST=$(echo "$REGISTRY" | cut -d'/' -f1) + + # StrictHostKeyChecking=accept-new trusts on first connect but rejects + # changed keys on subsequent runs — safer than no-verify + ssh -i ~/.ssh/staging \ + -p "${SSH_PORT:-22}" \ + -o StrictHostKeyChecking=accept-new \ + "${SSH_USER}@${SSH_HOST}" bash -s << EOF + set -euo pipefail + + # Registry uses HTTP — --tls-verify=false required for podman login & pull + # (see: troubleshooting #12) + echo "${REGISTRY_TOKEN}" \ + | podman login "${REGISTRY_HOST}" \ + -u "${REGISTRY_USER}" --password-stdin --tls-verify=false + + podman pull --tls-verify=false "${REGISTRY}/storefront:staging" + podman pull --tls-verify=false "${REGISTRY}/admin:staging" + + cd /opt/staging + podman compose up -d --remove-orphans + + # Remove dangling images from previous deploys + podman image prune -f + EOF