Files
3x-ui/.github/workflows/ci.yml
T
Hamed f4030c889c fix(frontend): make inbound forms accessible to screen readers (#7025)
Expose selectable options and selected values, associate validation guidance with controls, and contain keyboard focus in inbound dialogs. Preserve create and edit serialization and add Chromium keyboard and accessibility checks.
2026-11-05 18:08:49 +01:00

380 lines
15 KiB
YAML

name: CI
on:
pull_request:
paths:
- "**.go"
- "go.mod"
- "go.sum"
- "frontend/**"
- ".nvmrc"
- "Makefile"
- "install.sh"
- "deploy/test/installer-panel-port.sh"
- ".github/workflows/ci.yml"
push:
branches:
- main
paths:
- "**.go"
- "go.mod"
- "go.sum"
- "frontend/**"
- ".nvmrc"
- "Makefile"
- "install.sh"
- "deploy/test/installer-panel-port.sh"
- ".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$|^TestVlessManagedFallbackConflictPostgres$' -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 5 ]; then
echo "expected at least 5 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
- name: SQLite import host settings
run: |
set -o pipefail
go test ./internal/web/service -run '^TestImportSQLiteIntoPostgresHostSettings$' -count=1 -v | tee /tmp/postgres-import-host-settings.log
passed=$(grep -c -- '^--- PASS: TestImportSQLiteIntoPostgresHostSettings ' /tmp/postgres-import-host-settings.log || true)
if [ "$passed" -ne 1 ]; then
echo "expected the SQLite import integration test to pass, 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 deploy/test/installer-panel-port.sh
shellcheck --severity=error install.sh update.sh x-ui.sh
- name: Installer panel port regression tests
run: bash deploy/test/installer-panel-port.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: Inbound keyboard and accessibility tree
working-directory: frontend
run: |
set -euo pipefail
./node_modules/.bin/vite preview --outDir storybook-static --host 127.0.0.1 --port 6006 > /tmp/inbound-preview.log 2>&1 &
preview_pid=$!
trap 'kill "$preview_pid" 2>/dev/null || true' EXIT
curl --retry 30 --retry-connrefused --retry-delay 1 --retry-max-time 45 -fsS http://127.0.0.1:6006/iframe.html > /dev/null
node tools/check-inbound-keyboard.mjs http://127.0.0.1:6006
- name: Audit
run: npm run audit
working-directory: frontend