Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
96ecd81
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Jan 27, 2026
1f4a46a
Merge commit '88ad5f0025a2ad63a22812dcace1792034158a73'
Feb 2, 2026
693c77a
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Feb 5, 2026
6541a2a
Merged PR 57170: [internal/release/8.0] Update dependencies from dnce…
Feb 5, 2026
d13bef6
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Feb 6, 2026
7f6e087
Merged PR 57615: [internal/release/8.0] Update dependencies from dnce…
Feb 6, 2026
26eda04
Merged PR 57701: [internal/release/8.0] Update dependencies from dnce…
Feb 11, 2026
3eb4dac
Merge commit 'fe8711df6170aebd9206fb4cc1f2686beb52c3ea'
Feb 11, 2026
fb9efa3
Update dependencies from https://dev.azure.com/dnceng/internal/_git/d…
dotnet-bot Feb 12, 2026
ccddb58
Merged PR 57828: [internal/release/8.0] Update dependencies from dnce…
Feb 12, 2026
3fbb320
Merge commit 'ccddb58237acbffdddb7678aad16dd1bf450cac8' into internal…
vseanreesermsft Mar 10, 2026
d18a6d7
Merge pull request #37900 from vseanreesermsft/internal-merge-8.0-202…
vseanreesermsft Mar 11, 2026
4e02b8f
Merge branch 'release/9.0' into merge/release/8.0-to-release/9.0
AndriySvyryd Mar 27, 2026
6e2ae93
Remove darc-int-dotnet-runtime package source
AndriySvyryd Mar 27, 2026
1a7876a
[release/10.0] Source code updates from dotnet/dotnet (#38026)
dotnet-maestro[bot] Apr 1, 2026
5e2677f
[release/9.0] Update branding to 9.0.16 (#38063)
vseanreesermsft Apr 8, 2026
576af87
Update dependencies from https://github.com/dotnet/arcade build 20260…
dotnet-maestro[bot] Apr 8, 2026
3c2677e
Update dependencies from build 309217 (#38046)
dotnet-maestro[bot] Apr 8, 2026
d08bbae
Merge pull request #37908 from dotnet/merge/release/8.0-to-release/9.0
AndriySvyryd Apr 8, 2026
825bc1a
Merge branch 'release/10.0' into merge/release/9.0-to-release/10.0
AndriySvyryd Apr 9, 2026
3bf43a4
Merge pull request #38064 from dotnet/merge/release/9.0-to-release/10.0
AndriySvyryd Apr 9, 2026
137123b
[release/10.0] Source code updates from dotnet/dotnet (#38073)
dotnet-maestro[bot] Apr 10, 2026
4c78aa7
Fix permissions for the API workflow (#38072)
AndriySvyryd Apr 10, 2026
c5255f3
Update dependencies from build 309867 (#38078)
dotnet-maestro[bot] Apr 10, 2026
c6beae7
Bump actions/github-script from 8 to 9 (#38080)
dependabot[bot] Apr 10, 2026
74e800c
Bump actions/checkout from 4 to 6 (#38081)
dependabot[bot] Apr 10, 2026
f7d7301
Fix API diff workflow manual trigger input (#38082)
AndriySvyryd Apr 10, 2026
0de56fb
Filter out full-text catalog operations from history table creation (…
roji Apr 10, 2026
3e204de
Format the API change comment as a C# diff for readability (#38084)
AndriySvyryd Apr 10, 2026
56d8920
Merge branch 'release/10.0' into main
AndriySvyryd Apr 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .agents/skills/run-apichief/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ ApiChief can run against either a compiled assembly or a previously emitted base
| `emit baseline` | Emit a JSON API baseline | `-o <file>` |
| `emit summary` | Emit a human-readable API summary | `-o <file>`, `-x` |
| `emit review` | Emit API review files | `-o <dir>`, `-n` |
| `emit delta` | Emit a delta against an existing baseline | `<baseline-path>`, `-o <file>` |
| `emit delta` | Emit a delta against an existing baseline, or markdown diff review files | `<baseline-path>`, `-o <file-or-dir>`, `--diff` |
| `check breaking` | Fail if breaking changes exist vs. a baseline | `<baseline-path>` |

Default to `emit baseline` if the user only asks to "run ApiChief".
Expand Down Expand Up @@ -105,6 +105,9 @@ Before running, report the selected TFM if it matters for the task.

# Emit API review artifacts
& $dotnet $apiChief $assemblyPath emit review -o ".\\artifacts\\tmp\\API.$name"

# Emit GitHub-friendly markdown diff files against a baseline
& $dotnet $apiChief $assemblyPath emit delta ".\\src\\$name\\$name.baseline.json" --diff -o ".\\artifacts\\tmp\\API.$name.Diff"
```

`emit delta` also supports passing a `.json` file as the current input instead of a DLL.
Expand All @@ -124,4 +127,5 @@ After `emit baseline`:
- show the chosen TFM(s)
- report the output path(s)
- for `check breaking`, state pass/fail
- for `emit delta`, mention that exit code `0` means changes, `2` means no changes, and `-1` means an error
- for `emit delta` and `emit delta --diff`, mention that exit code `0` means changes, `2` means no changes, and `-1` means an error
- prefer `emit delta --diff` output because it generates ready-to-post ```diff fenced markdown split across per-type `.md` files
178 changes: 110 additions & 68 deletions .github/workflows/api-review-baselines.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,54 @@
# This workflow labels merged PRs that modify `*.baseline.json` files with `api-review`,
# computes ApiChief deltas between the original and merged baseline files, and posts the
# results back to the pull request as a review comment.
# This workflow labels PRs that modify `*.baseline.json` files with `api-review`,
# computes ApiChief deltas between the base and selected PR baseline files, and posts the
# results back to the pull request as a comment.

name: Comment API baseline deltas on merged PRs
name: Comment API baseline deltas on PRs

on:
pull_request_target:
types: [closed]
branches:
- main
- release/**
workflow_dispatch:
inputs:
pr-number:
description: Pull request number to process
required: true
type: number

permissions:
contents: read
issues: write
pull-requests: read
pull-requests: write

jobs:
api-review:
if: github.event.pull_request.merged == true
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Detect changed baseline files and add label
id: detect
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request.number;
const prNumberInput = context.eventName === 'workflow_dispatch'
? context.payload.inputs?.['pr-number']
: context.payload.pull_request.number;
const prNumber = Number(prNumberInput);

if (!Number.isInteger(prNumber) || prNumber <= 0) {
core.setFailed(`Invalid PR number: ${prNumber}`);
return;
}

const { data: pullRequest } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});

const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
Expand All @@ -45,11 +65,18 @@ jobs:
previous_filename: file.previous_filename ?? null
}));

core.setOutput('pr_number', String(prNumber));
core.setOutput('base_sha', pullRequest.base.sha);
core.setOutput('target_sha', pullRequest.merge_commit_sha ?? pullRequest.head.sha);
core.setOutput('has_baselines', String(baselineFiles.length > 0));
core.setOutput('files_json', JSON.stringify(baselineFiles));

if (!pullRequest.merged) {
console.log(`PR #${prNumber} is not merged; using head SHA ${pullRequest.head.sha}.`);
}

if (baselineFiles.length === 0) {
console.log('No changed baseline files detected on this merged PR.');
console.log(`No changed baseline files detected on PR #${prNumber}.`);
return;
}

Expand All @@ -60,30 +87,30 @@ jobs:
labels: ['api-review']
});

console.log(`Detected ${baselineFiles.length} changed baseline file(s).`);
console.log(`Detected ${baselineFiles.length} changed baseline file(s) on PR #${prNumber}.`);

- name: Check out merged commit
- name: Check out selected commit
if: steps.detect.outputs.has_baselines == 'true'
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
ref: ${{ steps.detect.outputs.target_sha }}
fetch-depth: 1

- name: Restore repo-local .NET SDK
if: steps.detect.outputs.has_baselines == 'true'
shell: bash
run: ./restore.sh

- name: Build ApiChief and compute baseline deltas
- name: Build ApiChief and compute review diffs
if: steps.detect.outputs.has_baselines == 'true'
id: delta
shell: bash
env:
FILES_JSON: ${{ steps.detect.outputs.files_json }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
BASE_SHA: ${{ steps.detect.outputs.base_sha }}
TARGET_SHA: ${{ steps.detect.outputs.target_sha }}
run: |
set -euo pipefail

Expand All @@ -110,7 +137,7 @@ jobs:
owner = os.environ['OWNER']
repo = os.environ['REPO']
base_sha = os.environ['BASE_SHA']
merge_sha = os.environ['MERGE_SHA']
target_sha = os.environ['TARGET_SHA']
dotnet = os.environ['DOTNET']
apichief = os.environ['APICHIEF_PATH']
workdir = pathlib.Path(os.environ['WORKDIR'])
Expand All @@ -130,25 +157,36 @@ jobs:
return False
raise

def truncate(text: str, limit: int = 20000) -> str:
if len(text) <= limit:
return text
return text[:limit] + '\n... (truncated)\n'
max_comment_length = 60000
comment_bodies: list[str] = []
current_body = ''

def append_section(section: str) -> None:
nonlocal current_body

sections: list[str] = []
if not current_body:
current_body = section
return

candidate = current_body + '\n\n' + section
if len(candidate) > max_comment_length:
comment_bodies.append(current_body)
current_body = section
else:
current_body = candidate

for index, file in enumerate(files):
filename = file['filename']
old_path = workdir / f'{index}.old.baseline.json'
new_path = workdir / f'{index}.new.baseline.json'
delta_path = workdir / f'{index}.delta.json'
review_dir = workdir / f'{index}.review'

old_exists = download_file(base_sha, filename, old_path)
new_exists = download_file(merge_sha, filename, new_path)
new_exists = download_file(target_sha, filename, new_path)

if old_exists and new_exists:
result = subprocess.run(
[dotnet, apichief, str(new_path), 'emit', 'delta', str(old_path), '-o', str(delta_path)],
[dotnet, apichief, str(new_path), 'emit', 'delta', str(old_path), '--diff', '-o', str(review_dir)],
capture_output=True,
text=True,
check=False)
Expand All @@ -158,73 +196,77 @@ jobs:

if result.returncode != 0:
raise RuntimeError(
f'ApiChief delta failed for {filename} with exit code {result.returncode}:\n'
f'ApiChief delta diff failed for {filename} with exit code {result.returncode}:\n'
f'{result.stdout}\n{result.stderr}')

delta_text = truncate(delta_path.read_text(encoding='utf-8'))
sections.append(
f"### `{filename}`\n\n"
f"<details>\n<summary>Show delta</summary>\n\n```json\n{delta_text}\n```\n</details>")
review_files = sorted(review_dir.rglob('*.md'))
if not review_files:
continue

section = (
f"## API review baseline changes for `{filename}`\n\n"
'The diff below was generated by `ApiChief` between the base and selected PR versions.\n\n')

for review_file in review_files:
section += (
f"{review_file.read_text(encoding='utf-8').rstrip()}\n\n")

append_section(section.rstrip())
elif new_exists:
sections.append(f"### `{filename}`\n\nThis baseline file was **added** in the merged PR.")
append_section(
f"## API review baseline changes for `{filename}`\n\n"
'This baseline file was **added** in the selected PR.')
elif old_exists:
sections.append(f"### `{filename}`\n\nThis baseline file was **removed** in the merged PR.")
append_section(
f"## API review baseline changes for `{filename}`\n\n"
'This baseline file was **removed** in the selected PR.')

if not sections:
sections.append('No API deltas were produced for the modified baseline files.')
if not current_body:
append_section(
'## API review baseline changes\n\n'
'No API deltas were produced for the modified baseline files.')

body = (
'<!-- api-review-delta-comment -->\n'
'## API review baseline changes\n\n'
'This merged PR modified one or more `*.baseline.json` files. '
'The deltas below were generated by `ApiChief` between the pre-merge and merged versions.\n\n'
+ '\n\n'.join(sections)
)
if current_body:
comment_bodies.append(current_body)

comment_path = workdir / 'comment.md'
comment_path.write_text(body[:65000], encoding='utf-8')
comments_dir = workdir / 'comments'
comments_dir.mkdir(parents=True, exist_ok=True)

for index, body in enumerate(comment_bodies, start=1):
comment_path = comments_dir / f'{index:03}.md'
comment_path.write_text(body.rstrip() + '\n', encoding='utf-8')

with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as output:
output.write(f'comment_path={comment_path}\n')
output.write(f'comments_dir={comments_dir}\n')
PY

- name: Upsert PR comment with delta
- name: Create PR comment with diffs
if: steps.detect.outputs.has_baselines == 'true'
uses: actions/github-script@v8
uses: actions/github-script@v9
env:
COMMENT_PATH: ${{ steps.delta.outputs.comment_path }}
COMMENTS_DIR: ${{ steps.delta.outputs.comments_dir }}
PR_NUMBER: ${{ steps.detect.outputs.pr_number }}
with:
script: |
const fs = require('fs');
const path = require('path');
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue_number = context.payload.pull_request.number;
const marker = '<!-- api-review-delta-comment -->';
const body = fs.readFileSync(process.env.COMMENT_PATH, 'utf8');
const issue_number = Number(process.env.PR_NUMBER);
const commentsDir = process.env.COMMENTS_DIR;

const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100
});
const commentFiles = fs.readdirSync(commentsDir)
.filter(file => file.endsWith('.md'))
.sort((a, b) => a.localeCompare(b));

const existing = comments.find(comment => comment.body?.includes(marker));

if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body
});
console.log(`Updated existing API review comment (${existing.id}).`);
} else {
for (const file of commentFiles) {
const body = fs.readFileSync(path.join(commentsDir, file), 'utf8');
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body
});
console.log('Created new API review comment.');
}

console.log(`Created ${commentFiles.length} API review comment(s) for PR #${issue_number}.`);
2 changes: 1 addition & 1 deletion .github/workflows/issues-closed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-slim
steps:
- name: Reclose as not planned if external user
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const issue = context.payload.issue;
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/label-and-milestone-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-slim
steps:
- name: Label issues and update milestones
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const owner = context.repo.owner;
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate-pr-target-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-slim
steps:
- name: Check PR target branch and author permissions
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const pr = context.payload.pull_request;
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ _pkginfo.txt
# but keep track of directories ending in .cache
!*.[Cc]ache/

# VS Code cache files
*.lscache

# Others
ClientBin/
~$*
Expand Down
Loading
Loading