mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 23:35:00 +03:00
8eb58a4a14
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
356 lines
14 KiB
YAML
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
|