diff --git a/.github/workflows/fork-integration.yml b/.github/workflows/fork-integration.yml index e7600f58d..3d93c41c6 100644 --- a/.github/workflows/fork-integration.yml +++ b/.github/workflows/fork-integration.yml @@ -21,7 +21,9 @@ jobs: startsWith(github.event.comment.body, '/integration-test ')) && contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }} - runs-on: ubuntu-latest + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest timeout-minutes: 5 permissions: pull-requests: write @@ -47,9 +49,9 @@ jobs: [[ "$COMMENT_BODY" =~ ^/integration-test([[:space:]]+([0-9a-f]{7,40}))?[[:space:]]*$ ]] \ || refuse "use \`/integration-test\`, or \`/integration-test \` with the head commit you reviewed." requested="${BASH_REMATCH[2]}" - pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER") - sha=$(jq -r '.head.sha' <<<"$pr") - head_repo=$(jq -r '.head.repo.full_name // empty' <<<"$pr") + # gh's built-in --jq; the protected runner image does not guarantee a jq binary. + read -r sha head_repo < <(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \ + --jq '[.head.sha, (.head.repo.full_name // "")] | @tsv') || true [[ "$sha" =~ ^[0-9a-f]{40}$ ]] || refuse "could not read the PR head commit." [ -n "$head_repo" ] || refuse "the fork repository no longer exists." # Naming the commit binds the run to what was reviewed, not to a later push. @@ -71,7 +73,9 @@ jobs: startsWith(github.event.comment.body, '/integration-test ')) && !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }} - runs-on: ubuntu-latest + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest timeout-minutes: 5 permissions: pull-requests: write @@ -104,7 +108,9 @@ jobs: name: Report fork integration needs: [authorize, integration] if: ${{ always() && needs.authorize.result == 'success' }} - runs-on: ubuntu-latest + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest timeout-minutes: 5 permissions: statuses: write diff --git a/tests/test_integration_contract.py b/tests/test_integration_contract.py index e46de2bb3..5ca55ac3c 100644 --- a/tests/test_integration_contract.py +++ b/tests/test_integration_contract.py @@ -224,6 +224,8 @@ def test_fork_integration_runs_only_member_requested_pr_heads(): HEAD = "0123456789abcdef0123456789abcdef01234567" +# Verified against the live API on the protected runner; a text change here must be re-verified. +PR_HEAD_JQ = '[.head.sha, (.head.repo.full_name // "")] | @tsv' @pytest.mark.skipif(sys.platform == "win32", reason="the authorize step runs bash on Linux") @@ -247,13 +249,14 @@ def test_fork_integration_authorize_pins_the_pr_head_or_the_reviewed_commit( script = textwrap.dedent(authorize.split(" run: |\n", 1)[1]) bin_dir = tmp_path / "bin" bin_dir.mkdir() - pr = {"head": {"sha": HEAD, "repo": {"full_name": head_repo} if head_repo else None}} - (tmp_path / "pr.json").write_text(json.dumps(pr)) + # What real gh prints for that query; any other --jq expression gets no output and is refused. + (tmp_path / "pr.tsv").write_text(f"{HEAD}\t{head_repo or ''}\n") fake_gh = bin_dir / "gh" fake_gh.write_text( "#!/bin/sh\n" f'echo "$*" >> "{tmp_path}/calls"\n' - f'case "$*" in "api repos/databricks/unity-gateway/pulls/7") cat "{tmp_path}/pr.json";; esac\n' + f"case \"$*\" in 'api repos/databricks/unity-gateway/pulls/7 --jq {PR_HEAD_JQ}')" + f' cat "{tmp_path}/pr.tsv";; esac\n' ) fake_gh.chmod(0o755) output = tmp_path / "github-output" @@ -284,6 +287,16 @@ def test_fork_integration_authorize_pins_the_pr_head_or_the_reviewed_commit( assert reason in calls and "statuses/" not in calls +def test_fork_integration_github_api_jobs_run_on_allowlisted_runners(): + # The org IP allow list rejects GitHub API calls from GitHub-hosted runners. + workflow = _workflow("fork-integration.yml") + protected = " runs-on:\n group: databricks-protected-runner-group\n labels: linux-ubuntu-latest\n" + + assert workflow.count(protected) == 3 + assert "runs-on: ubuntu-latest" not in workflow + assert not re.search(r"(?m)^\s*[^#\n]*\bjq\s+-", workflow) + + def test_no_workflow_uses_pull_request_target(): # GitHub disables pull_request_target by default in public repos from 2026-11-02. workflows = sorted((Path(__file__).parent.parent / ".github/workflows").glob("*.yml"))