docs: refocus AGENTS.md on code style and testing for AI agents

Replace CI/CD documentation with practical guidelines for AI coding
agents including module structure, naming conventions, error handling
patterns, Oban worker examples, and testing patterns with Mox.
This commit is contained in:
Amadeus Mader 2026-03-04 14:19:41 +01:00
parent 4d15e0892d
commit 4473f81fa5

359
AGENTS.md
View file

@ -1,218 +1,255 @@
# Agent Guidelines for Pinchflat # Agent Guidelines for Pinchflat
This document provides context and guidelines for AI agents working on this codebase. Guidelines for AI agents working on this Elixir/Phoenix codebase.
## Project Overview ## Project Overview
Pinchflat is an Elixir/Phoenix application for self-hosted media management. It uses: Pinchflat is a self-hosted media management app using:
- **Backend**: Elixir 1.17+, Phoenix 1.7, Ecto with SQLite - **Backend**: Elixir 1.17+, Phoenix 1.7, Ecto with SQLite
- **Frontend**: Phoenix LiveView, Tailwind CSS, esbuild - **Frontend**: Phoenix LiveView, Tailwind CSS, esbuild
- **Background Jobs**: Oban - **Background Jobs**: Oban
- **Containerization**: Docker with multi-arch support (amd64/arm64) - **Testing**: ExUnit with Mox for mocking
## Development Environment ## Build/Lint/Test Commands
This project uses Nix flakes for reproducible development environments.
```bash ```bash
# Enter dev shell # Enter Nix dev shell (required for tooling)
nix develop nix develop
# Or with specific shell # Run all checks (compile, format, credo, tests, sobelow, prettier)
nix develop . --command fish mix check
# Run tests only
mix test
# Run a single test file
mix test test/pinchflat/media_test.exs
# Run a specific test by line number
mix test test/pinchflat/media_test.exs:42
# Run tests matching a pattern
mix test --only describe:"list_media_items"
# Format Elixir code
mix format
# Run Credo linter
mix credo
# Security analysis
mix sobelow --config
# Format JS/CSS/YAML/JSON
yarn run lint:fix
``` ```
### Available Tools (via flake.nix) ## Code Style Guidelines
- `lefthook` - Git hooks manager ### Module Structure
- `actionlint` - GitHub Actions linter
- `typos` - Spell checker
- `prettier` - Code formatter (JS/CSS/YAML/JSON)
- `cocogitto` - Conventional commits
- `just` - Command runner
- Docker tooling (docker, docker-buildx, docker-compose)
## Git Hooks (lefthook) Follow this order in modules:
Pre-commit hooks are configured in `lefthook.yml`: 1. `@moduledoc` - required for all public modules
2. `use`/`import`/`alias`/`require` statements
3. Module attributes (`@allowed_fields`, `@impl`, etc.)
4. Public functions with `@doc`
5. Private functions
| Hook | Files | Purpose | ```elixir
| ------------ | ----------------------------------------- | ------------------- | defmodule Pinchflat.Media do
| `prettier` | `*.{css,html,js,json,md,mjs,ts,yaml,yml}` | Format check | @moduledoc """
| `mix format` | `*.{ex,exs}` | Elixir formatting | The Media context.
| `typos` | All staged files | Spell checking | """
| `actionlint` | `.github/workflows/*.{yml,yaml}` | Validate GH Actions |
### Bypassing Hooks import Ecto.Query, warn: false
use Pinchflat.Media.MediaQuery
If hooks fail due to environment issues (e.g., permission errors): alias Pinchflat.Repo
alias Pinchflat.Media.MediaItem
```bash @doc """
git commit --no-verify -m "message" Returns the list of media_items.
"""
def list_media_items do
Repo.all(MediaItem)
end
defp some_private_function do
# ...
end
end
``` ```
## GitHub Actions Workflows ### Imports and Aliases
### `lint_and_test.yml` - Use `alias` for frequently used modules
- Prefer explicit imports: `import Ecto.Query, warn: false`
- Group aliases alphabetically
- Use `alias __MODULE__` for self-reference in schemas
Runs on PRs and pushes to master. Uses Docker Compose for testing. ### Naming Conventions
Key features: - **Modules**: PascalCase matching file path (`Pinchflat.Media.MediaItem`)
- **Functions**: snake*case with verb prefixes (`get*`, `list*`, `create*`, `update*`, `delete*`)
- **Predicate functions**: end with `?` (`pending_download?/1`)
- **Private functions**: prefix with `do_` for wrapper pattern (`do_delete_media_files`)
- **Workers**: suffix with `Worker` (`MediaDownloadWorker`)
- **Fixtures**: suffix with `_fixture` (`media_item_fixture`)
- Concurrency control (cancels in-progress runs on same PR) ### Formatting
- 30-minute timeout
- Pinned action versions (SHA-based for security)
### `docker_release.yml` - Line length: 120 characters max
- Use `mix format` - config in `.formatter.exs`
- Prettier for JS/CSS/YAML/JSON
Builds and pushes Docker images to GHCR. ### Return Types
**Architecture**: Document returns in `@doc`:
``` ```elixir
prepare job (determines platform matrix) @doc """
| Creates a media_item.
v
build job (parallel) Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}}
├── linux/amd64 on ubuntu-latest (native) """
└── linux/arm64 on ubuntu-24.04-arm (native, NOT QEMU) def create_media_item(attrs) do
| # ...
v end
merge job (creates multi-arch manifest)
``` ```
**Trigger behavior**: ### Error Handling
- Push to master: Builds amd64 only (fast dev iteration) - Use `{:ok, result}` / `{:error, reason}` tuples
- Release published: Builds both amd64 and arm64 - Use `!` suffix for functions that raise (`get_media_item!/1`)
- workflow_dispatch: User chooses platforms - Pattern match on error tuples explicitly
- Rescue specific exceptions when needed:
**Important**: GHCR requires lowercase repository names. The workflow converts `$GITHUB_REPOSITORY` to lowercase using `${GITHUB_REPOSITORY,,}`. ```elixir
def perform(%Oban.Job{args: %{"id" => id}}) do
## Releasing a New Version # ...
rescue
1. Update version in `mix.exs`: Ecto.NoResultsError -> Logger.info("Record not found")
Ecto.StaleEntryError -> Logger.info("Record stale")
```elixir end
version: "YYYY.M.D",
```
2. Commit and tag:
```bash
git add mix.exs
git commit -m "chore: bump version to vYYYY.M.D"
git tag vYYYY.M.D
```
3. Push to your fork:
```bash
git push origin master && git push origin vYYYY.M.D
```
4. Create GitHub release:
```bash
gh release create vYYYY.M.D --title "vYYYY.M.D" --generate-notes --repo <owner>/pinchflat
```
### Cleaning Up Tags
If you need to delete and recreate tags:
```bash
# Delete locally
git tag -d vX.Y.Z
# Delete on remote
git push origin --delete vX.Y.Z
# Recreate on specific commit
git tag vX.Y.Z <commit-sha>
# Force push to overwrite remote
git push origin vX.Y.Z --force
``` ```
## Validating GitHub Actions Locally ### Oban Workers
Always validate workflow changes before pushing: ```elixir
defmodule Pinchflat.Downloading.MediaDownloadWorker do
@moduledoc false
```bash use Oban.Worker,
# Check all workflows queue: :media_fetching,
actionlint priority: 5,
unique: [period: :infinity, states: [:available, :scheduled, :retryable, :executing]],
tags: ["media_item", "show_in_dashboard"]
# Check specific file @impl Oban.Worker
actionlint .github/workflows/docker_release.yml def perform(%Oban.Job{args: %{"id" => id} = args}) do
# Return :ok, {:ok, result}, {:error, reason}, or {:snooze, seconds}
end
end
``` ```
### Common Issues actionlint Catches ### Testing Patterns
- Invalid YAML syntax Use `Pinchflat.DataCase` for database tests:
- Unknown/misspelled action inputs
- Type errors in expressions (`${{ }}`)
- Invalid `runs-on` values
- Shell script issues (via shellcheck)
- Deprecated features
### Shellcheck Directives ```elixir
defmodule Pinchflat.MediaTest do
use Pinchflat.DataCase
For intentional word splitting in workflows, add the directive inside the `run:` block: import Pinchflat.MediaFixtures
import Pinchflat.SourcesFixtures
```yaml alias Pinchflat.Media
run: |
# shellcheck disable=SC2046 describe "list_media_items/0" do
docker buildx imagetools create $(jq -cr '...' <<< "$JSON") \ test "returns all media_items" do
$(printf 'image@sha256:%s ' *) media_item = media_item_fixture()
assert Media.list_media_items() == [media_item]
end
end
end
``` ```
## CI Best Practices ### Mocking with Mox
1. **Pin action versions to SHAs** for security: Mocks are defined in `test/test_helper.exs`:
```yaml - `YtDlpRunnerMock` - yt-dlp commands
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - `HTTPClientMock` - HTTP requests
``` - `UserScriptRunnerMock` - user scripts
- `AppriseRunnerMock` - notifications
2. **Quote shell variables** to satisfy shellcheck: ```elixir
test "calls user script" do
expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
assert data.id == media_item.id
{:ok, "", 0}
end)
```yaml Media.delete_media_item(media_item, delete_files: true)
run: echo "value" >> "$GITHUB_OUTPUT" end
```
3. **Use native ARM runners** instead of QEMU for faster builds:
```yaml
runs-on: ubuntu-24.04-arm # Native ARM64
```
4. **Scope caches by platform** to avoid conflicts:
```yaml
cache-from: type=gha,scope=build-linux-amd64
```
5. **Add concurrency controls** to save CI minutes:
```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
```
## Running Tests
```bash
# Via Docker Compose (as CI does)
docker compose -f docker-compose.ci.yml up -d
docker compose exec phx mix deps.get
docker compose exec phx mix ecto.create
docker compose exec phx mix ecto.migrate
docker compose exec phx mix check --no-fix --no-retry
``` ```
## Code Style ### Test Fixtures
- Elixir: Follow `mix format` (configured in `.formatter.exs`) Create fixtures in `test/support/fixtures/`:
- JavaScript/CSS: Follow Prettier (configured in `.prettierrc.js`)
- Commits: Conventional commits recommended (cocogitto installed) ```elixir
def media_item_fixture(attrs \\ %{}) do
{:ok, media_item} =
attrs
|> Enum.into(%{
media_id: Faker.String.base64(12),
title: Faker.Commerce.product_name(),
source_id: source_fixture().id
})
|> Pinchflat.Media.create_media_item()
media_item
end
```
### Test Helpers
From `Pinchflat.TestingHelperMethods`:
- `now/0` - current UTC datetime
- `now_minus/2` - datetime in past (`now_minus(5, :days)`)
- `now_plus/2` - datetime in future
- `assert_changed/2` - verify state change
## Project Structure
```
lib/
pinchflat/ # Business logic contexts
media/ # Media context (MediaItem, queries)
sources/ # Source context
downloading/ # Download workers and helpers
yt_dlp/ # yt-dlp integration
pinchflat_web/ # Phoenix web layer
test/
pinchflat/ # Context tests
pinchflat_web/ # Controller/LiveView tests
support/
fixtures/ # Test data factories
data_case.ex # Database test setup
conn_case.ex # HTTP test setup
```
## Pre-commit Hooks
Lefthook runs on commit:
- `prettier` - JS/CSS/YAML/JSON formatting
- `mix format` - Elixir formatting
- `typos` - spell checking
- `actionlint` - GitHub Actions validation
Bypass if needed: `git commit --no-verify -m "message"`