Skip to content

Commit 264636a

Browse files
svelderrainruizGitHub Copilot
andauthored
Support org-owned fork PR creation without upstream mirroring (#963) (#965)
* #963 Support org-owned fork PR creation without upstream mirroring * #963 Fix same-owner fork GraphQL metadata query * #963 Allow explicit issue overrides for fork-aware PR helper * #963 Hydrate explicit issue PR metadata from GitHub * #963 Fix leading-dash PR body parsing --------- Co-authored-by: GitHub Copilot <copilot@users.noreply.github.com>
1 parent ccd539f commit 264636a

22 files changed

Lines changed: 1200 additions & 71 deletions

.github/PR_COMMENT_SNIPPETS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pwsh -File tools/Branch-Orchestrator.ps1 -Issue <number> -Execute -PRTemplate wo
4747

4848
```text
4949
pwsh -File tools/New-PullRequestBody.ps1 -Template human-change -Issue <number> -OutputPath pr-body.md
50-
gh pr create --title "<title>" --body-file pr-body.md
50+
node tools/npm/run-script.mjs priority:pr -- --issue <number> --repo <owner/repo> --branch <branch> --base <base> --title "<title>" --body-file pr-body.md
5151
```
5252

5353
## Re-run Orchestrated With Same Inputs

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ line buffers).
170170
- CLI PR creation should prefer `pwsh -File tools/Branch-Orchestrator.ps1 -Issue <number> -Execute` and switch
171171
templates with `-PRTemplate workflow-policy|human-change` when the review surface differs from default maintenance
172172
work.
173+
- The lower-level PR helper is `node tools/npm/run-script.mjs priority:pr`; prefer it over raw `gh pr create`
174+
because it keeps user forks and same-owner org forks on one deterministic path, including the GraphQL
175+
`headRepositoryId` flow when `origin` and `upstream` share the same owner. Pass `--issue <number>` when the branch
176+
belongs to a non-standing issue and the helper should not infer intent from the standing-priority cache.
173177
- Automation-authored PRs must keep the `Agent Metadata` block in the body so reviewer routing remains deterministic.
174178
- Ensure CI is green (lint + Pester). Verify no lingering processes on self-hosted runners.
175179
- For `gh issue create` / `gh issue edit` with multiline Markdown bodies in mixed

docs/DEVELOPER_GUIDE.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -409,8 +409,10 @@ For Docker/Desktop VI history validation, run fast-loop lanes explicitly:
409409
locally. Add `--observe-only` to mirror staged rollout mode. See `docs/COMMIT_INTEGRITY_CHECK.md` for full contract
410410
and rollout flags (`COMMIT_INTEGRITY_ENFORCE`).
411411
- Prefer opening PRs from your fork with `node tools/npm/run-script.mjs priority:pr`; the helper ensures `origin`
412-
targets your fork (creating it via `gh repo fork` if needed), pushes the current branch, and calls
413-
`gh pr create --title <derived-title> --body <rendered-body> --repo <upstream> --base develop --head <fork>:branch`.
412+
targets your fork (creating it via `gh repo fork` if needed), pushes the current branch, and opens the PR through one
413+
fork-aware contract. User-owned forks still flow through `gh pr create`; same-owner renamed forks switch to GitHub
414+
GraphQL `createPullRequest` with `headRepositoryId` so agents do not need to mirror branches back to upstream just to
415+
create the PR.
414416
- The machine-readable GitHub intake source of truth lives in
415417
`tools/priority/github-intake-catalog.json`. Use
416418
`pwsh -File tools/Resolve-GitHubIntakeRoute.ps1 -ListScenarios` or
@@ -419,7 +421,7 @@ For Docker/Desktop VI history validation, run fast-loop lanes explicitly:
419421
- For the higher-level scenario facade, use
420422
`pwsh -File tools/New-GitHubIntakeDraft.ps1 -Scenario <name> -OutputPath <body-file>`
421423
to render the correct issue or PR body from the catalog before invoking
422-
`gh issue create` / `gh pr create`. For PR scenarios, the helper can hydrate
424+
`gh issue create` / `node tools/npm/run-script.mjs priority:pr`. For PR scenarios, the helper can hydrate
423425
issue title/URL and standing-priority state from the existing issue snapshot
424426
under `tests/results/_agent/issue/`.
425427
- For a machine-readable execution planner and explicit apply helper, use
@@ -431,8 +433,8 @@ For Docker/Desktop VI history validation, run fast-loop lanes explicitly:
431433
`tests/results/_agent/intake/`.
432434
- When you need the repository's richer intake metadata blocks and template variants, prefer
433435
`pwsh -File tools/Branch-Orchestrator.ps1 -Issue <number> -Execute [-PRTemplate <variant>]` or
434-
`pwsh -File tools/New-PullRequestBody.ps1 ... -OutputPath pr-body.md` plus `gh pr create --title <title> --body-file
435-
pr-body.md`.
436+
`pwsh -File tools/New-PullRequestBody.ps1 ... -OutputPath pr-body.md` plus
437+
`node tools/npm/run-script.mjs priority:pr -- --issue <number> --repo <owner/repo> --branch <branch> --base <base> --title <title> --body-file pr-body.md`.
436438
- Detailed enforcement notes (feature-branch guards, merge history workflow,
437439
merge queue parameters) live in
438440
[`docs/knowledgebase/FEATURE_BRANCH_POLICY.md`](./knowledgebase/FEATURE_BRANCH_POLICY.md).

