Side note: This is the procedure for tending to open PRs, not merged ones. All PRs mentioned in this article are in progress as of 2026-11-13. In fact, these were taken from GitHub's PR and check logs, documentation, original contribution rules for each project, and raw query data and measured records from contribution logs. Times are in Korea Standard Time.
Table of Contents
- One-line summary
- 1. Why check: Open PRs grow stale even when left alone
- 2. Querying: Five status values and UNKNOWN
- 3. The three conflicts had different causes
- 4. Rebasing without a clone: Git Data API and SSH signatures
- 5. What was not submitted: Different doors for different projects
- 6. How to run tests that cannot be executed on a Mac
- 7. What was learned: Tips for tending to open PRs
- Links
One-line summary
| Target | 45 open PRs in the upstream project. Submitted between mid and late October, none of which are drafts. 7 older PRs from my own repository and community repositories are counted separately. |
| Query | gh pr view --json mergeable,mergeStateStatus,reviewDecision once per PR |
| Distribution | BLOCKED 24 · UNSTABLE 9 · CLEAN 7 · BEHIND 3 · DIRTY 2. First query had DIRTY 3 and UNKNOWN 4. |
| Handling | Out of the three conflicts, one was resolved by rebasing onto the new base. The other two need to be rewritten and are in progress. |
1. Why check: Open PRs grow stale even when left alone
On the day you submit a PR, there are no conflicts and checks pass. However, as you wait for reviews over several weeks, the base branch keeps moving. Other people modify the same files, the documentation structure changes, and the environment of the runner executing checks changes. Our PR hasn't changed a single character, yet one day a conflict label appears or tests that used to pass start failing.
From a maintainer's perspective, a PR with conflicts is a PR that cannot be reviewed. When your turn for review comes up and there are conflicts, that turn simply gets passed by. Therefore, after open PRs grew to dozens, I decided not to just look when notifications arrive, but to query the whole set at once and categorize them by status.
This article is a record of that check performed in the early morning of 2026-11-13. Unlike parts 1–6, it does not follow a single merged PR, but rather covers the procedure of tending to multiple open PRs. Since these are PRs with no final outcome yet, none of them are described as accepted.
2. Querying: Five status values and UNKNOWN
First, I created a list of open PRs (gh search prs --author <계정> --state open), and ran the following command once for each PR.
gh pr view <번호> --repo <소유자>/<저장소> --json mergeable,mergeStateStatus,reviewDecision
mergeable only tells whether there is a conflict (MERGEABLE·CONFLICTING·UNKNOWN), mergeStateStatus breaks down "can the merge button be pressed right now?" into finer details. The meanings intended by GitHub's GraphQL documentation and the counts found this time are as follows.
| Value | Documentation meaning (summary) | Count | How we interpreted it |
|---|---|---|---|
BLOCKED |
Merge is blocked | 24 | Mostly waiting for review. Review decisions: Review required 17, No decision 5, Approved 1, Changes requested 1 |
UNSTABLE |
Mergeable, but some checks are not passing | 9 | Open check logs to verify first whether the cause is our change |
CLEAN |
Can be merged and checks pass | 7 | Nothing for us to do. 1 of them has even received approval |
BEHIND |
PR branch is behind the base | 3 | Not a conflict. Only the base branch is ahead |
DIRTY |
Merge commit cannot be cleanly created | 2 | Conflict. Trace the cause down to the PR level |
We learned two things from this.
The UNKNOWN of the first query is not a final result. In the first query, four items came out as UNKNOWN . GitHub's REST documentation states that if this value is empty, it means the background job calculating mergeability has just started, so you should request again after some time. Indeed, when I queried again with the same command about 4 minutes later, three came out as BLOCKED, and one came out as UNSTABLE . The longer a PR went unqueried, the more likely this value appeared on the first fetch, so batch checking scripts should be made to collect only UNKNOWN and query them once more.
CLEAN does not mean "will be merged soon". The seven items simply mean there are no technical blocks, and merges are decided by maintainers. Conversely, the 24 BLOCKED items were mostly in a state of waiting for their review turn rather than having things to fix. Among the 45 items, the only ones we need to take action on are
DIRTY and those among UNSTABLE where the cause is on our end. UNSTABLE The reasons for the nine items varied individually, so they are not categorized in this article.
There were also cases where check failures were not caused by our changes. Seen a day prior on 11-12,
containers/kubernetes-mcp-server#1518 had its browser test changed to a failure while approved by one reviewer. The same test failed only on the stale base, and the cause appeared to be a change in the check runner's image. We rebased it onto the new base while keeping the change contents intact. Afterward, a second approval was added, and in this query it was CLEAN . This PR is also in progress.
3. The three conflicts had different causes
There were three DIRTY items in the first query. Knowing just the fact that a conflict occurred does not tell us what needs to be done, so I found out which merged PR caused the conflict for all three. The causes differed, and thus the handling differed as well.
| Our PR | Conflicting merge | Size of that merge | Cause | Handling |
|---|---|---|---|---|
| ai-dynamo/dynamo#16465 4 docs, +15/−4 | #17229, 11-06 04:34 | 29 files, +97/−3068 | Neighboring lines of lines we modified were changed | Rebased onto new base. Resolved |
| NVIDIA/dcgm-exporter#760 1 doc, +16/−7 | #768, 11-07 03:45 | 961 files, +161632/−27808 | The document containing the modified paragraph was completely rewritten | Must be rewritten to match the new doc. In progress |
| charmbracelet/catwalk#676 3 files, +63/−0 | #611, 10-29 23:22 | 73 files, +39578/−18179 | Major version overhaul changed module paths and configuration format | Must be rewritten in the new format. In progress |
When neighboring lines are modified. The dynamo PR is a change to add missing deprecation notices to four documents for deprecated features. Meanwhile, upstream merged a PR stripping out related integration code and modified the immediate neighboring lines of two of the same documents. What I checked first before rebasing was "is our change still needed?". Opening the four files in the new base branch, the deprecation notices were still missing. So I re-applied the same changes to the files in the new base, and the change volume remained +15/−4. Querying again, the conflict was resolved and it returned to waiting for review (BLOCKED).
When documentation is completely rewritten. The dcgm-exporter PR modifies paragraphs on the procedure to extract debug dumps. However, a PR releasing a new version (4.9.0) modified 961 files and completely rewrote that document. This PR was merged just 30 minutes after creation, so there was no way to know in advance. In the new document, the paragraph we modified has disappeared, and the location to place dumps has also changed. This is not a conflict that can be resolved simply by rebasing. We first need to measure whether a single copy command left in the new document fails on the new image as well, but the new version hasn't been measured yet (unmeasured). After measuring, it is correct to either rewrite it as a single-line PR or close it if the issue has disappeared.
When the major version is overhauled. The catwalk PR is a change adding provider configurations. Meanwhile, v3 was merged, changing module paths and configuration file formats. Files in the old format cannot simply be rebased as-is; they must be rewritten in the new format and built with the new version's tools. This is also in progress.
BEHIND is not a conflict. For these three items, the base branch is simply ahead and merge commits can be created cleanly. They were not touched this time. Since there are no conflicts, I judged there is no reason to rush a rebase before reviews start. Some projects discourage force pushes after a review has started, so it is better to minimize unnecessary rebases.
Looking at these three cases, conflicts were divided not by "the degree to which our changes and others' changes overlapped," but by "how much the ground our changes stood on has changed." If it's just neighboring lines, rebasing is enough; if paragraphs or formats have disappeared, the change itself must be re-evaluated.
4. Rebasing without a clone: Git Data API and SSH signatures
Downloading dozens of repositories entirely just to rebase is burdensome. Downloading a multi-gigabyte repository for a PR that modifies just a few lines of documentation is also wasteful. Therefore, rebasing is done without downloading the repository, using GitHub's Git Data API. Locally, no commits are created and no branches are switched. The sequence is as follows.
- Align the fork's default branch with upstream. The new base commit must be in the fork so that commits can be created on top of it.
- Download only the files to be changed from the new base, re-apply the same modifications, and run tests that can be executed.
- Create blobs for each file (
POST git/blobs), and specify the new base tree asbase_treeto create a tree (POST git/trees). - ). Assemble the commit object's body directly and sign it with an SSH key.
- Attach the signature to create a commit (
POST git/commits), and verify that the response'sverification.verifiedis true. - Only when true, move the PR branch ref to the new commit. The target being moved is only the branch in my fork.
The target signed in step 4 is exactly the text git uses when storing a commit.
tree <새 트리 SHA>
parent <새 기준 커밋 SHA>
author <이름> <메일> <epoch 초> +0900
committer <이름> <메일> <epoch 초> +0900
<커밋 메시지>
ssh-keygen -Y sign -n git -f <개인 키 파일> <위 내용을 담은 파일>
Here are the hurdles encountered along the way.
- If even a single character differs, the signature will not match. The author timestamp passed to the API is in ISO format, while the timestamp inside the commit object consists of epoch seconds and a timezone. Both values must point to the exact same instant, and the newline at the end of the message must also match. Therefore, before moving the branch, you must always verify
verification.verified, and stop if it is false. - The documentation only mentions PGP signatures, but SSH signatures were also verified. The REST documentation
signatureis described as a PGP signature for the commit. We signed it with the SSH key registered in our account as a signing key, and the created commits were marked as "Verified". Since this behavior is not documented, we note it based only on actual measurement. - Check the branch head and update it. The tool accepts an expected value that "the branch head should be this SHA right now," and if it differs, it does nothing. This is because the maintainer might have added more commits to the branch in the meantime.
- Large repositories cannot read the entire tree at once. Calls that read a tree recursively may have their responses truncated if there are too many items, and
truncatedcomes back as true. After experiencing this with a repository having a large vendor directory, we made it read the non-recursive tree level by level along the path.
The most expensive lesson learned was about file modes. The initial tool we built fixed the mode of uploaded files to 100644 . When re-applying
containers/podman-compose#1585 on October 31, scripts that used to be executables were turned into regular files, and 30 integration tests failed with PermissionError . Since not a single character of the content differed, this was a change that could not be noticed by looking only at the body of the diff. The next day, we fixed the tool to follow the base tree's mode and recreated the commits with the same content. Since then, whenever we use the signing tool, we always verify the following. This PR is also still in progress.
gh pr diff <번호> --repo <소유자>/<저장소> | grep -n 'mode'
It is normal for nothing to be output. old mode 100755 and new mode 100644 appearing are unintended changes.
5. Things Not Submitted: Different Doors for Each Project
While reviewing, we also checked if any of our registered issues could be connected to PRs. The conclusion was "nothing to submit right now." This was not because we couldn't make patches, but because the project rules dictated a strict sequence.
- Projects that accept PRs only for issues with an approval label. The contribution rules for fake-gpu-operator state that every PR must be linked to a single open issue that a maintainer has labeled
approved, and checks enforce this. The two issues we registered do not have labels yet. - Projects that automatically close PRs if they lack a label. kubeflow/pipelines requires that PRs from non-organization members be linked to issues with the
readylabel. This applies to bug fixes and documentation changes as well, and if the condition is not met, a workflow leaves a comment and closes the PR. - Projects that require prior approval to proceed. The AI policy shared by the Moby and Docker open-source projects cites opening an issue first so that maintainers can verify the need and approve progress before submitting an AI-assisted PR as an example of "contribution padding." Twenty-seven days have passed since we registered our documentation issue, but with no labels or comments, we have not submitted a PR.
- Issues where another contributor has already opened a PR. Other people have already opened PRs for all five of the issues we registered. Submitting duplicate fixes would only increase the workload for the maintainers.
We did submit one thing. It is kubevirt/kubevirt#19889 , which fixes KubeVirt's virt-operator repeatedly printing logs saying "dropping old CA" while discarding no certificates. KubeVirt's AI contribution policy requires reviewing and understanding every line of AI-generated code before submission and encourages disclosing the use of tools. Therefore, we attached the Assisted-by trailer and submitted it as a
draft PR . After a human reads through the diff entirely, it will be changed to a review-requested state. This PR is also in progress, and once merged, we will write a separate post about it.
6. How to Measure Tests That Cannot Be Run on a Mac
The unit tests for the KubeVirt change above could not be run on a Mac because the package containing the tests depends on Linux-only packages. Instead of submitting them with the thought that "CI will run the tests," we measured them directly using the following method.
- Cross-compile only the test binaries for Linux on the Mac. Since it uses only the repository's vendor directory, nothing new is downloaded.
- Place those binaries into a temporary pod in the TT Lab cluster and run them. The pod uses an image already pulled to the node, drops all capabilities, runs as a non-root user, has no service account token, and is launched with a 1Gi memory limit. It is deleted upon completion.
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOTOOLCHAIN=local GOPROXY=off \
GOFLAGS="-mod=vendor -buildvcs=false" go test -c -o <시험 바이너리> <패키지 경로>
| What | Result |
|---|---|
| 2 newly written tests, pre-fix code | 0 passed, 2 failed |
| 2 newly written tests, fixed code | 2 passed |
| CA bundle-related tests | 27 out of 27 passed |
| Entire package (3 iterations with shuffled order) | 160 out of 160 passed, approx. 0.53 seconds |
Neighboring apply entire package |
219 out of 219 passed |
The environment is Linux kernel 7.0, x86_64, Go 1.26.6. Symptoms before the fix were also measured. Counting the leader operator's logs for 24 minutes in a TT Lab cluster running KubeVirt v1.9.0 yielded 711 lines of the same log. This matches the rate of one line per CA bundle set every 237 adjustments.
There was one trap here as well. The initially written tests passed even with the pre-fix code. The test environment's default logger filtered out info-level logs, causing the test asserting "logs should not be printed" to pass thanks to the log filtering. Only after modifying the test to increase log verbosity and revert it afterward did both tests fail on the pre-fix code. If we had skipped the principle of first confirming that new tests fail on the pre-fix code, we would have submitted tests that verify nothing.
7. Lessons Learned: Tips for Maintaining Open PRs
- Check open PRs periodically all at once. Notifications often fail to report conflicts. You can just run a single command for each PR.
UNKNOWNis checked again. The value from the first check means "not calculated yet," and the value appears after a few minutes.DIRTYis the only conflict.BEHINDmeans it is behind, andBLOCKEDis mostly waiting for review. The actions required depend on the status.- Handle conflicts after identifying the merge that caused them. If neighboring lines were changed, re-apply the changes; if paragraphs or formats disappeared, re-evaluate whether the change is still needed. Before re-applying, check whether the issue still persists on the new base.
- When creating commits via API, check if anything other than the content is lost. File modes were like that. Always verify signature validation and whether file modes changed.
- Read where the project's door is before submitting a PR. Issue labeling, progress approval, and AI tool attribution vary by project. PRs that skip rules are automatically closed or waste maintainer time.
- Always confirm whether new tests fail on the pre-fix code first. Tests that only verify passing do not tell you what they are protecting.
Titi's Note. I put several "Please buy this book" slips into the library. When I went back a long while later, some slips had to be rewritten because the shelf numbers had changed, and others required getting a librarian's stamp first. So now, I don't just put slips in and forget about them; I occasionally take them out all at once and read through them to see if they are still correct.
Links
All materials were accessed on 2026-11-13.
- GitHub Documentation: REST, getting a single PR and
mergeable, GraphQLMergeStateStatus, REST, creating commits andsignature, REST, trees andtruncated - Re-applied PR: ai-dynamo/dynamo#16465(Merge that caused the conflict #17229), containers/kubernetes-mcp-server#1518
- PR that needs rewriting: NVIDIA/dcgm-exporter#760(Merge that caused the conflict #768), charmbracelet/catwalk#676(Merge that caused the conflict #611)
- PR that lost file modes: containers/podman-compose#1585
- PR submitted as a draft: kubevirt/kubevirt#19889
- Contribution Rules: KubeVirt AI Contribution Policy, Moby & Docker AI Policy, fake-gpu-operator CONTRIBUTING, kubeflow/pipelines CONTRIBUTING