99# The integration-trigger workflow validates the comment author and
1010# dispatches this workflow with the PR number. The run posts a result
1111# comment back to the PR when the matrix completes.
12- # 2. Manually from the Actions tab (workflow_dispatch) with pr_number or
13- # git_ref, for ad-hoc testing.
14- # 3. Nightly on `main` at 03:00 IST (21:30 UTC). The schedule is skipped if
15- # the current main SHA has already had a successful integration run.
12+ # 2. Manually from the Actions tab (workflow_dispatch):
13+ # - One PR (e.g. "100") OR comma-separated list ("100,200,300") in pr_numbers.
14+ # - Or a git_ref for ad-hoc testing.
15+ # 3. Nightly on `main` at 03:00 IST (21:30 UTC). The `prepare` job short-circuits
16+ # if the current main SHA has already had a successful integration run,
17+ # emitting an empty targets array so the matrix jobs skip cleanly.
1618#
1719# Security: PR-triggered runs are gated on maintainer comment authorization;
1820# fork-PR code runs in the main repo context (access to secrets) only because
@@ -21,12 +23,12 @@ name: Integration Tests
2123on :
2224 workflow_dispatch :
2325 inputs :
24- pr_number :
25- description : " PR number to test (for external contributions and ad-hoc runs )"
26+ pr_numbers :
27+ description : " PR number(s) to test — single PR or comma-separated for batch (e.g. '100' or '100,200,300' )"
2628 required : false
2729 type : string
2830 git_ref :
29- description : " Git ref (branch/tag/commit) to test"
31+ description : " Git ref (branch/tag/commit) to test — used only when pr_numbers is empty "
3032 required : false
3133 type : string
3234
@@ -37,49 +39,82 @@ permissions:
3739 id-token : write
3840 contents : read
3941
42+ # Target-aware concurrency:
43+ # - Different PRs / batches don't cancel each other.
44+ # - Re-dispatch of the same PR / batch cancels the stale run.
45+ # - Schedule runs share the main-ref group.
4046concurrency :
41- # Each PR and each ad-hoc git_ref gets its own concurrency group so parallel
42- # dispatches don't cancel each other. Schedule runs share the main group.
43- group : ${{ github.workflow }}-${{ inputs.pr_number || inputs.git_ref || github.ref }}
47+ group : ${{ github.workflow }}-${{ github.event.inputs.pr_numbers || github.event.inputs.git_ref || github.ref }}
4448 cancel-in-progress : true
4549
4650jobs :
47- # Schedule-only gate: skip the nightly run if the current main SHA has
48- # already had a successful integration run. Does not run for
49- # workflow_dispatch, so PR-triggered dispatches skip the ~30s runner spin.
50- check-nightly-needed :
51- if : github.event_name == 'schedule'
51+ prepare :
5252 runs-on :
5353 group : databricks-protected-runner-group
5454 labels : linux-ubuntu-latest
5555 outputs :
56- should_run : ${{ steps.decide .outputs.should_run }}
56+ targets : ${{ steps.parse .outputs.targets }}
5757 steps :
58- - name : Decide whether to run
59- id : decide
60- uses : actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
61- with :
62- script : |
63- const runs = await github.rest.actions.listWorkflowRuns({
64- owner: context.repo.owner,
65- repo: context.repo.repo,
66- workflow_id: 'integration.yml',
67- branch: 'main',
68- status: 'success',
69- head_sha: context.sha,
70- });
71- core.info(`main SHA=${context.sha}, prior successful runs=${runs.data.total_count}`);
72- core.setOutput('should_run', runs.data.total_count > 0 ? 'false' : 'true');
58+ - name : Parse targets
59+ id : parse
60+ shell : bash
61+ env :
62+ EVENT_NAME : ${{ github.event_name }}
63+ INPUT_PR_NUMBERS : ${{ github.event.inputs.pr_numbers }}
64+ INPUT_GIT_REF : ${{ github.event.inputs.git_ref }}
65+ DEFAULT_REF : ${{ github.ref }}
66+ GH_TOKEN : ${{ github.token }}
67+ run : |
68+ set -euo pipefail
69+ entry() { printf '{"pr":"%s","ref":"%s"}' "$1" "$2"; }
70+ targets="["
71+ if [[ "$EVENT_NAME" == "schedule" ]]; then
72+ # Nightly skip-if-unchanged: if this main SHA already has a green
73+ # integration run, emit empty targets so the matrix jobs skip.
74+ already_tested=$(curl -sfS \
75+ -H "Authorization: Bearer $GH_TOKEN" \
76+ -H "Accept: application/vnd.github+json" \
77+ "https://api.github.com/repos/$GITHUB_REPOSITORY/actions/workflows/integration.yml/runs?branch=main&status=success&head_sha=$GITHUB_SHA" \
78+ | jq -r '.total_count // 0')
79+ if [[ "$already_tested" -gt 0 ]]; then
80+ echo "Nightly skip: main @ $GITHUB_SHA already has $already_tested successful run(s)."
81+ else
82+ targets+=$(entry "nightly" "$DEFAULT_REF")
83+ fi
84+ elif [[ -n "${INPUT_PR_NUMBERS//[[:space:]]/}" ]]; then
85+ first=1
86+ IFS=',' read -ra prs <<< "$INPUT_PR_NUMBERS"
87+ for pr in "${prs[@]}"; do
88+ pr_trimmed="${pr//[[:space:]]/}"
89+ [[ -z "$pr_trimmed" ]] && continue
90+ if [[ ! "$pr_trimmed" =~ ^[0-9]+$ ]]; then
91+ echo "::error::Invalid PR number '$pr_trimmed' in pr_numbers='$INPUT_PR_NUMBERS' — expected digits, comma-separated."
92+ exit 1
93+ fi
94+ [[ $first -eq 0 ]] && targets+=","
95+ first=0
96+ targets+=$(entry "$pr_trimmed" "refs/pull/$pr_trimmed/head")
97+ done
98+ elif [[ -n "${INPUT_GIT_REF//[[:space:]]/}" ]]; then
99+ targets+=$(entry "manual" "$INPUT_GIT_REF")
100+ else
101+ targets+=$(entry "manual" "$DEFAULT_REF")
102+ fi
103+ targets+="]"
104+ echo "targets=$targets" >> "$GITHUB_OUTPUT"
105+ echo "Parsed targets: $targets"
73106
74107 run-uc-cluster-e2e-tests :
75- needs : check-nightly-needed
76- # On workflow_dispatch the gate is skipped (schedule-only), so use !cancelled()
77- # to let the job run; on schedule it runs iff the gate says should_run.
78- if : |
79- !cancelled() && (
80- github.event_name != 'schedule' ||
81- needs.check-nightly-needed.outputs.should_run == 'true'
82- )
108+ # Do not add `if: always()` / `if: !cancelled()` here or on sibling test jobs —
109+ # `needs: prepare` propagates the external-fork skip cleanly, and forcing
110+ # evaluation would make `fromJSON(needs.prepare.outputs.targets)` fail on an
111+ # empty output. Matrix shape contract: {pr, ref} — defined in the `prepare` job.
112+ needs : prepare
113+ strategy :
114+ fail-fast : false
115+ max-parallel : 2
116+ matrix :
117+ target : ${{ fromJSON(needs.prepare.outputs.targets) }}
83118 runs-on :
84119 group : databricks-protected-runner-group
85120 labels : linux-ubuntu-latest
@@ -96,16 +131,15 @@ jobs:
96131 - name : Check out repository
97132 uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
98133 with :
99- # For workflow_dispatch with pr_number: checkout that PR's head (internal or fork)
100- # For workflow_dispatch with git_ref: checkout that ref
101- # For schedule: falls through to github.ref (refs/heads/main)
102- ref : ${{ (github.event.inputs.pr_number && format('refs/pull/{0}/head', github.event.inputs.pr_number)) || github.event.inputs.git_ref || github.ref }}
103- # Fetch enough history for PR testing
104- fetch-depth : 0
105-
106- - name : Setup JFrog PyPI Proxy
107- uses : ./.github/actions/setup-jfrog-pypi
134+ ref : ${{ matrix.target.ref }}
108135
136+ - name : Setup Python Dependencies
137+ id : deps
138+ uses : ./.github/actions/setup-python-deps
139+
140+ - name : Setup JFrog PyPI Proxy (fallback)
141+ if : steps.deps.outputs.cache-hit != 'true'
142+ uses : ./.github/actions/setup-jfrog-pypi
109143
110144 - name : Set up python
111145 id : setup-python
@@ -119,6 +153,8 @@ jobs:
119153
120154 - name : Install uv
121155 uses : astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
156+ with :
157+ cache-local-path : ~/.cache/uv
122158
123159 - name : Install Hatch
124160 id : install-dependencies
@@ -131,19 +167,17 @@ jobs:
131167 if : always()
132168 uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
133169 with :
134- name : uc-cluster-test-logs
170+ name : uc-cluster-test-logs-${{ matrix.target.pr }}
135171 path : logs/
136172 retention-days : 5
137173
138174 run-sqlwarehouse-e2e-tests :
139- needs : check-nightly-needed
140- # On workflow_dispatch the gate is skipped (schedule-only), so use !cancelled()
141- # to let the job run; on schedule it runs iff the gate says should_run.
142- if : |
143- !cancelled() && (
144- github.event_name != 'schedule' ||
145- needs.check-nightly-needed.outputs.should_run == 'true'
146- )
175+ needs : prepare
176+ strategy :
177+ fail-fast : false
178+ max-parallel : 2
179+ matrix :
180+ target : ${{ fromJSON(needs.prepare.outputs.targets) }}
147181 runs-on :
148182 group : databricks-protected-runner-group
149183 labels : linux-ubuntu-latest
@@ -161,16 +195,15 @@ jobs:
161195 - name : Check out repository
162196 uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
163197 with :
164- # For workflow_dispatch with pr_number: checkout that PR's head (internal or fork)
165- # For workflow_dispatch with git_ref: checkout that ref
166- # For schedule: falls through to github.ref (refs/heads/main)
167- ref : ${{ (github.event.inputs.pr_number && format('refs/pull/{0}/head', github.event.inputs.pr_number)) || github.event.inputs.git_ref || github.ref }}
168- # Fetch enough history for PR testing
169- fetch-depth : 0
170-
171- - name : Setup JFrog PyPI Proxy
172- uses : ./.github/actions/setup-jfrog-pypi
198+ ref : ${{ matrix.target.ref }}
199+
200+ - name : Setup Python Dependencies
201+ id : deps
202+ uses : ./.github/actions/setup-python-deps
173203
204+ - name : Setup JFrog PyPI Proxy (fallback)
205+ if : steps.deps.outputs.cache-hit != 'true'
206+ uses : ./.github/actions/setup-jfrog-pypi
174207
175208 - name : Set up python
176209 id : setup-python
@@ -184,6 +217,8 @@ jobs:
184217
185218 - name : Install uv
186219 uses : astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
220+ with :
221+ cache-local-path : ~/.cache/uv
187222
188223 - name : Install Hatch
189224 id : install-dependencies
@@ -196,19 +231,17 @@ jobs:
196231 if : always()
197232 uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
198233 with :
199- name : sql-endpoint-test-logs
234+ name : sql-endpoint-test-logs-${{ matrix.target.pr }}
200235 path : logs/
201236 retention-days : 5
202237
203238 run-cluster-e2e-tests :
204- needs : check-nightly-needed
205- # On workflow_dispatch the gate is skipped (schedule-only), so use !cancelled()
206- # to let the job run; on schedule it runs iff the gate says should_run.
207- if : |
208- !cancelled() && (
209- github.event_name != 'schedule' ||
210- needs.check-nightly-needed.outputs.should_run == 'true'
211- )
239+ needs : prepare
240+ strategy :
241+ fail-fast : false
242+ max-parallel : 2
243+ matrix :
244+ target : ${{ fromJSON(needs.prepare.outputs.targets) }}
212245 runs-on :
213246 group : databricks-protected-runner-group
214247 labels : linux-ubuntu-latest
@@ -224,16 +257,15 @@ jobs:
224257 - name : Check out repository
225258 uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
226259 with :
227- # For workflow_dispatch with pr_number: checkout that PR's head (internal or fork)
228- # For workflow_dispatch with git_ref: checkout that ref
229- # For schedule: falls through to github.ref (refs/heads/main)
230- ref : ${{ (github.event.inputs.pr_number && format('refs/pull/{0}/head', github.event.inputs.pr_number)) || github.event.inputs.git_ref || github.ref }}
231- # Fetch enough history for PR testing
232- fetch-depth : 0
233-
234- - name : Setup JFrog PyPI Proxy
235- uses : ./.github/actions/setup-jfrog-pypi
260+ ref : ${{ matrix.target.ref }}
261+
262+ - name : Setup Python Dependencies
263+ id : deps
264+ uses : ./.github/actions/setup-python-deps
236265
266+ - name : Setup JFrog PyPI Proxy (fallback)
267+ if : steps.deps.outputs.cache-hit != 'true'
268+ uses : ./.github/actions/setup-jfrog-pypi
237269
238270 - name : Set up python
239271 id : setup-python
@@ -247,6 +279,8 @@ jobs:
247279
248280 - name : Install uv
249281 uses : astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
282+ with :
283+ cache-local-path : ~/.cache/uv
250284
251285 - name : Install Hatch
252286 id : install-dependencies
@@ -259,16 +293,25 @@ jobs:
259293 if : always()
260294 uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
261295 with :
262- name : cluster-test-logs
296+ name : cluster-test-logs-${{ matrix.target.pr }}
263297 path : logs/
264298 retention-days : 5
265299
300+ # Posts a per-job pass/fail summary comment back to the PR when dispatched
301+ # with a single PR number (the slash-command path). Skipped for batch
302+ # dispatches (pr_numbers contains a comma) and for schedule / git_ref runs.
303+ # Matrix jobs' result fields are aggregated across cells, which is why this
304+ # only runs for single-PR dispatches.
266305 report-status :
267306 needs :
268307 - run-uc-cluster-e2e-tests
269308 - run-sqlwarehouse-e2e-tests
270309 - run-cluster-e2e-tests
271- if : always() && github.event_name == 'workflow_dispatch' && inputs.pr_number != ''
310+ if : |
311+ always() &&
312+ github.event_name == 'workflow_dispatch' &&
313+ inputs.pr_numbers != '' &&
314+ !contains(inputs.pr_numbers, ',')
272315 runs-on :
273316 group : databricks-protected-runner-group
274317 labels : linux-ubuntu-latest
@@ -278,7 +321,7 @@ jobs:
278321 - name : Post result comment
279322 uses : actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
280323 env :
281- PR_NUMBER : ${{ inputs.pr_number }}
324+ PR_NUMBER : ${{ inputs.pr_numbers }}
282325 UC_RESULT : ${{ needs.run-uc-cluster-e2e-tests.result }}
283326 SQLW_RESULT : ${{ needs.run-sqlwarehouse-e2e-tests.result }}
284327 CLUSTER_RESULT : ${{ needs.run-cluster-e2e-tests.result }}
@@ -296,9 +339,10 @@ jobs:
296339 const runUrl =
297340 `https://github.com/${context.repo.owner}/${context.repo.repo}` +
298341 `/actions/runs/${context.runId}`;
342+ const prNumber = process.env.PR_NUMBER.trim();
299343 await github.rest.issues.createComment({
300344 owner: context.repo.owner,
301345 repo: context.repo.repo,
302- issue_number: parseInt(process.env.PR_NUMBER , 10),
303- body: `Integration results for PR #${process.env.PR_NUMBER } — ${line}\n\n[Run details](${runUrl}).`,
346+ issue_number: parseInt(prNumber , 10),
347+ body: `Integration results for PR #${prNumber } — ${line}\n\n[Run details](${runUrl}).`,
304348 });
0 commit comments