diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml index ef509eb..0365672 100644 --- a/.github/workflows/quality-gates.yml +++ b/.github/workflows/quality-gates.yml @@ -28,6 +28,16 @@ jobs: cache: "npm" cache-dependency-path: webui/package-lock.json + - name: Setup golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.11.4 + install-mode: binary + verify: true + + - name: Go Format & Lint Gates + run: ./scripts/lint.sh + - name: Refactor Line Gate run: ./tests/scripts/check-refactor-line-gate.sh diff --git a/.gitignore b/.gitignore index e6fd9d9..6f5b334 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ Thumbs.db # Claude Code .claude/ CLAUDE.local.md + +# Local tool bootstrap cache +.tmp/ diff --git a/docs/CONTRIBUTING.en.md b/docs/CONTRIBUTING.en.md index 1af41b4..0752b98 100644 --- a/docs/CONTRIBUTING.en.md +++ b/docs/CONTRIBUTING.en.md @@ -59,7 +59,7 @@ docker-compose -f docker-compose.dev.yml up | Language | Standards | | --- | --- | -| **Go** | Run `gofmt` and ensure `go test ./...` passes before committing | +| **Go** | Run `./scripts/lint.sh` (gofmt + golangci-lint) and ensure `go test ./...` passes before committing | | **JavaScript/React** | Follow existing project style (functional components) | | **Commit messages** | Use semantic prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `style:`, `perf:`, `chore:` | diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 75c4337..ad16e97 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -59,7 +59,7 @@ docker-compose -f docker-compose.dev.yml up | 语言 | 规范 | | --- | --- | -| **Go** | 提交前运行 `gofmt`,确保 `go test ./...` 通过 | +| **Go** | 提交前运行 `./scripts/lint.sh`(包含 gofmt+golangci-lint)并确保 `go test ./...` 通过 | | **JavaScript/React** | 保持现有代码风格(函数组件) | | **提交信息** | 使用语义化前缀:`feat:`、`fix:`、`docs:`、`refactor:`、`style:`、`perf:`、`chore:` | diff --git a/scripts/lint.sh b/scripts/lint.sh index 7ce1c2d..af07c27 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,12 +5,50 @@ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT_DIR" LINT_BIN="${GOLANGCI_LINT_BIN:-golangci-lint}" +BOOTSTRAP_VERSION="${GOLANGCI_LINT_VERSION:-v2.11.4}" +BOOTSTRAP_BIN="${ROOT_DIR}/.tmp/golangci-lint-${BOOTSTRAP_VERSION}" + +bootstrap_golangci_lint() { + local version_no_v archive_url tmp_dir + version_no_v="${BOOTSTRAP_VERSION#v}" + archive_url="https://github.com/golangci/golangci-lint/releases/download/${BOOTSTRAP_VERSION}/golangci-lint-${version_no_v}-linux-amd64.tar.gz" + + mkdir -p "${ROOT_DIR}/.tmp" + tmp_dir="$(mktemp -d)" + trap 'rm -rf "${tmp_dir}"' RETURN + + curl -sSfL "${archive_url}" -o "${tmp_dir}/golangci-lint.tar.gz" + tar -xzf "${tmp_dir}/golangci-lint.tar.gz" -C "${tmp_dir}" + cp "${tmp_dir}/golangci-lint-${version_no_v}-linux-amd64/golangci-lint" "${BOOTSTRAP_BIN}" + chmod +x "${BOOTSTRAP_BIN}" + + echo "bootstrapped golangci-lint ${BOOTSTRAP_VERSION} to ${BOOTSTRAP_BIN}" >&2 +} + +run_lint() { + local bin="$1" + if [[ "$bin" == *" "* ]]; then + eval "$bin fmt --diff -c .golangci.yml" && eval "$bin run -c .golangci.yml" + else + "$bin" fmt --diff -c .golangci.yml && "$bin" run -c .golangci.yml + fi +} # v2 separates formatters from linters; enforce both in one entrypoint. -if [[ "$LINT_BIN" == *" "* ]]; then - eval "$LINT_BIN fmt --diff -c .golangci.yml" - eval "$LINT_BIN run -c .golangci.yml" -else - "$LINT_BIN" fmt --diff -c .golangci.yml - "$LINT_BIN" run -c .golangci.yml +if lint_output="$(run_lint "$LINT_BIN" 2>&1)"; then + [[ -n "$lint_output" ]] && echo "$lint_output" + exit 0 fi + +if [[ -n "${GOLANGCI_LINT_BIN:-}" ]]; then + echo "$lint_output" >&2 + echo "lint failed with explicit GOLANGCI_LINT_BIN=${GOLANGCI_LINT_BIN}; skip auto-bootstrap." >&2 + exit 1 +fi + +echo "default golangci-lint is incompatible; bootstrapping ${BOOTSTRAP_VERSION}..." >&2 +if [[ ! -x "${BOOTSTRAP_BIN}" ]]; then + bootstrap_golangci_lint +fi + +run_lint "${BOOTSTRAP_BIN}"