Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
34 changes: 34 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,36 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v5

- name: Install Copilot CLI
shell: bash
run: |
npm install -g @githubnext/github-copilot-cli
"$(npm bin -g)/copilot" --version
- name: Validate Copilot environment
shell: bash
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.CI_PR_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.CI_PR_TOKEN }}
GH_TOKEN: ${{ secrets.CI_PR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.CI_PR_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 +118,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
199 changes: 196 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,196 @@ 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()
if ($LASTEXITCODE -ne 0) {
$copilotErrorMessage = "Copilot CLI exited with code $LASTEXITCODE.`nOutput:`n$analysis"
Write-Host $copilotErrorMessage
if ([string]::IsNullOrWhiteSpace($script:CopilotAnalysis)) {
$script:CopilotAnalysis = $copilotErrorMessage
}
throw "Copilot CLI failed with exit code $LASTEXITCODE."
}
$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 {
Write-Host "Copilot analysis failed due to an unexpected error. See verbose output for details."
Write-Verbose ("Copilot analysis exception: {0}" -f $_.Exception.Message)
Write-Verbose ("Full exception: {0}" -f $_ | Out-String)
if ([string]::IsNullOrWhiteSpace($script:CopilotAnalysis)) {
$script:CopilotAnalysis = "Copilot analysis failed due to an unexpected error. Check workflow logs for details."
}
} 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"
$allLogContent = @()

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) {
foreach ($logFile in $logFiles) {
$currentLogContent = Get-Content -Path $logFile.FullName
if ($currentLogContent) {
$allLogContent += $currentLogContent
if ($slice.Count -eq 0) {
$slice = Get-ProvisionerWindow -Lines $currentLogContent
}
if ($slice.Count -gt 0) {
break
}
}
}
}
} catch {
$rawContent = Get-Content -Path $zipPath -ErrorAction SilentlyContinue
if ($rawContent) {
$allLogContent = $rawContent
$slice = Get-ProvisionerWindow -Lines $rawContent
}
}

if ($slice.Count -eq 0 -and $allLogContent.Count -gt 0) {
$slice = $allLogContent | 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 +220,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_COPILOT_ANALYSIS_END" | Out-File -Append -FilePath $env:GITHUB_ENV
$script:CopilotAnalysis | Out-File -Append -FilePath $env:GITHUB_ENV
"EOF_COPILOT_ANALYSIS_END" | Out-File -Append -FilePath $env:GITHUB_ENV
}

if ($finishedWorkflowRun.conclusion -in ("failure", "cancelled", "timed_out")) {
exit 1
Expand Down
2 changes: 1 addition & 1 deletion images/ubuntu/Ubuntu2204-Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ to accomplish this.
- Ant 1.10.12
- Gradle 9.3.1
- Lerna 9.0.5
- Maven 3.9.12
- Maven 3.9.13
- Sbt 1.12.5

### Tools
Expand Down
2 changes: 1 addition & 1 deletion images/ubuntu/Ubuntu2404-Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ to accomplish this.
- Ant 1.10.14
- Gradle 9.3.1
- Lerna 9.0.5
- Maven 3.9.12
- Maven 3.9.13

### Tools
- Ansible 2.20.3
Expand Down
10 changes: 10 additions & 0 deletions images/windows/scripts/tests/Tools.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ Describe "PowerShell Core" {
}
}

Describe "PowerShell Gallery" {
It "PSGallery repository is registered" {
Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}

It "Install-Module from PSGallery" {
"Install-Module -Name Microsoft.WinGet.Client -Repository PSGallery -Scope CurrentUser -Force -ErrorAction Stop" | Should -ReturnZeroExitCode
}
}

Describe "Sbt" {
It "sbt" {
"sbt --version" | Should -ReturnZeroExitCode
Expand Down
Loading