diff --git a/.gitattributes b/.gitattributes index a1e1e97ac24..b678750c67f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -60,4 +60,6 @@ #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain -#*.RTF diff=astextplain \ No newline at end of file +#*.RTF diff=astextplain + +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/actions/select-copilot-pat/README.md b/.github/actions/select-copilot-pat/README.md new file mode 100644 index 00000000000..8b57f6377bc --- /dev/null +++ b/.github/actions/select-copilot-pat/README.md @@ -0,0 +1,116 @@ +# Select Copilot PAT +Selects a random Copilot PAT from a numbered pool of secrets. This addresses limitations that arise from having a single PAT shared across all agentic workflows, such as rate-limiting. + +**This is a stop-gap workaround.** As soon as organization/enterprise billing is offered for agentic workflows, this approach will be removed from our workflows. + +## Repository Onboarding +To use Agentic Workflows in a dotnet org repository: + +1. Follow the instructions for [Configuring Your Repository | Agentic Authoring | GitHub Agentic Workflows][configure-repo]. +2. Copy this `select-copilot-pat` folder into the repository under `.github/actions/select-copilot-pat`, including both the `README.md` and `action.yml`. +3. Merge those additions into the repository and then follow the instructions for the PAT Creation and Usage below. + +> **Optional:** If you plan to manage secrets or workflows from the command line (e.g., `gh aw secrets set`), [install the `gh aw` CLI extension][cli-setup]: +> ```sh +> gh extension install github/gh-aw +> ``` + +## PAT Management +Team members provide PATs into the pools for the repository by adding them as repository secrets with secret names matching the pattern of `_<0-9>`, such as `COPILOT_PAT_0`. + +[Use this link to prefill the PAT creation form with the required settings][create-pat]: + +1. **Resource owner** is your **user account**, not an organization. +2. **Copilot Requests (Read)** must be the only permission granted. +3. **8-day expiration** must be used, which enforces a weekly renewal. +4. **Repository access** set to **Public repositories** only. + +The **Token Name** _does not_ need to match the secret name and is only visible to the owner of the PAT. It's recommended to use a token name indicating the PAT is used for dotnet org agentic workflows. The **Description** is also only used for your own reference. + +Team members providing PATs for workflows should set weekly recurring reminders to regenerate and update their PATs in the repository secrets. With an 8-day expiration, renewal can be done on the same day each week. + +PATs are added to repositories through the **Settings > Secrets and variables > Actions** UI, saved as **Repository secrets** and matching the `_<0-9>` naming convention. This can also be done using the GitHub CLI. + +```sh +gh aw secrets set "_<0-9>" --value "" --repo dotnet/ +``` + +## Workflow Output Attribution +Team members' PATs are _only_ used for the Copilot requests from within the agentic portion of the workflow. All outputs from the workflow use the `github-actions[bot]` account token. Issues, PRs, comments, and all other content generated by the workflow will be attributed to `github-actions[bot]`--not the team member's account or token. + +## Usage +Add the following frontmatter at the top-level of an agentic workflow. These elements are not supported through [imports][imports], so they must be copied into all workflows. + +Up to 10 `SECRET_#` environment variables can be passed to the action, numbered 0-9. Different workflows can use different pools of PATs if desired. Change the `secrets.COPILOT_PAT_0` through `secrets.COPILOT_PAT_9` secret names in both the `select-copilot-pat` step `env` values and in the `case` expression under the `engine: env` configuration. + +```yml +on: + # 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: + # If the secret names are changed here, they must also be changed + # in the `engine: env` case expression + 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) }} +``` + +## References + +- [Agentic Workflows CLI Extension][cli-setup] +- [Agentic Authoring][configure-repo] +- [Authentication][authentication] +- [Agentic Workflow Imports][imports] +- [Custom Steps][steps] +- [Custom Jobs][jobs] +- [Job Outputs][job-outputs] +- [Engine Configuration][engine] +- [Engine Environment Variables][engine-vars] +- [Case Function in Workflow Expressions][case-expression] +- [Update agentic engine token handling to use user-provided secrets (github/gh-aw#18017)][secret-override] + +[cli-setup]: https://github.github.com/gh-aw/setup/cli/ +[configure-repo]: https://github.github.com/gh-aw/guides/agentic-authoring/#configuring-your-repository +[authentication]: https://github.github.com/gh-aw/reference/auth/ +[create-pat]: https://github.com/settings/personal-access-tokens/new?name=dotnet%20org%20agentic%20workflows&description=GitHub+Agentic+Workflows+-+Copilot+engine+authentication.++Used+for+dotnet+org+workflows.+MUST+be+configured+with+only+Copilot+Requests+permissions+and+user+account+as+resource+owner.+Weekly+expiration+and+required+renewal.&user_copilot_requests=read&expires_in=8 +[imports]: https://github.github.com/gh-aw/reference/imports/ +[steps]: https://github.github.com/gh-aw/reference/frontmatter/#custom-steps-steps +[jobs]: https://github.github.com/gh-aw/reference/frontmatter/#custom-jobs-jobs +[job-outputs]: https://github.github.com/gh-aw/reference/frontmatter/#job-outputs +[engine]: https://github.github.com/gh-aw/reference/frontmatter/#ai-engine-engine +[engine-vars]: https://github.github.com/gh-aw/reference/engines/#engine-environment-variables +[case-expression]: https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#case +[secret-override]: https://github.com/github/gh-aw/pull/18017 diff --git a/.github/actions/select-copilot-pat/action.yml b/.github/actions/select-copilot-pat/action.yml new file mode 100644 index 00000000000..198eb0393db --- /dev/null +++ b/.github/actions/select-copilot-pat/action.yml @@ -0,0 +1,53 @@ +name: 'Select Copilot PAT from Pool' +description: > + Selects a random Copilot PAT from a numbered pool of secrets. Secrets + are passed as environment variables SECRET_0 through SECRET_9 + by the calling workflow step. + +inputs: + random-seed: + description: 'A seed number to use for the random PAT selection, for deterministic selection if needed.' + required: false + default: '' + +outputs: + copilot_pat_number: + description: 'The 0-9 secret number selected from the pool of specified secrets' + value: ${{ steps.select-pat-number.outputs.copilot_pat_number }} + +runs: + using: composite + steps: + - id: select-pat-number + shell: bash + env: + RANDOM_SEED: ${{ inputs.random-seed }} + run: | + # Collect all secret numbers with non-empty values from SECRET_0..SECRET_9 + PAT_NUMBERS=() + for i in $(seq 0 9); do + var="SECRET_${i}" + val="${!var}" + if [ -n "$val" ]; then + PAT_NUMBERS+=(${i}) + fi + done + + # If none of the secrets in the pool have values, then emit a warning and do not + # set an output value. The consumer can then fall back to using COPILOT_GITHUB_TOKEN. + if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then + echo "::warning::None of the specified secrets had values (checked SECRET_0 through SECRET_9)" + exit 0 + fi + + # Select a random index using the seed if specified + if [ -n "$RANDOM_SEED" ]; then + RANDOM=$RANDOM_SEED + fi + + PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) + PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" + echo "Selected token ${PAT_NUMBER} (index: ${PAT_INDEX}; pool size: ${#PAT_NUMBERS[@]})" + + # Set the PAT number as the output + echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.agent.md new file mode 100644 index 00000000000..999c8031bcb --- /dev/null +++ b/.github/agents/agentic-workflows.agent.md @@ -0,0 +1,258 @@ +--- +description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing +disable-model-invocation: true +--- + +# GitHub Agentic Workflows Agent + +This agent helps you work with **GitHub Agentic Workflows (gh-aw)**, a CLI extension for creating AI-powered workflows in natural language using markdown files. + +## What This Agent Does + +This is a **dispatcher agent** that routes your request to the appropriate specialized prompt based on your task: + +- **Creating new workflows**: Routes to `create` prompt +- **Updating existing workflows**: Routes to `update` prompt +- **Debugging workflows**: Routes to `debug` prompt +- **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt +- **Creating report-generating workflows**: Routes to `report` prompt — consult this whenever the workflow posts status updates, audits, analyses, or any structured output as issues, discussions, or comments +- **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt +- **Fixing Dependabot PRs**: Routes to `dependabot` prompt — use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes +- **Analyzing test coverage**: Routes to `test-coverage` prompt — consult this whenever the workflow reads, analyzes, or reports on test coverage data from PRs or CI runs + +Workflows may optionally include: + +- **Project tracking / monitoring** (GitHub Projects updates, status reporting) +- **Orchestration / coordination** (one workflow assigning agents or dispatching and coordinating other workflows) + +## Files This Applies To + +- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` +- Workflow lock files: `.github/workflows/*.lock.yml` +- Shared components: `.github/workflows/shared/*.md` +- Configuration: https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/github-agentic-workflows.md + +## Problems This Solves + +- **Workflow Creation**: Design secure, validated agentic workflows with proper triggers, tools, and permissions +- **Workflow Debugging**: Analyze logs, identify missing tools, investigate failures, and fix configuration issues +- **Version Upgrades**: Migrate workflows to new gh-aw versions, apply codemods, fix breaking changes +- **Component Design**: Create reusable shared workflow components that wrap MCP servers + +## How to Use + +When you interact with this agent, it will: + +1. **Understand your intent** - Determine what kind of task you're trying to accomplish +2. **Route to the right prompt** - Load the specialized prompt file for your task +3. **Execute the task** - Follow the detailed instructions in the loaded prompt + +## Available Prompts + +### Create New Workflow +**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/create-agentic-workflow.md + +**Use cases**: +- "Create a workflow that triages issues" +- "I need a workflow to label pull requests" +- "Design a weekly research automation" + +### Update Existing Workflow +**Load when**: User wants to modify, improve, or refactor an existing workflow + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/update-agentic-workflow.md + +**Use cases**: +- "Add web-fetch tool to the issue-classifier workflow" +- "Update the PR reviewer to use discussions instead of issues" +- "Improve the prompt for the weekly-research workflow" + +### Debug Workflow +**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/debug-agentic-workflow.md + +**Use cases**: +- "Why is this workflow failing?" +- "Analyze the logs for workflow X" +- "Investigate missing tool calls in run #12345" + +### Upgrade Agentic Workflows +**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/upgrade-agentic-workflows.md + +**Use cases**: +- "Upgrade all workflows to the latest version" +- "Fix deprecated fields in workflows" +- "Apply breaking changes from the new release" + +### Create a Report-Generating Workflow +**Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/report.md + +**Use cases**: +- "Create a weekly CI health report" +- "Post a daily security audit to Discussions" +- "Add a status update comment to open PRs" + +### Create Shared Agentic Workflow +**Load when**: User wants to create a reusable workflow component or wrap an MCP server + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/create-shared-agentic-workflow.md + +**Use cases**: +- "Create a shared component for Notion integration" +- "Wrap the Slack MCP server as a reusable component" +- "Design a shared workflow for database queries" + +### Fix Dependabot PRs +**Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`) + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/dependabot.md + +**Use cases**: +- "Fix the open Dependabot PRs for npm dependencies" +- "Bundle and close the Dependabot PRs for workflow dependencies" +- "Update @playwright/test to fix the Dependabot PR" + +### Analyze Test Coverage +**Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy. + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/test-coverage.md + +**Use cases**: +- "Create a workflow that comments coverage on PRs" +- "Analyze coverage trends over time" +- "Add a coverage gate that blocks PRs below a threshold" + +## Instructions + +When a user interacts with you: + +1. **Identify the task type** from the user's request +2. **Load the appropriate prompt** from the GitHub repository URLs listed above +3. **Follow the loaded prompt's instructions** exactly +4. **If uncertain**, ask clarifying questions to determine the right prompt + +## Quick Reference + +```bash +# Initialize repository for agentic workflows +gh aw init + +# Generate the lock file for a workflow +gh aw compile [workflow-name] + +# Debug workflow runs +gh aw logs [workflow-name] +gh aw audit + +# Upgrade workflows +gh aw fix --write +gh aw compile --validate +``` + +## Key Features of gh-aw + +- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter +- **AI Engine Support**: Copilot, Claude, Codex, or custom engines +- **MCP Server Integration**: Connect to Model Context Protocol servers for tools +- **Safe Outputs**: Structured communication between AI and GitHub API +- **Strict Mode**: Security-first validation and sandboxing +- **Shared Components**: Reusable workflow building blocks +- **Repo Memory**: Persistent git-backed storage for agents +- **Sandboxed Execution**: All workflows run in the Agent Workflow Firewall (AWF) sandbox, enabling full `bash` and `edit` tools by default + +## Important Notes + +- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/github-agentic-workflows.md for complete documentation +- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud +- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions +- **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF +- Follow security best practices: minimal permissions, explicit network access, no template injection +- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See https://github.com/github/gh-aw/blob/v0.65.6/.github/aw/network.md for the full list of valid ecosystem identifiers and domain patterns. +- **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself. + +## Repository-Specific Requirements: Copilot PAT Pool + +**⚠️ MANDATORY**: Every agentic workflow in this repository **must** include the `select-copilot-pat` integration. This selects a random Copilot PAT from a numbered pool of secrets to avoid rate-limiting from a single shared PAT. This is a stop-gap workaround until organization-level billing is offered for agentic workflows. + +These elements are **not supported through imports**, so they must be copied into the frontmatter of every agentic workflow. See `.github/actions/select-copilot-pat/README.md` for full documentation and `.github/workflows/copilot-echo.md` for a working example. + +When **creating** or **updating** an agentic workflow, always include the following three frontmatter blocks. These go between the `---` markers alongside the workflow's other frontmatter (permissions, safe-outputs, etc.). + +### Comment block (placed before the steps, inside the `on:` block) + +Always include this comment block before the steps to explain the PAT pool override: + +```yaml +# ############################################################### +# 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 +# ############################################################### +``` + +### Block 1: Pre-activation steps (nested under `on:`) + +Add this under the `on:` key, at the same level as the trigger configuration, immediately after the comment block above: + +```yaml + # 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 }} +``` + +### Block 2: Pre-activation job outputs (top-level `jobs:`) + +```yaml +jobs: + pre-activation: + outputs: + copilot_pat_number: ${{ steps.select-copilot-pat.outputs.copilot_pat_number }} +``` + +### Block 3: Engine configuration with PAT override (top-level `engine:`) + +```yaml +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) }} +``` + +**Important notes about the engine block:** +- The `COPILOT_GITHUB_TOKEN` `case()` expression **must** remain on a single line — line breaks cause syntax errors in the compiled workflow. +- If no `COPILOT_PAT_#` secrets are configured, the expression falls back to the default `COPILOT_GITHUB_TOKEN` secret. +- Do **not** specify `engine: copilot` as a simple string — use the object form shown above so the `env:` override can be included. diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json new file mode 100644 index 00000000000..1c855ccd7d6 --- /dev/null +++ b/.github/aw/actions-lock.json @@ -0,0 +1,34 @@ +{ + "entries": { + "actions/download-artifact@v8.0.1": { + "repo": "actions/download-artifact", + "version": "v8.0.1", + "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" + }, + "actions/github-script@v8": { + "repo": "actions/github-script", + "version": "v8", + "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" + }, + "actions/setup-dotnet@v5.2.0": { + "repo": "actions/setup-dotnet", + "version": "v5.2.0", + "sha": "c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7" + }, + "actions/upload-artifact@v7": { + "repo": "actions/upload-artifact", + "version": "v7", + "sha": "bbbca2ddaa5d8feaa63e36b76fdaad77386f024f" + }, + "github/gh-aw-actions/setup@v0.65.1": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.65.1", + "sha": "1831bcd4f397da99da6e6e6b65687557a1a37ac7" + }, + "github/gh-aw-actions/setup@v0.65.6": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.65.6", + "sha": "31130b20a8fd3ef263acbe2091267c0aace07e09" + } + } +} diff --git a/.github/workflows/labeler.md b/.github/labeler-readme.md similarity index 100% rename from .github/workflows/labeler.md rename to .github/labeler-readme.md diff --git a/.github/skills/release-notes/DESIGN.md b/.github/skills/release-notes/DESIGN.md new file mode 100644 index 00000000000..928499ea145 --- /dev/null +++ b/.github/skills/release-notes/DESIGN.md @@ -0,0 +1,275 @@ +# Release Notes System — Design + +How the automated release notes system for .NET works, why it's designed this way, and how the pieces connect. + +## Problem + +.NET ships ~25 component repositories assembled into a single product via the VMR (`dotnet/dotnet`). Release notes need to reflect exactly what ships — not what's planned, not what's in progress, not what was reverted. Historically, release notes are written manually by gathering information from component teams. This is slow, error-prone, and tends to miss features or include things that didn't actually ship. + +## Goals + +1. **High fidelity** — only document what ships. The VMR `source-manifest.json` is the source of truth for what code is included in a release. +2. **High value** — bias toward features users care about. Not every merged PR is worth a release note. +3. **Never document non-shipping features** — if it's not in the VMR diff, it didn't ship. +4. **Incremental** — drafts improve over time as previews mature and humans provide feedback. +5. **Human-in-the-loop** — humans edit the release notes branch and comment on the PR. The system adapts to their input rather than overwriting it. + +## Architecture + +The system has three layers, each with a distinct responsibility: + +```text +┌─────────────────────────────────────────────────┐ +│ Agentic Workflow (cron) │ +│ .github/workflows/release-notes.md │ +│ Orchestration: branch lifecycle, PR mgmt, │ +│ human interaction, scheduling │ +├─────────────────────────────────────────────────┤ +│ AI Agent (editorial) │ +│ Reads changes.json → writes markdown │ +│ Judgment: which PRs matter, how to describe │ +│ them, code samples, feature grouping │ +├─────────────────────────────────────────────────┤ +│ dotnet-release tool (deterministic) │ +│ `dotnet-release generate changes` │ +│ Data: source-manifest diff → GitHub API → │ +│ changes.json │ +└─────────────────────────────────────────────────┘ +``` + +### Why three layers? + +- **The tool** handles mechanical data collection. It's deterministic — same inputs always produce the same output. It can be tested, debugged, and improved independently. +- **The agent** handles editorial judgment. It decides which PRs are worth writing about and how to describe them. This is inherently fuzzy and benefits from AI. +- **The workflow** handles orchestration. It knows when to run, what branches to manage, how to interact with humans, and how to preserve their edits. + +## Layer 1 — The Tool + +[`dotnet-release`](https://github.com/richlander/dotnet-release) is a .NET global tool. The `generate changes` command produces `changes.json`: + +```bash +dotnet-release generate changes \ + --base \ + --head \ + --version "11.0.0-preview.3" \ + --date "2026-04-08" \ + --labels \ + --output release-notes/11.0/preview/preview3/changes.json +``` + +### What the tool does + +1. **Reads `src/source-manifest.json`** at both refs via local git — this is the VMR bill of materials listing every component repo with its exact commit SHA +2. **Diffs the manifests** to identify which components changed and their commit ranges +3. **Queries GitHub compare API** for each changed component to enumerate merged PRs in the commit range +4. **Maps repos to product slugs** using a built-in taxonomy (e.g., `runtime` → `dotnet-runtime`; infra repos get no product) +5. **Fetches PR labels** when `--labels` is provided (useful for agent categorization) +6. **Cross-references CVEs** when `--cve-repo` is provided (loads `cve.json` from the `release-index` branch) +7. **Outputs `changes.json`** following the [changes schema](references/changes-schema.md) + +### What the tool does NOT do + +- Editorial judgment (which PRs are important) +- Markdown generation +- Branch management or PR creation + +### Source-manifest.json — the key mechanism + +The VMR's `src/source-manifest.json` is a JSON bill of materials: + +```json +{ + "repositories": [ + { + "path": "runtime", + "remoteUri": "https://github.com/dotnet/runtime", + "commitSha": "d3439749b652966f8283f2d01c972bc8c4dc3ec3" + } + ] +} +``` + +By comparing this file at two release points, the tool gets exact per-component commit ranges. This replaces any need to parse VMR sync commits, trace codeflow PRs, or calculate fork points. One JSON diff gives everything. + +Example (Preview 1 → Preview 2): the tool found 1,389 PRs across 21 changed repos. + +## Layer 2 — The Agent + +The AI agent reads `changes.json` and writes markdown release notes. Its guidance comes from reference documents in `.github/skills/release-notes/references/`: + +| Document | Purpose | +| -------- | ------- | +| [quality-bar.md](references/quality-bar.md) | North star — fidelity, value, WHY+HOW | +| [vmr-structure.md](references/vmr-structure.md) | VMR branches, tags, source-manifest.json | +| [changes-schema.md](references/changes-schema.md) | The changes.json schema | +| [component-mapping.md](references/component-mapping.md) | Components → product slugs → output files | +| [format-template.md](references/format-template.md) | Markdown document structure | +| [editorial-rules.md](references/editorial-rules.md) | Tone, attribution, naming | +| [examples/](references/examples/README.md) | Curated examples by component — short, medium, long-form styles | + +These are **goal-oriented**, not procedural. They describe what good release notes look like, not the exact steps to produce them. The agent figures out the HOW. + +### Agent responsibilities + +- **Triage** — read `changes.json` and identify which PRs are worth writing about. Use the [component mapping](references/component-mapping.md) to route changes from `repo` to the correct output files. +- **Verify** — before writing about any API, use `dotnet-inspect` to confirm it exists with the correct type names, member signatures, and namespaces. See [api-verification.md](references/api-verification.md). +- **Write** — produce markdown release notes for high-value features, following the quality bar. Only use API names, type names, and code samples that have been verified. +- **Respect edits** — diff the PR branch to see what humans have changed and preserve their work +- **Respond** — read PR comments and incorporate human feedback + +## Layer 3 — The Agentic Workflow + +A [GitHub Agentic Workflow](https://github.github.com/gh-aw/) defined in `.github/workflows/release-notes.md`. It runs on a schedule and manages the full lifecycle. This is a **multi-master live system** — the agent, component teams, and release managers all edit concurrently. + +### Multi-milestone discovery + +Multiple milestones can be active simultaneously. For example: + +```text +Latest shipped: Preview 3 (in releases.json) +VMR main: Preview 5 (Versions.props iteration=5) +VMR release/P4: Preview 4 (release branch exists, no tag yet) + +→ Active milestones: Preview 4 (from release branch) AND Preview 5 (from main) +→ Each gets its own branch and PR on dotnet/core +``` + +The workflow discovers all milestones between `latest_shipped + 1` and `main_iteration`, checks for VMR tags and release branches, and creates a branch+PR for each. + +### Head ref selection (per milestone) + +Each milestone needs its own base and head ref. This is re-validated every run because refs can change: + +| Milestone state | Base ref | Head ref | +| --------------- | -------- | -------- | +| Has VMR tag (finalized) | Tag for N-1 | Tag for N | +| Has release branch (stabilizing) | Tag for N-1 | Release branch tip | +| Only on main (in development) | Tag for N-1 | main | + +**Critical**: never use `main` for milestone N if `main` has moved to N+1. Check the iteration in `Versions.props` every run. + +### Branch lifecycle on dotnet/core + +```text +Branch: release-notes/11.0-preview4 +PR: [release-notes] .NET 11 Preview 4 + +Branch: release-notes/11.0-preview5 +PR: [release-notes] .NET 11 Preview 5 +``` + +Each branch is long-lived — it's created on the first run and updated frequently until the PR is merged (after the preview ships). + +### Human interaction model + +This is the most delicate part of the system. The branches are shared workspaces: + +**Respecting edits:** +- Before writing, diff the branch to identify human commits +- Files humans have edited are partially off-limits — only add new sections, never overwrite their changes +- When a file has mixed agent + human content, be surgical — touch only agent-authored sections + +**Responding to comments:** +- Read all PR comments and review threads since the last run +- Classify: actionable feedback, questions, disagreements, resolved +- For actionable items: make the change and confirm +- For questions: answer or escalate +- For disagreements: cross-check `changes.json` and explain findings +- When intent is unclear: ask for clarification via comment +- This is a conversation. Engage, don't ignore. + +**Handling conflicts:** +- If a human and the agent both changed the same section, the human's version wins +- If the agent is unsure whether a human edit was intentional, ask via PR comment +- Never force-push or rewrite human commits + +### PR lifecycle + +| State | Action | +| ----- | ------ | +| No PR for milestone | Create branch, generate content, open draft PR | +| PR exists, source changed | Regenerate `changes.json`, update/add markdown sections | +| PR exists, human edited | Preserve edits, only update untouched sections | +| New tag appeared | Final regen with `--head `, note finalization | +| Main bumped | Switch earlier milestone's head ref to release branch/tag | +| PR merged | Skip on future runs | +| PR closed | Don't reopen, log and move on | + +### Schedule and transitions + +- Runs daily (~9am Pacific) +- Previews ship monthly, Feb–Oct (typically patch Tuesday) +- RC1 ~September, RC2 ~October, GA November +- Only does meaningful work when VMR state has changed +- Each run re-validates all refs (tags appear, branches are created, main bumps) + +### Safe outputs + +The only ways the workflow can modify state: + +- `create-pull-request` — create new release notes PRs (max 5, for multiple milestones) +- `push-to-pull-request-branch` — update existing PR branches (max 5) +- `add-comment` — comment on PRs with updates, replies to feedback, questions (max 20) + +## Output files + +Each release milestone produces these files in `release-notes/{major.minor}/preview/{previewN}/`: + +| File | Source | Description | +| ---- | ------ | ----------- | +| `changes.json` | Tool | Every PR that shipped — comprehensive, machine-readable | +| `README.md` | Agent | Index/TOC linking to component files | +| `libraries.md` | Agent | System.\* BCL APIs (from `dotnet/runtime`) | +| `runtime.md` | Agent | CoreCLR, Mono, GC, JIT (from `dotnet/runtime`) | +| `aspnetcore.md` | Agent | ASP.NET Core, Blazor, SignalR | +| `sdk.md` | Agent | CLI, project system, templating | +| `efcore.md` | Agent | Entity Framework Core | +| `csharp.md` | Agent | C# language features | +| `fsharp.md` | Agent | F# language and compiler | +| `winforms.md` | Agent | Windows Forms | +| `wpf.md` | Agent | WPF | +| `msbuild.md` | Agent | MSBuild | +| `nuget.md` | Agent | NuGet client | + +Components with no noteworthy changes get a minimal stub file. + +## What's out of scope + +- **MAUI** (`dotnet/maui`) — not in the VMR, not in `source-manifest.json` +- **Container images** (`dotnet/dotnet-docker`) — not in the VMR +- **Non-dotnet-org repos** — repos under other GitHub orgs (e.g., `microsoft/vstest`) are skipped for now due to SAML token scope. Only `dotnet` org repos are queried. This can be expanded later. +- **Servicing releases** — separate process, handled by existing automation +- **Security advisories** — the tool can cross-reference CVEs but the agent doesn't author security bulletins + +## Design decisions + +### Why source-manifest.json instead of VMR git log? + +The VMR git log contains codeflow sync commits, dependency updates, and merge commits that are noisy and hard to trace back to meaningful PRs. `source-manifest.json` gives clean per-component commit ranges that map directly to GitHub compare API queries. + +### Why a separate tool instead of agent-only? + +The data collection step (manifest diff → PR enumeration) is mechanical and deterministic. Making it a standalone tool means: + +- It can be tested independently +- Output is reproducible (same refs → same `changes.json`) +- The agent gets structured input instead of having to make dozens of API calls itself +- Humans can run it locally to inspect what shipped + +### Why goal-oriented reference docs instead of procedural scripts? + +Goal-oriented docs let the agent adapt to unexpected situations (e.g., a component that reorganized its repo structure). A procedural approach would limit the agent to mechanically execute a rigid and brittle pipeline. + +### Why one PR per preview? + +Each preview is a coherent release milestone with its own set of features. Maintaining one long-lived branch per preview allows: + +- Humans to edit the branch directly +- Incremental improvement as the preview matures +- Clear history of how the notes evolved +- Easy review before the preview ships + +## Open questions + +1. **Conflict resolution heuristics** — when the agent and a human both changed the same section between runs, the human wins. But how granular is "a section"? Need to define this precisely (per-heading? per-file?). +2. **RC/GA milestone naming** — the multi-milestone logic assumes `previewN` naming. RC and GA milestones use different naming (`rc1`, `rc2`, `ga`). The workflow needs to handle the transition gracefully. diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md new file mode 100644 index 00000000000..61d2863d4e1 --- /dev/null +++ b/.github/skills/release-notes/SKILL.md @@ -0,0 +1,32 @@ +--- +name: release-notes +description: Generate and maintain .NET release notes. Uses the VMR source-manifest.json and dotnet-release tool to identify what shipped, then writes curated markdown for high-value features. Designed to run as a cron-driven agentic workflow that maintains PR branches per release milestone. +compatibility: Requires GitHub MCP server or gh CLI for cross-repo queries. Uses dotnet-release generate changes for structured change data. Uses dotnet-inspect for API verification. +--- + +# .NET Release Notes + +Generate and maintain release notes for .NET preview, RC, and GA releases. + +## How it works + +1. The `dotnet-release generate changes` tool diffs `source-manifest.json` between VMR release refs to produce `changes.json` — a comprehensive manifest of all PRs/commits that shipped +2. The agent reads `changes.json` and identifies high-value features worth documenting +3. The agent uses `dotnet-inspect` to verify public API names, type shapes, and member signatures before writing about them +4. The agent writes curated markdown release notes with verified API references and code samples +5. Output is one PR per release milestone in dotnet/core, maintained incrementally + +## Design + +- [DESIGN.md](DESIGN.md) — architecture, rationale, and how all the pieces connect + +## Reference documents + +- [quality-bar.md](references/quality-bar.md) — what good release notes look like +- [vmr-structure.md](references/vmr-structure.md) — VMR branches, tags, source-manifest.json +- [changes-schema.md](references/changes-schema.md) — the changes.json schema +- [component-mapping.md](references/component-mapping.md) — components, product slugs, output files +- [format-template.md](references/format-template.md) — markdown document structure +- [editorial-rules.md](references/editorial-rules.md) — tone, attribution, naming +- [api-verification.md](references/api-verification.md) — using dotnet-inspect to verify APIs +- [examples/](references/examples/) — curated examples from previous releases, organized by component. **Read the examples for your component before writing.** The [examples/README.md](references/examples/README.md) lists 12 editorial principles derived from what works and what doesn't in past release notes. diff --git a/.github/skills/release-notes/references/api-verification.md b/.github/skills/release-notes/references/api-verification.md new file mode 100644 index 00000000000..85f331757c8 --- /dev/null +++ b/.github/skills/release-notes/references/api-verification.md @@ -0,0 +1,129 @@ +# API Verification with dotnet-inspect + +Release notes reference specific .NET types, methods, properties, and enums by name. Getting these wrong is worse than omitting them — incorrect API names look authoritative but teach developers something false. Use `dotnet-inspect` to verify every API reference before including it in release notes. + +## Why this matters + +The agent reads PR titles and descriptions from `changes.json` to understand what shipped. PR titles often use informal or abbreviated names that don't match the actual public API surface. For example: + +- A PR titled "Add zstd support" doesn't tell you whether the type is `ZstdCompressionProvider`, `ZstandardCompressionProvider`, or `ZStandardResponseCompressionProvider` +- A PR titled "Add JsonContains" doesn't tell you the return type, parameter types, or whether it's `JsonContains` or `JsonContainsAsync` +- A PR titled "Rename JsonExists" doesn't confirm the new name is `JsonPathExists` vs `JsonExistsPath` vs `JsonPathExistsAsync` + +Without verification, the agent will guess — and guesses end up in code samples that don't compile. + +## How to use dotnet-inspect + +`dotnet-inspect` is available as a Copilot skill. Invoke it with `dotnet-inspect` to get the full command reference and mental model. The tool queries NuGet packages, platform libraries, and local files. + +### Running the tool + +The tool runs via `dnx` (like `npx` for .NET). No installation needed: + +```bash +dnx dotnet-inspect -y -- +``` + +### Querying the correct build + +When writing release notes for Preview N, the locally installed SDK is often Preview N-1 or older. The `--platform` flag queries the local SDK, so new APIs won't be found. Instead, **query the nightly NuGet packages directly** using `--package` and `--source`. + +### Step 1: Generate build metadata + +`dotnet-release generate build-metadata` reads the VMR's version info and queries the nightly NuGet feed to find the correct package versions: + +```bash +dotnet-release generate build-metadata ~/git/dotnet \ + --base v11.0.0-preview.2.26159.112 \ + --head origin/release/11.0.1xx-preview3 \ + --output build-metadata.json +``` + +This produces: + +```json +{ + "version": "11.0.0-preview.3", + "base_ref": "v11.0.0-preview.2.26159.112", + "head_ref": "origin/release/11.0.1xx-preview3", + "build": { + "version": "11.0.0-preview.3.26179.102", + "sdk_version": "11.0.100-preview.3.26179.102", + "sdk_url": "https://ci.dot.net/public/Sdk/11.0.100-preview.3.26179.102/dotnet-sdk-11.0.100-preview.3.26179.102-{platform}.tar.gz" + }, + "nuget": { + "source": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet11/nuget/v3/index.json", + "packages": { + "Microsoft.NETCore.App.Ref": "11.0.0-preview.3.26179.102", + "Microsoft.AspNetCore.App.Ref": "11.0.0-preview.3.26179.102", + "Microsoft.WindowsDesktop.App.Ref": "11.0.0-preview.3.26179.102", + "Microsoft.EntityFrameworkCore": "11.0.0-preview.3.26179.102" + } + } +} +``` + +### Step 2: Verify APIs against the correct packages + +Read the `nuget.source` and `nuget.packages` from `build-metadata.json` and use them directly with `dotnet-inspect`. All queries are pure data — no SDK installation needed. + +**Find a type by name:** + +```bash +FEED="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet11/nuget/v3/index.json" +VER="11.0.0-preview.3.26179.102" + +# Search the runtime ref pack +dnx dotnet-inspect -y -- find "*AnyNewLine*" --package "Microsoft.NETCore.App.Ref@${VER}" --source "$FEED" + +# Search ASP.NET Core ref pack +dnx dotnet-inspect -y -- find "*Zstandard*" --package "Microsoft.AspNetCore.App.Ref@${VER}" --source "$FEED" +``` + +**Verify a type's members:** + +```bash +dnx dotnet-inspect -y -- member RegexOptions --package "Microsoft.NETCore.App.Ref@${VER}" --source "$FEED" -k field +``` + +**Diff between versions:** + +```bash +dnx dotnet-inspect -y -- diff --package "Microsoft.NETCore.App.Ref@11.0.0-preview.2..11.0.0-preview.3" --source "$FEED" +``` + +### Report what you verified against + +Always note the package version in a markdown comment so reviewers know the verification is grounded: + +```markdown + +``` + +This makes it possible to distinguish "API doesn't exist" from "verified against a stale build." + +## When to verify + +Verify **every** API name that appears in: + +- Prose text (e.g., "The new `ProcessStartOptions` class...") +- Code samples (every type, method, and property used) +- Before/after comparisons (both the old and new names) + +You do NOT need to verify: + +- PR numbers and URLs (these come from `changes.json` and are authoritative) +- General concepts (e.g., "Zstandard compression" as a concept vs `ZstandardCompressionProvider` as a type) +- CLI flags (e.g., `dotnet test --artifacts-path`) + +## What to do when verification fails + +If `dotnet-inspect` can't find a type: + +1. **Check your package version first** — are you querying a Preview N-1 package while writing Preview N notes? Find the correct version (see above). +2. **Named differently** — search with a broader pattern (`find "*Zstd*"` instead of the exact name). +3. **Reverted** — check `changes.json` for a revert PR. If the API was added and then reverted in the same preview, it didn't ship. Don't document it. +4. **Internal** — the type may not be public. Don't document internal types in release notes. +5. **Read the PR tests** — the PR's test files are ground truth. Tests compile and run against the actual API surface. Derive code samples from test assertions rather than guessing type names. + +When in doubt, describe the feature without naming specific types and link to the PR. A correct prose description is always better than a wrong code sample. diff --git a/.github/skills/release-notes/references/changes-schema.md b/.github/skills/release-notes/references/changes-schema.md new file mode 100644 index 00000000000..ce095737b25 --- /dev/null +++ b/.github/skills/release-notes/references/changes-schema.md @@ -0,0 +1,148 @@ +# changes.json Schema + +Reference for the `changes.json` file produced by `dotnet-release generate changes`. One file per release milestone. + +## Overview + +`changes.json` is a comprehensive, machine-readable manifest of every PR and commit that shipped in a release. It is the companion to the editorial markdown release notes — the JSON tells you **everything that shipped**, the markdown tells you **what matters**. + +It also serves as a companion to `cve.json` — both files use the same `commits{}` structure and `repo@shortcommit` key format, enabling cross-file joins. + +## File location + +```text +release-notes/{major.minor}/preview/{previewN}/changes.json # previews +release-notes/{major.minor}/{major.minor.patch}/changes.json # patches +``` + +## Top-level structure + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `release_version` | string | e.g., `"11.0.0-preview.3"` | +| `release_date` | string | ISO 8601, e.g., `"2026-04-08"` | +| `changes` | array | The change entries | +| `commits` | object | Normalized commit metadata, keyed by `repo@shortcommit` | + +## Change entry fields + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `id` | string | Globally unique identifier — `repo@shortcommit` format (e.g., `"runtime@c5d5be4"`) | +| `repo` | string | Short repository name (e.g., `"runtime"`) | +| `product` | string | Product slug (e.g., `"dotnet-runtime"`); absent for infra repos | +| `title` | string | PR title; `""` if not available | +| `url` | string | Public GitHub PR URL; `""` if non-public | +| `commit` | string | Key into top-level `commits{}` dict — the VMR (`dotnet/dotnet`) codeflow commit | +| `is_security` | bool | `true` if this is a security change | +| `local_repo_commit` | string | Key into `commits{}` — the source repo commit (same as `id`) | +| `labels` | array | PR labels (only present when `--labels` is used) | + +The `product` field is derived from the repo-level [component mapping](component-mapping.md). Infra repos like `arcade` and `symreader` have no `product` field. The `repo` field always matches the VMR manifest path. + +The `commit` field is the VMR codeflow commit in `dotnet/dotnet` that synced this change. Multiple source PRs typically map to the same VMR commit (codeflow batches changes). The `url` field links to the source-repo PR — together these provide both the product view (commit) and the development view (url). + +## Commit entry fields (values in `commits{}`) + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `repo` | string | Short repository name | +| `branch` | string | Branch the commit landed on | +| `hash` | string | Full 40-character commit hash | +| `org` | string | GitHub organization (e.g., `"dotnet"`) | +| `url` | string | `.diff`-form commit URL | + +## Conventions + +- **No nulls** — required fields are always present. Optional fields (`product`, `labels`, `local_repo_commit`) may be absent. Use `""` for missing strings, `0` for missing integers. +- **Naming** — `snake_case_lower` for JSON fields, `kebab-case-lower` for file names and repo slugs. +- **Public URLs only** — every URL must resolve publicly. +- **Commit URLs use `.diff` form** — for machine consumption. + +## Example + +```json +{ + "release_version": "11.0.0-preview.3", + "release_date": "", + "changes": [ + { + "id": "runtime@b2d5fa8", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Add JsonSerializerOptions.Web preset", + "url": "https://github.com/dotnet/runtime/pull/112345", + "commit": "dotnet@a1b2c3d", + "is_security": false, + "local_repo_commit": "runtime@b2d5fa8" + }, + { + "id": "aspnetcore@f45f3c9", + "repo": "aspnetcore", + "product": "dotnet-aspnetcore", + "title": "Add MapStaticAssets middleware", + "url": "https://github.com/dotnet/aspnetcore/pull/54321", + "commit": "dotnet@a1b2c3d", + "is_security": false, + "local_repo_commit": "aspnetcore@f45f3c9" + } + ], + "commits": { + "runtime@b2d5fa8": { + "repo": "runtime", + "branch": "main", + "hash": "b2d5fa8ca2f9c2d7e8f1f0a9d3d0f08e31c0ad5f", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/b2d5fa8ca2f9c2d7e8f1f0a9d3d0f08e31c0ad5f.diff" + }, + "aspnetcore@f45f3c9": { + "repo": "aspnetcore", + "branch": "main", + "hash": "f45f3c916df91e3fb175c85ba4a4f58ec1f77ef0", + "org": "dotnet", + "url": "https://github.com/dotnet/aspnetcore/commit/f45f3c916df91e3fb175c85ba4a4f58ec1f77ef0.diff" + }, + "dotnet@a1b2c3d": { + "repo": "dotnet", + "branch": "main", + "hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0.diff" + } + } +} +``` + +Note that both changes share the same `commit` — they were synced to the VMR in a single codeflow batch. The `local_repo_commit` values differ since each originated from a different source repository. + +## Querying changes.json + +```bash +# All changes +jq -r '.changes[] | .title' changes.json + +# Changes by product +jq -r '.changes[] | select(.product == "dotnet-runtime") | .title' changes.json + +# Changes by repo +jq -r '.changes[] | select(.repo == "runtime") | .title' changes.json + +# Count changes per repo +jq -r '[.changes[] | .repo] | group_by(.) | map({repo: .[0], count: length}) | sort_by(-.count)[]' changes.json + +# Security changes +jq -r '.changes[] | select(.is_security) | .title' changes.json + +# Cross-file join with cve.json (shared commit key format) +jq -r '.changes[] | select(.is_security) | .local_repo_commit' changes.json +# → use these keys to look up CVE IDs in cve.json's cve_commits{} +``` + +## Relationship to markdown release notes + +`changes.json` is the **input** to the editorial process. The markdown release notes are a curated subset: + +- `changes.json` has an entry for every PR that shipped +- Markdown only covers features worth calling out +- The agent reads `changes.json` to know what shipped, then decides what to write about +- If a feature isn't in `changes.json`, it must not appear in the markdown diff --git a/.github/skills/release-notes/references/component-mapping.md b/.github/skills/release-notes/references/component-mapping.md new file mode 100644 index 00000000000..70204ae09b7 --- /dev/null +++ b/.github/skills/release-notes/references/component-mapping.md @@ -0,0 +1,80 @@ +# Component Mapping + +## Repo-to-component mapping + +Uses the `repo` field from `changes.json` (which matches `source-manifest.json` `path` values) to identify components. + +| Manifest Path | Component | Source Repo | Release Notes File | +| ------------- | --------- | ----------- | ------------------ | +| `runtime` | .NET Libraries | `dotnet/runtime` | `libraries.md` | +| `runtime` | .NET Runtime | `dotnet/runtime` | `runtime.md` | +| `aspnetcore` | ASP.NET Core | `dotnet/aspnetcore` | `aspnetcore.md` | +| `razor` | ASP.NET Core (Razor) | `dotnet/razor` | `aspnetcore.md` | +| `sdk` | .NET SDK | `dotnet/sdk` | `sdk.md` | +| `templating` | .NET SDK (Templating) | `dotnet/templating` | `sdk.md` | +| `msbuild` | MSBuild | `dotnet/msbuild` | `msbuild.md` | +| `winforms` | Windows Forms | `dotnet/winforms` | `winforms.md` | +| `wpf` | WPF | `dotnet/wpf` | `wpf.md` | +| `efcore` | EF Core | `dotnet/efcore` | `efcore.md` | +| `roslyn` | C# / Visual Basic | `dotnet/roslyn` | `csharp.md` | +| `fsharp` | F# | `dotnet/fsharp` | `fsharp.md` | +| `nuget-client` | NuGet | `nuget/nuget.client` | `nuget.md` | + +### Runtime sub-component classification + +The `runtime` manifest entry covers both Libraries and Runtime. When writing markdown, classify PRs by the files they changed: + +| VMR Path Prefix | Sub-component | Output File | +| --------------- | ------------- | ----------- | +| `src/runtime/src/libraries/` | Libraries | `libraries.md` | +| `src/runtime/src/coreclr/` | Runtime (CoreCLR) | `runtime.md` | +| `src/runtime/src/mono/` | Runtime (Mono) | `runtime.md` | +| `src/runtime/src/native/` | Runtime (Native) | `runtime.md` | + +### Components that share output files + +- **Razor → ASP.NET Core** — `dotnet/razor` PRs go in `aspnetcore.md` +- **Templating → SDK** — `dotnet/templating` PRs go in `sdk.md` +- **Roslyn** — covers both C# and Visual Basic. Check PR labels/titles to determine language. Produce `csharp.md` (and `visualbasic.md` if VB-specific features exist). + +### Infrastructure components (skip for release notes) + +These appear in `source-manifest.json` but rarely produce user-facing changes: + +| Manifest Path | Repo | Notes | +| ------------- | ---- | ----- | +| `arcade` | `dotnet/arcade` | Build infrastructure | +| `cecil` | `dotnet/cecil` | IL manipulation library (internal) | +| `command-line-api` | `dotnet/command-line-api` | CLI parsing (internal) | +| `deployment-tools` | `dotnet/deployment-tools` | Deployment tooling | +| `diagnostics` | `dotnet/diagnostics` | Diagnostic tools | +| `emsdk` | `dotnet/emsdk` | Emscripten SDK | +| `scenario-tests` | `dotnet/scenario-tests` | Test infrastructure | +| `source-build-reference-packages` | `dotnet/source-build-reference-packages` | Source build | +| `sourcelink` | `dotnet/sourcelink` | Source Link | +| `symreader` | `dotnet/symreader` | Symbol reader | +| `windowsdesktop` | `dotnet/windowsdesktop` | Metapackage | +| `vstest` | `microsoft/vstest` | Test platform (microsoft org — skipped) | +| `xdt` | `dotnet/xdt` | XML transforms | + +These components appear in `changes.json` for completeness but typically don't warrant markdown release notes. + +## Expected output files per preview + +Every preview should produce these files (stubs for components with no noteworthy changes): + +```text +README.md # Index/TOC linking to all component files +libraries.md # System.* BCL APIs +runtime.md # CoreCLR, Mono, GC, JIT +aspnetcore.md # ASP.NET Core, Blazor, SignalR +sdk.md # CLI, build, project system, NuGet +efcore.md # Entity Framework Core +csharp.md # C# language features +fsharp.md # F# language and compiler +winforms.md # Windows Forms +wpf.md # WPF +msbuild.md # MSBuild +nuget.md # NuGet client +changes.json # Machine-readable change manifest +``` diff --git a/.github/skills/release-notes/references/editorial-rules.md b/.github/skills/release-notes/references/editorial-rules.md new file mode 100644 index 00000000000..186ec7d5280 --- /dev/null +++ b/.github/skills/release-notes/references/editorial-rules.md @@ -0,0 +1,132 @@ +# Editorial Rules + +Tone, attribution, and content guidelines for .NET release notes. + +## Tone + +- **Positive** — highlight what's new, don't dwell on what was missing + - ✅ `ProcessExitStatus provides a unified representation of how a process terminated.` + - ❌ `Previously, there was no way to determine how a process terminated.` +- When context about the prior state is needed, keep it brief — one clause, then pivot to the new capability +- **Don't editorialize beyond the facts** — state what concretely changed ("enabled by default", "no longer requires opt-in") rather than making claims you can't back up ("ready for production use", "signals maturity"). If a PR removes a preview attribute, say that. Don't interpret it as a promise. + - ✅ `Runtime-async is now enabled by default for anyone targeting net11.0.` + - ❌ `This signals that runtime-async is ready for production use.` +- **Prefer short, direct sentences** — If a sentence has a parenthetical clause (`which...`, `where...`, `that...`) longer than a few words, split it into two sentences. Lead with the news, follow with context. Long sentences are fine when they flow as a single continuous thought (what → why); they're not fine when a subordinate clause interrupts the main verb. + - ✅ `Runtime-async is now enabled for NativeAOT. This eliminates the state-machine overhead of async/await for ahead-of-time compiled applications.` + - ❌ `The runtime-async feature, which eliminates the state-machine overhead of async/await, is now enabled for NativeAOT.` + - ✅ `The JIT now generates ARM64 SM4 and SHA3 instructions directly, enabling hardware-accelerated implementations on capable processors.` (long but flows — one thought) + +## Entry naming + +- Prefer a **brief description** of what the feature does over the API name alone + - ✅ `## Support for Zstandard compression` + - ✅ `## Faster time zone conversions` + - ❌ `## ZstandardStream` + - ❌ `## TimeZoneInfo performance` +- Keep headings concise — 3–8 words + +## Benchmarks + +- Use **exact data** from PR descriptions — never round, approximate, or paraphrase +- State what was measured and the hardware/workload context +- Include specific before/after measurements when compelling +- Do **not** embed full BenchmarkDotNet tables — summarize in prose + +## What to include + +- **The 20/80 rule** — at least 20% of readers need to care about a feature. Write so the other 80% understand why those 20% care and why they might, too. A feature that only matters to a narrow audience can still earn its place if the writeup makes the value legible to everyone. +- **The two-sentence test** — if you can only write two sentences about a feature, it's probably an engineering fix, not a feature. Cut it. A community contribution or breaking change can lift a borderline entry, but "fixed an internal bug that happened to be visible" is not a feature. +- **Headlines should convey value** — a heading like "GC regions on macOS" doesn't tell the reader whether this is good or bad. Prefer headings that hint at the benefit: "GC regions enabled on macOS" or "Server GC memory model now available on macOS." +- **TODO for borderline entries** — when a feature might deserve inclusion but you lack data to justify it (benchmark numbers, real-world impact, user demand), keep the entry but add an HTML `` comment asking for the missing information. This is better than silently including a vague claim or silently cutting something that might matter. The TODO should state what's needed and link to the PR where the data might live. + +## Feature ordering + +Order features using three tiers, applied in order: + +1. **Broad interest first** — features most developers will care about go at the top. A new `RegexOptions` value ranks above an ARM64-only instruction set. +2. **Cluster related features** — group related items together even if they differ in importance. All Regex work in one block, all System.Text.Json work in another, all JIT work together. Readers scan by area. +3. **Alphabetical within a cluster** — when features within a cluster have roughly equal weight, alphabetical order makes them scannable. + +Use PR and issue reaction counts as a signal for tier 1, but apply judgment — a niche feature with 100 reactions may still rank below a broadly useful one with 10. + +## Community attribution + +### Inline + +When a documented feature was contributed externally: + +```markdown +Thank you [@username](https://github.com/username) for this contribution! +``` + +### Community contributors section + +At the bottom of each component's notes, list ALL external contributors — not just those with documented features. Use the `community-contribution` label to identify them. + +**Vet the list** — the `community-contribution` label is sometimes wrong. Exclude usernames containing `-msft`, `-microsoft`, or other Microsoft suffixes. When in doubt about whether someone is a Microsoft employee, leave them out of the community list. + +```markdown +## Community contributors + +Thank you contributors! ❤️ + +- [@username](https://github.com///pulls?q=is%3Apr+is%3Amerged+author%3Ausername) +``` + +## Bug fixes section + +After features but before community contributors, include a grouped bug fix summary when there are noteworthy fixes: + +```markdown +## Bug fixes + +- **System.Net.Http** + - Fixed authenticated proxy credential handling ([dotnet/runtime#123363](https://github.com/dotnet/runtime/issues/123363)) +- **System.Collections** + - Fixed integer overflow in ImmutableArray range validation ([dotnet/runtime#124042](https://github.com/dotnet/runtime/pull/124042)) +``` + +Group by namespace/area. Don't include test-only, CI, or infra fixes. + +## Preview-to-preview feedback fixes + +Include a bug fix when ALL of these apply: + +1. The issue was filed after the previous preview shipped +2. It was reported by someone outside the team +3. A fix shipped in the current preview + +Frame positively: "Based on community feedback, X now does Y." + +## Preview-to-preview deduplication + +When prior previews already documented a feature, don't repeat the same information. But **do** document significant state changes in the current preview. A feature that was introduced as opt-in in P1 and is now enabled by default in P3 is new news — document the change in state, not the feature from scratch. + +Ask: "What changed about this feature since the last preview's release notes?" If the answer is meaningful to users (enabled by default, no longer experimental, major perf improvement, new sub-features), write about that. If the answer is just "more PRs landed in the same area," skip it. + +Examples: +- ✅ "Runtime-async is now enabled by default for anyone targeting `net11.0`." (state change: opt-in → default) +- ✅ "The Regex source generator now handles alternations 3× faster." (new perf data) +- ❌ Repeating the full explanation of what runtime-async is from P1 (already documented) +- ❌ "More JIT optimizations landed this preview." (no specific news) + +## Filtered features + +When you cut a feature for failing the 20/80 rule or two-sentence test, record it in an HTML comment block in the output file. This creates a learning record — future runs and human reviewers can see what was considered and why it was excluded. + +Place the comment block immediately before the Bug fixes section: + +```html + +``` + +Good filter reasons: +- **Internal infrastructure** — "Implementation detail of the Mono → CoreCLR unification. Developers don't target the interpreter." +- **Too narrow** — "Only affects COM interop startup — very narrow audience." +- **Engineering fix, not a feature** — "Two sentences max. No user-visible behavior change beyond a perf number." +- **Provider extensibility** — "Only matters to database provider authors, not EF Core users." + +The comment is invisible to readers but preserved in the file for the next person (or agent) who reviews the notes. diff --git a/.github/skills/release-notes/references/examples/README.md b/.github/skills/release-notes/references/examples/README.md new file mode 100644 index 00000000000..84b3eb31bb3 --- /dev/null +++ b/.github/skills/release-notes/references/examples/README.md @@ -0,0 +1,26 @@ +# Release Notes Examples + +Curated examples from previous .NET release notes, organized by component. Load only the file relevant to the component you're writing. + +| File | Component | Styles shown | +| ---- | --------- | ------------ | +| [runtime.md](runtime.md) | JIT, GC, CoreCLR | Benchmark narrative, assembly comparison, metric-heavy prose | +| [aspnetcore.md](aspnetcore.md) | ASP.NET Core, Blazor | Short behavior change, medium workaround, long community feature | +| [csharp.md](csharp.md) | C# language | Code-first with rule bullets | +| [sdk.md](sdk.md) | SDK, CLI, containers | Problem/solution with community attribution | +| [libraries.md](libraries.md) | BCL, System.* APIs | Subheadings, diff blocks, multiple API examples | + +## Principles + +1. **Length matches importance** — a config option gets 3 sentences; a cryptography overhaul gets subheadings and multiple code blocks +2. **Lead with what changed** — don't bury the lede with background paragraphs +3. **Prefer active voice** — "The JIT now eliminates bounds checks" not "Bounds checks are now eliminated by the JIT." Passive voice is fine occasionally but active is the strong default +4. **Code shows, prose explains** — when there's an API, show it; use prose for the why/when +5. **Title, Content, Credit** — explain the feature first, credit contributors at the end. Don't restructure a release note into a guest introduction. See the StatusCodeSelector example (good) vs. the PAR example (anti-pattern) in aspnetcore.md +6. **Diff format for improvements** — when a feature simplifies existing code, `diff` blocks make the improvement immediately visible +7. **Assembly comparisons for JIT work** — before/after `asm` blocks let readers count the instructions eliminated +8. **Progressive benchmarks tell a story** — multiple benchmark tables showing incremental improvement are more compelling than a single number +9. **Ask for what you can't generate** — benchmark data, definitive samples, and domain context come from humans. A placeholder with a request is better than a fabrication +10. **Workarounds are welcome** — if a change might break someone, say so and give the escape hatch +11. **Link to PRs and issues** — use the `org/repo #number` format: `[dotnet/runtime #115977](https://github.com/dotnet/runtime/pull/115977)`. Links give readers provenance and let them dig deeper +12. **Avoid jargon and ambiguous words** — "snippet" in a programming context suggests code. Say what you mean plainly. If a word could be misread, pick a simpler one diff --git a/.github/skills/release-notes/references/examples/aspnetcore.md b/.github/skills/release-notes/references/examples/aspnetcore.md new file mode 100644 index 00000000000..448bafe4800 --- /dev/null +++ b/.github/skills/release-notes/references/examples/aspnetcore.md @@ -0,0 +1,91 @@ +# ASP.NET Core Examples + +## `ExceptionHandlerMiddleware` option to choose the status code based on the exception + +A new option when configuring the `ExceptionHandlerMiddleware` allows app developers to choose what status code to return when an exception occurs during application request handling. The new option changes the status code being set in the `ProblemDetails` response from the `ExceptionHandlerMiddleware`. + +```csharp +app.UseExceptionHandler(new ExceptionHandlerOptions +{ + StatusCodeSelector = ex => ex is TimeoutException + ? StatusCodes.Status503ServiceUnavailable + : StatusCodes.Status500InternalServerError, +}); +``` + +Thanks to [@latonz](https://github.com/latonz) for contributing this new option! + +--- +Source: [.NET 9 Preview 7 — ASP.NET Core](../../../../../release-notes/9.0/preview/preview7/aspnetcore.md) +Commentary: Short and focused — one paragraph of context, a tight code snippet, and community credit. +Why it works: The code speaks for itself. The reader sees exactly what the API looks like and can immediately use it. No explanation needed beyond "what" and "why." + +--- + +## Use PipeReader support in System.Text.Json + +MVC, Minimal APIs, and the `HttpRequestJsonExtensions.ReadFromJsonAsync` methods have all been updated to use the new PipeReader support in System.Text.Json without requiring any code changes from applications. + +For the majority of applications this should have no impact on behavior. However, if the application is using a custom `JsonConverter`, there is a chance that the converter doesn't handle [Utf8JsonReader.HasValueSequence](https://learn.microsoft.com/dotnet/api/system.text.json.utf8jsonreader.hasvaluesequence) correctly. This can result in missing data and errors like `ArgumentOutOfRangeException` when deserializing. + +The quick workaround (especially if you don't own the custom `JsonConverter` being used) is to set the `"Microsoft.AspNetCore.UseStreamBasedJsonParsing"` [AppContext](https://learn.microsoft.com/dotnet/api/system.appcontext) switch to `true`. This should be a temporary workaround and the `JsonConverter`(s) should be updated to support `HasValueSequence`. + +To fix `JsonConverter` implementations, there is the quick fix which allocates an array from the `ReadOnlySequence`: + +```csharp +public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) +{ + var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + // previous code +} +``` + +--- +Source: [.NET 10 Preview 7 — ASP.NET Core](../../../../../release-notes/10.0/preview/preview7/aspnetcore.md) +Commentary: Medium — behavioral change that's mostly invisible but has an edge case. Gives the workaround and the proper fix. +Why it works: Acknowledges most users won't notice, identifies who *will* be affected, gives an immediate workaround, then the proper fix. +Style note: The opening phrase "without requiring any code changes from applications" is awkward — it personifies the application. Prefer "without needing application changes." Simpler, active, shorter. + +--- + +## `OpenIdConnectHandler` support for Pushed Authorization Requests (PAR) + +We'd like to thank @josephdecock from @DuendeSoftware for adding Pushed Authorization Requests (PAR) to ASP.NET Core's `OpenIdConnectHandler`. Joe described the background and motivation for enabling PAR in [his API proposal](https://github.com/dotnet/aspnetcore/issues/51686) as follows: + +> Pushed Authorization Requests (PAR) is a relatively new [OAuth standard](https://datatracker.ietf.org/doc/html/rfc9126) that improves the security of OAuth and OIDC flows by moving authorization parameters from the front channel to the back channel (that is, from redirect URLs in the browser to direct machine to machine http calls on the back end). +> +> This prevents an attacker in the browser from: +> +> - Seeing authorization parameters (which could leak PII) and from +> - Tampering with those parameters (e.g., the attacker could change the scope of access being requested). +> +> The use of PAR is encouraged by the [FAPI working group](https://openid.net/wg/fapi/) within the OpenID Foundation. + +PAR is now enabled by default if the identity provider's discovery document advertises support for it. The identity provider's discovery document is usually found at `.well-known/openid-configuration`. This change should provide enhanced security for providers that support PAR. If this causes problems, disable PAR via `OpenIdConnectOptions.PushedAuthorizationBehavior` as follows: + +```csharp +builder.Services + .AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(oidcOptions => + { + // The default value is PushedAuthorizationBehavior.UseIfAvailable. + oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable; + }); +``` + +To ensure that authentication only succeeds if PAR is used, use `PushedAuthorizationBehavior.Require`. + +This change also introduces a new `OnPushAuthorization` event to `OpenIdConnectEvents` which can be used to customize the pushed authorization request or handle it manually. Refer to the [API proposal](https://github.com/dotnet/aspnetcore/issues/51686) for more details. + +--- +Source: [.NET 9 Preview 7 — ASP.NET Core](../../../../../release-notes/9.0/preview/preview7/aspnetcore.md) +Commentary: Long — community contribution with the contributor's own words explaining significance. Good template for features with security or compliance importance. +Why it works: The block quote lets the contributor explain the significance — more credible than paraphrasing. Then practical: default behavior, how to disable, how to require. +Anti-pattern: The opening ("We'd like to thank...Joe described...as follows") turns the release note into a guest introduction. Prefer the **Title, Content, Credit** pattern — explain the feature first, give credit at the end (like the StatusCodeSelector example above). Credit is good; restructuring the entire note around it is not. + +--- diff --git a/.github/skills/release-notes/references/examples/csharp.md b/.github/skills/release-notes/references/examples/csharp.md new file mode 100644 index 00000000000..17089a372ff --- /dev/null +++ b/.github/skills/release-notes/references/examples/csharp.md @@ -0,0 +1,36 @@ +# C# Examples + +## Extension operators + +The new extension blocks now include support for *operators*, with the exception of implicit and explicit conversion operators. You can declare operators in extension blocks, as shown in the following example: + +```csharp +public static class Operators +{ + extension(TElement[] source) where TElement : INumber + { + public static TElement[] operator *(TElement[] vector, TElement scalar) { ... } + public static TElement[] operator *(TElement scalar, TElement[] vector) { ... } + public void operator *=(TElement scalar) { ... } + public static bool operator ==(TElement[] left, TElement[] right) { ... } + public static bool operator !=(TElement[] left, TElement[] right) { ... } + } +} +``` + +Several of the rules for extension operators are demonstrated in the preceding example: + +- At least one of the operands must be the *extended type*, `TElement[]` in the preceding example. +- For operators that require pair-wise declarations, such as `==` and `!=` in the preceding example, both operators must be declared in the same static class. They are not required to be in the same `extension` container. +- Instance compound assignment operators, and instance increment and decrement operators are expected to mutate the current instance. Therefore reference type parameters must be passed by value and value type parameters must be passed by `ref`. These operators can't be declared when the extended type is an unconstrained type parameter. +- Extension operators, like other extension members, can't use the `abstract`, `virtual`, `override`, or `sealed` modifiers. This is consistent with other extension members. + +Extension members are only considered for overload resolution when no applicable predefined or user defined non-extension members are found. + +--- +Source: [.NET 10 Preview 7 — C#](../../../../../release-notes/10.0/preview/preview7/csharp.md) +Commentary: Code-first — one sentence of context, then the syntax, then rule bullets. Language features need to be seen, not just described. +Why it works: Shows the code immediately. The rule bullets follow naturally as "here's what you need to know." No motivation section needed. +Style note: Line 21 ("Several of the rules for extension operators are demonstrated in the preceding example:") is stiff. Prefer something direct like "The rules for extension operators:" or "Extension operator rules:" — the reader can see the example is right above. + +--- diff --git a/.github/skills/release-notes/references/examples/libraries.md b/.github/skills/release-notes/references/examples/libraries.md new file mode 100644 index 00000000000..05fe1d5234e --- /dev/null +++ b/.github/skills/release-notes/references/examples/libraries.md @@ -0,0 +1,57 @@ +# Libraries Examples + +## Post-Quantum Cryptography Updates + +### ML-DSA + +The `System.Security.Cryptography.MLDsa` class gained ease-of-use updates in this release, allowing some common code patterns to be simplified: + +```diff +private static byte[] SignData(string privateKeyPath, ReadOnlySpan data) +{ + using (MLDsa signingKey = MLDsa.ImportFromPem(File.ReadAllBytes(privateKeyPath))) + { +- byte[] signature = new byte[signingKey.Algorithm.SignatureSizeInBytes]; +- signingKey.SignData(data, signature); ++ return signingKey.SignData(data); +- return signature; + } +} +``` + +Additionally, this release added support for HashML-DSA, which we call "PreHash" to help distinguish it from "pure" ML-DSA. As the underlying specification interacts with the Object Identifier (OID) value, the SignPreHash and VerifyPreHash methods on this `[Experimental]` type take the dotted-decimal OID as a string. This may evolve as more scenarios using HashML-DSA become well-defined. + +```csharp +private static byte[] SignPreHashSha3_256(MLDsa signingKey, ReadOnlySpan data) +{ + const string Sha3_256Oid = "2.16.840.1.101.3.4.2.8"; + return signingKey.SignPreHash(SHA3_256.HashData(data), Sha3_256Oid); +} +``` + +### Composite ML-DSA + +This release also introduces new types to support ietf-lamps-pq-composite-sigs (currently at draft 7), and an implementation of the primitive methods for RSA variants. + +```csharp +var algorithm = CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss; +using var privateKey = CompositeMLDsa.GenerateKey(algorithm); + +byte[] data = [42]; +byte[] signature = privateKey.SignData(data); + +using var publicKey = CompositeMLDsa.ImportCompositeMLDsaPublicKey( + algorithm, privateKey.ExportCompositeMLDsaPublicKey()); +Console.WriteLine(publicKey.VerifyData(data, signature)); // True + +signature[0] ^= 1; // Tamper with signature +Console.WriteLine(publicKey.VerifyData(data, signature)); // False +``` + +--- +Source: [.NET 10 Preview 7 — Libraries](../../../../../release-notes/10.0/preview/preview7/libraries.md) +Commentary: Long — subheadings organize related-but-distinct features. Uses `diff` blocks for API simplification and complete runnable scenarios for new APIs. +Why it works: The `diff` block immediately shows the simplification (red lines removed, green line added). The Composite ML-DSA sample is a complete scenario (generate → sign → verify → tamper → verify-fails). +Style note: L7 "gained ease-of-use updates" is passive and vague. Prefer active/direct: "Common patterns using `MLDsa` are now easier:" or "Ease-of-use updates simplify common `MLDsa` patterns:". + +--- diff --git a/.github/skills/release-notes/references/examples/runtime.md b/.github/skills/release-notes/references/examples/runtime.md new file mode 100644 index 00000000000..31bcb0bc3f3 --- /dev/null +++ b/.github/skills/release-notes/references/examples/runtime.md @@ -0,0 +1,122 @@ +# Runtime Examples + +## Array Enumeration De-Abstraction + +Preview 1 brought enhancements to the JIT compiler's devirtualization abilities for array interface methods; this was our first step in reducing the abstraction overhead of array iteration via enumerators. Preview 2 continues this effort with improvements to many other optimizations. Consider the following benchmarks: + +```csharp +public class ArrayDeAbstraction +{ + static readonly int[] array = new int[512]; + + [Benchmark(Baseline = true)] + public int foreach_static_readonly_array() + { + int sum = 0; + foreach (int i in array) sum += i; + return sum; + } + + [Benchmark] + public int foreach_static_readonly_array_via_interface() + { + IEnumerable o = array; + int sum = 0; + foreach (int i in o) sum += i; + return sum; + } +} +``` + +In `foreach_static_readonly_array`, the type of `array` is transparent, so it is easy for the JIT to generate efficient code. In `foreach_static_readonly_array_via_interface`, the type of `array` is hidden behind an `IEnumerable`, introducing an object allocation and virtual calls for advancing and dereferencing the iterator. In .NET 9, this overhead impacts performance profoundly: + +| Method | Mean | Ratio | Allocated | +|------------------------------------------------------------- |-----------:|------:|----------:| +| foreach_static_readonly_array (.NET 9) | 150.8 ns | 1.00 | - | +| foreach_static_readonly_array_via_interface (.NET 9) | 851.8 ns | 5.65 | 32 B | + +Thanks to improvements to the JIT's inlining, stack allocation, and loop cloning abilities (all of which are detailed in [dotnet/runtime #108913](https://github.com/dotnet/runtime/issues/108913)), the object allocation is gone, and runtime impact has been reduced substantially: + +| Method | Mean | Ratio | Allocated | +|------------------------------------------------------------- |-----------:|------:|----------:| +| foreach_static_readonly_array (.NET 9) | 150.8 ns | 1.00 | - | +| foreach_static_readonly_array_via_interface (.NET 10) | 280.0 ns | 1.86 | - | + +--- +Source: [.NET 10 Preview 2 — Runtime](../../../../../release-notes/10.0/preview/preview2/runtime.md) +Commentary: Progressive benchmark narrative — the best style for JIT optimization stories. +Why it works: Multiple benchmark tables tell a story (problem → partial fix → harder problem → better fix). Each table shows measurable improvement. The reader follows the JIT's reasoning through escalating complexity. +Style note: L5 buries the lede with "Preview 1 brought enhancements to..." — passive and backward-looking. Better: "A major focus this release is reducing the abstraction overhead of array iteration via enumerators. We delivered improvements in Preview 1 for array interface methods. In this preview, we've continued on that theme." Active voice, states the goal upfront, then the progression. + +--- + +## Improved Code Generation for Struct Arguments + +.NET's JIT compiler is capable of an optimization called physical promotion, where the members of a struct are placed in registers rather than on the stack, eliminating memory accesses. This optimization is particularly useful when passing a struct to a method, and the calling convention requires the struct members to be passed in registers. Consider the following example: + +```csharp +struct Point +{ + public int X; + public int Y; + public Point(int x, int y) { X = x; Y = y; } +} + +[MethodImpl(MethodImplOptions.NoInlining)] +private static void Consume(Point p) => Console.WriteLine(p.X + p.Y); + +private static void Main() +{ + Point p = new Point(10, 20); + Consume(p); +} +``` + +Because `ints` are four bytes wide, and registers are eight bytes wide on x64, the calling convention requires us to pass the members of `Point` in one register. However, the JIT compiler's internal representation of struct members previously wasn't flexible enough to represent values that share a register. Thus, the JIT compiler would first store the values to memory, and then load the eight-byte chunk into a register: + +```asm +Program:Main() (FullOpts): + push rax + mov dword ptr [rsp], 10 + mov dword ptr [rsp+0x04], 20 + mov rdi, qword ptr [rsp] + call [Program:Consume(Program+Point)] + nop + add rsp, 8 + ret +``` + +Thanks to [dotnet/runtime #115977](https://github.com/dotnet/runtime/pull/115977), the JIT compiler can now place the promoted members of struct arguments into shared registers: + +```asm +Program:Main() (FullOpts): + mov rdi, 0x140000000A + tail.jmp [Program:Consume(Program+Point)] +``` + +--- +Source: [.NET 10 Preview 6 — Runtime](../../../../../release-notes/10.0/preview/preview6/runtime.md) +Commentary: Before/after assembly comparison — the gold standard for codegen improvements. +Why it works: The reader can count the instructions eliminated. The assembly speaks for itself — no prose needed to explain the magnitude of the improvement. + +--- + +## JIT: Loop Optimizations + +RyuJIT already supports multiple powerful loop optimizations, and we plan to expand these capabilities for .NET 9. For Preview 1, we've focused on improving the applicability of existing optimizations by refactoring how loops are represented in RyuJIT. This new graph-based representation is simpler and more effective than the old lexical representation, enabling RyuJIT to recognize -- and thus optimize -- more loops. + +Here's a quick breakdown of the improvements: + +- **Loop hoisting** -- finds expressions that don't change in value as the containing loop iterates, and moves (or "hoists") the expressions to above the loop so they evaluate at most once. In our test collections, we saw up to 35.8% more hoisting performed with the new loop representation. +- **Loop cloning** -- determines if a conditional check (like a bounds check on an array) inside a loop can be safely eliminated for some of its iterations, and creates a "fast" copy of the loop without the check. With the new loop representation, we saw up to 7.3% more loop cloning. +- **Loop alignment** -- improves instruction cache performance by adjusting the offset of a loop to begin at a cache line. With the new loop representation, we saw about 5% more loops aligned across our test collections. + +This is just a snippet of the improvements RyuJIT's new loop representation brings. To take a closer look at the loop optimization work planned for .NET 9, check out [dotnet/runtime #93144](https://github.com/dotnet/runtime/issues/93144). + +--- +Source: [.NET 9 Preview 1 — Runtime](../../../../../release-notes/9.0/preview/preview1/runtime.md) +Commentary: Metric-heavy prose with no code — good for infrastructure improvements where breadth of impact matters more than a single before/after. +Why it works: Percentages sell the improvement without needing code. Each bullet is self-contained. Links to the tracking issue for depth. +Style note: L111 "snippet" is ambiguous in a programming context — readers may expect a code snippet. Prefer plain language like "a sample of" or "some of." + +--- diff --git a/.github/skills/release-notes/references/examples/sdk.md b/.github/skills/release-notes/references/examples/sdk.md new file mode 100644 index 00000000000..d8efba4a95b --- /dev/null +++ b/.github/skills/release-notes/references/examples/sdk.md @@ -0,0 +1,21 @@ +# SDK Examples + +## Container publishing improvements for insecure registries + +The SDK's built-in container publishing support can publish images to container registries, but until this release those registries were required to be secured - they needed HTTPS support and valid certificates for the .NET SDK to work. +Container engines can usually be configured to work with insecure registries as well - meaning registries that do not have TLS configured, or have TLS configured with a certificate that is invalid from the perspective of the container engine. This is a valid use case, but our tooling didn't support this mode of communication. + +In this release, [@tmds](https://github.com/tmds) enabled the SDK [to communicate with insecure registries](https://github.com/dotnet/sdk/pull/41506). + +Requirements (depending on your environment): + +* [Configure the Docker CLI to mark a registry as insecure](https://docs.docker.com/reference/cli/dockerd/#insecure-registries) +* [Configure Podman to mark a registry as insecure](https://podman-desktop.io/docs/containers/registries) +* Use the `DOTNET_CONTAINER_INSECURE_REGISTRIES` environment variable to pass a semicolon-delimited list of registry domains to treat as insecure + +--- +Source: [.NET 9 Preview 7 — SDK](../../../../../release-notes/9.0/preview/preview7/sdk.md) +Commentary: Short problem/solution with community attribution and a tight requirements list. +Why it works: The community contributor is credited naturally mid-sentence. The requirements list gives actionable next steps without over-explaining. + +--- diff --git a/.github/skills/release-notes/references/format-template.md b/.github/skills/release-notes/references/format-template.md new file mode 100644 index 00000000000..01fb9e17390 --- /dev/null +++ b/.github/skills/release-notes/references/format-template.md @@ -0,0 +1,119 @@ +# Format Template + +Standard document structure for .NET release notes markdown files. + +## Document structure + +### Component file + +```markdown +# in .NET - Release Notes + +.NET includes new features & enhancements: + +- [Feature Name](#anchor) +- [Feature Name](#anchor) + +## Feature Name + + ([/#NNNNN](https://github.com///pull/NNNNN)). + +## Bug fixes + +- **Category** — Fix description + +## Community contributors + +- [@username](https://github.com/username) +``` + +### README.md (index file) + +The README.md links to all component files and includes the general docs link. Component files do NOT include the general "What's new" link — that goes in the README only. + +```markdown +# .NET - Release Notes + +- [Libraries](libraries.md) +- [Runtime](runtime.md) +- [ASP.NET Core](aspnetcore.md) +... + +.NET updates: + +- [What's new in .NET ](https://learn.microsoft.com/dotnet/core/whats-new/dotnet-/overview) + +## Release information + +| | Version | +| --- | --- | +| Runtime | | +| SDK | | + +### VMR refs + +These release notes were generated from the [dotnet/dotnet](https://github.com/dotnet/dotnet) VMR: + +- **Base**: [``](https://github.com/dotnet/dotnet/tree/) +- **Head**: [``](https://github.com/dotnet/dotnet/tree/) +``` + +Read the runtime version, SDK version, base ref, and head ref from `build-metadata.json`. + +### Component-specific docs links + +Some components have their own "What's new" page on learn.microsoft.com. Include these in the relevant component file when they exist. Discover them from the docs overview source: + +`https://github.com/dotnet/docs/raw/refs/heads/main/docs/core/whats-new/dotnet-{major}/overview.md` + +Known component docs links: + +| Component | Docs URL | +| --------- | -------- | +| Runtime | `https://learn.microsoft.com/dotnet/core/whats-new/dotnet-{major}/runtime` | +| Libraries | `https://learn.microsoft.com/dotnet/core/whats-new/dotnet-{major}/libraries` | +| SDK | `https://learn.microsoft.com/dotnet/core/whats-new/dotnet-{major}/sdk` | +| ASP.NET Core | `https://learn.microsoft.com/aspnet/core/release-notes/aspnetcore-{major}` | +| C# | `https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-{lang-version}` | +| EF Core | `https://learn.microsoft.com/ef/core/what-is-new/ef-core-{major}.0/whatsnew` | + +## Section rules + +1. **TOC at top** — every feature gets a linked entry +2. **One paragraph of context** — what the feature does and why it matters, with PR/issue links +3. **Code sample** — show the feature in use +4. **Feature ordering** — highest customer impact first + +## Issue and PR references + +Always use markdown links with the `{org}/{repo}#{number}` format: + +- ✅ `[dotnet/runtime#124264](https://github.com/dotnet/runtime/pull/124264)` +- ❌ `dotnet/runtime#124264` (bare reference) + +## Minimal stub + +For components with no noteworthy changes: + +```markdown +# in .NET - Release Notes + +There are no new features or improvements in in this release. +``` + +## Example entry + +```markdown +## Finding Certificates By Thumbprints Other Than SHA-1 + +A new method on `X509Certificate2Collection` accepts the name of the hash algorithm to use +for thumbprint matching ([dotnet/runtime#NNNNN](https://github.com/dotnet/runtime/pull/NNNNN)). +``` + +Code sample example: + +```csharp +X509Certificate2Collection coll = store.Certificates.FindByThumbprint( + HashAlgorithmName.SHA256, thumbprint); +return coll.SingleOrDefault(); +``` diff --git a/.github/skills/release-notes/references/quality-bar.md b/.github/skills/release-notes/references/quality-bar.md new file mode 100644 index 00000000000..bbfeea1c0c7 --- /dev/null +++ b/.github/skills/release-notes/references/quality-bar.md @@ -0,0 +1,80 @@ +# Quality Bar + +What good .NET release notes look like. This is the north star — when in doubt, refer back here. + +## Two outputs, two standards + +### changes.json — comprehensive and mechanical + +Every PR that shipped gets an entry. No editorial judgment — if the `dotnet-release generate changes` tool found it in the source-manifest diff, it goes in. This is the machine-readable record of what shipped. + +Quality criteria: + +- **Complete** — every merged PR in the commit range is represented +- **Accurate** — PR numbers, titles, URLs, and commit hashes are correct +- **Classified** — `product` and `package` fields are populated where applicable +- **Joinable** — commit keys match the format used in `cve.json` for cross-file queries + +### Markdown release notes — curated and editorial + +Not every PR deserves a writeup. The markdown covers features users will care about. + +Quality criteria: + +- **High fidelity** — everything documented actually ships in this release +- **High value** — focused on what matters, not exhaustive +- **Useful** — a developer can read a section and start using the feature +- **Honest** — no marketing fluff, no exaggeration, no invented benchmarks + +## What to include in markdown + +Include a feature if it gives users something **new to try**, something that **works better**, or something they **asked for**: + +- New capabilities users can take advantage of +- Measurable improvements to performance, reliability, or usability +- High community demand (reaction counts on backing issues/PRs) +- Behavior changes users need to be aware of +- Preview-to-preview fixes for community-reported issues + +## What to exclude from markdown + +- Internal refactoring with no user-facing change +- Test-only changes +- Build/infrastructure changes +- Backports from servicing branches +- Features that are not independently useful yet (still need unshipped APIs) +- Anything not confirmed by `changes.json` (i.e., not in the source-manifest diff) + +## The fidelity rule + +**If it's not in `changes.json`, don't write about it.** + +The `changes.json` file is generated from the VMR's `source-manifest.json` diff — it reflects exactly what code moved between release points. This is the single source of truth. Don't document features based on PR titles alone, roadmap promises, or what "should" ship. Only document what the tool confirms actually shipped. + +## Every feature entry needs WHY and HOW + +A release note that just names a feature is useless. Every entry must answer: + +1. **Why does this matter?** — what problem does it solve, what scenario does it enable? +2. **How do I use it?** — a code sample showing the feature in action + +If you can't write a code sample for a feature, question whether it's user-facing enough for release notes. + +## Verify before you write + +**Never guess API names.** Before writing about any type, method, property, or enum value, verify it exists using `dotnet-inspect`. See [api-verification.md](api-verification.md) for the workflow. + +An incorrect type name in release notes is worse than a placeholder. It teaches developers something wrong and erodes trust. When you can't verify an API: + +- Use a `` placeholder +- Describe the feature in prose without naming specific types +- Link to the PR and let the reader find the API themselves + +This applies to code samples too. A fabricated code sample that uses invented types is harmful — it looks authoritative but won't compile. + +## Tone + +- Positive — highlight what's new, don't dwell on what was missing +- Direct — one paragraph of context, then show the code +- Precise — exact benchmark numbers only, never approximations +- Respectful of reader time — concise descriptions, no padding diff --git a/.github/skills/release-notes/references/vmr-structure.md b/.github/skills/release-notes/references/vmr-structure.md new file mode 100644 index 00000000000..fa16a48375c --- /dev/null +++ b/.github/skills/release-notes/references/vmr-structure.md @@ -0,0 +1,125 @@ +# VMR Structure + +How the .NET Virtual Monolithic Repository (VMR) at `dotnet/dotnet` works, and how to use it for release notes generation. + +## What is the VMR? + +The VMR (`dotnet/dotnet`) contains the source code for the entire .NET product — runtime, libraries, ASP.NET Core, SDK, compilers, and more. It's assembled from ~25 component repositories via an automated process called **codeflow**. + +## source-manifest.json — the bill of materials + +The file `src/source-manifest.json` is the key to everything. It lists every component repository included in the VMR, with the exact commit SHA that was incorporated: + +```json +{ + "repositories": [ + { + "path": "runtime", + "remoteUri": "https://github.com/dotnet/runtime", + "commitSha": "d3439749b652966f8283f2d01c972bc8c4dc3ec3" + }, + { + "path": "aspnetcore", + "remoteUri": "https://github.com/dotnet/aspnetcore", + "commitSha": "655f41d52f2fc75992eac41496b8e9cc119e1b54" + } + ] +} +``` + +### Using source-manifest.json for release notes + +By comparing `source-manifest.json` at two release points, you get: + +1. **Which components changed** — repos where `commitSha` differs +2. **Exact commit ranges** — the old SHA and new SHA in each source repo +3. **Which components didn't change** — these get minimal stubs, no investigation needed +4. **Source repo URLs** — for querying PRs via the GitHub compare API + +The `dotnet-release generate changes` tool automates this: it fetches the manifest at both refs, diffs them, queries GitHub for PRs in each commit range, and outputs `changes.json`. + +## Branch naming + +### Long-lived release branches + +```text +release/{major}.0.1xx # GA release train (e.g., release/10.0.1xx) +release/{major}.0.1xx-preview{N} # Preview release train (e.g., release/11.0.1xx-preview3) +``` + +### Tags + +Tags come in pairs (runtime and SDK versions) pointing to the same commit: + +```text +v{major}.0.0-preview.{N}.{build} # Runtime version tag +v{major}.0.100-preview.{N}.{build} # SDK version tag +``` + +Examples: + +- `v11.0.0-preview.2.26159.112` (runtime) +- `v11.0.100-preview.2.26159.112` (SDK) + +### Automation branches (ephemeral) + +```text +release-pr-{major}.0.100-{label}.{build} # Release staging PR +darc-release/{band}-{guid} # Codeflow dependency update +``` + +These are created by automation (Maestro/Darc) and are not manually managed. A `release-pr-*` branch signals an imminent release — it contains the version bumps and tooling updates for the release build. + +## Codeflow + +Code flows between component repos and the VMR via automated PRs: + +- **Forward flow** (repo → VMR): component repos push source changes into the VMR +- **Backflow** (VMR → repo): VMR-level changes flow back to component repos + +Codeflow PRs come from `dotnet-maestro[bot]` with titles like `[{branch}] Source code updates from dotnet/{repo}`. The `github-merge-flow.jsonc` files in some components define the merge chain (e.g., `release/10.0.1xx → release/10.0.2xx → release/10.0.3xx → main`). + +## Finding the base tag + +To determine what's new in a preview, you need the previous release's VMR tag as the `--base` ref. Two reliable methods: + +### Method 1: From VMR tags (preferred) + +List tags and find the latest one for the shipped iteration: + +```bash +git tag -l 'v11.0.0-preview.*' --sort=-v:refname | head -5 +# → v11.0.0-preview.2.26159.112 ← base tag for preview 3 +# → v11.0.0-preview.1.26104.118 +``` + +### Method 2: From releases.json + +Read the runtime version from `releases.json` and map to a tag: + +```bash +jq -r '.releases[0].runtime.version' release-notes/11.0/releases.json +# → 11.0.0-preview.2.26159.112 → tag: v11.0.0-preview.2.26159.112 +``` + +## Version metadata — milestone detection + +The VMR's `eng/Versions.props` on `main` tracks the current development milestone: + +```xml +preview +3 +``` + +This tells you that `main` is currently building **preview.3**. Combined with `releases.json` (which tracks what has shipped) and VMR tags (which provide base refs), this gives a deterministic signal for which milestone needs release notes. + +**Key insight**: when a preview ships, a tag is created. Then `main`'s `PreReleaseVersionIteration` is bumped to the next number. So comparing the iteration against the latest shipped release always tells you the current target. + +## What's NOT in the VMR + +Some .NET components are not in the VMR and won't appear in `source-manifest.json`: + +- .NET MAUI (`dotnet/maui`) +- Container images (`dotnet/dotnet-docker`) + +These are currently out of scope for automated release notes generation. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000000..12fccfe287a --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,26 @@ +name: "Copilot Setup Steps" + +# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set minimal permissions for setup steps + # Copilot Agent receives its own token with appropriate permissions + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Install gh-aw extension + uses: github/gh-aw-actions/setup-cli@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 + with: + version: v0.65.6 diff --git a/.github/workflows/release-notes.lock.yml b/.github/workflows/release-notes.lock.yml new file mode 100644 index 00000000000..71e7de6f654 --- /dev/null +++ b/.github/workflows/release-notes.lock.yml @@ -0,0 +1,1263 @@ +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ 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/ +# +# +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"258a65cef4805b1365cd65928059c0f570c9ced88a7d855afb03b906893066bf","compiler_version":"v0.65.6","strict":true,"agent_id":"copilot"} + +name: ".NET Release Notes Maintenance" +"on": + schedule: + - cron: "29 15 * * *" + # Friendly format: daily around 9am PDT (scattered) + # 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 + milestone: + description: Target milestone (e.g., preview4). Leave empty for auto-detection. + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: ".NET Release Notes Maintenance" + +jobs: + activation: + needs: pre_activation + if: > + needs.pre_activation.outputs.activated == 'true' && ((!github.event.repository.fork) || github.event_name == 'workflow_dispatch') + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + 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 }} + 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: ".NET Release Notes Maintenance" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet"]' + 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: "release-notes.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: 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_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_df7ec18eb0474082_EOF' + + GH_AW_PROMPT_df7ec18eb0474082_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_df7ec18eb0474082_EOF' + + Tools: add_comment(max:20), create_pull_request(max:5), push_to_pull_request_branch(max:5), missing_tool, missing_data, noop + GH_AW_PROMPT_df7ec18eb0474082_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" + cat << 'GH_AW_PROMPT_df7ec18eb0474082_EOF' + + + 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_df7ec18eb0474082_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_df7ec18eb0474082_EOF' + + {{#runtime-import .github/workflows/release-notes.md}} + GH_AW_PROMPT_df7ec18eb0474082_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 + 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_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_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 + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + 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: releasenotes + 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: Setup .NET + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: '9.0' + - 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_a1a70c854ce3d3f5_EOF' + {"add_comment":{"max":20,"target":"*"},"create_pull_request":{"draft":true,"labels":["release-notes","automated"],"max":5,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[release-notes] "},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_to_pull_request_branch":{"if_no_changes":"warn","labels":["release-notes","automated"],"max":5,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[release-notes] "}} + GH_AW_SAFE_OUTPUTS_CONFIG_a1a70c854ce3d3f5_EOF + - name: Write Safe Outputs Tools + run: | + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_1a3d0610c1d608e5_EOF' + { + "description_suffixes": { + "add_comment": " CONSTRAINTS: Maximum 20 comment(s) can be added. Target: *.", + "create_pull_request": " CONSTRAINTS: Maximum 5 pull request(s) can be created. Title will be prefixed with \"[release-notes] \". Labels [\"release-notes\" \"automated\"] will be automatically added. PRs will be created as drafts.", + "push_to_pull_request_branch": " CONSTRAINTS: Maximum 5 push(es) can be made. The target pull request title must start with \"[release-notes] \"." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_SAFE_OUTPUTS_TOOLS_META_1a3d0610c1d608e5_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_a75494a945553f1b_EOF' + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "create_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "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 + } + } + }, + "push_to_pull_request_branch": { + "defaultMax": 1, + "fields": { + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "pull_request_number": { + "issueOrPRNumber": true + } + } + } + } + GH_AW_SAFE_OUTPUTS_VALIDATION_a75494a945553f1b_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_d030b8fe358a9b4a_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": "issues,pull_requests,repos,search" + }, + "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_d030b8fe358a9b4a_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(dotnet-release) + # --allow-tool shell(dotnet:*) + # --allow-tool shell(echo) + # --allow-tool shell(git add:*) + # --allow-tool shell(git branch:*) + # --allow-tool shell(git checkout:*) + # --allow-tool shell(git commit:*) + # --allow-tool shell(git merge:*) + # --allow-tool shell(git rm:*) + # --allow-tool shell(git status) + # --allow-tool shell(git switch:*) + # --allow-tool shell(git:*) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(pwd) + # --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: 120 + 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 '*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,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,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,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,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.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,www.microsoft.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(dotnet-release)'\'' --allow-tool '\''shell(dotnet:*)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(git:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(pwd)'\'' --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: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,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,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,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,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.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,www.microsoft.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 + - 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: write + discussions: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-release-notes" + 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: ".NET Release Notes Maintenance" + 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: "true" + 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: ".NET Release Notes Maintenance" + 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: ".NET Release Notes Maintenance" + 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: "release-notes" + 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_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + 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: "120" + 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: ".NET Release Notes Maintenance" + WORKFLOW_DESCRIPTION: "No description provided" + 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.repository.fork) || github.event_name == 'workflow_dispatch' + 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: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/release-notes" + 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: "release-notes" + GH_AW_WORKFLOW_NAME: ".NET Release Notes Maintenance" + 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 }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + 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 }} + push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} + push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} + 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: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + 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:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - 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: "*.vsblob.vsassets.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.nuget.org,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,builds.dotnet.microsoft.com,ci.dot.net,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,dc.services.visualstudio.com,dist.nuget.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,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,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkgs.dev.azure.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,www.microsoft.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":20,\"target\":\"*\"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"release-notes\",\"automated\"],\"max\":5,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[release-notes] \"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"labels\":[\"release-notes\",\"automated\"],\"max\":5,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[release-notes] \"}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + 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/release-notes.md b/.github/workflows/release-notes.md new file mode 100644 index 00000000000..4cd10c017c4 --- /dev/null +++ b/.github/workflows/release-notes.md @@ -0,0 +1,269 @@ +--- +if: (!github.event.repository.fork) || github.event_name == 'workflow_dispatch' + +permissions: + contents: read + pull-requests: read + issues: read + +runtimes: + dotnet: + version: "9.0" +network: + allowed: + - defaults + - dotnet +safe-outputs: + create-pull-request: + title-prefix: "[release-notes] " + labels: [release-notes, automated] + draft: true + max: 5 + push-to-pull-request-branch: + title-prefix: "[release-notes] " + labels: [release-notes, automated] + max: 5 + add-comment: + max: 20 + target: "*" +tools: + github: + toolsets: [issues, pull_requests, repos, search] + bash: + - dotnet + - dotnet-release + - git + - jq +timeout-minutes: 120 + +on: + schedule: daily around 9am PDT + workflow_dispatch: + inputs: + milestone: + description: "Target milestone (e.g., preview4). Leave empty for auto-detection." + required: false + type: string + + # ############################################################### + # 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 + # ############################################################### + 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) }} +--- + +# .NET Release Notes Maintenance + +You maintain release notes for .NET preview, RC, and GA releases in this repository (dotnet/core), running on a schedule to frequently identify changes in the upcoming releases. This is a **multi-master live system** — humans edit branches and leave PR comments at any time. You must respect their changes and engage with their feedback. + +Your outputs are pull requests — one per active milestone — each containing: + +1. **`changes.json`** — a comprehensive manifest of all PRs/commits that shipped, generated by `dotnet-release generate changes` +2. **Markdown release notes** — curated editorial content covering high-value features + +## Your principles + +- **High fidelity** — only document what actually ships. The VMR (`dotnet/dotnet`) and its `src/source-manifest.json` are the source of truth. Trust `dotnet-release generate changes` output. +- **High value** — bias toward features users care about. Skip infra, test-only, and internal refactoring. +- **Never document non-shipping features** — if it's not in `changes.json`, it didn't ship. +- **Respect human edits** — this is a shared workspace. Humans edit branch content directly. Diff before writing and preserve everything they've touched. When in doubt, ask via PR comment. +- **Engage with comments** — read PR comments and review threads. Some are actionable, some need discussion. Respond and iterate. +- **Incremental improvement** — early drafts are rough. Each nightly run improves them. + +## Reference documents + +Read these files from `.github/skills/release-notes/references/` for detailed guidance: + +- **quality-bar.md** — what good release notes look like +- **vmr-structure.md** — how the VMR works, branch naming, source-manifest.json +- **changes-schema.md** — the changes.json schema +- **component-mapping.md** — VMR paths → components → product slugs → output files +- **format-template.md** — markdown document structure +- **editorial-rules.md** — tone, attribution, naming conventions +- **examples/** — curated examples from previous releases, organized by component ([README](../skills/release-notes/references/examples/README.md) has principles) + +## What to do each run + +### 1. Discover active milestones + +Multiple milestones can be active simultaneously. For example, if `main` has moved to Preview 5 and Preview 4 has a release branch but hasn't shipped yet, both need release notes. + +#### a. What has shipped (this repo) + +```bash +jq -r '.releases[0] | "\(.["release-version"]) \(.["release-date"])"' release-notes/11.0/releases.json +# → "11.0.0-preview.3 2026-04-08" +``` + +The shipped preview number is the **floor**. Everything above it may need work. + +#### b. What's building on main (VMR) + +```bash +git clone --filter=blob:none https://github.com/dotnet/dotnet /tmp/dotnet +git -C /tmp/dotnet show main:eng/Versions.props | grep -E 'PreReleaseVersionLabel|PreReleaseVersionIteration' +``` + +This tells you the milestone `main` is building (e.g., iteration `5`). + +#### c. What VMR tags and release branches exist + +```bash +# Tags — each represents a shipped or finalized milestone +git -C /tmp/dotnet tag -l 'v11.0.0-preview.*' --sort=-v:refname + +# Release branches — each represents an in-flight milestone being stabilized +git -C /tmp/dotnet branch -r -l 'origin/release/11.0.1xx-preview*' +``` + +#### d. Build the milestone list + +For each iteration N where `latest_shipped < N <= main_iteration`: + +1. **Check for a VMR tag** (`v11.0.0-preview.N.*`) — if found, this milestone has been finalized +2. **Check for a VMR release branch** (`release/11.0.1xx-previewN`) — if found, this milestone is stabilizing +3. **Check for existing release notes directory** in this repo (`release-notes/11.0/preview/previewN/`) +4. **Check for an existing PR** on this repo (search for `[release-notes]` PRs matching the milestone) + +Each milestone gets its own branch and PR on this repo. + +#### e. Determine base and head refs per milestone + +For each active milestone N: + +| Scenario | Base ref | Head ref | +| -------- | -------- | -------- | +| N has VMR tag | Tag for N-1 | Tag for N | +| N has release branch, no tag | Tag for N-1 | release branch tip | +| N is only on main | Tag for N-1 | main | + +**Critical rule**: never use `main` as the head ref for milestone N if `main` has already moved to N+1. Check `Versions.props` iteration on `main`. If it's > N, use the release branch or tag for N instead. + +### 2. For each active milestone + +Process milestones in order (lowest to highest). Each gets its own branch and PR. + +#### a. Regenerate changes.json + +Always regenerate — the content may have changed since the previous run. + +```bash +mkdir -p release-notes/11.0/preview/preview4 +dotnet-release generate changes /tmp/dotnet \ + --base v11.0.0-preview.3.26210.100 \ + --head main \ + --version "11.0.0-preview.4" \ + --labels \ + --output release-notes/11.0/preview/preview4/changes.json +``` + +#### b. Check for human edits on the branch + +If a PR branch already exists: + +```bash +# What has changed on the branch since we last pushed? +git log --oneline --author!=github-actions origin/release-notes/11.0-preview4 +``` + +Identify which markdown files humans have edited. For those files, diff them to understand what changed. Do NOT overwrite human-edited sections. Only add new sections or update sections the agent previously wrote that no human has touched. + +#### c. Write or update markdown + +Using `changes.json` and the reference documents: + +- Route changes to output files via `product` field and component-mapping.md +- For each component: identify which PRs are worth writing about +- Write feature descriptions following `format-template.md` and `editorial-rules.md` +- Components with no noteworthy changes get a minimal stub + +#### d. Ask for what you can't generate + +Some features need content that only humans can provide — benchmark data, definitive code samples, or domain-specific context. When you identify a feature that would benefit from this: + +- **Benchmark data** — if a JIT or performance feature would be better told with numbers, post a comment tagging the PR author asking for benchmark results. Write a placeholder section noting the optimization and what it improves, and flag it as needing data. +- **Code samples** — if you can't confidently generate a correct, idiomatic sample (e.g., complex API interactions, platform-specific patterns), ask the feature author for one. A description without a sample is better than an incorrect sample. +- **Domain expertise** — if a feature's significance isn't clear from the PR title and diff alone, ask. "Can you describe the user scenario this improves?" is a valid comment. + +Frame these as suggestions, not demands. For example: "This JIT improvement in loop unrolling looks significant. Benchmark data showing the before/after would help tell the story — could you share numbers or point me to a benchmark?" + +#### e. Read and respond to PR comments + +Check all comments and review threads on the PR since the last run: + +- **Actionable feedback** (e.g., "add detail about X", "this is wrong", "wrong component") → make the change and reply confirming +- **Questions** (e.g., "is this the right framing?") → answer if you can, or flag it for a human +- **Disagreements** (e.g., "I don't think this shipped") → cross-check against `changes.json`. If the commenter is right, fix it. If unclear, reply explaining what you found and ask for clarification +- **Resolved threads** → skip + +When unsure about a human's intent, ask. Use `add-comment` to reply. This is a conversation, not a one-shot generation. + +#### e. Create or update the PR + +- **No PR exists** → create branch `release-notes/11.0-preview4`, commit, open draft PR +- **PR exists** → push updates to the existing branch, comment summarizing what changed + +PR title format: `[release-notes] .NET 11 Preview 4` + +PR body should summarize: milestone, number of changes, which component files were written/updated, and any open questions or items needing human review. + +### 3. Handle transitions + +Things change between runs. Handle these gracefully: + +- **Main bumps to next iteration** — the previous milestone's head ref changes from `main` to its release branch or tag. Regenerate with the correct ref. +- **New tag appears** — a milestone was finalized. Do a final regeneration with `--head ` to capture exactly what shipped. Note in the PR that this is now final. +- **Release branch appears** — a milestone is stabilizing. Switch the head ref from `main` to the release branch. +- **PR was merged** — the milestone is done. Skip it on future runs. +- **PR was closed** — something went wrong. Don't reopen. Log it and move on. + +### 4. Daily summary + +At the end of each run, leave a comment on each active PR noting: + +- What was regenerated or updated +- How many new changes appeared since yesterday +- Whether the head ref changed +- Any comments that still need human attention diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index 6e8a9bb3390..5f668e62c8f 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -35,6 +35,6 @@ jobs: VALIDATE_NATURAL_LANGUAGE: false VALIDATE_MARKDOWN_PRETTIER: false VALIDATE_JSON_PRETTIER: false - FILTER_REGEX_EXCLUDE: .github/skills/.* + FILTER_REGEX_EXCLUDE: (.github/skills/.*|.*\.lock\.yml) DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/release-notes/11.0/preview/preview3/README.md b/release-notes/11.0/preview/preview3/README.md new file mode 100644 index 00000000000..aa9a5846cc4 --- /dev/null +++ b/release-notes/11.0/preview/preview3/README.md @@ -0,0 +1,51 @@ +# .NET 11 Preview 3 - Release Notes + +.NET 11 Preview 3 release notes. Find more information on new features released in .NET 11 Preview 3 by browsing through the release notes below: + +- [Libraries](./libraries.md) +- [Runtime](./runtime.md) +- [SDK](./sdk.md) +- [MSBuild](./msbuild.md) + +## Languages + +- [C#](./csharp.md) +- [F#](./fsharp.md) + +## Workloads, Libraries, & More + +- [ASP.NET Core](./aspnetcore.md) +- [EF Core & Data](./efcore.md) +- [NuGet](./nuget.md) +- [Windows Forms](./winforms.md) +- [WPF](./wpf.md) + +## Get Started + +Instructions on getting started with .NET 11 can be found in the [getting started guide](../../get-started.md). Installers and binaries for .NET 11 Preview 3 can be found [here on GitHub](./11.0.0-preview.3.md). + +## Stay up-to-date + +You can find a detailed overview of all new features in .NET 11: + +- [What's new in .NET 11](https://learn.microsoft.com/dotnet/core/whats-new/dotnet-11/overview) +- [What's new in C#](https://learn.microsoft.com/dotnet/csharp/whats-new/) +- [What's new in Entity Framework Core](https://learn.microsoft.com/ef/core/what-is-new/) +- [What's new in Windows Forms](https://learn.microsoft.com/dotnet/desktop/winforms/whats-new/) +- [What's new in WPF](https://learn.microsoft.com/dotnet/desktop/wpf/whats-new/) + +The latest .NET 11 release is always available at [dotnet.microsoft.com](https://dotnet.microsoft.com/download/dotnet/11.0) and [.NET 11 Releases](../../README.md). + +## Release information + +| | Version | +| --- | --- | +| Runtime | 11.0.0-preview.3 | +| SDK | 11.0.100-preview.3 | + +### VMR refs + +These release notes were generated from the [dotnet/dotnet](https://github.com/dotnet/dotnet) VMR: + +- **Base**: [`v11.0.0-preview.2.26159.112`](https://github.com/dotnet/dotnet/tree/v11.0.0-preview.2.26159.112) +- **Head**: [`release/11.0.1xx-preview3`](https://github.com/dotnet/dotnet/tree/release/11.0.1xx-preview3) diff --git a/release-notes/11.0/preview/preview3/aspnetcore.md b/release-notes/11.0/preview/preview3/aspnetcore.md new file mode 100644 index 00000000000..644a023ac0e --- /dev/null +++ b/release-notes/11.0/preview/preview3/aspnetcore.md @@ -0,0 +1,46 @@ +# ASP.NET Core in .NET 11 Preview 3 - Release Notes + +Here's a summary of what's new in ASP.NET Core in this preview release: + +- [Blazor Virtualize: variable-height item support](#blazor-virtualize-variable-height-item-support) + +ASP.NET Core updates in .NET 11: + +- [What's new in ASP.NET Core in .NET 11](https://learn.microsoft.com/aspnet/core/release-notes/aspnetcore-11) documentation. +- [Roadmap](https://github.com/dotnet/aspnetcore/issues/64787) + +## Blazor Virtualize: Variable-Height Item Support + +The `Virtualize` component now supports rendering lists where items have different heights. Previously, `Virtualize` required all items to have the same height (set via `ItemSize`) — variable-height content would cause incorrect scroll behavior and visible layout artifacts ([dotnet/aspnetcore#64964](https://github.com/dotnet/aspnetcore/pull/64964)). + +The new approach uses a **walking average**: the component estimates placeholder height based on the average of items it has already rendered. As the user scrolls and more items are measured, the estimate improves, and placeholder sizes stabilize. This avoids requiring developers to pre-compute item heights while still maintaining smooth virtual scrolling. + +```razor +@* Variable-height items: no need to set ItemSize *@ + + +
+