docs/documentation-manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
"tools/New-GitHubIntakeDraft.ps1",
151151
"tools/Resolve-GitHubIntakeRoute.ps1",
152152
"tools/Write-GitHubIntakeAtlas.ps1",
153+
"tools/priority/create-pr.mjs",
153154
"tools/priority/github-intake-catalog.json"
154155
]
155156
},

docs/knowledgebase/GitHub-Intake-Layer.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ instead of inferring the correct path from prose alone.
4848

4949
For PR scenarios, the draft helper can auto-populate the issue title, issue URL, and standing-priority marker from
5050
the current issue snapshot under `tests/results/_agent/issue/`, so future agents do not need to restate that context
51-
after bootstrap has already synced it.
51+
after bootstrap has already synced it. When an explicit issue is outside the standing-priority cache, the helper also
52+
falls back to `gh issue view` so non-standing issue PRs still hydrate title/URL cleanly on a fresh clone.
5253

5354
- Default dry-run execution plan:
5455

@@ -131,7 +132,7 @@ instead of inferring the correct path from prose alone.
131132
-IssueTitle "Epic: modernize the GitHub intake layer for future agents" `
132133
-IssueUrl "https://github.com/LabVIEW-Community-CI-CD/compare-vi-cli-action/issues/875" `
133134
-Base develop -Branch issue/875-modernize-github-intake-layer -OutputPath pr-body.md
134-
gh pr create --title "<title>" --body-file pr-body.md
135+
node tools/npm/run-script.mjs priority:pr -- --issue <number> --repo <owner/repo> --branch <branch> --base <base> --title "<title>" --body-file pr-body.md
135136
```
136137

137138
- Branch + PR bootstrap:
@@ -141,8 +142,11 @@ instead of inferring the correct path from prose alone.
141142
```
142143

143144
The helper script derives the PR title from linked issue metadata when available, falls back to the current branch's
144-
head commit subject when necessary, and then calls `gh pr create --title ... --body-file ...` with the rendered intake
145-
document.
145+
head commit subject when necessary, and then calls `priority:pr` with the rendered intake document. For user-owned
146+
forks, the helper still routes through `gh pr create`. For same-owner renamed forks, it switches to GitHub GraphQL
147+
`createPullRequest` with `headRepositoryId` so future agents do not need an upstream-mirror workaround just to open
148+
the PR. When the branch is tied to a non-standing issue, pass `--issue <number>` so the helper does not need to infer
149+
intent from the standing-priority cache.
146150

