ci: run the Postgres suites that never ran and lint the shell (#6917)

Eight Postgres-only tests skipped everywhere because no workflow named
them. The durable-first job now runs the service and outbound ones, and a
scratch-database job runs the migration tests that drop their own tables.
Every Postgres step counts top-level passes, so a renamed test cannot
leave a step green.

A shell-lint job runs actionlint on the workflows and shellcheck on the
shipped scripts (the legacy installers at error severity), golangci-lint
is pinned, and both the codegen job and make verify fail when
docs/public/openapi.json drifts from the panel's spec; docs-ci also fails
on a stale API reference. npm audit moves to its own script outside make
verify, since it depends on the network.
This commit is contained in:
25f2002654
2026-10-24 23:25:23 +05:30
committed by GitHub
parent 180cc37503
commit e8feb9d641
6 changed files with 123 additions and 15 deletions
+86 -8
View File
@@ -71,24 +71,34 @@ jobs:
- name: PostgreSQL durable-first tests - name: PostgreSQL durable-first tests
run: | run: |
set -o pipefail 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 # 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. # 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 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 exit 1
fi fi
- name: PostgreSQL schema and migration tests - name: PostgreSQL schema and migration tests
run: | run: |
set -o pipefail 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 # All must pass. Counting, not SKIP-matching: renaming a test would
# otherwise leave this step green while testing nothing. # otherwise leave this step green while testing nothing.
passed=$(grep -c -- '^--- PASS' /tmp/postgres-schema.log || true) passed=$(grep -c -- '^--- PASS' /tmp/postgres-schema.log || true)
if [ "$passed" -lt 5 ]; then if [ "$passed" -lt 6 ]; then
echo "expected at least 5 passing PostgreSQL schema tests, got $passed" >&2 echo "expected at least 6 passing PostgreSQL schema tests, got $passed" >&2
exit 1 exit 1
fi fi
@@ -112,6 +122,46 @@ jobs:
exit 1 exit 1
fi 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: codegen:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -128,6 +178,8 @@ jobs:
working-directory: frontend working-directory: frontend
- name: Fail if generated files are stale (run 'npm run gen' and commit) - 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 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: govulncheck:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -207,7 +259,33 @@ jobs:
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v9 uses: golangci/golangci-lint-action@v9
with: 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: frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -243,5 +321,5 @@ jobs:
run: npm run build-storybook run: npm run build-storybook
working-directory: frontend working-directory: frontend
- name: Audit - name: Audit
run: npm audit --omit=dev --audit-level=high run: npm run audit
working-directory: frontend working-directory: frontend
+17
View File
@@ -5,10 +5,14 @@ on:
branches: [main] branches: [main]
paths: paths:
- 'docs/**' - 'docs/**'
- 'frontend/public/openapi.json'
- 'frontend/src/pages/api-docs/endpoints.ts'
- '.github/workflows/docs-ci.yml' - '.github/workflows/docs-ci.yml'
pull_request: pull_request:
paths: paths:
- 'docs/**' - 'docs/**'
- 'frontend/public/openapi.json'
- 'frontend/src/pages/api-docs/endpoints.ts'
- '.github/workflows/docs-ci.yml' - '.github/workflows/docs-ci.yml'
permissions: permissions:
@@ -48,5 +52,18 @@ jobs:
- name: Test - name: Test
run: pnpm 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 - name: Build
run: pnpm build run: pnpm build
+4 -3
View File
@@ -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` 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 allowlist (`tools/openapigen/main.go`) or it is silently omitted from
schemas/examples (and `build-openapi.mjs` then fails on the missing schema). 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 `frontend/public/openapi.json` → `docs/public/openapi.json`, then
`cd docs && pnpm gen:api` to refresh the MDX under `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/` - 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 — (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 `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 `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: `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`, 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), `make vulncheck`, a live-Postgres job (where a SKIP counts as a failure),
+13 -2
View File
@@ -34,6 +34,10 @@ lint-go: dist-stub ## golangci-lint on Go sources
lint-fe: ## oxlint on frontend sources lint-fe: ## oxlint on frontend sources
cd $(FRONTEND) && npm run lint 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 .PHONY: lint
lint: lint-go lint-fe ## All linters lint: lint-go lint-fe ## All linters
@@ -72,6 +76,10 @@ test: test-go test-fe ## All tests
vulncheck: dist-stub ## govulncheck vulncheck: dist-stub ## govulncheck
go run golang.org/x/vuln/cmd/govulncheck@latest ./... 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 .PHONY: build-fe
build-fe: ## Build the Vite bundles into internal/web/dist build-fe: ## Build the Vite bundles into internal/web/dist
cd $(FRONTEND) && npm run build 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 cd $(FRONTEND) && npm run build-storybook
# The PR gate. Matches ci.yml: codegen freshness, both linters, the formatter, # 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 .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" @echo "verify: OK"
+1
View File
@@ -24,6 +24,7 @@
"build-storybook": "storybook build", "build-storybook": "storybook build",
"gen": "npm run gen:zod && npm run gen:api", "gen": "npm run gen:zod && npm run gen:api",
"gen:api": "node scripts/build-openapi.mjs", "gen:api": "node scripts/build-openapi.mjs",
"audit": "npm audit --omit=dev --audit-level=high",
"gen:zod": "cd .. && go run ./tools/openapigen", "gen:zod": "cd .. && go run ./tools/openapigen",
"prepare": "cd .. && husky frontend/.husky || true" "prepare": "cd .. && husky frontend/.husky || true"
}, },
+2 -2
View File
@@ -99,7 +99,7 @@ iplimit_log_path="${log_folder}/3xipl.log"
iplimit_banned_log_path="${log_folder}/3xipl-banned.log" iplimit_banned_log_path="${log_folder}/3xipl-banned.log"
confirm() { confirm() {
if [[ $# > 1 ]]; then if [[ $# -gt 1 ]]; then
echo && read -rp "$1 [Default $2]: " temp echo && read -rp "$1 [Default $2]: " temp
if [[ "${temp}" == "" ]]; then if [[ "${temp}" == "" ]]; then
temp=$2 temp=$2
@@ -3639,7 +3639,7 @@ show_menu() {
esac esac
} }
if [[ $# > 0 ]]; then if [[ $# -gt 0 ]]; then
case $1 in case $1 in
"start") "start")
check_install 0 && start 0 check_install 0 && start 0