diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 959efc4f8604ed..4ff4a7ea77497e 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -5,6 +5,11 @@ "version": "v8", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" }, + "actions/upload-artifact@v4": { + "repo": "actions/upload-artifact", + "version": "v4", + "sha": "ea165f8d65b6e75b540449e92b4886f43607fa02" + }, "github/gh-aw-actions/setup@v0.63.0": { "repo": "github/gh-aw-actions/setup", "version": "v0.63.0", @@ -14,6 +19,16 @@ "repo": "github/gh-aw-actions/setup", "version": "v0.63.1", "sha": "53e09ec0be6271e81a69f51ef93f37212c8834b0" + }, + "github/gh-aw-actions/setup@v0.64.5": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.64.5", + "sha": "5d2ebfd87a1a45a8a8323c1a12c01b055730dac5" + }, + "github/gh-aw-actions/setup@v0.65.6": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.65.6", + "sha": "31130b20a8fd3ef263acbe2091267c0aace07e09" } } } diff --git a/.github/skills/breaking-change-doc/Build-IssueComment.ps1 b/.github/skills/breaking-change-doc/Build-IssueComment.ps1 new file mode 100644 index 00000000000000..4848f2eaf50e78 --- /dev/null +++ b/.github/skills/breaking-change-doc/Build-IssueComment.ps1 @@ -0,0 +1,94 @@ +# Build-IssueComment.ps1 +# Reads a breaking change issue draft markdown file, URL-encodes the title, +# body, and labels, and produces a PR comment markdown file containing: +# - A header +# - The full draft for inline review +# - A clickable link that pre-fills a new issue in dotnet/docs +# - An email reminder +# +# Usage: +# pwsh .github/skills/breaking-change-doc/Build-IssueComment.ps1 ` +# -IssueDraftPath issue-draft.md ` +# -Title "[Breaking change]: Something changed" ` +# -OutputPath pr-comment.md +# +# The issue draft file should contain only the issue body markdown (no title). + +param( + [Parameter(Mandatory = $true)] + [string]$IssueDraftPath, + + [Parameter(Mandatory = $true)] + [string]$Title, + + [string]$OutputPath = "pr-comment.md", + + [string]$Labels = "breaking-change,Pri1,doc-idea", + + [string]$DocsRepo = "dotnet/docs" +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path $IssueDraftPath)) { + Write-Error "Issue draft file not found: $IssueDraftPath" + exit 1 +} + +$issueBody = Get-Content -Path $IssueDraftPath -Raw -Encoding UTF8 + +# URL-encode using .NET Uri class (same technique the old script used) +$encodedTitle = [Uri]::EscapeDataString($Title) +$encodedBody = [Uri]::EscapeDataString($issueBody) +$encodedLabels = [Uri]::EscapeDataString($Labels) +$encodedEmailSubject = [Uri]::EscapeDataString("[Breaking Change] $Title") + +$issueUrl = "https://github.com/$DocsRepo/issues/new?title=$encodedTitle&body=$encodedBody&labels=$encodedLabels" +$notificationEmailUrl = "mailto:dotnetbcn@microsoft.com?subject=$encodedEmailSubject" + +$comment = @" +## Breaking Change Documentation + +$issueBody + +--- + +> [!NOTE] +> This documentation was generated with AI assistance from Copilot. + +:point_right: **[Click here to create the issue in dotnet/docs]($issueUrl)** + +After creating the issue, please email a link to it to +[.NET Breaking Change Notifications]($notificationEmailUrl). +"@ + +# GitHub comment body limit is 65536 characters. If the comment exceeds this, +# replace the inline draft with a short summary pointing at the file. +$maxCommentLength = 65000 +if ($comment.Length -gt $maxCommentLength) { + Write-Warning "Comment body ($($comment.Length) chars) exceeds GitHub limit. Truncating inline draft." + $comment = @" +## Breaking Change Documentation + +The full draft is too large to display inline. See ``issue-draft.md`` in the +workflow artifacts for the complete content. + +--- + +> [!NOTE] +> This documentation was generated with AI assistance from Copilot. + +:point_right: **[Click here to create the issue in dotnet/docs]($issueUrl)** + +After creating the issue, please email a link to it to +[.NET Breaking Change Notifications]($notificationEmailUrl). +"@ +} + +$comment | Out-File -FilePath $OutputPath -Encoding UTF8 -NoNewline + +Write-Host "Wrote PR comment to $OutputPath ($($comment.Length) characters)" +Write-Host "Issue URL length: $($issueUrl.Length) characters" +if ($issueUrl.Length -gt 8192) { + Write-Warning "URL exceeds 8192 characters. Some browsers may truncate it. Consider shortening the issue body." +} diff --git a/.github/skills/breaking-change-doc/Get-VersionInfo.ps1 b/.github/skills/breaking-change-doc/Get-VersionInfo.ps1 new file mode 100644 index 00000000000000..76332081aa3d38 --- /dev/null +++ b/.github/skills/breaking-change-doc/Get-VersionInfo.ps1 @@ -0,0 +1,196 @@ +# Get-VersionInfo.ps1 +# Determines the .NET version context for a merged PR using the GitHub CLI (gh). +# +# Usage: +# pwsh .github/skills/breaking-change-doc/Get-VersionInfo.ps1 -PrNumber 114929 +# +# Output: JSON object with LastTagBeforeMerge, FirstTagWithChange, EstimatedVersion + +param( + [Parameter(Mandatory = $true)] + [string]$PrNumber, + + [string]$SourceRepo = "dotnet/runtime", + + [string]$BaseRef = "" +) + +$ErrorActionPreference = "Stop" + +function ConvertFrom-DotNetTag { + param([string]$tagName) + + if (-not $tagName -or $tagName -eq "Unknown") { + return $null + } + + if ($tagName -match '^v(\d+)\.(\d+)\.(\d+)(?:-(.+))?$') { + $major = [int]$matches[1] + $minor = [int]$matches[2] + $build = [int]$matches[3] + $prerelease = if ($matches[4]) { $matches[4] } else { $null } + + $prereleaseType = $null + $prereleaseNumber = $null + + if ($prerelease -and $prerelease -match '^([a-zA-Z]+)\.(\d+)') { + $rawType = $matches[1] + $prereleaseNumber = [int]$matches[2] + + if ($rawType -ieq "rc") { + $prereleaseType = "RC" + } else { + $prereleaseType = $rawType.Substring(0, 1).ToUpper() + $rawType.Substring(1).ToLower() + } + } + + return @{ + Major = $major + Minor = $minor + Build = $build + Prerelease = $prerelease + PrereleaseType = $prereleaseType + PrereleaseNumber = $prereleaseNumber + IsRelease = $null -eq $prerelease + } + } + + return $null +} + +function Format-DotNetVersion { + param($parsedTag) + + if (-not $parsedTag) { + return "Next release" + } + + $baseVersion = ".NET $($parsedTag.Major).$($parsedTag.Minor)" + + if ($parsedTag.IsRelease) { + return $baseVersion + } + + if ($parsedTag.PrereleaseType -and $parsedTag.PrereleaseNumber) { + return "$baseVersion $($parsedTag.PrereleaseType) $($parsedTag.PrereleaseNumber)" + } + + return "$baseVersion ($($parsedTag.Prerelease))" +} + +function Get-EstimatedNextVersion { + param($parsedTag, [string]$baseRef) + + if (-not $parsedTag) { + return "Next release" + } + + $isMainBranch = $baseRef -eq "main" + + if ($parsedTag.IsRelease) { + if ($isMainBranch) { + $nextMajor = $parsedTag.Major + 1 + return ".NET $nextMajor.0 Preview 1" + } else { + return ".NET $($parsedTag.Major).$($parsedTag.Minor)" + } + } + + if ($isMainBranch -and $parsedTag.PrereleaseType -eq "RC") { + $nextMajor = $parsedTag.Major + 1 + return ".NET $nextMajor.0 Preview 1" + } else { + $nextPreview = $parsedTag.PrereleaseNumber + 1 + return ".NET $($parsedTag.Major).$($parsedTag.Minor) $($parsedTag.PrereleaseType) $nextPreview" + } +} + +try { + # Step 1: Get PR merge info via GitHub CLI + $prJson = gh pr view $PrNumber --repo $SourceRepo --json mergeCommit,mergedAt,baseRefName 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Failed to fetch PR #$PrNumber from $SourceRepo" + } + $prData = $prJson | ConvertFrom-Json + + $targetCommit = $prData.mergeCommit.oid + $mergedAt = $prData.mergedAt + + if (-not $BaseRef) { + $BaseRef = $prData.baseRefName + } + + # Step 2: Get recent releases (tags with published dates) in a single API call + $releasesJson = gh release list --repo $SourceRepo --limit 100 --json tagName,publishedAt 2>$null + $releases = @() + if ($LASTEXITCODE -eq 0 -and $releasesJson) { + $releases = @($releasesJson | ConvertFrom-Json) + } + + # Filter to .NET version tags (v{major}.{minor}.{patch}[-prerelease]) with a valid publishedAt + $versionReleases = @($releases | Where-Object { $_.tagName -match '^v\d+\.\d+\.\d+' -and $_.publishedAt }) + + $lastTagBefore = "Unknown" + $firstTagWith = "Not yet released" + + if ($mergedAt -and $versionReleases.Count -gt 0) { + $mergedAtDate = [DateTimeOffset]::Parse($mergedAt) + + # Find the most recent release published before the merge + $beforeMerge = @($versionReleases | + Where-Object { [DateTimeOffset]::Parse($_.publishedAt) -lt $mergedAtDate } | + Sort-Object { [DateTimeOffset]::Parse($_.publishedAt) } -Descending) + + if ($beforeMerge.Count -gt 0) { + $lastTagBefore = $beforeMerge[0].tagName + } + + # Find candidate releases published at or after the merge, oldest first + $afterMerge = @($versionReleases | + Where-Object { [DateTimeOffset]::Parse($_.publishedAt) -ge $mergedAtDate } | + Sort-Object { [DateTimeOffset]::Parse($_.publishedAt) }) + + # Verify containment via the compare API: behind_by == 0 means the tag + # includes every commit reachable from the merge commit. + if ($targetCommit -and $afterMerge.Count -gt 0) { + foreach ($release in $afterMerge) { + $tag = $release.tagName + $behindBy = gh api "repos/$SourceRepo/compare/${targetCommit}...${tag}" --jq '.behind_by' 2>$null + if ($LASTEXITCODE -eq 0 -and $behindBy -match '^\d+$' -and [int]$behindBy -eq 0) { + $firstTagWith = $tag + break + } + } + } + } + + # Step 3: Estimate version + $estimatedVersion = "Next release" + + if ($firstTagWith -ne "Not yet released") { + $parsedFirstTag = ConvertFrom-DotNetTag $firstTagWith + $estimatedVersion = Format-DotNetVersion $parsedFirstTag + } else { + $parsedLastTag = ConvertFrom-DotNetTag $lastTagBefore + $estimatedVersion = Get-EstimatedNextVersion $parsedLastTag $BaseRef + } + + # Output as JSON + @{ + LastTagBeforeMerge = $lastTagBefore + FirstTagWithChange = $firstTagWith + EstimatedVersion = $estimatedVersion + MergeCommit = $targetCommit + MergedAt = $mergedAt + BaseRef = $BaseRef + } | ConvertTo-Json + +} catch { + # Return error info as JSON so the agent can handle it + @{ + LastTagBeforeMerge = "Unknown" + FirstTagWithChange = "Not yet released" + EstimatedVersion = "Next release" + Error = $_.Exception.Message + } | ConvertTo-Json +} diff --git a/.github/skills/breaking-change-doc/SKILL.md b/.github/skills/breaking-change-doc/SKILL.md new file mode 100644 index 00000000000000..5a363add915ba3 --- /dev/null +++ b/.github/skills/breaking-change-doc/SKILL.md @@ -0,0 +1,307 @@ +--- +name: breaking-change-doc +description: > + Generate breaking change documentation for merged dotnet/runtime PRs. + USE FOR: creating breaking change docs, "document this breaking change", + "write breaking change issue for PR #NNNNN", processing PRs labeled + needs-breaking-change-doc-created. DO NOT USE FOR: general code review + (use code-review skill), bug fixes, API proposals (use api-proposal skill). +--- + +# Breaking Change Documentation Skill + +Generate high-quality breaking change documentation for merged dotnet/runtime +pull requests and file it as an issue in [dotnet/docs](https://github.com/dotnet/docs). + +## Overview + +When a PR in dotnet/runtime introduces a breaking change, the docs team needs +a structured issue in dotnet/docs describing the change, its impact, and +migration guidance. This skill automates that process: + +1. **Gather PR context** — read the PR, its diff, related issues, comments, and reviews. +2. **Detect version** — run the helper script to determine which .NET release the change lands in. +3. **Check for duplicates** — search dotnet/docs for existing breaking-change issues for this PR. +4. **Fetch reference material** — read the issue template and recent example issues from dotnet/docs. +5. **Author the documentation** — produce the issue body following the template structure and example quality. +6. **Publish** — write output files and optionally comment on the source PR. + +### Trigger modes + +- **Interactive**: Ask Copilot (e.g. "Document the breaking change + in PR #114929"). The skill presents a draft for review before publishing. +- **Automated**: The [GitHub Agentic Workflow](https://github.github.com/gh-aw/) + at `.github/workflows/breaking-change-doc.md` triggers when a PR labeled + `needs-breaking-change-doc-created` is merged (or the label is added to an + already-merged PR). It can also be run manually via `workflow_dispatch` with + an optional `suppress_output` flag for dry-run inspection. The compiled + workflow (`.lock.yml`) must be regenerated with `gh aw compile` after changes. + +### Files + +| File | Purpose | +|------|---------| +| `.github/workflows/breaking-change-doc.md` | gh-aw workflow — triggers on PR merge/label | +| `.github/workflows/breaking-change-doc.lock.yml` | Compiled workflow (generated by `gh aw compile`) | +| `.github/skills/breaking-change-doc/SKILL.md` | This skill | +| `.github/skills/breaking-change-doc/Get-VersionInfo.ps1` | `gh` CLI-based .NET version detection | +| `.github/skills/breaking-change-doc/Build-IssueComment.ps1` | Builds PR comment with URL-encoded issue creation link | + +--- + +## Step 0: Accept Input + +The user provides one of: +- A PR number (e.g. `#114929` or `114929`) +- A PR URL (e.g. `https://github.com/dotnet/runtime/pull/114929`) +- A request like "document the breaking change in PR 114929" + +Extract the PR number. The source repository is always `dotnet/runtime`. + +--- + +## Step 1: Gather PR Context + +Use GitHub tools to read comprehensive PR data. Collect **all** of the following: + +1. **PR metadata**: title, author, base branch, merge commit SHA, merged-at date, labels, state. +2. **PR body**: the full description. +3. **Changed files**: list of file paths modified. +4. **PR comments and reviews**: read all comments and review comments for context about the change's impact. +5. **Closing issues**: if the PR closes any issues, read those issues fully (body + comments) — they often contain the motivation and user-reported impact. +6. **Feature area labels**: extract `area-*` labels. If none exist, report an error asking the user to set one. + +### Identifying the feature area + +Map the `area-*` label to the dotnet/docs feature area dropdown value: + +| `area-*` label pattern | Feature area | +|---|---| +| `area-System.Net.*`, `area-Networking` | Networking | +| `area-System.Security.*`, `area-Cryptography` | Cryptography | +| `area-System.Text.Json`, `area-Serialization` | Serialization | +| `area-System.Xml.*` | XML, XSLT | +| `area-Extensions-*` | Extensions | +| `area-System.Globalization` | Globalization | +| `area-System.Runtime.InteropServices*`, `area-Interop` | Interop | +| `area-CodeGen-*`, `area-JIT` | JIT | +| `area-System.Linq*` | LINQ | +| `area-System.CodeDom`, `area-Analyzers` | Code analysis | +| `area-Infrastructure-*`, `area-SDK` | SDK | +| `area-System.Windows.Forms*` | Windows Forms | +| `area-WPF` | Windows Presentation Foundation (WPF) | +| Most other `area-System.*` labels | Core .NET libraries | + +If the mapping is unclear, use "Other (please put exact area in description textbox)" and +include the actual area label in the description. + +--- + +## Step 2: Detect Version Information + +Run the helper script to determine the .NET version context: + +``` +pwsh .github/skills/breaking-change-doc/Get-VersionInfo.ps1 -PrNumber +``` + +The script outputs JSON with: +- `LastTagBeforeMerge` — the closest release tag before the merge commit +- `FirstTagWithChange` — the first tag that contains this commit (or "Not yet released") +- `EstimatedVersion` — human-readable version string like ".NET 11 Preview 3" +- `MergeCommit` — the merge commit SHA +- `MergedAt` — when the PR was merged + +Use `EstimatedVersion` as the version for the breaking change issue. + +If the script returns an error, fall back to reading the PR's base branch and recent +tags manually to estimate the version. + +--- + +## Step 3: Check for Existing Documentation + +Search for existing breaking change issues in dotnet/docs: + +- Search `dotnet/docs` issues for `Breaking change ` with label `breaking-change`. +- If a matching issue already exists, report it to the user and **stop** — do not create a duplicate. + +--- + +## Step 4: Fetch Reference Material + +### Issue template + +Read the breaking change issue template from dotnet/docs at: +`.github/ISSUE_TEMPLATE/02-breaking-change.yml` + +``` +gh api repos/dotnet/docs/contents/.github/ISSUE_TEMPLATE/02-breaking-change.yml -H "Accept: application/vnd.github.raw" +``` + +This template defines the required sections and dropdown values. Use it as a +structural reference only — do **not** output YAML. + +### Example issues + +Search `dotnet/docs` for 2-3 recent issues with the `breaking-change` label. +Read their bodies to understand the expected quality, tone, and level of detail. + +--- + +## Step 5: Author the Documentation + +Generate a complete breaking change issue. The output must be **clean markdown** +formatted to work with the GitHub issue form template. Structure it as follows: + +### Required sections + +#### Title +`[Breaking change]: ` + +Do not just repeat the PR title. Write a clear, user-facing summary. + +#### Description +Brief description of the breaking change. Include the PR link. + +#### Version +Use the `EstimatedVersion` from Step 2. Must match one of the template dropdown values +(e.g., ".NET 11 Preview 3"). If it doesn't match exactly, use +"Other (please put exact version in description textbox)" and state the version in the description. + +#### Previous behavior +Describe what happened before the change. Include a **code example** if applicable +showing the old behavior. + +#### New behavior +Describe what happens now. Include a **code example** if applicable showing the +new behavior. Highlight exceptions thrown, changed return values, or different +default settings. + +#### Type of breaking change +Categorize as one or more of: +- **Binary incompatible**: existing binaries may fail to load/execute +- **Source incompatible**: existing source may fail to compile +- **Behavioral change**: existing binaries behave differently at runtime + +Most changes are behavioral. Only mark binary/source incompatible when the +change actually affects compilation or binary loading. + +#### Reason for change +Explain **why** the change was made. Reference the motivation from the PR body +and closing issues. + +#### Recommended action +Provide **specific, actionable** guidance: +- Code changes the user should make +- Configuration switches to restore old behavior (if any exist, e.g. AppContext switches) +- Workarounds + +#### Feature area +Use the mapping from Step 1. + +#### Affected APIs +List all affected APIs. For methods, specify whether it's all overloads or +specific ones. Use fully qualified names (e.g., `System.IO.Compression.ZipArchiveEntry.Open()`). + +### Quality guidelines + +- **Professional tone** — this is official Microsoft documentation. +- **Concrete examples** — before/after code snippets make the change tangible. +- **Actionable guidance** — don't just describe the problem, help users fix it. +- **Accurate version** — use the detected version from Step 2. +- **Complete API list** — review the diff to find all affected public APIs. + +--- + +## Step 6: Publish + +### Write the issue draft file + +Create the output directory and write the full markdown content from Step 5 +(everything except the title line) to +`artifacts/docs/breakingChanges/issue-draft.md`. This file is the primary +output and can be reviewed before taking any further action. + +```bash +mkdir -p artifacts/docs/breakingChanges +``` + +### Build the PR comment file + +Run the helper script to produce a PR comment with a URL-encoded issue link: + +```bash +pwsh .github/skills/breaking-change-doc/Build-IssueComment.ps1 \ + -IssueDraftPath artifacts/docs/breakingChanges/issue-draft.md \ + -Title "" \ + -OutputPath artifacts/docs/breakingChanges/pr-comment.md +``` + +The script: +- URL-encodes the title, body, and labels using `[Uri]::EscapeDataString` +- Builds a clickable `https://github.com/dotnet/docs/issues/new?...` link +- Writes `pr-comment.md` containing the full draft, the link, and an email reminder +- Warns if the URL exceeds browser length limits + +### Post the comment + +When running interactively (not in dry-run mode), post the contents of +`artifacts/docs/breakingChanges/pr-comment.md` as a comment on the original +dotnet/runtime PR using GitHub tools. + +When running in automated (gh-aw) mode, use the add_comment safe-output tool to post the comment. + +When in dry-run mode or when the user has not explicitly asked to comment, +skip posting — the two files under `artifacts/docs/breakingChanges/` are the +outputs for review. + +### AI-generated content disclosure + +The `Build-IssueComment.ps1` script includes the standard AI disclosure note +in the generated comment. No additional action is needed. + +### Email reminder + +The generated comment includes an email reminder. When running interactively, +also remind the user: +> Please email a link to this breaking change issue to +> [.NET Breaking Change Notifications](mailto:dotnetbcn@microsoft.com). + +--- + +## Draft-only mode + +If the user asks for a draft or review before publishing, or if you are uncertain +about any aspect of the documentation: + +1. Present the full issue content in chat for review. +2. Ask the user to confirm before creating the issue. +3. Only publish after explicit confirmation. + +When the user has not explicitly asked to create the issue, default to draft mode. + +--- + +## Processing multiple PRs + +If the user provides a GitHub search query or asks to process multiple PRs: + +1. Search for matching PRs using the query. +2. For each PR, run Steps 1-5. +3. Present a summary table of all PRs with their status (already documented vs needs docs). +4. For PRs needing docs, present drafts and ask for confirmation before creating issues. + +--- + +## Troubleshooting + +| Problem | Solution | +|---|---| +| No `area-*` label on PR | Ask the user to add one, or ask which area applies | +| Version detection script fails | Read the PR's base branch and use `git describe --tags --abbrev=0` manually | +| PR not yet merged | Breaking change docs are for merged PRs only — inform the user | +| Existing docs issue found | Report the existing issue URL and stop | +| Cannot determine affected APIs | Review the diff carefully; list the public types/methods in changed files | +| Workflow not triggering | Ensure `gh aw compile` was run and `.lock.yml` is committed | +| AI output needs review | Check technical accuracy of before/after behavior, API list completeness, and migration guidance | diff --git a/.github/workflows/breaking-change-doc.lock.yml b/.github/workflows/breaking-change-doc.lock.yml new file mode 100644 index 00000000000000..92c97cc4f74f2c --- /dev/null +++ b/.github/workflows/breaking-change-doc.lock.yml @@ -0,0 +1,1194 @@ +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.65.6). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Generate breaking change documentation for merged PRs labeled needs-breaking-change-doc-created. Produces two markdown files (issue-draft.md and pr-comment.md) and optionally comments on the PR. +# +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7c86aef08973645499a88ff9ac23e1719f681df3b8dc13c01af6fb3fae9ca83c","compiler_version":"v0.65.6","strict":true,"agent_id":"copilot"} + +name: "Breaking Change Documentation" +"on": + pull_request_target: + types: + - closed + - labeled + # steps: # Steps injected into pre-activation job + # - name: Checkout the select-copilot-pat action folder + # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + # with: + # fetch-depth: 1 + # persist-credentials: false + # sparse-checkout: .github/actions/select-copilot-pat + # sparse-checkout-cone-mode: true + # - env: + # SECRET_0: ${{ secrets.COPILOT_PAT_0 }} + # SECRET_1: ${{ secrets.COPILOT_PAT_1 }} + # SECRET_2: ${{ secrets.COPILOT_PAT_2 }} + # SECRET_3: ${{ secrets.COPILOT_PAT_3 }} + # SECRET_4: ${{ secrets.COPILOT_PAT_4 }} + # SECRET_5: ${{ secrets.COPILOT_PAT_5 }} + # SECRET_6: ${{ secrets.COPILOT_PAT_6 }} + # SECRET_7: ${{ secrets.COPILOT_PAT_7 }} + # SECRET_8: ${{ secrets.COPILOT_PAT_8 }} + # SECRET_9: ${{ secrets.COPILOT_PAT_9 }} + # id: select-copilot-pat + # name: Select Copilot token from pool + # uses: ./.github/actions/select-copilot-pat + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + pr_number: + description: Pull Request Number + required: true + type: string + suppress_output: + default: false + description: Suppress workflow output (dry-run — only produce markdown workflow artifacts) + required: false + type: boolean + +permissions: {} + +concurrency: + cancel-in-progress: true + group: breaking-change-doc-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }} + +run-name: "Breaking Change Documentation" + +jobs: + activation: + needs: pre_activation + if: > + needs.pre_activation.outputs.activated == 'true' && (github.event_name == 'workflow_dispatch' || + ( + !github.event.repository.fork && + github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'needs-breaking-change-doc-created') + )) + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + body: ${{ steps.sanitized.outputs.body }} + comment_id: "" + comment_repo: "" + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + text: ${{ steps.sanitized.outputs.text }} + title: ${{ steps.sanitized.outputs.title }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "latest" + GH_AW_INFO_AGENT_VERSION: "latest" + GH_AW_INFO_CLI_VERSION: "v0.65.6" + GH_AW_INFO_WORKFLOW_NAME: "Breaking Change Documentation" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.11" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: ${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "breaking-change-doc.lock.yml" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_COMPILED_VERSION: "v0.65.6" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Compute current body text + id: sanitized + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_INPUTS_PR_NUMBER: ${{ github.event.inputs.pr_number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh + { + cat << 'GH_AW_PROMPT_f46a753a06b313f7_EOF' + + GH_AW_PROMPT_f46a753a06b313f7_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_f46a753a06b313f7_EOF' + + Tools: add_comment, missing_tool, missing_data, noop + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_f46a753a06b313f7_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_f46a753a06b313f7_EOF' + + {{#runtime-import .github/workflows/breaking-change-doc.md}} + GH_AW_PROMPT_f46a753a06b313f7_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_EVENT_INPUTS_PR_NUMBER: ${{ github.event.inputs.pr_number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_INPUTS_PR_NUMBER: ${{ github.event.inputs.pr_number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_INPUTS_PR_NUMBER: process.env.GH_AW_GITHUB_EVENT_INPUTS_PR_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash ${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash ${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: breakingchangedoc + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Set runtime paths + id: set-runtime-paths + run: | + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash ${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure gh CLI for GitHub Enterprise + run: bash ${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh + env: + GH_TOKEN: ${{ github.token }} + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest + - name: Install AWF binary + run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.11 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.11 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.11 ghcr.io/github/gh-aw-firewall/squid:0.25.11 ghcr.io/github/gh-aw-mcpg:v0.2.11 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_e4f1701b94f3ff9c_EOF' + {"add_comment":{"max":1,"target":"*"},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"}} + GH_AW_SAFE_OUTPUTS_CONFIG_e4f1701b94f3ff9c_EOF + - name: Write Safe Outputs Tools + run: | + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_aa956acbe39a3949_EOF' + { + "description_suffixes": { + "add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: *." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_SAFE_OUTPUTS_TOOLS_META_aa956acbe39a3949_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_65e8725d06d4acd6_EOF' + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + } + } + GH_AW_SAFE_OUTPUTS_VALIDATION_65e8725d06d4acd6_EOF + node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash ${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.11' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_443fee0fd0da6a12_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_443fee0fd0da6a12_EOF + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool shell(cat) + # --allow-tool shell(date) + # --allow-tool shell(echo) + # --allow-tool shell(gh:*) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(ls) + # --allow-tool shell(pwd) + # --allow-tool shell(pwsh) + # --allow-tool shell(sort) + # --allow-tool shell(tail) + # --allow-tool shell(uniq) + # --allow-tool shell(wc) + # --allow-tool shell(yq) + # --allow-tool write + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.11 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(gh:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(pwsh)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.65.6 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash ${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash ${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + run: bash ${RUNNER_TEMP}/gh-aw/actions/parse_token_usage.sh + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - if: always() + name: Upload breaking change drafts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + if-no-files-found: ignore + name: breaking-change-docs + path: artifacts/docs/breakingChanges/ + retention-days: 30 + + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + if-no-files-found: ignore + - name: Upload firewall audit logs + if: always() + continue-on-error: true + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: firewall-audit-logs + path: | + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-breaking-change-doc" + cancel-in-progress: false + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Breaking Change Documentation" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Breaking Change Documentation" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Breaking Change Documentation" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "breaking-change-doc" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_TIMEOUT_MINUTES: "20" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Download container images + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.11 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.11 ghcr.io/github/gh-aw-firewall/squid:0.25.11 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + WORKFLOW_NAME: "Breaking Change Documentation" + WORKFLOW_DESCRIPTION: "Generate breaking change documentation for merged PRs labeled needs-breaking-change-doc-created. Produces two markdown files (issue-draft.md and pr-comment.md) and optionally comments on the PR." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Install GitHub Copilot CLI + run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest + - name: Install AWF binary + run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.11 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.11 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.65.6 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + + pre_activation: + if: > + github.event_name == 'workflow_dispatch' || + ( + !github.event.repository.fork && + github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'needs-breaking-change-doc-created') + ) + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} + matched_command: '' + select-copilot-pat_result: ${{ steps.select-copilot-pat.outcome }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + await main(); + - name: Checkout the select-copilot-pat action folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + sparse-checkout: .github/actions/select-copilot-pat + sparse-checkout-cone-mode: true + - name: Select Copilot token from pool + id: select-copilot-pat + uses: ./.github/actions/select-copilot-pat + env: + SECRET_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_9: ${{ secrets.COPILOT_PAT_9 }} + + safe_outputs: + needs: + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/breaking-change-doc" + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_WORKFLOW_ID: "breaking-change-doc" + GH_AW_WORKFLOW_NAME: "Breaking Change Documentation" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} + comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + destination: ${{ runner.temp }}/gh-aw/actions + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Output Items + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: safe-output-items + path: /tmp/gh-aw/safe-output-items.jsonl + if-no-files-found: ignore + diff --git a/.github/workflows/breaking-change-doc.md b/.github/workflows/breaking-change-doc.md new file mode 100644 index 00000000000000..3abd66915e76a8 --- /dev/null +++ b/.github/workflows/breaking-change-doc.md @@ -0,0 +1,142 @@ +--- +description: > + Generate breaking change documentation for merged PRs labeled + needs-breaking-change-doc-created. Produces two markdown files + (issue-draft.md and pr-comment.md) and optionally comments on the PR. + +concurrency: + group: "breaking-change-doc-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}" + cancel-in-progress: true + +permissions: + contents: read + pull-requests: read + issues: read + +tools: + bash: ["pwsh", "gh"] + +safe-outputs: + add-comment: + target: "*" + noop: + report-as-issue: false # Disable posting noop messages as issue comments + +if: | + github.event_name == 'workflow_dispatch' || + ( + !github.event.repository.fork && + github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'needs-breaking-change-doc-created') + ) + +post-steps: + - name: Upload breaking change drafts + if: always() + uses: actions/upload-artifact@v4 + with: + name: breaking-change-docs + path: artifacts/docs/breakingChanges/ + retention-days: 30 + if-no-files-found: ignore + +on: + pull_request_target: + types: [closed, labeled] + workflow_dispatch: + inputs: + pr_number: + description: "Pull Request Number" + required: true + type: string + suppress_output: + description: "Suppress workflow output (dry-run — only produce markdown workflow artifacts)" + required: false + type: boolean + default: false +# ############################################################### +# Override the COPILOT_GITHUB_TOKEN secret usage for the workflow +# with a randomly-selected token from a pool of secrets. +# +# As soon as organization-level billing is offered for Agentic +# Workflows, this stop-gap approach will be removed. +# +# See: /.github/actions/select-copilot-pat/README.md +# ############################################################### + + # Add the pre-activation step of selecting a random PAT from the supplied secrets + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + name: Checkout the select-copilot-pat action folder + with: + persist-credentials: false + sparse-checkout: .github/actions/select-copilot-pat + sparse-checkout-cone-mode: true + fetch-depth: 1 + + - id: select-copilot-pat + name: Select Copilot token from pool + uses: ./.github/actions/select-copilot-pat + env: + SECRET_0: ${{ secrets.COPILOT_PAT_0 }} + SECRET_1: ${{ secrets.COPILOT_PAT_1 }} + SECRET_2: ${{ secrets.COPILOT_PAT_2 }} + SECRET_3: ${{ secrets.COPILOT_PAT_3 }} + SECRET_4: ${{ secrets.COPILOT_PAT_4 }} + SECRET_5: ${{ secrets.COPILOT_PAT_5 }} + SECRET_6: ${{ secrets.COPILOT_PAT_6 }} + SECRET_7: ${{ secrets.COPILOT_PAT_7 }} + SECRET_8: ${{ secrets.COPILOT_PAT_8 }} + SECRET_9: ${{ secrets.COPILOT_PAT_9 }} + +# Add the pre-activation output of the randomly selected PAT +jobs: + pre-activation: + outputs: + copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} + +# Override the COPILOT_GITHUB_TOKEN expression used in the activation job +# Consume the PAT number from the pre-activation step and select the corresponding secret +engine: + id: copilot + env: + # We cannot use line breaks in this expression as it leads to a syntax error in the compiled workflow + # If none of the `COPILOT_PAT_#` secrets were selected, then the default COPILOT_GITHUB_TOKEN is used + COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} +--- + +# Breaking Change Documentation + +Create breaking change documentation for the pull request identified below. + +## PR to document + +- If triggered by a pull request event, the PR number is `${{ github.event.pull_request.number }}`. +- If triggered by `workflow_dispatch`, the PR number is `${{ github.event.inputs.pr_number }}`. + +## Dry-run mode + +- If triggered by `workflow_dispatch` with `suppress_output` = `true`, + **do not** post a comment on the PR after producing the files. Just + write the markdown files and stop. +- For pull_request triggers, always post the comment. + +## Instructions + +Using the breaking-change-doc skill from +`.github/skills/breaking-change-doc/SKILL.md`, execute **all steps (0 through +6)** for the PR above. + +In Step 6, if dry-run mode is active, skip publishing any output to the pull +request. The generated files in `artifacts/docs/breakingChanges/` are +automatically uploaded as a workflow artifact named **breaking-change-docs** +and can be downloaded from the workflow run summary page. + +## When no action is needed + +If no action is needed (PR has no area label, documentation already exists, +etc.), you MUST call the `noop` tool with a message explaining why: + +```json +{"noop": {"message": "No action needed: [brief explanation]"}} +``` diff --git a/.github/workflows/breaking-change-doc.yml b/.github/workflows/breaking-change-doc.yml deleted file mode 100644 index b115c38e729f30..00000000000000 --- a/.github/workflows/breaking-change-doc.yml +++ /dev/null @@ -1,73 +0,0 @@ -# This workflow generates breaking change documentation for merged pull requests. -# It runs automatically when a PR with the 'needs-breaking-change-doc-created' label is merged, -# or when that label is added to an already merged PR. -# It can be manually triggered to generate documentation for any specific PR. -# -# The workflow uses GitHub Models AI to analyze the PR changes and create appropriate -# breaking change documentation that gets posted as a PR comment as a clickable link -# to open an issue in the dotnet/docs repository. -name: Breaking Change Documentation - -on: - pull_request_target: - types: [closed, labeled] - workflow_dispatch: - inputs: - pr_number: - description: "Pull Request Number" - required: true - type: number - -permissions: - contents: read - pull-requests: write - models: read - -jobs: - generate-breaking-change-doc: - if: | - github.repository_owner == 'dotnet' && ( - (github.event_name == 'pull_request_target' && github.event.action == 'closed' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-breaking-change-doc-created')) || - (github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.pull_request.merged == true && github.event.label.name == 'needs-breaking-change-doc-created') || - github.event_name == 'workflow_dispatch' - ) - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 # Need full history for version detection - - - name: Verify PowerShell - run: | - pwsh --version - - - name: Verify GitHub CLI - run: | - gh --version - - - name: Install GitHub Models extension - run: | - gh extension install github/gh-models --force - env: - GH_TOKEN: ${{ github.token }} - - - name: Fetch latest tags - run: | - git fetch --tags --force - - - name: Run breaking change documentation script - shell: pwsh - working-directory: eng/breakingChanges - run: ./breaking-change-doc.ps1 -PrNumber ${{ inputs.pr_number || github.event.pull_request.number }} -Comment - env: - GH_TOKEN: ${{ github.token }} - GITHUB_MODELS_API_KEY: ${{ secrets.MODELS_TOKEN }} - - - name: Upload artifacts - uses: actions/upload-artifact@v7 - with: - name: breaking-change-doc-artifacts-${{ inputs.pr_number || github.event.pull_request.number }} - path: artifacts/docs/breakingChanges/ - retention-days: 7 diff --git a/eng/breakingChanges/README.md b/eng/breakingChanges/README.md deleted file mode 100644 index aa691584608d29..00000000000000 --- a/eng/breakingChanges/README.md +++ /dev/null @@ -1,172 +0,0 @@ -# Breaking Change Documentation Automation - -This script automates the creation of high-quality breaking change documentation for .NET runtime PRs using AI-powered analysis. - -## Key Features - -- **GitHub Models Integration**: Uses GitHub's AI models (no API keys required) with fallback to other providers -- **Dynamic Template Fetching**: Automatically fetches the latest breaking change issue template from dotnet/docs -- **Example-Based Learning**: Analyzes recent breaking change issues to improve content quality -- **Version Detection**: Analyzes GitHub tags to determine accurate .NET version information for proper milestone assignment -- **Flexible Workflow**: Multiple execution modes (CollectOnly, Comment, CreateIssues) with analysis-only default -- **Comprehensive Data Collection**: Gathers PR details, related issues, merge commits, review comments, and closing issues -- **Area Label Detection**: Automatically detects feature areas from GitHub labels (area-*) with file path fallback -- **Individual File Output**: Creates separate JSON files per PR for easy examination - -## Quick Setup - -1. **Install Prerequisites:** - - GitHub CLI: `gh auth login` - - Choose LLM provider: - - **GitHub Models** (recommended): `gh extension install github/gh-models` - - **OpenAI**: Set `$env:OPENAI_API_KEY = "your-key"` - - **Others**: See configuration section below - -2. **Configure:** - ```powershell - # Edit config.ps1 to set: - # - LlmProvider = "github-models" (or other provider) - ``` - -3. **Run the workflow:** - ```powershell - .\breaking-change-doc.ps1 -Help - ``` - -4. **Choose your workflow:** - ```powershell - # Default: Analysis only (generates drafts without making GitHub changes) - .\breaking-change-doc.ps1 -PrNumber 123456 - - # Add comments with create issue links - .\breaking-change-doc.ps1 -PrNumber 123456 -Comment - - # Create issues directly - .\breaking-change-doc.ps1 -PrNumber 123456 -CreateIssues - - # Just collect data - .\breaking-change-doc.ps1 -PrNumber 123456 -CollectOnly - ``` - -## Commands - -```powershell -# Help (shows all parameters and examples) -.\breaking-change-doc.ps1 -Help - -# Default workflow (analysis only - generates drafts) -.\breaking-change-doc.ps1 -PrNumber 123456 - -# Add comments with issue creation links -.\breaking-change-doc.ps1 -PrNumber 123456 -Comment - -# Create issues directly -.\breaking-change-doc.ps1 -PrNumber 123456 -CreateIssues - -# Data collection only -.\breaking-change-doc.ps1 -PrNumber 123456 -CollectOnly - -# Query multiple PRs -.\breaking-change-doc.ps1 -Query "repo:dotnet/runtime state:closed label:needs-breaking-change-doc-created is:merged" - -# Clean previous data -.\breaking-change-doc.ps1 -Clean - -# Clean and process -.\breaking-change-doc.ps1 -Clean -PrNumber 123456 -``` - -## Configuration - -Edit `config.ps1` to customize: -- **LLM provider**: GitHub Models, OpenAI, Anthropic, Azure OpenAI -- **Search parameters**: Date ranges, labels, excluded milestones -- **Output settings**: Labels, assignees, notification emails - -## LLM Providers - -**GitHub Models** (recommended - no API key needed): -```powershell -gh extension install github/gh-models -# Set provider in config.ps1: LlmProvider = "github-models" -``` - -**OpenAI**: -```powershell -$env:OPENAI_API_KEY = "your-key" -# Set provider in config.ps1: LlmProvider = "openai" -``` - -**Anthropic Claude**: -```powershell -$env:ANTHROPIC_API_KEY = "your-key" -# Set provider in config.ps1: LlmProvider = "anthropic" -``` - -**Azure OpenAI**: -```powershell -$env:AZURE_OPENAI_API_KEY = "your-key" -# Configure endpoint in config.ps1: LlmProvider = "azure-openai" -``` - -## Output - -- **Data Collection**: `(repoRoot)\artifacts\docs\breakingChanges\data\summary_report.md`, `(repoRoot)\artifacts\docs\breakingChanges\data\pr_*.json` -- **Issue Drafts**: `(repoRoot)\artifacts\docs\breakingChanges\issue-drafts\*.md` -- **Comment Drafts**: `(repoRoot)\artifacts\docs\breakingChanges\comment-drafts\*.md` -- **GitHub Issues**: Created automatically when using -CreateIssues -- **GitHub Comments**: Added to PRs when using -Comment - -## Workflow Steps - -1. **Fetch PRs** - Downloads PR data from dotnet/runtime with comprehensive details -2. **Version Detection** - Analyzes GitHub tags to determine accurate .NET version information -3. **Template & Examples** - Fetches latest issue template and analyzes recent breaking change issues -3. **AI Analysis** - Generates high-quality breaking change documentation using AI -4. **Output Generation** - Creates issue drafts and comment drafts for review -5. **Optional Actions** - Adds comments with issue creation links (-Comment) or creates issues directly (-CreateIssues) - -## Version Detection - -The script automatically determines accurate .NET version information using the local git repository: -- **Fast and reliable**: Uses `git describe` commands on the repository -- **No API rate limits**: Avoids GitHub API calls for version detection -- **Accurate timing**: Analyzes actual commit ancestry and tag relationships -- **Merge commit analysis**: For merged PRs, finds the exact merge commit and determines version context -- **Branch-aware**: For unmerged PRs, uses target branch information - -## Manual Review - -AI generates 90%+ ready documentation, but review for: -- Technical accuracy -- API completeness -- Edge cases - -## Cleanup - -Between runs: -```powershell -.\breaking-change-doc.ps1 -Clean -``` - -## Parameters - -| Parameter | Description | Example | -|-----------|-------------|---------| -| `-Help` | Show help and parameter information | `.\breaking-change-doc.ps1 -Help` | -| `-PrNumber` | Process a specific PR number | `.\breaking-change-doc.ps1 -PrNumber 123456` | -| `-Query` | GitHub search query for multiple PRs | `.\breaking-change-doc.ps1 -Query "repo:dotnet/runtime state:closed label:needs-breaking-change-doc-created is:merged"` | -| `-CollectOnly` | Only collect PR data, don't generate documentation | `.\breaking-change-doc.ps1 -PrNumber 123456 -CollectOnly` | -| `-Comment` | Add comments to PRs with issue creation links | `.\breaking-change-doc.ps1 -PrNumber 123456 -Comment` | -| `-CreateIssues` | Create GitHub issues directly | `.\breaking-change-doc.ps1 -PrNumber 123456 -CreateIssues` | -| `-Clean` | Clean previous data before starting | `.\breaking-change-doc.ps1 -Clean` | - -**Note**: Either `-PrNumber` or `-Query` must be specified (unless using `-Clean` or `-Help` alone). - -## Troubleshooting - -**GitHub CLI**: `gh auth status` and `gh auth login` -**API Keys**: Verify environment variables are set for non-GitHub Models providers -**Rate Limits**: Script includes delays between API calls -**Git Operations**: Ensure git is in PATH and repository is up to date (`git fetch --tags`) -**Parameter Issues**: Use `-Help` to see current parameter list and examples diff --git a/eng/breakingChanges/breaking-change-doc.ps1 b/eng/breakingChanges/breaking-change-doc.ps1 deleted file mode 100644 index d793c597a51345..00000000000000 --- a/eng/breakingChanges/breaking-change-doc.ps1 +++ /dev/null @@ -1,1231 +0,0 @@ -# Breaking Change Documentation Tool - All-in-One Script -# This script automates the creation of breaking change documentation for .NET runtime PRs -# Combines all functionality into a single, easy-to-use script - -param( - [switch]$CollectOnly = $false, # Only collect PR data, don't create issues - [switch]$CreateIssues = $false, # Create GitHub issues directly - [switch]$Comment = $false, # Add comments with links to create issues - [switch]$Clean = $false, # Clean previous data before starting - [string]$PrNumber = $null, # Process only specific PR number - [string]$Query = $null, # GitHub search query for PRs - [switch]$Help = $false # Show help -) - -# Show help -if ($Help) { - Write-Host @" -Breaking Change Documentation Workflow - -DESCRIPTION: - Automates the creation of high-quality breaking change documentation - for .NET runtime PRs using an LLM to analyze and author docs. - - DEFAULT BEHAVIOR: Analyzes PRs and generates documentation drafts without - making any changes to GitHub. Use -CreateIssues or -Comment to execute actions. - -USAGE: - .\breaking-change-doc.ps1 [parameters] - -PARAMETERS: - -CollectOnly Only collect PR data, don't create documentation - -CreateIssues Create GitHub issues directly - -Comment Add comments with links to create issues - -Clean Clean previous data before starting - -PrNumber Process only specific PR number - -Query GitHub search query for PRs (required if no -PrNumber) - -Help Show this help - -EXAMPLES: - .\breaking-change-doc.ps1 -PrNumber 114929 # Process specific PR - .\breaking-change-doc.ps1 -Query "state:closed label:needs-breaking-change-doc-created is:merged merged:>2024-09-16 -milestone:11.0.0" - .\breaking-change-doc.ps1 -Query "state:closed label:needs-breaking-change-doc-created is:merged" -Comment - .\breaking-change-doc.ps1 -PrNumber 114929 -CreateIssues # Create issues directly - .\breaking-change-doc.ps1 -Query "your-search-query" -CollectOnly # Only collect data - -QUERY EXAMPLES: - # PRs merged after specific date, excluding milestone: - "state:closed label:needs-breaking-change-doc-created is:merged merged:>2024-09-16 -milestone:11.0.0" - - # All PRs with the target label: - "state:closed label:needs-breaking-change-doc-created is:merged" - - # PRs from specific author: - "state:closed label:needs-breaking-change-doc-created is:merged author:username" - -SETUP: - 1. Install GitHub CLI and authenticate: gh auth login - 2. Choose LLM provider: - - For GitHub Models: gh extension install github/gh-models (optional: set GITHUB_MODELS_API_KEY) - - For GitHub Copilot: Install GitHub Copilot CLI from https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli (optional: set GITHUB_COPILOT_API_KEY) - - For OpenAI: `$env:OPENAI_API_KEY = "your-key" - - For Azure OpenAI: `$env:AZURE_OPENAI_API_KEY = "your-key" and set LlmBaseUrl in config.ps1 - - For others: Set appropriate API key - 3. Edit config.ps1 to customize settings -"@ - exit 0 -} - -Write-Host "🤖 Breaking Change Documentation Tool" -ForegroundColor Cyan -Write-Host "====================================" -ForegroundColor Cyan - -# Load configuration -if (Test-Path ".\config.ps1") { - . ".\config.ps1" -} else { - Write-Error "config.ps1 not found. Please create configuration file." - exit 1 -} - -# Ensure powershell-yaml module is available for GitHub Models -if ($Config.LlmProvider -eq "github-models") { - if (-not (Get-Module -ListAvailable -Name "powershell-yaml")) { - Write-Host "📦 Installing powershell-yaml module for GitHub Models support..." -ForegroundColor Yellow - try { - Install-Module -Name "powershell-yaml" -Scope CurrentUser -Force -AllowClobber - Write-Host "✅ powershell-yaml module installed successfully" -ForegroundColor Green - } - catch { - Write-Error "❌ Failed to install powershell-yaml module: $($_.Exception.Message)" - Write-Error " Please install manually: Install-Module -Name powershell-yaml -Scope CurrentUser" - exit 1 - } - } - - # Import the module - Import-Module powershell-yaml -ErrorAction Stop -} - -# Validate prerequisites -Write-Host "`n🔍 Validating prerequisites..." -ForegroundColor Yellow - -# Check GitHub CLI -if (-not (Get-Command "gh" -ErrorAction SilentlyContinue)) { - Write-Error "❌ GitHub CLI not found. Install from https://cli.github.com/" - exit 1 -} - -try { - gh auth status | Out-Null - if ($LASTEXITCODE -ne 0) { - Write-Error "❌ GitHub CLI not authenticated. Run 'gh auth login'" - exit 1 - } - Write-Host "✅ GitHub CLI authenticated" -ForegroundColor Green -} catch { - Write-Error "❌ GitHub CLI error: $($_.Exception.Message)" - exit 1 -} - -# Check LLM API key or GitHub CLI for GitHub Models/Copilot -$llmProvider = $Config.LlmProvider -$apiKey = switch ($llmProvider) { - "openai" { $env:OPENAI_API_KEY } - "anthropic" { $env:ANTHROPIC_API_KEY } - "azure-openai" { $env:AZURE_OPENAI_API_KEY } - "github-models" { $env:GITHUB_MODELS_API_KEY } # Optional API key for GitHub Models - "github-copilot" { $env:GITHUB_COPILOT_API_KEY } # Optional API key for GitHub Copilot CLI - default { $env:OPENAI_API_KEY } -} - -if ($llmProvider -eq "github-models") { - # Check if gh-models extension is installed - try { - $modelsExtension = gh extension list 2>$null | Select-String "gh models" - if (-not $modelsExtension) { - Write-Error "❌ GitHub Models extension not found. Install with: gh extension install github/gh-models" - exit 1 - } - Write-Host "✅ GitHub Models extension found" -ForegroundColor Green - } catch { - Write-Error "❌ Could not check GitHub Models extension: $($_.Exception.Message)" - exit 1 - } -} elseif ($llmProvider -eq "github-copilot") { - # Check if standalone GitHub Copilot CLI is installed - try { - $copilotVersion = copilot --version 2>$null - if (-not $copilotVersion -or $LASTEXITCODE -ne 0) { - Write-Error "❌ GitHub Copilot CLI not found. Install from: https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli" - exit 1 - } - Write-Host "✅ GitHub Copilot CLI found (version: $($copilotVersion.Split("`n")[0]))" -ForegroundColor Green - } catch { - Write-Error "❌ Could not check GitHub Copilot CLI: $($_.Exception.Message)" - Write-Error " Install from: https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli" - exit 1 - } -} elseif (-not $apiKey) { - Write-Error "❌ No LLM API key found. Set environment variable:" - Write-Host " For OpenAI: `$env:OPENAI_API_KEY = 'your-key'" - Write-Host " For Anthropic: `$env:ANTHROPIC_API_KEY = 'your-key'" - Write-Host " For Azure OpenAI: `$env:AZURE_OPENAI_API_KEY = 'your-key'" - Write-Host " For GitHub Models: Use 'github-models' provider (no key needed, or set GITHUB_MODELS_API_KEY for different account)" - Write-Host " For GitHub Copilot: Install GitHub Copilot CLI from https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli (no key needed, or set GITHUB_COPILOT_API_KEY for different account)" - exit 1 -} else { - Write-Host "✅ LLM API key found ($llmProvider)" -ForegroundColor Green -} - -# Determine repository root and set up output paths -$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path -$repoRoot = Split-Path -Parent (Split-Path -Parent $scriptPath) -$outputRoot = Join-Path $repoRoot "artifacts\docs\breakingChanges" - -# Define output directories -$dataDir = Join-Path $outputRoot "data" -$issueDraftsDir = Join-Path $outputRoot "issue-drafts" -$commentDraftsDir = Join-Path $outputRoot "comment-drafts" -$promptsDir = Join-Path $outputRoot "prompts" - -if ($Clean) { - Write-Host "`n🧹 Cleaning previous data..." -ForegroundColor Yellow - if (Test-Path $outputRoot) { Remove-Item $outputRoot -Recurse -Force } - Write-Host "✅ Cleanup completed" -ForegroundColor Green - - if (-not $PrNumber -and -not $Query) { - exit 0 - } -} - -New-Item -ItemType Directory -Path $dataDir -Force | Out-Null -New-Item -ItemType Directory -Path $issueDraftsDir -Force | Out-Null -New-Item -ItemType Directory -Path $commentDraftsDir -Force | Out-Null -New-Item -ItemType Directory -Path $promptsDir -Force | Out-Null - -# Validate parameters -if (-not $PrNumber -and -not $Query) { - Write-Error @" -❌ Either -PrNumber or -Query must be specified. - -EXAMPLES: - Process specific PR: - .\breaking-change-doc.ps1 -PrNumber 114929 - - Query for PRs (example - customize as needed): - .\breaking-change-doc.ps1 -Query "state:closed label:needs-breaking-change-doc-created is:merged merged:>2024-09-16 -milestone:11.0.0" - -Use -Help for more examples and detailed usage information. -"@ - exit 1 -} - -if ($PrNumber -and $Query) { - Write-Error "❌ Cannot specify both -PrNumber and -Query. Choose one." - exit 1 -} - -# Determine action mode - default to analysis only if no action specified -$executeActions = $CreateIssues -or $Comment - -# Validate parameter combinations -if (($CreateIssues -and $Comment) -or ($CollectOnly -and ($CreateIssues -or $Comment))) { - Write-Error "❌ Cannot combine -CollectOnly, -CreateIssues, and -Comment. Choose one action mode." - exit 1 -} - -$actionMode = if ($CollectOnly) { "Collect Only" } - elseif ($CreateIssues) { "Create Issues" } - elseif ($Comment) { "Add Comments" } - else { "Analysis Only" } - -Write-Host " Action Mode: $actionMode" -ForegroundColor Cyan -if (-not $executeActions -and -not $CollectOnly) { - Write-Host " 📝 Will generate drafts without making changes to GitHub" -ForegroundColor Yellow -} - -# Function to safely truncate text -function Limit-Text { - param([string]$text, [int]$maxLength = 2000) - - if (-not $text -or $text.Length -le $maxLength) { - return $text - } - - $truncated = $text.Substring(0, $maxLength) - $lastPeriod = $truncated.LastIndexOf('.') - $lastNewline = $truncated.LastIndexOf("`n") - - $cutPoint = [Math]::Max($lastPeriod, $lastNewline) - if ($cutPoint -gt ($maxLength * 0.8)) { - $truncated = $truncated.Substring(0, $cutPoint + 1) - } - - return $truncated + "`n`n[Content truncated for length]" -} - -# Function to execute a script block with a temporary GITHUB_TOKEN -function Enter-GitHubSession { - param([string]$ApiKey) - - # Store original token - $originalGitHubToken = $env:GH_TOKEN - - if ($ApiKey) { - # Set temporary token - $env:GH_TOKEN = $ApiKey - } - - return $originalGitHubToken -} - -function Exit-GitHubSession { - param([string]$OriginalGitHubToken) - - # Restore original token - if ($OriginalGitHubToken) { - $env:GH_TOKEN = $OriginalGitHubToken - } else { - Remove-Item env:GH_TOKEN -ErrorAction SilentlyContinue - } -} - -# Function to fetch issue template from GitHub -function Get-IssueTemplate { - try { - Write-Host " 📋 Fetching issue template..." -ForegroundColor DarkGray - # Use public GitHub API (no auth required for public repos) - $response = Invoke-RestMethod -Uri "https://api.github.com/repos/$($Config.DocsRepo)/contents/$($Config.IssueTemplatePath)" -Headers @{ 'User-Agent' = 'dotnet-runtime-breaking-change-tool' } - $templateContent = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($response.content)) - return $templateContent - } - catch { - Write-Error "❌ Failed to fetch issue template from $($Config.DocsRepo)/$($Config.IssueTemplatePath): $($_.Exception.Message)" - Write-Error " Template is required for high-quality documentation generation. Please check repository access and template path." - exit 1 - } -} - -# Function to fetch example breaking change issues -function Get-ExampleBreakingChangeIssues { - try { - Write-Host " 📚 Fetching example breaking change issues..." -ForegroundColor DarkGray - # Use public GitHub API for issues - $response = Invoke-RestMethod -Uri "https://api.github.com/repos/$($Config.DocsRepo)/issues?labels=breaking-change&state=all&per_page=3" -Headers @{ 'User-Agent' = 'dotnet-runtime-breaking-change-tool' } - - if ($response.Count -eq 0) { - Write-Error "❌ No example breaking change issues found in $($Config.DocsRepo) with label 'breaking-change'" - Write-Error " Examples are required for high-quality documentation generation. Please check repository and label." - exit 1 - } - - $examples = @() - foreach ($issue in $response) { - $examples += @" -**Example #$($issue.number)**: $($issue.title) -URL: $($issue.html_url) -Body: $(Limit-Text -text $issue.body -maxLength 800) -"@ - } - return $examples -join "`n`n---`n`n" - } - catch { - Write-Error "❌ Failed to fetch example breaking change issues from $($Config.DocsRepo): $($_.Exception.Message)" - Write-Error " Examples are required for high-quality documentation generation. Please check repository access." - exit 1 - } -} - -# Function to parse a .NET runtime tag into its components -function ConvertFrom-DotNetTag { - param([string]$tagName) - - if (-not $tagName -or $tagName -eq "Unknown") { - return $null - } - - # Parse v(major).(minor).(build)(-prerelease) - if ($tagName -match '^v(\d+)\.(\d+)\.(\d+)(?:-(.+))?$') { - $major = [int]$matches[1] - $minor = [int]$matches[2] - $build = [int]$matches[3] - $prerelease = if ($matches[4]) { $matches[4] } else { $null } - - # Parse prerelease into type and number using single regex - $prereleaseType = $null - $prereleaseNumber = $null - - if ($prerelease -and $prerelease -match '^([a-zA-Z]+)\.(\d+)') { - $rawType = $matches[1] - $prereleaseNumber = [int]$matches[2] - - # Normalize prerelease type casing - if ($rawType -ieq "rc") { - $prereleaseType = "RC" - } else { - # Capitalize first letter for other types - $prereleaseType = $rawType.Substring(0,1).ToUpper() + $rawType.Substring(1).ToLower() - } - } - - return @{ - Major = $major - Minor = $minor - Build = $build - Prerelease = $prerelease - PrereleaseType = $prereleaseType - PrereleaseNumber = $prereleaseNumber - IsRelease = $null -eq $prerelease - } - } - - return $null -} - -# Function to format a parsed tag as a readable .NET version -function Format-DotNetVersion { - param($parsedTag) - - if (-not $parsedTag) { - return "Next release" - } - - $baseVersion = ".NET $($parsedTag.Major).$($parsedTag.Minor)" - - if ($parsedTag.IsRelease) { - return $baseVersion - } - - if ($parsedTag.PrereleaseType -and $parsedTag.PrereleaseNumber) { - return "$baseVersion $($parsedTag.PrereleaseType) $($parsedTag.PrereleaseNumber)" - } - - # Fallback for unknown prerelease formats - return "$baseVersion ($($parsedTag.Prerelease))" -} - -# Function to estimate the next version based on current tag and branch -function Get-EstimatedNextVersion { - param($parsedTag, [string]$baseRef) - - if (-not $parsedTag) { - return "Next release" - } - - $isMainBranch = $baseRef -eq "main" - - # If this is a release version - if ($parsedTag.IsRelease) { - if ($isMainBranch) { - # Assume changes to main when last tag is release go to next release. - $nextMajor = $parsedTag.Major + 1 - return ".NET $nextMajor.0 Preview 1" - } else { - # Next patch/build - return ".NET $($parsedTag.Major).$($parsedTag.Minor)" - } - } - - # If this is a prerelease version - if ($isMainBranch -and $parsedTag.PrereleaseType -eq "RC") { - # Assume changes to main when last tag is RC go to next release. - $nextMajor = $parsedTag.Major + 1 - return ".NET $nextMajor.0 Preview 1" - } else { - # Next preview - $nextPreview = $parsedTag.PrereleaseNumber + 1 - return ".NET $($parsedTag.Major).$($parsedTag.Minor) $($parsedTag.PrereleaseType) $nextPreview" - } -} - -# Function to find the closest tag by commit distance -function Find-ClosestTagByDistance { - param([string]$targetCommit, [int]$maxTags = 10) - - $recentTags = git tag --sort=-version:refname 2>$null | Select-Object -First $maxTags - $closestTag = $null - $minDistance = [int]::MaxValue - - foreach ($tag in $recentTags) { - # Check if this tag contains the target commit (skip if it does for merged PRs) - if ($targetCommit -match '^[a-f0-9]{40}$') { - # This is a commit hash, check if tag contains it - git merge-base --is-ancestor $targetCommit $tag 2>$null - if ($LASTEXITCODE -eq 0) { - # This tag contains our commit, skip it - continue - } - } - - # Calculate commit distance between tag and target - $distance = git rev-list --count "$tag..$targetCommit" 2>$null - if ($LASTEXITCODE -eq 0 -and $distance -match '^\d+$') { - $distanceNum = [int]$distance - if ($distanceNum -lt $minDistance) { - $minDistance = $distanceNum - $closestTag = $tag - } - } - } - - return $closestTag -} - -# Function to get version information using local git repository -function Get-VersionInfo { - param([string]$prNumber, [string]$mergedAt, [string]$baseRef = "main") - - try { - # Change to the repository directory (we're already in the repo) - Push-Location $repoRoot - - try { - # Ensure we have latest info - git fetch --tags 2>$null | Out-Null - - # Determine the target commit for version analysis - $targetCommit = $null - $firstTagWith = "Not yet released" - - if ($prNumber -and $mergedAt) { - # For merged PRs, try to get the merge commit - $targetCommit = gh pr view $prNumber --repo $Config.SourceRepo --json mergeCommit --jq '.mergeCommit.oid' 2>$null - - if ($targetCommit) { - # Get the first tag that includes this commit - $firstTagWith = git describe --tags --contains $targetCommit 2>$null - if ($firstTagWith -and $firstTagWith -match '^([^~^]+)') { - $firstTagWith = $matches[1] - } - } - } - - # If no target commit yet (unmerged PR or failed to get merge commit), use branch head - if (-not $targetCommit) { - $targetCommit = git rev-parse "origin/$baseRef" 2>$null - } - - # Find the last tag before this commit - $lastTagBefore = "Unknown" - if ($targetCommit) { - $closestTag = Find-ClosestTagByDistance -targetCommit $targetCommit - if ($closestTag) { - $lastTagBefore = $closestTag - } else { - # Fallback strategies - if ($baseRef -eq "main") { - # Try git describe on the target branch - $lastTagBefore = git describe --tags --abbrev=0 "origin/$baseRef" 2>$null - if (-not $lastTagBefore) { - # Final fallback: most recent tag overall - $lastTagBefore = git tag --sort=-version:refname | Select-Object -First 1 2>$null - } - } else { - $lastTagBefore = git describe --tags --abbrev=0 "origin/$baseRef" 2>$null - } - } - } - - # Clean up tag names and estimate version - $lastTagBefore = if ($lastTagBefore) { $lastTagBefore.Trim() } else { "Unknown" } - $firstTagWith = if ($firstTagWith -and $firstTagWith -ne "Not yet released") { $firstTagWith.Trim() } else { "Not yet released" } - - # Determine the estimated version using new tag parsing logic - $estimatedVersion = "Next release" - - if ($firstTagWith -ne "Not yet released") { - # If we know the first tag that contains this change, use it directly - $parsedFirstTag = ConvertFrom-DotNetTag $firstTagWith - $estimatedVersion = Format-DotNetVersion $parsedFirstTag - } else { - # Estimate based on the last tag before this change - $parsedLastTag = ConvertFrom-DotNetTag $lastTagBefore - $estimatedVersion = Get-EstimatedNextVersion $parsedLastTag $baseRef - } - - return @{ - LastTagBeforeMerge = $lastTagBefore - FirstTagWithChange = $firstTagWith - EstimatedVersion = $estimatedVersion - } - } - finally { - Pop-Location - } - } - catch { - Write-Warning "Could not get version information using git: $($_.Exception.Message)" - return @{ - LastTagBeforeMerge = "Unknown" - FirstTagWithChange = "Not yet released" - EstimatedVersion = "Next release" - } - } -} - -# Function to call LLM API -function Invoke-LlmApi { - param([string]$Prompt, [string]$SystemPrompt = "", [int]$MaxTokens = 3000, [string]$PrNumber = "unknown") - - switch ($Config.LlmProvider) { - "github-models" { - # Use GitHub CLI with models extension - try { - # Create prompt file in YAML format for GitHub Models - $promptFile = Join-Path $promptsDir "pr_${PrNumber}_prompt.yml" - - # Create YAML structure for GitHub Models - $messages = @() - - if ($SystemPrompt) { - $messages += @{ - role = "system" - content = $SystemPrompt - } - } - - $messages += @{ - role = "user" - content = $Prompt - } - - $promptYaml = @{ - name = "Breaking Change Documentation" - description = "Generate breaking change documentation for .NET runtime PR" - model = $Config.LlmModel - modelParameters = @{ - temperature = 0.1 - max_tokens = $MaxTokens - } - messages = $messages - } - - # Convert to YAML and save to file - $promptYaml | ConvertTo-Yaml | Out-File -FilePath $promptFile -Encoding UTF8 - - try { - $gitHubSession = Enter-GitHubSession $apiKey - $output = gh models run --file $promptFile - $exitCode = $LASTEXITCODE - } finally { - Exit-GitHubSession $gitHubSession - } - - if ($exitCode -ne 0) { - throw "gh models run failed with exit code $exitCode" - } - - # Join the output lines with newlines to preserve formatting - return $output -join "`n" - } - catch { - Write-Error "GitHub Models API call failed: $($_.Exception.Message)" - return $null - } - } - "github-copilot" { - # Use GitHub Copilot CLI in programmatic mode - try { - # Create prompt file for GitHub Copilot CLI - $promptFile = Join-Path $promptsDir "pr_${PrNumber}_copilot_prompt.txt" - - # Combine system prompt and user prompt, emphasizing text-only response - $fullPrompt = if ($SystemPrompt) { - "$SystemPrompt`n`nIMPORTANT: Please respond with only the requested text content. Do not create, modify, or execute any files. Just return the text response.`n`n$Prompt" - } else { - "IMPORTANT: Please respond with only the requested text content. Do not create, modify, or execute any files. Just return the text response.`n`n$Prompt" - } - - # Write prompt to file - $fullPrompt | Out-File -FilePath $promptFile -Encoding UTF8 - - try { - $gitHubSession = Enter-GitHubSession $apiKey - # Add --allow-all-tools for non-interactive mode and --allow-all-paths to avoid file access prompts - $rawResponse = copilot -p "@$promptFile" --log-level none --allow-all-tools --allow-all-paths - } finally { - Exit-GitHubSession $gitHubSession - } - - # Parse the response to extract just the content, removing usage statistics - # The response format typically includes usage stats at the end starting with "Total usage est:" - $lines = $rawResponse -split "`n" - $contentLines = @() - $foundUsageStats = $false - - foreach ($line in $lines) { - if ($line -match "^Total usage est:" -or $line -match "^Total duration") { - $foundUsageStats = $true - break - } - if (-not $foundUsageStats) { - $contentLines += $line - } - } - - # Join the content lines and trim whitespace - $response = ($contentLines -join "`n").Trim() - return $response - } - catch { - Write-Error "GitHub Copilot CLI call failed: $($_.Exception.Message)" - return $null - } - } - "openai" { - # OpenAI API - $endpoint = if ($Config.LlmBaseUrl) { "$($Config.LlmBaseUrl)/chat/completions" } else { "https://api.openai.com/v1/chat/completions" } - $headers = @{ - 'Content-Type' = 'application/json' - 'Authorization' = "Bearer $apiKey" - } - - $messages = @() - if ($SystemPrompt) { $messages += @{ role = "system"; content = $SystemPrompt } } - $messages += @{ role = "user"; content = $Prompt } - - $body = @{ - model = $Config.LlmModel - messages = $messages - max_tokens = $MaxTokens - temperature = 0.1 - } - - try { - $requestJson = $body | ConvertTo-Json -Depth 10 - $response = Invoke-RestMethod -Uri $endpoint -Method POST -Headers $headers -Body $requestJson - return $response.choices[0].message.content - } - catch { - Write-Error "OpenAI API call failed: $($_.Exception.Message)" - return $null - } - } - "anthropic" { - # Anthropic API - $endpoint = if ($Config.LlmBaseUrl) { "$($Config.LlmBaseUrl)/messages" } else { "https://api.anthropic.com/v1/messages" } - $headers = @{ - 'Content-Type' = 'application/json' - 'x-api-key' = $apiKey - 'anthropic-version' = "2023-06-01" - } - - $fullPrompt = if ($SystemPrompt) { "$SystemPrompt`n`nHuman: $Prompt`n`nAssistant:" } else { "Human: $Prompt`n`nAssistant:" } - - $body = @{ - model = $Config.LlmModel - max_tokens = $MaxTokens - messages = @(@{ role = "user"; content = $fullPrompt }) - temperature = 0.1 - } - - try { - $requestJson = $body | ConvertTo-Json -Depth 10 - $response = Invoke-RestMethod -Uri $endpoint -Method POST -Headers $headers -Body $requestJson - return $response.content[0].text - } - catch { - Write-Error "Anthropic API call failed: $($_.Exception.Message)" - return $null - } - } - "azure-openai" { - # Azure OpenAI API - # Endpoint format: https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version={api-version} - if (-not $Config.LlmBaseUrl) { - Write-Error "Azure OpenAI requires LlmBaseUrl to be set in config (e.g., 'https://your-resource.openai.azure.com')" - return $null - } - - $apiVersion = if ($Config.AzureApiVersion) { $Config.AzureApiVersion } else { "2024-02-15-preview" } - $endpoint = "$($Config.LlmBaseUrl)/openai/deployments/$($Config.LlmModel)/chat/completions?api-version=$apiVersion" - - $headers = @{ - 'Content-Type' = 'application/json' - 'api-key' = $apiKey - } - - $messages = @() - if ($SystemPrompt) { $messages += @{ role = "system"; content = $SystemPrompt } } - $messages += @{ role = "user"; content = $Prompt } - - $body = @{ - messages = $messages - max_tokens = $MaxTokens - temperature = 0.1 - } - - try { - $requestJson = $body | ConvertTo-Json -Depth 10 - $response = Invoke-RestMethod -Uri $endpoint -Method POST -Headers $headers -Body $requestJson - return $response.choices[0].message.content - } - catch { - Write-Error "Azure OpenAI API call failed: $($_.Exception.Message)" - return $null - } - } - default { - Write-Error "Unknown LLM provider: $($Config.LlmProvider)" - return $null - } - } -} - -# STEP 1: Collect PR data -Write-Host "`n📥 Step 1: Collecting comprehensive PR data..." -ForegroundColor Green - -if ($PrNumber) { - # Single PR mode - fetch only the specified PR - Write-Host " Mode: Single PR #$PrNumber" - $prs = @(@{ number = $PrNumber }) -} else { - # Query mode - fetch all PRs matching criteria - Write-Host " Mode: Query - $Query" - - try { - $prsJson = gh pr list --repo $Config.SourceRepo --search $Query --limit $Config.MaxPRs --json number - $prs = $prsJson | ConvertFrom-Json - } catch { - Write-Error "Failed to fetch PRs: $($_.Exception.Message)" - exit 1 - } - - Write-Host " Found $($prs.Count) PRs to collect data for" -} - -# Collect detailed data for each PR -$analysisData = @() -foreach ($pr in $prs) { - Write-Host " Collecting data for PR #$($pr.number): $($pr.title)" -ForegroundColor Gray - - # Get comprehensive PR details including comments, reviews, and commits - try { - $prDetails = gh pr view $pr.number --repo $Config.SourceRepo --json number,title,author,url,baseRefName,closedAt,mergedAt,mergeCommit,labels,files,state,body,comments,reviews,closingIssuesReferences,commits - $prDetailData = $prDetails | ConvertFrom-Json - } catch { - Write-Warning "Could not fetch detailed PR data for #$($pr.number)" - continue - } - - # Extract commits from the PR details - $commits = @() - if ($prDetailData.commits) { - foreach ($commit in $prDetailData.commits) { - $commitMessage = $commit.messageHeadline - if ($commit.messageBody -and $commit.messageBody.Trim() -ne "") { - $commitMessage += "`n`n" + $commit.messageBody - } - $commits += $commitMessage - } - } - - # Get closing issues with full details and comments - $closingIssues = @() - foreach ($issueRef in $prDetailData.closingIssuesReferences) { - if ($issueRef.number) { - try { - Write-Host " Fetching issue #$($issueRef.number)..." -ForegroundColor DarkGray - $issueDetails = gh issue view $issueRef.number --repo $Config.SourceRepo --json number,title,body,comments,labels,state,createdAt,closedAt,url - $issueData = $issueDetails | ConvertFrom-Json - $closingIssues += @{ - Number = $issueData.number - Title = $issueData.title - Body = $issueData.body - Comments = $issueData.comments - Labels = $issueData.labels | ForEach-Object { $_.name } - State = $issueData.state - CreatedAt = $issueData.createdAt - ClosedAt = $issueData.closedAt - Url = $issueData.url - } - } - catch { - Write-Warning "Could not fetch issue #$($issueRef.number)" - } - } - } - - # Create merge commit URL - $mergeCommitUrl = if ($prDetailData.mergeCommit.oid) { - "https://github.com/$($Config.SourceRepo)/commit/$($prDetailData.mergeCommit.oid)" - } else { - $null - } - - # Get version information using local git repository - Write-Host " 🏷️ Getting version info..." -ForegroundColor DarkGray - $versionInfo = Get-VersionInfo -prNumber $prDetailData.number -mergedAt $prDetailData.closedAt -baseRef $prDetailData.baseRefName - - # Check for existing docs issues - $hasDocsIssue = $false - try { - $searchResult = gh issue list --repo $Config.DocsRepo --search "Breaking change $($prDetailData.number)" --json number,title - $existingIssues = $searchResult | ConvertFrom-Json - $hasDocsIssue = $existingIssues.Count -gt 0 - } catch { - Write-Warning "Could not check for existing docs issues for PR #$($prDetailData.number)" - } - - # Get feature areas from area- labels first, then fall back to file paths - $featureAreas = @() - - # First try to get feature areas from area- labels - foreach ($label in $prDetailData.labels) { - if ($label.name -match "^area-(.+)$") { - $featureAreas += $matches[1] - } - } - - $featureAreas = $featureAreas | Select-Object -Unique - if ($featureAreas.Count -eq 0) { - Write-Error "Unable to determine feature area for PR #$($prDetailData.Number). Please set an 'area-' label." - } - - $analysisData += @{ - Number = $prDetailData.number - Title = $prDetailData.title - Url = $prDetailData.url - Author = $prDetailData.author.login - BaseRef = $prDetailData.baseRefName - ClosedAt = $prDetailData.closedAt - MergedAt = $prDetailData.mergedAt - MergeCommit = @{ - Sha = $prDetailData.mergeCommit.oid - Url = $mergeCommitUrl - } - Commits = $commits - Body = $prDetailData.body - Comments = $prDetailData.comments - Reviews = $prDetailData.reviews - ClosingIssues = $closingIssues - HasDocsIssue = $hasDocsIssue - ExistingDocsIssues = if ($hasDocsIssue) { $existingIssues } else { @() } - FeatureAreas = $featureAreas -join ", " - ChangedFiles = $prDetailData.files | ForEach-Object { $_.path } - Labels = $prDetailData.labels | ForEach-Object { $_.name } - VersionInfo = $versionInfo - } - - # Save individual PR data file - $prFileName = Join-Path $dataDir "pr_$($prDetailData.number).json" - $analysisData[-1] | ConvertTo-Json -Depth 10 | Out-File $prFileName -Encoding UTF8 - Write-Host " 💾 Saved: $prFileName" -ForegroundColor DarkGray - - Start-Sleep -Seconds $Config.RateLimiting.DelayBetweenCalls -} - -# Save combined data with comprehensive details (for overview) -$analysisData | ConvertTo-Json -Depth 10 | Out-File (Join-Path $dataDir "combined.json") -Encoding UTF8 - -# Create summary report -$queryInfo = if ($PrNumber) { - "Single PR #$PrNumber" -} else { - "Query: $Query" -} - -$summaryReport = @" -# Breaking Change Documentation Collection Report - -**Generated**: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -**Mode**: $queryInfo - -## Summary - -- **Total PRs collected**: $($analysisData.Count) -- **PRs with existing docs issues**: $($analysisData | Where-Object HasDocsIssue | Measure-Object).Count -- **PRs needing docs issues**: $($analysisData | Where-Object { -not $_.HasDocsIssue } | Measure-Object).Count - -## PRs Needing Documentation - -$($analysisData | Where-Object { -not $_.HasDocsIssue } | ForEach-Object { - "- **PR #$($_.Number)**: $($_.Title)`n - URL: $($_.Url)`n - Feature Areas: $($_.FeatureAreas)`n" -} | Out-String) - -## PRs With Existing Documentation - -$($analysisData | Where-Object HasDocsIssue | ForEach-Object { - "- **PR #$($_.Number)**: $($_.Title)`n - URL: $($_.Url)`n" -} | Out-String) -"@ - -$summaryReport | Out-File (Join-Path $dataDir "summary_report.md") -Encoding UTF8 - -Write-Host "✅ Data collection completed" -ForegroundColor Green -Write-Host " 📊 Summary: $(Join-Path $dataDir "summary_report.md")" -Write-Host " 📋 Combined: $(Join-Path $dataDir "combined.json")" -Write-Host " 📄 Individual: $(Join-Path $dataDir "pr_*.json") ($($analysisData.Count) files)" - -if ($CollectOnly) { - exit 0 -} - -# STEP 2: Generate breaking change issues -Write-Host "`n📝 Step 2: Generating breaking change documentation..." -ForegroundColor Green - -$prsNeedingDocs = $analysisData | Where-Object { -not $_.HasDocsIssue } - -if ($prsNeedingDocs.Count -eq 0) { - if ($PrNumber) { - Write-Host " PR #$PrNumber already has documentation or doesn't need it." -ForegroundColor Yellow - } else { - Write-Host " No PRs found that need documentation issues." -ForegroundColor Yellow - } - exit 0 -} - -if ($PrNumber) { - Write-Host " Processing PR #$PrNumber for issue generation" -} else { - Write-Host " Processing $($prsNeedingDocs.Count) PRs for issue generation" -} - -foreach ($pr in $prsNeedingDocs) { - Write-Host " 🔍 Processing PR #$($pr.Number): $($pr.Title)" -ForegroundColor Cyan - - # Use commits data already collected in Step 1 - $commits = $pr.Commits - - # Prepare data for LLM - $comments = if ($pr.Comments -and $pr.Comments.Count -gt 0) { - ($pr.Comments | ForEach-Object { "**@$($_.author.login)**: $(Limit-Text -text $_.body -maxLength 300)" }) -join "`n`n" - } else { "No comments" } - - $reviews = if ($pr.Reviews -and $pr.Reviews.Count -gt 0) { - ($pr.Reviews | ForEach-Object { "**@$($_.author.login)** ($($_.state)): $(Limit-Text -text $_.body -maxLength 200)" }) -join "`n`n" - } else { "No reviews" } - - $closingIssuesInfo = if ($pr.ClosingIssues -and $pr.ClosingIssues.Count -gt 0) { - $issuesList = $pr.ClosingIssues | ForEach-Object { - $issueComments = if ($_.Comments -and $_.Comments.Count -gt 0) { - "Comments: $($_.Comments.Count) comments available" - } else { - "No comments" - } - @" -**Issue #$($_.Number)**: $($_.Title) -$($_.Url) -$(if ($_.Body) { "$(Limit-Text -text $_.Body -maxLength 500)" }) -$issueComments -"@ - } - "`n## Related Issues`n" + ($issuesList -join "`n`n") - } else { "" } - - # Fetch issue template and examples (required for quality) - $issueTemplate = Get-IssueTemplate - $exampleIssues = Get-ExampleBreakingChangeIssues - - # Use version information collected in Step 1 - $versionInfo = $pr.VersionInfo - - # Create LLM prompt - $systemPrompt = @" -You are an expert .NET developer and technical writer. Create high-quality breaking change documentation for Microsoft .NET. - -**CRITICAL: Generate clean markdown content following the structure shown in the examples. Do NOT output YAML or fill in template forms. The template is provided only as a reference for sections and values.** - -Focus on: -1. Clear, specific descriptions of what changed -2. Concrete before/after behavior with examples -3. Actionable migration guidance for developers -4. Appropriate breaking change categorization -5. Professional tone for official Microsoft documentation - -Use the provided template as a reference for structure only, and follow the examples for the actual output format. -Pay special attention to the version information provided to ensure accuracy. -"@ - - $templateSection = @" -## Issue Template Structure Reference -The following GitHub issue template shows the required sections and possible values for breaking change documentation. -**IMPORTANT: This is NOT the expected output format. Use this only as a reference for what sections to include and what values are available. Generate clean markdown content, not YAML.** - -```yaml -$issueTemplate -``` -"@ - - $exampleSection = @" - -## Examples of Good Breaking Change Documentation -Here are recent examples of well-written breaking change documentation: - -$exampleIssues -"@ - - $versionSection = @" - -## Version Information -**Last GitHub tag before this PR was merged**: $($versionInfo.LastTagBeforeMerge) -**First GitHub tag that includes this change**: $($versionInfo.FirstTagWithChange) -**Estimated .NET version for this change**: $($versionInfo.EstimatedVersion) - -Use this version information to accurately determine when this breaking change was introduced. -"@ - - $userPrompt = @" -Analyze this .NET runtime pull request and create breaking change documentation. - -## PR Information -**Number**: #$($pr.Number) -**Title**: $($pr.Title) -**URL**: $($pr.Url) -**Author**: $($pr.Author) -**Base Branch**: $($pr.BaseRef) -**Merged At**: $($pr.MergedAt) -**Feature Areas**: $($pr.FeatureAreas) - -$(if ($pr.MergeCommit.Url) { "**Merge Commit**: $($pr.MergeCommit.Url)" }) - -**PR Body**: -$(Limit-Text -text $pr.Body -maxLength 1500) - -## Commits -$(if ($commits -and $commits.Count -gt 0) { - $commitInfo = $commits | ForEach-Object { - "$(Limit-Text -text $_ -maxLength 300)" - } - $commitInfo -join "`n`n" -} else { - "No commit information available" -}))) - -## Changed Files -$($pr.ChangedFiles -join "`n") - -## Comments -$comments - -## Reviews -$reviews -$closingIssuesInfo - -$templateSection -$exampleSection -$versionSection - -**OUTPUT FORMAT: Generate a complete breaking change issue in clean markdown format following the structure and style of the examples above. Do NOT output YAML template syntax.** - -Generate the complete issue following the template structure and using the examples as guidance for quality and style. -"@ - - # Call LLM API - Write-Host " 🤖 Generating content..." -ForegroundColor Gray - $llmResponse = Invoke-LlmApi -SystemPrompt $systemPrompt -Prompt $userPrompt -PrNumber $pr.Number - - if (-not $llmResponse) { - Write-Error "Failed to get LLM response for PR #$($pr.Number)" - continue - } - - # Parse response - if ($llmResponse -match '(?s)\*\*Issue Title\*\*:\s*(.+?)\s*\*\*Issue Body\*\*:\s*(.+)$') { - $issueTitle = $matches[1].Trim() - $issueBody = $matches[2].Trim() - } else { - $issueTitle = "[Breaking change]: $($pr.Title -replace '^\[.*?\]\s*', '')" - $issueBody = $llmResponse - } - - # Save issue draft - $issueFile = Join-Path $issueDraftsDir "issue_pr_$($pr.Number).md" - @" -# $issueTitle - -$issueBody - ---- -*Generated by Breaking Change Documentation Tool* -*PR: $($pr.Url)* -*Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')* -"@ | Out-File $issueFile -Encoding UTF8 - - Write-Host " 📄 Draft saved: $issueFile" -ForegroundColor Gray - - # Add comment with link to create issue using GitHub's issue creation URL - $commentFile = Join-Path $commentDraftsDir "comment_pr_$($pr.Number).md" - - # URL encode the title and full issue body - $encodedTitle = [Uri]::EscapeDataString($issueTitle) - $encodedBody = [Uri]::EscapeDataString($issueBody) - $encodedLabels = [Uri]::EscapeDataString($Config.IssueTemplate.Labels -join ",") - - # Create GitHub issue creation URL with full content and labels - $createIssueUrl = "https://github.com/$($Config.DocsRepo)/issues/new?title=$encodedTitle&body=$encodedBody&labels=$encodedLabels" - - $commentBody = @" -## 📋 Breaking Change Documentation Required - -[Create a breaking change issue with AI-generated content]($createIssueUrl) - -*Generated by Breaking Change Documentation Tool - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')* -"@ - - # Save comment draft - $commentBody | Out-File $commentFile -Encoding UTF8 - Write-Host " 💬 Comment draft saved: $commentFile" -ForegroundColor Gray - - # Handle different action modes - if (-not $executeActions) { - # Draft only mode, just log the commands that could be run. - Write-Host " 📝 To create an issue use command:" -ForegroundColor Yellow - Write-Host " gh issue create --repo $($Config.DocsRepo) --title `"$issueTitle`" --body `"...[content truncated]...`" --label `"$($Config.IssueTemplate.Labels -join ',')`" --assignee `"$($Config.IssueTemplate.Assignee)`"" -ForegroundColor Gray - - Write-Host " 💬 To add a comment use command:" -ForegroundColor Yellow - Write-Host " gh pr comment $($pr.Number) --repo $($Config.SourceRepo) --body-file `"$commentFile`"" -ForegroundColor Gray - - } elseif ($CreateIssues) { - # Create GitHub issue directly - try { - Write-Host " 🚀 Creating GitHub issue..." -ForegroundColor Gray - - $result = gh issue create --repo $Config.DocsRepo --title $issueTitle --body $issueBody --label ($Config.IssueTemplate.Labels -join ",") --assignee $Config.IssueTemplate.Assignee - - if ($LASTEXITCODE -eq 0) { - Write-Host " ✅ Issue created: $result" -ForegroundColor Green - - # Add comment to original PR - $prComment = "Breaking change documentation issue created: $result" - gh pr comment $pr.Number --repo $Config.SourceRepo --body $prComment | Out-Null - } else { - Write-Error "Failed to create issue for PR #$($pr.Number)" - } - } - catch { - Write-Error "Error creating issue for PR #$($pr.Number): $($_.Exception.Message)" - } - } elseif ($Comment) { - # Add a comment to the PR to allow the author to create the issue - try { - Write-Host " 💬 Adding comment to PR..." -ForegroundColor Gray - - $result = gh pr comment $pr.Number --repo $Config.SourceRepo --body-file $commentFile - - if ($LASTEXITCODE -eq 0) { - Write-Host " ✅ Comment added to PR #$($pr.Number)" -ForegroundColor Green - } else { - Write-Error "Failed to add comment to PR #$($pr.Number)" - } - } - catch { - Write-Error "Error adding comment to PR #$($pr.Number): $($_.Exception.Message)" - } - } - - Start-Sleep -Seconds $Config.RateLimiting.DelayBetweenIssues -} - -# Final summary -Write-Host "`n🎯 Workflow completed!" -ForegroundColor Green - -if (-not $executeActions -and -not $CollectOnly) { - Write-Host " 📝 Analysis completed - drafts generated without making changes" -ForegroundColor Yellow - Write-Host " 💡 Use -CreateIssues or -Comment to execute actions on GitHub" -} elseif ($CreateIssues) { - Write-Host " ✅ Issues created in: $($Config.DocsRepo)" -ForegroundColor Green - Write-Host " 📧 Email issue links to: $($Config.IssueTemplate.NotificationEmail)" -ForegroundColor Yellow -} elseif ($Comment) { - Write-Host " 💬 Comments added to PRs with create issue links" -ForegroundColor Green - Write-Host " 📝 Issue drafts saved in: $issueDraftsDir" - Write-Host " 🔗 Click the links in PR comments to create issues when ready" -} else { - Write-Host " 📝 Issue drafts saved in: $issueDraftsDir" -} - -Write-Host "`n📁 Output files:" -Write-Host " 📊 Summary: $(Join-Path $dataDir "summary_report.md")" -Write-Host " 📋 Combined: $(Join-Path $dataDir "combined.json")" -Write-Host " 📄 Individual: $(Join-Path $dataDir "pr_*.json")" -Write-Host " 📝 Drafts: $(Join-Path $issueDraftsDir "*.md")" diff --git a/eng/breakingChanges/config.ps1 b/eng/breakingChanges/config.ps1 deleted file mode 100644 index 66689847ea1593..00000000000000 --- a/eng/breakingChanges/config.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -# Configuration for Breaking Change Documentation Workflow - -$Config = @{ - # LLM Settings - LlmProvider = "github-models" # openai, anthropic, azure-openai, github-models, github-copilot - LlmModel = "openai/gpt-4o" # For GitHub Models: openai/gpt-4o, openai/gpt-4o-mini, microsoft/phi-4, etc. - # For Azure OpenAI: deployment name (e.g., "gpt-4o", "gpt-35-turbo") - LlmApiKey = $null # Uses environment variables by default (not needed for github-models or github-copilot) - LlmBaseUrl = $null # For Azure OpenAI: https://your-resource.openai.azure.com - AzureApiVersion = "2024-02-15-preview" # Azure OpenAI API version (optional, defaults to 2024-02-15-preview) - - # GitHub Settings - SourceRepo = "dotnet/runtime" - DocsRepo = "dotnet/docs" - IssueTemplatePath = ".github/ISSUE_TEMPLATE/02-breaking-change.yml" # Path to issue template in DocsRepo - - # Analysis Settings - MaxPRs = 100 - - # Output Settings - IssueTemplate = @{ - Labels = @("breaking-change", "Pri1", "doc-idea") - Assignee = "gewarren" - NotificationEmail = "dotnetbcn@microsoft.com" - } - - # Rate Limiting - RateLimiting = @{ - DelayBetweenCalls = 2 # seconds - DelayBetweenIssues = 3 # seconds - } -}