Merge the forge-side CI work into master

d05f114 · Gabriel Morell · 2026-09-12 20:34

8 files +164 -4
Message
{commit_body(@commit)}

Files changed

added .github/workflows/ci.yml
+126 −0
@@ -0,0 +1,126 @@
1 +name: ci
2 +
3 +on:
4 + push:
5 + branches: [master]
6 + tags: ["v*"]
7 + pull_request:
8 +
9 +jobs:
10 + test:
11 + # Self-hosted ARC runners (same as ../angler). Swap for `ubuntu-latest`
12 + # if you'd rather use GitHub-hosted runners. The Postgres service container
13 + # requires the runner to support service containers (Docker/dind).
14 + runs-on: arc-runner-set
15 + env:
16 + MIX_ENV: test
17 + # Under forgejo-runner/act the job runs in a container, so the postgres
18 + # service is reached by its service name, not localhost.
19 + POSTGRES_HOST: postgres
20 + # service is reached at its container port under forgejo-runner
21 + POSTGRES_PORT: 5432
22 + # Cap the test DB pool below the Postgres service's default
23 + # max_connections (100); big CI runners otherwise scale pool to cores×2
24 + # and exhaust connections.
25 + POOL_SIZE: "40"
26 + services:
27 + postgres:
28 + image: docker.io/postgres:16-alpine
29 + env:
30 + POSTGRES_USER: postgres
31 + POSTGRES_PASSWORD: postgres
32 + # config/test.exs connects on 54328 (matches compose.yml), so publish
33 + # the service container's 5432 on that host port.
34 + ports:
35 + - 54328:5432
36 + options: >-
37 + --health-cmd "pg_isready -U postgres"
38 + --health-interval 5s
39 + --health-timeout 3s
40 + --health-retries 5
41 + steps:
42 + - uses: actions/checkout@v4
43 +
44 + # ARC's default runner image is bare; install the toolchain needed to
45 + # compile native deps.
46 + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends make build-essential
47 + # Forge fork of erlef/setup-beam: defaults ImageOS to ubuntu-24.04,
48 + # since forgejo/act injects an unmappable ImageOS from the runs-on label.
49 + - uses: https://gg.neiam.co/neiam/action-setup-beam@v1
50 + with:
51 + # Match the build image in the Dockerfile (OTP must be >= 27.3.3).
52 + elixir-version: "1.17.3"
53 + otp-version: "27.3.4.11"
54 +
55 + - uses: actions/cache@v4
56 + with:
57 + path: |
58 + deps
59 + _build
60 + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
61 + restore-keys: ${{ runner.os }}-mix-
62 +
63 + - run: mix deps.get
64 +
65 + # `mix precommit` = compile --warnings-as-errors, deps.unlock --unused,
66 + # format, test (which creates + migrates the test DB).
67 + - run: mix precommit
68 +
69 + image:
70 + needs: test
71 + runs-on: arc-runner-set
72 + permissions:
73 + contents: read
74 + packages: write
75 + steps:
76 + - uses: actions/checkout@v4
77 + - uses: docker/setup-buildx-action@v3
78 +
79 + # GHCR uses the built-in token. Docker Hub and Quay creds come from
80 + # org-wide secrets (DOCKERHUB_USER/DOCKERHUB_TOKEN, QUAY_USERNAME/QUAY_TOKEN).
81 + - name: Log in to GHCR
82 + if: github.event_name != 'pull_request'
83 + uses: docker/login-action@v3
84 + with:
85 + registry: ghcr.io
86 + username: ${{ vars.GHCR_USER || github.actor }}
87 + password: ${{ secrets.GHCR_TOKEN }}
88 +
89 + - name: Log in to Docker Hub
90 + if: github.event_name != 'pull_request'
91 + uses: docker/login-action@v3
92 + with:
93 + registry: docker.io
94 + username: ${{ secrets.DOCKERHUB_USER }}
95 + password: ${{ secrets.DOCKERHUB_TOKEN }}
96 +
97 + - name: Log in to Quay
98 + if: github.event_name != 'pull_request'
99 + uses: docker/login-action@v3
100 + with:
101 + registry: quay.io
102 + username: ${{ secrets.QUAY_USERNAME }}
103 + password: ${{ secrets.QUAY_TOKEN }}
104 +
105 + - id: meta
106 + uses: docker/metadata-action@v5
107 + with:
108 + images: |
109 + ghcr.io/${{ github.repository }}
110 + docker.io/neiam/waxx
111 + quay.io/neiam/waxx
112 + tags: |
113 + type=ref,event=branch
114 + type=ref,event=pr
115 + type=semver,pattern={{version}}
116 + type=semver,pattern={{major}}.{{minor}}
117 + type=sha
118 + type=raw,value=latest,enable={{is_default_branch}}
119 +
120 + - uses: docker/build-push-action@v6
121 + with:
122 + context: .
123 + platforms: linux/amd64
124 + push: ${{ github.event_name != 'pull_request' }}
125 + tags: ${{ steps.meta.outputs.tags }}
126 + labels: ${{ steps.meta.outputs.labels }}
modified README.md
+26 −0
@@ -171,6 +171,32 @@ mix ecto.drop && mix ecto.create && mix ecto.migrate
171 171 MIX_ENV=test mix ecto.drop && MIX_ENV=test mix ecto.create && MIX_ENV=test mix ecto.migrate
172 172 ```
173 173
174 +## Container images
175 +
176 +CI publishes a multi-registry image on every push to `master`:
177 +
178 +- `ghcr.io/neiam/waxx`
179 +- `docker.io/neiam/waxx`
180 +- `quay.io/neiam/waxx`
181 +
182 +Tags: `latest` (default branch), `vX.Y.Z` + `X.Y` (git tags), the branch name,
183 +and the commit SHA.
184 +
185 +```sh
186 +docker pull ghcr.io/neiam/waxx:latest
187 +
188 +docker run --rm -p 4000:4000 \
189 + -e PHX_SERVER=true \
190 + -e SECRET_KEY_BASE="$(openssl rand -base64 48)" \
191 + -e PHX_HOST=localhost \
192 + -e POSTGRES_HOST=host.containers.internal \
193 + -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=waxx \
194 + ghcr.io/neiam/waxx:latest
195 +```
196 +
197 +Or set `DATABASE_URL=ecto://user:pass@host/db` instead of the discrete
198 +`POSTGRES_*` variables.
199 +
174 200 ## Deployment
175 201
176 202 The repo ships a Kubernetes deploy plan modelled on the one used for
modified config/test.exs
+6 −3
@@ -8,11 +8,14 @@ import Config
8 8 config :waxx, Waxx.Repo,
9 9 username: "postgres",
10 10 password: "postgres",
11 − hostname: "localhost",
12 − port: 54328,
11 + hostname: System.get_env("POSTGRES_HOST") || "localhost",
12 + port: String.to_integer(System.get_env("POSTGRES_PORT") || "54328"),
13 13 database: "waxx_test#{System.get_env("MIX_TEST_PARTITION")}",
14 14 pool: Ecto.Adapters.SQL.Sandbox,
15 − pool_size: System.schedulers_online() * 2
15 + # One sandbox connection per concurrent async test. On a many-core CI runner
16 + # this can exceed Postgres' max_connections (default 100) — cap it there with
17 + # POOL_SIZE. test_helper.exs keeps ExUnit's max_cases in lockstep.
18 + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "#{System.schedulers_online() * 2}")
16 19
17 20 # We don't run a server during test. If one is required,
18 21 # you can enable the server option below.
modified test/test_helper.exs
+6 −1
@@ -1,2 +1,7 @@
1 −ExUnit.start()
1 +# Cap ExUnit's concurrency at the Repo pool size so async tests never wait on a
2 +# sandbox connection (each owns one for its duration). Without this, the default
3 +# `max_cases` (= cores × 2) on a many-core runner outruns a capped `POOL_SIZE`.
4 +max_cases = Application.get_env(:waxx, Waxx.Repo)[:pool_size]
5 +
6 +ExUnit.start(max_cases: max_cases)
2 7 Ecto.Adapters.SQL.Sandbox.mode(Waxx.Repo, :manual)

Parents: 44d57fe 4ae2807