From 24b1aa27307c04ae3c32d75a5b3f1cb54e470587 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Thu, 11 Jun 2026 15:12:14 +0200 Subject: [PATCH 01/10] Improve PR template and breaking-change label automation - Add a structured "Related Issue" section using GitHub closing keywords - Add a Review Guide prompt (major changes, impact, reviewer focus) with a note that the focus item is for human reviewers only - Add checklist items for issue linkage / no duplicate PRs and invert the breaking-change item (checked = not breaking) - Extend label-title-prefix to prepend [BREAKING] when the "breaking change" label is added - Add label-breaking-change workflow to apply the "breaking change" label when a PR title contains [BREAKING] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/pull_request_template.md | 30 ++++++++++++++--- .github/workflows/label-breaking-change.yml | 36 +++++++++++++++++++++ .github/workflows/label-title-prefix.yml | 35 ++++++++++++++++++-- 3 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/label-breaking-change.yml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6658ebc9fd3..067cfb7c074 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,23 +1,43 @@ -### Motivation and Context +### Motivation & Context -### Description +### Description & Review Guide +- **What are the major changes?** +- **What is the impact of these changes?** +- **What do you want reviewers to focus on?** + + + +### Related Issue + + + +Fixes # + ### Contribution Checklist - [ ] The code builds clean without any errors or warnings -- [ ] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md) - [ ] All unit tests pass, and I have added new tests where possible -- [ ] **Is this a breaking change?** If yes, add "[BREAKING]" prefix to the title of the PR. \ No newline at end of file +- [ ] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md) +- [ ] This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above). +- [ ] **This is not a breaking change.** If it _is_ a breaking change, add the `breaking change` label (or add a "[BREAKING]" prefix to the title) — a workflow keeps the label and title prefix in sync automatically. diff --git a/.github/workflows/label-breaking-change.yml b/.github/workflows/label-breaking-change.yml new file mode 100644 index 00000000000..b793df46b43 --- /dev/null +++ b/.github/workflows/label-breaking-change.yml @@ -0,0 +1,36 @@ +name: Label breaking change +on: + pull_request_target: + types: [opened, edited] + +jobs: + add_breaking_label: + name: "PR: add breaking change label" + continue-on-error: true + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + name: "PR: add breaking change label" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const breakingLabel = "breaking change"; + const title = context.payload.pull_request.title || ""; + + // Add the label when the title contains the "[BREAKING]" prefix. + // The label-title-prefix workflow handles the reverse direction + // (adding the "[BREAKING]" prefix when the label is applied). + if (/\[BREAKING\]/i.test(title)) { + const labels = context.payload.pull_request.labels.map(l => l.name); + if (!labels.includes(breakingLabel)) { + await github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: [breakingLabel] + }); + } + } diff --git a/.github/workflows/label-title-prefix.yml b/.github/workflows/label-title-prefix.yml index 8457e8e4289..03e4d4fbb37 100644 --- a/.github/workflows/label-title-prefix.yml +++ b/.github/workflows/label-title-prefix.yml @@ -25,6 +25,11 @@ jobs: ".NET": ".NET" }; + // Labels that prepend a bracketed prefix to the title (e.g. "[BREAKING] ..."). + let bracketPrefixLabels = { + "breaking change": "[BREAKING]" + }; + function addTitlePrefix(title, prefix) { // Update the title based on the label and prefix @@ -43,18 +48,42 @@ jobs: return title; } + function addBracketPrefix(title, prefix) + { + // Prepend a bracketed prefix (e.g. "[BREAKING]") if it is not already present. + if (title.includes(prefix)) { + return title; + } + + return prefix + " " + title; + } + labelAdded = context.payload.label.name - // Check if the issue or PR has the label + // Determine the new title based on which kind of label was added. + let newTitle = null; if (labelAdded in prefixLabels) { let prefix = prefixLabels[labelAdded]; + let currentTitle = context.eventName === 'issues' + ? context.payload.issue.title + : context.payload.pull_request.title; + newTitle = addTitlePrefix(currentTitle, prefix); + } else if (labelAdded in bracketPrefixLabels) { + let prefix = bracketPrefixLabels[labelAdded]; + let currentTitle = context.eventName === 'issues' + ? context.payload.issue.title + : context.payload.pull_request.title; + newTitle = addBracketPrefix(currentTitle, prefix); + } + + if (newTitle !== null) { switch(context.eventName) { case 'issues': github.rest.issues.update({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - title: addTitlePrefix(context.payload.issue.title, prefix) + title: newTitle }); break @@ -63,7 +92,7 @@ jobs: pull_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - title: addTitlePrefix(context.payload.pull_request.title, prefix) + title: newTitle }); break default: From a973accf75e71a859a5be1f36b82dc3e26800548 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Thu, 11 Jun 2026 15:21:10 +0200 Subject: [PATCH 02/10] Add pull-requests agent skill with dotnet/python links - Add root .github/skills/pull-requests/SKILL.md covering PR description authoring (following the PR template) and the review-comment workflow (review -> plan -> user review -> implement -> reply to all -> resolve) - Symlink the skill from python/.github/skills and dotnet/.github/skills - Reference the skill from python/AGENTS.md and dotnet/AGENTS.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/pull-requests/SKILL.md | 115 ++++++++++++++++++++++++++ dotnet/.github/skills/pull-requests | 1 + dotnet/AGENTS.md | 4 + python/.github/skills/pull-requests | 1 + python/AGENTS.md | 1 + 5 files changed, 122 insertions(+) create mode 100644 .github/skills/pull-requests/SKILL.md create mode 120000 dotnet/.github/skills/pull-requests create mode 120000 python/.github/skills/pull-requests diff --git a/.github/skills/pull-requests/SKILL.md b/.github/skills/pull-requests/SKILL.md new file mode 100644 index 00000000000..56af1bf7501 --- /dev/null +++ b/.github/skills/pull-requests/SKILL.md @@ -0,0 +1,115 @@ +--- +name: pull-requests +description: > + Guidance for creating pull requests and handling PR review comments in the + Agent Framework repository. Use this when writing a PR description (filling out + the PR template) or when responding to and resolving review comments on an + existing PR. +--- + +# Pull Request Workflow + +This skill covers two tasks: (1) writing a high-quality PR description, and +(2) handling review comments on an existing PR. + +## 1. Writing the PR description + +Always follow the repository PR template at +[`.github/pull_request_template.md`](../../pull_request_template.md). Keep its +exact structure and headings. Fill every section: + +### `### Motivation & Context` +Explain *why* the change is needed: the problem it solves and the scenario it +contributes to. Describe the net change relative to `main` — this is implied, so +do **not** spell out "vs main" explicitly. + +### `### Description & Review Guide` +Describe the changes, the overall approach, and the design. Answer the three +prompts: +- **What are the major changes?** +- **What is the impact of these changes?** +- **What do you want reviewers to focus on?** — This item is for **human + reviewers only**. Automated/AI reviewers must ignore it and review the entire + change rather than narrowing scope to it. + +### `### Related Issue` +Link the issue the PR fixes using a GitHub closing keyword (`Fixes #123` / +`Closes #123`) so it closes automatically on merge. A PR with no linked issue may +be closed regardless of how valid the change is. Before opening, confirm there is +no other open PR for the same issue; if there is, explain how this PR differs. + +### `### Contribution Checklist` +Check every item that applies. For the breaking-change item: +- Leave **"This is not a breaking change."** checked for the common case. +- If the change **is** breaking, add the `breaking change` label **or** put a + `[BREAKING]` prefix in the title — a workflow keeps the label and the title + prefix in sync automatically (see `.github/workflows/label-title-prefix.yml` + and `.github/workflows/label-breaking-change.yml`). + +### Do not +- Do **not** add ad-hoc sections such as "Validation" or "Tests run"; CI/CD and + the checklist already cover validation status. +- Do **not** remove or reorder the template's headings. + +### Creating the PR +Open new PRs as **drafts** until they are ready for review. Example: + +```bash +gh pr create --repo microsoft/agent-framework --base main \ + --head : --draft \ + --title "" --body "" +``` + +## 2. Handling review comments + +When a PR receives review comments, follow this sequence — **do not start editing +code before the user has reviewed the plan**: + +1. **Review the comments.** Read every review comment and thread on the PR, + including inline code comments and general review summaries. +2. **Make a plan.** Produce a concrete plan describing how each comment will be + addressed (or why it should not be, with reasoning). +3. **Let the user review the plan.** Present the plan and wait for the user's + approval or adjustments before implementing anything. +4. **Implement.** Make the agreed changes. +5. **Reply to every comment.** Add a reply to **all** comments explaining how it + was addressed (or the agreed outcome) — leave none unanswered. +6. **Resolve resolved threads.** Mark a review thread as resolved only when the + comment has actually been addressed. + +### Useful commands + +List review comments and threads: + +```bash +# Inline review comments +gh api repos/{owner}/{repo}/pulls/{pr}/comments + +# Review threads with resolution state (GraphQL) +gh api graphql -f query=' + query($owner:String!,$repo:String!,$pr:Int!){ + repository(owner:$owner,name:$repo){ + pullRequest(number:$pr){ + reviewThreads(first:100){ + nodes{ id isResolved comments(first:50){ nodes{ id body author{login} } } } + } + } + } + }' -F owner={owner} -F repo={repo} -F pr={pr} +``` + +Reply to an inline review comment: + +```bash +gh api repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}/replies \ + -f body="Addressed in : " +``` + +Resolve a review thread (needs the thread node id from the GraphQL query above): + +```bash +gh api graphql -f query=' + mutation($threadId:ID!){ + resolveReviewThread(input:{threadId:$threadId}){ thread{ isResolved } } + }' -F threadId={thread_id} +``` diff --git a/dotnet/.github/skills/pull-requests b/dotnet/.github/skills/pull-requests new file mode 120000 index 00000000000..d70f65f7f26 --- /dev/null +++ b/dotnet/.github/skills/pull-requests @@ -0,0 +1 @@ +../../../.github/skills/pull-requests \ No newline at end of file diff --git a/dotnet/AGENTS.md b/dotnet/AGENTS.md index 965dd9f035d..7cf79f31201 100644 --- a/dotnet/AGENTS.md +++ b/dotnet/AGENTS.md @@ -10,6 +10,10 @@ See `./.github/skills/build-and-test/SKILL.md` for detailed instructions on buil See `./.github/skills/project-structure/SKILL.md` for an overview of the project structure. +## Pull Requests + +See `./.github/skills/pull-requests/SKILL.md` for guidance on writing PR descriptions and handling/resolving PR review comments. + ### Core types - `AIAgent`: The abstract base class that all agents derive from, providing common methods for interacting with an agent. diff --git a/python/.github/skills/pull-requests b/python/.github/skills/pull-requests new file mode 120000 index 00000000000..d70f65f7f26 --- /dev/null +++ b/python/.github/skills/pull-requests @@ -0,0 +1 @@ +../../../.github/skills/pull-requests \ No newline at end of file diff --git a/python/AGENTS.md b/python/AGENTS.md index 8dc259c42bb..90d84aaf542 100644 --- a/python/AGENTS.md +++ b/python/AGENTS.md @@ -14,6 +14,7 @@ Instructions for AI coding agents working in the Python codebase. - `python-feature-lifecycle` — package vs feature lifecycle stages, decorators, enums, and promotion guidance - `python-package-management` — monorepo structure, lazy loading, versioning, new packages - `python-samples` — sample file structure, PEP 723, documentation guidelines +- `pull-requests` — writing PR descriptions (template) and handling/resolving PR review comments ## Maintaining Documentation From 02331fd4f89b55e9503ec4f4404cb22fe102d012 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Thu, 11 Jun 2026 15:25:40 +0200 Subject: [PATCH 03/10] Fold breaking-change labeling into label-pr workflow Move the title -> 'breaking change' label logic into the existing label-pr workflow (which already applies the python/.NET labels) and drop the separate label-breaking-change workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/label-breaking-change.yml | 36 --------------------- .github/workflows/label-pr.yml | 23 +++++++++++++ 2 files changed, 23 insertions(+), 36 deletions(-) delete mode 100644 .github/workflows/label-breaking-change.yml diff --git a/.github/workflows/label-breaking-change.yml b/.github/workflows/label-breaking-change.yml deleted file mode 100644 index b793df46b43..00000000000 --- a/.github/workflows/label-breaking-change.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Label breaking change -on: - pull_request_target: - types: [opened, edited] - -jobs: - add_breaking_label: - name: "PR: add breaking change label" - continue-on-error: true - runs-on: ubuntu-latest - permissions: - pull-requests: write - - steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - name: "PR: add breaking change label" - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const breakingLabel = "breaking change"; - const title = context.payload.pull_request.title || ""; - - // Add the label when the title contains the "[BREAKING]" prefix. - // The label-title-prefix workflow handles the reverse direction - // (adding the "[BREAKING]" prefix when the label is applied). - if (/\[BREAKING\]/i.test(title)) { - const labels = context.payload.pull_request.labels.map(l => l.name); - if (!labels.includes(breakingLabel)) { - await github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [breakingLabel] - }); - } - } diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 7d0282b9160..b7936f999f3 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -19,3 +19,26 @@ jobs: - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6 with: repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}" + + - name: "PR: add breaking change label from title" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} + script: | + const breakingLabel = "breaking change"; + const title = context.payload.pull_request.title || ""; + + // Add the label when the title contains the "[BREAKING]" prefix. + // The label-title-prefix workflow handles the reverse direction + // (adding the "[BREAKING]" prefix when the label is applied). + if (/\[BREAKING\]/i.test(title)) { + const labels = context.payload.pull_request.labels.map(l => l.name); + if (!labels.includes(breakingLabel)) { + await github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: [breakingLabel] + }); + } + } From 55584b3d74d87aaa58a9ff9b35a13cc820d4c201 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 12 Jun 2026 09:25:29 +0200 Subject: [PATCH 04/10] Address PR title prefix review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/pull_request_template.md | 10 +- .github/scripts/title_prefix.js | 252 +++++++++++++++++++++++ .github/skills/pull-requests/SKILL.md | 9 +- .github/tests/test_title_prefix.js | 214 +++++++++++++++++++ .github/workflows/label-pr.yml | 31 ++- .github/workflows/label-title-prefix.yml | 88 +------- 6 files changed, 498 insertions(+), 106 deletions(-) create mode 100644 .github/scripts/title_prefix.js create mode 100644 .github/tests/test_title_prefix.js diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 067cfb7c074..a790437b7e0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -25,10 +25,10 @@ Please help reviewers and future users, providing the following information: ### Related Issue + closed automatically when this PR is merged, e.g. "Fixes #123" or "Closes #123". + PRs that are not linked to an issue may be closed, no matter how valid the change is. + Also check whether an open PR already exists for this issue; if so, + explain how this PR is different. --> Fixes # @@ -40,4 +40,4 @@ Fixes # - [ ] All unit tests pass, and I have added new tests where possible - [ ] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md) - [ ] This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above). -- [ ] **This is not a breaking change.** If it _is_ a breaking change, add the `breaking change` label (or add a "[BREAKING]" prefix to the title) — a workflow keeps the label and title prefix in sync automatically. +- [x] **This is not a breaking change.** If it _is_ a breaking change, add the `breaking change` label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically. diff --git a/.github/scripts/title_prefix.js b/.github/scripts/title_prefix.js new file mode 100644 index 00000000000..f9a7442847e --- /dev/null +++ b/.github/scripts/title_prefix.js @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft. All rights reserved. + +const BREAKING_CHANGE_LABEL = 'breaking change'; +const BREAKING_PREFIX = '[BREAKING]'; + +const DEFAULT_PREFIX_LABELS = Object.freeze({ + python: 'Python', + '.NET': '.NET', +}); + +const DEFAULT_BRACKET_PREFIX_LABELS = Object.freeze({ + [BREAKING_CHANGE_LABEL]: BREAKING_PREFIX, +}); + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function getMatchingValueByKey(valuesByKey, keyToFind) { + const matchingKey = Object.keys(valuesByKey).find((key) => key.toLowerCase() === keyToFind.toLowerCase()); + return matchingKey === undefined ? null : valuesByKey[matchingKey]; +} + +function getPrefixPattern(prefixes) { + return prefixes.map(escapeRegExp).join('|'); +} + +function canonicalizePrefix(prefix, prefixes) { + return prefixes.find((knownPrefix) => knownPrefix.toLowerCase() === prefix.toLowerCase()) ?? prefix; +} + +function normalizeLeadingBracketPrefix(title, bracketPrefixes) { + const bracketPattern = getPrefixPattern(bracketPrefixes); + if (!bracketPattern) { + return title; + } + + const leadingBracketPrefix = new RegExp(`^(${bracketPattern})(?=\\s|$)`, 'i'); + return title.replace( + leadingBracketPrefix, + (bracketPrefix) => canonicalizePrefix(bracketPrefix, bracketPrefixes), + ); +} + +function parseLeadingTitlePrefix(title, titlePrefixes) { + const titlePrefixPattern = getPrefixPattern(titlePrefixes); + if (!titlePrefixPattern) { + return null; + } + + const match = title.match(new RegExp(`^(${titlePrefixPattern}):\\s*`, 'i')); + if (!match) { + return null; + } + + return { + prefix: canonicalizePrefix(match[1], titlePrefixes), + rest: title.slice(match[0].length).trimStart(), + }; +} + +function removeBracketPrefixToken(title, bracketPrefix) { + const bracketPrefixPattern = escapeRegExp(bracketPrefix); + return title + .replace(new RegExp(`(^|\\s+)${bracketPrefixPattern}(?=\\s|$)`, 'ig'), '$1') + .replace(/\s{2,}/g, ' ') + .trim(); +} + +function addTitlePrefix(title, prefix, bracketPrefixes = Object.values(DEFAULT_BRACKET_PREFIX_LABELS)) { + const bracketPattern = getPrefixPattern(bracketPrefixes); + const prefixPattern = escapeRegExp(prefix); + + if (bracketPattern) { + const bracketThenTitlePrefix = new RegExp(`^(${bracketPattern})(\\s+)(${prefixPattern})(?=:)`, 'i'); + if (bracketThenTitlePrefix.test(title)) { + return title.replace( + bracketThenTitlePrefix, + (match, bracketPrefix, spacing) => `${canonicalizePrefix(bracketPrefix, bracketPrefixes)}${spacing}${prefix}`, + ); + } + + title = normalizeLeadingBracketPrefix(title, bracketPrefixes); + } + + if (!title.startsWith(`${prefix}: `)) { + if (title.match(new RegExp(`^${prefixPattern}`, 'i'))) { + return title.replace(new RegExp(`^${prefixPattern}`, 'i'), prefix); + } + + return `${prefix}: ${title}`; + } + + return title; +} + +function hasBracketPrefix(title, bracketPrefix, titlePrefixes = Object.values(DEFAULT_PREFIX_LABELS)) { + const bracketPrefixPattern = escapeRegExp(bracketPrefix); + const leadingBracketPrefix = new RegExp(`^${bracketPrefixPattern}(?=\\s|$)`, 'i'); + if (leadingBracketPrefix.test(title)) { + return true; + } + + const leadingTitlePrefix = parseLeadingTitlePrefix(title, titlePrefixes); + if (!leadingTitlePrefix) { + return false; + } + + return leadingBracketPrefix.test(leadingTitlePrefix.rest); +} + +function addBracketPrefix(title, bracketPrefix, titlePrefixes = Object.values(DEFAULT_PREFIX_LABELS)) { + const bracketPrefixPattern = escapeRegExp(bracketPrefix); + const leadingBracketPrefix = new RegExp(`^${bracketPrefixPattern}(?=\\s|$)`, 'i'); + if (leadingBracketPrefix.test(title)) { + return title.replace(leadingBracketPrefix, bracketPrefix); + } + + const leadingTitlePrefix = parseLeadingTitlePrefix(title, titlePrefixes); + if (leadingTitlePrefix) { + if (leadingBracketPrefix.test(leadingTitlePrefix.rest)) { + const normalizedRest = leadingTitlePrefix.rest.replace(leadingBracketPrefix, bracketPrefix); + return `${leadingTitlePrefix.prefix}: ${normalizedRest}`; + } + + const titleWithoutBracketPrefix = removeBracketPrefixToken(leadingTitlePrefix.rest, bracketPrefix); + return `${leadingTitlePrefix.prefix}: ${bracketPrefix}` + + (titleWithoutBracketPrefix ? ` ${titleWithoutBracketPrefix}` : ''); + } + + const titleWithoutBracketPrefix = removeBracketPrefixToken(title, bracketPrefix); + return `${bracketPrefix}${titleWithoutBracketPrefix ? ` ${titleWithoutBracketPrefix}` : ''}`; +} + +function hasLabel(labels, labelName) { + return labels.some((label) => label.toLowerCase() === labelName.toLowerCase()); +} + +function getCurrentTitle(context) { + switch (context.eventName) { + case 'issues': + return context.payload.issue.title; + case 'pull_request_target': + return context.payload.pull_request.title; + default: + throw new Error(`Unrecognized eventName: ${context.eventName}`); + } +} + +async function updateTitleForAddedLabel({ + github, + context, + core, + prefixLabels = DEFAULT_PREFIX_LABELS, + bracketPrefixLabels = DEFAULT_BRACKET_PREFIX_LABELS, +}) { + const labelAdded = context.payload.label?.name; + if (!labelAdded) { + throw new Error('This script must be run from a labeled event.'); + } + + const currentTitle = getCurrentTitle(context); + let newTitle = null; + + const titlePrefix = getMatchingValueByKey(prefixLabels, labelAdded); + if (titlePrefix !== null) { + newTitle = addTitlePrefix(currentTitle, titlePrefix, Object.values(bracketPrefixLabels)); + } + + const bracketPrefix = getMatchingValueByKey(bracketPrefixLabels, labelAdded); + if (bracketPrefix !== null) { + newTitle = addBracketPrefix(currentTitle, bracketPrefix, Object.values(prefixLabels)); + } + + if (newTitle === null) { + core.info(`No title prefix configured for label "${labelAdded}".`); + return { updated: false, newTitle: currentTitle }; + } + + if (newTitle === currentTitle) { + core.info(`Title already includes the prefix for label "${labelAdded}".`); + return { updated: false, newTitle }; + } + + switch (context.eventName) { + case 'issues': + await github.rest.issues.update({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + title: newTitle, + }); + break; + + case 'pull_request_target': + await github.rest.pulls.update({ + pull_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + title: newTitle, + }); + break; + + default: + throw new Error(`Unrecognized eventName: ${context.eventName}`); + } + + return { updated: true, newTitle }; +} + +async function syncBreakingChangeLabelFromTitle({ + github, + context, + core, + labelName = BREAKING_CHANGE_LABEL, + bracketPrefix = BREAKING_PREFIX, + titlePrefixes = Object.values(DEFAULT_PREFIX_LABELS), +}) { + const pullRequest = context.payload.pull_request; + if (!pullRequest) { + throw new Error('This script must be run from a pull_request_target event.'); + } + + const title = pullRequest.title || ''; + if (!hasBracketPrefix(title, bracketPrefix, titlePrefixes)) { + core.info(`Title does not include ${bracketPrefix} in the title prefix.`); + return { added: false }; + } + + const labels = pullRequest.labels?.map((label) => label.name).filter(Boolean) ?? []; + if (hasLabel(labels, labelName)) { + core.info(`PR already has the "${labelName}" label.`); + return { added: false }; + } + + await github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: [labelName], + }); + + return { added: true }; +} + +module.exports = { + addBracketPrefix, + addTitlePrefix, + hasBracketPrefix, + syncBreakingChangeLabelFromTitle, + updateTitleForAddedLabel, +}; diff --git a/.github/skills/pull-requests/SKILL.md b/.github/skills/pull-requests/SKILL.md index 56af1bf7501..c8ec894e421 100644 --- a/.github/skills/pull-requests/SKILL.md +++ b/.github/skills/pull-requests/SKILL.md @@ -41,10 +41,11 @@ no other open PR for the same issue; if there is, explain how this PR differs. ### `### Contribution Checklist` Check every item that applies. For the breaking-change item: - Leave **"This is not a breaking change."** checked for the common case. -- If the change **is** breaking, add the `breaking change` label **or** put a - `[BREAKING]` prefix in the title — a workflow keeps the label and the title - prefix in sync automatically (see `.github/workflows/label-title-prefix.yml` - and `.github/workflows/label-breaking-change.yml`). +- If the change **is** breaking, add the `breaking change` label **or** put + `[BREAKING]` in the title prefix, before or after a language prefix such as + `Python:` or `.NET:` — workflows keep the label and the title prefix in sync + automatically (see `.github/workflows/label-title-prefix.yml` and + `.github/workflows/label-pr.yml`). ### Do not - Do **not** add ad-hoc sections such as "Validation" or "Tests run"; CI/CD and diff --git a/.github/tests/test_title_prefix.js b/.github/tests/test_title_prefix.js new file mode 100644 index 00000000000..5c5e4d4e981 --- /dev/null +++ b/.github/tests/test_title_prefix.js @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft. All rights reserved. + +/** + * Tests for title_prefix.js. + * + * Run with: node --test .github/tests/test_title_prefix.js + */ + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + addBracketPrefix, + addTitlePrefix, + hasBracketPrefix, + syncBreakingChangeLabelFromTitle, + updateTitleForAddedLabel, +} = require('../scripts/title_prefix.js'); + +function createCore() { + const messages = []; + return { + messages, + info(message) { + messages.push(message); + }, + }; +} + +function createGithub() { + const calls = []; + return { + calls, + rest: { + issues: { + async addLabels(params) { + calls.push({ api: 'issues.addLabels', params }); + }, + async update(params) { + calls.push({ api: 'issues.update', params }); + }, + }, + pulls: { + async update(params) { + calls.push({ api: 'pulls.update', params }); + }, + }, + }, + }; +} + +function createPullRequestContext({ title, labels = [], action = 'labeled', label = 'breaking change' }) { + return { + eventName: 'pull_request_target', + issue: { number: 123 }, + repo: { owner: 'microsoft', repo: 'agent-framework' }, + payload: { + action, + label: { name: label }, + pull_request: { + title, + labels: labels.map((name) => ({ name })), + }, + }, + }; +} + +function createIssueContext({ title, label }) { + return { + eventName: 'issues', + issue: { number: 123 }, + repo: { owner: 'microsoft', repo: 'agent-framework' }, + payload: { + label: { name: label }, + issue: { title }, + }, + }; +} + +describe('addBracketPrefix', () => { + it('prepends the breaking prefix when no title prefix exists', () => { + assert.equal(addBracketPrefix('Improve docs', '[BREAKING]'), '[BREAKING] Improve docs'); + }); + + it('normalizes a case-insensitive leading breaking prefix', () => { + assert.equal(addBracketPrefix('[breaking] Improve docs', '[BREAKING]'), '[BREAKING] Improve docs'); + }); + + it('treats a breaking prefix after a language prefix as already present', () => { + assert.equal(addBracketPrefix('Python: [breaking] Improve docs', '[BREAKING]'), 'Python: [BREAKING] Improve docs'); + assert.equal(addBracketPrefix('.NET: [Breaking] Improve docs', '[BREAKING]'), '.NET: [BREAKING] Improve docs'); + }); + + it('preserves a breaking prefix before a language prefix', () => { + assert.equal(addBracketPrefix('[Breaking] Python: Improve docs', '[BREAKING]'), '[BREAKING] Python: Improve docs'); + assert.equal(addBracketPrefix('[breaking] .NET: Improve docs', '[BREAKING]'), '[BREAKING] .NET: Improve docs'); + }); + + it('moves a later breaking token into the title prefix', () => { + assert.equal(addBracketPrefix('Python: Improve docs [BREAKING]', '[BREAKING]'), 'Python: [BREAKING] Improve docs'); + assert.equal( + addBracketPrefix('Docs: explain the [BREAKING] convention', '[BREAKING]'), + '[BREAKING] Docs: explain the convention', + ); + }); +}); + +describe('addTitlePrefix', () => { + it('prepends a language prefix', () => { + assert.equal(addTitlePrefix('Improve docs', 'Python'), 'Python: Improve docs'); + }); + + it('normalizes a language prefix after a breaking prefix without duplicating it', () => { + assert.equal(addTitlePrefix('[BREAKING] python: Improve docs', 'Python'), '[BREAKING] Python: Improve docs'); + }); +}); + +describe('hasBracketPrefix', () => { + it('accepts breaking before or after a language prefix', () => { + assert.equal(hasBracketPrefix('[breaking] Python: Improve docs', '[BREAKING]'), true); + assert.equal(hasBracketPrefix('Python: [breaking] Improve docs', '[BREAKING]'), true); + assert.equal(hasBracketPrefix('.NET: [BREAKING] Improve docs', '[BREAKING]'), true); + }); + + it('rejects breaking mentions outside the title prefix', () => { + assert.equal(hasBracketPrefix('Docs: explain the [BREAKING] convention', '[BREAKING]'), false); + assert.equal(hasBracketPrefix('Python: Improve docs [BREAKING]', '[BREAKING]'), false); + }); +}); + +describe('updateTitleForAddedLabel', () => { + it('updates a PR title when the breaking label is added', async () => { + const github = createGithub(); + const result = await updateTitleForAddedLabel({ + github, + context: createPullRequestContext({ title: 'Python: Improve docs' }), + core: createCore(), + }); + + assert.equal(result.updated, true); + assert.equal(result.newTitle, 'Python: [BREAKING] Improve docs'); + assert.deepEqual(github.calls, [ + { + api: 'pulls.update', + params: { + pull_number: 123, + owner: 'microsoft', + repo: 'agent-framework', + title: 'Python: [BREAKING] Improve docs', + }, + }, + ]); + }); + + it('skips no-op title updates', async () => { + const github = createGithub(); + const result = await updateTitleForAddedLabel({ + github, + context: createIssueContext({ title: 'Python: [BREAKING] Improve docs', label: 'breaking change' }), + core: createCore(), + }); + + assert.equal(result.updated, false); + assert.deepEqual(github.calls, []); + }); +}); + +describe('syncBreakingChangeLabelFromTitle', () => { + it('adds the breaking change label when breaking appears after a language prefix', async () => { + const github = createGithub(); + const result = await syncBreakingChangeLabelFromTitle({ + github, + context: createPullRequestContext({ title: 'Python: [breaking] Improve docs', labels: ['python'] }), + core: createCore(), + }); + + assert.equal(result.added, true); + assert.deepEqual(github.calls, [ + { + api: 'issues.addLabels', + params: { + issue_number: 123, + owner: 'microsoft', + repo: 'agent-framework', + labels: ['breaking change'], + }, + }, + ]); + }); + + it('does not add the label when breaking is only mentioned later in the title', async () => { + const github = createGithub(); + const result = await syncBreakingChangeLabelFromTitle({ + github, + context: createPullRequestContext({ title: 'Docs: explain the [BREAKING] convention' }), + core: createCore(), + }); + + assert.equal(result.added, false); + assert.deepEqual(github.calls, []); + }); + + it('skips PRs that already have the breaking change label', async () => { + const github = createGithub(); + const result = await syncBreakingChangeLabelFromTitle({ + github, + context: createPullRequestContext({ title: '[BREAKING] Improve docs', labels: ['Breaking Change'] }), + core: createCore(), + }); + + assert.equal(result.added, false); + assert.deepEqual(github.calls, []); + }); +}); diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index b7936f999f3..329a165cd2d 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -6,13 +6,16 @@ # https://github.com/actions/labeler name: Label pull request -on: [pull_request_target] +on: + pull_request_target: + types: [opened, synchronize, reopened, edited] jobs: add_label: runs-on: ubuntu-latest permissions: contents: read + issues: write pull-requests: write steps: @@ -20,25 +23,17 @@ jobs: with: repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}" + - name: Checkout scripts + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + sparse-checkout: .github/scripts + fetch-depth: 1 + persist-credentials: false + - name: "PR: add breaking change label from title" uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} script: | - const breakingLabel = "breaking change"; - const title = context.payload.pull_request.title || ""; - - // Add the label when the title contains the "[BREAKING]" prefix. - // The label-title-prefix workflow handles the reverse direction - // (adding the "[BREAKING]" prefix when the label is applied). - if (/\[BREAKING\]/i.test(title)) { - const labels = context.payload.pull_request.labels.map(l => l.name); - if (!labels.includes(breakingLabel)) { - await github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: [breakingLabel] - }); - } - } + const { syncBreakingChangeLabelFromTitle } = require('./.github/scripts/title_prefix.js'); + await syncBreakingChangeLabelFromTitle({ github, context, core }); diff --git a/.github/workflows/label-title-prefix.yml b/.github/workflows/label-title-prefix.yml index 03e4d4fbb37..4533ed0988e 100644 --- a/.github/workflows/label-title-prefix.yml +++ b/.github/workflows/label-title-prefix.yml @@ -15,87 +15,17 @@ jobs: pull-requests: write steps: + - name: Checkout scripts + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + sparse-checkout: .github/scripts + fetch-depth: 1 + persist-credentials: false + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 name: "Issue/PR: update title" with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - let prefixLabels = { - "python": "Python", - ".NET": ".NET" - }; - - // Labels that prepend a bracketed prefix to the title (e.g. "[BREAKING] ..."). - let bracketPrefixLabels = { - "breaking change": "[BREAKING]" - }; - - function addTitlePrefix(title, prefix) - { - // Update the title based on the label and prefix - // Check if the title starts with the prefix (case-sensitive) - if (!title.startsWith(prefix + ": ")) { - // If not, check if the first word is the label (case-insensitive) - if (title.match(new RegExp(`^${prefix}`, 'i'))) { - // If yes, replace it with the prefix (case-sensitive) - title = title.replace(new RegExp(`^${prefix}`, 'i'), prefix); - } else { - // If not, prepend the prefix to the title - title = prefix + ": " + title; - } - } - - return title; - } - - function addBracketPrefix(title, prefix) - { - // Prepend a bracketed prefix (e.g. "[BREAKING]") if it is not already present. - if (title.includes(prefix)) { - return title; - } - - return prefix + " " + title; - } - - labelAdded = context.payload.label.name - - // Determine the new title based on which kind of label was added. - let newTitle = null; - if (labelAdded in prefixLabels) { - let prefix = prefixLabels[labelAdded]; - let currentTitle = context.eventName === 'issues' - ? context.payload.issue.title - : context.payload.pull_request.title; - newTitle = addTitlePrefix(currentTitle, prefix); - } else if (labelAdded in bracketPrefixLabels) { - let prefix = bracketPrefixLabels[labelAdded]; - let currentTitle = context.eventName === 'issues' - ? context.payload.issue.title - : context.payload.pull_request.title; - newTitle = addBracketPrefix(currentTitle, prefix); - } - - if (newTitle !== null) { - switch(context.eventName) { - case 'issues': - github.rest.issues.update({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - title: newTitle - }); - break - - case 'pull_request_target': - github.rest.pulls.update({ - pull_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - title: newTitle - }); - break - default: - core.setFailed('Unrecognited eventName: ' + context.eventName); - } - } + const { updateTitleForAddedLabel } = require('./.github/scripts/title_prefix.js'); + await updateTitleForAddedLabel({ github, context, core }); From 25bd3522ca5a8c34301ca6e1b7bea78365af60c6 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 12 Jun 2026 09:39:33 +0200 Subject: [PATCH 05/10] Pin patched MessagePack for .NET restore Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 3f546a3cdf6..d3b7c43a4ff 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -37,6 +37,8 @@ + + From 66cd4618df79d232c1ffd15b8e6ccd181123b7c6 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 12 Jun 2026 14:34:31 +0200 Subject: [PATCH 06/10] Revert MessagePack central pin Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 2 -- 1 file changed, 2 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index d3b7c43a4ff..3f546a3cdf6 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -37,8 +37,6 @@ - - From dc409e11519ebbe54b7fd930590a5b9982b63cda Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 12 Jun 2026 14:36:20 +0200 Subject: [PATCH 07/10] Move title prefix tests out of tracked GitHub tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/tests/test_title_prefix.js | 214 ----------------------------- 1 file changed, 214 deletions(-) delete mode 100644 .github/tests/test_title_prefix.js diff --git a/.github/tests/test_title_prefix.js b/.github/tests/test_title_prefix.js deleted file mode 100644 index 5c5e4d4e981..00000000000 --- a/.github/tests/test_title_prefix.js +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -/** - * Tests for title_prefix.js. - * - * Run with: node --test .github/tests/test_title_prefix.js - */ - -const { describe, it } = require('node:test'); -const assert = require('node:assert/strict'); - -const { - addBracketPrefix, - addTitlePrefix, - hasBracketPrefix, - syncBreakingChangeLabelFromTitle, - updateTitleForAddedLabel, -} = require('../scripts/title_prefix.js'); - -function createCore() { - const messages = []; - return { - messages, - info(message) { - messages.push(message); - }, - }; -} - -function createGithub() { - const calls = []; - return { - calls, - rest: { - issues: { - async addLabels(params) { - calls.push({ api: 'issues.addLabels', params }); - }, - async update(params) { - calls.push({ api: 'issues.update', params }); - }, - }, - pulls: { - async update(params) { - calls.push({ api: 'pulls.update', params }); - }, - }, - }, - }; -} - -function createPullRequestContext({ title, labels = [], action = 'labeled', label = 'breaking change' }) { - return { - eventName: 'pull_request_target', - issue: { number: 123 }, - repo: { owner: 'microsoft', repo: 'agent-framework' }, - payload: { - action, - label: { name: label }, - pull_request: { - title, - labels: labels.map((name) => ({ name })), - }, - }, - }; -} - -function createIssueContext({ title, label }) { - return { - eventName: 'issues', - issue: { number: 123 }, - repo: { owner: 'microsoft', repo: 'agent-framework' }, - payload: { - label: { name: label }, - issue: { title }, - }, - }; -} - -describe('addBracketPrefix', () => { - it('prepends the breaking prefix when no title prefix exists', () => { - assert.equal(addBracketPrefix('Improve docs', '[BREAKING]'), '[BREAKING] Improve docs'); - }); - - it('normalizes a case-insensitive leading breaking prefix', () => { - assert.equal(addBracketPrefix('[breaking] Improve docs', '[BREAKING]'), '[BREAKING] Improve docs'); - }); - - it('treats a breaking prefix after a language prefix as already present', () => { - assert.equal(addBracketPrefix('Python: [breaking] Improve docs', '[BREAKING]'), 'Python: [BREAKING] Improve docs'); - assert.equal(addBracketPrefix('.NET: [Breaking] Improve docs', '[BREAKING]'), '.NET: [BREAKING] Improve docs'); - }); - - it('preserves a breaking prefix before a language prefix', () => { - assert.equal(addBracketPrefix('[Breaking] Python: Improve docs', '[BREAKING]'), '[BREAKING] Python: Improve docs'); - assert.equal(addBracketPrefix('[breaking] .NET: Improve docs', '[BREAKING]'), '[BREAKING] .NET: Improve docs'); - }); - - it('moves a later breaking token into the title prefix', () => { - assert.equal(addBracketPrefix('Python: Improve docs [BREAKING]', '[BREAKING]'), 'Python: [BREAKING] Improve docs'); - assert.equal( - addBracketPrefix('Docs: explain the [BREAKING] convention', '[BREAKING]'), - '[BREAKING] Docs: explain the convention', - ); - }); -}); - -describe('addTitlePrefix', () => { - it('prepends a language prefix', () => { - assert.equal(addTitlePrefix('Improve docs', 'Python'), 'Python: Improve docs'); - }); - - it('normalizes a language prefix after a breaking prefix without duplicating it', () => { - assert.equal(addTitlePrefix('[BREAKING] python: Improve docs', 'Python'), '[BREAKING] Python: Improve docs'); - }); -}); - -describe('hasBracketPrefix', () => { - it('accepts breaking before or after a language prefix', () => { - assert.equal(hasBracketPrefix('[breaking] Python: Improve docs', '[BREAKING]'), true); - assert.equal(hasBracketPrefix('Python: [breaking] Improve docs', '[BREAKING]'), true); - assert.equal(hasBracketPrefix('.NET: [BREAKING] Improve docs', '[BREAKING]'), true); - }); - - it('rejects breaking mentions outside the title prefix', () => { - assert.equal(hasBracketPrefix('Docs: explain the [BREAKING] convention', '[BREAKING]'), false); - assert.equal(hasBracketPrefix('Python: Improve docs [BREAKING]', '[BREAKING]'), false); - }); -}); - -describe('updateTitleForAddedLabel', () => { - it('updates a PR title when the breaking label is added', async () => { - const github = createGithub(); - const result = await updateTitleForAddedLabel({ - github, - context: createPullRequestContext({ title: 'Python: Improve docs' }), - core: createCore(), - }); - - assert.equal(result.updated, true); - assert.equal(result.newTitle, 'Python: [BREAKING] Improve docs'); - assert.deepEqual(github.calls, [ - { - api: 'pulls.update', - params: { - pull_number: 123, - owner: 'microsoft', - repo: 'agent-framework', - title: 'Python: [BREAKING] Improve docs', - }, - }, - ]); - }); - - it('skips no-op title updates', async () => { - const github = createGithub(); - const result = await updateTitleForAddedLabel({ - github, - context: createIssueContext({ title: 'Python: [BREAKING] Improve docs', label: 'breaking change' }), - core: createCore(), - }); - - assert.equal(result.updated, false); - assert.deepEqual(github.calls, []); - }); -}); - -describe('syncBreakingChangeLabelFromTitle', () => { - it('adds the breaking change label when breaking appears after a language prefix', async () => { - const github = createGithub(); - const result = await syncBreakingChangeLabelFromTitle({ - github, - context: createPullRequestContext({ title: 'Python: [breaking] Improve docs', labels: ['python'] }), - core: createCore(), - }); - - assert.equal(result.added, true); - assert.deepEqual(github.calls, [ - { - api: 'issues.addLabels', - params: { - issue_number: 123, - owner: 'microsoft', - repo: 'agent-framework', - labels: ['breaking change'], - }, - }, - ]); - }); - - it('does not add the label when breaking is only mentioned later in the title', async () => { - const github = createGithub(); - const result = await syncBreakingChangeLabelFromTitle({ - github, - context: createPullRequestContext({ title: 'Docs: explain the [BREAKING] convention' }), - core: createCore(), - }); - - assert.equal(result.added, false); - assert.deepEqual(github.calls, []); - }); - - it('skips PRs that already have the breaking change label', async () => { - const github = createGithub(); - const result = await syncBreakingChangeLabelFromTitle({ - github, - context: createPullRequestContext({ title: '[BREAKING] Improve docs', labels: ['Breaking Change'] }), - core: createCore(), - }); - - assert.equal(result.added, false); - assert.deepEqual(github.calls, []); - }); -}); From a2d1d9e7c81b97067ff63629030c47eaf4800836 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 12 Jun 2026 15:52:43 +0200 Subject: [PATCH 08/10] Exclude skill docs from CI path filters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dotnet-build-and-test.yml | 1 + .github/workflows/dotnet-format.yml | 1 + .github/workflows/python-code-quality.yml | 1 + .github/workflows/python-lab-tests.yml | 1 + .github/workflows/python-merge-tests.yml | 1 + .github/workflows/python-tests.yml | 1 + 6 files changed, 6 insertions(+) diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index 6582240a5cb..702e36bf84a 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -48,6 +48,7 @@ jobs: filters: | dotnet: - 'dotnet/**' + - '!dotnet/.github/skills/**' cosmosdb: - 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**' # The Foundry hosted-agent IT is costly (builds a container, pushes to ACR, diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index b9672967ef8..94358c2b2b1 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -10,6 +10,7 @@ on: branches: ["main", "feature*"] paths: - dotnet/** + - '!dotnet/.github/skills/**' - '.github/workflows/dotnet-format.yml' concurrency: diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index 6527a89cd83..f89019f8772 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -6,6 +6,7 @@ on: branches: ["main"] paths: - "python/**" + - "!python/.github/skills/**" env: # Configure a constant location for the uv cache diff --git a/.github/workflows/python-lab-tests.yml b/.github/workflows/python-lab-tests.yml index 3f959f85c25..38a94e1ee97 100644 --- a/.github/workflows/python-lab-tests.yml +++ b/.github/workflows/python-lab-tests.yml @@ -31,6 +31,7 @@ jobs: filters: | python: - 'python/**' + - '!python/.github/skills/**' # run only if 'python' files were changed - name: python tests if: steps.filter.outputs.python == 'true' diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index b1b1a8627b8..aa0fd24312e 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -49,6 +49,7 @@ jobs: filters: | python: - 'python/**' + - '!python/.github/skills/**' - '.github/actions/setup-local-mcp-server/**' - '.github/workflows/python-merge-tests.yml' - '.github/workflows/python-integration-tests.yml' diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 955fc9054d4..0d8e4256b7b 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -5,6 +5,7 @@ on: branches: ["main", "feature*"] paths: - "python/**" + - "!python/.github/skills/**" env: # Configure a constant location for the uv cache UV_CACHE_DIR: /tmp/.uv-cache From 7bbf4ea308983bc06d185e038fd02496deb1ebc4 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 12 Jun 2026 15:54:50 +0200 Subject: [PATCH 09/10] Match skill symlinks in CI path exclusions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dotnet-build-and-test.yml | 1 + .github/workflows/dotnet-format.yml | 1 + .github/workflows/python-code-quality.yml | 1 + .github/workflows/python-lab-tests.yml | 1 + .github/workflows/python-merge-tests.yml | 1 + .github/workflows/python-tests.yml | 1 + 6 files changed, 6 insertions(+) diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index 702e36bf84a..74522b0aa9b 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -48,6 +48,7 @@ jobs: filters: | dotnet: - 'dotnet/**' + - '!dotnet/.github/skills/*' - '!dotnet/.github/skills/**' cosmosdb: - 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**' diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 94358c2b2b1..2334a2c9339 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -10,6 +10,7 @@ on: branches: ["main", "feature*"] paths: - dotnet/** + - '!dotnet/.github/skills/*' - '!dotnet/.github/skills/**' - '.github/workflows/dotnet-format.yml' diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index f89019f8772..2bcb467ddec 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -6,6 +6,7 @@ on: branches: ["main"] paths: - "python/**" + - "!python/.github/skills/*" - "!python/.github/skills/**" env: diff --git a/.github/workflows/python-lab-tests.yml b/.github/workflows/python-lab-tests.yml index 38a94e1ee97..15225c2cd9f 100644 --- a/.github/workflows/python-lab-tests.yml +++ b/.github/workflows/python-lab-tests.yml @@ -31,6 +31,7 @@ jobs: filters: | python: - 'python/**' + - '!python/.github/skills/*' - '!python/.github/skills/**' # run only if 'python' files were changed - name: python tests diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index aa0fd24312e..5ed8c03c98a 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -49,6 +49,7 @@ jobs: filters: | python: - 'python/**' + - '!python/.github/skills/*' - '!python/.github/skills/**' - '.github/actions/setup-local-mcp-server/**' - '.github/workflows/python-merge-tests.yml' diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 0d8e4256b7b..e00dee65ba0 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -5,6 +5,7 @@ on: branches: ["main", "feature*"] paths: - "python/**" + - "!python/.github/skills/*" - "!python/.github/skills/**" env: # Configure a constant location for the uv cache From 07e898dca1434721b7617b55712b82bbd1fac174 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Fri, 12 Jun 2026 15:55:41 +0200 Subject: [PATCH 10/10] Exclude AGENTS docs from CI path filters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dotnet-build-and-test.yml | 2 ++ .github/workflows/dotnet-format.yml | 2 ++ .github/workflows/python-code-quality.yml | 2 ++ .github/workflows/python-lab-tests.yml | 2 ++ .github/workflows/python-merge-tests.yml | 2 ++ .github/workflows/python-tests.yml | 2 ++ 6 files changed, 12 insertions(+) diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index 74522b0aa9b..7585cb9e313 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -48,6 +48,8 @@ jobs: filters: | dotnet: - 'dotnet/**' + - '!dotnet/AGENTS.md' + - '!dotnet/**/AGENTS.md' - '!dotnet/.github/skills/*' - '!dotnet/.github/skills/**' cosmosdb: diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 2334a2c9339..118ded3bfd4 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -10,6 +10,8 @@ on: branches: ["main", "feature*"] paths: - dotnet/** + - '!dotnet/AGENTS.md' + - '!dotnet/**/AGENTS.md' - '!dotnet/.github/skills/*' - '!dotnet/.github/skills/**' - '.github/workflows/dotnet-format.yml' diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index 2bcb467ddec..a548dc838ef 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -6,6 +6,8 @@ on: branches: ["main"] paths: - "python/**" + - "!python/AGENTS.md" + - "!python/**/AGENTS.md" - "!python/.github/skills/*" - "!python/.github/skills/**" diff --git a/.github/workflows/python-lab-tests.yml b/.github/workflows/python-lab-tests.yml index 15225c2cd9f..8e5eeb68fcf 100644 --- a/.github/workflows/python-lab-tests.yml +++ b/.github/workflows/python-lab-tests.yml @@ -31,6 +31,8 @@ jobs: filters: | python: - 'python/**' + - '!python/AGENTS.md' + - '!python/**/AGENTS.md' - '!python/.github/skills/*' - '!python/.github/skills/**' # run only if 'python' files were changed diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index 5ed8c03c98a..2ab6d39c4e3 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -49,6 +49,8 @@ jobs: filters: | python: - 'python/**' + - '!python/AGENTS.md' + - '!python/**/AGENTS.md' - '!python/.github/skills/*' - '!python/.github/skills/**' - '.github/actions/setup-local-mcp-server/**' diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index e00dee65ba0..022abf3d04a 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -5,6 +5,8 @@ on: branches: ["main", "feature*"] paths: - "python/**" + - "!python/AGENTS.md" + - "!python/**/AGENTS.md" - "!python/.github/skills/*" - "!python/.github/skills/**" env: