name: Pre-commit Checks on: pull_request: branches: [ main ] workflow_call: # Called by ci-gate.yml for release pipeline workflow_dispatch: # No concurrency group — intentionally omitted. # This workflow triggers on both pull_request and workflow_call (from # ci-gate.yml / release-gate.yml). A shared concurrency key would cause # direct PR runs and workflow_call runs to cancel each other mid-flight. # See #3554 (reverted in #3599) for context. permissions: contents: read jobs: pre-commit: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # Full history so the author-identity hook can resolve the PR commit # range (merge-base..head) from local objects WITHOUT fetching: a hook # that mutates git state mid-run trips pre-commit's "files were # modified by this hook". fetch-depth: 0 - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: '3.12' cache: 'pip' - name: Set up PDM uses: pdm-project/setup-pdm@973541a5febeafcfdadf8a51211435be6ecfd90f # v4.5 with: python-version: '3.12' cache: true - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '24' - name: Install Node.js dependencies run: npm ci - name: Install system dependencies for SQLCipher run: | sudo apt-get update sudo apt-get install -y libsqlcipher-dev # Replicate pre-commit/action@v3.0.1 manually so the network-flaky hook # *download* can be retried WITHOUT retrying the actual lint check. # # pre-commit lazily downloads hook environments from external sources # (PyPI for ruff, GitHub release binaries for shellcheck, etc.), and a # single HTTP 5xx from any one of them fails the whole job. See run #2524 # where `Building wheel for shellcheck_py` hit HTTP 502 fetching the # shellcheck binary during the wheel build. A second attempt benefits from # the partially-populated cache and almost always succeeds; two attempts is # enough (a hook env that fails to install twice in a row is not a # transient outage). # # That download is done up-front by `pre-commit install-hooks`, so only # THAT step is wrapped in nick-fields/retry. The lint check itself runs # exactly once: an auto-fixing hook (ruff-format, …) that modifies a file # makes `pre-commit run` exit non-zero, and that MUST fail the job. # Retrying the run would mask it — the second pass sees the already-fixed # tree and exits 0, so unformatted code would sail through with a green # check. - name: Compute pre-commit cache key run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> "$GITHUB_ENV" - name: Cache pre-commit hook environments uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - name: Install pre-commit # Version-pinned (matches pyproject.toml's `pre-commit~=4.5`, lock at # 4.6.0) for reproducibility, consistent with every other CI tool # install (checkov, semgrep, pdm, towncrier, …) which all pin `==`. # NOTE: this is *not* hash-pinned, so Scorecard's Pinned-Dependencies # check still flags it ("pipCommand not pinned by hash"). That is an # accepted risk — this is a read-only lint job (contents: read, # persist-credentials: false), and --require-hashes would mean pinning # all transitive deps and regenerating on every (automated) bump. # Tracked as dismissed Scorecard alert #7777 ("won't fix"). run: python -m pip install pre-commit==4.6.0 # Retry ONLY the hook-environment download (the network-flaky step). - name: Install pre-commit hook environments uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 with: timeout_minutes: 15 max_attempts: 2 retry_on: error shell: bash command: pre-commit install-hooks # Run the checks exactly once — NO retry. An auto-fixing hook that # modifies a file must fail the job, not be masked by a clean re-run. # timeout-minutes restores the 15-minute ceiling the retry wrapper used to # put on this step (a hung hook is otherwise bounded only by GitHub's # 6-hour job default); install-hooks keeps its own retry timeout above. - name: Run pre-commit timeout-minutes: 15 run: pre-commit run --show-diff-on-failure --color=always --all-files