Skip to main content

Contributing Guide

Contributions of code, bug reports, and feature proposals are welcome!

Local Development Environment

# Prerequisites: Python 3.12+, Node.js 20+, uv, pnpm, ffmpeg
# The documentation site website/ also needs Node 24 (pinned in website/.node-version)
# Operating system: Linux / MacOS / Windows WSL2 (native Windows is unsupported)

# Install dependencies
uv sync
cd frontend && pnpm install && cd ..

# Install the pre-commit hooks once (ruff / eslint / pull_request_target tripwire)
uv run pre-commit install

# Initialize the database
uv run alembic upgrade head

# Start the backend (terminal 1)
# Note: --reload-dir is required to limit the watched directories; otherwise watchfiles
# scans node_modules / .venv / .git / .worktrees and hundreds of thousands of files, costing 50%+ of one core
uv run uvicorn server.app:app --reload --reload-dir server --reload-dir lib --port 1241

# Start the frontend (terminal 2)
cd frontend && pnpm dev

# Open http://localhost:5173

Documentation Site

website/ is a separate package root with its own lockfile and is not grouped into a workspace with frontend:

cd website && pnpm install

pnpm start # Development preview
pnpm build # Dual-locale build; a broken link or anchor fails outright
pnpm typecheck

# Site search only works against build output, not in the dev server
pnpm build && pnpm serve

# Sync the repo-root CONTRIBUTING.md into the docs-site page (start / build already run this automatically, so a manual run is rarely needed)
pnpm sync-contributing

# CI consistency gate: orphan translations / docs-site headings missing an explicit anchor / UI JSON key completeness — a non-zero exit on any hit;
# it reads output already synced by sync-contributing, so run sync-contributing first
pnpm check-consistency

Running Tests

# Backend tests
python -m pytest

# Frontend typecheck + tests
cd frontend && pnpm check

Code Quality

Lint & Format (ruff):

uv run ruff check . && uv run ruff format .
  • Rules: E/F/I/UP, with E402 and E501 ignored
  • line-length: 120
  • Enforced in CI: ruff check . && ruff format --check .

Lint (frontend ESLint):

cd frontend && pnpm lint # Check
cd frontend && pnpm lint:fix # Auto-fix what can be fixed
  • Configuration: frontend/eslint.config.js (flat config)
  • Rules: typescript-eslint/recommendedTypeChecked + react/recommended + react-hooks/recommended + jsx-a11y/recommended
  • Typed linting enables projectService: true, allowing async-related checks such as no-floating-promises and no-misused-promises
  • Enforced in CI: the frontend-tests job's Lint step

ESLint disable conventions

