Files
3x-ui/.github/workflows/ci.yml
T
Hamed 8eb58a4a14 perf(ip-sync): avoid redundant client IP reads and writes (#6930)
The access-log scanner and the cross-node merge stored the same IP list in
opposite orders, so neither saw a row as unchanged, and the merge re-read
every preloaded row and re-upserted every attribution row each sync cycle.
All writers now share one canonical encoder (timestamp, then IP ascending),
the first CAS is seeded from the preload, and identical attribution rows are
skipped. A create race between the scanner and node sync now merges into the
winning row instead of dropping the loser's IPs, which on PostgreSQL also
aborted the scanner's whole scan transaction.

Closes #6927
2026-10-25 14:13:18 +01:00

356 lines
14 KiB
YAML

name: CI
on:
pull_request:
paths:
- "**.go"
- "go.mod"
- "go.sum"
- "frontend/**"
- ".nvmrc"
- "Makefile"
- ".github/workflows/ci.yml"
push:
branches:
- main
paths:
- "**.go"
- "go.mod"
- "go.sum"
- "frontend/**"
- ".nvmrc"
- "Makefile"
- ".github/workflows/ci.yml"
permissions:
contents: read
jobs:
go-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Stub internal/web/dist for go:embed
run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
- name: Test
run: |
go list ./... | grep -v '/frontend/node_modules/' > /tmp/go-packages.txt
go test -shuffle=on -count=1 $(cat /tmp/go-packages.txt)
postgres-durable-first:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: xui_durable
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d xui_durable"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
XUI_DB_TYPE: postgres
XUI_DB_DSN: "host=127.0.0.1 port=5432 user=postgres password=postgres dbname=xui_durable sslmode=disable"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Stub internal/web/dist for go:embed
run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
- name: PostgreSQL durable-first tests
run: |
set -o pipefail
go test ./internal/web/service -run 'PostgresCommitFailure|^TestSyncInboundClientsOnPostgres$|^TestSetClientLimitHwidIsSerializedWithSyncInbound$' -count=1 -v | tee /tmp/postgres-durable-first.log
# Count passes rather than assert no SKIP: a renamed or deleted test
# prints "no tests to run" and exits 0, leaving the step green for nothing.
passed=$(grep -c -- '^--- PASS' /tmp/postgres-durable-first.log || true)
if [ "$passed" -lt 4 ]; then
echo "expected at least 4 passing durable-first tests, got $passed" >&2
exit 1
fi
- name: PostgreSQL batched stats identities
run: |
set -o pipefail
go test ./internal/web/service -run '^TestClientStatsIdentitiesPostgres$' -count=1 -v | tee /tmp/postgres-stats-identities.log
passed=$(grep -c -- '^--- PASS' /tmp/postgres-stats-identities.log || true)
if [ "$passed" -ne 1 ]; then
echo "expected 1 passing PostgreSQL stats identities test, got $passed" >&2
exit 1
fi
- name: PostgreSQL client IP sync regressions
run: |
set -o pipefail
go test -p=1 ./internal/web/service ./internal/web/job -run '^TestClientIpSyncPostgres(Scanner)?$' -count=1 -v | tee /tmp/postgres-client-ip-sync.log
passed=$(grep -c -- '^--- PASS' /tmp/postgres-client-ip-sync.log || true)
if [ "$passed" -ne 2 ]; then
echo "expected 2 passing PostgreSQL client IP sync tests, got $passed" >&2
exit 1
fi
- name: PostgreSQL global-traffic heartbeat regressions
run: |
set -o pipefail
go test ./internal/web/service -run '^TestGlobalTrafficHeartbeatPostgres$' -count=1 -v | tee /tmp/postgres-global-traffic-heartbeat.log
passed=$(grep -c -- '^--- PASS' /tmp/postgres-global-traffic-heartbeat.log || true)
if [ "$passed" -ne 1 ]; then
echo "expected 1 passing global-traffic heartbeat test, got $passed" >&2
exit 1
fi
- name: PostgreSQL deferred-commit test
run: |
set -o pipefail
go test ./internal/web/service/outbound -run '^TestAddTrafficReturnsDeferredCommitFailure$' -count=1 -v | tee /tmp/postgres-outbound.log
passed=$(grep -c -- '^--- PASS' /tmp/postgres-outbound.log || true)
if [ "$passed" -lt 1 ]; then
echo "expected at least 1 passing deferred-commit test, got $passed" >&2
exit 1
fi
- name: PostgreSQL schema and migration tests
run: |
set -o pipefail
go test ./internal/database -run '^(TestHostAutoMigrateCreatesColumns_Postgres|TestMigrate_Postgres|TestClientWeeklyRenewMigration_Postgres|TestMigrate_LegacyBooleanVerifyPeerCertByNameIsBlanked_Postgres|TestClientKeepAlivePostgresMigrationPreservesLegacyScalars|TestPostgresModelSettled_TracksSchemaPresence)$' -count=1 -v | tee /tmp/postgres-schema.log
# All must pass. Counting, not SKIP-matching: renaming a test would
# otherwise leave this step green while testing nothing.
passed=$(grep -c -- '^--- PASS' /tmp/postgres-schema.log || true)
if [ "$passed" -lt 6 ]; then
echo "expected at least 6 passing PostgreSQL schema tests, got $passed" >&2
exit 1
fi
- name: PostgreSQL subscription Host tests
run: |
set -o pipefail
go test ./internal/sub -run '^TestSubHosts_Postgres$' -count=1 -v | tee /tmp/postgres-hosts.log
passed=$(grep -c -- '^--- PASS' /tmp/postgres-hosts.log || true)
if [ "$passed" -ne 1 ]; then
echo "expected passing PostgreSQL Host coverage, got $passed" >&2
exit 1
fi
- name: PostgreSQL Telegram lookup and legacy migration tests
run: |
set -o pipefail
go test ./internal/web/service -run '^(TestGetClientTrafficTgBot_IndexedPostgres|TestGetClientTrafficTgBot_SettingsSerializationStyles)$' -count=1 -v | tee /tmp/postgres-tgbot.log
passed=$(grep -c -- '^--- PASS' /tmp/postgres-tgbot.log || true)
if [ "$passed" -ne 2 ]; then
echo "expected 2 passing PostgreSQL Telegram lookup tests, got $passed" >&2
exit 1
fi
# The migration tests write and drop their own tables, so they need a
# scratch database instead of the durable one the panel tests use.
postgres-migration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: xui_scratch
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d xui_scratch"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
XUI_TEST_PG_DSN: "host=127.0.0.1 port=5432 user=postgres password=postgres dbname=xui_scratch sslmode=disable"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Stub internal/web/dist for go:embed
run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
- name: PostgreSQL migration tests
run: |
set -o pipefail
go test ./internal/database -run '^(TestMigrateData_CompositeKeyTableLargerThanBatch|TestMigrateData_PreservesFalseDefaultedColumns|TestMigrateData_FailedCopyLeavesDestinationUntouched|TestClientHwidSchemaPostgres)$' -count=1 -v | tee /tmp/postgres-migration.log
# Count passes rather than assert no SKIP: a renamed or deleted test
# prints "no tests to run" and exits 0, leaving the step green for nothing.
passed=$(grep -c -- '^--- PASS' /tmp/postgres-migration.log || true)
if [ "$passed" -lt 4 ]; then
echo "expected at least 4 passing PostgreSQL migration tests, got $passed" >&2
exit 1
fi
codegen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
- name: Regenerate schemas, examples and OpenAPI
run: npm run gen
working-directory: frontend
- name: Fail if generated files are stale (run 'npm run gen' and commit)
run: git diff --exit-code -- frontend/src/generated frontend/public/openapi.json
- name: Fail if the docs spec is not the panel's
run: cmp frontend/public/openapi.json docs/public/openapi.json
govulncheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Stub internal/web/dist for go:embed
run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Run govulncheck
run: govulncheck ./...
# Race + shuffle hygiene gate: data races and order-dependent tests fail the build.
race:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Stub internal/web/dist for go:embed
run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
- name: Race + shuffle
run: |
go list ./... | grep -v '/frontend/node_modules/' > /tmp/go-packages.txt
# internal/web/service runs ~10x slower under -race and overruns the 10m default.
go test -race -shuffle=on -count=1 -timeout 25m $(cat /tmp/go-packages.txt)
# A real master and node panel, each its own process, driven through node sync.
# A SKIP here means the binary was never handed over, so it fails the job.
node-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Master + node end to end
run: |
set -o pipefail
make node-e2e 2>&1 | tee /tmp/node-e2e.log
if grep -q -- '--- SKIP' /tmp/node-e2e.log; then echo "node-e2e skipped"; exit 1; fi
# Brief native-fuzz smoke on the security-/parser-critical decoders. Each runs the
# generated corpus plus 30s of exploration; a crash here is a real input-handling bug.
fuzz-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Stub internal/web/dist for go:embed
run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
- name: Fuzz critical parsers (smoke)
run: |
go test -run '^$' -fuzz 'FuzzParseLink$' -fuzztime=30s ./internal/util/link/
go test -run '^$' -fuzz 'FuzzDecodeCertPin$' -fuzztime=30s ./internal/web/runtime/
golangci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Stub internal/web/dist for go:embed
run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
# Pinned: "latest" let the lint result drift between runs, so the same
# commit could pass today and fail tomorrow on a new upstream release.
version: v2.15.0
# Lint the shell scripts and the workflows themselves. The three legacy
# scripts carry a warning backlog (unquoted expansions, masked return codes)
# and are gated on errors only; the scripts that are clean today are gated on
# everything.
shell-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: actionlint
# The shellcheck integration is off: it reports the stylistic rules
# (SC2086, SC2046) that main's own workflows trip. The shell scripts
# the panel ships are linted by the shellcheck step below.
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -color -shellcheck=
- name: Install shellcheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: shellcheck
run: |
shellcheck --severity=style DockerInit.sh DockerEntrypoint.sh deploy/test/smoke-noninteractive.sh
shellcheck --severity=error install.sh update.sh x-ui.sh
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install
run: npm ci
working-directory: frontend
- name: Lint
run: npm run lint
working-directory: frontend
- name: Format check
run: npm run format:check
working-directory: frontend
- name: Typecheck
run: npm run typecheck
working-directory: frontend
- name: Install Playwright Chromium (Storybook story tests)
run: npx playwright install --with-deps chromium
working-directory: frontend
- name: Test
run: npm test
working-directory: frontend
- name: Build
run: npm run build
working-directory: frontend
- name: Build Storybook
run: npm run build-storybook
working-directory: frontend
- name: Audit
run: npm run audit
working-directory: frontend