Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/trigger-ubuntu-win-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,38 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v5

- name: Install Copilot CLI
shell: bash
run: |
curl -fsSL https://gh.io/copilot-install | bash
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
copilot --version
- name: Validate Copilot environment
shell: bash
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.MODELS_TOKEN }}
run: |
if [[ -z "$COPILOT_GITHUB_TOKEN" ]]; then
echo "MODELS_TOKEN is empty or unavailable in this run"
exit 1
fi
Comment on lines +88 to +96
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The validation step will fail the entire workflow if MODELS_TOKEN is not available, which may not be desirable for all scenarios (e.g., forks or runs where Copilot analysis is optional). Consider whether this should be a hard failure or if the workflow should gracefully skip the Copilot analysis when the token is unavailable. If Copilot is optional, consider using continue-on-error: true for this step.

Copilot uses AI. Check for mistakes.
if ! command -v copilot >/dev/null 2>&1; then
echo "copilot binary not found in PATH"
ls -la "$HOME/.local/bin" || true
exit 1
fi
- name: Wait for workflow completion
env:
CI_PR_TOKEN: ${{ secrets.CI_PR_TOKEN }}
CI_REPO: ${{ vars.CI_REPO }}
COPILOT_GITHUB_TOKEN: ${{ secrets.MODELS_TOKEN }}
GH_TOKEN: ${{ secrets.MODELS_TOKEN }}
GITHUB_TOKEN: ${{ secrets.MODELS_TOKEN }}
COPILOT_AUTO_UPDATE: "false"
COPILOT_MODEL: gpt-5
COPILOT_ALLOW_ALL: "false"
run: |
./helpers/WaitWorkflowCompletion.ps1 `
-WorkflowRunId "${{ needs.trigger-workflow.outputs.ci_workflow_run_id }}" `
Expand All @@ -92,13 +120,21 @@ jobs:
- name: Add Summary
if: always()
continue-on-error: true
run: |
"# Test Partner Image" >> $env:GITHUB_STEP_SUMMARY
"| Key | Value |" >> $env:GITHUB_STEP_SUMMARY
"| :-----------: | :--------: |" >> $env:GITHUB_STEP_SUMMARY
"| Workflow Run | [Link](${{ needs.trigger-workflow.outputs.ci_workflow_run_url }}) |" >> $env:GITHUB_STEP_SUMMARY
"| Workflow Result | $env:CI_WORKFLOW_RUN_RESULT |" >> $env:GITHUB_STEP_SUMMARY
" " >> $env:GITHUB_STEP_SUMMARY
if (-not [string]::IsNullOrWhiteSpace($env:CI_COPILOT_ANALYSIS)) {
"## Copilot Log Analysis" >> $env:GITHUB_STEP_SUMMARY
'```text' >> $env:GITHUB_STEP_SUMMARY
"$env:CI_COPILOT_ANALYSIS" >> $env:GITHUB_STEP_SUMMARY
'```' >> $env:GITHUB_STEP_SUMMARY
" " >> $env:GITHUB_STEP_SUMMARY
}
cancel-workflow:
runs-on: ubuntu-latest
Expand Down
29 changes: 29 additions & 0 deletions helpers/GitHubApi.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,29 @@ class GithubApi
return $response
}

[object] GetWorkflowRunJobs([string]$WorkflowRunId) {
$url = "actions/runs/$WorkflowRunId/jobs"
$response = $this.InvokeRestMethod($url, 'GET', $null, $null)
return $response
}

[void] DownloadJobLogs([string]$JobId, [string]$DestinationPath) {
$requestUrl = $this.BuildUrl("actions/jobs/$JobId/logs", $null, "api")
$params = @{
Uri = $requestUrl
Method = "GET"
Headers = @{}
OutFile = $DestinationPath
MaximumRedirection = 5
ErrorAction = "Stop"
}
if ($this.AuthHeader) {
$params.Headers += $this.AuthHeader
}

Invoke-WebRequest @params | Out-Null
}

