Skip to content

Commit af18ed3

Browse files
Fix ScriptAnalyzer formatting and add PR CI/Copilot wait loop
1 parent 189b46b commit af18ed3

3 files changed

Lines changed: 309 additions & 1 deletion

File tree

scripts/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,30 @@ To test changes to the update script:
9191
- **No updates available**: If the Google Fonts API returns the same data, no PR will be created
9292
- **Authentication errors**: Ensure the GitHub App credentials are correctly configured
9393
- **API rate limits**: The Google Fonts API key must have sufficient quota for daily requests
94+
95+
## Wait-ForCopilotAndCI.ps1
96+
97+
This script continuously polls a pull request and only exits when all of the following are true:
98+
99+
- CI checks are green (no pending checks and no failures)
100+
- Copilot review has completed for the latest PR head commit
101+
- There are no unresolved review threads that contain Copilot comments
102+
103+
The loop never exits just because it is waiting. It keeps polling until all gates are satisfied.
104+
105+
### Usage
106+
107+
```powershell
108+
pwsh ./scripts/Wait-ForCopilotAndCI.ps1 -PullRequestNumber 210
109+
```
110+
111+
Optional parameters:
112+
113+
- `-Owner` and `-Repository` to target another repo (defaults to `PSModule/GoogleFonts`)
114+
- `-PollIntervalSeconds` to adjust polling interval (default: `30`)
115+
116+
Example with explicit repo and interval:
117+
118+
```powershell
119+
pwsh ./scripts/Wait-ForCopilotAndCI.ps1 -Owner PSModule -Repository GoogleFonts -PullRequestNumber 210 -PollIntervalSeconds 45
120+
```

