Adds a postgres:17-alpine service, a wait-for-postgres step, and a
Run tests step that runs mix ecto.create/migrate/test in the same
docker-run pattern. If tests fail, the Build and push step is
automatically skipped.
The runner is containerised so the host Docker daemon cannot see its
filesystem. Replace `-v ${{ github.workspace }}:/app` with a
`tar | docker run -i ... tar xf -` pipeline to stream the workspace
into the verification container.
P0 (security-critical):
- Gate CSV/ADIF upload tabs behind authentication, add 30s cooldown to all upload handlers
- Cap CSV/ADIF imports at 2,000 rows server-side in both parsers
- Add submitter_verified boolean to contacts (client-cannot-set, anonymous=false)
- Create k8s/secret.example.yaml with placeholders, add LIVE_VIEW_SIGNING_SALT
P1 (high-priority):
- Add Mox.verify_on_exit!() to valkey_test.exs
- Replace DateTime.utc_now() truncation with static ~U literals in map_live_test.exs
- Replace Process.sleep with render_async in pskr_spots_live_test.exs (6 occurrences)
- Add MonitorLive.Show test coverage (4 tests: owner view, non-owner redirect, config success/error)
- Extract duct-detection and mechanism-classification logic from ContactLive.Show into Propagation.PathAnalysis
- Split ContactLive.Show render into 12 function components
- Update CLAUDE.md: remove stale ML model, mark HRDPS active, add backtest/pskr dirs
- Batch CSV import enrichment jobs via new enqueue_for_contacts/1
P2 (medium-priority):
- Set secure:true on session and remember-me cookies in production
- Change SMTP TLS from verify_none to verify_peer with public_key cacerts
- Make /metrics fail-closed in production when PROMETHEUS_AUTH_TOKEN unset
- Add RateLimiter (anon_limit:10, auth_limit:60) to /api/contacts/map
- Add content-security-policy-report-only header
- Add comment noting String.to_atom is compile-time safe in hrdps_client.ex
- Delegate duplicated haversine_km to canonical Microwaveprop.Geo.haversine_km/4
- Consolidate score-tier/color/verdict formatting into Microwaveprop.Format
- Update CLAUDE.md testing section to match actual raw-string-matching practice
- Batch HrrrPointEnqueuer Repo.insert_all calls to single round-trip
- Split weather.ex (1696→216 lines) and radio.ex (1285→54 lines) into purpose-based sub-facades
P3 (low-priority):
- Add LIVE_VIEW_SIGNING_SALT warning comment, extend filter_parameters
- Add host/community validation to snmp_client.ex
- Add raw/1 safety comment in algo_live.ex
- Add hex-audit and cargo-audit Makefile targets
- Add privacy_live smoke test
- Replace notify_listener busy-poll loop with Process.monitor/1 + assert_receive
- Add ContactCommonVolumeRadar changeset validation tests (5 tests)
All three image build workflows previously hardcoded REGISTRY=codeberg.org
and only pulled USER/PASSWORD from secrets. Switching the URL to come from
the same secrets bag means a registry rotation only needs the
REGISTRY_URL / REGISTRY_USER / REGISTRY_PASSWORD secrets updated, not a
commit to every workflow file.
Both build workflows now push to codeberg.org/gmcintire/<image>
alongside the runtime base. Login URL is the hardcoded env.REGISTRY
rather than secrets.REGISTRY_URL so a stale/wrong URL secret can't
silently push to the wrong registry.
k8s deployment manifests still reference git.mcintire.me images and
need a follow-up bump to codeberg-side tags after the next CI build
lands an image at codeberg.org.
The host docker socket wasn't mounted consistently across act_runners,
so which runner picked up the push was the difference between a clean
build and "unable to connect to docker." Switch to a dockerd service
pinned to the same bridge network as the job; the CLI now talks to
it over TCP via DOCKER_HOST=tcp://dockerd:2375. Every runner has a
daemon, every time — no more re-runs for the scheduling lottery.
The 30s poll is enough to ride out a slow daemon startup, but not a
runner where the socket isn't mounted at all. Print DOCKER_HOST, the
socket stat, and the verbose `docker info` error on timeout so the
next flaky run tells us whether it's a startup race or a missing
mount.
Forgejo act_runner mounts the host docker socket into the job
container; the daemon sometimes takes a beat to answer, which has
been surfacing as intermittent "unable to connect to docker" failures
on login. Gate the build on a `docker info` poll (up to 30s) so later
steps don't race the socket, and wrap login + buildx push in a 3-try
exponential-backoff (2s/4s/8s) so a transient registry hiccup doesn't
require a manual workflow re-run.
The Dockerfile uses `--mount=type=cache` which requires BuildKit.
The static docker tarball only ships the CLI, so the plugin has to
be installed separately. Also flip the build step to
`docker buildx build --push` so build and push happen in one pass
instead of two (avoids the intermediate local-daemon load).
The runner image's bookworm apt repos have been failing GPG
verification with "At least one invalid signature was encountered"
on every job. Clearing lists and reinstalling debian-archive-keyring
on the host doesn't help because the runner spins up a fresh
container per job. Dodge apt entirely by grabbing the static Docker
27.5.1 client binary from download.docker.com. The binary talks to
the host's /var/run/docker.sock that act_runner mounts into the job
container, which is all we need for build + push.
Docker final stage bakes BUILD_TIMESTAMP into /app/BUILD_TIMESTAMP via
build-arg (only consumed after mix compile, so earlier layer caching is
preserved). Application.build_timestamp/0 reads the file, falls back to
DEPLOY_TIMESTAMP env, then to compile time. Navbar renders a compact
"Deployed Xm ago" with the full UTC time in the tooltip.
setup-buildx-action downloads from GitHub Releases which is slow
and unreliable from the Forgejo runner. Plain docker build/push
works fine since Docker is already on the runner host.