147151
## Idle Repository Mode
148152

@@ -183,8 +187,9 @@ Human-authored PRs should use the `human-change` template so they do not acciden
183187
reviewer-routing semantics.
184188
- Use the `human-change` PR template when the PR is not automation-authored and should not carry the agent metadata
185189
contract.
186-
- Prefer explicit `--title` plus `--body-file` over `gh pr create --fill`; the title/body contract stays deterministic
187-
and avoids GitHub CLI flag conflicts.
190+
- Prefer `priority:pr` with explicit `--title` plus `--body-file` over raw `gh pr create --fill`; the title/body
191+
contract stays deterministic, avoids GitHub CLI flag conflicts, and keeps same-owner fork PR creation on one helper
192+
path.
188193
- Use the wiki as a public portal for discoverability, not as a substitute for checked-in docs.
189194
- Prefer the checked-in intake catalog plus `Resolve-GitHubIntakeRoute.ps1` when deciding which supported issue or PR
190195
surface to use.

docs/schemas/github-intake-catalog-v1.schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"type": "string",
1717
"enum": [
1818
"gh-issue-create",
19+
"priority-pr-create",
1920
"gh-pr-create",
2021
"branch-orchestrator",
2122
"open-link"

docs/schemas/github-intake-execution-plan-v1.schema.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"type": "string",
9191
"enum": [
9292
"gh-issue-create",
93+
"priority-pr-create",
9394
"gh-pr-create",
9495
"branch-orchestrator",
9596
"open-link"
@@ -234,6 +235,7 @@
234235
"type": "string",
235236
"enum": [
236237
"gh-issue-create",
238+
"priority-pr-create",
237239
"gh-pr-create",
238240
"branch-orchestrator",
239241
"open-link"

tests/GitHubIntake.Tests.ps1

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,65 @@ Describe 'GitHubIntake.psm1' {
6868
$route.targetKey | Should -Be 'human-change'
6969
$route.targetPath | Should -Be '.github/PULL_REQUEST_TEMPLATE/human-change.md'
7070
$route.helperPath | Should -Be 'tools/New-GitHubIntakeDraft.ps1'
71-
$route.executeCommand | Should -Be 'gh pr create --title "<title>" --body-file pr-body.md'
72-
$route.executionKind | Should -Be 'gh-pr-create'
71+
$route.executeCommand | Should -Be 'node tools/npm/run-script.mjs priority:pr -- --issue <number> --repo <owner/repo> --branch <branch> --base <base> --title "<title>" --body-file pr-body.md'
72+
$route.executionKind | Should -Be 'priority-pr-create'
7373
$route.execution.branchSource | Should -Be 'current-or-input'
7474
}
7575

76+
It 'normalizes legacy gh-pr-create execution kinds to the priority helper contract' {
77+
$catalogPath = Join-Path $TestDrive 'github-intake-catalog.json'
78+
@'
79+
{
80+
"schema": "github-intake/catalog@v1",
81+
"issueTemplates": [],
82+
"pullRequestTemplates": [
83+
{
84+
"key": "human-change",
85+
"path": ".github/PULL_REQUEST_TEMPLATE/human-change.md",
86+
"templateLabel": "human-change",
87+
"metadataMode": "human",
88+
"summary": "Human-authored PR template."
89+
}
90+
],
91+
"contactLinks": [],
92+
"routes": [
93+
{
94+
"scenario": "legacy-human-pr",
95+
"routeType": "pull-request-template",
96+
"targetKey": "human-change",
97+
"helperPath": "tools/New-GitHubIntakeDraft.ps1",
98+
"command": "pwsh -File tools/New-GitHubIntakeDraft.ps1 -Scenario legacy-human-pr -OutputPath pr-body.md",
99+
"executeCommand": "gh pr create --title \"<title>\" --body-file pr-body.md",
100+
"execution": {
101+
"kind": "gh-pr-create",
102+
"titleSource": "issue-derived",
103+
"bodySource": "draft-output",
104+
"baseSource": "input-or-default",
105+
"branchSource": "current-or-input",
106+
"issueSource": "input-or-snapshot"
107+
},
108+
"summary": "Legacy route."
109+
}
110+
]
111+
}
112+
'@ | Set-Content -LiteralPath $catalogPath -Encoding utf8
113+
114+
$previous = $env:COMPAREVI_GITHUB_INTAKE_CATALOG_PATH
115+
try {
116+
$env:COMPAREVI_GITHUB_INTAKE_CATALOG_PATH = $catalogPath
117+
$route = Resolve-GitHubIntakeRoute -Scenario 'legacy-human-pr'
118+
} finally {
119+
if ($null -eq $previous) {
120+
Remove-Item Env:COMPAREVI_GITHUB_INTAKE_CATALOG_PATH -ErrorAction SilentlyContinue
121+
} else {
122+
$env:COMPAREVI_GITHUB_INTAKE_CATALOG_PATH = $previous
123+
}
124+
}
125+
126+
$route.executionKind | Should -Be 'priority-pr-create'
127+
$route.execution.kind | Should -Be 'priority-pr-create'
128+
}
129+
76130
It 'resolves an issue snapshot from the override directory when present' {
77131
$snapshotDir = Join-Path $TestDrive 'issue'
78132
New-Item -ItemType Directory -Path $snapshotDir -Force | Out-Null
@@ -95,6 +149,35 @@ Describe 'GitHubIntake.psm1' {
95149
$snapshot.title | Should -Be 'Catalog issue'
96150
}
97151

152+
It 'falls back to GitHub issue lookup when no local snapshot exists for an explicit issue' {
153+
Mock -ModuleName GitHubIntake Resolve-GitHubIssueSnapshotFromGitHub {
154+
param([int]$Issue)
155+
[pscustomobject]@{
156+
number = $Issue
157+
title = 'Live catalog issue'
158+
url = 'https://example.test/issues/963'
159+
labels = @('enhancement')
160+
}
161+
}
162+
163+
$snapshotDir = Join-Path $TestDrive 'missing-issue-dir'
164+
$previous = $env:COMPAREVI_GITHUB_INTAKE_SNAPSHOT_DIR
165+
try {
166+
$env:COMPAREVI_GITHUB_INTAKE_SNAPSHOT_DIR = $snapshotDir
167+
$snapshot = Resolve-GitHubIssueSnapshot -Issue 963
168+
} finally {
169+
if ($null -eq $previous) {
170+
Remove-Item Env:COMPAREVI_GITHUB_INTAKE_SNAPSHOT_DIR -ErrorAction SilentlyContinue
171+
} else {
172+
$env:COMPAREVI_GITHUB_INTAKE_SNAPSHOT_DIR = $previous
173+
}
174+
}
175+
176+
$snapshot.number | Should -Be 963
177+
$snapshot.title | Should -Be 'Live catalog issue'
178+
Should -Invoke Resolve-GitHubIssueSnapshotFromGitHub -ModuleName GitHubIntake -Times 1 -Exactly
179+
}
180+
98181
It 'loads the intake catalog from the override path when present' {
99182
$catalogPath = Join-Path $TestDrive 'github-intake-catalog.json'
100183
@'
@@ -184,6 +267,24 @@ Describe 'GitHubIntake.psm1' {
184267
$context.snapshotResolved | Should -BeTrue
185268
}
186269

270+
It 'treats label objects from live snapshots as standing-priority markers' {
271+
Mock -ModuleName GitHubIntake Resolve-GitHubIssueSnapshot {
272+
[pscustomobject]@{
273+
number = 963
274+
title = 'Live GH issue'
275+
url = 'https://example.test/issues/963'
276+
labels = @([pscustomobject]@{ name = 'standing-priority' })
277+
}
278+
}
279+
280+
$context = Resolve-GitHubIntakeDraftContext -Scenario 'human-pr' -Issue 963 -CurrentBranch 'issue/963-org-owned-fork-pr-helper'
281+
282+
$context.issueTitle | Should -Be 'Live GH issue'
283+
$context.issueUrl | Should -Be 'https://example.test/issues/963'
284+
$context.standingPriority | Should -BeTrue
285+
$context.snapshotResolved | Should -BeTrue
286+
}
287+
187288
It 'preserves a null execute command when the catalog route omits it' {
188289
$catalogPath = Join-Path $TestDrive 'github-intake-catalog.json'
189290
@'

tests/Invoke-GitHubIntakeExecutionPlan.Tests.ps1

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,45 @@ Describe 'Invoke-GitHubIntakeExecutionPlan' {
7373
$state.Invocation.PRTemplate | Should -Be 'workflow-policy'
7474
}
7575

76+
It 'dispatches priority-pr-create plans through node after rendering the draft' {
77+
$plan = New-GitHubIntakeExecutionPlan `
78+
-Scenario human-pr `
79+
-Issue 963 `
80+
-IssueTitle 'Support org-owned fork PR creation without upstream mirroring' `
81+
-Branch 'issue/963-org-owned-fork-pr-helper' `
82+
-DraftOutputPath (Join-Path $TestDrive 'pr-body.md')
83+
84+
$calls = [System.Collections.Generic.List[object]]::new()
85+
$result = Invoke-GitHubIntakeExecutionPlan `
86+
-Plan $plan `
87+
-DraftRenderer {
88+
param([hashtable]$DraftParameters)
89+
$calls.Add([pscustomobject]@{ kind = 'draft'; outputPath = $DraftParameters.OutputPath; scenario = $DraftParameters.Scenario }) | Out-Null
90+
Set-Content -LiteralPath $DraftParameters.OutputPath -Value '# pr draft' -NoNewline
91+
return $DraftParameters.OutputPath
92+
} `
93+
-NativeInvoker {
94+
param([string]$FilePath, [string[]]$Arguments)
95+
$calls.Add([pscustomobject]@{ kind = 'native'; filePath = $FilePath; arguments = @($Arguments) }) | Out-Null
96+
return 'https://example.test/pull/963'
97+
}
98+
99+
$result.executionKind | Should -Be 'priority-pr-create'
100+
$result.draftWritten | Should -BeTrue
101+
$result.commandFilePath | Should -Be 'node'
102+
$calls.Count | Should -Be 2
103+
$calls[0].kind | Should -Be 'draft'
104+
$calls[1].kind | Should -Be 'native'
105+
$calls[1].filePath | Should -Be 'node'
106+
$calls[1].arguments | Should -Contain 'tools/npm/run-script.mjs'
107+
$calls[1].arguments | Should -Contain 'priority:pr'
108+
$calls[1].arguments | Should -Contain '--issue'
109+
$calls[1].arguments | Should -Contain '963'
110+
$calls[1].arguments | Should -Contain '--branch'
111+
$calls[1].arguments | Should -Contain 'issue/963-org-owned-fork-pr-helper'
112+
$calls[1].arguments | Should -Contain '--body-file'
113+
}
114+
76115
It 'refuses to apply a plan with missing required inputs' {
77116
$plan = New-GitHubIntakeExecutionPlan -Scenario bug
78117

tests/Invoke-GitHubIntakeScenario.Tests.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Describe 'Invoke-GitHubIntakeScenario.ps1' {
4545
}
4646

4747
$plan = $json | ConvertFrom-Json -Depth 10
48-
$plan.execution.kind | Should -Be 'gh-pr-create'
48+
$plan.execution.kind | Should -Be 'priority-pr-create'
4949
$plan.execution.branch | Should -Be 'issue/923-work'
5050
$plan.execution.title | Should -Be 'Execution planner issue (#923)'
5151
$plan.requirements.canApply | Should -BeTrue

0 commit comments

Comments
 (0)