-
Notifications
You must be signed in to change notification settings - Fork 293
[GitHub Actions] Reflect checks from fanned out PRs #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
f6afa35
[GitHub Actions] Authenticate with GitHub App and Organization Detection
jayhawk-commits 58c4585
Replace all PAT instances with GitHub App-created token
jayhawk-commits ca0ef16
Update pr-fanout.yml
jayhawk-commits 005326c
Minor mixups when comparing diffs between forks
jayhawk-commits c9bc576
Missed one instance of PAT to replace
jayhawk-commits 8de337b
DELETE requests will not have json content in response
jayhawk-commits d3f3a58
PR number detection in auto-label workflow
jayhawk-commits f830206
[GitHub Actions] Reflect checks from fanned out PRs
jayhawk-commits 7340507
Add trigger for commit status changes
jayhawk-commits 28c749a
Merge branch 'develop' into users/jayhawk-commits/reflect-checks
jayhawk-commits 9f43f09
Standardize schedule
jayhawk-commits File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| """ | ||
| PR Checks Reflection Script | ||
| ----------------------------- | ||
| This script polls the status of checks on fanned-out pull requests (in sub-repositories) | ||
| and reflects them as synthetic checks on the original monorepo pull request. | ||
| The branch name convention for fanned-out PRs is computed using the FanoutNaming class. | ||
|
|
||
| Steps: | ||
| 1. Fetch the status of checks for the monorepo PR. | ||
| 2. Load the subtree mapping from repos-config.json. | ||
| 3. For each sub-repo, check if there is an open PR with the expected branch name. | ||
| 4. For each check in the sub-repo PR, reflect it as a synthetic check on the monorepo PR. | ||
|
|
||
| Arguments: | ||
| --repo : Full repository name (e.g., org/repo) | ||
| --pr : Pull request number | ||
| --config : OPTIONAL, path to the repos-config.json file | ||
| --subrepo : OPTIONAL, only process this subrepo by its repo name (e.g., ROCm/hipBLASlt). | ||
| --dry-run : If set, will only log actions without making changes. | ||
| --debug : If set, enables detailed debug logging. | ||
|
|
||
| Example Usage: | ||
| To run in debug mode and perform a dry-run (no changes made): | ||
| python pr_reflect_checks.py --repo ROCm/rocm-libraries --pr 123 --debug --dry-run | ||
| To run in debug mode and perform a dry-run for a specific subrepo: | ||
| python pr_reflect_checks.py --repo ROCm/rocm-libraries --pr 123 --subrepo ROCm/hipBLASlt --debug --dry-run | ||
| """ | ||
|
|
||
| import argparse | ||
| import logging | ||
| from typing import List, Optional | ||
|
|
||
| from github_cli_client import GitHubCLIClient | ||
| from config_loader import load_repo_config | ||
| from utils_fanout_naming import FanoutNaming | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| def parse_arguments(argv: Optional[List[str]] = None) -> argparse.Namespace: | ||
| """Parse command-line arguments.""" | ||
| parser = argparse.ArgumentParser(description="Reflect fanned-out PR checks onto the monorepo PR.") | ||
| parser.add_argument("--repo", required=True, help="Full repository name (e.g., org/repo)") | ||
| parser.add_argument("--pr", required=True, type=int, help="Pull request number") | ||
| parser.add_argument("--config", required=False, default=".github/repos-config.json", help="Path to the repos-config.json file") | ||
| parser.add_argument("--subrepo", required=False, help="If set, only process this subrepo.") | ||
| parser.add_argument("--dry-run", action="store_true", help="If set, only logs actions without making changes.") | ||
| parser.add_argument("--debug", action="store_true", help="If set, enables detailed debug logging.") | ||
| return parser.parse_args(argv) | ||
|
|
||
| def reflect_checks_from_subrepos(client: GitHubCLIClient, config: list, monorepo_repo: str, | ||
| monorepo_pr_number: int, subrepo_filter: Optional[str] = None, | ||
| dry_run: bool = False) -> None: | ||
| """Reflect checks from subrepo PRs onto the corresponding monorepo PR.""" | ||
| monorepo_branch = client.get_branch_name_for_pr(monorepo_repo, monorepo_pr_number) | ||
| monorepo_pr_sha = client.get_head_sha_for_pr(monorepo_repo, monorepo_pr_number) | ||
| monorepo_checks = { | ||
| check["name"]: check | ||
| for check in client.get_check_runs_for_ref(monorepo_repo, monorepo_branch) | ||
| } | ||
| for entry in config: | ||
| if subrepo_filter and entry.url != subrepo_filter: | ||
| continue | ||
| reflect_checks_for_subrepo( | ||
| client, entry, monorepo_repo, monorepo_pr_sha, | ||
| monorepo_checks, monorepo_pr_number, dry_run | ||
| ) | ||
|
|
||
| def reflect_checks_for_subrepo(client: GitHubCLIClient, entry, monorepo_repo: str, monorepo_pr_sha: str, | ||
| monorepo_checks: dict, monorepo_pr_number: int, dry_run: bool) -> None: | ||
| """Reflect checks for a single subrepo entry onto the monorepo PR.""" | ||
| subrepo = entry.url | ||
| branch = FanoutNaming.compute_branch_name(monorepo_pr_number, entry.name) | ||
| pr = client.get_pr_by_head_branch(subrepo, branch) | ||
| if not pr: | ||
| logger.info(f"No open PR found in {subrepo} for branch {branch}") | ||
| return | ||
| checks = client.get_check_runs_for_ref(subrepo, branch) | ||
| for check in checks: | ||
| synthetic_name = f"{entry.name}: {check['name']}" | ||
| status = check["status"] | ||
| details_url = check.get("details_url", "") | ||
| conclusion = check.get("conclusion") | ||
| completed_at = check.get("completed_at") | ||
| title = check.get("output", {}).get("title", synthetic_name) | ||
| summary = check.get("output", {}).get("summary", "") | ||
| existing = monorepo_checks.get(synthetic_name) | ||
| needs_update = ( | ||
| not existing or | ||
| existing["status"] != status or | ||
| existing.get("conclusion") != conclusion or | ||
| existing.get("output", {}).get("summary") != summary | ||
| ) | ||
| if not needs_update: | ||
| logger.debug(f"Skipped unchanged check: {synthetic_name}") | ||
| continue | ||
| logger.info(f"Reflecting check: {synthetic_name}") | ||
| if not dry_run: | ||
| client.upsert_check_run( | ||
| monorepo_repo, synthetic_name, monorepo_pr_sha, | ||
| status, details_url, conclusion, completed_at, | ||
| title, summary | ||
| ) | ||
| else: | ||
| logger.info(f"Dry run: would reflect check: {check}") | ||
|
|
||
| def main(argv: Optional[List[str]] = None) -> None: | ||
| """Main function to execute the PR checks reflection logic.""" | ||
| args = parse_arguments(argv) | ||
| logging.basicConfig( | ||
| level=logging.DEBUG if args.debug else logging.INFO | ||
| ) | ||
| client = GitHubCLIClient() | ||
| config = load_repo_config(args.config) | ||
| reflect_checks_from_subrepos( | ||
| client, | ||
| config, | ||
| monorepo_repo = args.repo, | ||
| monorepo_pr_number = args.pr, | ||
| subrepo_filter = args.subrepo, | ||
| dry_run = args.dry_run | ||
| ) | ||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| # Reflect Fanout Checks GitHub Action | ||
| # ------------------------------------ | ||
| # This workflow periodically runs a Python script that reflects the check status of | ||
| # fanout pull requests (in sub-repositories) back onto the monorepo pull request. | ||
| # Using polling mechanism for migration, as getting push events from all the | ||
| # sub-repositories is not feasible. | ||
| # | ||
| # Key Features: | ||
| # - Runs every 15 minutes using a cron schedule | ||
| # - Also supports manual triggering via workflow_dispatch | ||
| # - Uses the GitHub CLI to list all open PRs on the monorepo | ||
| # - For each open PR, executes `pr-reflect-checks.py` to post synthetic checks | ||
| # - Skips closed or merged PRs automatically | ||
| # | ||
| # Prerequisites: | ||
| # - Your Python script must support CLI args: --repo and --pr | ||
| # - The `gh` CLI tool is preinstalled in GitHub-hosted runners | ||
| # - The script must handle cases where fanout branches or checks are missing | ||
| # | ||
| # Example Invocation: | ||
| # python scripts/reflect/pr-reflect-checks.py --repo ROCm/rocm-libraries --pr 123 | ||
|
|
||
|
|
||
| name: Reflect Subrepo Checks (Polling) | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: '*/15 * * * *' # every 15 minutes | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| reflect: | ||
| runs-on: ubuntu-24.04 | ||
| steps: | ||
| - name: Generate a token | ||
| id: generate-token | ||
| uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 | ||
| with: | ||
| app-id: ${{ secrets.APP_ID }} | ||
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
|
|
||
| - name: Checkout code | ||
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| with: | ||
| sparse-checkout: .github | ||
| token: ${{ steps.generate-token.outputs.token }} | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | ||
| with: | ||
| python-version: '3.12' | ||
|
|
||
| - name: Install python dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install pydantic requests | ||
|
|
||
| - name: Set up Git user | ||
| run: | | ||
| git config user.name "assistant-librarian[bot]" | ||
| git config user.email "assistant-librarian[bot]@users.noreply.github.com" | ||
|
|
||
| - name: Reflect Checks | ||
| env: | ||
| GH_TOKEN: ${{ steps.generate-token.outputs.token }} | ||
| run: | | ||
| prs=$(gh pr list --repo "${{ github.repository }}" --state open --json number --jq '.[].number') | ||
| if [ -z "$prs" ]; then | ||
| echo "No open pull requests found — skipping." | ||
| exit 0 | ||
| fi | ||
| # Iterate over each pull request and reflect the checks | ||
| for pr in $prs; do | ||
| echo "Processing PR #${pr}" | ||
| python .github/scripts/pr_reflect_checks.py --repo "${{ github.repository }}" --pr "${pr}" --debug | ||
| done |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # Reflect Fanout Checks via Repository Dispatch | ||
| # --------------------------------------------- | ||
| # This workflow listens for repository_dispatch events from sub-repositories, | ||
| # indicating that a check run was updated on a fanout PR. It reflects that | ||
| # check status back onto the corresponding monorepo pull request as a synthetic check. | ||
| # | ||
| # Triggered by: repository_dispatch from subrepos when a check run is updated | ||
| # | ||
| # Expected Payload: | ||
| # { | ||
| # "subrepo": "org/name", | ||
| # "subrepo_pr": 42, | ||
| # "monorepo_pr": 123 | ||
| # } | ||
|
|
||
| name: Reflect Subrepo Checks (Dispatch) | ||
|
|
||
| on: | ||
| repository_dispatch: | ||
| types: [reflect-checks-push] | ||
|
|
||
| jobs: | ||
| reflect-dispatched-checks: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Generate a token | ||
| id: generate-token | ||
| uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e | ||
| with: | ||
| app-id: ${{ secrets.APP_ID }} | ||
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
|
|
||
| - name: Checkout .github directory | ||
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| with: | ||
| sparse-checkout: .github | ||
| token: ${{ steps.generate-token.outputs.token }} | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | ||
| with: | ||
| python-version: '3.12' | ||
|
|
||
| - name: Install python dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install pydantic requests | ||
|
|
||
| - name: Set up Git user | ||
| run: | | ||
| git config user.name "assistant-librarian[bot]" | ||
| git config user.email "assistant-librarian[bot]@users.noreply.github.com" | ||
|
|
||
| - name: Reflect Checks from Subrepo | ||
| env: | ||
| GH_TOKEN: ${{ steps.generate-token.outputs.token }} | ||
| run: | | ||
| pr="${{ github.event.client_payload.monorepo_pr }}" | ||
| subrepo="${{ github.event.client_payload.subrepo }}" | ||
| echo "Dispatch received for monorepo PR #$pr from subrepo $subrepo" | ||
| python .github/scripts/pr_reflect_checks.py --repo "${{ github.repository }}" --pr "${pr}" --subrepo "${subrepo}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # Reflect Checks to Monorepo (Push-style) | ||
| # ---------------------------------------- | ||
| # This GitHub Actions workflow runs on check updates in subrepo PRs that are part of a | ||
| # fanout from a monorepo. When a check is updated (e.g., CI passes/fails), this workflow: | ||
| # | ||
| # - Parses the branch name to extract the monorepo PR number | ||
| # - Sends a `repository_dispatch` event to the monorepo to reflect the updated check | ||
| # - Requires a GitHub App token (`GH_APP_TOKEN`) with repo/workflow access to monorepo | ||
| # | ||
| # Triggered on: check_run events (completed) | ||
| # Target repo: monorepo, which listens for `repository_dispatch` events | ||
|
|
||
| name: Reflect Check Updates to Monorepo | ||
|
|
||
| on: | ||
| check_run: | ||
| status: | ||
|
|
||
| jobs: | ||
| dispatch: | ||
| # only run if the check_run event is for a pull request | ||
| if: github.event.check_run.pull_requests != [] | ||
| runs-on: ubuntu-24.04 | ||
| steps: | ||
| - name: Generate a token | ||
| id: generate-token | ||
| uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 | ||
| with: | ||
| app-id: ${{ secrets.APP_ID }} | ||
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
|
|
||
| - name: Extract monorepo PR number | ||
| id: extract | ||
| run: | | ||
| head_branch="${{ github.event.check_run.check_suite.head_branch }}" | ||
| # Extract monorepo PR number from branch (monorepo-pr/<num>/<subtree>) | ||
| if [[ "$head_branch" =~ ^monorepo-pr/([0-9]+)/.+$ ]]; then | ||
| MONOREPO_PR="${BASH_REMATCH[1]}" | ||
| else | ||
| echo "Branch '$head_branch' does not match expected pattern. Not a fanout branch." | ||
| exit 0 # Skip silently | ||
| fi | ||
| echo "monorepo_pr=$MONOREPO_PR" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Send repository_dispatch to monorepo | ||
| env: | ||
| GH_TOKEN: ${{ steps.generate-token.outputs.token }} | ||
| run: | | ||
| gh api repos/ROCm/rocm-libraries/dispatches \ | ||
| --method POST \ | ||
| --field event_type="reflect-checks-push" \ | ||
| --field client_payload='{"subrepo": "'"${{ github.repository }}"'", "monorepo_pr": ${{ steps.extract.outputs.monorepo_pr }}}' | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.