[object] DispatchWorkflow([string]$EventType, [object]$EventPayload) {
$url = "dispatches"
$body = @{
Expand All @@ -53,6 +76,12 @@ class GithubApi
return $response
}

[object] ReRunFailedJobs([string]$WorkflowRunId) {
$url = "actions/runs/$WorkflowRunId/rerun-failed-jobs"
$response = $this.InvokeRestMethod($url, 'POST', $null, $null)
return $response
}

[string] hidden BuildUrl([string]$url, [string]$RequestParams, [string]$ApiPrefix) {
$baseUrl = $this.BuildBaseUrl($this.Repository, $ApiPrefix)
if ([string]::IsNullOrEmpty($RequestParams)) {
Expand Down
175 changes: 172 additions & 3 deletions helpers/WaitWorkflowCompletion.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Param (

Import-Module (Join-Path $PSScriptRoot "GitHubApi.psm1")

$script:CopilotAnalysis = ""

function Wait-ForWorkflowCompletion($WorkflowRunId, $RetryIntervalSeconds) {
do {
Start-Sleep -Seconds $RetryIntervalSeconds
Expand All @@ -20,17 +22,172 @@ function Wait-ForWorkflowCompletion($WorkflowRunId, $RetryIntervalSeconds) {
return $workflowRun
}

function Write-FailedJobLogs {
param (
$WorkflowJobs,
$GitHubApi,
[int] $TailLines = 0
)

if (-not ($WorkflowJobs -and $WorkflowJobs.jobs)) {
return
}

$failedJobs = $WorkflowJobs.jobs | Where-Object { $_.conclusion -eq "failure" }
if (-not $failedJobs) {
Write-Host "No failed jobs found in workflow run."
return
}

function Get-ProvisionerWindow {
param([string[]] $Lines)

if (-not $Lines) { return @() }

$start = $null
for ($i = $Lines.Length - 1; $i -ge 0; $i--) {
if ($Lines[$i] -match "Provisioning with") {
$start = $i
break
}
}

if ($start -eq $null) { return @() }

$end = $Lines.Length - 1
for ($j = $start; $j -lt $Lines.Length; $j++) {
if ($Lines[$j] -match "Provisioning step had errors: Running the cleanup provisioner, if present") {
$end = $j - 1
break
}
}

if ($end -lt $start) { return @() }
return $Lines[$start..$end]
}

function Invoke-CopilotLogAnalysis {
param([string[]] $LogLines)

if (-not $LogLines -or $LogLines.Count -eq 0) { return }
if ([string]::IsNullOrWhiteSpace($env:COPILOT_GITHUB_TOKEN)) { return }

$copilotCmd = $null
$cmdInfo = Get-Command copilot -ErrorAction SilentlyContinue
if ($cmdInfo) {
$copilotCmd = $cmdInfo.Source
} else {
$candidatePaths = @(
(Join-Path $env:HOME ".local/bin/copilot"),
"/usr/local/bin/copilot",
"/opt/homebrew/bin/copilot"
)
$copilotCmd = $candidatePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
}
if ([string]::IsNullOrWhiteSpace($copilotCmd)) { return }

$prompt = @"
Analyze the following CI provisioner failure log.
Return only 2 short lines:
1) Root cause
2) Suggested fix
Log:
$($LogLines -join "`n")
"@

$promptFile = Join-Path $env:RUNNER_TEMP "copilot-log-analysis.txt"
$prompt | Out-File -FilePath $promptFile -Encoding utf8NoBOM

try {
if ([string]::IsNullOrWhiteSpace($env:COPILOT_AUTO_UPDATE)) { $env:COPILOT_AUTO_UPDATE = "false" }
if ([string]::IsNullOrWhiteSpace($env:COPILOT_MODEL)) { $env:COPILOT_MODEL = "gpt-5" }
if ([string]::IsNullOrWhiteSpace($env:COPILOT_ALLOW_ALL)) { $env:COPILOT_ALLOW_ALL = "false" }
if ([string]::IsNullOrWhiteSpace($env:GH_TOKEN)) { $env:GH_TOKEN = $env:COPILOT_GITHUB_TOKEN }
if ([string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) { $env:GITHUB_TOKEN = $env:COPILOT_GITHUB_TOKEN }

$analysis = (Get-Content -Path $promptFile -Raw | & $copilotCmd --no-ask-user --no-custom-instructions 2>&1 | Out-String).Trim()
$analysis = [regex]::Replace($analysis, "(?ms)\r?\n?\s*Total usage est:.*$", "").Trim()
if ($analysis -match "No authentication information found") {
Write-Host "Copilot auth error: No authentication information found."
if ([string]::IsNullOrWhiteSpace($script:CopilotAnalysis)) {
$script:CopilotAnalysis = "Copilot auth error: No authentication information found."
}
return
}

if (-not [string]::IsNullOrWhiteSpace($analysis)) {
Write-Host $analysis
if ([string]::IsNullOrWhiteSpace($script:CopilotAnalysis)) {
$script:CopilotAnalysis = $analysis
}
} else {
Write-Host "Copilot analysis returned empty output."
if ([string]::IsNullOrWhiteSpace($script:CopilotAnalysis)) {
$script:CopilotAnalysis = "Copilot analysis returned empty output."
}
}
} catch {
} finally {
Remove-Item -Path $promptFile -Force -ErrorAction SilentlyContinue
}
}

foreach ($job in $failedJobs) {
$zipPath = Join-Path $env:RUNNER_TEMP "job-$($job.id)-logs.zip"
$extractPath = Join-Path $env:RUNNER_TEMP "job-$($job.id)-logs"
$logContent = @()

try {
$GitHubApi.DownloadJobLogs($job.id, $zipPath)
if (-not (Test-Path $zipPath) -or (Get-Item $zipPath).Length -eq 0) {
Write-Host "No downloadable logs for failed job $($job.name)."
continue
}

$slice = @()
try {
Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force -ErrorAction Stop | Out-Null
$logFiles = Get-ChildItem -Path $extractPath -Recurse -File | Sort-Object Length -Descending
if ($logFiles.Count -gt 0) {
$logContent = Get-Content -Path $logFiles[0].FullName
$slice = Get-ProvisionerWindow -Lines $logContent
}
} catch {
$rawContent = Get-Content -Path $zipPath -ErrorAction SilentlyContinue
if ($rawContent) {
$logContent = $rawContent
$slice = Get-ProvisionerWindow -Lines $rawContent
}
}

if ($slice.Count -eq 0 -and $logContent.Count -gt 0) {
$slice = $logContent | Select-Object -Last 200
Write-Host "Provisioner window not found; using last 200 log lines."
}

if ($slice.Count -gt 0) {
Invoke-CopilotLogAnalysis -LogLines $slice
($slice | Select-Object -Last ($(if ($TailLines -gt 0) { $TailLines } else { $slice.Count }))) -join "`n" | Write-Host
} else {
Write-Host "Failed job logs parsed, but no lines were available for analysis."
}
} finally {
Remove-Item -Path $zipPath -Force -ErrorAction SilentlyContinue
Remove-Item -Path $extractPath -Recurse -Force -ErrorAction SilentlyContinue
}
}
}

$gitHubApi = Get-GithubApi -Repository $Repository -AccessToken $AccessToken

$attempt = 1
do {
$finishedWorkflowRun = Wait-ForWorkflowCompletion -WorkflowRunId $WorkflowRunId -RetryIntervalSeconds $RetryIntervalSeconds
Write-Host "Workflow run finished with result: $($finishedWorkflowRun.conclusion)"
if ($finishedWorkflowRun.conclusion -in ("success", "cancelled", "timed_out")) {
break
} elseif ($finishedWorkflowRun.conclusion -eq "failure") {
if ($attempt -le $MaxRetryCount) {
Write-Host "Workflow run will be restarted. Attempt $attempt of $MaxRetryCount"
$gitHubApi.ReRunFailedJobs($WorkflowRunId)
$attempt += 1
} else {
Expand All @@ -39,8 +196,20 @@ do {
}
} while ($true)

Write-Host "Last result: $($finishedWorkflowRun.conclusion)."
try {
$workflowJobs = $gitHubApi.GetWorkflowRunJobs($WorkflowRunId)
if ($finishedWorkflowRun.conclusion -eq "failure") {
Write-FailedJobLogs -WorkflowJobs $workflowJobs -GitHubApi $gitHubApi -TailLines 0
}
} catch {
Write-Host "Failed to load workflow jobs/logs: $($_.Exception.Message)"
}
"CI_WORKFLOW_RUN_RESULT=$($finishedWorkflowRun.conclusion)" | Out-File -Append -FilePath $env:GITHUB_ENV
if (-not [string]::IsNullOrWhiteSpace($script:CopilotAnalysis) -and -not [string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) {
"CI_COPILOT_ANALYSIS<<EOF" | Out-File -Append -FilePath $env:GITHUB_ENV
$script:CopilotAnalysis | Out-File -Append -FilePath $env:GITHUB_ENV
"EOF" | Out-File -Append -FilePath $env:GITHUB_ENV
}

if ($finishedWorkflowRun.conclusion -in ("failure", "cancelled", "timed_out")) {
exit 1
Expand Down
Loading