The project has followed a zero-warning policy since PR 3 (#219); every rule is an error. If a rule must be bypassed, follow these conventions:

  • Form: // eslint-disable-next-line <rule> -- <中文理由>; the reason after -- is required
  • Forbidden: file-level /* eslint-disable */, // eslint-disable-line without a reason, and combined use with @ts-ignore
  • PR description requirement: every new disable must be listed in the PR body as a table with rule | file:line | 理由
  • File-level disabling is allowed only through an eslint.config.js files override, with the reason documented in a config comment
  • Unacceptable reasons: "too much trouble," "leave it like this for now," or "later fix"
  • Acceptable reason examples: "React setter reference is stable," "mount-only initialization," or "generated preview video has no subtitle source"

Local IDE recommendation (do not commit to the repository):

.vscode/ is already in .gitignore. Add frontend/.vscode/settings.json locally to make VS Code / Cursor show lint warnings in real time and apply automatic fixes when saving:

{
"eslint.workingDirectories": [{ "pattern": "./frontend" }],
"editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }
}

Known constraint:

  • TypeScript version lock: the peer range of typescript-eslint@8.x is typescript <6.1; upgrade typescript-eslint before upgrading TypeScript to 6.1+

Test coverage:

  • CI requires ≥80%
  • asyncio_mode = "auto" (async tests do not need to be marked manually)

Pytest marker discipline

Every test case must have exactly one type marker. By default, CI runs -m "not e2e":

MarkerMeaningForbidden
unitFast and isolated; does not touch real I/O or external services
integrationCross-module collaboration using real dependencies (in-memory DB, temporary filesystem, and so on)Do not mock the public entry point of the module under test (for example, an integration test for MediaGenerator must not mock MediaGenerator.generate, because that would test the mock itself)
e2eEnd-to-end; depends on real external resources (remote APIs, LLM calls, heavyweight real ffmpeg work)Skipped by default in CI; run locally when needed

The marker can be applied at the test, class, or module (pytestmark) level. After combining all three levels, exactly one classification must still match.

Marker classification is enforced during pytest collection and does not depend on manual review:

  • An unmarked or multiply marked test fails collection immediately (tests/conftest.py::_enforce_classification_markers), and the error lists the specific nodeid
  • --strict-markers also makes markers not registered in pyproject.toml fail during collection

The existing unit/integration classifications came from bulk defaults (tests that invoke real ffmpeg to generate test audio or video assets, or that match uses_db, are classified as integration; all others as unit) and are not guaranteed to be semantically exact for every test. Classify new tests according to the table above—the use of real ffmpeg to generate a test fixture is different from the "heavyweight real ffmpeg work" in the e2e definition: the former invokes ffmpeg to produce test input, while the latter means a heavyweight ffmpeg processing pipeline in an end-to-end scenario.

Documentation Maintenance

The only published location for user documentation is docs.arc-reel.com; source files live in website/docs/ (see "Documentation Site" above for local builds and previews). Chinese is the sole authoring source; English translations are generated by AI, and humans review only the Chinese source. Internal documentation (ADRs, CONTEXT.md, AGENTS.md, the security threat model, provider API documentation indexes, and so on) is not published on the site and remains under the repository's docs/ directory. SECURITY.md also remains in the repository root because the GitHub Security tab depends on it.

This file is the source of truth for the contributing guide. During builds, it is copied to the site's development section (website/scripts/sync-contributing.mjs); the Chinese copy is not committed.

Page responsibilities

PageShould containShould not contain
README.mdProduct positioning, core value, and the shortest path to getting startedA complete model list, every environment variable, or internal implementation details
website/docs/guide/getting-started.mdThe complete path from deployment to the first generated videoProduction-grade reverse proxy and backup strategies
website/docs/guide/workflows.mdContent modes, video-making workflows, review checkpoints, and selection guidanceProvider credentials and operations commands
website/docs/guide/providers.mdProvider types, capability coverage, selection principles, and configuration hierarchyPrice promises likely to become outdated
website/docs/guide/jianying-export.mdLocating the CapCut draft directory, exporting, and further editing stepsThe video generation process itself
website/docs/guide/faq.mdFrequently asked questions and short answersLong tutorials
website/docs/ops/deployment.mdDeployment, upgrades, backup, recovery, monitoring, and securityProduct marketing copy
website/docs/dev/architecture.mdStable architectural boundaries, data flows, and extension pointsTemporary implementation plans and incomplete designs
SECURITY.mdSupported versions, supported deployment boundaries, private vulnerability reporting, and coordinated disclosure policyDetails of unfixed vulnerabilities and dynamic risk registers
docs/security/threat-model.mdSecurity assets, trust boundaries, attack surfaces, existing controls, and reassessment triggersDirectly exploitable unfixed vulnerabilities and patch history

Writing conventions

  • Keep the README stable: the README only needs to help a first-time repository visitor answer, "What is ArcReel, is it right for me, how is it different from calling a model API directly, and what is the fastest way to run it?" Put specific model names, prices, and API parameters on the corresponding site pages so that the homepage does not need to be rewritten every time a provider changes.
  • Treat runtime capabilities as authoritative for provider information: documentation describes the media types covered, how ArcReel unifies configuration, how to choose between different capabilities, and where to confirm specifics; the models actually selectable on the Settings page and the provider's official documentation are definitive.
  • Give headings explicit anchor IDs: write every heading on a published page as ## 标题 {#english-id}. The Chinese and English locales share the same anchor to prevent changes to copy from invalidating automatically generated Chinese slugs. Use relative file paths for cross-references within the site (such as ../ops/deployment.md), and use absolute GitHub links when pointing to repository files not published on the site.
  • Commit documentation changes with feature changes: when adding a content mode or video-making workflow, adding a provider or media capability, or changing deployment directories, ports, environment variables, data directories, backup methods, migration behavior, public APIs, licenses, or commercial-use terms, update the corresponding documentation at the same time.
  • No JSX or import in docs-site .md files: website/docusaurus.config.ts sets markdown.format: "detect", so .md files are parsed as CommonMark rather than MDX. Neither raises a compile error, and neither is executed as MDX: a JSX tag is output verbatim as raw HTML (a tag with children leaks that content directly onto the page), and an import statement is displayed verbatim as page text. Use .mdx for pages that need JSX.

Workflow

Branch strategy (trunk-based)

  • main is the only long-lived branch. Complete all work on short-lived branches created from the latest main, then merge them back into main through a PR
  • Never push directly with git push origin main. Even personal branches use the PR workflow; review the diff and acceptance checklist yourself first

Branch naming convention

Use <type>/<slug>, where type is one of the conventional commit types:

  • feat/ — New feature (for example, feat/reference-video-backend)
  • fix/ — Bug fix (for example, fix/queue-lease-timeout)
  • refactor/ — Refactoring (for example, refactor/session-actor)
  • docs/ — Documentation only (for example, docs/contribution-infra)
  • chore/ — Builds, tooling, version numbers, or cleanup (for example, chore/freeze-versions)
  • ci/ — CI configuration (for example, ci/testing-discipline)
  • test/ — Tests only

Use lowercase words separated by hyphens for slug, briefly describing the branch's focus.

Short branch lifetime

The time from creation to merge must be ≤3 days. If it runs longer, split it or rebase it onto the main branch first—do not drag a one-month-old branch directly into review.

Squash merge

Squash each PR into one commit when merging into main, with a conventional commit message (see the next section). Choose "Squash and merge" from the GitHub merge button.

Commit Conventions

Commit messages follow the Conventional Commits format:

feat: 新增功能描述
fix: 修复问题描述
refactor: 重构描述
docs: 文档变更
chore: 构建/工具变更

Release Process

Version numbers and the changelog are maintained automatically by release-please (configuration in .release-please-config.json, workflow in .github/workflows/release-please.yml). Developers do not need to bump version numbers manually—only write compliant conventional commits.

Workflow

  1. Squash-merge the PR into main according to the conventional commits specification
  2. release-please scans commits since the previous release and automatically opens or updates a Release PR titled like chore(main): release X.Y.Z, containing the next version bump and an updated CHANGELOG.md
  3. Merging that Release PR automatically creates a vX.Y.Z tag and publishes a GitHub Release

commit type → version increment

commit typeVersion incrementchangelog
featminor✨ 新功能
fixpatch🐛 Bug 修复
feat! / any type + ! / footer containing BREAKING CHANGE:major (minor when version <1.0.0)⚠️ BREAKING CHANGES (at the top of the changelog)
perf / refactor / docs / revertNo incrementShown (⚡ / ♻️ / 📚 / ↩️)
chore / ci / build / test / styleNo incrementHidden

By default, only feat and fix (as well as breaking changes) trigger a version bump in release-please. Configuring perf/refactor/docs/revert with hidden: false affects only their presentation in the changelog; it does not make them trigger a patch bump. If an iteration contains only these commit types, no Release PR is produced until the next fix/feat commit arrives.

The fields in pyproject.toml and frontend/package.json named version are maintained automatically by release-please (see the pyproject.toml comment # managed by release-please) and are read-only for developers. uv.lock is also synchronized automatically by running uv lock in the release-please workflow on the Release PR branch. The actual version state is defined by the git tag and .release-please-manifest.json.

commit examples

# New feature (minor bump)
feat(image-backends): 支持 OpenAI DALL-E 3 后端

# Bug fix (patch bump)
fix(queue): 修复任务 lease 超时后未正确归还的问题

# With a scope and a body
feat(grid): 支持 grid_12 布局

将宫格系统扩展到 12 宫格,适用于长篇剧集的批量预览。

This repository does not use breaking-change markers. The frontend and backend are released together, and the backend API does not make versioned compatibility guarantees—the bundled frontend evolves with each version, while external integrations (OpenClaw and others) fetch the latest contract at runtime through /skill.md rather than depending on a version number. When deleting or changing endpoints referenced by public/skill.md.template, update that template at the same time. Classify API changes normally as fix/refactor; do not add a ! suffix or a BREAKING CHANGE: footer. To correct an incorrectly marked commit after it has been merged, edit that PR's description and append a BEGIN_COMMIT_OVERRIDE/END_COMMIT_OVERRIDE block. release-please then recalculates the changelog and version number according to the override (this requires squash merging, which this repository uses). The workflow runs only on pushes to main; after editing, wait for the next push to main or rerun the release-please workflow manually. During the 0.x stage, bump-minor-pre-major limits the version jump caused by an incorrect marker to minor, but does not correct the changelog.

The following syntax is documented only to help identify incorrect markers. There are two equivalent ways to mark a breaking change:

# Form 1: append ! after the type
feat(api)!: 移除 /api/v1/legacy 端点

# Form 2: a footer containing BREAKING CHANGE (more common; allows a multi-line description)
feat(auth): 统一 API Key 验证逻辑

BREAKING CHANGE: /api/v1/api-keys 的返回结构改为 { items: [...] },
旧客户端需要适配。

In both forms, release-please will:

  • Bump the version number to major; when the current version is <1.0.0, the bump-minor-pre-major configuration limits this to a minor bump
  • Insert a separate ⚠️ BREAKING CHANGES section at the top of the changelog, summarizing the description of each breaking change
  • Keep the commit's regular entry under the corresponding type section (such as ✨ 新功能)