Skip to content

Commit 4cee223

Browse files
ci: run fork-PR unit/lint/type checks offline from a prewarmed cache (#831)
Fork pull_request runs never receive a GitHub OIDC token, so .github/actions/setup-jfrog (which mints a JFrog PyPI token from that OIDC token) dies with "ACTIONS_ID_TOKEN_REQUEST_TOKEN: unbound variable" and every required check goes red on every fork PR — regardless of the diff (#831, reported against the trivial fork PR #671). Public PyPI is unreachable from the protected runners, so a fork run can obtain no package source at all. Adopt the Databricks SOP (mirrors databricks/dbt-databricks): a trusted warmer workflow that DOES get OIDC pre-builds the in-project .venv for every matrix leg and saves it to the Actions cache; fork PRs restore that .venv and run the tools directly from .venv/bin, fully offline, never touching JFrog. Everything still flows through JFrog (the warmer populates the cache from it); the fork just holds no credentials, so there is no pull_request_target / secret-exposure risk. - .github/workflows/warm-deps-cache.yml: new, sole writer of the forkvenv-* cache. Triggers on push:main (dep files), a daily schedule (beats GitHub's 7-day cache eviction), and workflow_dispatch with an optional pr_number that warms a fork's changed lockfiles (fetches ONLY poetry.lock/pyproject.toml, no source). Warms the full py x depset x extras matrix, priming mypy stubs too. - .github/actions/restore-deps: new composite action; restores the newest forkvenv-* entry for the leg via a restore-keys prefix and reports cache-hit. - .github/workflows/code-quality-checks.yml: each job tries restore-deps first and runs offline from .venv/bin on a hit; on a miss (all same-repo PRs, or a fork whose lockfiles changed pre-warm) it falls back to the original setup-poetry JFrog flow, unchanged. Same-repo PR behavior is unaffected. Scope: unit + lint + type checks (the fork-reachable required checks). e2e / kernel-e2e / integration / DBR-LTS stay merge-queue-gated (they need live warehouse secrets). Closes #831 Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
1 parent d303706 commit 4cee223

3 files changed

Lines changed: 391 additions & 6 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Restore Prewarmed Dependencies
2+
description: |
3+
Restores a fully-built in-project .venv from the "forkvenv-*" cache written by
4+
warm-deps-cache.yml, so FORK PRs run offline without touching JFrog (#831).
5+
6+
Outputs cache-hit. When true, the caller must SKIP setup-poetry (the .venv is
7+
already complete) and only install Poetry itself. When false — a same-repo PR,
8+
or a fork whose lockfiles changed and haven't been warmed — the caller falls
9+
back to setup-poetry (JFrog OIDC), which works in every trusted context and on
10+
cache-miss for same-repo PRs.
11+
12+
inputs:
13+
python-version:
14+
description: Python version this leg targets (must match the warmed leg)
15+
required: true
16+
dependency-version:
17+
description: 'Dependency set: "default" or "min"'
18+
required: false
19+
default: "default"
20+
extras:
21+
description: 'Extras installed in the warmed venv: "" (base), "pyarrow", or "kernel"'
22+
required: false
23+
default: ""
24+
25+
outputs:
26+
cache-hit:
27+
description: "true if a warmed .venv was restored (install can be skipped)"
28+
value: ${{ steps.restore.outputs.cache-matched-key != '' }}
29+
30+
runs:
31+
using: composite
32+
steps:
33+
- name: Restore warmed .venv
34+
id: restore
35+
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
36+
with:
37+
path: .venv
38+
# The warmer appends a timestamp to the key (caches are immutable), so
39+
# there is no fixed exact key to hit. restore-keys does a PREFIX match
40+
# and returns the most-recently-created entry for this lockfile +
41+
# matrix leg — exactly the freshest warmed venv.
42+
key: forkvenv-${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.dependency-version }}-${{ inputs.extras || 'base' }}-${{ hashFiles('**/poetry.lock') }}-never
43+
restore-keys: |
44+
forkvenv-${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.dependency-version }}-${{ inputs.extras || 'base' }}-${{ hashFiles('**/poetry.lock') }}-
45+
46+
- name: Report restore outcome
47+
shell: bash
48+
run: |
49+
if [ -n "${{ steps.restore.outputs.cache-matched-key }}" ]; then
50+
echo "Restored warmed .venv (${{ steps.restore.outputs.cache-matched-key }}) — install will be skipped."
51+
else
52+
echo "No warmed .venv for this lockfile/leg — caller falls back to setup-poetry (JFrog)."
53+
fi

.github/workflows/code-quality-checks.yml

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@ name: Code Quality Checks
22

33
on: [pull_request]
44

5+
# How fork PRs are made to work (issue #831)
6+
# ------------------------------------------
7+
# Every job below installs deps via .github/actions/setup-poetry ->
8+
# setup-jfrog, which needs a GitHub OIDC token to authenticate to the JFrog
9+
# PyPI proxy. GitHub withholds that token from fork `pull_request` runs, and
10+
# public PyPI is unreachable from the protected runners — so on a fork PR the
11+
# JFrog path cannot run at all (this is what made every fork check red).
12+
#
13+
# Each job therefore takes a cache-first path:
14+
# 1. restore-deps restores a fully-built .venv prewarmed by
15+
# warm-deps-cache.yml (a trusted workflow that DOES have OIDC).
16+
# 2. On a cache HIT (the normal fork path) we run the tools directly from
17+
# .venv/bin — NOT `poetry run` — because even bootstrapping Poetry
18+
# (`pip install poetry`) would hit JFrog. The restored venv already
19+
# contains pytest/black/mypy and the connector, so no install is needed.
20+
# 3. On a cache MISS (every same-repo PR, or a fork whose lockfiles changed
21+
# before a maintainer re-warmed) we fall back to the original
22+
# setup-poetry (JFrog OIDC) flow, unchanged. Same-repo PRs are unaffected.
23+
#
24+
# Each step branches on the restore step's output, `steps.deps.outputs.cache-hit`.
25+
526
permissions:
627
contents: read
728
id-token: write
@@ -26,28 +47,49 @@ jobs:
2647
steps:
2748
- name: Check out repository
2849
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
50+
51+
# --- Fork path: restore a prewarmed .venv and run offline -------------
52+
- name: Restore prewarmed dependencies
53+
id: deps
54+
uses: ./.github/actions/restore-deps
55+
with:
56+
python-version: ${{ matrix.python-version }}
57+
dependency-version: ${{ matrix.dependency-version }}
58+
- name: Set up Python (cache hit — needed for the restored venv interpreter)
59+
if: steps.deps.outputs.cache-hit == 'true'
60+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
61+
with:
62+
python-version: ${{ matrix.python-version }}
63+
- name: Run tests (offline, from prewarmed venv)
64+
if: steps.deps.outputs.cache-hit == 'true'
65+
run: .venv/bin/python -m pytest tests/unit -m "not realkernel"
66+
67+
# --- Fallback path: same-repo PRs / cache miss use JFrog (unchanged) --
2968
- name: Setup Poetry
69+
if: steps.deps.outputs.cache-hit != 'true'
3070
uses: ./.github/actions/setup-poetry
3171
with:
3272
python-version: ${{ matrix.python-version }}
3373
cache-suffix: "${{ matrix.dependency-version }}-"
3474
- name: Install Python tools for custom versions
35-
if: matrix.dependency-version != 'default'
75+
if: steps.deps.outputs.cache-hit != 'true' && matrix.dependency-version != 'default'
3676
run: poetry run pip install toml packaging
3777
- name: Generate requirements file
38-
if: matrix.dependency-version != 'default'
78+
if: steps.deps.outputs.cache-hit != 'true' && matrix.dependency-version != 'default'
3979
run: |
4080
poetry run python scripts/dependency_manager.py ${{ matrix.dependency-version }} --output requirements-${{ matrix.dependency-version }}.txt
4181
echo "Generated requirements for ${{ matrix.dependency-version }} versions:"
4282
cat requirements-${{ matrix.dependency-version }}.txt
4383
- name: Override with custom dependency versions
44-
if: matrix.dependency-version != 'default'
84+
if: steps.deps.outputs.cache-hit != 'true' && matrix.dependency-version != 'default'
4585
run: poetry run pip install -r requirements-${{ matrix.dependency-version }}.txt
4686
- name: Show installed versions
87+
if: steps.deps.outputs.cache-hit != 'true'
4788
run: |
4889
echo "=== Dependency Version: ${{ matrix.dependency-version }} ==="
4990
poetry run pip list
5091
- name: Run tests
92+
if: steps.deps.outputs.cache-hit != 'true'
5193
run: poetry run python -m pytest tests/unit -m "not realkernel"
5294

5395
run-unit-tests-with-arrow:
@@ -69,11 +111,33 @@ jobs:
69111
steps:
70112
- name: Check out repository
71113
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
114+
# libkrb5 is a runtime system lib (not in the venv), so install it on
115+
# both the fork and fallback paths.
72116
- name: Install Kerberos system dependencies
73117
run: |
74118
sudo apt-get update
75119
sudo apt-get install -y libkrb5-dev
120+
121+
# --- Fork path: restore prewarmed pyarrow venv, run offline ------------
122+
- name: Restore prewarmed dependencies
123+
id: deps
124+
uses: ./.github/actions/restore-deps
125+
with:
126+
python-version: ${{ matrix.python-version }}
127+
dependency-version: ${{ matrix.dependency-version }}
128+
extras: pyarrow
129+
- name: Set up Python (cache hit — needed for the restored venv interpreter)
130+
if: steps.deps.outputs.cache-hit == 'true'
131+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
132+
with:
133+
python-version: ${{ matrix.python-version }}
134+
- name: Run tests (offline, from prewarmed venv)
135+
if: steps.deps.outputs.cache-hit == 'true'
136+
run: .venv/bin/python -m pytest tests/unit -m "not realkernel"
137+
138+
# --- Fallback path: same-repo PRs / cache miss use JFrog (unchanged) ---
76139
- name: Setup Poetry
140+
if: steps.deps.outputs.cache-hit != 'true'
77141
uses: ./.github/actions/setup-poetry
78142
with:
79143
python-version: ${{ matrix.python-version }}
@@ -84,22 +148,24 @@ jobs:
84148
install-args: "--extras pyarrow"
85149
cache-suffix: "pyarrow-${{ matrix.dependency-version }}-"
86150
- name: Install Python tools for custom versions
87-
if: matrix.dependency-version != 'default'
151+
if: steps.deps.outputs.cache-hit != 'true' && matrix.dependency-version != 'default'
88152
run: poetry run pip install toml packaging
89153
- name: Generate requirements file with pyarrow
90-
if: matrix.dependency-version != 'default'
154+
if: steps.deps.outputs.cache-hit != 'true' && matrix.dependency-version != 'default'
91155
run: |
92156
poetry run python scripts/dependency_manager.py ${{ matrix.dependency-version }} --output requirements-${{ matrix.dependency-version }}-arrow.txt
93157
echo "Generated requirements for ${{ matrix.dependency-version }} versions with PyArrow:"
94158
cat requirements-${{ matrix.dependency-version }}-arrow.txt
95159
- name: Override with custom dependency versions
96-
if: matrix.dependency-version != 'default'
160+
if: steps.deps.outputs.cache-hit != 'true' && matrix.dependency-version != 'default'
97161
run: poetry run pip install -r requirements-${{ matrix.dependency-version }}-arrow.txt
98162
- name: Show installed versions
163+
if: steps.deps.outputs.cache-hit != 'true'
99164
run: |
100165
echo "=== Dependency Version: ${{ matrix.dependency-version }} with PyArrow ==="
101166
poetry run pip list
102167
- name: Run tests
168+
if: steps.deps.outputs.cache-hit != 'true'
103169
run: poetry run python -m pytest tests/unit -m "not realkernel"
104170

105171
run-unit-tests-with-kernel:
@@ -113,6 +179,8 @@ jobs:
113179
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
114180

115181
name: "Unit Tests + Kernel (Python ${{ matrix.python-version }})"
182+
# base restore-deps entries omit 3.9-kernel (no kernel wheel there); this
183+
# matrix already starts at 3.10, so every leg has a warmed counterpart.
116184

117185
steps:
118186
- name: Check out repository
@@ -121,7 +189,35 @@ jobs:
121189
run: |
122190
sudo apt-get update
123191
sudo apt-get install -y libkrb5-dev
192+
193+
# --- Fork path: restore prewarmed kernel venv, run offline ------------
194+
# NOTE: the [kernel] extra pulls databricks-sql-kernel, the one PRIVATE
195+
# (JFrog-only) package. Baking it into the warmed venv is precisely what
196+
# lets a fork exercise the kernel routing without any JFrog credential.
197+
- name: Restore prewarmed dependencies
198+
id: deps
199+
uses: ./.github/actions/restore-deps
200+
with:
201+
python-version: ${{ matrix.python-version }}
202+
extras: kernel
203+
- name: Set up Python (cache hit — needed for the restored venv interpreter)
204+
if: steps.deps.outputs.cache-hit == 'true'
205+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
206+
with:
207+
python-version: ${{ matrix.python-version }}
208+
- name: Assert the real kernel wheel is installed (offline)
209+
if: steps.deps.outputs.cache-hit == 'true'
210+
run: .venv/bin/python -c "import databricks_sql_kernel as k; assert k.__file__, 'kernel wheel missing __file__ — not the real wheel'; print('real kernel wheel:', k.__file__)"
211+
- name: Unit tests, realkernel deselected (offline)
212+
if: steps.deps.outputs.cache-hit == 'true'
213+
run: .venv/bin/python -m pytest tests/unit -m "not realkernel"
214+
- name: Drive use_kernel=True through the REAL wheel — routing (offline)
215+
if: steps.deps.outputs.cache-hit == 'true'
216+
run: .venv/bin/python -m pytest tests/unit/test_session.py -m realkernel -v
217+
218+
# --- Fallback path: same-repo PRs / cache miss use JFrog (unchanged) --
124219
- name: Setup Poetry
220+
if: steps.deps.outputs.cache-hit != 'true'
125221
uses: ./.github/actions/setup-poetry
126222
with:
127223
python-version: ${{ matrix.python-version }}
@@ -132,19 +228,23 @@ jobs:
132228
install-args: "--extras kernel"
133229
cache-suffix: "kernel-"
134230
- name: Show installed versions
231+
if: steps.deps.outputs.cache-hit != 'true'
135232
run: |
136233
echo "=== with databricks-sql-kernel ==="
137234
poetry run pip list
138235
- name: Assert the real kernel wheel is installed (not a stub)
236+
if: steps.deps.outputs.cache-hit != 'true'
139237
run: |
140238
poetry run python -c "import databricks_sql_kernel as k; assert k.__file__, 'kernel wheel missing __file__ — not the real wheel'; print('real kernel wheel:', k.__file__)"
141239
- name: Unit tests (kernel wheel present, realkernel deselected)
240+
if: steps.deps.outputs.cache-hit != 'true'
142241
# The bulk of tests/unit fakes databricks_sql_kernel in
143242
# sys.modules, so the real-wheel routing test is deselected here
144243
# and run on its own below (a shared session would shadow the
145244
# real wheel — both real-wheel tests fail loudly if that happens).
146245
run: poetry run python -m pytest tests/unit -m "not realkernel"
147246
- name: Drive use_kernel=True through the REAL wheel (routing)
247+
if: steps.deps.outputs.cache-hit != 'true'
148248
# Separate invocation, explicit file path: never collects the
149249
# fake-module test file, so sys.modules stays unpolluted. This is
150250
# the no-network proof that sql.connect(use_kernel=True) actually
@@ -162,11 +262,26 @@ jobs:
162262
steps:
163263
- name: Check out repository
164264
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
265+
- name: Restore prewarmed dependencies
266+
id: deps
267+
uses: ./.github/actions/restore-deps
268+
with:
269+
python-version: ${{ matrix.python-version }}
270+
- name: Set up Python (cache hit — needed for the restored venv interpreter)
271+
if: steps.deps.outputs.cache-hit == 'true'
272+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
273+
with:
274+
python-version: ${{ matrix.python-version }}
275+
- name: Black (offline, from prewarmed venv)
276+
if: steps.deps.outputs.cache-hit == 'true'
277+
run: .venv/bin/black --check src
165278
- name: Setup Poetry
279+
if: steps.deps.outputs.cache-hit != 'true'
166280
uses: ./.github/actions/setup-poetry
167281
with:
168282
python-version: ${{ matrix.python-version }}
169283
- name: Black
284+
if: steps.deps.outputs.cache-hit != 'true'
170285
run: poetry run black --check src
171286

172287
check-types:
@@ -179,11 +294,30 @@ jobs:
179294
steps:
180295
- name: Check out repository
181296
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
297+
- name: Restore prewarmed dependencies
298+
id: deps
299+
uses: ./.github/actions/restore-deps
300+
with:
301+
python-version: ${{ matrix.python-version }}
302+
- name: Set up Python (cache hit — needed for the restored venv interpreter)
303+
if: steps.deps.outputs.cache-hit == 'true'
304+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
305+
with:
306+
python-version: ${{ matrix.python-version }}
307+
- name: Mypy (offline, from prewarmed venv)
308+
if: steps.deps.outputs.cache-hit == 'true'
309+
# --install-types would reach the index; the warmed venv already has
310+
# the stub packages, so run mypy without it on the offline path.
311+
run: |
312+
mkdir -p .mypy_cache
313+
.venv/bin/mypy --non-interactive src
182314
- name: Setup Poetry
315+
if: steps.deps.outputs.cache-hit != 'true'
183316
uses: ./.github/actions/setup-poetry
184317
with:
185318
python-version: ${{ matrix.python-version }}
186319
- name: Mypy
320+
if: steps.deps.outputs.cache-hit != 'true'
187321
run: |
188322
mkdir .mypy_cache
189323
poetry run mypy --install-types --non-interactive src

0 commit comments

Comments
 (0)