Skip to content

CI on every push

.gitea/workflows/ci.yml has two jobs:

name: ci
on: [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.com

Jobs reach the runner’s own Docker daemon through a proxy — not the host’s — so docker build and docker push work inside a job.

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.

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¼.