Use this guide for host-runtime dependencies before Rindle background jobs process variants. Image processing uses libvips (via Vix). AV processing uses FFmpeg.
Image runtime (libvips)
Image-only adopters need libvips on the host before ProcessVariant jobs run:
- install libvips for the target platform
- run
mix rindle.doctor - only then start background jobs that generate image variants
| Platform | Install |
|---|---|
| macOS (Homebrew) | brew install vips |
| Ubuntu / Debian (apt) | sudo apt-get update && sudo apt-get install -y libvips-dev |
| Alpine (apk) | apk add --no-cache vips-dev |
| GitHub Actions | sudo apt-get install -y libvips-dev (same as CI quality job) |
AV runtime (FFmpeg)
Use this section when your adopter app enables video or audio processing. The AV runtime contract is small and explicit:
- install
FFmpeg >= 6.0for the target platform - run
mix rindle.doctor - only then start background jobs that process AV variants
README stays the narrow quickstart. Getting Started is the canonical deep onboarding guide. This file is the shared install/runtime matrix both of those entrypoints link to.
Maintainer: CI lane severity
Adopters can skip this section. It documents how this repository gates merges and releases.
.github/workflows/ci.yml is the source of truth for job wiring; GitHub branch protection and required-check settings live outside the repo. The name: CI and ci.yml filename are invariant because release automation selects that workflow by name and filename.
CI work is split by trigger so only representative signal stays on the PR critical path (≤7 min target):
- The
package-consumerlane is split. A lean representativeimage-onlypackage-consumerruns on PR (stays merge-blocking viaCI Summary); a newpackage-consumer-fullruns onpush:main/release with the full 5-profile matrix
- release preflight +
hex.publish --dry-runand is NOT a required PR check.- The broad OTP×Elixir compat matrix,
gcs-soak,package-consumer-gcs-live, and an owned gating Dialyzer lane move to a separatenightly.yml(name: Nightly), advisory and never a required PR check.mux-soakstays here inci.ymlas a label-gated PR lane (not moved to nightly).The
name: CI/ci.ymlfilename invariant and the merge-blocking PR lanes remain unchanged. The table below is the maintained public classification.
| Job / step | Severity | When it runs | Notes |
|---|---|---|---|
quality — Compile, Check formatting | merge-blocking | Every PR/push; Elixir 1.15/OTP 26 and 1.17/OTP 27 matrix | Both matrix cells must pass |
quality — Credo quality | merge-blocking | Canonical lint cell (Elixir 1.17/OTP 27) | scripts/maintainer/credo_quality.sh: reviewed warnings, public-contract checks, and complexity inventory |
quality — Doctor (full, raise) | merge-blocking | Canonical lint cell (Elixir 1.17/OTP 27) | Measured public lib/ report under MIX_ENV=dev |
quality — Credo (strict, advisory style) | advisory | Canonical lint cell | Full-tree style output remains visible with step-level continue-on-error |
quality — Verify AV runtime with public doctor task | advisory | Same job | Requires DB, Oban, and canonical profile readiness that this job intentionally does not prepare |
quality — Run focused AV behavior tests | merge-blocking | Same job, after FFmpeg/libvips installation | Real FFmpeg/ffprobe fixture and Vix/libvips behavior proof; no vips CLI is required |
quality — Run tests with coverage | merge-blocking | Same job | Default mix test suite run once via mix coveralls.multiple --type local --type json (single run → console gate + cover/excoveralls.json); both matrix cells must pass |
optional-dependencies | merge-blocking | Every PR/push; Elixir 1.15/OTP 26 and 1.17/OTP 27 matrix | ADMIN-06 proof: mix deps.get --no-optional-deps and mix compile --no-optional-deps --warnings-as-errors |
integration | merge-blocking | Starts independently; CI Summary waits for it | Lifecycle + MinIO adapter tests plus the disposable-database migration E2E suite (documented Ecto.Migrator path, lock contention, and real-role privilege refusals) |
contract — Run AV hygiene gate, contract tests, SAFE-01 | merge-blocking | Starts independently; installs FFmpeg before its proof | AV hygiene plus deterministic --only contract tests and scripts/maintainer/refactor_contract.sh; the telemetry contract exercises the real AV path |
proof | merge-blocking | needs: [quality, optional-dependencies] | docs-parity install/migrations, onboarding/capabilities, operations, and product/admin domain suites; adoption proof matrix drift gate; batch_owner_erasure_task_test.exs; Postgres only; Elixir 1.17/OTP 27 |
package-consumer-full — repo hygiene gate | off-critical-path | push:main/release (if: github.event_name != 'pull_request') | scripts/maintainer/repo_hygiene_check.sh --ci; release/main gate, not merge-blocking on PRs |
package-consumer (lean, PR) | merge-blocking | Starts independently; CI Summary waits for it | Representative image-only install-smoke + version alignment; full profile breadth remains on main/release |
package-consumer-full | off-critical-path | push:main/release (if: github.event_name != 'pull_request') | Full 5-profile matrix + release preflight + hex.publish --dry-run; NOT a required PR check (omitted from CI Summary.needs); release proof is the push:main run conclusion |
adoption-demo-unit | merge-blocking | needs: [quality, optional-dependencies]; Postgres only | Fast ExUnit proof for examples/adoption_demo: brand mark/wordmark, admin-console mount, lifecycle-state display, README walkthrough parity (storage-free, direct-insert seeds) |
adoption-demo-e2e-smoke | merge-blocking | Every PR; starts independently with Postgres + MinIO-local | Lean Chromium smoke (e2e/smoke.spec.js + e2e/admin-console.spec.js only, no screenshot spec) in the pinned Playwright container. No secrets or repo/event gate, so it runs on forks and is included in CI Summary.needs. |
adoption-demo-e2e | off-critical-path | push:main only (repo szTheory/rindle + if: github.event_name != 'pull_request'); needs: [quality, optional-dependencies] | Full Playwright browser proof for examples/adoption_demo (image, tus, stretch journeys, admin lifecycle render, homepage cold-start smoke + screenshot specs). NOT in CI Summary.needs; its PR-side proxy is the lean adoption-demo-e2e-smoke lane above. |
cohort-demo-smoke | off-critical-path | push:main only (repo szTheory/rindle + if: github.event_name != 'pull_request'); needs: [quality, optional-dependencies] | Docker-compose cold-start gate (scripts/ci/cohort_demo_smoke.sh) that builds the demo image, boots the stack, and asserts the seeded homepage and admin console serve 200. NOT in CI Summary.needs; it is a push:main/release signal. |
brandbook-tokens | merge-blocking | needs: [quality, optional-dependencies]; repo szTheory/rindle only | PIPE-01 drift gate: regenerates brandbook token CSS, admin CSS, gallery proof, and shipped priv/ CSS copy, then fails on any generated-artifact diff |
adopter | merge-blocking | needs: [quality, optional-dependencies, integration, contract] | Canonical adopter lifecycle only (doc parity in proof job) |
mux-soak | secret-gated soak (label-gated PR lane) | Label streaming on PR; needs: quality | Not in branch protection required checks; fails closed when secrets are absent. |
dialyzer | nightly (gating) | nightly.yml: schedule 07:27 UTC / workflow_dispatch | Owned type-contract lane; not a required PR check, but a failure makes Nightly red. |
gcs-soak | nightly (gating) | nightly.yml: schedule 07:27 UTC / workflow_dispatch; no needs:; repo szTheory/rindle + secrets | Skipped when secrets are absent; a live-GCS regression makes Nightly red. |
package-consumer-gcs-live | nightly (gating) | nightly.yml: schedule 07:27 UTC / workflow_dispatch; no needs:; repo szTheory/rindle + secrets | Live GCS install-smoke when secrets are present (skipped otherwise); failures make Nightly red. |
Reproducing the coverage step locally (COV-04)
The full CI coverage step — the quality — Run tests with coverage row above — is
reproduced locally with a single command:
mix coveralls.multiple --type local --type json
One suite run emits both the console coverage gate and cover/excoveralls.json.
--type local runs the same local analyzer / ensure_minimum_coverage as the
gate; --type json is a side-artifact only and never decides pass/fail.
Local async-isolation evidence (issue #42)
The issue-evidence matrix is a maintainer-only local command, not a CI job and not a replacement for the single Quality coverage invocation. First inspect its fixed 25-seed plan without running coverage:
bash scripts/maintainer/async_isolation_evidence.sh --validate
When collecting finite evidence, follow the active maintainer procedure for a new report path. The runner rejects existing report files and paths outside the maintainer-managed evidence location.
The runner starts one fresh foreground mix coveralls.multiple --type local --type json --seed SEED --slowest 20 process per fixed seed, stops at the first nonzero
exit, and records only the seed, revision, toolchain, argv, exit, and a bounded
sanitized failure location. Do not add this loop to CI or run a second coverage
command for any one seed.
To reproduce the merge-blocking gate alone (no JSON artifact), mix coveralls
is unchanged — it runs the identical local analyzer and produces the same
pass/fail verdict.
Truthful quality policy
The reviewed Credo aggregate and measured public Doctor report are merge-blocking on the canonical Quality lint cell. Reproduce them locally with:
mix credo_quality
MIX_ENV=dev mix doctor --full --raise
mix refactor_contract
mix quality_signals runs those three deterministic checks in that order, and mix ci
includes it before the repository's one default test-suite execution. The full-tree
mix credo --strict --format oneline style report remains intentionally advisory in CI;
it is visible for maintainer review but is not the reviewed actionable policy.
Active planning acceptance is also executable. After changing a current PLAN or VALIDATION artifact, run:
./scripts/maintainer/automation_first_contract.sh
The contract rejects manual-only verification/UAT while permitting checkpoints whose sole purpose is credential bootstrap or irreversible-action authorization. Repository hygiene runs the same check.
For focused real AV behavior, first install the host prerequisites described above (FFmpeg >= 6 and libvips for Vix), then run:
mix test test/rindle/probe/av_probe_test.exs test/rindle/processor/image_test.exs --seed 0
This test command exercises FFmpeg/ffprobe and Vix/libvips directly; it does not require
a standalone vips CLI. The public mix rindle.doctor remains a separate, advisory
runtime readiness command in Quality because that job intentionally lacks the adopter's
DB, Oban, and profile-host setup. Run it only from a DB/Oban/profile-ready adopter host.
Dialyzer remains an owned, gating Nightly signal, outside the PR-local alias and critical
path. Push-main/full-verification-only lanes likewise remain outside mix ci; the release
workflow still relies on the complete ci.yml run for the exact release SHA.
Release train
.github/workflows/release.yml gate-ci-green waits for
ci.yml on the release SHA to finish with conclusion success. When the latest run is
not green, or the wait times out, publish fails closed — there is no bypass path.
Branch protection enforces a single required status check, CI Summary (enforced via
scripts/setup_branch_protection.sh, REQUIRED_CHECKS=("CI Summary")). None of the individual
lanes are required contexts; they gate merges transitively through CI Summary.needs, which lists
quality, optional-dependencies, integration, contract, proof, package-consumer (lean),
adoption-demo-unit, adoption-demo-e2e-smoke, adopter, brandbook-tokens, and
ci-script-tests.
cohort-demo-smoke and adoption-demo-e2e run only on push:main and are NOT in
CI Summary.needs — their regressions are caught on main (and block release via the push:main run
conclusion), not on the PR merge gate. package-consumer-full is likewise omitted from
CI Summary.needs (it is if: github.event_name != 'pull_request'). The lean
adoption-demo-e2e-smoke lane is the merge-blocking PR-side browser-render proxy for the
push:main-only full E2E lane.
Verify The Runtime
Run this in the adopter app after mix deps.get and after installing FFmpeg:
mix rindle.doctor
The command must pass before you debug Oban workers, variant failures, or delivery URLs.
FFmpeg Install Matrix
macOS (Homebrew)
brew install ffmpeg
mix rindle.doctor
Ubuntu / Debian (apt)
sudo apt-get update
sudo apt-get install -y ffmpeg
mix rindle.doctor
Alpine (apk)
apk add --no-cache ffmpeg
mix rindle.doctor
Fly.io Dockerfile
Add FFmpeg to the image build:
RUN apt-get update \
&& apt-get install -y ffmpeg \
&& rm -rf /var/lib/apt/lists/*Run mix rindle.doctor during build or release validation before the app
starts workers.
Heroku Aptfile
Add an Aptfile at the app root with:
ffmpegThen run mix rindle.doctor as part of release validation.
Render Dockerfile
Add FFmpeg to the Render image build:
RUN apt-get update \
&& apt-get install -y ffmpeg \
&& rm -rf /var/lib/apt/lists/*Run mix rindle.doctor in the build or pre-deploy command.
GitHub Actions
Use the repository installer so CI resolves a stable static build and validates the required FFmpeg major version:
- name: Install FFmpeg
run: bash scripts/ci/install_ffmpeg.sh
- name: Verify Rindle runtime
run: mix rindle.doctorCanonical AV Profile Shape
The onboarding story stays on the stock web_720p plus poster surface. The
explicit variant declarations are:
variants: [
web_720p: [kind: :video, preset: :web_720p],
poster: [kind: :image, preset: :video_poster_scene]
]That is the same public posture taught in README and Getting Started.