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

9.9 KiB

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.

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.