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 (release-train coupling) and are not renamed by the Phase-106 split.
Phase 106 trigger split (forward reference). Phase 106 splits CI work 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 (quality,integration,contract,proof,adopter) are unchanged. Full rationale:106-LANE-CLASSIFICATION.md.
| 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 (strict) | advisory | Same job | Step-level continue-on-error |
quality — Doctor (full, raise) | advisory | Same job | Step-level continue-on-error |
quality — Verify AV runtime with public doctor task | advisory | Same job | Step-level continue-on-error |
quality — Run tests with coverage | merge-blocking | Same job | Default mix test suite via Coveralls; both matrix cells must pass |
quality — Dialyzer | advisory (until Phase 106) | Same job | Step-level continue-on-error. Phase 106 extracts this into an owned, gating Dialyzer job in nightly.yml (removed from PR runs) |
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 | needs: [quality, optional-dependencies] | Lifecycle + MinIO adapter tests |
contract — Run AV hygiene gate | merge-blocking | needs: [quality, optional-dependencies] | scripts/assert_av_hygiene.sh |
contract — Run contract tests | advisory | Same job | Step-level continue-on-error; job still required in graph |
proof | merge-blocking | needs: [quality, optional-dependencies] | docs_parity_test.exs, 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 — Phase 106: runs only inside package-consumer-full, so it is no longer on the PR lane (release/main gate, not merge-blocking on PRs) |
package-consumer (lean, PR) | merge-blocking | needs: [quality, optional-dependencies] | Phase 106: representative image-only install-smoke + version alignment; stays in CI Summary.needs |
package-consumer-full | off-critical-path | push:main/release (if: github.event_name != 'pull_request') | Phase 106: 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 | merge-blocking | needs: [quality, optional-dependencies]; repo szTheory/rindle only | Playwright browser proof for examples/adoption_demo (image, tus, stretch journeys, admin lifecycle render, homepage cold-start smoke) |
cohort-demo-smoke | merge-blocking | needs: [quality, optional-dependencies]; repo szTheory/rindle only | Docker-compose cold-start gate (scripts/ci/cohort_demo_smoke.sh): builds the demo image, boots the full stack, asserts homepage + admin console serve 200 with seeded data — the boot path human UAT used to cover |
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 absent. Phase 106: stays in ci.yml as a label-gated PR lane (NOT moved to nightly) |
gcs-soak | nightly (gating) | nightly.yml: schedule 07:27 UTC / workflow_dispatch; no needs:; repo szTheory/rindle + secrets | Skipped when secrets absent. Phase 106: moved to nightly.yml and gating — continue-on-error dropped, so a live-GCS regression is real 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 present (skipped otherwise). Phase 106: moved to nightly.yml and gating — continue-on-error dropped, so a live-GCS regression is real nightly red |
Static analysis policy (CI-04)
Decision: Credo (strict) and Dialyzer remain advisory in the quality
job. Wiring uses step-level continue-on-error: true in
.github/workflows/ci.yml.
Making either tool merge-blocking is explicitly rejected for the current release train.
Rationale:
- Signal value: Static analysis catches style and typespec drift; failures remain visible in CI logs for maintainers without blocking adopter-critical merge lanes.
- Fork latency: Dialyzer PLT build is slow; merge-blocking would raise contributor and fork PR cost disproportionate to adopter impact.
- Green-main honesty: Adopter-critical lanes are already merge-blocking (
mix coveralls,proof,package-consumer,adopter,integration, contract AV hygiene). Static analysis is maintainer hygiene, not adopter contract.
Doctor and AV doctor steps remain advisory without a separate CI-04 decision record (CI-04 names Credo and Dialyzer only). See matrix rows above.
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, adopter, brandbook-tokens, and ci-script-tests. As of Phase 106,
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').
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 FedericoCarboni/setup-ffmpeg so CI exercises the same runtime posture:
- name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3
with:
ffmpeg-version: 6.0
- 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.