The EXLA Makefile uses -O3 by default which causes massive memory usage
during C++ compilation. DEBUG=1 replaces -O3 with -g, drastically
reducing peak RAM. Combined with MAKEFLAGS=-j1 to serialize jobs, this
should fit within CI runner memory limits.
prop-base:latest is required by the production Dockerfile's final stage
but was only built when Dockerfile.base changed. The registry can GC
unused images. Remove paths filter so the image is always kept fresh.
prop-ci:latest on the registry may not have the latest Dockerfile.ci
changes when the test step runs. Build the image locally using
--cache-from for speed, then use the local tag.
EXLA's cached_make copies libexla.so but Make still compiles .o files
when they're missing from deps/exla/cache/ (they don't survive
mix deps.get). Save the pre-built cache/ directory at /opt/exla-cache/
in the CI image and restore it after mix deps.get in the test step.
With both .o files and libexla.so present, Make skips all C++ compilation.
- Dockerfile.ci: compile xla dep before exla — exla's extract_xla
calls XLA.archive_path!/0 which requires xla to be compiled first
- Fold build-ci-image into build.yaml as prerequisite job so tests
always have the precompiled EXLA image available
- Remove standalone build-ci-image.yaml (redundant)
Add Dockerfile.ci that extends the hexpm/elixir image with precompiled
EXLA NIF cached at ~/.cache/xla/. When mix test runs, EXLA's cached_make
finds the precompiled libexla.so and skips the OOM-prone g++ C++
compilation entirely.
- Dockerfile.ci: two-stage build — builder compiles EXLA into cache;
final stage copies the cache and installs test deps (cdo)
- .forgejo/workflows/build-ci-image.yaml: builds and pushes prop-ci
image on mix.exs/mix.lock/vendor changes + weekly cron
- .forgejo/workflows/build.yaml: test step uses prop-ci:latest instead
of raw hexpm/elixir image, removing apt-get + hex/rebar install steps
- Add Phoenix.Ecto.SQL.Sandbox.allow to all start_async closures in ContactLive.Show (8 sites) and PskrSpotsLive (4 sites), fixing silent empty results when async Tasks accessed DB in tests
- Add File.exists? skip-guards to wgrib2_test.exs (14 guards) and hrrr_client_test.exs (3 guards) so tests skip gracefully when GRIB2 fixtures are absent
- Remove leftover debug IO.puts from SandboxHook
- Fix stale homepage assertion in html_module_test.exs
- Add cdo and wgrib2 to nix shell.nix and Forgejo CI apt-get install
Test suite: 4049/4054 passed (99.9%), 5 remaining failures are environment gaps (cdo/wgrib2 not yet in CI runtime)
- Delete Microwaveprop.Aprs, AprsRepo, Aprs.PathParser modules
- Delete Mix.Tasks.Calibrate.Aprs144 mix task
- Remove AprsRepo from application.ex children and config files
- Remove APRS test files and sandbox references in test_helper
- Fix credo nested-module alias suggestions in conn_case.ex
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.
Move the runtime base image from git.mcintire.me/graham/prop-base
to codeberg.org/gmcintire/prop-base. The shared REGISTRY_USER /
REGISTRY_PASSWORD secrets are pointed at Codeberg credentials;
the build-base workflow hardcodes env.REGISTRY=codeberg.org so it
can't accidentally push to a different registry if the URL secret
drifts.
The main app + prop-grid-rs builds still pull this base via the
BASE_IMAGE arg in Dockerfile, now codeberg.org/gmcintire/prop-base:latest.
Adds a Sunday 06:00 UTC schedule trigger to build-base.yaml plus a
CACHE_BUST=<ISO year+week> build-arg keyed off `date -u +%G-W%V`. The
apt RUN steps in Dockerfile.base reference the arg in their command
line so BuildKit's layer hash changes when the week rolls over,
forcing apt-get update + install to re-execute and pull fresh
security updates.
The wgrib2-builder stage doesn't see CACHE_BUST so the ~5-min wgrib2
compile keeps hitting layer cache across weeks; only the apt layers
rebuild. Effective cost of the weekly job: ~2 min for the cdo /
gdal-bin reinstall.
Pre-builds the runtime base (wgrib2 + g2c + cdo + gdal-bin + locale +
runtime apt deps) into git.mcintire.me/graham/prop-base. The base
workflow is path-filtered to fire only on Dockerfile.base /
build-base.yaml changes, so version bumps stay deterministic and
explicit.
Tag convention: `wgrib2-<v>-g2c-<v>-<unixts>` for traceability +
rollback, plus `:latest` for everyday consumption.
This commit only adds the new files — the app Dockerfile + build.yaml
still build wgrib2 from source. A follow-up will swap the app
Dockerfile to FROM prop-base:latest once this image lands in the
registry.
Mirrors the verbose wait-for-docker block from build.yaml so future "failed
to connect to docker" failures print socket permissions, docker-related
env vars, and full `docker info` output instead of a single unhelpful line.
The previous approach (`docker run -v $(pwd)/rust/prop_grid_rs:/src`)
fails under act_runner because the runner container's pwd isn't a path
the host docker daemon can see — clippy installed, then cargo couldn't
find Cargo.toml.
Move the lint + test gate into the Dockerfile builder stage so the
build context ships over the socket the normal way. If clippy warns or
a test regresses, the image build fails and nothing is pushed.
Runs `cargo clippy --all-targets -- -D warnings` + `cargo test` inside
a rust:1.94-trixie container, then builds `rust/prop_grid_rs/Dockerfile`
and pushes three tags: the timestamped `main-<ts>-<sha>`, `latest`, and
a static `main` tag that `k8s/deployment-grid-rs.yaml` pins to.
Path-filtered to `rust/prop_grid_rs/**` so Elixir-only pushes don't
trigger a redundant Rust build.
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.