mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 09:26:52 +01:00
The review and publish jobs run in the same workflow_run; the artifact is intra-run. Downloading with run-id set to the intake run (github.event.workflow_run.id) looked in the wrong run and failed with 'Artifact not found'. Drop run-id/github-token so download-artifact resolves the artifact uploaded earlier in this run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 75d852d6-18a6-4c58-986f-ed5ab16618fa
240 lines
11 KiB
YAML
240 lines
11 KiB
YAML
name: Authoring-assist advisory runner
|
|
|
|
# Privileged companion to `Authoring-assist advisory`. Runs in the trusted
|
|
# workflow_run context so it can post a comment on fork PRs. Two jobs:
|
|
# review - read-only; resolves the PR, reads the PR-head article content, runs
|
|
# the (trusted-base) suggestion tool + renderer, uploads the comment.
|
|
# publish - holds pull-requests:write; upserts the single advisory comment.
|
|
# The suggestion tool is a deterministic PowerShell script that only PARSES
|
|
# article text (it never executes AL or PR content), and it runs from the trusted
|
|
# base checkout with the trusted routing seed, so only inert article text comes
|
|
# from the PR head. The advisory is NON-BLOCKING and is never a required check.
|
|
|
|
on:
|
|
workflow_run:
|
|
workflows:
|
|
- Authoring-assist advisory
|
|
types:
|
|
- completed
|
|
|
|
concurrency:
|
|
group: authoring-assist-runner-${{ github.event.workflow_run.id }}
|
|
cancel-in-progress: true
|
|
|
|
permissions: {}
|
|
|
|
jobs:
|
|
review:
|
|
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
pull-requests: read
|
|
defaults:
|
|
run:
|
|
shell: pwsh
|
|
outputs:
|
|
pr_number: ${{ steps.pr.outputs.number }}
|
|
head_sha: ${{ steps.pr.outputs.head_sha }}
|
|
has_report: ${{ steps.run.outputs.has_report }}
|
|
steps:
|
|
- name: Checkout trusted base tools
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: refs/heads/main
|
|
path: trusted
|
|
fetch-depth: 1
|
|
# The tool reads untrusted PR-head article text; keep no token in
|
|
# .git/config. Public content is fetched unauthenticated below.
|
|
persist-credentials: false
|
|
|
|
- name: Resolve PR details
|
|
id: pr
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
$headers = @{
|
|
Accept = 'application/vnd.github+json'
|
|
Authorization = "Bearer $env:GITHUB_TOKEN"
|
|
'User-Agent' = 'bcquality-authoring-assist'
|
|
}
|
|
$expectedHeadSha = '${{ github.event.workflow_run.head_sha }}'
|
|
$prNumber = '${{ github.event.workflow_run.pull_requests[0].number }}'
|
|
|
|
if (-not $prNumber) {
|
|
$headOwner = '${{ github.event.workflow_run.head_repository.owner.login }}'
|
|
$headBranch = '${{ github.event.workflow_run.head_branch }}'
|
|
if (-not $headOwner -or -not $headBranch) {
|
|
throw 'No PR number in workflow_run payload and insufficient head metadata for fallback.'
|
|
}
|
|
$encodedHead = [System.Uri]::EscapeDataString("${headOwner}:$headBranch")
|
|
$searchUri = "https://api.github.com/repos/${{ github.repository }}/pulls?state=open&head=$encodedHead&per_page=30"
|
|
$candidates = Invoke-RestMethod -Uri $searchUri -Headers $headers -Method GET
|
|
$matching = @($candidates | Where-Object { $_.head.sha -eq $expectedHeadSha })
|
|
if ($matching.Count -eq 1) { $prNumber = [string]$matching[0].number }
|
|
elseif ($matching.Count -gt 1) { throw 'Fallback PR lookup returned multiple matches.' }
|
|
}
|
|
if (-not $prNumber) { throw 'No pull request number resolved.' }
|
|
|
|
$pr = Invoke-RestMethod -Uri "https://api.github.com/repos/${{ github.repository }}/pulls/$prNumber" -Headers $headers -Method GET
|
|
if ($expectedHeadSha -and $pr.head.sha -ne $expectedHeadSha) {
|
|
throw 'Resolved PR head SHA does not match workflow_run head SHA.'
|
|
}
|
|
|
|
"number=$($pr.number)" >> $env:GITHUB_OUTPUT
|
|
"head_sha=$($pr.head.sha)" >> $env:GITHUB_OUTPUT
|
|
"base_ref=$($pr.base.ref)" >> $env:GITHUB_OUTPUT
|
|
|
|
- name: List changed knowledge articles
|
|
id: changed
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
$headers = @{
|
|
Accept = 'application/vnd.github+json'
|
|
Authorization = "Bearer $env:GITHUB_TOKEN"
|
|
'User-Agent' = 'bcquality-authoring-assist'
|
|
}
|
|
$n = '${{ steps.pr.outputs.number }}'
|
|
$changed = [System.Collections.Generic.List[string]]::new()
|
|
for ($page = 1; $page -le 30; $page++) {
|
|
$uri = "https://api.github.com/repos/${{ github.repository }}/pulls/$n/files?per_page=100&page=$page"
|
|
$files = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET
|
|
if (-not $files -or @($files).Count -eq 0) { break }
|
|
foreach ($f in $files) {
|
|
if ($f.status -in @('added','modified','renamed','changed') `
|
|
-and $f.filename -match '(^|/)knowledge/' `
|
|
-and $f.filename -match '\.md$') {
|
|
$changed.Add($f.filename) | Out-Null
|
|
}
|
|
}
|
|
if (@($files).Count -lt 100) { break }
|
|
}
|
|
$outFile = Join-Path $env:GITHUB_WORKSPACE 'changed-articles.txt'
|
|
($changed | Sort-Object -Unique) -join "`n" | Set-Content -LiteralPath $outFile -Encoding UTF8
|
|
Write-Host "Changed knowledge articles: $($changed.Count)"
|
|
"count=$($changed.Count)" >> $env:GITHUB_OUTPUT
|
|
|
|
- name: Fetch PR head content
|
|
if: steps.changed.outputs.count != '0'
|
|
run: |
|
|
$root = Join-Path $env:GITHUB_WORKSPACE 'head'
|
|
New-Item -ItemType Directory -Force -Path $root | Out-Null
|
|
git -C $root init -q
|
|
git -C $root remote add origin "https://github.com/${{ github.repository }}.git"
|
|
# PR head commits (incl. forks) are replicated under refs/pull/N/head.
|
|
git -C $root fetch --depth=1 origin "refs/pull/${{ steps.pr.outputs.number }}/head"
|
|
if ($LASTEXITCODE -ne 0) { throw "git fetch of PR head failed (exit $LASTEXITCODE)" }
|
|
git -C $root checkout -q FETCH_HEAD
|
|
|
|
- name: Generate suggestions and render comment
|
|
id: run
|
|
if: steps.changed.outputs.count != '0'
|
|
env:
|
|
AUTHORING_ASSIST_DOC_URL: ${{ vars.AUTHORING_ASSIST_DOC_URL }}
|
|
run: |
|
|
$out = Join-Path $env:GITHUB_WORKSPACE 'advisory-output'
|
|
New-Item -ItemType Directory -Force -Path $out | Out-Null
|
|
|
|
$changed = @(Get-Content -LiteralPath (Join-Path $env:GITHUB_WORKSPACE 'changed-articles.txt') |
|
|
Where-Object { $_ -and $_.Trim() })
|
|
|
|
$tool = Join-Path $env:GITHUB_WORKSPACE 'trusted/tools/Suggest-ArticleSignals.ps1'
|
|
$renderer = Join-Path $env:GITHUB_WORKSPACE 'trusted/tools/New-AuthoringAssistComment.ps1'
|
|
# Routing seed is optional (owned by the routing-index tuning thread).
|
|
# Pass it through only if that thread has landed one in trusted/tools;
|
|
# otherwise the tool runs self-contained.
|
|
$seed = Join-Path $env:GITHUB_WORKSPACE 'trusted/tools/routing-seed.json'
|
|
$headRoot = Join-Path $env:GITHUB_WORKSPACE 'head'
|
|
$reportPath = Join-Path $out 'report.json'
|
|
$commentPath = Join-Path $out 'comment.md'
|
|
|
|
# Trusted logic over head article text; enrich with the trusted seed
|
|
# only when present. Only inert article text comes from head.
|
|
$seedArgs = if (Test-Path -LiteralPath $seed) { @('-SeedPath', $seed) } else { @() }
|
|
& $tool -BCQualityRoot $headRoot @seedArgs -ChangedFiles $changed -AsJson |
|
|
Set-Content -LiteralPath $reportPath -Encoding UTF8
|
|
|
|
$repoUrl = "https://github.com/${{ github.repository }}"
|
|
& $renderer -JsonPath $reportPath `
|
|
-RepoUrl $repoUrl `
|
|
-Ref '${{ steps.pr.outputs.head_sha }}' `
|
|
-DocUrl $env:AUTHORING_ASSIST_DOC_URL |
|
|
Set-Content -LiteralPath $commentPath -Encoding UTF8
|
|
|
|
$report = Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json
|
|
"article_count=$($report.articleCount)" >> $env:GITHUB_OUTPUT
|
|
"has_report=true" >> $env:GITHUB_OUTPUT
|
|
Write-Host "Articles with suggestions: $($report.articleCount)"
|
|
|
|
- name: Upload advisory output
|
|
if: steps.run.outputs.has_report == 'true'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: authoring-assist-output-${{ steps.pr.outputs.number }}
|
|
path: ${{ github.workspace }}/advisory-output
|
|
if-no-files-found: warn
|
|
|
|
publish:
|
|
needs: review
|
|
if: ${{ needs.review.result == 'success' && needs.review.outputs.has_report == 'true' && needs.review.outputs.pr_number != '' }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
pull-requests: write
|
|
issues: write
|
|
defaults:
|
|
run:
|
|
shell: pwsh
|
|
steps:
|
|
- name: Download advisory output
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: authoring-assist-output-${{ needs.review.outputs.pr_number }}
|
|
path: ${{ github.workspace }}/advisory-output
|
|
|
|
- name: Upsert advisory comment
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
$anchor = '<!-- authoring-assist-advisory -->'
|
|
$body = Get-Content -LiteralPath (Join-Path $env:GITHUB_WORKSPACE 'advisory-output/comment.md') -Raw
|
|
$report = Get-Content -LiteralPath (Join-Path $env:GITHUB_WORKSPACE 'advisory-output/report.json') -Raw | ConvertFrom-Json
|
|
|
|
$headers = @{
|
|
Accept = 'application/vnd.github+json'
|
|
Authorization = "Bearer $env:GITHUB_TOKEN"
|
|
'User-Agent' = 'bcquality-authoring-assist'
|
|
}
|
|
$repo = '${{ github.repository }}'
|
|
$n = '${{ needs.review.outputs.pr_number }}'
|
|
|
|
# Find an existing advisory comment (idempotent update-in-place).
|
|
$existing = $null
|
|
for ($page = 1; $page -le 20; $page++) {
|
|
$uri = "https://api.github.com/repos/$repo/issues/$n/comments?per_page=100&page=$page"
|
|
$comments = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET
|
|
if (-not $comments -or @($comments).Count -eq 0) { break }
|
|
$hit = $comments | Where-Object { $_.body -and $_.body.Contains($anchor) } | Select-Object -First 1
|
|
if ($hit) { $existing = $hit; break }
|
|
if (@($comments).Count -lt 100) { break }
|
|
}
|
|
|
|
# Do not create a brand-new "no suggestions" comment; only update an
|
|
# existing one to reflect that state.
|
|
if (-not $existing -and [int]$report.articleCount -eq 0) {
|
|
Write-Host 'No suggestions and no existing advisory comment; nothing to post.'
|
|
return
|
|
}
|
|
|
|
$payload = @{ body = $body } | ConvertTo-Json -Depth 4
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
|
|
if ($existing) {
|
|
$uri = "https://api.github.com/repos/$repo/issues/comments/$($existing.id)"
|
|
Invoke-RestMethod -Uri $uri -Headers $headers -Method PATCH -Body $bytes -ContentType 'application/json' | Out-Null
|
|
Write-Host "Updated advisory comment $($existing.id)."
|
|
} else {
|
|
$uri = "https://api.github.com/repos/$repo/issues/$n/comments"
|
|
Invoke-RestMethod -Uri $uri -Headers $headers -Method POST -Body $bytes -ContentType 'application/json' | Out-Null
|
|
Write-Host 'Created advisory comment.'
|
|
}
|