-
Notifications
You must be signed in to change notification settings - Fork 148
217 lines (200 loc) · 10.1 KB
/
Copy pathwarm-deps-cache.yml
File metadata and controls
217 lines (200 loc) · 10.1 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
name: Warm Dependency Cache
# Sole writer of the "forkvenv-*" GitHub Actions cache that lets FORK PRs run
# unit / lint / type CI fully offline (issue #831).
#
# Why this exists
# ---------------
# Every job in code-quality-checks.yml installs dependencies through
# .github/actions/setup-poetry -> setup-jfrog, which mints a JFrog PyPI token
# via GitHub OIDC. GitHub deliberately withholds the OIDC token
# (ACTIONS_ID_TOKEN_REQUEST_TOKEN) from `pull_request` runs originating in a
# fork, so setup-jfrog dies with "unbound variable" and EVERY required check is
# red on every fork PR — regardless of the diff (see #831, reported against the
# trivial fork PR #671).
#
# Public PyPI is not reachable from the protected runners (everything must go
# through the JFrog proxy), and a fork run can obtain no JFrog credential at
# all. The fix (Databricks SOP, mirrors databricks/dbt-databricks) is to invert
# the flow: a TRUSTED workflow here — which does get OIDC — pre-builds the
# in-project .venv for every matrix leg the PR jobs consume and saves it to the
# Actions cache. The fork PR then RESTORES that .venv and skips `poetry install`
# entirely, so it never needs JFrog. Real tests still run; the fork just holds
# no credentials, so there is no pull_request_target / secret-exposure risk.
#
# What it caches
# --------------
# The fully-built in-project .venv, one cache entry per
# (python-version x dependency-version x extras)
# leg — the same axes code-quality-checks.yml iterates. The min / pyarrow /
# kernel variants pip-install overrides ON TOP of `poetry install`, so the venv
# is saved AFTER those steps to capture the complete resolved environment.
#
# Cache key: forkvenv-<os>-<py>-<depset>-<extras>-<lockhash>-<timestamp>
# GitHub caches are immutable and evict after 7 days of no reads; the timestamp
# suffix lets a re-warm write a fresh entry (old ones age out), and the daily
# schedule keeps the cache from expiring. Consumers restore by the
# "...-<lockhash>-" prefix via restore-keys, so they pick up the newest entry
# for the current lockfile automatically.
#
# Fork dependency changes
# -----------------------
# If a fork PR modifies poetry.lock / pyproject.toml, its lockhash won't match
# any warmed entry and its CI will miss the cache. A maintainer then reviews the
# dependency change and runs this workflow with `pr_number = N`: it fetches ONLY
# the fork's lockfiles (git checkout FETCH_HEAD -- <lockfiles>, no source code)
# and warms a cache entry for the new hash. The contributor re-runs CI.
on:
push:
branches: [main]
paths:
- "poetry.lock"
- "pyproject.toml"
schedule:
# Daily at 06:07 UTC — beats GitHub's 7-day cache eviction. Off the :00
# mark to avoid the cron thundering herd.
- cron: "7 6 * * *"
workflow_dispatch:
inputs:
pr_number:
description: "Fork PR number to warm the cache for (reads lockfiles from the fork branch). Leave empty to warm from main."
required: false
type: string
permissions:
contents: read
id-token: write
pull-requests: read
# Only one warm run at a time per ref — a schedule tick and a push shouldn't
# race to write overlapping cache entries.
concurrency:
group: warm-deps-${{ github.ref }}
cancel-in-progress: false
jobs:
warm:
runs-on:
group: databricks-protected-runner-group
labels: linux-ubuntu-latest
strategy:
# One leg failing (e.g. a transient JFrog hiccup) shouldn't starve the
# rest of the cache — warm everything we can.
fail-fast: false
matrix:
# Mirror code-quality-checks.yml exactly so every fork check has a
# matching warmed entry. extras: "" is the base unit-test / lint / type
# environment; pyarrow and kernel back the two extra unit-test tiers.
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
dependency-version: ["default", "min"]
extras: ["", "pyarrow", "kernel"]
exclude:
# MUST match code-quality-checks.yml's excludes exactly, or a PR leg
# with no warmed cache entry misses and falls back to JFrog (which
# forks can't reach). The PR matrix excludes ONLY 3.12-min and
# 3.13-min — 3.14-min IS a real PR leg, so it must be warmed.
- python-version: "3.12"
dependency-version: "min"
- python-version: "3.13"
dependency-version: "min"
# The kernel wheel is cp310-abi3 (Requires-Python >=3.10); the
# [kernel] extra is a no-op on 3.9, so there's no kernel leg to warm.
- python-version: "3.9"
extras: "kernel"
name: "Warm (py ${{ matrix.python-version }}, ${{ matrix.dependency-version }} deps, extras=${{ matrix.extras || 'base' }})"
steps:
- name: Check out repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Overlay fork PR lockfiles
if: inputs.pr_number != ''
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ inputs.pr_number }}
run: |
set -euo pipefail
# Fetch ONLY the fork's dependency files — never its source or
# workflows — so warming a fork's cache can't execute fork-controlled
# code in this trusted, OIDC-bearing context.
PR_DATA=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")
FORK_REPO=$(echo "$PR_DATA" | jq -r '.head.repo.full_name')
FORK_REF=$(echo "$PR_DATA" | jq -r '.head.ref')
echo "Warming cache for PR #${PR_NUMBER} from ${FORK_REPO}@${FORK_REF}"
git remote add fork "https://github.com/${FORK_REPO}.git"
git fetch --depth=1 fork "${FORK_REF}"
git checkout FETCH_HEAD -- poetry.lock pyproject.toml
echo "Overlaid poetry.lock / pyproject.toml from the fork branch."
# Capture the lock hash from the PRISTINE, committed poetry.lock BEFORE
# setup-poetry runs (it calls `poetry lock`, which rewrites poetry.lock
# in place and changes hashFiles's result). restore-deps in the PR jobs
# hashes the pristine lock, so the warmer MUST hash it at the same point
# or the keys never match and every fork restore misses.
- name: Capture pristine lock hash
id: lockhash
run: |
set -euo pipefail
echo "value=${{ hashFiles('**/poetry.lock') }}" >> "$GITHUB_OUTPUT"
echo "Pristine poetry.lock hash: ${{ hashFiles('**/poetry.lock') }}"
- name: Install Kerberos system dependencies
if: matrix.extras == 'pyarrow' || matrix.extras == 'kernel'
run: |
sudo apt-get update
sudo apt-get install -y libkrb5-dev
- name: Setup Poetry (builds .venv from JFrog)
uses: ./.github/actions/setup-poetry
with:
python-version: ${{ matrix.python-version }}
# extras="" -> base install (no --extras). Otherwise install exactly
# the one extra this leg targets, matching the PR job's isolation.
# NB: the non-empty branch must come FIRST — GitHub's `A && B || C`
# treats an empty-string B as falsy and would fall through to C, so
# `extras=='' && '' || format(...)` wrongly yields "--extras " (no
# value) for the base leg. Test for non-empty and default to "".
install-args: ${{ matrix.extras != '' && format('--extras {0}', matrix.extras) || '' }}
# Distinct suffix per leg so setup-poetry's own "venv-*" cache (used by
# same-repo PRs) never collides across dependency-version / extras.
cache-suffix: "warm-${{ matrix.dependency-version }}-${{ matrix.extras || 'base' }}-"
- name: Install Python tools for custom versions
if: matrix.dependency-version != 'default'
run: poetry run pip install toml packaging
- name: Generate + apply min dependency versions
if: matrix.dependency-version != 'default'
run: |
set -euo pipefail
poetry run python scripts/dependency_manager.py ${{ matrix.dependency-version }} \
--output requirements-${{ matrix.dependency-version }}.txt
echo "Generated requirements for ${{ matrix.dependency-version }} versions:"
cat requirements-${{ matrix.dependency-version }}.txt
# pip-installs the pinned floor ON TOP of the poetry venv — the venv is
# cached AFTER this so the fork restores the complete resolved env.
poetry run pip install -r requirements-${{ matrix.dependency-version }}.txt
- name: Prime mypy stub packages into the venv
# check-types runs `mypy --install-types`, which fetches stub packages
# from the index at runtime. Bake them into the warmed venv now (on the
# default/base leg the type job restores) so the offline fork run has
# them and can drop --install-types.
if: matrix.extras == '' && matrix.dependency-version == 'default'
run: |
mkdir -p .mypy_cache
poetry run mypy --install-types --non-interactive src || true
- name: Show installed versions
run: |
echo "=== py ${{ matrix.python-version }} | ${{ matrix.dependency-version }} deps | extras=${{ matrix.extras || 'base' }} ==="
poetry run pip list
- name: Compute cache key
id: key
run: |
set -euo pipefail
TIMESTAMP=$(date -u +%Y%m%d%H%M%S)
# Pristine hash captured BEFORE setup-poetry's `poetry lock` mutated
# the file — matches what restore-deps computes in the PR jobs.
LOCK_HASH="${{ steps.lockhash.outputs.value }}"
PY="${{ matrix.python-version }}"
DEP="${{ matrix.dependency-version }}"
EXTRAS="${{ matrix.extras || 'base' }}"
BASE="forkvenv-${RUNNER_OS}-${PY}-${DEP}-${EXTRAS}-${LOCK_HASH}"
echo "base=${BASE}" >> "$GITHUB_OUTPUT"
echo "full=${BASE}-${TIMESTAMP}" >> "$GITHUB_OUTPUT"
echo "Cache key: ${BASE}-${TIMESTAMP}"
- name: Save .venv to fork cache
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: .venv
key: ${{ steps.key.outputs.full }}