Repository navigation
Expand file tree
/
Copy pathreusable-python-static_analysis.yml
More file actions
347 lines (328 loc) · 13.6 KB
/
Copy pathreusable-python-static_analysis.yml
File metadata and controls
347 lines (328 loc) · 13.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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
name: Static analysis
on:
workflow_call:
inputs:
package-manager:
description: Package manager to use (e.g. "conda", "poetry", "pixi")
required: true
type: string
default: poetry
app-name:
description: >
Path (relative to repo root) of the app's source tree, e.g. 'eval-engine'. Scopes PR file filtering, and is the Pylint target itself unless pixi-task is set (then source-dir takes over).
required: true
type: string
python-version:
description: Python version used with pylint
required: false
type: string
default: '3.10'
install-extras:
description: 'Extra dependencies for pip to install (e.g. ["torch", "cuda"])'
required: false
type: string
virtual-repo-names:
description: >
List of repository names to resolve on for pip or Conda
(e.g. ["public-pypi-dev"], or ["public-conda-dev"])
required: false
type: string
pixi-version:
description: Version of Pixi to use
required: false
type: string
default: v0.67.2 # default to last version before lock file version v7
pixi-environment:
description: >
Pixi environment name to install (e.g. 'test-engine'). When provided,
takes precedence over the name derived from python-version (py<version-without-dots>).
required: false
type: string
pixi-task:
description: >
Pixi task name to run linting (e.g. 'lint-engine'). Must match a task
defined in pixi.toml. Validated against available tasks before execution.
Only applies to the pixi package manager; conda/poetry/hatch use pylint directly.
required: false
type: string
pixi-task-args:
description: >
JSON list of extra arguments to pass to the pixi task after '--'
(e.g. '["-v"]'). Optional.
required: false
type: string
source-dir:
description: >
Pixi all-files lint target, relative to the pixi task's cwd, e.g. 'eval_engine'. Only used when pixi-task is set; otherwise the task's 'cmd' must specify its own target.
required: false
type: string
test-dir:
description: >
Test dir for the all-files run, e.g. 'tests'. Relative to the pixi task's cwd if pixi-task is set, else repo root. Defaults to 'tests'; pass '' if there are no tests. With pixi-task, only applies alongside source-dir.
required: false
type: string
default: 'tests'
lfs:
description: Boolean to indicate if GitHub LFS is needed
required: false
type: boolean
default: false
timeout-minutes:
description: Timeout in minutes for the job
required: false
type: number
default: 20
os:
description: 'List of OS to test against (e.g. ["ubuntu-latest", "macos-latest"])'
required: false
type: string
default: "['ubuntu-latest']"
cache-number:
description: Cache number to reset cache if poetry.lock has not changed
required: false
type: number
default: 1
private-github-repos:
description: >
Comma-separated list of repositories to request a GitHub App token for
(e.g. 'scigeoh5,petro-lingo'). When provided, GH_APP_ID and GH_APP_PRIVATE_KEY
secrets must also be passed.
required: false
type: string
secrets:
JFROG_ARTIFACTORY_URL:
description: JFrog Artifactory URL, needed if packages are on Artifactory
required: false
JFROG_ARTIFACTORY_TOKEN:
description: JFrog Artifactory Token, needed if packages are on Artifactory
required: false
GH_CLIENT_ID:
description: GitHub Client ID for accessing private repositories
required: false
GH_APP_PRIVATE_KEY:
description: GitHub App private key for accessing private repositories
required: false
env:
source_tree: ${{ inputs.app-name }}
source_dir: ${{ inputs.source-dir }}
test_dir: ${{ inputs.test-dir }}
jobs:
pylint:
name: pylint
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
strategy:
matrix:
os: ${{ fromJson(inputs.os) }}
runs-on: ${{ matrix.os }}
defaults:
run:
shell: 'bash -l {0}'
timeout-minutes: ${{ inputs.timeout-minutes }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
lfs: ${{ inputs.lfs }}
fetch-depth: 0
persist-credentials: true
- name: Capture modified files
if: github.event_name == 'pull_request'
env:
PACKAGE_MANAGER: ${{ inputs.package-manager }}
PYTHON_VERSION: ${{ inputs.python-version }}
PIXI_TASK: ${{ inputs.pixi-task }}
run: |
git fetch --deepen=500 origin ${GITHUB_BASE_REF}
case "$PACKAGE_MANAGER" in
pixi) LOCK_FILE="pixi.lock" ;;
poetry) LOCK_FILE="poetry.lock" ;;
conda)
os_conda=$( [[ "$RUNNER_OS" == "Linux" ]] && echo "linux" || echo "win")
LOCK_FILE="environments/py-${PYTHON_VERSION}-${os_conda}-64-dev.conda.lock.yml"
;;
hatch) LOCK_FILE="pyproject.toml" ;;
*)
echo "::error::Unknown package manager '$PACKAGE_MANAGER': cannot determine lock file for Pylint force-run detection"
exit 1
;;
esac
changed_files=$(git diff --diff-filter=AM --name-only refs/remotes/origin/${GITHUB_BASE_REF}... --)
if [ -n "$LOCK_FILE" ] && echo "$changed_files" | grep -qxF "$LOCK_FILE"; then
echo "::notice::Lock file '$LOCK_FILE' changed: forcing Pylint to run on all files"
echo "FORCE_ALL_FILES=true" >> $GITHUB_ENV
else
if [ -n "$PIXI_TASK" ]; then
pattern="^(${source_tree})/.*\.py$"
else
pattern="^(${source_tree}|${test_dir})/.*\.py$"
fi
{
echo 'FILES_PARAM<<_EOF_'
echo "$changed_files" | grep -E "$pattern" || true
echo '_EOF_'
} >> $GITHUB_ENV
fi
- name: Remove git credentials
shell: bash
run: |
git config --local --unset-all http.https://github.com/.extraheader || [ $? -eq 5 ]
git config --local --unset-all include.path || [ $? -eq 5 ]
- name: Set up Python version
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
if: ${{ inputs.package-manager == 'poetry' || inputs.package-manager == 'hatch' }}
with:
python-version: ${{inputs.python-version}}
- uses: $/.github/actions/reusable-python-setup_conda
name: Setup conda env
env:
GIT_LFS_SKIP_SMUDGE: "1"
if: ${{ inputs.package-manager == 'conda' }}
with:
cache-number: ${{ inputs.cache-number }}
python-version: ${{ inputs.python-version }}
virtual-repo-names: ${{ inputs.virtual-repo-names }}
JFROG_ARTIFACTORY_URL: ${{ secrets.JFROG_ARTIFACTORY_URL }}
JFROG_ARTIFACTORY_TOKEN: ${{ secrets.JFROG_ARTIFACTORY_TOKEN }}
- uses: $/.github/actions/reusable-python-setup_poetry
name: Setup poetry env
if: ${{ inputs.package-manager == 'poetry' }}
with:
cache-number: ${{ inputs.cache-number }}
runner-os: ${{ runner.os }}
install-extras: ${{ inputs.install-extras }}
virtual-repo-names: ${{ inputs.virtual-repo-names }}
JFROG_ARTIFACTORY_TOKEN: ${{ secrets.JFROG_ARTIFACTORY_TOKEN }}
- uses: $/.github/actions/reusable-python-setup_hatch
name: Setup hatch env
if: ${{ inputs.package-manager == 'hatch' }}
with:
cache-number: ${{ inputs.cache-number }}
runner-os: ${{ runner.os }}
- name: Configure git for pixi environments
if: ${{ inputs.package-manager == 'pixi' }}
shell: bash
run: |
git config --global core.longpaths true
echo "GIT_LFS_SKIP_SMUDGE=1" >> "$GITHUB_ENV"
- name: Generate GitHub App token for private repos
if: ${{ inputs.private-github-repos != '' }}
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: app-token
with:
client-id: ${{ secrets.GH_CLIENT_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
repositories: ${{ inputs.private-github-repos }}
- name: Register the personal access token
if: ${{ inputs.private-github-repos != '' }}
shell: bash
run: echo "${{ steps.app-token.outputs.token }}" | gh auth login --with-token
- name: Configure the Git credential helper
if: ${{ inputs.private-github-repos != '' }}
shell: bash
run: gh auth setup-git
- uses: $/.github/actions/reusable-python-setup_pixi
name: Setup pixi env
if: ${{ inputs.package-manager == 'pixi' }}
with:
cache-number: ${{ inputs.cache-number }}
python-version: ${{ inputs.python-version }}
pixi-version: ${{ inputs.pixi-version }}
environment-name: ${{ inputs.pixi-environment }}
JFROG_ARTIFACTORY_URL: ${{ secrets.JFROG_ARTIFACTORY_URL }}
JFROG_ARTIFACTORY_TOKEN: ${{ secrets.JFROG_ARTIFACTORY_TOKEN }}
- name: Validate pixi task
if: ${{ inputs.package-manager == 'pixi' && inputs.pixi-task }}
env:
PIXI_TASK: ${{ inputs.pixi-task }}
run: |
pixi_json=$(pixi task list --json)
jq_filter='any(.. | objects; .name == $task and (has("cmd") or has("depends_on") or has("cwd") or has("clean_env")))'
if ! echo "$pixi_json" | jq -e --arg task "$PIXI_TASK" "$jq_filter"; then
echo "::error::Pixi task '$PIXI_TASK' is not defined in pixi.toml."
exit 1
fi
task_cwd=$(echo "$pixi_json" | jq -r --arg task "$PIXI_TASK" '.. | objects | select(.name == $task) | .cwd // empty' | head -1)
if [ -n "$task_cwd" ]; then echo "PIXI_TASK_CWD=$task_cwd" >> $GITHUB_ENV; fi
- name: Validate pixi task args
if: ${{ inputs.package-manager == 'pixi' && inputs.pixi-task-args }}
env:
PIXI_TASK_ARGS: ${{ inputs.pixi-task-args }}
run: |
if ! echo "$PIXI_TASK_ARGS" | jq -e 'type == "array" and all(.[]; type == "string")' > /dev/null; then
echo "::error::Invalid pixi-task-args. Must be a JSON array of strings."
exit 1
fi
- name: Pylint skipped
if: ${{ (github.event_name == 'pull_request') && !env.FILES_PARAM && !env.FORCE_ALL_FILES }}
run: |
echo "::notice::Pylint skipped: no applicable file changes detected."
- name: Run Pylint on modified files
if: ${{ (github.event_name == 'pull_request') && env.FILES_PARAM }}
env:
PIXI_TASK: ${{ inputs.pixi-task }}
PIXI_TASK_ARGS: ${{ inputs.pixi-task-args }}
run: |
mapfile -t files <<< "$FILES_PARAM"
if ${{ inputs.package-manager == 'conda' }}; then
pylint "${files[@]}"
elif ${{ inputs.package-manager == 'poetry' }}; then
poetry run pylint "${files[@]}"
elif ${{ inputs.package-manager == 'pixi' }}; then
if [ -n "$PIXI_TASK_CWD" ]; then
prefix="${PIXI_TASK_CWD%/}/"
filtered_files=()
for file in "${files[@]}"; do
case "$file" in
"$prefix"*)
filtered_files+=("${file#"$prefix"}")
;;
esac
done
files=("${filtered_files[@]}")
fi
if [ -n "$PIXI_TASK" ]; then
if [ -n "$PIXI_TASK_ARGS" ]; then
readarray -t args < <(echo "$PIXI_TASK_ARGS" | jq -r '.[]')
pixi run "$PIXI_TASK" -- "${args[@]}" "${files[@]}"
else
pixi run "$PIXI_TASK" -- "${files[@]}"
fi
else
pixi run pylint "${files[@]}"
fi
else
hatch run pylint "${files[@]}"
fi
- name: Run Pylint on all files
if: ${{ github.event_name != 'pull_request' || env.FORCE_ALL_FILES }}
env:
PIXI_TASK: ${{ inputs.pixi-task }}
PIXI_TASK_ARGS: ${{ inputs.pixi-task-args }}
run: |
targets=("$source_tree")
[ -n "$test_dir" ] && targets+=("$test_dir")
if ${{ inputs.package-manager == 'conda' }}; then
pylint --verbose "${targets[@]}"
elif ${{ inputs.package-manager == 'poetry' }}; then
poetry run pylint --verbose "${targets[@]}"
elif ${{ inputs.package-manager == 'pixi' }}; then
if [ -n "$PIXI_TASK" ]; then
args=()
if [ -n "$PIXI_TASK_ARGS" ]; then
readarray -t task_args < <(echo "$PIXI_TASK_ARGS" | jq -r '.[]')
args+=("${task_args[@]}")
fi
if [ -n "$source_dir" ]; then
args+=("$source_dir")
[ -n "$test_dir" ] && args+=("$test_dir")
fi
if [ ${#args[@]} -gt 0 ]; then
pixi run "$PIXI_TASK" -- "${args[@]}"
else
pixi run "$PIXI_TASK"
fi
else
pixi run pylint --verbose "${targets[@]}"
fi
else
hatch run pylint --verbose "${targets[@]}"
fi