Repository navigation
278 lines (252 loc) · 11.6 KB
/
Copy pathpre-commit.yml
File metadata and controls
278 lines (252 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
name: pre-commit checks
on:
push:
branches:
- "master"
- "[0-9].[0-9]*"
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
# Nightly full-tree sweep. Per-PR runs only lint changed files, so a change
# that invalidates an untouched file (e.g. a type change that breaks an
# importing test) can pass every PR yet leave master red. This catches that.
schedule:
- cron: "0 6 * * *"
permissions:
contents: read
actions: read
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
pre-commit:
runs-on: ubuntu-26.04
timeout-minutes: 20
strategy:
matrix:
# Run the full version spread on push (master/release) and nightly,
# but only the current version on PRs — lint/format/type results
# rarely differ across patch versions, so 3x per PR is wasteful.
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "next"]') }}
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
# Full history so we can diff a PR/push against its base commit to
# determine changed files (see "Determine changed files" below).
fetch-depth: 0
# Determined early (right after checkout, before any dependency
# installs) so the frontend/docs installs below can skip themselves
# when nothing under those trees changed — npm ci and yarn install are
# the two most expensive steps in this job, and every hook that needs
# them (oxfmt/oxlint/stylelint/type-checking for superset-frontend,
# oxlint-docs for docs) is itself gated in .pre-commit-config.yaml on
# exactly those path prefixes, so skipping the installs alongside the
# hooks that would use them is safe, not just faster. The Node.js setup
# has its own, slightly wider gate (needs_node): oxfmt-websocket runs
# `npx oxfmt` with no install at all, but still wants the .nvmrc Node.
- name: Determine changed files
id: changed_files
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
emit_needs() {
# $1: mode (all/none). The "files" mode is handled separately,
# via scripts/pre_commit_dependency_needs.py, not this function.
case "$1" in
all)
echo "needs_frontend=true" >> "$GITHUB_OUTPUT"
echo "needs_docs=true" >> "$GITHUB_OUTPUT"
echo "needs_node=true" >> "$GITHUB_OUTPUT"
;;
none)
echo "needs_frontend=false" >> "$GITHUB_OUTPUT"
echo "needs_docs=false" >> "$GITHUB_OUTPUT"
echo "needs_node=false" >> "$GITHUB_OUTPUT"
;;
esac
}
# Scheduled runs check the whole tree (see the pre-commit step).
if [ "${EVENT_NAME}" = "schedule" ]; then
echo "mode=all" >> "$GITHUB_OUTPUT"
emit_needs all
exit 0
fi
# Resolve the commit to diff against.
base=""
if [ "${EVENT_NAME}" = "pull_request" ]; then
# HEAD is the PR merge commit, so its first parent is the current
# tip of the base branch. github.event.pull_request.base.sha is
# NOT that: GitHub freezes it at PR creation, so on a long-lived
# PR it points at the original branch point and the diff picks up
# all of the base branch's churn since then — thousands of paths,
# enough for the CHANGED_FILES env var below to exceed the
# kernel's per-variable size limit and kill the step with
# "Argument list too long" before bash even starts.
base="$(git rev-parse HEAD^1 2>/dev/null || true)"
elif [ -n "${BEFORE_SHA:-}" ] && \
[ "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]; then
base="${BEFORE_SHA}"
fi
# Fail closed: if the diff base can't be resolved, check every file
# instead of silently checking nothing. Previously an empty file list
# made `pre-commit run --files` a no-op that still reported success,
# which let unlinted code reach master.
if [ -z "${base}" ] || ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
echo "::notice::Could not resolve a diff base; falling back to --all-files."
echo "mode=all" >> "$GITHUB_OUTPUT"
emit_needs all
exit 0
fi
# Files present in HEAD that changed since the base (drop deletions).
files="$(git diff --name-only --diff-filter=ACMRT "${base}...HEAD")"
# Env vars have a hard per-variable size limit (E2BIG at step
# start). A PR that legitimately touches thousands of files is
# better served by --all-files anyway.
if [ "$(printf '%s' "${files}" | wc -c)" -gt 100000 ]; then
echo "::notice::Changed-file list too large to pass via env; falling back to --all-files."
echo "mode=all" >> "$GITHUB_OUTPUT"
emit_needs all
exit 0
fi
if [ -z "${files}" ]; then
echo "mode=none" >> "$GITHUB_OUTPUT"
emit_needs none
else
echo "mode=files" >> "$GITHUB_OUTPUT"
{
echo "files<<__CHANGED_FILES_EOF__"
echo "${files}"
echo "__CHANGED_FILES_EOF__"
} >> "$GITHUB_OUTPUT"
# scripts/pre_commit_dependency_needs.py (tests:
# tests/unit_tests/scripts/pre_commit_dependency_needs_test.py)
# writes needs_frontend/needs_docs/needs_node straight to
# $GITHUB_OUTPUT. A
# prior inline `printf | grep -q` version risked a false "not
# needed" result: under `pipefail`, grep -q's early exit on a
# match can SIGPIPE a producer still mid-write for a large
# enough list (our own 100000-byte cap above is bigger than a
# typical 64KB pipe buffer), silently skipping a needed install
# instead of running it. Plain Python reading stdin to EOF has
# no equivalent early-exit-close behavior.
printf '%s\n' "${files}" | python3 scripts/pre_commit_dependency_needs.py
fi
- name: Setup Python
uses: $/.github/actions/setup-backend/
with:
python-version: ${{ matrix.python-version }}
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
- name: Install helm-docs
run: go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2
# Spike: run the existing .pre-commit-config.yaml through prek (a Rust
# reimplementation of pre-commit) instead of pre-commit itself, to see
# whether it's viable to speed up this job. CI-only — contributors keep
# installing/running `pre-commit` locally exactly as documented; nothing
# here changes that.
- name: Install prek
run: |
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prek/releases/download/v0.4.11/prek-installer.sh | sh
- name: Setup Node.js
if: steps.changed_files.outputs.needs_node == 'true'
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: "superset-frontend/.nvmrc"
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install Frontend Dependencies
if: steps.changed_files.outputs.needs_frontend == 'true'
run: |
cd superset-frontend
npm ci
- name: Cache yarn (docs)
if: steps.changed_files.outputs.needs_docs == 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/yarn
key: docs-yarn-v1-${{ runner.os }}-${{ hashFiles('docs/yarn.lock') }}
restore-keys: |
docs-yarn-v1-${{ runner.os }}-
- name: Install Docs Dependencies
if: steps.changed_files.outputs.needs_docs == 'true'
run: |
cd docs
yarn install --immutable
- name: Cache prek environments
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/prek
key: prek-v1-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
prek-v1-${{ runner.os }}-py${{ matrix.python-version }}-
- name: pre-commit (via prek)
env:
MODE: ${{ steps.changed_files.outputs.mode }}
CHANGED_FILES: ${{ steps.changed_files.outputs.files }}
run: |
set +e # Don't exit immediately on failure
export SKIP=type-checking-frontend
case "${MODE}" in
all)
echo "ℹ️ Running prek on all files."
prek run --all-files
;;
files)
echo "ℹ️ Running prek on changed files:"
echo "${CHANGED_FILES}"
# shellcheck disable=SC2086
prek run --files ${CHANGED_FILES}
;;
none)
echo "ℹ️ No source files changed; nothing for prek to check."
exit 0
;;
*)
echo "⚠️ Unrecognized changed-files mode '${MODE}'; checking all files."
prek run --all-files
;;
esac
PRE_COMMIT_EXIT_CODE=$?
git diff --quiet --exit-code
GIT_DIFF_EXIT_CODE=$?
if [ "${PRE_COMMIT_EXIT_CODE}" -ne 0 ] || [ "${GIT_DIFF_EXIT_CODE}" -ne 0 ]; then
if [ "${PRE_COMMIT_EXIT_CODE}" -ne 0 ]; then
echo "❌ Pre-commit check failed (exit code: ${PRE_COMMIT_EXIT_CODE})."
echo "🔍 Modified files:"
git diff --name-only
else
echo "❌ Git working directory is dirty."
echo "📌 This likely means that pre-commit made changes that were not committed."
echo "🔍 Modified files:"
git diff --name-only
fi
echo "🚒 To prevent/address this CI issue, please install/use pre-commit locally."
echo "📖 More details here: https://superset.apache.org/docs/contributing/development#git-hooks"
exit 1
fi
# Sole-input mypy regression guards (tests/mypy_fixtures/). Each fixture
# pins a type that mypy resolves differently when the file is its only
# input than when the whole tree is loaded, so neither the scoped run
# above nor the nightly all-files sweep can stand in for this: a revert
# of the guarded declaration only surfaces when the fixture is checked
# alone. Hence one prek invocation per fixture.
- name: mypy sole-input fixtures
run: |
set -euo pipefail
for fixture in tests/mypy_fixtures/[!_]*.py; do
echo "ℹ️ Checking ${fixture} as mypy's sole input."
prek run mypy-sole-input-fixtures --stage manual --files "${fixture}"
done
actions-timeline:
needs: pre-commit
if: always()
runs-on: ubuntu-slim
steps:
- uses: Kesin11/actions-timeline@57fc93f20c6da7fbc14063c6d24a2a5627c799ad # v3.2.0
with:
expand-composite-actions: true