-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathshepherd-task.ps1
More file actions
193 lines (147 loc) · 6.08 KB
/
Copy pathshepherd-task.ps1
File metadata and controls
193 lines (147 loc) · 6.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
<#
.SYNOPSIS
Shepherds a child Task issue end-to-end: from Copilot assignment through merge.
.DESCRIPTION
Orchestrates two phases by launching separate `copilot --yolo` sessions:
Phase 1: Assignment to Ready for Review
Phase 2: Ready for Review to Merged
Between phases, the script verifies state using gh CLI (not copilot exit codes).
.PARAMETER TaskIssue
The issue number (e.g., 1841) or URL of the child task to shepherd.
.PARAMETER BaseBranch
The base branch the task PR should target.
.PARAMETER Repo
Repository in OWNER/REPO format.
#>
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$TaskIssue,
[Parameter(Mandatory = $true, Position = 1)]
[string]$BaseBranch,
[Parameter(Mandatory = $true, Position = 2)]
[string]$Repo
)
$ErrorActionPreference = "Stop"
function Write-Status($msg) {
Write-Host "[shepherd-task] $msg" -ForegroundColor Cyan
}
function Write-Fail($msg) {
Write-Host "[shepherd-task] FAILED: $msg" -ForegroundColor Red
}
function Write-Ok($msg) {
Write-Host "[shepherd-task] $msg" -ForegroundColor Green
}
# --- Helper: Find the PR linked to the task issue ---
function Find-LinkedPR {
# Strategy A: Issue timeline for cross-referenced PRs
$prNumber = gh api "/repos/$Repo/issues/$TaskIssue/timeline" `
--jq '.[] | select(.event == "cross-referenced") | select(.source.issue.pull_request != null) | select(.source.issue.state == "open") | .source.issue.number' 2>$null |
Select-Object -First 1
if ($prNumber) { return $prNumber.Trim() }
# Strategy B: Search PR bodies for the issue number
$prNumber = gh pr list -R $Repo --state open --json number,body `
--jq ".[] | select(.body | test(`"#$TaskIssue`")) | .number" 2>$null |
Select-Object -First 1
if ($prNumber) { return $prNumber.Trim() }
# Strategy C: Title or branch name match
$prNumber = gh pr list -R $Repo --state open --json number,title,headRefName `
--jq ".[] | select((.title | test(`"$TaskIssue`"; `"i`")) or (.headRefName | test(`"$TaskIssue`"))) | .number" 2>$null |
Select-Object -First 1
if ($prNumber) { return $prNumber.Trim() }
return $null
}
# --- Helper: Verify all CI checks pass (excluding expected failure) ---
function Test-CIPassing {
param([string]$PRNumber)
$failures = gh pr checks $PRNumber -R $Repo --json name,state,bucket `
--jq '.[] | select(.bucket == "fail") | select(.name != "No remove-before-merge directories") | .name' 2>$null
return [string]::IsNullOrWhiteSpace($failures)
}
# --- Helper: Check for unresolved bot review comments ---
function Test-NoUnresolvedReviews {
param([string]$PRNumber)
$unresolved = gh api graphql -F number=$PRNumber -f query='
query($number: Int!) {
repository(owner: "github", name: "copilot-sdk") {
pullRequest(number: $number) {
reviewThreads(first: 100) {
nodes { isResolved comments(first: 1) { nodes { author { login } } } }
}
}
}
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[0].author.login' 2>$null
return [string]::IsNullOrWhiteSpace($unresolved)
}
# =============================================================================
# PHASE 1: Assignment to Ready for Review
# =============================================================================
Write-Status "Phase 1: Launching copilot --yolo for task #$TaskIssue"
$phase1Prompt = @"
Invoke skill ``shepherd-task-to-ready`` with these inputs:
- TASK_ISSUE: $TaskIssue
- BASE_BRANCH: $BaseBranch
- REPO: $Repo
"@
Write-Status "Phase 1 prompt: $phase1Prompt"
$phase1Prompt | copilot --yolo
Write-Status "Phase 1: copilot exited. Verifying state..."
# --- Verify Phase 1 outcome ---
$prNumber = Find-LinkedPR
if (-not $prNumber) {
Write-Fail "No open PR found linked to issue #$TaskIssue after Phase 1."
exit 1
}
Write-Status "Found PR #$prNumber"
# Verify base branch
$actualBase = gh pr view $prNumber -R $Repo --json baseRefName --jq '.baseRefName'
if ($actualBase -ne $BaseBranch) {
Write-Status "PR base is '$actualBase', fixing to '$BaseBranch'..."
gh pr edit $prNumber -R $Repo --base $BaseBranch
}
# Verify CI passing
if (-not (Test-CIPassing $prNumber)) {
Write-Fail "CI checks not passing on PR #$prNumber after Phase 1."
exit 1
}
# Verify no unresolved reviews
if (-not (Test-NoUnresolvedReviews $prNumber)) {
Write-Fail "Unresolved review comments remain on PR #$prNumber after Phase 1."
exit 1
}
Write-Ok "Phase 1 VERIFIED: PR #$prNumber is ready. CI passing, no unresolved comments."
# =============================================================================
# PHASE 2: Ready for Review to Merged
# =============================================================================
Write-Status "Phase 2: Launching copilot --yolo for PR #$prNumber"
$phase2Prompt = @"
Invoke skill ``shepherd-task-from-ready-to-merged-to-base`` with these inputs:
- TASK_ISSUE: $TaskIssue
- BASE_BRANCH: $BaseBranch
- REPO: $Repo
- PR_NUMBER: $prNumber
"@
Write-Status "Phase 2 prompt: $phase2Prompt"
$phase2Prompt | copilot --yolo
Write-Status "Phase 2: copilot exited. Verifying state..."
# --- Verify Phase 2 outcome ---
$prState = gh pr view $prNumber -R $Repo --json state --jq '.state'
if ($prState -ne "MERGED") {
Write-Fail "PR #$prNumber is in state '$prState', expected MERGED."
exit 1
}
# Verify merged into correct branch (strip remote prefix for comparison)
$mergedBase = gh pr view $prNumber -R $Repo --json baseRefName --jq '.baseRefName'
$expectedBase = $BaseBranch -replace '^[^/]+/', ''
if ($mergedBase -ne $expectedBase) {
Write-Fail "PR #$prNumber was merged into '$mergedBase', expected '$expectedBase'."
exit 1
}
# Verify issue is closed
$issueState = gh issue view $TaskIssue -R $Repo --json state --jq '.state'
if ($issueState -ne "CLOSED") {
Write-Status "Issue #$TaskIssue still open, closing..."
gh issue close $TaskIssue -R $Repo
}
Write-Ok "SHEPHERD TASK COMPLETE: Task #$TaskIssue has been fully shepherded."
Write-Ok "PR #$prNumber merged to $BaseBranch."
exit 0