Skip to content

CNTRLPLANE-4013: add GitHub Actions workflow rehearsal script - #9253

Draft
celebdor wants to merge 1 commit into
openshift:mainfrom
celebdor:gha-rehearse-script
Draft

CNTRLPLANE-4013: add GitHub Actions workflow rehearsal script#9253
celebdor wants to merge 1 commit into
openshift:mainfrom
celebdor:gha-rehearse-script

Conversation

@celebdor

@celebdor celebdor commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

What this PR does / why we need it:

Adds contrib/ci/gha-rehearse.sh to enable rehearsing new GitHub Actions workflows from PR branches before merging.

GitHub Actions workflow_dispatch requires the workflow file to exist on the default branch for it to appear in the Actions UI. This script pushes minimal stubs to the default branch so an admin can select the PR branch and run the real workflow code, then cleans up the stub commit afterwards.

  • setup <PR>: identifies new workflow files in a PR, pushes workflow_dispatch stubs to the default branch, prints gh workflow run commands
  • cleanup: safely reverts the stub commit (refuses if HEAD is not a stub commit)

Which issue(s) this PR fixes:

Fixes CNTRLPLANE-4013

Special notes for your reviewer:

Requires admin push access to the default branch. The script uses low-level git plumbing (read-tree, hash-object, write-tree, commit-tree) to build the stub commit without touching the working directory.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

Summary by CodeRabbit

  • New Features
    • Added a utility for rehearsing new or renamed GitHub Actions workflows before merging.
    • Supports setup and cleanup operations with validation, workflow-dispatch preparation, and clear instructions for testing workflows against a pull request branch.
    • Reports errors for invalid commands or unexpected rehearsal state.

Adds contrib/ci/gha-rehearse.sh to enable rehearsing new GitHub Actions
workflows from PR branches before merging.

GitHub Actions workflow_dispatch requires the workflow file to exist on
the default branch to appear in the UI. This script pushes minimal stubs
to the default branch so an admin can select the PR branch and run the
real workflow code, then cleans up the stub commit afterwards.

Ref: CNTRLPLANE-4013

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Aug 7, 2026
@openshift-ci-robot

openshift-ci-robot commented Aug 7, 2026

Copy link
Copy Markdown

@celebdor: This pull request references CNTRLPLANE-4013 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "5.0.0" version, but no target version was set.

Details

In response to this:

What this PR does / why we need it:

Adds contrib/ci/gha-rehearse.sh to enable rehearsing new GitHub Actions workflows from PR branches before merging.

GitHub Actions workflow_dispatch requires the workflow file to exist on the default branch for it to appear in the Actions UI. This script pushes minimal stubs to the default branch so an admin can select the PR branch and run the real workflow code, then cleans up the stub commit afterwards.

  • setup <PR>: identifies new workflow files in a PR, pushes workflow_dispatch stubs to the default branch, prints gh workflow run commands
  • cleanup: safely reverts the stub commit (refuses if HEAD is not a stub commit)

Which issue(s) this PR fixes:

Fixes CNTRLPLANE-4013

Special notes for your reviewer:

Requires admin push access to the default branch. The script uses low-level git plumbing (read-tree, hash-object, write-tree, commit-tree) to build the stub commit without touching the working directory.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@celebdor
celebdor marked this pull request as draft August 7, 2026 11:48
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds gha-rehearse.sh for rehearsing new or renamed GitHub Actions workflows. The setup command discovers changed workflows, creates dispatch stubs, records state, pushes the default branch, and prints execution commands. The cleanup command validates the rehearsal state and resets the default branch. The utility also handles invalid usage and unexpected state.

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant gha-rehearse.sh
  participant GitRepository
  participant GitHubActions
  Operator->>gha-rehearse.sh: Run setup
  gha-rehearse.sh->>GitRepository: Find changed workflows
  gha-rehearse.sh->>GitRepository: Commit and push stubs
  Operator->>GitHubActions: Dispatch workflows on the PR branch
  Operator->>gha-rehearse.sh: Run cleanup
  gha-rehearse.sh->>GitRepository: Validate rehearsal state
  gha-rehearse.sh->>GitRepository: Reset the default branch
