diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4d41a204..58ca62213 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,24 +71,34 @@ jobs: - name: PostgreSQL durable-first tests run: | set -o pipefail - go test ./internal/web/service -run 'PostgresCommitFailure' -count=1 -v | tee /tmp/postgres-durable-first.log + 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) + 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 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 durable-first test, got $passed" >&2 + 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)$' -count=1 -v | tee /tmp/postgres-schema.log + 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 5 ]; then - echo "expected at least 5 passing PostgreSQL schema tests, got $passed" >&2 + if [ "$passed" -lt 6 ]; then + echo "expected at least 6 passing PostgreSQL schema tests, got $passed" >&2 exit 1 fi @@ -112,6 +122,46 @@ jobs: 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: @@ -128,6 +178,8 @@ jobs: 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 @@ -207,7 +259,33 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: latest + # 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 @@ -243,5 +321,5 @@ jobs: run: npm run build-storybook working-directory: frontend - name: Audit - run: npm audit --omit=dev --audit-level=high + run: npm run audit working-directory: frontend diff --git a/.github/workflows/docs-ci.yml b/.github/workflows/docs-ci.yml index 2d4c2c20b..735bbed33 100644 --- a/.github/workflows/docs-ci.yml +++ b/.github/workflows/docs-ci.yml @@ -5,10 +5,14 @@ on: branches: [main] paths: - 'docs/**' + - 'frontend/public/openapi.json' + - 'frontend/src/pages/api-docs/endpoints.ts' - '.github/workflows/docs-ci.yml' pull_request: paths: - 'docs/**' + - 'frontend/public/openapi.json' + - 'frontend/src/pages/api-docs/endpoints.ts' - '.github/workflows/docs-ci.yml' permissions: @@ -48,5 +52,18 @@ jobs: - name: Test run: pnpm test + # The API reference is generated from the panel's own spec. The copy into + # docs/public and the generated pages both have to be in step with it. + - name: The docs spec must be the panel's + run: cmp frontend/public/openapi.json docs/public/openapi.json + working-directory: . + + - name: Regenerate the API reference + run: pnpm gen:api + + - name: Fail if the generated API reference is stale + run: git diff --exit-code -- docs/content/docs/en/reference/api + working-directory: . + - name: Build run: pnpm build diff --git a/CLAUDE.md b/CLAUDE.md index 3784785b4..5c0986587 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -102,10 +102,11 @@ file locations when it can answer in one hop. never hand-write them. A new struct must be added to openapigen's `StructAllow` allowlist (`tools/openapigen/main.go`) or it is silently omitted from schemas/examples (and `build-openapi.mjs` then fails on the missing schema). -- A new or renamed endpoint has a FOURTH step nothing checks: copy +- A new or renamed endpoint has a FOURTH step: copy `frontend/public/openapi.json` → `docs/public/openapi.json`, then `cd docs && pnpm gen:api` to refresh the MDX under - `docs/content/docs/en/reference/api/`. `docs-ci.yml` fires only on `docs/**`. + `docs/content/docs/en/reference/api/`. The `codegen` job and `make verify` + fail on a stale copy, and `docs-ci.yml` fails on stale MDX. - A new English i18n key goes in EVERY locale JSON in `internal/web/translation/` (14 files) AND must be referenced from `frontend/src` or Go in the SAME commit — `frontend/src/test/i18n-dead-keys.test.ts` fails both ways. It is a frontend @@ -189,7 +190,7 @@ reads as a broken repo, not a missing step. Run `make dist-stub` once; every `make` Go target already depends on it, which is why `make test-go` beats `go test ./...`. Run `make help` for all targets. The local gate: - make verify # gen-check + lint + typecheck + test + build + build-storybook + make verify # gen-check + lint + typecheck + test + build + build-storybook + docs-openapi-check That is the *fast* gate, not all of CI. `ci.yml` also runs `make race`, `make vulncheck`, a live-Postgres job (where a SKIP counts as a failure), diff --git a/Makefile b/Makefile index de5c3a1d2..9eed80c0e 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,10 @@ lint-go: dist-stub ## golangci-lint on Go sources lint-fe: ## oxlint on frontend sources cd $(FRONTEND) && npm run lint +.PHONY: audit-fe +audit-fe: ## Audit the frontend's production dependencies (needs the network) + cd $(FRONTEND) && npm run audit + .PHONY: lint lint: lint-go lint-fe ## All linters @@ -72,6 +76,10 @@ test: test-go test-fe ## All tests vulncheck: dist-stub ## govulncheck go run golang.org/x/vuln/cmd/govulncheck@latest ./... +.PHONY: docs-openapi-check +docs-openapi-check: ## Fail if the docs OpenAPI spec is not the panel's + cmp frontend/public/openapi.json docs/public/openapi.json + .PHONY: build-fe build-fe: ## Build the Vite bundles into internal/web/dist cd $(FRONTEND) && npm run build @@ -85,7 +93,10 @@ build-storybook: ## Build the static Storybook (compile-checks all stories) cd $(FRONTEND) && npm run build-storybook # The PR gate. Matches ci.yml: codegen freshness, both linters, the formatter, -# typecheck, both test suites, a full build, and the Storybook compile-check. +# typecheck, both test suites, a full build, the Storybook compile-check and the +# docs spec check. The dependency audit is a separate target: it needs the +# network and a live advisory database, so an unchanged commit could pass today +# and fail tomorrow. .PHONY: verify -verify: gen-check lint format-check typecheck test build build-storybook ## Full local gate (mirrors CI) +verify: gen-check lint format-check typecheck test build build-storybook docs-openapi-check ## Full local gate (mirrors CI) @echo "verify: OK" diff --git a/frontend/package.json b/frontend/package.json index c376188b2..c78106aba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "build-storybook": "storybook build", "gen": "npm run gen:zod && npm run gen:api", "gen:api": "node scripts/build-openapi.mjs", + "audit": "npm audit --omit=dev --audit-level=high", "gen:zod": "cd .. && go run ./tools/openapigen", "prepare": "cd .. && husky frontend/.husky || true" }, diff --git a/x-ui.sh b/x-ui.sh index ab41e3824..8e245c271 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -99,7 +99,7 @@ iplimit_log_path="${log_folder}/3xipl.log" iplimit_banned_log_path="${log_folder}/3xipl-banned.log" confirm() { - if [[ $# > 1 ]]; then + if [[ $# -gt 1 ]]; then echo && read -rp "$1 [Default $2]: " temp if [[ "${temp}" == "" ]]; then temp=$2 @@ -3639,7 +3639,7 @@ show_menu() { esac } -if [[ $# > 0 ]]; then +if [[ $# -gt 0 ]]; then case $1 in "start") check_install 0 && start 0