CI on every push
The workflow
Section titled “The workflow”.gitea/workflows/ci.yml has two jobs:
name: cion: [push, workflow_dispatch]
env: IMAGE: git.fromjon.com/<owner>/jonathandobson-com TURNSTILE_SITE_KEY: <public site key> # public by design; inlined at build time
jobs: verify: runs-on: ubuntu-latest container: image: mcr.microsoft.com/playwright:v1.64.1-noble # keep equal to @playwright/test steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 26.13.0, cache: npm } - run: npm ci - run: cp .env.example .env.local # Turnstile test keys, no real secrets - uses: actions/cache@v4 with: path: .next/cache key: nextjs-${{ hashFiles('package-lock.json') }}-${{ github.sha }} restore-keys: nextjs-${{ hashFiles('package-lock.json') }}- - run: npm run verify - run: npm run test:e2e env: { E2E_SKIP_BUILD: "1" } # reuse the build from verify
image: needs: verify if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.fromjon.com -u <owner> --password-stdin - run: > docker build --build-arg NEXT_PUBLIC_TURNSTILE_SITE_KEY="$TURNSTILE_SITE_KEY" --label org.opencontainers.image.revision="${{ github.sha }}" -t "$IMAGE:${{ github.sha }}" -t "$IMAGE:main" . - run: | docker push "$IMAGE:${{ github.sha }}" docker push "$IMAGE:main" - if: always() run: docker logout git.fromjon.comJobs reach the runner’s own Docker daemon through a proxy — not the host’s
— so docker build and docker push work inside a job.
Three failed runs, and what each taught
Section titled “Three failed runs, and what each taught”| Run | image failed with |
Lesson |
|---|---|---|
| 1 | unauthorized on docker login with secrets.GITEA_TOKEN |
Gitea’s container registry rejects the built-in job token for login |
| 2 | Same, with permissions: packages: write |
Requesting the scope explicitly doesn’t help |
| 3 | Same, with a personal access token | The token had repository scope; the registry checks package scope |
The fix: a personal access token named ci-registry with package: Read and
Write only, stored as the repository Actions secret REGISTRY_TOKEN.
Token scopes can’t be edited afterwards — delete and recreate.
Caching
Section titled “Caching”| Step | Before | After |
|---|---|---|
npx playwright install --with-deps chromium |
69 s | gone — the Playwright image ships browsers and system packages |
| Second production build for e2e | ~25 s | gone — E2E_SKIP_BUILD=1 reuses verify’s build |
.next/cache |
cold every run | restored by actions/cache |
| npm downloads | — | setup-node’s cache: npm (works out of the box: the runner has a built-in cache server) |
docker build |
5½ min first time | 51 s — the runner’s daemon keeps layers in its volume |
verify went from about 4 minutes to about 2¼.