Loading

Possibly related PRs


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
No-Weak-Crypto ❌ Error The script calls git hash-object at lines 96 and 100; this repository uses SHA-1 object format, so the new code relies on SHA-1-backed hashing. Avoid SHA-1 for security-sensitive integrity decisions. Use SHA-256 or document and formally exempt these Git object IDs as non-security content addresses.
✅ Passed checks (10 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the addition of the GitHub Actions workflow rehearsal script.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed The PR adds only contrib/ci/gha-rehearse.sh; it adds no Ginkgo tests or It/Describe/Context/When titles to assess.
Test Structure And Quality ✅ Passed The PR changes only contrib/ci/gha-rehearse.sh, a Bash utility; it adds or modifies no Ginkgo tests, so these test-quality requirements do not apply.
Topology-Aware Scheduling Compatibility ✅ Passed The PR adds only a GitHub Actions Bash script; it adds or modifies no deployment manifests, operator code, controllers, or scheduling constraints.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed The PR adds only a Bash GitHub Actions rehearsal utility; no new Ginkgo e2e tests or test networking, IPv4, or external-connectivity assumptions are present.
Container-Privileges ✅ Passed The PR adds only a Bash script; its generated workflow stub has no privileged, host namespace, SYS_ADMIN, root, or allowPrivilegeEscalation settings.
No-Sensitive-Data-In-Logs ✅ Passed The script logs workflow paths, branch/repository metadata, and commit subjects only; it does not echo passwords, tokens, API keys, PII, session IDs, hostnames, or customer data.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@openshift-ci
openshift-ci Bot requested review from Nirshal and jparrill August 7, 2026 11:49
@openshift-ci

openshift-ci Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: celebdor
Once this PR has been reviewed and has the lgtm label, please assign muraee for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
contrib/ci/gha-rehearse.sh (2)

116-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the workflow names instead of re-parsing the generated stubs.

Lines 79-80 already extract each workflow name. Line 118 parses the same value back out of the file that Line 82 just wrote. The two parsers must stay identical, and they already share the same portability defect.

Store the names in a parallel array during the generation loop.

♻️ Proposed refactor
+    local -a stub_names=()
     for wf in "${new_workflows[@]}"; do
         local name
         name="$(git show "FETCH_HEAD:${wf}" 2>/dev/null \
             | grep -m1 '^name:' | sed 's/^name:[[:space:]]*//' || echo "${wf##*/}")"
+        stub_names+=("$name")
         mkdir -p "${tmpdir}/$(dirname "$wf")"
         generate_stub "$name" > "${tmpdir}/${wf}"
     done
     echo "To rehearse, run:"
-    for wf in "${new_workflows[@]}"; do
-        local stub_name
-        stub_name="$(grep -m1 '^name:' "${tmpdir}/${wf}" | sed 's/^name:\s*//')"
-        echo "  gh workflow run '${stub_name}' --repo ${REPO} --ref ${head_branch}"
+    for stub_name in "${stub_names[@]}"; do
+        echo "  gh workflow run '${stub_name}' --repo ${REPO} --ref ${head_branch}"
     done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/ci/gha-rehearse.sh` around lines 116 - 120, Update the workflow
generation loop to store each extracted workflow name in a parallel array
alongside new_workflows, then use the corresponding stored name when emitting
the gh workflow run command. Remove the grep/sed re-parsing in the final loop
and preserve the existing name extraction logic used during generation.

110-110: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a confirmation prompt before the script writes to the default branch.

This line pushes to the default branch of a shared repository without asking the user. A mistyped pull request number, or a run against the wrong repository, publishes stub workflows to main for every collaborator. Recovery requires the cleanup command and depends on the marker file surviving.

The push also requires permission to bypass branch protection. The header comment at Lines 9-16 lists gh CLI, git as the only requirements. Add the admin push requirement there.

Consider a prompt that the user can skip with an environment variable, so automated tests stay non-interactive.

♻️ Proposed change
+    if [[ "${GHA_REHEARSE_ASSUME_YES:-}" != "1" ]]; then
+        read -r -p "Push ${`#new_workflows`[@]} stub(s) to ${REPO}:${DEFAULT_BRANCH}? [y/N] " reply
+        [[ "$reply" == [yY] ]] || die "aborted by user"
+    fi
     git push origin "${stub_commit}:refs/heads/${DEFAULT_BRANCH}"

And in the header comment:

-# Requirements: gh CLI, git
+# Requirements: gh CLI, git, and permission to push directly to the default
+# branch (bypassing branch protection).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/ci/gha-rehearse.sh` at line 110, Update the push flow in
gha-rehearse.sh to require explicit user confirmation before pushing stub_commit
to DEFAULT_BRANCH, while allowing non-interactive runs to bypass the prompt
through an environment variable. Document the bypass variable and the required
permission to bypass branch protection in the script’s header requirements
alongside the existing gh CLI and git prerequisites.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contrib/ci/gha-rehearse.sh`:
- Line 80: In both name-extraction sites using sed in
contrib/ci/gha-rehearse.sh#L80-L80 and contrib/ci/gha-rehearse.sh#L118-L118,
replace the GNU-specific \s pattern with the POSIX [[:space:]] class so
whitespace is stripped correctly on BSD/macOS sed.
- Around line 55-60: Move the fetch of origin/${DEFAULT_BRANCH} in the rehearse
script to before the new_workflows existence-check loop, ensuring git cat-file
evaluates the current remote-tracking tree. Remove the later redundant fetch
while preserving the existing workflow generation and push flow.
- Around line 138-139: Update the cleanup push in the rehearsal script to use
--force-with-lease, pinning the lease to the default branch tip fetched at line
126, while still pushing parent to refs/heads/${DEFAULT_BRANCH}.
- Around line 72-73: Update the EXIT trap in cmd_setup to expand tmpdir when the
trap is defined rather than when it executes, while preserving safe cleanup of
the temporary directory under set -u.
- Around line 20-21: Move the die function above the repository metadata
initialization, then defer gh repo view calls from the top-level scope into the
command execution path so usage errors avoid network calls. Replace readonly
command substitutions with separate assignments for REPO and DEFAULT_BRANCH,
validate that both are non-empty, call die with a clear error when either lookup
fails, and declare the validated values readonly afterward.
- Around line 43-49: Update the workflow-file discovery in the rehearse script
to retrieve all pull-request files, replacing the non-paginated gh pr view files
query with gh api --paginate against the pull request files endpoint and
extracting each filename. Preserve the existing workflow YAML filtering and
workflow_files population, and continue deriving head_branch from the
pull-request metadata.

---

Nitpick comments:
In `@contrib/ci/gha-rehearse.sh`:
- Around line 116-120: Update the workflow generation loop to store each
extracted workflow name in a parallel array alongside new_workflows, then use
the corresponding stored name when emitting the gh workflow run command. Remove
the grep/sed re-parsing in the final loop and preserve the existing name
extraction logic used during generation.
- Line 110: Update the push flow in gha-rehearse.sh to require explicit user
confirmation before pushing stub_commit to DEFAULT_BRANCH, while allowing
non-interactive runs to bypass the prompt through an environment variable.
Document the bypass variable and the required permission to bypass branch
protection in the script’s header requirements alongside the existing gh CLI and
git prerequisites.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 976153a3-0c95-4b81-8880-9a19c61f2d12

📥 Commits

Reviewing files that changed from the base of the PR and between bb403c3 and 6f9371f.

📒 Files selected for processing (1)
  • contrib/ci/gha-rehearse.sh

Comment on lines +20 to +21
readonly REPO="${GHA_REHEARSE_REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}"
readonly DEFAULT_BRANCH="${GHA_REHEARSE_DEFAULT_BRANCH:-$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard against silent failures of gh repo view.

set -e does not abort on a failed command substitution inside a readonly declaration. The declaration builtin returns 0, so its own status masks the failure. If gh is unauthenticated, if the network fails, or if the current directory is not a GitHub repository, REPO and DEFAULT_BRANCH become empty strings.

An empty DEFAULT_BRANCH propagates into git push origin "${stub_commit}:refs/heads/" at Line 110 and into origin/ refs elsewhere. Separate the assignment from readonly and validate both values.

These two commands also run on every invocation, including the usage error path at Line 147. Deferring them into the command functions removes two network calls from the failure path.

🛡️ Proposed fix
-readonly REPO="${GHA_REHEARSE_REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}"
-readonly DEFAULT_BRANCH="${GHA_REHEARSE_DEFAULT_BRANCH:-$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)}"
+REPO="${GHA_REHEARSE_REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" \
+    || die "cannot determine repository; is 'gh' authenticated?"
+DEFAULT_BRANCH="${GHA_REHEARSE_DEFAULT_BRANCH:-$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)}" \
+    || die "cannot determine default branch; is 'gh' authenticated?"
+[[ -n "$REPO" ]] || die "repository name is empty"
+[[ -n "$DEFAULT_BRANCH" ]] || die "default branch name is empty"
+readonly REPO DEFAULT_BRANCH
 readonly MARKER_FILE=".github/.gha-rehearse-marker"

Note: die must be defined before this block for the fix to work. Move the die definition above the assignments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/ci/gha-rehearse.sh` around lines 20 - 21, Move the die function above
the repository metadata initialization, then defer gh repo view calls from the
top-level scope into the command execution path so usage errors avoid network
calls. Replace readonly command substitutions with separate assignments for REPO
and DEFAULT_BRANCH, validate that both are non-empty, call die with a clear
error when either lookup fails, and declare the validated values readonly
afterward.

Comment on lines +43 to +49
local -r pr_json="$(gh pr view "$pr_number" --repo "$REPO" --json headRefName,files)"
local -r head_branch="$(echo "$pr_json" | jq -r '.headRefName')"

local -a workflow_files=()
while IFS= read -r path; do
workflow_files+=("$path")
done < <(echo "$pr_json" | jq -r '.files[].path' | grep '^\.github/workflows/.*\.ya\?ml$')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

gh pr view --json files 100 file limit truncated

💡 Result:

The command gh pr view --json files historically suffers from silent truncation, limiting results to only the first 100 changed files without warning [1][2]. This occurs because the underlying GitHub CLI implementation previously lacked pagination support for the files field [1][3]. If you need to retrieve a complete list of files for a pull request that has more than 100 changed files, you should use the GitHub CLI's API interface, which supports pagination via the --paginate flag [4]. The recommended alternative approach is to use the REST API endpoint for PR files: gh api --paginate repos/{owner}/{repo}/pulls/{pull_number}/files --jq '.[].filename' Alternatively, you can use the GraphQL API if you require specific file data, ensuring you include pageInfo to handle the pagination: gh api --paginate graphql -f query=' query ($owner: String!, $repo: String!, $pr: Int!, $endCursor: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { files(first: 100, after: $endCursor) { nodes { path } pageInfo { hasNextPage, endCursor } } } } }' -F owner='{owner}' -F repo='{repo}' -F pr={pull_number} --jq '.data.repository.pullRequest.files.nodes[].path' Please note that efforts to implement automatic pagination directly within the gh pr view command have been documented and addressed in the GitHub CLI repository [3], so you may want to check if a recent version of the CLI has resolved this limitation in your specific environment [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- script context ---'
sed -n '1,100p' contrib/ci/gha-rehearse.sh
printf '%s\n' '--- gh CLI references and version constraints ---'
rg -n --hidden -S 'gh (pr view|api)|GitHub CLI|gh version' .github contrib README.md 2>/dev/null | head -200 || true

Repository: openshift/hypershift

Length of output: 3798


🌐 Web query:

site:cli.github.com/manual gh pr view --json files pagination changed files

💡 Result:

The gh pr view command does not support manual pagination for the files field when using the --json flag [1]. When you request the files field via gh pr view --json files, the GitHub CLI fetches file data associated with the pull request from the GitHub API [1][2]. Because gh interacts directly with the API, it is subject to the default limits and behaviors of the underlying GitHub GraphQL API, which typically truncates or limits the number of files returned in a single request (often to 100) [1][2]. If you need to retrieve a list of all changed files, particularly for pull requests with many changed files, consider these alternatives: 1. Use gh pr diff --name-only to list all file paths changed in the pull request. This command is specifically designed to output file names and handles larger sets of files more effectively than the files JSON field [3]. 2. If you must use JSON, verify the total number of changed files using the changedFiles field [1][4], and if the number exceeds the limit of the files field, you may need to use a script that interacts with the GitHub REST API (gh api) to paginate through the pull request files endpoint manually. In summary, the files field in gh pr view is not a paginated field that you can control via CLI flags [1][2]. For comprehensive lists of changed files, gh pr diff --name-only is the recommended approach [3].

Citations:


🌐 Web query:

site:github.com/cli/cli gh pr view files pagination 100 issue pull request

💡 Result:

The command gh pr view --json files has historically been subject to a silent truncation limit, returning a maximum of 100 files for pull requests that contain more [1][2][3]. This limitation occurs because the underlying GraphQL query previously lacked pagination support for the files field [1][4]. As of August 2026, work has been undertaken to address this issue by adding proper pagination to the files and commits fields in gh pr view [1][4]. Specifically, pull requests addressing this issue have implemented pageInfo handling to fetch all files regardless of the count, matching the pattern used for other paginated resources like reviews and comments [1][4]. If you are using a version of the GitHub CLI that does not yet include these fixes, or if you need to handle extremely large datasets in automated workflows, the recommended workaround is to use gh api graphql to construct a custom paginated query [5][6]. You can use the --paginate flag with gh api to automatically follow pageInfo and retrieve all results [5][6]: gh api graphql -f query=' query($owner: String!, $repo: String!, $pr: Int!, $endCursor: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { files(first: 100, after: $endCursor) { pageInfo { hasNextPage, endCursor } nodes { path } } } } }' -F owner='OWNER' -F repo='REPO' -F pr=123 --paginate --jq '.data.repository.pullRequest.files.nodes.[].path' This approach allows you to bypass the 100-item restriction by explicitly iterating through the file list [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- local script context ---'
sed -n '35,75p' contrib/ci/gha-rehearse.sh

printf '%s\n' '--- installed CLI, if present ---'
if command -v gh >/dev/null 2>&1; then
  gh version
else
  echo 'gh is not installed'
fi

printf '%s\n' '--- GitHub CLI issue and pull request metadata ---'
python3 - <<'PY'
import json, urllib.request
for path in (
    "repos/cli/cli/issues/13338",
    "repos/cli/cli/issues/9916",
    "repos/cli/cli/pulls/13340",
):
    req = urllib.request.Request(
        "https://api.github.com/" + path,
        headers={"Accept": "application/vnd.github+json", "User-Agent": "review-verifier"},
    )
    try:
        with urllib.request.urlopen(req, timeout=15) as response:
            data = json.load(response)
        print(json.dumps({
            "path": path,
            "state": data.get("state"),
            "title": data.get("title"),
            "merged": data.get("merged"),
            "merged_at": data.get("merged_at"),
            "closed_at": data.get("closed_at"),
            "html_url": data.get("html_url"),
        }, sort_keys=True))
    except Exception as exc:
        print(json.dumps({"path": path, "error": str(exc)}))
PY

Repository: openshift/hypershift

Length of output: 2268


🌐 Web query:

GitHub CLI current release gh pr view files field pagination implementation 2025 2026

💡 Result:

As of August 2026, the GitHub CLI (gh) has been updated to support pagination for the files and commits fields in the gh pr view --json command [1]. Historically, gh pr view --json files and gh pr view --json commits were limited to returning a maximum of 100 items, silently truncating any additional data without warning [2][3]. Because the underlying GraphQL fragments did not include pageInfo and lacked corresponding preloading functions in the CLI's codebase, users previously had to manually fetch complete data using the gh api graphql command with pagination parameters [4][3][5]. Following community feedback regarding this limitation [2][6], recent updates to the GitHub CLI have implemented native pagination for these fields [1]. This was achieved by: - Extending the pull request GraphQL fragments for files and commits to include pageInfo (hasNextPage and endCursor) [1]. - Adding PageInfo to the internal response structs [1]. - Implementing preloading functions (e.g., preloadPrFiles, preloadPrCommits) that automatically fetch subsequent pages beyond the initial 100-node limit, aligning the behavior with other paginated subresources like reviews and comments [1][2]. This change ensures that gh pr view --json returns the complete list of files and commits regardless of the total count [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
import urllib.request
urls = [
    "https://raw.githubusercontent.com/cli/cli/trunk/pkg/cmd/pr/view/view.go",
    "https://raw.githubusercontent.com/cli/cli/master/pkg/cmd/pr/view/view.go",
]
for url in urls:
    try:
        text = urllib.request.urlopen(
            urllib.request.Request(url, headers={"User-Agent": "review-verifier"}),
            timeout=15,
        ).read().decode()
        print(f"--- {url} ---")
        for i, line in enumerate(text.splitlines(), 1):
            if "files" in line.lower() or "pageInfo" in line or "after" in line:
                print(f"{i}:{line}")
        break
    except Exception as exc:
        print(f"{url}: {exc}")
PY

Repository: openshift/hypershift

Length of output: 575


Require pagination support for the gh CLI.

Older gh versions return only the first 100 entries for gh pr view --json files. Because this script does not specify a minimum version, users can miss workflow files. Use gh api --paginate "repos/$REPO/pulls/$pr_number/files" --jq '.[].filename', or enforce a gh version that supports pagination.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/ci/gha-rehearse.sh` around lines 43 - 49, Update the workflow-file
discovery in the rehearse script to retrieve all pull-request files, replacing
the non-paginated gh pr view files query with gh api --paginate against the pull
request files endpoint and extracting each filename. Preserve the existing
workflow YAML filtering and workflow_files population, and continue deriving
head_branch from the pull-request metadata.

Comment on lines +55 to +60
local -a new_workflows=()
for wf in "${workflow_files[@]}"; do
if ! git cat-file -e "origin/${DEFAULT_BRANCH}:${wf}" 2>/dev/null; then
new_workflows+=("$wf")
fi
done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Fetch the default branch before the existence check; the stale check can clobber a real workflow.

Line 57 tests origin/${DEFAULT_BRANCH}:${wf} against the local remote-tracking ref. The script fetches that ref only later, at Line 89. If the local origin/${DEFAULT_BRANCH} is stale, the check reads an old tree.

The failure mode is destructive. Assume a workflow file was merged upstream after the last local fetch. The stale check classifies it as new. The script then generates a stub for it. Line 92 reads the index from the freshly fetched tip, which does contain the real file. Line 97 overwrites that entry with the stub blob. Line 110 pushes as a fast-forward, so the push succeeds and the real workflow content is replaced on the default branch.

Move the fetch above the existence loop.

🐛 Proposed fix
+    git fetch origin "${DEFAULT_BRANCH}" --quiet
+
     local -a new_workflows=()
     for wf in "${workflow_files[@]}"; do
         if ! git cat-file -e "origin/${DEFAULT_BRANCH}:${wf}" 2>/dev/null; then
             new_workflows+=("$wf")
         fi
     done

Then remove the now-redundant fetch at Line 89:

     echo "Pushing stubs to ${DEFAULT_BRANCH}..."
-    git fetch origin "${DEFAULT_BRANCH}" --quiet
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/ci/gha-rehearse.sh` around lines 55 - 60, Move the fetch of
origin/${DEFAULT_BRANCH} in the rehearse script to before the new_workflows
existence-check loop, ensuring git cat-file evaluates the current
remote-tracking tree. Remove the later redundant fetch while preserving the
existing workflow generation and push flow.

Comment on lines +72 to +73
local -r tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

The EXIT trap cannot see tmpdir, so the temporary directory leaks.

trap receives a single-quoted string. Bash expands $tmpdir when the trap fires, not when it is set. tmpdir is a local variable of cmd_setup. When cmd_setup returns normally and the script reaches the end at Line 148, tmpdir is out of scope. Under set -u the trap then fails with an unbound variable error, and the directory stays on disk.

Expand the path at trap-definition time.

🛡️ Proposed fix
     local -r tmpdir="$(mktemp -d)"
-    trap 'rm -rf "$tmpdir"' EXIT
+    # shellcheck disable=SC2064  # expand tmpdir now, not when the trap fires
+    trap "rm -rf -- '${tmpdir}'" EXIT
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local -r tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
local -r tmpdir="$(mktemp -d)"
# shellcheck disable=SC2064 # expand tmpdir now, not when the trap fires
trap "rm -rf -- '${tmpdir}'" EXIT
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/ci/gha-rehearse.sh` around lines 72 - 73, Update the EXIT trap in
cmd_setup to expand tmpdir when the trap is defined rather than when it
executes, while preserving safe cleanup of the temporary directory under set -u.

for wf in "${new_workflows[@]}"; do
local name
name="$(git show "FETCH_HEAD:${wf}" 2>/dev/null \
| grep -m1 '^name:' | sed 's/^name:\s*//' || echo "${wf##*/}")"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

\s in sed is a GNU extension and fails on BSD/macOS sed. Both name-extraction sites use sed 's/^name:\s*//'. GNU sed supports \s. BSD sed, which ships on macOS, does not. On macOS the pattern matches a literal s character instead of whitespace, so name: Unit Tests becomes Unit Tests with a leading space. The stub YAML and the printed gh workflow run command then carry the wrong name. The shebang is /usr/bin/env bash, so the script is not restricted to GNU userland. Replace \s with the POSIX class [[:space:]] at both sites.

  • contrib/ci/gha-rehearse.sh#L80-L80: change sed 's/^name:\s*//' to sed 's/^name:[[:space:]]*//'.
  • contrib/ci/gha-rehearse.sh#L118-L118: change sed 's/^name:\s*//' to sed 's/^name:[[:space:]]*//'.
📍 Affects 1 file
  • contrib/ci/gha-rehearse.sh#L80-L80 (this comment)
  • contrib/ci/gha-rehearse.sh#L118-L118
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/ci/gha-rehearse.sh` at line 80, In both name-extraction sites using
sed in contrib/ci/gha-rehearse.sh#L80-L80 and
contrib/ci/gha-rehearse.sh#L118-L118, replace the GNU-specific \s pattern with
the POSIX [[:space:]] class so whitespace is stripped correctly on BSD/macOS
sed.

Comment on lines +138 to +139
local -r parent="$(git rev-parse "origin/${DEFAULT_BRANCH}~1")"
git push origin "${parent}:refs/heads/${DEFAULT_BRANCH}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

The cleanup push is a rewind and will be rejected without --force-with-lease.

Line 139 pushes the parent commit to the default branch. That moves the branch backwards, so it is not a fast-forward. Git rejects a non-fast-forward update on a plain git push. The documented cleanup path therefore fails, and the stub commit stays on the default branch.

Use --force-with-lease pinned to the tip that Line 126 fetched. The lease keeps the operation safe if another commit lands between the fetch and the push.

🐛 Proposed fix
     echo "Reverting stub commit on ${DEFAULT_BRANCH}..."
+    local -r stub_tip="$(git rev-parse "origin/${DEFAULT_BRANCH}")"
     local -r parent="$(git rev-parse "origin/${DEFAULT_BRANCH}~1")"
-    git push origin "${parent}:refs/heads/${DEFAULT_BRANCH}"
+    git push --force-with-lease="refs/heads/${DEFAULT_BRANCH}:${stub_tip}" \
+        origin "${parent}:refs/heads/${DEFAULT_BRANCH}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local -r parent="$(git rev-parse "origin/${DEFAULT_BRANCH}~1")"
git push origin "${parent}:refs/heads/${DEFAULT_BRANCH}"
local -r stub_tip="$(git rev-parse "origin/${DEFAULT_BRANCH}")"
local -r parent="$(git rev-parse "origin/${DEFAULT_BRANCH}~1")"
git push --force-with-lease="refs/heads/${DEFAULT_BRANCH}:${stub_tip}" \
origin "${parent}:refs/heads/${DEFAULT_BRANCH}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/ci/gha-rehearse.sh` around lines 138 - 139, Update the cleanup push
in the rehearsal script to use --force-with-lease, pinning the lease to the
default branch tip fetched at line 126, while still pushing parent to
refs/heads/${DEFAULT_BRANCH}.

@celebdor celebdor added the area/ci-tooling Indicates the PR includes changes for CI or tooling label Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci-tooling Indicates the PR includes changes for CI or tooling do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants