Files
3x-ui/.github/claude/repo-context.md
T
Sanaei 58669f6146 refactor(ci): replace the in-house review lanes with the official code-review skill
The four pull_request_target review jobs in claude-bot.yml (Senior
Developer / QA / Tester / Arbiter and their shared rubric) are replaced by
a single review job running the official code-review plugin - the same
skill behind Anthropic's hosted Code Review and the review workflow
/install-github-app generates. The hosted service needs a Team/Enterprise
organisation, so the plugin runs in CI on the maintainer's subscription
instead: inline findings on PR open and ready-for-review, plus manual
(re-)review when the owner or a collaborator comments "@claude review".

The official example triggers on pull_request, but GitHub withholds
secrets from fork runs and essentially every 3x-ui pull request is from a
fork, so the job keeps the lanes' pull_request_target posture: the
workspace is the base revision and nothing from the pull request is
checked out or executed.

What the lanes uniquely knew is distilled into REVIEW.md, handed to the
skill via --append-system-prompt and pinned by bot_context_test.go the way
repo-context.md is: the runtime.Runtime dispatch rule, migration and
upgrade safety, the four-step route contract chain including the unchecked
docs copy, the i18n rule, the three link implementations, and the
wire-format verification bar. The mention job now ignores "@claude
review" comments on pull requests so the review trigger does not also
wake the generic bot, and the lane-only rubric file goes with the lanes.

The remaining prompts also lose their tone micro-rules (no emoji, no
exclamation marks, no filler) and the workflow's comment banners are
removed.
2026-08-20 15:09:48 +02:00

182 lines
9.9 KiB
Markdown

# Repository context for the Claude bot
Shared briefing for the jobs in `.github/workflows/claude-bot.yml`. It exists so
these facts live in ONE place next to the code instead of being restated in each
prompt, where they went stale silently. (Pull-request review is separate: its
code-review skill is briefed with `CLAUDE.md` and `REVIEW.md`, not this.)
`CLAUDE.md`, `frontend/CLAUDE.md` and `docs/architecture.md` outrank this file.
Where they disagree with it, they win and this file is the thing to fix.
`docs/architecture.md` carries a "Symptom -> File" index and the cron-job table,
which answer "which file owns X" in one hop; grepping blind wastes turns on a
question it already answers.
## Stack
3x-ui is an open-source web control panel for managing Xray-core servers.
- Backend: Go 1.26, module `github.com/mhsanaei/3x-ui/v3`, Gin and GORM.
- It runs Xray-core as a managed child process (`internal/xray/process.go`) and
imports `github.com/xtls/xray-core` for config types and the gRPC
stats/handler/router API. The release the panel BUNDLES is pinned in
`DockerInit.sh`; the version it COMPILES against is pinned in `go.mod`, and
the two are not always the same.
- MTProto inbounds run a SECOND managed child, the `mtg-multi` binary (a
multi-secret mtg fork, panel-side code in `internal/mtproto/`), one process
per inbound. Client, ad-tag and quota/expiry edits are hot-applied through the
fork's management API (`PUT /secrets`) so connections survive, with a process
restart as the fallback on older binaries.
- Storage: SQLite by default (`/etc/x-ui/x-ui.db` on Linux, the executable
directory on Windows) or PostgreSQL (`XUI_DB_TYPE` / `XUI_DB_DSN`). The SQLite
driver is CGo, so `CGO_ENABLED=0` builds fail.
- Frontend: React 19 + Ant Design 6 + Vite 8 + TypeScript in `frontend/`, built
into `internal/web/dist/` (gitignored) and embedded with `embed.FS`.
## Where things live
| area | path |
| --- | --- |
| entry point + `x-ui` CLI | `main.go` |
| env parsing | `internal/config/` |
| schema, migrations | `internal/database/`, `internal/database/model/` |
| Xray child process + config | `internal/xray/` |
| MTProto inbounds | `internal/mtproto/` |
| subscription server | `internal/sub/` |
| HTTP handlers | `internal/web/controller/` |
| business logic | `internal/web/service/` |
| cron jobs (schedules in `web.go startTask()`) | `internal/web/job/` |
| master/sub-node over mTLS | `internal/web/runtime/` |
| i18n | `internal/web/locale/`, `internal/web/translation/` |
| UI source | `frontend/src/` |
| install / upgrade | `install.sh`, `x-ui.sh`, `DockerInit.sh` |
## Hard rules a change must respect
- **Dispatch through `runtime.Runtime`.** Every state-changing inbound or client
operation goes through the interface in `internal/web/runtime/`, never
straight to `internal/xray/api.go`. A direct call passes every local test and
silently breaks every multi-node deployment; it is invisible in a single-box
reading of a diff.
- **Layering.** Controllers are thin — bind, validate, respond — with no GORM
queries, no Xray calls and no business rules. `internal/util/*` is leaf-only
and must not import service, controller or database. `internal/web/dist/` and
`frontend/src/generated/` are generated; a hand-edit is a violation.
- **Comments in committed Go/TS/TSX: 2 lines MAX per block**, spent on the *why*
a name cannot hold — an invariant, an issue number, a non-obvious constraint.
Exempt, never flag: `//go:build`, `//go:generate`, `//nolint:`,
`// Code generated ... DO NOT EDIT.`. HTML `<!-- -->` is fine.
- **The route contract chain**, which breaks in four distinct places:
1. a new `g.POST`/`g.GET` in `internal/web/controller/` needs a matching entry
in `frontend/src/pages/api-docs/endpoints.ts` — pinned BOTH ways by
`TestRouteRegistryContract` in `internal/web/routes_contract_test.go`, so a
renamed or removed route that leaves a stale entry fails too;
2. generated artefacts must be regenerated with `make gen`, or CI's `codegen`
job fails on a dirty `frontend/src/generated` or
`frontend/public/openapi.json`;
3. a NEW struct crossing the API boundary must be added to the `StructAllow`
allowlist in `tools/openapigen/main.go`, or it is SILENTLY dropped from the
schemas and `frontend/scripts/build-openapi.mjs` then fails — a guaranteed
CI break, not a style nit;
4. the step NOTHING checks — `frontend/public/openapi.json` must be copied to
`docs/public/openapi.json` and the MDX regenerated with
`cd docs && pnpm gen:api`, because `docs-ci.yml` fires only on `docs/**`.
Step 4 is the one that reaches production wrong.
- **i18n.** A new English key goes in EVERY locale JSON in
`internal/web/translation/` (13 files) AND must be referenced from
`frontend/src` or Go in the SAME change.
`frontend/src/test/i18n-dead-keys.test.ts` fails on a missing locale file and
on an orphan key alike.
- **Migrations.** Schema changes are GORM `AutoMigrate` PLUS hand-written
migrations in `internal/database/db.go`. There are no migration files and no
down-migrations, and everything has to work on SQLite AND PostgreSQL.
- **Tests.** Stdlib `testing` only (no testify), table-driven with `t.Run`
subtests and `t.Helper()` on helpers. An assertion must pin the exact value,
typed error or emitted string — `err != nil` and `len(x) > 0` are findings,
not nits. Prefer real dependencies: a throwaway DB via
`database.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))` with `t.Cleanup`, and
`httptest` for HTTP. `internal/sub`'s `initSubDB(t)` is the template.
A test must FAIL without its fix; one that passes either way certifies
nothing and then gets cited as proof the fix works.
## The three link implementations
Link and subscription generation is implemented three times, independently:
| language | path | what it feeds |
| --- | --- | --- |
| Go | `internal/util/link/`, `internal/sub/` | what the panel serves |
| TS | `frontend/src/lib/xray/` | what the panel UI shows |
| TS | `docs/lib/xray/` | what the docs site shows |
A change to share-link or subscription output that touches one and not the
others is how they drift apart.
## Downstream programs that must accept what the panel emits
- **XTLS/Xray-core** — the Xray config the panel generates, and the VLESS/VMess
transport and security fields.
- **MetaCubeX/mihomo** — consumes the Clash YAML from `internal/sub/`.
- **SagerNet/sing-box** — parses the share links the panel emits.
- **mhsanaei/mtg-multi** — the MTProto sidecar whose TOML (`[secrets]`,
`[secret-ad-tags]`, `[secret-limits]`) and management API
(`PUT /secrets`, `POST /secrets/{name}/reset-quota`) `internal/mtproto/`
writes and calls.
## What CI runs
`.github/workflows/ci.yml`, on every pull request touching Go or frontend code.
It is paths-filtered, so a docs-only or workflow-only change produces no run.
| job | what it proves |
| --- | --- |
| `go-test` | `go test -shuffle=on -count=1` over every package except `frontend/node_modules` |
| `race` | the same set under `-race -shuffle=on` |
| `postgres-durable-first` | live PostgreSQL 16: the `PostgresCommitFailure` tests plus `TestHostAutoMigrateCreatesColumns_Postgres` and `TestMigrate_Postgres`. Both steps COUNT passes rather than assert on SKIP, so a renamed or deleted test fails the job |
| `govulncheck` | known vulnerabilities |
| `golangci` | `golangci-lint` |
| `fuzz-smoke` | 30s each on `FuzzParseLink` and `FuzzDecodeCertPin` |
| `codegen` | `npm run gen` then `git diff --exit-code` on the generated files |
| `frontend` | MSW worker drift, lint, format:check, typecheck, `npm test` (Vitest + headless-Chromium Storybook), build, build-storybook, `npm audit` |
**What CI does NOT prove.** These test families `t.Skip` unless an environment
variable is set, and CI sets only the PostgreSQL ones above:
| gate | covers |
| --- | --- |
| `XUI_TEST_PG_DSN` | PostgreSQL-specific paths |
| `XUI_DB_TYPE` + `XUI_DB_DSN` | dialect-dependent behaviour |
| `XRAY_E2E_BINARY` | the Xray gRPC end-to-end tests in `internal/xray/` |
| `XUI_SCALE_TEST` | scale tests in `internal/sub/`, `internal/web/job/`, `internal/web/service/` |
Mutation testing (`mutation.yml`) runs nightly and never on a pull request, so a
test that cannot fail is invisible to CI. `make verify` is the local gate.
## Support facts reporters get wrong
- Linux install: `bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)`
- Install generates a RANDOM username, password and web base path — never
admin/admin. The `x-ui` menu on the server shows or resets them.
- The installer service environment file is DISTRO-DEPENDENT:
`/etc/default/x-ui` (Debian/Ubuntu), `/etc/conf.d/x-ui` (Arch),
`/etc/sysconfig/x-ui` (RHEL/Fedora). Naming the wrong one means the reporter's
edit is silently never read by systemd — a common cause of "I set the variable
and nothing happened".
- Windows is supported. There the database sits next to the executable, not in
`/etc` — never quote the Linux path to a Windows user.
- SQLite to PostgreSQL: `x-ui migrate-db --dsn "postgres://..."`, then set
`XUI_DB_TYPE`/`XUI_DB_DSN` in that file and `systemctl restart x-ui`. The
source SQLite file is left in place.
- Docker image `ghcr.io/mhsanaei/3x-ui`; PostgreSQL profile
`docker compose --profile postgres up -d`. Fail2ban IP-limit enforcement needs
`NET_ADMIN` + `NET_RAW` (compose grants them; a bare `docker run` must add
`--cap-add=NET_ADMIN --cap-add=NET_RAW`).
- Never state that a `XUI_*` variable does not exist without grepping
`internal/config/` and `internal/tunnelmonitor/` first. The
`XUI_TUNNEL_HEALTH_*` family is the usual answer to "the panel restarts Xray
every few minutes".
- Security per inbound is none / tls / reality. XTLS is a VLESS *flow*
(`xtls-rprx-vision`), not a security setting — never tell anyone to pick XTLS
in the security dropdown.
- Never hardcode a version. For "is this already fixed" use
`gh release list -L 10`, `gh search commits`, and `git log -S`.