AGENTS.md
Rama main · réplica de lectura del repositorio original.
# AGENTS.md
Operational guide for coding agents and contributors working in the **FreeFrame** repo.
FreeFrame is a self-hostable, open-source alternative to Frame.io — collaborative media
review, annotation, and approval for images, audio, and video.
This file focuses on **how to make a change that passes CI and review here**. For depth:
- **Architecture:** [`docs/architecture.md`](docs/architecture.md)
- **Human setup & standards:** [`docs/contributing.md`](docs/contributing.md)
- **Deployment:** [`docs/deployment.md`](docs/deployment.md)
- **Live API surface:** http://localhost:8000/docs (Swagger) once the stack is up
---
## Quickstart
```bash
git clone https://github.com/YOUR_USERNAME/freeframe.git
cd freeframe
cp .env.example .env
docker compose -f docker-compose.dev.yml up --build
# open http://localhost:3000
```
Ten services start in Docker: postgres, redis, minio, minio-init, api, worker, email_worker,
maintenance_worker, beat, web.
> **Hot reload covers `api` and `web` only.** The four Celery containers have no autoreload, so
> a change under `apps/api/tasks/` (or to anything a task imports) does nothing until you restart
> them:
> ```bash
> docker compose -f docker-compose.dev.yml restart worker email_worker maintenance_worker beat
> ```
> **The dev stack really deletes local data on a schedule.** `maintenance_worker` and `beat` run
> the stale-upload reaper hourly and hard-delete soft-deleted rows at 03:00 UTC, reclaiming S3
> objects. Set `STALE_UPLOAD_TIMEOUT_HOURS=0` and `SOFT_DELETE_RETENTION_DAYS=0` in `.env` to
> switch both off while you work.
### Dev endpoints
| What | URL / address | Notes |
|---------------------|------------------------------|-----------------------------------------|
| Frontend (Next.js) | http://localhost:3000 | hot reload |
| API (FastAPI) | http://localhost:8000 | |
| API docs (Swagger) | http://localhost:8000/docs | the fastest way to find an endpoint |
| Silo console | http://localhost:9001 | S3 API is on :9000 (Silo, a MinIO fork) |
| Postgres | `localhost:5433` (dev only) | host mapping `5433:5432`; in-container and prod it's `5432` |
| Redis | `localhost:6379` | |
---
## Repo layout
```
freeframe/
├── apps/
│ ├── api/ # FastAPI backend
│ │ ├── main.py # app entry point
│ │ ├── config.py # env-driven settings
│ │ ├── models/ # SQLAlchemy ORM models
│ │ ├── schemas/ # Pydantic request/response schemas
│ │ ├── routers/ # API route handlers
│ │ ├── services/ # business logic (auth, s3, permissions, …)
│ │ ├── tasks/ # Celery tasks (transcode, email, watermark, reminders,
│ │ │ # cleanup — the only code that hard-deletes)
│ │ ├── middleware/ # auth deps, share-link auth, rate limiting, setup guard
│ │ ├── alembic/ # database migrations
│ │ └── tests/ # pytest suite (mock-DB based — see Gotchas)
│ └── web/ # Next.js 16 App Router frontend
│ ├── app/ # routes/pages
│ ├── components/ # React components
│ ├── hooks/ # React hooks
│ ├── lib/ # API client + utilities
│ └── stores/ # Zustand stores
└── packages/
└── transcoder/ # pluggable transcoder package (FFmpeg default)
```
---
## Run the checks CI runs
A PR is mergeable when these are green. **Run them before you say you're done.** CI
(`.github/workflows/ci.yml`) runs the same commands.
**Backend** (from repo root, or inside the `api` container):
```bash
# local (what CI runs)
python -m pytest apps/api/tests/ -v
# or inside the running dev container
docker compose -f docker-compose.dev.yml exec api python -m pytest apps/api/tests/ -v
```
**Frontend** (workspace filter `web`, run from repo root):
```bash
pnpm --filter web build # must succeed (CI gate)
pnpm --filter web test
pnpm --filter web exec tsc --noEmit # type check
pnpm --filter web lint
```
**Installer** (`install.sh`, `deploy/freeframe`; CI's `installer` job runs these on
Ubuntu 24.04 with pinned ShellCheck 0.11.0 and bats-core 1.14.0):
```bash
shellcheck -x install.sh deploy/freeframe deploy/tests/ci-e2e.sh
bats --count deploy/tests # CI fails below 450 tests, a floor against deleted tests
bats --timing deploy/tests # needs bats-core 1.5 or newer (Ubuntu 22.04's apt bats is too old)
# without a recent bats, or on macOS (the Alpine image skips the few tests that
# need flock, setsid or systemd-analyze):
docker run --rm -v "$PWD:/code" -w /code bats/bats:latest deploy/tests
```
The installer end-to-end job (`installer-e2e`) runs `bash deploy/tests/ci-e2e.sh PHASE`
for each of `prepare`, `foreign`, `build`, `install`, `api`, `backup-restore`, `update`,
`rerun` and `status` (and `diagnose` on failure), in that order, on a GitHub runner.
**Never run it on your machine:** it uses sudo, writes `/opt/freeframe` and
`/usr/local/bin/freeframe`, and starts a compose project named `freeframe`, the name a dev
FreeFrame uses too (it refuses to run outside GitHub Actions). On a pull request it runs
only when the change touches `deploy/**`, `install.sh`, `docker-compose*.yml`,
`apps/*/Dockerfile.prod*`, `apps/*/docker-entrypoint.sh`, `*.dockerignore` or
`.github/workflows/ci.yml`; every push to main runs it.
After #585 merges, CI also lints the workflows: run `actionlint` from the repo root.
> **Use the Node version in [`.nvmrc`](.nvmrc).** CI reads it, and so do `nvm use` and `fnm`.
> A different system Node can fail tests that pass in CI. CI also checks that npm, which
> `docker-compose.dev.yml` installs `apps/web` with, can still resolve `apps/web/package.json`.
> CI ignores changes limited to `*.md`, `docs/**`, `LICENSE`, and the issue/PR templates,
> so a docs-only PR (like editing this file) will not trigger the test/build jobs.
---
## ⚠️ Gotchas that trip up agents
Read this section before writing backend code or tests.
- **Backend tests run against a fully *mocked* database.** `apps/api/tests/conftest.py`
provides a `MagicMock` `Session` (the models use Postgres-specific UUID types that are
incompatible with SQLite), so there is **no real DB in tests**. Don't write tests that
expect real persistence. Instead patch the query/service layer and drive behavior through
the `client` + `mock_db` fixtures. See `apps/api/tests/test_share_session.py` for the
pattern (patch `validate_share_link`, exercise the real logic on top). Use the `real_db`
fixture only for code paths explicitly designed for a live transactional Postgres.
- **CI has floor guards — never delete or gut tests/core files.** The pipeline fails if:
fewer than 5 test files exist, fewer than 40 tests pass, the FastAPI app exposes fewer
than 30 routes, or any file on its critical-files allowlist (e.g. `apps/api/main.py`,
`routers/share.py`, `services/permissions.py`, key `apps/web` files) goes missing. If a
test is genuinely obsolete, replace it — don't remove coverage.
- **Soft delete is common but not universal, and it is not automatic.** 13 of the 29 models
carry a `deleted_at` column; 16 do not, including `MediaFile`, `Annotation`,
`CommentAttachment`, `CommentReaction`, `Notification` and `ActivityLog`. Check the model
before assuming. Where the column exists, prefer setting it over deleting the row, and
filter `deleted_at.is_(None)` **by hand in every query** — nothing filters it for you,
there is no automatic soft-delete layer. Soft deletion is not
permanent either: `tasks/cleanup_tasks.py` hard-deletes soft-deleted rows and reclaims their
S3 objects after `SOFT_DELETE_RETENTION_DAYS` (default 30).
- **Model change ⇒ Alembic migration.** After editing a SQLAlchemy model:
```bash
docker compose -f docker-compose.dev.yml exec api sh -c "cd apps/api && alembic revision --autogenerate -m 'describe change'"
```
**Review the generated migration** before committing — autogenerate is not always right.
- **Config is env-driven** (`apps/api/config.py`, `.env.example`). Add new settings there
with safe defaults; don't hardcode secrets, endpoints, or limits.
---
## Conventions
- **Commits:** [Conventional Commits](https://www.conventionalcommits.org/) style —
`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, optional scope (`fix(share): …`).
- **One focused change per PR.** Target the `main` branch. Branch names: `feat/<slug>`,
`fix/<slug>`.
- **Tests required** for new behavior or bug fixes (a fix should ship a regression test that
fails before and passes after).
- **CHANGELOG:** add user-facing changes to `CHANGELOG.md` under the **`[Unreleased]`**
heading (Keep a Changelog format: `Added` / `Changed` / `Fixed`). Don't invent or cut a
version — releases are handled separately.
- **UI changes:** include before/after screenshots in the PR (see
`.github/PULL_REQUEST_TEMPLATE.md`).
- **Docs:** update `docs/` or user-facing text when you change behavior.
---
## Releases & branches
FreeFrame ships **moving branch pointers** on top of immutable `vX.Y.Z` tags. Know which is which:
- **`main`** — active development. PRs target `main`; it may be ahead of any release.
- **`stable`** — the last **validated** release. **This is what production self-hosters run** (`git clone -b stable`). Never tell users to run `main` in production, and don't point install/deploy docs at `main` — point them at `stable`.
- **`latest`**: the newest published release (auto-moved by `.github/workflows/publish-images.yml` once that release's images are published). For early adopters.
- **`vX.Y.Z`** — immutable release tags; never move or delete them.
Rules for agents:
- **Don't create, move, or force-push `stable` / `latest`**: they are moved only by the release workflows (`publish-images.yml` on release publish, after the images; `promote-stable.yml` manually, which gates on green CI). See [`docs/RELEASING.md`](docs/RELEASING.md).
- **Don't cut releases or tags** unless explicitly asked — releases are manual and CHANGELOG-driven.
- Default user-facing install/deploy instructions to `stable` (see the README "Release channels" table).
---
## Finding things & getting help
- **An endpoint or schema:** browse http://localhost:8000/docs, or grep `apps/api/routers/`.
- **Report a bug / request a feature:** use the
[issue templates](https://github.com/Techiebutler/freeframe/issues/new/choose).
- **Security issues:** follow [`SECURITY.md`](SECURITY.md) — do not open a public issue.
- **License:** contributions are MIT-licensed ([`LICENSE`](LICENSE)).
```