diff --git a/.gitattributes b/.gitattributes index a1e1e97ac2..b678750c67 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/aw/actions-lock.json b/.github/aw/actions-lock.json new file mode 100644 index 0000000000..1c855ccd7d --- /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/skills/api-diff/SKILL.md b/.github/skills/api-diff/SKILL.md index 1e1a24b6ce..637a026558 100644 --- a/.github/skills/api-diff/SKILL.md +++ b/.github/skills/api-diff/SKILL.md @@ -1,30 +1,83 @@ --- name: api-diff -description: Generate an API comparison report between two .NET versions using the RunApiDiff.ps1 script. Invoke when the user asks to run, create, or generate an API diff. -disable-model-invocation: true +description: > + Download, inspect, and diff .NET APIs for a given build or release. Use + `dotnet-inspect` to verify that APIs actually exist in the shipped packages, + detect missed reverts, and generate before/after API diffs. Use + `RunApiDiff.ps1` when a full markdown diff report is needed. --- -# API Diff Generation +# API Downloading, Verification, and Diffing -Map the user's request to parameters for `release-notes/RunApiDiff.ps1` and run it. See [release-notes/RunApiDiff.md](../../../release-notes/RunApiDiff.md) for the full parameter reference. +Use this skill when you need evidence about the **actual public API surface** of a .NET build. -When no versions are mentioned, run with no parameters — the script auto-infers versions. +This is primarily for: + +- confirming whether an API really shipped in the build binaries/ref packs +- catching reverts or incomplete rollouts before they end up in release notes +- generating before/after API diffs for a release milestone + +## Preferred workflow + +### 1. Query the right build first + +Do **not** trust the locally installed SDK for preview work. Query the target build's packages directly. + +Use the process in [api-verification.md](../release-notes/references/api-verification.md): + +1. Generate or read `build-metadata.json` +2. Get `nuget.source` and the package versions for the target release +3. Run `dotnet-inspect` against those exact packages + +### 2. Verify APIs with `dotnet-inspect` + +Typical tasks: + +- **Find a type or member** to confirm the API exists +- **Compare versions** to see what changed between previews or between RC and GA +- **Validate naming** before writing prose or code samples + +Examples: + +```bash +# Find a type in the runtime ref pack +dnx dotnet-inspect -y -- find "*AnyNewLine*" \ + --package "Microsoft.NETCore.App.Ref@${VER}" \ + --source "$FEED" + +# Verify members on a type +dnx dotnet-inspect -y -- member RegexOptions \ + --package "Microsoft.NETCore.App.Ref@${VER}" \ + --source "$FEED" \ + -k field + +# Diff public APIs between two versions +dnx dotnet-inspect -y -- diff \ + --package "Microsoft.NETCore.App.Ref@11.0.0-preview.2..11.0.0-preview.3" \ + --source "$FEED" +``` + +If the API is missing from the target build, treat that as a serious signal: it may have been renamed, kept internal, or reverted. + +### 3. Use `RunApiDiff.ps1` for full diff reports + +When the user wants the markdown-ready, repo-shaped API diff output, use `release-notes/RunApiDiff.ps1`. See [release-notes/RunApiDiff.md](../../../release-notes/RunApiDiff.md) for the full parameter reference. ## Mapping natural language to parameters -| User says | Parameters | -|---|---| -| "generate the next API diff" | *(none)* | -| ".NET 10 GA vs .NET 11 Preview 1" | `-PreviousMajorMinor 10.0 -CurrentMajorMinor 11.0 -CurrentPrereleaseLabel preview.1` | -| "net9.0-preview6 to net10.0-preview5" | `-PreviousMajorMinor 9.0 -PreviousPrereleaseLabel preview.6 -CurrentMajorMinor 10.0 -CurrentPrereleaseLabel preview.5` | -| ".NET 10 RC 2 vs .NET 10 GA" | `-PreviousMajorMinor 10.0 -PreviousPrereleaseLabel rc.2 -CurrentMajorMinor 10.0` | -| "10.0.0-preview.7.25380.108 to 10.0.0-rc.1.25451.107" | `-PreviousVersion "10.0.0-preview.7.25380.108" -CurrentVersion "10.0.0-rc.1.25451.107"` | - -- **GA** or no qualifier → omit the PrereleaseLabel parameter -- **Preview N** / **previewN** → `-PrereleaseLabel preview.N` -- **RC N** / **rcN** → `-PrereleaseLabel rc.N` -- **netX.Y-previewN** (TFM format) → `-MajorMinor X.Y -PrereleaseLabel preview.N` -- Full NuGet version strings → use `-PreviousVersion` / `-CurrentVersion` directly +| User says | Parameters | +| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| "generate the next API diff" | _(none)_ | +| ".NET 10 GA vs .NET 11 Preview 1" | `-PreviousMajorMinor 10.0 -CurrentMajorMinor 11.0 -CurrentPrereleaseLabel preview.1` | +| "net9.0-preview6 to net10.0-preview5" | `-PreviousMajorMinor 9.0 -PreviousPrereleaseLabel preview.6 -CurrentMajorMinor 10.0 -CurrentPrereleaseLabel preview.5` | +| ".NET 10 RC 2 vs .NET 10 GA" | `-PreviousMajorMinor 10.0 -PreviousPrereleaseLabel rc.2 -CurrentMajorMinor 10.0` | +| "10.0.0-preview.7.25380.108 to 10.0.0-rc.1.25451.107" | `-PreviousVersion "10.0.0-preview.7.25380.108" -CurrentVersion "10.0.0-rc.1.25451.107"` | + +- **GA** or no qualifier -> omit the `PrereleaseLabel` parameter for `RunApiDiff.ps1`; if a downstream api-diff release label is needed, use `ga` +- **Preview N** / **previewN** -> `-PrereleaseLabel preview.N` (for example, `preview.4`, not `preview4`) +- **RC N** / **rcN** -> `-PrereleaseLabel rc.N` +- **netX.Y-previewN** (TFM format) -> `-MajorMinor X.Y -PrereleaseLabel preview.N` +- Full NuGet version strings -> use `-PreviousVersion` / `-CurrentVersion` directly - The "previous" version is always the older version; "current" is the newer one ## Running the script @@ -33,4 +86,4 @@ When no versions are mentioned, run with no parameters — the script auto-infer .\release-notes\RunApiDiff.ps1 [mapped parameters] ``` -Set an initial wait of at least 300 seconds — the script takes several minutes. Use `read_powershell` to poll for completion. The script does not print a final "done" message; it exits after generating a README.md in the output folder. After completion, summarize the results: how many diff files were generated and where. +Set an initial wait of at least 300 seconds — the script takes several minutes. After completion, summarize the results: how many diff files were generated and where. diff --git a/.github/skills/editorial-scoring/SKILL.md b/.github/skills/editorial-scoring/SKILL.md new file mode 100644 index 0000000000..a592f9cd79 --- /dev/null +++ b/.github/skills/editorial-scoring/SKILL.md @@ -0,0 +1,109 @@ +--- +name: editorial-scoring +description: > + Apply the shared reader-centric rubric used to rank candidate features for + release notes, blog posts, and docs. Use this when you need scoring and cut + guidance independent of any one output format or task. +--- + +# Editorial Scoring + +Use this skill whenever you need to answer **"How important is this to a reader?"** + +This skill is intentionally **rubric-focused, not task-focused**. It does not generate files on its own. Instead, it provides the shared editorial calibration that other skills should reuse: + +- `generate-features` — assigns the first-pass scores +- `review-release-notes` — audits and recalibrates those scores +- `release-notes` — uses the resulting cut to decide what gets written up +- future blog/docs workflows — can reuse the same rubric with different thresholds + +This is the **canonical home** for the shared scoring scale and 80/20 audience +filter. Other release-notes docs should point back here instead of restating the +rubric in full. + +## Core question + +Score from the perspective of a developer upgrading to the new release — not from the perspective of the engineer who implemented the feature. + +## Reader-centric scale + +| Score | Reader reaction | What to do with it | +| ----- | -------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| `10` | "This is the first feature I'll enable or test." | Lead story | +| `8+` | "I'm going to use this when I upgrade." | Strong release-note feature | +| `6+` | "I'm glad I know about this. It will likely come in handy." | Good grouped release-note material | +| `4+` | "I can see how someone could use this. I'll look up the docs if I ever need it." | Optional mention, grouping candidate, or short paragraph | +| `2+` | "This one is a mystery to me." | Usually skip unless stronger explanation changes the score | +| `0` | "This is total gobbledygook — internal .NET engineering work." | Cut it from public-facing content | + +## 80/20 audience filter + +Default to features that make sense to roughly **80% of the audience**. + +Keep a more specialized feature for the other **20%** only when the remaining 80% can still understand why it matters and react with something like: + +> "Not for me, but I'm glad that's there for the people who need it." + +This is how foundational but initially niche work can still earn a good score. + +## Strong positive signals + +- New public APIs with obvious user value +- CLI or workflow improvements developers will notice right away +- Features that are easy to explain by anchoring them to a familiar tool or workflow +- Clear performance, reliability, or usability wins +- Features people have been asking for +- Changes that require docs, migration notes, or examples to use well + +## Strong negative signals + +- Test-only work +- Build or infrastructure churn +- Refactoring with no user-visible effect +- VMR sync noise, dependency updates, or repo automation +- Highly specialized implementation details that read like internal jargon +- Features that only matter after several stacked conditions are true (for example: uses single-file publish **and** cares deeply about startup **and** is willing to do extra training/tuning) +- PRs with very thin descriptions where the only concrete signal is an internal runtime/tooling term like cDAC +- Existing niche surfaces that are barely documented and are not part of the model's normal customer-facing understanding +- Claimed "new API" features where the actual new public API cannot be identified +- Bug fixes for features that were never really announced or are still obscure to most readers +- Old, low-engagement bugs filed internally that sat for a long time without clear external demand + +## Common scoring mistakes + +- **Technical novelty bias** — "this is clever" is not the same as "users will care" +- **API inventory mode** — listing everything from an API diff is not release-note curation +- **Effort bias** — a difficult implementation may still score low if the reader value is small +- **Insider-language inflation** — terms like JIT, LSRA, cDAC, intrinsics, or pipeline details can sound impressive but still be a `0-4` unless the value is explained clearly +- **Audience multiplication blindness** — when a feature requires several independent interests or behaviors, multiply those filters together instead of scoring the broadest prerequisite alone +- **Description vacuum optimism** — if the PR does not explain the user scenario, do not fill in the blanks with a generous story +- **Documentation blindness** — if an "existing" feature is not findable in Learn docs and does not show up as a known customer scenario, that is a signal to score it around `1`, not to assume hidden importance +- **Phantom API stories** — if you cannot identify the concrete new public API, do not give API credit based on a vague title alone +- **Promotion by association** — a fix in a glamorous subsystem is still just a `1` if it only patches an unannounced or niche feature +- **Demand inversion** — do not mistake an old internal bug with no reactions for evidence that many users need to hear about the fix +- **Fragmentation bias** — do not dismiss each small related item in isolation when several `2-4` items together form one intelligible reader story, such as a cluster of "Unsafe evolution" changes + +## Working thresholds + +- **8+** — worthy of its own section +- **6-7** — usually grouped with similar items, sometimes a short standalone section +- **0-5** — bug-fix bucket, one-liner, or cut + +Several `3-5` items in the same area can still combine into **one** worthwhile writeup. Keep the individual scores honest, then let the writing stage roll them up into a single themed section. + +## Breaking changes are separate from score + +Use score for **reader interest/value**, and use `breaking_changes: true` for **reaction/migration significance**. + +That means: + +- a **low-score breaking change** often still deserves a short end-of-notes callout +- a **high-score breaking change** may deserve both a full feature writeup and a migration note + +Do **not** inflate a narrow breaking change to `7+` just to keep it visible. + +## Canonical references + +- [`../release-notes/references/feature-scoring.md`](../release-notes/references/feature-scoring.md) — detailed scoring guidance +- [`../release-notes/references/quality-bar.md`](../release-notes/references/quality-bar.md) — what good release notes look like +- [`../release-notes/references/examples/README.md`](../release-notes/references/examples/README.md) — editorial examples and why they work diff --git a/.github/skills/generate-changes/SKILL.md b/.github/skills/generate-changes/SKILL.md new file mode 100644 index 0000000000..80ad551a98 --- /dev/null +++ b/.github/skills/generate-changes/SKILL.md @@ -0,0 +1,129 @@ +--- +name: generate-changes +description: > + Generate `changes.json` for a .NET release milestone by selecting the correct + VMR base/head refs and running `release-notes-gen generate changes`. Handles + preview-only multi-branch targeting (`main` vs release branches vs tags) and + emits the authoritative manifest of what shipped. DO NOT USE FOR: API + verification/diffs (use api-diff), feature scoring (use generate-features), + or writing markdown release notes (use release-notes). +--- + +# Generate `changes.json` + +Produce the authoritative `changes.json` input for a preview, RC, or GA release milestone. + +This is the **VMR-aware data acquisition stage** of the release notes pipeline: + +1. Determine which milestone(s) are active +2. Resolve the correct `--base` and `--head` refs +3. Run `release-notes-gen generate changes` +4. Write `changes.json` into the correct `release-notes/` folder + +## When to use + +- A new preview, RC, or GA milestone needs fresh shipped-change data +- The user wants to know whether **multiple preview milestones** are active at once +- The release notes branch should be refreshed after the VMR moved forward +- `changes.json` is missing, stale, or suspected to have the wrong ref selection + +## Preview-only branch targeting + +This is the subtle part, and it mostly matters for **previews**. + +Multiple preview milestones can be active simultaneously: + +```text +Latest shipped in this repo: Preview 3 +VMR main: Preview 5 +VMR release branch exists: Preview 4 + +→ Generate one `changes.json` for Preview 4 +→ Generate one `changes.json` for Preview 5 +``` + +For each target milestone `N`: + +| Milestone state | Base ref | Head ref | +| --------------------------------------- | ----------- | ------------------ | +| Tag exists for N | Tag for N-1 | Tag for N | +| Release branch exists for N, no tag yet | Tag for N-1 | Release branch tip | +| Only on `main` | Tag for N-1 | `main` | + +**Critical rule:** never use `main` for milestone `N` if `main` has already moved to `N+1`. + +## Inputs + +The user should provide as much of this as they know: + +- **Target release** — e.g. `.NET 11 Preview 4`, `.NET 10 RC 2`, `.NET 10 GA` +- **VMR clone path** — defaults to a local clone of `dotnet/dotnet` +- Optionally, the exact refs if they already know them + +If the user does **not** specify the milestone, infer it from: + +1. `release-notes/{version}/releases.json` in this repo +2. `eng/Versions.props` on `main` in the VMR +3. Matching preview tags and release branches in the VMR + +## Process + +### 1. Determine the floor from `releases.json` + +Find the latest shipped milestone in this repo. That tells you the lowest in-flight milestone that may need work. + +### 2. Inspect the VMR + +- Read `eng/Versions.props` on `main` to determine the current prerelease iteration +- List matching VMR tags for finalized milestones +- List matching VMR release branches for stabilizing milestones + +Use the [VMR structure reference](../release-notes/references/vmr-structure.md) for naming conventions and branch patterns. + +### 3. Resolve `--base` and `--head` + +For each active milestone: + +- `--base` is the previous shipped milestone tag +- `--head` is the milestone tag, release branch tip, or `main`, depending on what exists + +### 4. Generate the file + +```bash +release-notes-gen generate changes \ + --base \ + --head \ + --version "" \ + --date "" \ + --labels \ + --output release-notes///changes.json +``` + +Examples: + +```bash +# Preview milestone +release-notes-gen generate changes ~/git/dotnet \ + --base v11.0.0-preview.3.26210.100 \ + --head origin/release/11.0.1xx-preview4 \ + --version "11.0.0-preview.4" \ + --labels \ + --output release-notes/11.0/preview/preview4/changes.json + +# GA/patch milestone +release-notes-gen generate changes ~/git/dotnet \ + --base v10.0.7 \ + --head v10.0.8 \ + --version "10.0.8" \ + --output release-notes/10.0/10.0.8/changes.json +``` + +## Output contract + +The output file must follow the shared schema documented in [changes-schema.md](../release-notes/references/changes-schema.md): + +- top-level `release_version`, `release_date`, `changes`, `commits` +- stable `id` values in `repo@shortcommit` format +- same authoritative source of truth used by later skills + +Once `changes.json` exists, the next step is usually `generate-features`. diff --git a/.github/skills/generate-features/SKILL.md b/.github/skills/generate-features/SKILL.md new file mode 100644 index 0000000000..d11bfe8c50 --- /dev/null +++ b/.github/skills/generate-features/SKILL.md @@ -0,0 +1,116 @@ +--- +name: generate-features +description: > + Generate `features.json` from `changes.json` by ranking and annotating shipped + changes. `features.json` keeps the same schema as `changes.json` and adds + optional scoring fields so release notes, docs, and blog posts can apply + different cutoffs. Uses the shared `editorial-scoring` rubric. DO NOT USE + FOR: regenerating VMR diffs (use generate-changes) or writing final markdown + (use release-notes). +--- + +# Generate `features.json` + +Create a scored, reusable feature list from `changes.json`. + +This is the **triage stage** of the release notes pipeline. It turns the comprehensive manifest of shipped changes into a ranked set of candidates for external communication. + +Use [`editorial-scoring`](../editorial-scoring/SKILL.md) as the canonical rubric. Do **not** invent a different notion of importance for this step. + +## Purpose + +`changes.json` answers **what shipped**. + +`features.json` answers **what is worth talking about**, while staying schema-compatible with `changes.json` so the two files can be joined, diffed, and compared easily. + +## Schema contract + +`features.json` intentionally mirrors `changes.json`: + +- same top-level fields: `release_version`, `release_date`, `changes`, `commits` +- same `id`, `repo`, `product`, and commit key structure +- same `commits{}` object so cross-file joins still work + +The only additions are **optional enrichment fields** on change entries, such as: + +- `score` (`number`) — higher means more likely to be documented +- `score_reason` (`string`) — short explanation of the score +- `score_breakdown` (`object`) — optional per-dimension scoring details +- `breaking_changes` (`bool`) — mark changes that users may need to react to, even when they are not headline items + +This schema is intentionally loose and can grow as the workflow learns what it needs. + +`breaking_changes` is **separate from score**. A change might only be a `3` or `4` on reader interest, but still need to be carried forward as a short migration note because it can affect people upgrading. + +## Default scoring guidance + +Use a consistent numeric scale within a file. The recommended starting point is **0-10**: + +| Score | Reader reaction | Typical outcome | +| ----- | ------------------------------------------------ | -------------------------------------- | +| `10` | "This is the first feature I'll enable or test." | Lead story | +| `8+` | "I'm going to use this when I upgrade." | Strong release-note feature | +| `6+` | "I'm glad I know about this." | Good grouped release-note material | +| `4+` | "Someone will care; I can look it up later." | Optional mention or grouping candidate | +| `2+` | "This is a mystery to me." | Usually skip | +| `0` | Internal gobbledygook | Never document | + +See [`editorial-scoring`](../editorial-scoring/SKILL.md) for the shared rubric and [feature-scoring.md](../release-notes/references/feature-scoring.md) for the detailed heuristics. + +Apply the **80/20 rule** from that document: prefer features that make sense to most upgraders, and only keep niche items when the broader audience can still appreciate why they matter. + +## Process + +### 1. Start from `changes.json` + +Do **not** invent features from memory, roadmaps, or PR titles found elsewhere. Everything in `features.json` must trace back to `changes.json`. + +### 2. Score what shipped + +Look for signals such as: + +- new public APIs or meaningful behavioral changes +- developer productivity wins +- performance work with clear user impact +- compatibility or migration significance +- changes that need docs or samples to be usable +- features whose value is obvious to a broad slice of users + +If a change is important mainly because users need to adjust to it, set `breaking_changes: true` even if the score stays low. Do **not** inflate the score just to keep the item visible. + +If several individually modest changes cluster around one theme, keep that cluster in mind as you score. Do **not** inflate each entry, but do preserve the related items so downstream writing can roll them up into one coherent feature writeup. Title prefixes and labels are often useful clues here, such as a set of `[browser]` runtime changes or multiple "Unsafe evolution" items. + +Down-rank or exclude: + +- infra and dependency churn +- test-only changes +- internal refactors with no user-facing impact +- reverts and partial work that did not survive into the build +- items that mostly require insider knowledge to understand why they matter + +### 3. Use API evidence to refine the score + +If a change depends on public APIs, use `api-diff` / `dotnet-inspect` to confirm the API exists in the actual build. Missing or reverted APIs should be scored down or excluded. + +### 4. Write `features.json` + +The output typically lives next to `changes.json`: + +```text +release-notes/{major.minor}/preview/{previewN}/features.json +release-notes/{major.minor}/{major.minor.patch}/features.json +``` + +Keep the file mechanically friendly: + +- preserve the same IDs and commit keys when possible +- make `score` optional, not required +- keep `score_reason` brief and evidence-based + +## How downstream skills use it + +- **`release-notes`** uses higher-scored entries to draft markdown +- **`release-notes`** can also surface low-scored entries with `breaking_changes: true` as one-line callouts in a breaking-changes section +- **`review-release-notes`** re-checks the scores against editorial examples and trims over-scored items +- **Docs/blog workflows** can apply their own cutoffs later +- Humans can adjust scoring without changing the source-of-truth shipped data diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md new file mode 100644 index 0000000000..a0728c2dfb --- /dev/null +++ b/.github/skills/release-notes/SKILL.md @@ -0,0 +1,33 @@ +--- +name: release-notes +description: Generate and maintain .NET release notes from `features.json`. Uses `generate-changes` for authoritative shipped-change data, `generate-features` for scoring/triage, `editorial-scoring` for the shared rubric, `api-diff`/`dotnet-inspect` for API verification, and a multi-model `review-release-notes` pass for final editorial QA. +compatibility: Requires GitHub MCP server or gh CLI for cross-repo queries. Pairs with the generate-changes, generate-features, editorial-scoring, api-diff, and review-release-notes skills. Claude Opus 4.6 is the default workflow model; the preferred final reviewer pair is Claude Opus 4.6 + GPT-5.4 for broader editorial feedback. +--- + +# .NET Release Notes + +Generate and maintain release notes for .NET preview, RC, and GA releases. + +This skill is the **editorial writing stage** of the pipeline. It turns a scored `features.json` file into the markdown that ships in this repository. + +## How it works + +1. `generate-changes` diffs `source-manifest.json` between VMR refs to produce `changes.json` +2. `generate-features` reads `changes.json` and emits `features.json` with optional scores using the shared `editorial-scoring` rubric +3. `api-diff` / `dotnet-inspect` verifies public APIs and catches missed reverts +4. `release-notes` writes curated markdown using the higher-value entries from `features.json` +5. `review-release-notes` runs a final multi-model editorial QA pass against the scoring rubric and examples +6. Output is one PR per release milestone in dotnet/core, maintained incrementally + +## 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 shared `changes.json` / `features.json` schema +- [../editorial-scoring/SKILL.md](../editorial-scoring/SKILL.md) — the reusable scoring rubric and cut guidance +- [feature-scoring.md](references/feature-scoring.md) — how to score and cut features +- [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 0000000000..4d2a480b49 --- /dev/null +++ b/.github/skills/release-notes/references/api-verification.md @@ -0,0 +1,131 @@ +# 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 + +`release-notes-gen generate build-metadata` reads the VMR's version info and queries the nightly NuGet feed to find the correct package versions: + +```bash +release-notes-gen 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. + +For scoring and feature selection, this is also a **quality bar**: if a change only looks interesting because it seems to add a new API, but you cannot identify that API in the public surface, score it down sharply. That usually means it is internal plumbing, a refactor, or an existing niche surface getting maintenance rather than a real release-note feature. 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 0000000000..4168fe9ac2 --- /dev/null +++ b/.github/skills/release-notes/references/changes-schema.md @@ -0,0 +1,223 @@ +# Shared Schema for `changes.json` and `features.json` + +Reference for the `changes.json` file produced by `release-notes-gen generate changes`, and for the derived `features.json` file used by downstream editorial skills. One file per release milestone. + +## Overview + +`changes.json` is a comprehensive, machine-readable manifest of every PR and commit that shipped in a release. `features.json` is a scored derivative that keeps the same structure while adding optional enrichment for editorial prioritization. + +Both files also serve as companions to `cve.json` — they 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 +release-notes/{major.minor}/preview/{previewN}/features.json # previews +release-notes/{major.minor}/{major.minor.patch}/features.json # patches +release-notes/features.json # root multi-release features sidecar +``` + +## 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 or feature entries | +| `commits` | object | Normalized commit metadata, keyed by `repo@shortcommit` | + +## Root `release-notes/features.json` sidecar + +`release-notes/features.json` is an optional **root-level sidecar** for +multi-release or long-running features that need consistent naming, preview +callouts, and extra editorial notes across multiple milestones. + +Use it when: + +- a feature remains preview for a full release and may become stable in a later one +- the same feature may appear in multiple preview or RC milestones +- the notes should lead with a standard blockquote such as + `> This is a preview feature for .NET 11.` +- the feature has an official name, and any aliases should only help matching that feature across runs +- common casing or shorthand variants should still resolve to the same feature name + +Suggested structure: + +```json +{ + "features": [ + { + "id": "unsafe-evolution", + "official_name": "Unsafe Evolution", + "aliases": ["Memory Safety v2", "memory safety v2"], + "repos": ["roslyn", "runtime"], + "preview_in": "11.0", + "stable_in": "12.0", + "callout": "This is a preview feature for .NET 11.", + "notes": "Use the official name in headings and body copy. Aliases are for matching only. Avoid generic phrasing such as 'Unsafe code adds ...'." + } + ] +} +``` + +This root file is **separate from** the per-milestone `features.json` files +under each preview or patch directory. It is curated metadata used by the +writing stage to keep long-running features consistent across the release. + +- `official_name` is the canonical name to use in headings and prose. +- `aliases` are match-only helpers for recognition, casing variants, or prior shorthand names. + +## Change entry fields + +- `id` (`string`) — globally unique identifier in `repo@shortcommit` format, for example `"runtime@c5d5be4"` +- `repo` (`string`) — short repository name, for example `"runtime"` +- `product` (`string`) — product slug, for example `"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{}` for the VMR (`dotnet/dotnet`) codeflow commit +- `is_security` (`bool`) — `true` if this is a security change +- `local_repo_commit` (`string`) — key into `commits{}` for the source repo commit (same as `id`) +- `labels` (`array`) — PR labels, only present when `--labels` is used +- `score` (`number`) — optional editorial score; higher means more likely to document +- `score_reason` (`string`) — optional short explanation for the score +- `score_breakdown` (`object`) — optional structured scoring details +- `breaking_changes` (`bool`) — optional flag for changes users may need to react to, even if they are not headline features + +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`, `score`, `score_reason`, `score_breakdown`, `breaking_changes`) 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. + +## Breaking changes are a separate axis + +`breaking_changes` is **not the same thing** as `score`. + +- `score` answers: **"How broadly interesting or valuable is this to readers?"** +- `breaking_changes` answers: **"Do some users need to react to this?"** + +That means a change can be: + +- **high score + `breaking_changes: true`** — a widely relevant feature that also needs migration guidance +- **low score + `breaking_changes: true`** — a niche or narrow change that still deserves a short callout near the end of the release notes + +In practice, a `score` around `0-4` with `breaking_changes: true` usually means **one line in a "Breaking changes" section**, not a full feature writeup. + +## 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", + "score": 9, + "score_reason": "Broadly useful new JSON configuration preset" + }, + { + "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", + "score": 4, + "score_reason": "Narrower impact but users upgrading affected apps need to be aware of it.", + "breaking_changes": true + } + ], + "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 between the files + +`changes.json` is the **source-of-truth input** to the editorial process. `features.json` is an enriched view that usually adds scoring and notes without changing the underlying identity of the shipped changes. + +- `changes.json` has an entry for every PR that shipped +- `features.json` usually preserves those entries and adds optional scoring metadata +- `features.json` can also flag entries with `breaking_changes: true` so downstream writing can keep a short migration note even when the score is low +- Both files can be joined through shared `id` and `commits{}` values + +## Relationship to markdown release notes + +The markdown release notes are a curated subset informed by these JSON files: + +- `changes.json` has an entry for every PR that shipped +- `features.json` helps rank which shipped changes are worth calling out +- Markdown only covers features worth calling out +- The agent reads `changes.json` and/or `features.json` to decide 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 0000000000..70204ae09b --- /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 0000000000..126418088f --- /dev/null +++ b/.github/skills/release-notes/references/editorial-rules.md @@ -0,0 +1,165 @@ +# 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 concrete deltas over inferred outcomes** — say what changed and, if useful, the direct consequence for a real scenario. Avoid claims about trust, confidence, convenience, or behavior unless the source explicitly supports them. + - ✅ `Regex recognizes all Unicode newline sequences.` + - ✅ `Tar extraction now rejects entries that would write outside the destination directory.` + - ✅ `Tar extraction now rejects path traversal entries, helping applications avoid overwriting files outside the target folder.` + - ❌ `Compression and archive handling are easier to trust.` + - ❌ `You can extract archives without worrying about attacks.` +- **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) + +## Familiar comparisons + +- When a feature matches a workflow developers already know from another tool, say so. A short comparison can make the value obvious faster than a longer explanation. +- Use the comparison to **anchor the mental model**, not to claim perfect parity. Say what is similar, then explain the .NET-specific behavior. +- Prefer common tools and workflows the reader is likely to know already (for example Docker, GitHub Actions, shell usage, or package managers). Skip the comparison if it feels forced or more obscure than the feature itself. + + - ✅ `dotnet run -e FOO=BAR` gives you Docker-style CLI environment-variable injection for local app runs, so you can test configuration changes without editing shell state or launch profiles. + - ❌ `dotnet run` now works just like Docker. (overstates the similarity) + +## 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` +- Prefer **specific, customer-facing verbs** over generic `gets` phrasing. Name the capability the customer now has: `offers`, `adds`, `supports`, `recognizes`, `enables`, and similar verbs are usually stronger. + - ✅ `## System.Text.Json offers more control over naming and ignore defaults` + - ✅ `## Regex recognizes all Unicode newline sequences` + - ❌ `## System.Text.Json gets more control over naming and ignore defaults` +- Avoid anthropomorphic or club-like transition verbs such as `joins` when a more literal term is available. Prefer `moves to`, `is now in`, `adds support for`, or `supports`. + - ✅ `## Zstandard moved to System.IO.Compression and ZIP reads validate CRC32` + - ❌ `## Zstandard joins System.IO.Compression and ZIP reads validate CRC32` +- Use the **established feature name** when one exists, especially for long-running preview features. If `release-notes/features.json` lists an `official_name`, use that in headings and prose. Treat aliases as match-only metadata, not as the default wording. + - ✅ `## Unsafe Evolution remains a preview feature in .NET 11` + - ✅ `## Unsafe Evolution adds clearer diagnostics in Preview 3` + - ❌ `## Memory Safety v2 adds clearer diagnostics in Preview 3` + - ❌ `## Unsafe code adds clearer diagnostics and annotations` +- 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 + +- **Shared audience filter** — apply the 80/20 rule from `editorial-scoring`; don't redefine a competing threshold here. Keep narrower items only when the broader audience can still see why they matter. +- **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. +- **Breaking changes are separate from hype** — a breaking change can be important even when it is not exciting. Keep the score honest; use `breaking_changes: true` to preserve a short callout instead of inflating the item into a headline feature. +- **Clusters can be stronger than the parts** — several related low-score items can justify one section when together they tell a clear story. Keep the individual scores honest, then merge them into one writeup instead of emitting several weak mini-features. Good examples include a group of "Unsafe evolution" changes or multiple runtime entries prefixed with `[browser]`. + +## 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. When citing the source work, use linked `org/repo #number` references with a space before `#`: + +```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." + +If the fix is for a feature that was never really announced or is still obscure to most readers, do **not** turn that fix into a standalone feature entry. It usually belongs in the bug-fix bucket, and often scores around `1`. + +## 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 0000000000..574af2bfde --- /dev/null +++ b/.github/skills/release-notes/references/examples/README.md @@ -0,0 +1,29 @@ +# 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, with a space before `#`: `[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 +13. **Concrete beats promotional** — describe the observable change, not the feeling it might create. "Regex recognizes all Unicode newline sequences" or "Tar extraction now rejects path traversal entries" beats "compression and archive handling are easier to trust." +14. **Prefer strong verbs over `gets`** — write the customer-facing capability directly: "System.Text.Json offers more control..." is stronger than "System.Text.Json gets more control..." +15. **Prefer literal transition verbs** — if something moved namespaces or assemblies, say that directly. "Zstandard moved to `System.IO.Compression`" is clearer than "Zstandard joins `System.IO.Compression`." 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 0000000000..448bafe480 --- /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 0000000000..17089a372f --- /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 0000000000..05fe1d5234 --- /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 0000000000..31bcb0bc3f --- /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 0000000000..d8efba4a95 --- /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/feature-scoring.md b/.github/skills/release-notes/references/feature-scoring.md new file mode 100644 index 0000000000..83b7f3353e --- /dev/null +++ b/.github/skills/release-notes/references/feature-scoring.md @@ -0,0 +1,54 @@ +# Feature Scoring + +Use optional `score` fields in `features.json` to rank which shipped changes are +worth turning into release notes, docs, or blog content. + +The score is a **decision aid**, not a hard rule. Editorial judgment still +matters. + +Use [`../../editorial-scoring/SKILL.md`](../../editorial-scoring/SKILL.md) as +the **canonical source** for the shared reader-centric scale, thresholds, and +80/20 audience filter. This file adds the more specific heuristics that tend to +matter during .NET release-note triage. + +## What increases a score + +- **User-visible change** — new public API, CLI switch, behavior change, workflow improvement +- **Breadth of impact** — helps many developers, not just a narrow edge case +- **Clarity** — easy to explain with a short WHY+HOW description +- **Familiar anchor** — easier to understand because it maps to a tool or workflow readers already know +- **Evidence** — supported by API verification, tests, benchmarks, or a clear PR description +- **Documentation need** — developers may need guidance, migration notes, or examples + +## What lowers a score + +- Test-only or infra-only work +- Mechanical refactors with no user-facing impact +- Churn in dependencies or tooling internals +- Reverts, partial implementations, or APIs that do not appear in the actual build +- Changes too small or obscure to justify external attention +- **Stacked audience gates** — if the reader has to care about A, then B, then be willing to do C, the addressable audience shrinks at each step and the score should usually drop hard +- **Sparse evidence + internal hints** — if the PR barely explains the scenario and drops terms like cDAC, diagnostics plumbing, or runtime internals without a strong end-user story, treat it as near-internal by default +- **Invisible existing surface** — if the change is framed as a simplification or extension of an existing feature, but that feature is not visible in common model knowledge and not easy to find in Learn docs, default to about a `1` unless stronger customer evidence exists +- **No identifiable public API** — if the only apparent story is "there is a new API here" but you cannot point to a concrete new public type/member via API diff or `dotnet-inspect`, default to a `1` unless there is a separately strong behavior or workflow story +- **Bug fix for an unannounced feature** — if the work mostly fixes or rounds out a feature that customers were never really told about, treat it as a bug-fix-level item, often around a `1` +- **Low-demand bug history** — if the backing issue was filed internally, has little or no outside engagement, and sat for a long time, that is a signal the item likely belongs near `1` + +## Good scoring behavior + +- Score based on the **reader value of the shipped outcome**, not the effort involved +- Ask whether a typical upgrader immediately understands why the feature matters +- Use a short comparison to a familiar tool or workflow when it makes the value legible faster (for example, comparing CLI env-var injection to Docker's `-e` pattern) +- Discount aggressively for **compound niche scenarios**. "Single-file publishers who deeply care about startup and are willing to do training/tuning work" is much smaller than "people who publish single-file apps." +- Aggressively score down items that mostly read like internal plumbing or jargon +- If the strongest concrete clue is an internal term like **cDAC**, assume the feature is for runtime/tooling internals unless there is clear evidence of mainstream developer value +- For an **existing** feature area, ask two quick questions: "Would the model already recognize this as a real customer scenario?" and "Can I find it in Learn docs?" If both answers are effectively no, treat the item as a `1` by default +- For an API-centric entry, name the actual public API. If you cannot identify the new public surface area, do not score it like a real API feature +- Do not promote a **bug fix for an obscure or previously unannounced feature** into a release-note feature just because the subsystem sounds interesting +- Treat **issue provenance and engagement** as demand signals. An old bug filed by an internal architect with zero reactions is evidence of low urgency, not hidden headline value +- Consider **cluster value**, not just single-entry value. Several related `2-4` items can earn one grouped writeup when together they tell a coherent story (for example, a set of "Unsafe evolution" changes or a cluster of `[browser]` runtime improvements) +- Keep niche items only when they still impress or educate the broader audience +- A community contribution can lift a borderline item slightly, but it does **not** turn a niche optimization into a headline feature by itself +- Re-check scores after `api-diff` / `dotnet-inspect` confirms or disproves the public API story +- Prefer a short `score_reason` grounded in evidence over long prose +- It is okay for many entries to have low scores; `changes.json` is comprehensive, `features.json` is selective 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 0000000000..0e8618e6c6 --- /dev/null +++ b/.github/skills/release-notes/references/format-template.md @@ -0,0 +1,135 @@ +# 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)). + +## Breaking changes + +- Short migration note or heads-up for narrower changes that users may need to react to + +## 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 in concrete terms, with PR/issue links; avoid inferred feelings or marketing-style claims +3. **Code sample** — show the feature in use +4. **Feature ordering** — highest customer impact first +5. **Breaking changes near the end** — low-score entries with `breaking_changes: true` usually belong in a short section before Bug fixes, not as full feature sections +6. **Preview feature callout** — when a feature is listed in `release-notes/features.json`, start its section with the standard blockquote callout from that file + +## Issue and PR references + +Always use markdown links with the `{org}/{repo} #{number}` format, with a space before `#`: + +- ✅ `[dotnet/runtime #124264](https://github.com/dotnet/runtime/pull/124264)` +- ❌ `dotnet/runtime#124264` (wrong spacing and 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)). +``` + +Preview-feature example: + +```markdown +## Unsafe Evolution remains a preview feature in .NET 11 + +> This is a preview feature for .NET 11. + +Preview 3 adds clearer language-version errors for updated memory-safety rules... +``` + +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 0000000000..5920c05076 --- /dev/null +++ b/.github/skills/release-notes/references/quality-bar.md @@ -0,0 +1,89 @@ +# 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 `release-notes-gen 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 +- Features that most readers can immediately understand the value of + +Apply the shared **80/20 audience filter** from +[`../../editorial-scoring/SKILL.md`](../../editorial-scoring/SKILL.md): +prioritize features most readers can immediately understand, and keep narrower +items only when the broader audience can still appreciate why they matter. + +## 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) +- Anything that reads like unexplained engineering jargon to most readers + +## 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 + +When relevant, add a **familiar comparison** to help the reader place the feature quickly. If a new CLI workflow resembles something people already know from another tool, say that briefly. For example, `dotnet run -e FOO=BAR` can be framed as Docker-style environment-variable injection for local runs. Use comparisons to clarify, not to imply exact parity. + +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 0000000000..c583885f66 --- /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 `release-notes-gen 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/skills/review-release-notes/SKILL.md b/.github/skills/review-release-notes/SKILL.md new file mode 100644 index 0000000000..6cdfda1b79 --- /dev/null +++ b/.github/skills/review-release-notes/SKILL.md @@ -0,0 +1,121 @@ +--- +name: review-release-notes +description: > + Review `features.json` scores and draft release notes against the editorial + examples and the shared `editorial-scoring` rubric. Use it to identify + over-scored, under-scored, or missing features before publishing release + notes. DO NOT USE FOR: generating `changes.json` or writing the first draft. +--- + +# Review Release Notes + +Audit a scored `features.json` file and its markdown draft from the perspective of the **reader**, not the implementer. + +This is the **editorial QA stage** of the pipeline. Its job is to make sure the release notes feel curated, legible, and exciting to the right audience — not like an API inventory. + +This skill **reuses the shared rubric** from [`editorial-scoring`](../editorial-scoring/SKILL.md). It critiques the scoring; it does not invent a separate scoring philosophy. + +## When to use + +- After `generate-features` produced `features.json` +- After `release-notes` drafted the markdown +- When the selection feels too broad, too niche, or too internally focused +- When you want to compare the current draft against the examples and recalibrate the cut + +## Inputs + +Review these, in order: + +1. `changes.json` — the source of truth for what shipped +2. `features.json` — the scored candidate list, if present +3. Draft markdown files (`libraries.md`, `runtime.md`, `sdk.md`, etc.) +4. Editorial examples in `references/examples/` +5. The scoring and quality bar references: + - `../editorial-scoring/SKILL.md` + - `references/feature-scoring.md` + - `references/quality-bar.md` + +If `features.json` does not exist yet, infer the current implicit scoring from: + +- which features were promoted to headings +- how much space each feature received +- what was grouped into bug-fix buckets or omitted + +## Core review questions + +Use the shared rubric from [`../editorial-scoring/SKILL.md`](../editorial-scoring/SKILL.md) +rather than inventing a new one here. In particular: + +- keep the same reader-centric `10 / 8 / 6 / 4 / 2 / 0` scale +- apply the same 80/20 audience filter +- compare the draft against the examples to see whether it still teaches instead + of merely enumerating + +### Example check + +Compare the draft against the component examples: + +- Does the length match the importance? +- Are medium-value items grouped instead of getting their own heavyweight sections? +- Are performance stories backed by evidence? +- Does the draft teach, not just enumerate? + +## Common failure modes + +- **Over-scoring** — too many `4-6` items are promoted to top-level sections +- **Under-explaining** — a real `8+` feature is present but buried or not framed clearly +- **API inventory mode** — the draft starts mirroring `api-diff` instead of telling a user story +- **Technical novelty bias** — clever implementation details outrank practical user value + +## Multi-model review pattern + +For the final editorial QA pass, use this skill as a **two-reviewer parallel check** to get broader viewpoint diversity: + +Preferred set: + +1. **Claude Opus 4.6** +2. **GPT-5.4** + +Give both reviewers the same inputs and the same requested output: + +- most correct scoring +- most wrong scoring +- important omissions +- cut list + +Then synthesize the overlap and disagreements. Treat consensus as a strong signal, but do **not** turn this into a blind vote — fidelity to `changes.json`, the shared `editorial-scoring` rubric, and the repo's editorial rules still wins. + +## Reviewer checklist + +Do not ask reviewers the vague question "do you like this?" Give them the same +specific checks instead: + +1. **Which headings still sound vague, passive, anthropomorphic, or promotional?** +2. **Which sections fail the 80/20 reader-value test and should be cut, grouped, or demoted?** +3. **Which sentences infer feelings or outcomes (`trust`, `confidence`, `easier`, `better`) instead of stating the concrete change?** +4. **Which sections drift into API-inventory mode instead of teaching a user-facing story?** +5. **Which code samples or examples are weak, confusing, or unsupported by the text?** +6. **Which links, issue/PR references, or formatting details still violate house style?** +7. **What is the single highest-value rewrite still needed in the draft?** +8. **Is the wording conventional, or is it inventing non-standard phrasing or terms?** +9. **Are the subject and its adjective or adverb paired in a familiar way?** +10. **Would this phrasing seem normal in release notes for another developer platform?** +11. **If `release-notes/features.json` lists this feature, does the section begin with the standard preview blockquote?** + +Ask reviewers to answer with file + heading + issue + suggested rewrite. This +produces actionable review instead of general taste feedback. + +## Output + +Return a concise review with: + +1. **Most correct scoring** — the features that are best prioritized +2. **Most wrong scoring** — over-scored and under-scored items +3. **Important omissions** — items from `changes.json` / `features.json` that should likely be promoted +4. **Cut list** — what to group, compress, demote, or drop first + +As a working rule of thumb: + +- **8+** — section-worthy +- **6-7** — grouped paragraph or short section +- **0-5** — bug-fix bucket, one-liner, or cut diff --git a/.github/skills/update-distro-packages/SKILL.md b/.github/skills/update-distro-packages/SKILL.md index f0b0c76039..7f151acca7 100644 --- a/.github/skills/update-distro-packages/SKILL.md +++ b/.github/skills/update-distro-packages/SKILL.md @@ -34,24 +34,24 @@ release-notes/{version}/distros/ ## Prerequisites -The `dotnet-release` tool must be installed for markdown generation and package availability queries. +The `release-notes-gen` tool must be installed for markdown generation and package availability queries. The public `dotnet-release` tool is now for browsing release data and CVEs. ```bash # Check if already installed -dotnet-release --version +release-notes-gen --version ``` If not installed: ```bash -dotnet tool install -g Dotnet.Release.Tools \ +dotnet tool install -g ReleaseNotes.Gen \ --add-source https://nuget.pkg.github.com/richlander/index.json ``` If already installed, update to latest: ```bash -dotnet tool update -g Dotnet.Release.Tools \ +dotnet tool update -g ReleaseNotes.Gen \ --add-source https://nuget.pkg.github.com/richlander/index.json ``` @@ -214,7 +214,7 @@ Confirm every Linux distro in `supported-os.json` has a corresponding file. Regenerate `dotnet-dependencies.md` from the JSON files: ```bash -dotnet-release generate dotnet-dependencies {version} release-notes +release-notes-gen generate dotnet-dependencies {version} release-notes ``` This produces `release-notes/{version}/dotnet-dependencies.md` with copy-pasteable install commands for each distro and release. Never hand-edit this file — it is generated from the JSON. @@ -246,7 +246,7 @@ Requires `PKGS_ORG_TOKEN` to be set. ```bash export PKGS_ORG_TOKEN= -dotnet-release query distro-packages --dotnet-version {version} --output /tmp/distro-packages.json +release-notes-gen query distro-packages --dotnet-version {version} --output /tmp/distro-packages.json ``` This queries pkgs.org and supplemental feeds (Ubuntu backports via Launchpad, Homebrew, NixOS) and writes a JSON file with package availability per distro. @@ -400,8 +400,8 @@ Show the user a summary of which distros+releases have packages and from which f After any JSON changes, regenerate both markdown files: ```bash -dotnet-release generate dotnet-dependencies {version} release-notes -dotnet-release generate dotnet-packages {version} release-notes +release-notes-gen generate dotnet-dependencies {version} release-notes +release-notes-gen generate dotnet-packages {version} release-notes ``` - `dotnet-dependencies.md` — what OS packages .NET requires (from dependency data) diff --git a/.github/skills/update-os-packages/SKILL.md b/.github/skills/update-os-packages/SKILL.md index 0a0ec19778..3f1fcea9d8 100644 --- a/.github/skills/update-os-packages/SKILL.md +++ b/.github/skills/update-os-packages/SKILL.md @@ -2,7 +2,7 @@ name: update-os-packages description: > Audit and update os-packages.json/md files that document required Linux - packages for each .NET release. Uses the dotnet-release tool to verify + packages for each .NET release. Uses the release-notes-gen tool to verify package names against distro archives and regenerate markdown. USE FOR: adding packages for new distro versions, fixing incorrect package names, periodic package audits. DO NOT USE FOR: supported-os.json changes (use @@ -26,15 +26,15 @@ The scope of `os-packages.json` is broader than `supported-os.json`. It includes ## Prerequisites -The `dotnet-release` tool must be installed. Packages are published to [GitHub Packages](https://github.com/richlander/dotnet-release/packages). +The `release-notes-gen` tool must be installed. The public `dotnet-release` tool is now for browsing release data and CVEs. Packages are published to [GitHub Packages](https://github.com/richlander/dotnet-release/packages). ```bash # GitHub Packages requires authentication — use a GitHub token (PAT or GITHUB_TOKEN) -dotnet tool install -g Dotnet.Release.Tools \ +dotnet tool install -g ReleaseNotes.Gen \ --add-source https://nuget.pkg.github.com/richlander/index.json # Verify -dotnet-release --help +release-notes-gen --help ``` > **Note:** GitHub Packages requires authentication even for public repositories. If you get a 401 error, configure credentials for the source: @@ -63,17 +63,17 @@ The user provides: Run the verify command for each .NET version to audit: ```bash -dotnet-release verify os-packages release-notes +release-notes-gen verify os-packages release-notes ``` Examples: ```bash # Check 10.0 against local files -dotnet-release verify os-packages 10.0 release-notes +release-notes-gen verify os-packages 10.0 release-notes # Check against live data on GitHub (no local clone needed) -dotnet-release verify os-packages 10.0 +release-notes-gen verify os-packages 10.0 ``` **Interpret the exit code:** @@ -164,7 +164,7 @@ Delete the release object from the `releases` array for that distribution. After updating the JSON, regenerate the markdown file: ```bash -dotnet-release generate os-packages release-notes +release-notes-gen generate os-packages release-notes ``` This overwrites `os-packages.md` with content derived from the updated JSON. @@ -186,7 +186,7 @@ CI runs markdownlint via super-linter. If linting fails, fix the generator or Ma 1. Run verify again to confirm issues are resolved: ```bash - dotnet-release verify os-packages release-notes + release-notes-gen verify os-packages release-notes ``` Expect exit code 0 (or only skipped distros remaining). diff --git a/.github/skills/update-os-packages/references/verify-output-example.md b/.github/skills/update-os-packages/references/verify-output-example.md index 09079259f0..b7a9cbd520 100644 --- a/.github/skills/update-os-packages/references/verify-output-example.md +++ b/.github/skills/update-os-packages/references/verify-output-example.md @@ -1,6 +1,6 @@ # Verify Output Example -Example output from `dotnet-release verify os-packages 10.0 release-notes`: +Example output from `release-notes-gen verify os-packages 10.0 release-notes`: ````markdown # .NET 10.0 — OS Packages Verification diff --git a/.github/skills/update-release-graph/SKILL.md b/.github/skills/update-release-graph/SKILL.md index 6f410aaa5f..057eea3005 100644 --- a/.github/skills/update-release-graph/SKILL.md +++ b/.github/skills/update-release-graph/SKILL.md @@ -60,7 +60,7 @@ release-notes/ ### Graph generators -The `dotnet-release` tool includes four graph generation commands: +The `release-notes-gen` tool includes four graph generation commands: | Command | Generates | From | |---------|-----------|------| @@ -72,7 +72,7 @@ The `dotnet-release` tool includes four graph generation commands: All accept the same arguments: ``` -dotnet-release generate [output-dir] [--url-root ] +release-notes-gen generate [output-dir] [--url-root ] ``` ## When to use @@ -85,16 +85,16 @@ dotnet-release generate [output-dir] [--url-root ] ## Prerequisites -### dotnet-release +### release-notes-gen -The `dotnet-release` tool handles both graph generation and legacy file operations. Packages are published to [GitHub Packages](https://github.com/richlander/dotnet-release/packages). +The `release-notes-gen` tool handles both graph generation and legacy file operations. The public `dotnet-release` tool is for navigating release data and CVEs. Packages are published to [GitHub Packages](https://github.com/richlander/dotnet-release/packages). ```bash -dotnet tool install -g Dotnet.Release.Tools \ +dotnet tool install -g ReleaseNotes.Gen \ --add-source https://nuget.pkg.github.com/richlander/index.json # Verify — should show graph generation commands -dotnet-release --help +release-notes-gen --help ``` > **Note:** GitHub Packages requires authentication even for public repositories. If you get a 401 error, configure credentials for the source: @@ -156,27 +156,27 @@ The simplest approach is `generate indexes` which runs all three generators in s ```bash # Generate all graph files in one shot -dotnet-release generate indexes release-notes +release-notes-gen generate indexes release-notes ``` Or run each generator individually (order matters: version-index → timeline-index → llms-index): ```bash -dotnet-release generate version-index release-notes -dotnet-release generate timeline-index release-notes -dotnet-release generate llms-index release-notes +release-notes-gen generate version-index release-notes +release-notes-gen generate timeline-index release-notes +release-notes-gen generate llms-index release-notes ``` **Custom URL root** (for PR review before merging to release-index): ```bash -dotnet-release generate indexes release-notes --url-root https://raw.githubusercontent.com/dotnet/core/ +release-notes-gen generate indexes release-notes --url-root https://raw.githubusercontent.com/dotnet/core/ ``` **Separate output directory** (to inspect output without overwriting source): ```bash -dotnet-release generate indexes release-notes /tmp/graph-output +release-notes-gen generate indexes release-notes /tmp/graph-output ``` The default URL root is `https://raw.githubusercontent.com/dotnet/core/refs/heads/release-index/release-notes/`. @@ -185,21 +185,21 @@ The default URL root is `https://raw.githubusercontent.com/dotnet/core/refs/head ```bash # Regenerate releases-index.json from releases.json files -dotnet-release generate releases-index release-notes +release-notes-gen generate releases-index release-notes # Regenerate releases.md -dotnet-release generate releases release-notes +release-notes-gen generate releases release-notes ``` ### 4. Validate ```bash # Verify release links and hashes (can take minutes — do not cancel) -dotnet-release verify releases release-notes +release-notes-gen verify releases release-notes # Or for a specific version -dotnet-release verify releases {ver} release-notes +release-notes-gen verify releases {ver} release-notes # Skip hash verification for faster iteration -dotnet-release verify releases release-notes --skip-hash +release-notes-gen verify releases release-notes --skip-hash # Lint generated markdown npx markdownlint --config .github/linters/.markdown-lint.yml release-notes/releases.md @@ -290,7 +290,7 @@ Create the `release.json` in the patch directory. ### 2. Regenerate -Same as the patch release process — run `dotnet-release generate indexes release-notes` → legacy files. +Same as the patch release process — run `release-notes-gen generate indexes release-notes` → legacy files. ### 3. Verify the new version appears @@ -375,8 +375,8 @@ Immutable files use only `prev-*` links (no `next`). Mutable files use `latest-* - Never hand-edit generated files (`index.json`, `manifest.json`, `llms.json`, downloads files, timeline indexes) - `_manifest.json` (with underscore prefix) is the source; `manifest.json` (without prefix) is the generated output - Similarly, `_llms.json` is optional source overrides; `llms.json` is the generated output -- `releases-index.json` (legacy flat format) is generated by `dotnet-release generate releases-index` -- `releases.md` is generated by `dotnet-release generate releases` +- `releases-index.json` (legacy flat format) is generated by `release-notes-gen generate releases-index` +- `releases.md` is generated by `release-notes-gen generate releases` - The generators are idempotent — running them on unchanged source data produces identical output - When running individually, order matters: `version-index` → `timeline-index` → `llms-index`; `generate indexes` handles this automatically - All `_links.*.href` values are absolute URLs; the base URL is controlled by `--url-root` @@ -389,10 +389,10 @@ Immutable files use only `prev-*` links (no `next`). Mutable files use `latest-* |---------|------------| | Hand-editing `index.json` or other generated files | Edit source data and re-run the generators | | Hand-editing `manifest.json` | Edit `_manifest.json` and re-run `VersionIndex` | -| Hand-editing `releases-index.json` | Run `dotnet-release generate releases-index release-notes` | -| Hand-editing `releases.md` | Run `dotnet-release generate releases release-notes` | +| Hand-editing `releases-index.json` | Run `release-notes-gen generate releases-index release-notes` | +| Hand-editing `releases.md` | Run `release-notes-gen generate releases release-notes` | | Running generators in wrong order | Use `generate indexes` (handles order automatically) or run: version-index → timeline-index → llms-index | | Missing `_manifest.json` for a version | The generators warn but fall back to `releases.json`; create `_manifest.json` for accurate lifecycle data | | Missing `release.json` for a patch | Patch detail index will be incomplete; ensure every patch has a `release.json` | -| Editing source data without regenerating | Always run `dotnet-release generate indexes release-notes` after changing source files | +| Editing source data without regenerating | Always run `release-notes-gen generate indexes release-notes` after changing source files | | Forgetting `--url-root` for PR review | Links will use the default release-index branch URL; pass `--url-root` with a commit SHA for verifiable links in PRs | diff --git a/.github/skills/update-supported-os/SKILL.md b/.github/skills/update-supported-os/SKILL.md index e958954780..b6c4c4556f 100644 --- a/.github/skills/update-supported-os/SKILL.md +++ b/.github/skills/update-supported-os/SKILL.md @@ -2,7 +2,7 @@ name: update-supported-os description: > Audit and update supported-os.json/md files to reflect current OS version - support. Uses the dotnet-release tool for automated verification against + support. Uses the release-notes-gen tool for automated verification against upstream lifecycle data and markdown regeneration. USE FOR: adding new OS versions, moving EOL versions to unsupported, periodic support matrix audits. DO NOT USE FOR: os-packages.json changes (use update-os-packages skill), @@ -23,17 +23,17 @@ Audit and update `supported-os.json` files in this repository. These files decla The following tools must be installed: -### dotnet-release +### release-notes-gen -The `dotnet-release` tool is used to verify and generate supported OS files. Packages are published to [GitHub Packages](https://github.com/richlander/dotnet-release/packages). +The `release-notes-gen` tool is used to verify and generate supported OS files. The public `dotnet-release` tool is now for browsing release data and CVEs. Packages are published to [GitHub Packages](https://github.com/richlander/dotnet-release/packages). ```bash # GitHub Packages requires authentication — use a GitHub token (PAT or GITHUB_TOKEN) -dotnet tool install -g Dotnet.Release.Tools \ +dotnet tool install -g ReleaseNotes.Gen \ --add-source https://nuget.pkg.github.com/richlander/index.json # Verify -dotnet-release --help +release-notes-gen --help ``` > **Note:** GitHub Packages requires authentication even for public repositories. If you get a 401 error, configure credentials for the source: @@ -75,17 +75,17 @@ The user provides: Run the verify command for each .NET version to audit: ```bash -dotnet-release verify supported-os release-notes +release-notes-gen verify supported-os release-notes ``` Examples: ```bash # Check 10.0 against local files -dotnet-release verify supported-os 10.0 release-notes +release-notes-gen verify supported-os 10.0 release-notes # Check against live data on GitHub (no local clone needed) -dotnet-release verify supported-os 10.0 +release-notes-gen verify supported-os 10.0 ``` **Interpret the exit code:** @@ -145,7 +145,7 @@ For each confirmed change, edit `release-notes//supported-os.json`: After updating the JSON, regenerate the markdown file: ```bash -dotnet-release generate supported-os release-notes +release-notes-gen generate supported-os release-notes ``` This overwrites `supported-os.md` with content derived from the updated JSON. @@ -171,7 +171,7 @@ Check if any newly added distro versions need entries in `os-packages.json`. If 1. Run verify again to confirm issues are resolved: ```bash - dotnet-release verify supported-os release-notes + release-notes-gen verify supported-os release-notes ``` Remaining items are acceptable if they are: diff --git a/.github/skills/verify-releases/SKILL.md b/.github/skills/verify-releases/SKILL.md index 732cbee18f..759f43fcee 100644 --- a/.github/skills/verify-releases/SKILL.md +++ b/.github/skills/verify-releases/SKILL.md @@ -2,7 +2,7 @@ name: verify-releases description: > Validate releases and release links: URL liveness, file hashes, CDN - latest.version files, and aka.ms redirect targets. Uses dotnet-release verify + latest.version files, and aka.ms redirect targets. Uses release-notes-gen verify and generate commands against the local release-notes directory. USE FOR: validate the latest release, validate release links, validating that all download links return HTTP 200, verifying SHA512 hashes match downloaded @@ -16,21 +16,21 @@ description: > # Verify Releases -Validate .NET release data in `release-notes/` using the `dotnet-release` CLI tool. This skill checks that download URLs are live, file hashes match, CDN latest.version files are current, and aka.ms redirects resolve correctly. +Validate .NET release data in `release-notes/` using the `release-notes-gen` CLI tool. This skill checks that download URLs are live, file hashes match, CDN latest.version files are current, and aka.ms redirects resolve correctly. ## Prerequisites -### dotnet-release +### release-notes-gen -The `dotnet-release` tool is published to [GitHub Packages](https://github.com/richlander/dotnet-release/packages). +The `release-notes-gen` tool is published to [GitHub Packages](https://github.com/richlander/dotnet-release/packages). The public `dotnet-release` tool is now for browsing release data and CVEs. ```bash # Install -dotnet tool install -g Dotnet.Release.Tools \ +dotnet tool install -g ReleaseNotes.Gen \ --add-source https://nuget.pkg.github.com/richlander/index.json # Verify — must show "verify releases" in usage output -dotnet-release +release-notes-gen ``` > **Note:** GitHub Packages requires authentication even for public repositories. If you get a 401 error, configure credentials: @@ -46,7 +46,7 @@ dotnet-release **Version check:** If the tool usage output does not include `verify releases` in its command list, the installed version is too old. Update with: ```bash -dotnet tool update -g Dotnet.Release.Tools \ +dotnet tool update -g ReleaseNotes.Gen \ --add-source https://nuget.pkg.github.com/richlander/index.json ``` @@ -57,7 +57,7 @@ dotnet tool update -g Dotnet.Release.Tools \ Downloads every binary and verifies SHA512 hashes against `releases.json`. This is the most thorough check and takes several minutes. ```bash -dotnet-release verify releases release-notes +release-notes-gen verify releases release-notes ``` ### Verify all supported versions (quick — skip hashes) @@ -65,20 +65,20 @@ dotnet-release verify releases release-notes Checks URL liveness, CDN latest.version, and aka.ms redirects only. Much faster — typically under 30 seconds. ```bash -dotnet-release verify releases release-notes --skip-hash +release-notes-gen verify releases release-notes --skip-hash ``` ### Verify a specific major version ```bash -dotnet-release verify releases 10.0 release-notes -dotnet-release verify releases 10.0 release-notes --skip-hash +release-notes-gen verify releases 10.0 release-notes +release-notes-gen verify releases 10.0 release-notes --skip-hash ``` ### Verify a specific patch release ```bash -dotnet-release verify releases 10.0.5 release-notes +release-notes-gen verify releases 10.0.5 release-notes ``` ## What gets verified @@ -112,13 +112,13 @@ dotnet-release verify releases 10.0.5 release-notes ### 1. Check tool version -Confirm `dotnet-release` is installed and has the `verify releases` command: +Confirm `release-notes-gen` is installed and has the `verify releases` command: ```bash -dotnet-release +release-notes-gen ``` -The usage output must include `dotnet-release verify releases [version] [path] [--skip-hash]`. If it does not, update the tool (see Prerequisites). +The usage output must include `release-notes-gen verify releases [version] [path] [--skip-hash]`. If it does not, update the tool (see Prerequisites). ### 2. Run verification @@ -126,13 +126,13 @@ For a standard validation (recommended for release sign-off): ```bash cd ~/git/core -dotnet-release verify releases release-notes +release-notes-gen verify releases release-notes ``` For a quick check during development: ```bash -dotnet-release verify releases release-notes --skip-hash +release-notes-gen verify releases release-notes --skip-hash ``` ### 3. Interpret results @@ -156,8 +156,8 @@ Present a summary table with per-version results: ## .NET Release Link Verification Report **Date:** YYYY-MM-DD -**Tool:** `dotnet-release` vX.Y.Z -**Command:** `dotnet-release verify releases release-notes` +**Tool:** `release-notes-gen` vX.Y.Z +**Command:** `release-notes-gen verify releases release-notes` | Version | Latest Release | Download URLs | SHA512 Hashes | CDN latest.version | aka.ms | Status | |---------|---------------|---------------|---------------|-------------------|--------|--------| @@ -177,10 +177,10 @@ After verifying releases, you may also want to regenerate the legacy index and m ```bash # Regenerate releases-index.json -dotnet-release generate releases-index release-notes +release-notes-gen generate releases-index release-notes # Regenerate releases.md -dotnet-release generate releases release-notes +release-notes-gen generate releases release-notes # Lint the generated markdown npx markdownlint --config .github/linters/.markdown-lint.yml release-notes/releases.md diff --git a/.github/skills/verify-releases/references/verify-output-example.md b/.github/skills/verify-releases/references/verify-output-example.md index f1348e443b..37ee9a21a0 100644 --- a/.github/skills/verify-releases/references/verify-output-example.md +++ b/.github/skills/verify-releases/references/verify-output-example.md @@ -2,7 +2,7 @@ ## Full verification (with hashes) -Command: `dotnet-release verify releases release-notes` +Command: `release-notes-gen verify releases release-notes` ``` Verifying release links for all supported versions in /Users/rich/git/core/release-notes... @@ -64,7 +64,7 @@ Exit code: `0` ## Quick verification (skip hashes) -Command: `dotnet-release verify releases release-notes --skip-hash` +Command: `release-notes-gen verify releases release-notes --skip-hash` ``` Verifying release links for all supported versions in /Users/rich/git/core/release-notes... @@ -115,8 +115,8 @@ Exit code: `0` ## .NET Release Link Verification Report **Date:** 2026-03-27 -**Tool:** `dotnet-release` v1.1.0 -**Command:** `dotnet-release verify releases release-notes` +**Tool:** `release-notes-gen` v1.1.0 +**Command:** `release-notes-gen verify releases release-notes` | Version | Latest Release | Download URLs | SHA512 Hashes | CDN latest.version | aka.ms | Status | |---------|---------------|---------------|---------------|-------------------|--------|--------| diff --git a/.github/workflows/release-notes.lock.yml b/.github/workflows/release-notes.lock.yml new file mode 100644 index 0000000000..8a258cabeb --- /dev/null +++ b/.github/workflows/release-notes.lock.yml @@ -0,0 +1,1287 @@ +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.65.1). 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":"49a735ce89ffcd0b73da9dda5c5b390b23e20d2fbb48ab92997d03cf56137193","compiler_version":"v0.65.1","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 directory (e.g., preview4). Package/API-diff labels use preview.4; GA is ga/omitted depending on the tool. 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@1831bcd4f397da99da6e6e6b65687557a1a37ac7 # v0.65.1 + 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.1" + 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.5" + 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.1" + 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_34e76750cb77576c_EOF' + + GH_AW_PROMPT_34e76750cb77576c_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_34e76750cb77576c_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_34e76750cb77576c_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_34e76750cb77576c_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_34e76750cb77576c_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_34e76750cb77576c_EOF' + + {{#runtime-import .github/workflows/release-notes.md}} + GH_AW_PROMPT_34e76750cb77576c_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' }} + 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@1831bcd4f397da99da6e6e6b65687557a1a37ac7 # v0.65.1 + 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.5 + - 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.5 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.5 ghcr.io/github/gh-aw-firewall/squid:0.25.5 ghcr.io/github/gh-aw-mcpg:v0.2.10 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_5bc8fc1a8a382f8b_EOF' + {"add_comment":{"max":20,"target":"*"},"create_pull_request":{"draft":true,"labels":["area-release-notes","automation"],"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":["area-release-notes","automation"],"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_5bc8fc1a8a382f8b_EOF + - name: Write Safe Outputs Tools + run: | + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_145ed235d2c9bc9a_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 [\"area-release-notes\" \"automation\"] 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_145ed235d2c9bc9a_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_9f3b5600d910518f_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_9f3b5600d910518f_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_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_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.10' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_a482666cb00b2b3f_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_a482666cb00b2b3f_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(release-notes-gen) + # --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.5 --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(release-notes-gen)'\'' --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.1 + 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: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - 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() + 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: 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@1831bcd4f397da99da6e6e6b65687557a1a37ac7 # v0.65.1 + 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" + 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/noop.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(); + - name: Handle No-Op Message + id: handle_noop_message + 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_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + 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: Handle Create Pull Request Error + id: handle_create_pr_error + 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 }} + 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_create_pr_error.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 + 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@1831bcd4f397da99da6e6e6b65687557a1a37ac7 # v0.65.1 + 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" + # --- 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.5 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.5 ghcr.io/github/gh-aw-firewall/squid:0.25.5 + - 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.5 + - 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.5 --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.1 + 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@1831bcd4f397da99da6e6e6b65687557a1a37ac7 # v0.65.1 + 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_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@1831bcd4f397da99da6e6e6b65687557a1a37ac7 # v0.65.1 + 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\":[\"area-release-notes\",\"automation\"],\"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\":[\"area-release-notes\",\"automation\"],\"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 0000000000..49095f4fd0 --- /dev/null +++ b/.github/workflows/release-notes.md @@ -0,0 +1,331 @@ +--- +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: [area-release-notes, automation] + draft: true + max: 5 + push-to-pull-request-branch: + title-prefix: "[release-notes] " + labels: [area-release-notes, automation] + max: 5 + add-comment: + max: 20 + target: "*" +tools: + github: + toolsets: [issues, pull_requests, repos, search] + bash: + - dotnet + - release-notes-gen + - git + - jq +timeout-minutes: 120 + +on: + schedule: daily around 9am PDT + workflow_dispatch: + inputs: + milestone: + description: "Target milestone directory (e.g., preview4). Package/API-diff labels use preview.4; GA is ga/omitted depending on the tool. 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 `release-notes-gen generate changes` +2. **`features.json`** — a scored derivative of `changes.json` used to rank what is worth documenting +3. **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 `release-notes-gen 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. +- **Use scoring as guidance, not law** — `features.json` helps prioritize, but humans and editorial judgment still decide what makes the cut. +- **Reader value first** — use the score from the reader's point of view: 10 = "first thing I'll try", 8 = "I'll use this on upgrade", 6 = "glad to know", 4 = "maybe later", 2 = "mystery", 0 = "internal gobbledygook". +- **Prefer the 80%** — default to features that make sense to most users. Keep 20%-audience features only when the other 80% can still appreciate why they matter. +- **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 and skills for detailed guidance: + +- **editorial-scoring** — shared reader-centric scoring rubric and 80/20 audience filter ([skill](../skills/editorial-scoring/SKILL.md)) +- **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 shared `changes.json` / `features.json` schema +- **feature-scoring.md** — how to score and cut candidate features +- **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) + +## Model strategy + +- Use **Claude Opus 4.6** as the default model throughout the workflow for orchestration, scoring, and drafting. +- For the **final `review-release-notes` pass**, prefer this **two-model reviewer set** to widen the editorial viewpoint: + - **Claude Opus 4.6** + - **GPT-5.4** +- Give both reviewers the same inputs and ask for the same output shape. Synthesize the overlap, inspect meaningful disagreements, and prefer the shared `editorial-scoring` rubric over any single model's preference. + +## 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 +release-notes-gen 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. Generate or refresh features.json + +Use `changes.json` as the source of truth and write a sibling `features.json` that preserves the same schema while adding optional scoring metadata: + +- keep the same `id` and `commits{}` values +- assign higher scores to externally meaningful, user-visible changes +- down-rank infra, churn, test-only work, and anything that appears reverted +- preserve any useful human annotations if the file already exists + +#### c. Re-check the cut with the reader rubric + +Before finalizing the candidate list, ask: + +- Is this one of the **first things a reader will want to try**? +- Will many readers **use it when they upgrade**? +- Will readers at least be **glad they learned about it now**? +- Or does it mainly read like **internal engineering jargon**? + +Default to features that make sense to roughly **80% of the audience**. Specialized features are still welcome, but only when the other 80% can still understand why they matter. + +#### d. 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. + +#### e. Write or update markdown + +Using `features.json`, `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` +- If `release-notes/features.json` exists, consult it before writing. For matching long-running features, use the established feature name and start the section with the standard preview blockquote from the sidecar file. +- Components with no noteworthy changes get a minimal stub + +#### f. 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?" + +#### g. 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. + +#### h. Run the final multi-model review + +Before pushing the draft, run the `review-release-notes` stage as a **two-agent parallel review**: + +- **Reviewer 1:** Claude Opus 4.6 +- **Reviewer 2:** GPT-5.4 + +Have each reviewer critique the same draft using the same rubric, examples, and +this checklist: + +- Which headings still sound vague, passive, anthropomorphic, or promotional? +- Which sections fail the 80/20 reader-value test and should be cut, grouped, or demoted? +- Which sentences infer feelings or outcomes instead of stating the concrete change? +- Which sections drift into API-inventory mode instead of teaching a user story? +- Which code samples or examples are weak or confusing? +- Which links, issue/PR references, or formatting details still violate house style? +- What is the single highest-value rewrite still needed? +- Is the wording conventional, or is it inventing non-standard phrasing or terms? +- Are the subject and its adjective or adverb paired in a familiar way? +- Would this phrasing seem normal within release notes for another developer platform? + +Ask for file + heading + issue + suggested rewrite, not generic preference. Then: + +- apply the changes that have clear consensus +- keep a human-readable note of any major disagreement +- avoid "majority vote" thinking when it conflicts with fidelity or house style + +#### i. 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/release-notes/RunApiDiff.md b/release-notes/RunApiDiff.md index 9a874e7f7a..e0aac975ab 100644 --- a/release-notes/RunApiDiff.md +++ b/release-notes/RunApiDiff.md @@ -26,25 +26,25 @@ When run with no arguments, the script infers the next version to diff by scanni By default the script assumes a diff will be produced for the next preview. When no version information is provided, the script scans existing `api-diff` folders in the repository to find the latest version and infers the next one in the progression (preview.1 → preview.2 → ... → preview.7 → rc.1 → rc.2 → GA → next major preview.1). When `PreviousVersion` or `CurrentVersion` is provided, the `MajorMinor` and `PrereleaseLabel` values are extracted from it automatically. | Parameter | Description | Default | -|---|---|---| +| --- | --- | --- | | `PreviousVersion` | Exact package version for the "before" comparison (e.g., `10.0.0-preview.7.25380.108`). MajorMinor and PrereleaseLabel are extracted automatically. | *(empty — inferred or searched)* | | `CurrentVersion` | Exact package version for the "after" comparison (e.g., `10.0.0-rc.1.25451.107`). MajorMinor and PrereleaseLabel are extracted automatically. | *(empty — inferred or searched)* | | `PreviousMajorMinor` | The "before" .NET major.minor version (e.g., `10.0`) | Inferred from api-diffs, extracted from `PreviousVersion`, or discovered from `PreviousNuGetFeed` | -| `PreviousPrereleaseLabel` | Prerelease label for the "before" version (e.g., `preview.7`, `rc.1`). Omit for GA. | Inferred from api-diffs, extracted from `PreviousVersion`, or discovered from `PreviousNuGetFeed` | +| `PreviousPrereleaseLabel` | Prerelease label for the "before" version (e.g., `preview.4`, `preview.7`, `rc.1`). Omit for GA when calling `RunApiDiff.ps1`; the api-diff release label itself is `ga`. | Inferred from api-diffs, extracted from `PreviousVersion`, or discovered from `PreviousNuGetFeed` | | `CurrentMajorMinor` | The "after" .NET major.minor version (e.g., `10.0`) | Inferred from api-diffs, extracted from `CurrentVersion`, or discovered from `CurrentNuGetFeed` | -| `CurrentPrereleaseLabel` | Prerelease label for the "after" version (e.g., `preview.7`, `rc.1`). Omit for GA. | Inferred from api-diffs, extracted from `CurrentVersion`, or discovered from `CurrentNuGetFeed` | +| `CurrentPrereleaseLabel` | Prerelease label for the "after" version (e.g., `preview.4`, `preview.7`, `rc.1`). Omit for GA when calling `RunApiDiff.ps1`; the api-diff release label itself is `ga`. | Inferred from api-diffs, extracted from `CurrentVersion`, or discovered from `CurrentNuGetFeed` | ### Feed Parameters | Parameter | Description | Default | -|---|---|---| +| --- | --- | --- | | `CurrentNuGetFeed` | NuGet feed URL for downloading "after" packages | `https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json` | | `PreviousNuGetFeed` | NuGet feed URL for downloading "before" packages | `https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json` | ### Path Parameters | Parameter | Description | Default | -|---|---|---| +| --- | --- | --- | | `CoreRepo` | Path to your local clone of the dotnet/core repo | Git repo root relative to the script | | `TmpFolder` | Working directory for downloaded and extracted packages | Auto-created temp directory | | `AttributesToExcludeFilePath` | Path to attributes exclusion file | `ApiDiffAttributesToExclude.txt` (same folder as script) | @@ -53,7 +53,7 @@ By default the script assumes a diff will be produced for the next preview. When ### Switches | Parameter | Description | -|---|---| +| --- | --- | | `ExcludeNetCore` | Skip the Microsoft.NETCore.App comparison | | `ExcludeAspNetCore` | Skip the Microsoft.AspNetCore.App comparison | | `ExcludeWindowsDesktop` | Skip the Microsoft.WindowsDesktop.App comparison | @@ -67,13 +67,18 @@ By default the script assumes a diff will be produced for the next preview. When .\RunApiDiff.ps1 # Specify only the current version; previous is inferred from existing api-diffs -.\RunApiDiff.ps1 -CurrentMajorMinor 11.0 -CurrentPrereleaseLabel preview.2 +.\RunApiDiff.ps1 -CurrentMajorMinor 11.0 -CurrentPrereleaseLabel preview.4 # Specify both versions explicitly .\RunApiDiff.ps1 ` -PreviousMajorMinor 10.0 -PreviousPrereleaseLabel preview.7 ` -CurrentMajorMinor 10.0 -CurrentPrereleaseLabel rc.1 +# Compare RC to GA; omit CurrentPrereleaseLabel for GA +.\RunApiDiff.ps1 ` + -PreviousMajorMinor 10.0 -PreviousPrereleaseLabel rc.2 ` + -CurrentMajorMinor 10.0 + # Use exact NuGet package versions (MajorMinor and PrereleaseLabel are extracted automatically) .\RunApiDiff.ps1 ` -PreviousVersion "10.0.0-preview.7.25380.108" ` diff --git a/release-notes/features.json b/release-notes/features.json new file mode 100644 index 0000000000..2a9fdc8b18 --- /dev/null +++ b/release-notes/features.json @@ -0,0 +1,14 @@ +{ + "features": [ + { + "id": "unsafe-evolution", + "official_name": "Unsafe Evolution", + "aliases": ["Memory Safety v2", "memory safety v2"], + "repos": ["roslyn", "runtime"], + "preview_in": "11.0", + "stable_in": "12.0", + "callout": "This is a preview feature for .NET 11.", + "notes": "Use the official name in headings and body copy. Keep aliases for matching only rather than as the default wording. Avoid generic phrasing such as 'Unsafe code adds ...'." + } + ] +}