From 3c16c274152ff78bcc7872113337df1acd5113ae Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 30 Apr 2026 20:39:48 -0700 Subject: [PATCH] ci(wheel-smoke): always-run with per-step if-gates for required-check eligibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `PR-built wheel + import smoke` gate caught the broken wheel from PR #2433 (`import inbox as _inbox_module` collision) but couldn't block the merge because it isn't a required check on staging. Promoting it to required is the right move per the runtime publish pipeline gates note (2026-04-27 RuntimeCapabilities ImportError outage), but the existing `paths: [workspace/**, scripts/...]` filter blocks PRs that don't touch those paths from ever generating the check run — branch protection would deadlock waiting on a check that never fires. Refactor (same shape as e2e-api.yml's e2e-api job): - Drop top-level `paths:` filter — workflow runs on every push/PR/ merge_group event. - Add `detect-changes` job using dorny/paths-filter to compute the `wheel=true|false` output. - Collapse to ONE always-running `local-build-install` job named `PR-built wheel + import smoke`. Per-step `if:` gates on the detect output. PRs untouched by wheel-relevant paths emit a no-op SUCCESS step ("paths filter excluded this commit") so the check passes without rebuilding the wheel. - merge_group + workflow_dispatch unconditionally `wheel=true` so the queue always validates the to-be-merged state, regardless of which PR composed it. Why one-job-with-step-gates instead of two-jobs-sharing-name: SKIPPED check runs block branch protection even when SUCCESS siblings exist (verified PR #2264 incident, 2026-04-29). Single always-run job emits exactly one SUCCESS check run regardless of paths filter. Follow-up: open a separate PR adding `PR-built wheel + import smoke` to the staging branch protection's required_status_checks.contexts once this lands. Doing both in one PR risks the protection update firing before the workflow refactor merges, deadlocking unrelated PRs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/runtime-prbuild-compat.yml | 83 ++++++++++++++------ 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/.github/workflows/runtime-prbuild-compat.yml b/.github/workflows/runtime-prbuild-compat.yml index 96f1a289..0bc9a511 100644 --- a/.github/workflows/runtime-prbuild-compat.yml +++ b/.github/workflows/runtime-prbuild-compat.yml @@ -23,55 +23,88 @@ name: Runtime PR-Built Compatibility # # By building from the PR's source and smoke-importing THAT wheel, we # fail at PR-time instead of after publish. +# +# Required-check shape (2026-05-01): the workflow runs on EVERY push + +# PR + merge_group event with no top-level `paths:` filter, then uses a +# detect-changes job + per-step `if:` gates inside ONE always-running +# job named `PR-built wheel + import smoke`. PRs that don't touch +# wheel-relevant paths get a no-op SUCCESS check run, satisfying branch +# protection without re-running the heavy build. Same pattern as +# e2e-api.yml — see its comment for the full rationale + the 2026-04-29 +# PR #2264 incident that motivated the always-run-with-if-gates shape. on: push: branches: [main, staging] - paths: - # Broad filter: this workflow's verdict can change whenever any - # workspace/ source file changes (because the wheel we build is - # produced from those files), or when the build script itself - # changes (it controls the wheel layout). - - 'workspace/**' - - 'scripts/build_runtime_package.py' - - 'scripts/wheel_smoke.py' - - '.github/workflows/runtime-prbuild-compat.yml' pull_request: branches: [main, staging] - paths: - - 'workspace/**' - - 'scripts/build_runtime_package.py' - - 'scripts/wheel_smoke.py' - - '.github/workflows/runtime-prbuild-compat.yml' workflow_dispatch: - # Required-check support: when this becomes a branch-protection gate, - # merge_group runs let the queue green-check this in addition to PRs. merge_group: types: [checks_requested] - # No cron: the same pre-merge run already covered the commit, and - # re-running daily wouldn't surface anything new (workspace/ doesn't - # change between cron firings unless a PR already passed this gate). concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.sha }} cancel-in-progress: true jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + wheel: ${{ steps.decide.outputs.wheel }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter + with: + filters: | + wheel: + - 'workspace/**' + - 'scripts/build_runtime_package.py' + - 'scripts/wheel_smoke.py' + - '.github/workflows/runtime-prbuild-compat.yml' + - id: decide + # Always run real work for manual dispatch + merge_group — no + # diff-against-base in those contexts, and the gate exists to + # validate the to-be-merged state regardless of which paths it + # touched (paths-filter would default to "no changes" which is + # the wrong answer when the queue is composing many PRs). + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "merge_group" ]; then + echo "wheel=true" >> "$GITHUB_OUTPUT" + else + echo "wheel=${{ steps.filter.outputs.wheel }}" >> "$GITHUB_OUTPUT" + fi + + # ONE job (no job-level `if:`) that always runs and reports under the + # required-check name `PR-built wheel + import smoke`. Real work is + # gated per-step on `needs.detect-changes.outputs.wheel`. Same shape + # as e2e-api.yml's e2e-api job — see its comment block for the full + # rationale (SKIPPED check runs block branch protection even with + # SUCCESS siblings; collapsing to one always-run job emits exactly + # one SUCCESS check run). local-build-install: - # Builds the wheel from THIS PR's workspace/ + scripts/ and tests - # IT — the artifact that WOULD be published if this PR merges. + needs: detect-changes name: PR-built wheel + import smoke runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: No-op pass (paths filter excluded this commit) + if: needs.detect-changes.outputs.wheel != 'true' + run: | + echo "No workspace/ / scripts/{build_runtime_package,wheel_smoke}.py / workflow changes — wheel gate satisfied without rebuilding." + echo "::notice::PR-built wheel + import smoke no-op pass (paths filter excluded this commit)." + - if: needs.detect-changes.outputs.wheel == 'true' + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - if: needs.detect-changes.outputs.wheel == 'true' + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.11' cache: pip cache-dependency-path: workspace/requirements.txt - name: Install build tooling + if: needs.detect-changes.outputs.wheel == 'true' run: pip install build - name: Build wheel from PR source (mirrors publish-runtime.yml) + if: needs.detect-changes.outputs.wheel == 'true' # Use a fixed test version so the wheel filename is predictable. # Doesn't reach PyPI — this build is local-only for the smoke. # Use the SAME build script with the SAME args as @@ -88,6 +121,7 @@ jobs: --out /tmp/runtime-build cd /tmp/runtime-build && python -m build - name: Install built wheel + workspace requirements + if: needs.detect-changes.outputs.wheel == 'true' run: | python -m venv /tmp/venv-built /tmp/venv-built/bin/pip install --upgrade pip @@ -96,6 +130,7 @@ jobs: /tmp/venv-built/bin/pip show molecule-ai-workspace-runtime a2a-sdk \ | grep -E '^(Name|Version):' - name: Smoke import the PR-built wheel + if: needs.detect-changes.outputs.wheel == 'true' # Same script publish-runtime.yml runs against the to-be-PyPI wheel. # Closes the PR-time vs publish-time gap: a PR adding a new SDK # call-shape no longer passes here (narrow `import main_sync`) only