scripts/Wait-ForCopilotAndCI.ps1

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
#Requires -Version 7.0
2+
[CmdletBinding()]
3+
param(
4+
[int] $PullRequestNumber,
5+
6+
[string] $Owner = 'PSModule',
7+
8+
[string] $Repository = 'GoogleFonts',
9+
10+
[ValidateRange(5, 3600)]
11+
[int] $PollIntervalSeconds = 30
12+
)
13+
14+
Set-StrictMode -Version Latest
15+
$ErrorActionPreference = 'Stop'
16+
17+
function Invoke-GitHubJson {
18+
<#
19+
.SYNOPSIS
20+
Runs a GitHub CLI command and parses JSON output.
21+
#>
22+
param(
23+
[Parameter(Mandatory)]
24+
[string[]] $Arguments
25+
)
26+
27+
$output = & gh @Arguments 2>&1
28+
if ($LASTEXITCODE -ne 0) {
29+
$joined = $output -join [Environment]::NewLine
30+
throw "gh $($Arguments -join ' ') failed with exit code $LASTEXITCODE.`n$joined"
31+
}
32+
33+
$text = $output -join [Environment]::NewLine
34+
if ([string]::IsNullOrWhiteSpace($text)) {
35+
return $null
36+
}
37+
38+
return $text | ConvertFrom-Json
39+
}
40+
41+
function Get-ItemCount {
42+
<#
43+
.SYNOPSIS
44+
Returns a safe count for null, scalar, or array inputs.
45+
#>
46+
param([object] $InputObject)
47+
48+
return @($InputObject).Count
49+
}
50+
51+
function Get-ActivePullRequestNumber {
52+
param([string] $Repo)
53+
54+
$pr = Invoke-GitHubJson -Arguments @('pr', 'view', '--repo', $Repo, '--json', 'number')
55+
if ($null -eq $pr -or $null -eq $pr.number) {
56+
throw 'Could not detect an active pull request. Provide -PullRequestNumber explicitly.'
57+
}
58+
59+
return [int] $pr.number
60+
}
61+
62+
function Get-UnresolvedCopilotThreads {
63+
param(
64+
[string] $RepoOwner,
65+
[string] $RepoName,
66+
[int] $PrNumber
67+
)
68+
69+
$query = 'query($owner:String!, $repo:String!, $pr:Int!) { repository(owner:$owner, name:$repo) { pullRequest(number:$pr) { reviewThreads(first:100) { nodes { isResolved comments(first:50) { nodes { author { login } body url createdAt } } } } } } }'
70+
$payload = Invoke-GitHubJson -Arguments @(
71+
'api',
72+
'graphql',
73+
'-f', "query=$query",
74+
'-F', "owner=$RepoOwner",
75+
'-F', "repo=$RepoName",
76+
'-F', "pr=$PrNumber"
77+
)
78+
79+
$threads = @($payload.data.repository.pullRequest.reviewThreads.nodes)
80+
if (-not $threads) {
81+
return @()
82+
}
83+
84+
$copilotThreads = foreach ($thread in $threads) {
85+
if ($thread.isResolved) {
86+
continue
87+
}
88+
89+
$comments = @($thread.comments.nodes)
90+
if (-not $comments) {
91+
continue
92+
}
93+
94+
$hasCopilotComment = $false
95+
foreach ($comment in $comments) {
96+
$author = $comment.author.login
97+
if ($author -match 'copilot') {
98+
$hasCopilotComment = $true
99+
break
100+
}
101+
}
102+
103+
if ($hasCopilotComment) {
104+
$latestComment = $comments | Sort-Object createdAt -Descending | Select-Object -First 1
105+
[pscustomobject]@{
106+
Url = $latestComment.url
107+
CreatedAt = $latestComment.createdAt
108+
Preview = if ($latestComment.body.Length -gt 120) { "$($latestComment.body.Substring(0, 120))..." } else { $latestComment.body }
109+
}
110+
}
111+
}
112+
113+
return @($copilotThreads)
114+
}
115+
116+
function Get-PullRequestSnapshot {
117+
param(
118+
[string] $RepoOwner,
119+
[string] $RepoName,
120+
[int] $PrNumber
121+
)
122+
123+
$repo = "$RepoOwner/$RepoName"
124+
$pr = Invoke-GitHubJson -Arguments @(
125+
'pr',
126+
'view',
127+
"$PrNumber",
128+
'--repo',
129+
$repo,
130+
'--json',
131+
'url,headRefOid,reviewRequests,reviews'
132+
)
133+
134+
$headSha = $pr.headRefOid
135+
$commit = Invoke-GitHubJson -Arguments @('api', "repos/$repo/commits/$headSha")
136+
$headCommitTime = [DateTimeOffset]::Parse($commit.commit.committer.date)
137+
138+
$allReviews = @($pr.reviews)
139+
$copilotReviews = @($allReviews | Where-Object {
140+
$_.author.login -match 'copilot'
141+
})
142+
$latestCopilotReview = $copilotReviews | Sort-Object submittedAt -Descending | Select-Object -First 1
143+
144+
$reviewRequests = @($pr.reviewRequests)
145+
$pendingCopilotRequest = @($reviewRequests | Where-Object { $_.login -match 'copilot' })
146+
147+
$latestCopilotReviewTime = $null
148+
if ($latestCopilotReview) {
149+
$latestCopilotReviewTime = [DateTimeOffset]::Parse($latestCopilotReview.submittedAt)
150+
}
151+
152+
$copilotReviewedCurrentHead = $false
153+
if ($null -ne $latestCopilotReviewTime -and $latestCopilotReviewTime -ge $headCommitTime) {
154+
$copilotReviewedCurrentHead = $true
155+
}
156+
157+
$unresolvedCopilotThreads = Get-UnresolvedCopilotThreads -RepoOwner $RepoOwner -RepoName $RepoName -PrNumber $PrNumber
158+
159+
$checks = @()
160+
try {
161+
$checks = @(Invoke-GitHubJson -Arguments @(
162+
'pr',
163+
'checks',
164+
"$PrNumber",
165+
'--repo',
166+
$repo,
167+
'--json',
168+
'name,state,bucket,workflow,link'
169+
))
170+
} catch {
171+
# Some repositories can briefly return no checks while workflow data is still being prepared.
172+
$checks = @()
173+
}
174+
175+
$pendingStates = @('PENDING', 'IN_PROGRESS', 'QUEUED', 'WAITING', 'REQUESTED')
176+
$failedStates = @('FAILURE', 'ERROR', 'TIMED_OUT', 'ACTION_REQUIRED', 'CANCELLED', 'STARTUP_FAILURE')
177+
178+
$pendingChecks = @($checks | Where-Object {
179+
$_.bucket -eq 'pending' -or $pendingStates -contains $_.state
180+
})
181+
$failedChecks = @($checks | Where-Object {
182+
$_.bucket -eq 'fail' -or $failedStates -contains $_.state
183+
})
184+
185+
$checkCount = Get-ItemCount -InputObject $checks
186+
$pendingCheckCount = Get-ItemCount -InputObject $pendingChecks
187+
$failedCheckCount = Get-ItemCount -InputObject $failedChecks
188+
$copilotThreadCount = Get-ItemCount -InputObject $unresolvedCopilotThreads
189+
$pendingCopilotRequestCount = Get-ItemCount -InputObject $pendingCopilotRequest
190+
191+
$ciGreen = ($checkCount -gt 0 -and $pendingCheckCount -eq 0 -and $failedCheckCount -eq 0)
192+
$copilotNoComments = ($copilotThreadCount -eq 0)
193+
$copilotReviewComplete = ($pendingCopilotRequestCount -eq 0 -and $copilotReviewedCurrentHead)
194+
195+
[pscustomobject]@{
196+
PullRequestNumber = $PrNumber
197+
PullRequestUrl = $pr.url
198+
HeadSha = $headSha
199+
HeadCommitTime = $headCommitTime
200+
LatestCopilotReviewTime = $latestCopilotReviewTime
201+
CopilotReviewedCurrentHead = $copilotReviewedCurrentHead
202+
PendingCopilotRequests = @($pendingCopilotRequest)
203+
UnresolvedCopilotThreads = @($unresolvedCopilotThreads)
204+
Checks = @($checks)
205+
PendingChecks = @($pendingChecks)
206+
FailedChecks = @($failedChecks)
207+
CiGreen = $ciGreen
208+
CopilotNoComments = $copilotNoComments
209+
CopilotReviewComplete = $copilotReviewComplete
210+
Ready = ($ciGreen -and $copilotNoComments -and $copilotReviewComplete)
211+
}
212+
}
213+
214+
$repoRef = "$Owner/$Repository"
215+
if (-not $PullRequestNumber) {
216+
$PullRequestNumber = Get-ActivePullRequestNumber -Repo $repoRef
217+
}
218+
219+
Write-Host "Watching PR #$PullRequestNumber in $repoRef"
220+
Write-Host "This loop only exits when CI is green and Copilot has no unresolved comments for the latest head commit."
221+
222+
$round = 0
223+
while ($true) {
224+
$round++
225+
$now = (Get-Date).ToString('u')
226+
Write-Host ''
227+
Write-Host "[$now] Poll round $round"
228+
229+
$snapshot = Get-PullRequestSnapshot -RepoOwner $Owner -RepoName $Repository -PrNumber $PullRequestNumber
230+
231+
Write-Host "PR: $($snapshot.PullRequestUrl)"
232+
Write-Host "Head SHA: $($snapshot.HeadSha)"
233+
Write-Host "CI green: $($snapshot.CiGreen)"
234+
Write-Host "Copilot review complete for head: $($snapshot.CopilotReviewComplete)"
235+
Write-Host "Copilot unresolved thread count: $($snapshot.UnresolvedCopilotThreads.Count)"
236+
237+
if ($snapshot.FailedChecks.Count -gt 0) {
238+
Write-Host 'Failing checks:'
239+
foreach ($check in $snapshot.FailedChecks) {
240+
Write-Host " - $($check.name) [$($check.state)]"
241+
}
242+
}
243+
244+
if ($snapshot.PendingChecks.Count -gt 0) {
245+
Write-Host 'Pending checks:'
246+
foreach ($check in $snapshot.PendingChecks) {
247+
Write-Host " - $($check.name) [$($check.state)]"
248+
}
249+
}
250+
251+
if (-not $snapshot.CopilotReviewComplete) {
252+
if ($snapshot.PendingCopilotRequests.Count -gt 0) {
253+
Write-Host 'Copilot review is still requested and pending.'
254+
}
255+
256+
if (-not $snapshot.CopilotReviewedCurrentHead) {
257+
if ($null -eq $snapshot.LatestCopilotReviewTime) {
258+
Write-Host 'Copilot has not posted a review yet for this PR.'
259+
} else {
260+
Write-Host "Latest Copilot review: $($snapshot.LatestCopilotReviewTime.ToString('u'))"
261+
Write-Host "Latest head commit: $($snapshot.HeadCommitTime.ToString('u'))"
262+
}
263+
}
264+
}
265+
266+
if ($snapshot.UnresolvedCopilotThreads.Count -gt 0) {
267+
Write-Host 'Unresolved Copilot threads:'
268+
foreach ($thread in $snapshot.UnresolvedCopilotThreads) {
269+
Write-Host " - $($thread.Url)"
270+
}
271+
}
272+
273+
if ($snapshot.Ready) {
274+
Write-Host ''
275+
Write-Host 'All gates are green. CI is successful and Copilot has no unresolved comments.'
276+
break
277+
}
278+
279+
Write-Host "Waiting $PollIntervalSeconds seconds before checking comments and pipeline status again..."
280+
Start-Sleep -Seconds $PollIntervalSeconds
281+
}

src/functions/public/Install-GoogleFont.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ Please run the command again with elevated rights (Run as Administrator) or prov
169169
URL = $URL
170170
DownloadPath = $downloadPath
171171
CachePath = $cachePath
172-
FromCache = (-not $Force) -and (Test-Path -LiteralPath $cachePath)
172+
FromCache = (-not $Force) -and (Test-Path -LiteralPath $cachePath)
173173
})
174174
}
175175

0 commit comments

Comments
 (0)