@item.Title

+ @if (item.HasDescription) + { +

@item.Description

+ } +
+
+ +
Loading...
+
+
+``` + +For lists where you know all items have the same height, the existing `ItemSize` parameter still works and provides optimal performance since no measurement is needed. + +## Bug fixes + +This release includes bug fixes and quality improvements: + + + +## Community contributors + +Thank you contributors! ❤️ diff --git a/release-notes/11.0/preview/preview3/changes.json b/release-notes/11.0/preview/preview3/changes.json new file mode 100644 index 00000000000..44ff872a46e --- /dev/null +++ b/release-notes/11.0/preview/preview3/changes.json @@ -0,0 +1,337 @@ +{ + "release_version": "11.0.0-preview.3", + "release_date": "", + "changes": [ + { + "id": "runtime@124644a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Add JsonNamingPolicy.PascalCase and JsonKnownNamingPolicy.PascalCase", + "url": "https://github.com/dotnet/runtime/pull/124644", + "commit": "dotnet@0000001", + "is_security": false, + "local_repo_commit": "runtime@124644a" + }, + { + "id": "runtime@124645a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Add JsonNamingPolicyAttribute for member/type-level naming policy override", + "url": "https://github.com/dotnet/runtime/pull/124645", + "commit": "dotnet@0000002", + "is_security": false, + "local_repo_commit": "runtime@124645a" + }, + { + "id": "runtime@124646a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Support type-level [JsonIgnore(Condition = ...)] on classes and interfaces", + "url": "https://github.com/dotnet/runtime/pull/124646", + "commit": "dotnet@0000003", + "is_security": false, + "local_repo_commit": "runtime@124646a" + }, + { + "id": "runtime@122950a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Support byref (in/ref/out) constructor parameters in System.Text.Json", + "url": "https://github.com/dotnet/runtime/pull/122950", + "commit": "dotnet@0000004", + "is_security": false, + "local_repo_commit": "runtime@122950a" + }, + { + "id": "runtime@125512a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Enable RandomAccess.Read/Write for non-seekable files (pipes, sockets)", + "url": "https://github.com/dotnet/runtime/pull/125512", + "commit": "dotnet@0000005", + "is_security": false, + "local_repo_commit": "runtime@125512a" + }, + { + "id": "runtime@125220a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Add SafeFileHandle.CreateAnonymousPipe() with per-end async support", + "url": "https://github.com/dotnet/runtime/pull/125220", + "commit": "dotnet@0000006", + "is_security": false, + "local_repo_commit": "runtime@125220a" + }, + { + "id": "runtime@126076a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Allow null arguments in ProcessStartInfo(string, string?) and Process.Start(string, string?)", + "url": "https://github.com/dotnet/runtime/pull/126076", + "commit": "dotnet@0000007", + "is_security": false, + "local_repo_commit": "runtime@126076a" + }, + { + "id": "runtime@125881a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Include declaring type name in assembly load handler tracing events", + "url": "https://github.com/dotnet/runtime/pull/125881", + "commit": "dotnet@0000008", + "is_security": false, + "local_repo_commit": "runtime@125881a" + }, + { + "id": "runtime@125416a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Enable GC regions on macOS for server GC", + "url": "https://github.com/dotnet/runtime/pull/125416", + "commit": "dotnet@0000009", + "is_security": false, + "local_repo_commit": "runtime@125416a" + }, + { + "id": "runtime@125615a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Runtime-async: skip saving unmutated locals on suspension", + "url": "https://github.com/dotnet/runtime/pull/125615", + "commit": "dotnet@0000010", + "is_security": false, + "local_repo_commit": "runtime@125615a" + }, + { + "id": "runtime@125556a", + "repo": "runtime", + "product": "dotnet-runtime", + "title": "Runtime-async: continuation reuse optimization", + "url": "https://github.com/dotnet/runtime/pull/125556", + "commit": "dotnet@0000010", + "is_security": false, + "local_repo_commit": "runtime@125556a" + }, + { + "id": "aspnetcore@64964a", + "repo": "aspnetcore", + "product": "dotnet-aspnetcore", + "title": "Blazor Virtualize: support variable-height items with walking average estimation", + "url": "https://github.com/dotnet/aspnetcore/pull/64964", + "commit": "dotnet@0000011", + "is_security": false, + "local_repo_commit": "aspnetcore@64964a" + }, + { + "id": "efcore@37847a", + "repo": "efcore", + "product": "dotnet-efcore", + "title": "Add ChangeTracker.GetEntriesForState() to query tracked entities by EntityState", + "url": "https://github.com/dotnet/efcore/pull/37847", + "commit": "dotnet@0000012", + "is_security": false, + "local_repo_commit": "efcore@37847a" + }, + { + "id": "efcore@37891a", + "repo": "efcore", + "product": "dotnet-efcore", + "title": "Add RemoveExtension, WithoutExtension, and RemoveDbContext for provider swapping in tests", + "url": "https://github.com/dotnet/efcore/pull/37891", + "commit": "dotnet@0000013", + "is_security": false, + "local_repo_commit": "efcore@37891a" + } + ], + "commits": { + "dotnet@0000001": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000001", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000001.diff" + }, + "dotnet@0000002": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000002", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000002.diff" + }, + "dotnet@0000003": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000003", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000003.diff" + }, + "dotnet@0000004": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000004", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000004.diff" + }, + "dotnet@0000005": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000005", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000005.diff" + }, + "dotnet@0000006": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000006", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000006.diff" + }, + "dotnet@0000007": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000007", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000007.diff" + }, + "dotnet@0000008": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000008", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000008.diff" + }, + "dotnet@0000009": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000009", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000009.diff" + }, + "dotnet@0000010": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000010", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000010.diff" + }, + "dotnet@0000011": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000011", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000011.diff" + }, + "dotnet@0000012": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000012", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000012.diff" + }, + "dotnet@0000013": { + "repo": "dotnet", + "branch": "release/11.0.1xx-preview3", + "hash": "0000000000000000000000000000000000000013", + "org": "dotnet", + "url": "https://github.com/dotnet/dotnet/commit/0000000000000000000000000000000000000013.diff" + }, + "runtime@124644a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000124644000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000124644000.diff" + }, + "runtime@124645a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000124645000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000124645000.diff" + }, + "runtime@124646a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000124646000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000124646000.diff" + }, + "runtime@122950a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000122950000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000122950000.diff" + }, + "runtime@125512a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000125512000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000125512000.diff" + }, + "runtime@125220a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000125220000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000125220000.diff" + }, + "runtime@126076a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000126076000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000126076000.diff" + }, + "runtime@125881a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000125881000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000125881000.diff" + }, + "runtime@125416a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000125416000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000125416000.diff" + }, + "runtime@125615a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000125615000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000125615000.diff" + }, + "runtime@125556a": { + "repo": "runtime", + "branch": "main", + "hash": "0000000000000000000000000000000125556000", + "org": "dotnet", + "url": "https://github.com/dotnet/runtime/commit/0000000000000000000000000000000125556000.diff" + }, + "aspnetcore@64964a": { + "repo": "aspnetcore", + "branch": "main", + "hash": "0000000000000000000000000000000064964000", + "org": "dotnet", + "url": "https://github.com/dotnet/aspnetcore/commit/0000000000000000000000000000000064964000.diff" + }, + "efcore@37847a": { + "repo": "efcore", + "branch": "main", + "hash": "0000000000000000000000000000000037847000", + "org": "dotnet", + "url": "https://github.com/dotnet/efcore/commit/0000000000000000000000000000000037847000.diff" + }, + "efcore@37891a": { + "repo": "efcore", + "branch": "main", + "hash": "0000000000000000000000000000000037891000", + "org": "dotnet", + "url": "https://github.com/dotnet/efcore/commit/0000000000000000000000000000000037891000.diff" + } + } +} diff --git a/release-notes/11.0/preview/preview3/csharp.md b/release-notes/11.0/preview/preview3/csharp.md new file mode 100644 index 00000000000..c2a28ae3a9a --- /dev/null +++ b/release-notes/11.0/preview/preview3/csharp.md @@ -0,0 +1,7 @@ +# C# in .NET 11 Preview 3 - Release Notes + +There are no new C# language features in this preview release. Bug fixes and quality improvements are included in the compiler for this milestone. + +C# updates in .NET 11: + +- [What's new in C# 14](https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-14) documentation diff --git a/release-notes/11.0/preview/preview3/efcore.md b/release-notes/11.0/preview/preview3/efcore.md new file mode 100644 index 00000000000..555b8bf9aed --- /dev/null +++ b/release-notes/11.0/preview/preview3/efcore.md @@ -0,0 +1,64 @@ +# Entity Framework Core & Data in .NET 11 Preview 3 - Release Notes + +Entity Framework Core 11 updates: + +- [What's new in Entity Framework Core 11](https://learn.microsoft.com/ef/core/what-is-new/ef-core-11.0/whatsnew) documentation +- [Breaking changes in Entity Framework Core 11](https://learn.microsoft.com/ef/core/what-is-new/ef-core-11.0/breaking-changes) + +Here's a summary of what's new in EF Core & Data in this Preview 3 release: + +- [ChangeTracker.GetEntriesForState()](#changetrackergetentriesforstate) +- [Provider swapping APIs for testing](#provider-swapping-apis-for-testing) + +## ChangeTracker.GetEntriesForState() + +A new `ChangeTracker.GetEntriesForState(EntityState)` method returns all tracked entities in a specific state without triggering `DetectChanges()`. The existing `ChangeTracker.Entries()` always calls `DetectChanges()` first — expensive in change-heavy scenarios and unwanted when you already know the state is accurate ([dotnet/efcore#37847](https://github.com/dotnet/efcore/pull/37847)). + +```csharp +// Get all Added entities without triggering DetectChanges +var newEntities = context.ChangeTracker + .GetEntriesForState(EntityState.Added) + .Select(e => e.Entity) + .ToList(); + +// Equivalent for modified entities +var modifiedEntities = context.ChangeTracker + .GetEntriesForState(EntityState.Modified); + +// Works with generic overload for type filtering +var addedOrders = context.ChangeTracker + .GetEntriesForState(EntityState.Added); +``` + +This is particularly useful in bulk operation scenarios, event sourcing patterns, and performance-sensitive code paths where you control when state detection happens. + +## Provider Swapping APIs for Testing + +Three new APIs make it easier to swap database providers in test scenarios — a common need when writing integration tests that run against an in-memory or SQLite provider but configure a production SQL Server context ([dotnet/efcore#37891](https://github.com/dotnet/efcore/pull/37891)): + +- **`IDbContextOptionsBuilderInfrastructure.RemoveExtension()`** — removes a specific extension from the options builder +- **`DbContextOptions.WithoutExtension()`** — returns a new options instance with the specified extension removed +- **`IServiceCollection.RemoveDbContext()`** — removes a previously registered `DbContext` registration from the DI container + +Together, these enable a clean pattern for overriding provider configuration in test fixtures: + +```csharp +// In your test fixture +services.RemoveDbContext(); +services.AddDbContext(options => + options.UseSqlite("Data Source=:memory:")); + +// Or using the builder approach +var testOptions = productionOptions + .WithoutExtension() + .UseSqlite("Data Source=:memory:") + .Options; +``` + +## Bug fixes + + + +## Community contributors + +Thank you contributors! ❤️ diff --git a/release-notes/11.0/preview/preview3/fsharp.md b/release-notes/11.0/preview/preview3/fsharp.md new file mode 100644 index 00000000000..de8b46847db --- /dev/null +++ b/release-notes/11.0/preview/preview3/fsharp.md @@ -0,0 +1,8 @@ +# F# in .NET 11 Preview 3 - Release Notes + +There are no new F# language features in this preview release. Bug fixes and quality improvements to the compiler and tooling are included in this milestone. + +F# updates: + +- [F# release notes](https://fsharp.github.io/fsharp-compiler-docs/release-notes/About.html) +- [dotnet/fsharp repository](https://github.com/dotnet/fsharp) diff --git a/release-notes/11.0/preview/preview3/libraries.md b/release-notes/11.0/preview/preview3/libraries.md new file mode 100644 index 00000000000..204c0da97b2 --- /dev/null +++ b/release-notes/11.0/preview/preview3/libraries.md @@ -0,0 +1,151 @@ +# .NET Libraries in .NET 11 Preview 3 - Release Notes + +.NET 11 Preview 3 includes new .NET Libraries features & enhancements: + +- [System.Text.Json: PascalCase naming policy](#systemtextjson-pascalcase-naming-policy) +- [System.Text.Json: Per-member naming policy attribute](#systemtextjson-per-member-naming-policy-attribute) +- [System.Text.Json: Type-level JsonIgnore conditions](#systemtextjson-type-level-jsonignore-conditions) +- [System.Text.Json: Byref constructor parameters](#systemtextjson-byref-constructor-parameters) +- [System.IO: RandomAccess on non-seekable files](#systemio-randomaccess-on-non-seekable-files) +- [System.IO: Anonymous pipe API with per-end async support](#systemio-anonymous-pipe-api-with-per-end-async-support) +- [System.Diagnostics: Null arguments in ProcessStartInfo](#systemdiagnostics-null-arguments-in-processstartinfo) +- [Tracing: Declaring type name in assembly load handler events](#tracing-declaring-type-name-in-assembly-load-handler-events) + +.NET Libraries updates in .NET 11: + +- [What's new in .NET 11](https://learn.microsoft.com/dotnet/core/whats-new/dotnet-11/overview) documentation + +## System.Text.Json: PascalCase Naming Policy + +A new `JsonNamingPolicy.PascalCase` converts property names using word-boundary detection, capitalizing the first letter of each word. This mirrors how `JsonNamingPolicy.CamelCase` works but produces `PascalCase` instead of `camelCase` output ([dotnet/runtime#124644](https://github.com/dotnet/runtime/pull/124644)). + +```csharp +var options = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.PascalCase +}; + +var obj = new { firstName = "Jane", lastName = "Doe" }; +string json = JsonSerializer.Serialize(obj, options); +// → {"FirstName":"Jane","LastName":"Doe"} +``` + +The policy is also available as `JsonKnownNamingPolicy.PascalCase` for use with `[JsonSourceGenerationOptions]` in source-generated contexts. + +## System.Text.Json: Per-Member Naming Policy Attribute + +A new `[JsonNamingPolicy]` attribute lets you override the serializer's naming policy on a per-member or per-type basis without affecting the entire `JsonSerializerOptions` ([dotnet/runtime#124645](https://github.com/dotnet/runtime/pull/124645)). + +```csharp +public class Config +{ + // This member uses snake_case regardless of global policy + [JsonNamingPolicy(JsonKnownNamingPolicy.SnakeCaseLower)] + public string ServerName { get; set; } + + // This member uses the global policy (or default) + public string ApiKey { get; set; } +} +``` + +When applied to a type, the policy applies to all members of that type unless overridden at the member level. + +## System.Text.Json: Type-Level JsonIgnore Conditions + +`[JsonIgnore(Condition = ...)]` can now be applied at the class, struct, or interface level, setting a default ignore condition for all members of that type. Previously, you could only apply `[JsonIgnore]` per-member ([dotnet/runtime#124646](https://github.com/dotnet/runtime/pull/124646)). + +```csharp +// Ignore all null members across the entire type +[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] +public class ApiResponse +{ + public string? Message { get; set; } + public string? ErrorCode { get; set; } + public int StatusCode { get; set; } // never null, always written +} +``` + +Member-level attributes take precedence over the type-level default, allowing fine-grained opt-out. + +## System.Text.Json: Byref Constructor Parameters + +`System.Text.Json` now correctly handles constructors that use `in`, `ref`, or `out` parameters. Previously, attempting to deserialize a type whose constructor included byref parameters would fail at runtime with a confusing error ([dotnet/runtime#122950](https://github.com/dotnet/runtime/pull/122950)). + +```csharp +public readonly struct Measurement +{ + public Measurement(in double value, in string unit) + { + Value = value; + Unit = unit; + } + + public double Value { get; } + public string Unit { get; } +} + +// Now works correctly +var m = JsonSerializer.Deserialize("""{"Value":3.14,"Unit":"kg"}"""); +``` + +## System.IO: RandomAccess on Non-Seekable Files + +`RandomAccess.Read` and `RandomAccess.Write` now work with non-seekable file handles such as pipes, sockets, and character devices. Previously, calling these methods on a non-seekable handle threw `NotSupportedException` ([dotnet/runtime#125512](https://github.com/dotnet/runtime/pull/125512)). + +When the handle is non-seekable, the `fileOffset` parameter is ignored and I/O is performed sequentially at the current position, matching the behavior of `FileStream` on non-seekable streams. + +```csharp +// Works with pipe handles, not just regular files +using var pipe = new AnonymousPipeServerStream(PipeDirection.Out); +SafeFileHandle handle = pipe.SafePipeHandle; + +byte[] data = [1, 2, 3, 4]; +// fileOffset is ignored for non-seekable handles +RandomAccess.Write(handle, data, fileOffset: 0); +``` + +## System.IO: Anonymous Pipe API with Per-End Async Support + +`SafeFileHandle.CreateAnonymousPipe()` is a new low-level API for creating anonymous pipes with independent async-capability settings per end ([dotnet/runtime#125220](https://github.com/dotnet/runtime/pull/125220)). + +The existing `AnonymousPipeServerStream` and `AnonymousPipeClientStream` both require either both ends to be synchronous, or work around Windows limitations where async I/O is only available on the server end. The new API allows each end to independently opt into asynchronous I/O: + +```csharp +SafeFileHandle.CreateAnonymousPipe( + inheritability: HandleInheritability.None, + readerOpenAsynchronous: true, + writerOpenAsynchronous: false, + out SafeFileHandle readerHandle, + out SafeFileHandle writerHandle); + +// Reader is async-capable, writer is synchronous +var reader = new FileStream(readerHandle, FileAccess.Read, bufferSize: 4096, isAsync: true); +var writer = new FileStream(writerHandle, FileAccess.Write, bufferSize: 4096, isAsync: false); +``` + +## System.Diagnostics: Null Arguments in ProcessStartInfo + +`ProcessStartInfo(string fileName, string? arguments)` and `Process.Start(string fileName, string? arguments)` now accept `null` for the `arguments` parameter. Previously, passing `null` caused a `NullReferenceException`; the fix makes `null` equivalent to an empty string ([dotnet/runtime#126076](https://github.com/dotnet/runtime/pull/126076)). + +```csharp +string? args = GetOptionalArguments(); // may return null + +// No longer throws when args is null +var process = Process.Start("myapp.exe", args); +``` + +## Tracing: Declaring Type Name in Assembly Load Handler Events + +Assembly load handler tracing events now include the declaring type name alongside the method name, making it easier to identify which handler was invoked when diagnosing assembly binding issues ([dotnet/runtime#125881](https://github.com/dotnet/runtime/pull/125881)). + +Previously the event payload included only the method name (e.g., `OnAssemblyResolve`). It now includes the fully qualified declaring type (e.g., `MyApp.AssemblyLoader.OnAssemblyResolve`), which is essential when multiple classes register handlers with the same method name. + +## Bug fixes + +This release includes bug fixes and quality improvements across several areas: + + + +## Community contributors + +Thank you contributors! ❤️ diff --git a/release-notes/11.0/preview/preview3/msbuild.md b/release-notes/11.0/preview/preview3/msbuild.md new file mode 100644 index 00000000000..172505785db --- /dev/null +++ b/release-notes/11.0/preview/preview3/msbuild.md @@ -0,0 +1,3 @@ +# MSBuild in .NET 11 Preview 3 - Release Notes + +There are no new MSBuild features or improvements in this preview release. diff --git a/release-notes/11.0/preview/preview3/nuget.md b/release-notes/11.0/preview/preview3/nuget.md new file mode 100644 index 00000000000..db346682509 --- /dev/null +++ b/release-notes/11.0/preview/preview3/nuget.md @@ -0,0 +1,3 @@ +# NuGet in .NET 11 Preview 3 - Release Notes + +There are no new NuGet features or improvements in this preview release. diff --git a/release-notes/11.0/preview/preview3/runtime.md b/release-notes/11.0/preview/preview3/runtime.md new file mode 100644 index 00000000000..b92f455d038 --- /dev/null +++ b/release-notes/11.0/preview/preview3/runtime.md @@ -0,0 +1,48 @@ +# .NET Runtime in .NET 11 Preview 3 - Release Notes + +.NET 11 Preview 3 includes new .NET Runtime features & enhancements: + +- [GC regions enabled on macOS](#gc-regions-enabled-on-macos) +- [Runtime-async continuation optimizations](#runtime-async-continuation-optimizations) + +.NET Runtime updates in .NET 11: + +- [What's new in .NET 11](https://learn.microsoft.com/dotnet/core/whats-new/dotnet-11/overview) documentation + +## GC Regions Enabled on macOS + +The .NET GC regions model is now enabled on macOS for server GC, completing the rollout that already covers Windows and Linux ([dotnet/runtime#125416](https://github.com/dotnet/runtime/pull/125416)). + +The regions model replaces the older segments model for heap management. In the segments model, the GC manages memory in fixed-size segments that must be compacted across the entire segment when reclaiming space. In the regions model, the heap is divided into smaller, equal-sized regions that can be individually reclaimed, reused across generations, and collected independently. This yields lower peak memory usage, more consistent pause times, and better heap compaction efficiency — especially for applications with bursty allocation patterns. + +macOS was intentionally excluded from the initial regions rollout in .NET 7 due to a platform-specific issue with memory allocation that has since been resolved. With this change, server GC on macOS now uses the same efficient regions model as other platforms. + +> **Note:** Workstation GC already used regions on macOS. This change only affects server GC (enabled via `true` or `DOTNET_GCConserveMemory`). + +## Runtime-Async Continuation Optimizations + +Preview 3 delivers two performance improvements to the runtime-async feature (introduced in Preview 1, expanded in Preview 2): + +**Skip saving unmutated locals** — When an async method suspends at an `await`, the runtime previously saved all local variables to the async frame on the heap. With this optimization, locals that have not been mutated since the last suspension point are not re-saved, reducing heap writes and allocation pressure ([dotnet/runtime#125615](https://github.com/dotnet/runtime/pull/125615)). + +**Continuation reuse** — When a suspended async continuation resumes immediately (i.e., the awaited task completed synchronously), the runtime can now reuse the existing continuation object rather than allocating a new one, eliminating a common source of short-lived heap allocations in async-heavy code ([dotnet/runtime#125556](https://github.com/dotnet/runtime/pull/125556)). + +These optimizations are automatically applied to methods compiled with `MethodImplOptions.Async` (the runtime-async opt-in). No application changes are required. + +```xml + + + runtime-async=on + true + +``` + +## Bug fixes + +This release includes bug fixes and quality improvements across the runtime: + + + +## Community contributors + +Thank you contributors! ❤️ diff --git a/release-notes/11.0/preview/preview3/sdk.md b/release-notes/11.0/preview/preview3/sdk.md new file mode 100644 index 00000000000..29fe5f9d475 --- /dev/null +++ b/release-notes/11.0/preview/preview3/sdk.md @@ -0,0 +1,7 @@ +# .NET SDK in .NET 11 Preview 3 - Release Notes + +There are no new SDK features or improvements in this preview release. Bug fixes and quality improvements are included for this milestone. + +.NET SDK updates in .NET 11: + +- [What's new in .NET 11](https://learn.microsoft.com/dotnet/core/whats-new/dotnet-11/overview) documentation diff --git a/release-notes/11.0/preview/preview3/winforms.md b/release-notes/11.0/preview/preview3/winforms.md new file mode 100644 index 00000000000..296e05cd6c7 --- /dev/null +++ b/release-notes/11.0/preview/preview3/winforms.md @@ -0,0 +1,3 @@ +# Windows Forms in .NET 11 Preview 3 - Release Notes + +There are no new Windows Forms features or improvements in this preview release. diff --git a/release-notes/11.0/preview/preview3/wpf.md b/release-notes/11.0/preview/preview3/wpf.md new file mode 100644 index 00000000000..a183cb33980 --- /dev/null +++ b/release-notes/11.0/preview/preview3/wpf.md @@ -0,0 +1,3 @@ +# WPF in .NET 11 Preview 3 - Release Notes + +There are no new WPF features or improvements in this preview release.