diff --git a/.github/scripts/pr_limit_moderation.js b/.github/scripts/pr_limit_moderation.js index 450813d22f..6cb23e053a 100644 --- a/.github/scripts/pr_limit_moderation.js +++ b/.github/scripts/pr_limit_moderation.js @@ -8,6 +8,7 @@ function getPullRequest(context) { return { author: pullRequest.user.login, + authorType: pullRequest.user.type, labels: pullRequest.labels?.map((label) => label.name).filter(Boolean) ?? [], number: pullRequest.number, }; @@ -49,6 +50,10 @@ function hasLabel(labels, labelName) { return labels.some((label) => label.toLowerCase() === labelName.toLowerCase()); } +function isDependabotAuthor({ author, authorType }) { + return authorType === 'Bot' && author.toLowerCase() === 'dependabot[bot]'; +} + function buildLimitMessage({ author, exemptLabelName, maxOpenPrs, openPrCount }) { return [ `Thank you for your contribution, @${author}.`, @@ -83,7 +88,17 @@ async function getOpenPrCount({ github, owner, repo, author, pullRequestNumber } async function enforcePrLimit({ github, context, core, exemptLabelName, maxOpenPrs, labelName }) { const { owner, repo } = context.repo; - const { author, labels, number } = getPullRequest(context); + const { author, authorType, labels, number } = getPullRequest(context); + + if (isDependabotAuthor({ author, authorType })) { + core.info(`Author ${author} is Dependabot; skipping open PR limit enforcement.`); + return { + author, + closed: false, + dependabotExempt: true, + openPrCount: null, + }; + } if (hasLabel(labels, exemptLabelName)) { core.info(`PR #${number} has the ${exemptLabelName} label; skipping open PR limit enforcement.`); diff --git a/.github/tests/test_pr_limit_moderation.js b/.github/tests/test_pr_limit_moderation.js index d3dccb0cce..5f0c8c865a 100644 --- a/.github/tests/test_pr_limit_moderation.js +++ b/.github/tests/test_pr_limit_moderation.js @@ -16,7 +16,7 @@ const { enforcePrLimit } = require('../scripts/pr_limit_moderation.js'); // Helpers // --------------------------------------------------------------------------- -function createContext({ author = 'community-user', labels = [], number = 123 } = {}) { +function createContext({ author = 'community-user', authorType = 'User', labels = [], number = 123 } = {}) { return { repo: { owner: 'microsoft', @@ -28,6 +28,7 @@ function createContext({ author = 'community-user', labels = [], number = 123 } labels: labels.map((name) => ({ name })), user: { login: author, + type: authorType, }, }, }, @@ -296,6 +297,30 @@ describe('PR limit enforcement', () => { assert.deepEqual(github.calls, []); }); + it('does not close Dependabot PRs', async () => { + const github = createGithub({ + itemNumbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)], + pullRequests: createPullRequestPage({ + author: 'dependabot[bot]', + numbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)], + }), + }); + + const result = await enforcePrLimit({ + github, + context: createContext({ author: 'dependabot[bot]', authorType: 'Bot' }), + core: createCore(), + exemptLabelName: 'pr-limit-exempt', + maxOpenPrs: 10, + labelName: 'too-many-prs', + }); + + assert.equal(result.closed, false); + assert.equal(result.dependabotExempt, true); + assert.equal(result.openPrCount, null); + assert.deepEqual(github.calls, []); + }); + it('counts the current PR when the author has more than one page of open PRs', async () => { const github = createGithub({ itemNumbers: [123, ...Array.from({ length: 100 }, (_, index) => index + 1)], diff --git a/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs b/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs index 54e3eb40f2..4db325b658 100644 --- a/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs +++ b/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs @@ -50,12 +50,16 @@ internal static partial class WorkflowHelper /// /// Executor that starts the concurrent processing by sending messages to the agents. /// - private sealed partial class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor") + [SendsMessage(typeof(List))] + [SendsMessage(typeof(TurnToken))] + private sealed partial class ConcurrentStartExecutor() + : Executor("ConcurrentStartExecutor", declareCrossRunShareable: true), IResettableExecutor { [MessageHandler] - internal ValueTask RouteMessages(List messages, IWorkflowContext context, CancellationToken cancellationToken) + internal ValueTask RouteMessages(IEnumerable messages, IWorkflowContext context, CancellationToken cancellationToken) { - return context.SendMessageAsync(messages, cancellationToken: cancellationToken); + List payload = messages as List ?? messages.ToList(); + return context.SendMessageAsync(payload, cancellationToken: cancellationToken); } [MessageHandler] @@ -63,13 +67,16 @@ internal static partial class WorkflowHelper { return context.SendMessageAsync(token, cancellationToken: cancellationToken); } + + public ValueTask ResetAsync() => default; } /// /// Executor that aggregates the results from the concurrent agents. /// - [YieldsOutput(typeof(List))] - private sealed partial class ConcurrentAggregationExecutor() : Executor>("ConcurrentAggregationExecutor") + [YieldsOutput(typeof(string))] + private sealed partial class ConcurrentAggregationExecutor() : + Executor>("ConcurrentAggregationExecutor"), IResettableExecutor { private readonly List _messages = []; @@ -90,5 +97,11 @@ internal static partial class WorkflowHelper await context.YieldOutputAsync(formattedMessages, cancellationToken); } } + + public ValueTask ResetAsync() + { + this._messages.Clear(); + return default; + } } }