From b9ab903f357d815d79d5074a7604b6f7a7ba992a Mon Sep 17 00:00:00 2001 From: William Zhao Date: Wed, 12 Aug 2026 14:26:45 -0400 Subject: [PATCH 1/2] ci: fix perf report PR lookup for fork branches gh pr list --head does not support owner:branch syntax, so fork PRs were never matched and performance-report skipped posting comments. Search by branch name and filter results by head repository owner instead. Signed-off-by: William Zhao --- e2e/perf/get-pr-info.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/e2e/perf/get-pr-info.py b/e2e/perf/get-pr-info.py index fb9c5181b..a3e5c10ab 100755 --- a/e2e/perf/get-pr-info.py +++ b/e2e/perf/get-pr-info.py @@ -28,14 +28,20 @@ def find_prs_by_head_branch(head_branch: str, head_owner: str | None = None) -> if not head_branch: return [] - head_ref = f"{head_owner}:{head_branch}" if head_owner else head_branch - print(f"Searching for PRs by head: {head_ref}", file=sys.stderr) + # gh pr list --head accepts the branch name only; owner:branch returns no results. + if head_owner: + print( + f"Searching for PRs by head branch {head_branch!r} (owner: {head_owner})", + file=sys.stderr, + ) + else: + print(f"Searching for PRs by head branch: {head_branch}", file=sys.stderr) try: result = subprocess.run( [ 'gh', 'pr', 'list', - '--head', head_ref, + '--head', head_branch, '--json', 'number,headRepositoryOwner', ], capture_output=True, From 705ba3585b7114d983fbdb34bc6a97ce14d78511 Mon Sep 17 00:00:00 2001 From: William Zhao Date: Wed, 12 Aug 2026 15:11:08 -0400 Subject: [PATCH 2/2] ci: harden perf-report workflow checkout and token scope Always checkout trusted default-branch scripts instead of fork commits, and limit GITHUB_TOKEN to steps that call the GitHub API. Repo argument to find_prs_by_head_branch was added such that we can do local/off-repo debugging. Signed-off-by: William Zhao --- .github/workflows/performance-report.yml | 13 ++++++-- e2e/perf/get-pr-info.py | 42 +++++++++++++++++++----- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/.github/workflows/performance-report.yml b/.github/workflows/performance-report.yml index 99152c99a..1d9d31ea4 100644 --- a/.github/workflows/performance-report.yml +++ b/.github/workflows/performance-report.yml @@ -21,12 +21,11 @@ jobs: name: Process Performance Results runs-on: ubuntu-latest if: github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: + # Always use trusted default-branch code; never checkout fork commits for since we cannot trust it. persist-credentials: false - name: Set up Python @@ -40,6 +39,8 @@ jobs: pip install -r e2e/perf/requirements.txt - name: Download performance test data artifacts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | e2e/perf/download-artifacts.py \ --run-id ${{ github.event.workflow_run.id }} \ @@ -80,6 +81,8 @@ jobs: - name: Get associated PRs id: get_prs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PR_NUMBERS=$(e2e/perf/get-pr-info.py \ --event-path $GITHUB_EVENT_PATH \ @@ -90,6 +93,8 @@ jobs: - name: Fetch baseline run id: get_baseline if: steps.get_prs.outputs.pr_numbers != '[]' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Prefer a successful push run of e2e-kind as the baseline when no # scheduled performance lane exists yet. @@ -109,6 +114,8 @@ jobs: - name: Download baseline artifacts if: steps.get_baseline.outputs.baseline_run_id != '' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | e2e/perf/download-artifacts.py \ --run-id ${{ steps.get_baseline.outputs.baseline_run_id }} \ @@ -182,6 +189,8 @@ jobs: - name: Post performance report to PRs if: steps.get_prs.outputs.pr_numbers != '[]' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PR_NUMBERS=$(echo '${{ steps.get_prs.outputs.pr_numbers }}' | python3 -c "import sys, json; print(' '.join(f'--pr {pr}' for pr in json.load(sys.stdin)))") diff --git a/e2e/perf/get-pr-info.py b/e2e/perf/get-pr-info.py index a3e5c10ab..7725926ad 100755 --- a/e2e/perf/get-pr-info.py +++ b/e2e/perf/get-pr-info.py @@ -23,8 +23,27 @@ from github_common import get_github_token, get_repo_info -def find_prs_by_head_branch(head_branch: str, head_owner: str | None = None) -> List[int]: - """Find open PRs for a head branch, optionally scoped to a fork/owner.""" +def find_prs_by_head_branch( + head_branch: str, + head_owner: str | None = None, + repo: str | None = None, +) -> List[int]: + """Find open PR numbers whose head branch matches. + + Used by workflow_run handlers to map an e2e-kind run back to the PR that + triggered it. Pass values from the triggering run's metadata: + + head_branch workflow_run.head_branch (e.g. "gracefulterm") + head_owner workflow_run.head_repository.owner.login (e.g. "wizhaoredhat") + repo upstream "owner/name" (e.g. "k8snetworkplumbingwg/multus-cni") + + Search by branch name only, then filter by head_owner when given. Do not pass + "owner:branch" to gh: `gh pr list --head` documents that syntax as unsupported + and returns no results for fork PRs. + + Returns: + Matching PR numbers, or [] when none are found or gh fails. + """ if not head_branch: return [] @@ -37,13 +56,17 @@ def find_prs_by_head_branch(head_branch: str, head_owner: str | None = None) -> else: print(f"Searching for PRs by head branch: {head_branch}", file=sys.stderr) + cmd = [ + 'gh', 'pr', 'list', + '--head', head_branch, + '--json', 'number,headRepositoryOwner', + ] + if repo: + cmd.extend(['--repo', repo]) + try: result = subprocess.run( - [ - 'gh', 'pr', 'list', - '--head', head_branch, - '--json', 'number,headRepositoryOwner', - ], + cmd, capture_output=True, text=True, check=True, @@ -97,7 +120,8 @@ def get_prs_from_event_file(event_path: Path) -> List[int]: (workflow_run.get('head_repository') or {}).get('owner') or {} ).get('login') - return find_prs_by_head_branch(head_branch, head_owner) + owner, name = get_repo_info() + return find_prs_by_head_branch(head_branch, head_owner, f"{owner}/{name}") def get_prs_from_api(owner: str, repo: str, run_id: int, token: str) -> List[int]: @@ -130,7 +154,7 @@ def get_prs_from_api(owner: str, repo: str, run_id: int, token: str) -> List[int (data.get('head_repository') or {}).get('owner') or {} ).get('login') - return find_prs_by_head_branch(head_branch, head_owner) + return find_prs_by_head_branch(head_branch, head_owner, f"{owner}/{repo}") def main():