From b38c47436048c53f30f3345269935f60e0202095 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 8 Apr 2026 15:58:46 -0400 Subject: [PATCH 1/6] Fix /create-issue workflow and add test failures for validation - Remove --no-restore from dotnet build (fix NETSDK1004) - Add workflow_dispatch trigger for testing from any branch - Make workflow steps handle both issue_comment and workflow_dispatch - Intentionally break two tests for end-to-end validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/create-failing-test-issue.yml | 158 +++++++++++++----- eng/testing/github-ci-trigger-patterns.txt | 1 + tests/Aspire.Hosting.Tests/ModelNameTests.cs | 2 +- .../ValueSnapshotTests.cs | 2 +- 4 files changed, 115 insertions(+), 48 deletions(-) diff --git a/.github/workflows/create-failing-test-issue.yml b/.github/workflows/create-failing-test-issue.yml index 77c95b5de70..986566d65a0 100644 --- a/.github/workflows/create-failing-test-issue.yml +++ b/.github/workflows/create-failing-test-issue.yml @@ -3,11 +3,42 @@ name: Create Failing-Test Issue on: issue_comment: types: [created] + workflow_dispatch: + inputs: + test_query: + description: 'Test name to create an issue for (leave empty to list all failures)' + required: false + type: string + source_url: + description: 'Source URL: PR, workflow run, or workflow job URL' + required: true + type: string + workflow: + description: 'Workflow selector alias or file path' + required: false + default: 'ci' + type: string + force_new: + description: 'Create a new issue even if one already exists' + required: false + default: false + type: boolean + pr_number: + description: 'PR or issue number to post result comments on (optional)' + required: false + type: number permissions: {} concurrency: - group: create-failing-test-issue-${{ github.event.issue.pull_request && 'pr' || 'issue' }}-${{ github.event.issue.number }} + group: >- + create-failing-test-issue-${{ + github.event_name == 'workflow_dispatch' + && format('dispatch-{0}', github.run_id) + || format('{0}-{1}', + github.event.issue.pull_request && 'pr' || 'issue', + github.event.issue.number) + }} cancel-in-progress: false jobs: @@ -15,7 +46,8 @@ jobs: name: Create failing-test issue if: >- github.repository == 'microsoft/aspire' && - startsWith(github.event.comment.body, '/create-issue') + (github.event_name == 'workflow_dispatch' || + startsWith(github.event.comment.body, '/create-issue')) runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -25,6 +57,7 @@ jobs: actions: read # list workflow runs and download artifacts steps: - name: Verify user has write access + if: github.event_name == 'issue_comment' id: verify-permission uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: @@ -61,6 +94,22 @@ jobs: uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | + if (context.eventName === 'workflow_dispatch') { + const testQuery = context.payload.inputs?.test_query ?? ''; + const sourceUrl = context.payload.inputs?.source_url ?? ''; + const workflow = context.payload.inputs?.workflow ?? 'ci'; + const forceNew = context.payload.inputs?.force_new === 'true'; + const listOnly = !testQuery; + + core.setOutput('test_query', testQuery); + core.setOutput('source_url', sourceUrl); + core.setOutput('workflow', workflow); + core.setOutput('force_new', forceNew ? 'true' : 'false'); + core.setOutput('list_only', listOnly ? 'true' : 'false'); + core.setOutput('pr_number', context.payload.inputs?.pr_number ?? ''); + return; + } + const helper = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/create-failing-test-issue.js`); const defaultSourceUrl = context.payload.issue.pull_request ? `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${context.issue.number}` @@ -84,9 +133,10 @@ jobs: core.setOutput('workflow', parsed.workflow); core.setOutput('force_new', parsed.forceNew ? 'true' : 'false'); core.setOutput('list_only', parsed.listOnly ? 'true' : 'false'); + core.setOutput('pr_number', String(context.issue.number)); - name: Add processing reaction - if: success() + if: success() && github.event_name == 'issue_comment' uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | @@ -124,7 +174,7 @@ jobs: args+=(--force-new) fi - dotnet build tools/CreateFailingTestIssue --no-restore -v:q + dotnet build tools/CreateFailingTestIssue -v:q dotnet run --no-build --project tools/CreateFailingTestIssue -- "${args[@]}" --output "$RUNNER_TEMP/failing-test-result.json" - name: Create or update failing-test issue @@ -134,10 +184,28 @@ jobs: RESULT_PATH: ${{ runner.temp }}/failing-test-result.json FORCE_NEW: ${{ steps.extract-command.outputs.force_new }} LIST_ONLY: ${{ steps.extract-command.outputs.list_only }} + PR_NUMBER: ${{ steps.extract-command.outputs.pr_number }} + RESOLVE_OUTCOME: ${{ steps.resolve-failure.outcome }} with: script: | const fs = require('fs'); const helper = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/create-failing-test-issue.js`); + const prNumber = process.env.PR_NUMBER ? parseInt(process.env.PR_NUMBER, 10) : null; + const resolverFailed = process.env.RESOLVE_OUTCOME === 'failure'; + const hasResultFile = fs.existsSync(process.env.RESULT_PATH) && fs.statSync(process.env.RESULT_PATH).size > 0; + + async function postComment(body) { + if (!prNumber) { + core.info(`No PR/issue number available. Comment:\n${body}`); + return; + } + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body + }); + } const helpBlock = [ '📋 **`/create-issue` — Usage**', @@ -153,11 +221,20 @@ jobs: '```', ].join('\n'); + // If the resolver step failed and produced no output, report the error + if (resolverFailed && !hasResultFile) { + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const message = `❌ The failing-test resolver failed to run. See the [workflow run](${runUrl}) for details.`; + await postComment(message); + core.setFailed(message); + return; + } + // List-only mode: show available failures + help when no --test was given if (process.env.LIST_ONLY === 'true') { let body = ''; - if (fs.existsSync(process.env.RESULT_PATH) && fs.statSync(process.env.RESULT_PATH).size > 0) { + if (hasResultFile) { const result = JSON.parse(fs.readFileSync(process.env.RESULT_PATH, 'utf8')); const tests = result.allFailures?.tests?.map(t => t.canonicalTestName ?? t.displayTestName) ?? result.diagnostics?.availableFailedTests @@ -174,23 +251,13 @@ jobs: body = 'No test failures were found. Use `--url` to point to a specific workflow run.\n\n'; } - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: `${body}${helpBlock}` - }); + await postComment(`${body}${helpBlock}`); return; } - if (!fs.existsSync(process.env.RESULT_PATH) || fs.statSync(process.env.RESULT_PATH).size === 0) { - const message = `@${context.payload.comment.user.login} ❌ The failing-test resolver did not produce a JSON result. See the workflow run for details.`; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: message - }); + if (!hasResultFile) { + const message = '❌ The failing-test resolver did not produce a JSON result. See the workflow run for details.'; + await postComment(message); core.setFailed(message); return; } @@ -200,13 +267,8 @@ jobs: const candidates = result.diagnostics?.availableFailedTests?.length ? `\n\n**Available failed tests:**\n${result.diagnostics.availableFailedTests.map(name => `- \`${name}\``).join('\n')}` : ''; - const message = `@${context.payload.comment.user.login} ❌ ${result.errorMessage ?? 'The failing-test resolver could not create an issue.'}${candidates}`; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: message - }); + const message = `❌ ${result.errorMessage ?? 'The failing-test resolver could not create an issue.'}${candidates}`; + await postComment(message); core.setFailed(result.errorMessage ?? 'The failing-test resolver failed.'); return; } @@ -276,30 +338,34 @@ jobs: disableHint = `\n\nTo disable this test on your PR, comment:\n\`\`\`\n/disable-test ${testName} ${issueUrl}\n\`\`\``; } - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: `✅ ${action[0].toUpperCase()}${action.slice(1)} failing-test issue #${issueNumber}: ${issueUrl}${disableHint}` - }); + await postComment(`✅ ${action[0].toUpperCase()}${action.slice(1)} failing-test issue #${issueNumber}: ${issueUrl}${disableHint}`); - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: 'rocket' - }); + if (context.eventName === 'issue_comment') { + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket' + }); + } - name: Post failure comment on unexpected error - if: failure() && steps.extract-command.outcome == 'success' && steps.verify-permission.outcome == 'success' + if: failure() && steps.extract-command.outcome == 'success' && (steps.verify-permission.outcome == 'success' || steps.verify-permission.outcome == 'skipped') uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + PR_NUMBER: ${{ steps.extract-command.outputs.pr_number }} with: script: | + const prNumber = process.env.PR_NUMBER ? parseInt(process.env.PR_NUMBER, 10) : null; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const message = `@${context.payload.comment.user.login} ❌ The \`/create-issue\` command failed. See the [workflow run](${runUrl}) for details.`; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: message - }); + const message = `❌ The \`/create-issue\` command failed. See the [workflow run](${runUrl}) for details.`; + if (prNumber) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: message + }); + } else { + core.info(message); + } diff --git a/eng/testing/github-ci-trigger-patterns.txt b/eng/testing/github-ci-trigger-patterns.txt index 28df2daa936..73786504db0 100644 --- a/eng/testing/github-ci-trigger-patterns.txt +++ b/eng/testing/github-ci-trigger-patterns.txt @@ -39,6 +39,7 @@ Aspire-Core.slnf .github/workflows/apply-test-attributes.yml .github/workflows/backmerge-release.yml .github/workflows/backport.yml +.github/workflows/create-failing-test-issue.* .github/workflows/dogfood-comment.yml .github/workflows/generate-api-diffs.yml .github/workflows/generate-ats-diffs.yml diff --git a/tests/Aspire.Hosting.Tests/ModelNameTests.cs b/tests/Aspire.Hosting.Tests/ModelNameTests.cs index 113bce695c4..df0320b628e 100644 --- a/tests/Aspire.Hosting.Tests/ModelNameTests.cs +++ b/tests/Aspire.Hosting.Tests/ModelNameTests.cs @@ -23,7 +23,7 @@ public void ValidateName_Empty_Error() var exception = Assert.Throws(() => ModelName.ValidateName(nameof(Resource), "")); // Assert - Assert.Equal($"Resource name '' is invalid. Name must be between 1 and 64 characters long. (Parameter 'name')", exception.Message); + Assert.Equal($"Resource name '' is BROKEN. Name must be between 1 and 64 characters long. (Parameter 'name')", exception.Message); // intentionally broken for /create-issue testing } [Fact] diff --git a/tests/Aspire.Hosting.Tests/ValueSnapshotTests.cs b/tests/Aspire.Hosting.Tests/ValueSnapshotTests.cs index 9177b7d7bb8..d859b130f8b 100644 --- a/tests/Aspire.Hosting.Tests/ValueSnapshotTests.cs +++ b/tests/Aspire.Hosting.Tests/ValueSnapshotTests.cs @@ -135,7 +135,7 @@ public void IsValueSet_FalseBeforeAnySet() { var snapshot = new ValueSnapshot(); - Assert.False(snapshot.IsValueSet); + Assert.True(snapshot.IsValueSet); // intentionally broken for /create-issue testing } [Fact] From 948ecfb5161105d6979908614071a36438425c14 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 8 Apr 2026 17:52:10 -0400 Subject: [PATCH 2/6] Fix: allow positional test name with --force-new flag The parser rejected positional test names when any flag was present, e.g. '/create-issue TestName --force-new' would fail with 'Unknown argument'. Now non-flag tokens are accepted as positional test names in flag mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/create-failing-test-issue.js | 20 +++++++++++++++---- .../CreateFailingTestIssueWorkflowTests.cs | 19 ++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.github/workflows/create-failing-test-issue.js b/.github/workflows/create-failing-test-issue.js index ec98e014aa4..c1c5b6d41e8 100644 --- a/.github/workflows/create-failing-test-issue.js +++ b/.github/workflows/create-failing-test-issue.js @@ -57,10 +57,22 @@ function parseCommand(body, defaultSourceUrl = null) { break; default: - return { - success: false, - errorMessage: `Unknown argument '${token}'. Supported arguments are --test, --url, --workflow, and --force-new.`, - }; + if (token.startsWith('--')) { + return { + success: false, + errorMessage: `Unknown argument '${token}'. Supported arguments are --test, --url, --workflow, and --force-new.`, + }; + } + + if (result.testQuery) { + return { + success: false, + errorMessage: 'Positional input is ambiguous. Use /create-issue --test "" [--url ] [--workflow ] [--force-new].', + }; + } + + result.testQuery = token; + break; } } diff --git a/tests/Infrastructure.Tests/WorkflowScripts/CreateFailingTestIssueWorkflowTests.cs b/tests/Infrastructure.Tests/WorkflowScripts/CreateFailingTestIssueWorkflowTests.cs index bf7a3c2a5a2..4286940e1c3 100644 --- a/tests/Infrastructure.Tests/WorkflowScripts/CreateFailingTestIssueWorkflowTests.cs +++ b/tests/Infrastructure.Tests/WorkflowScripts/CreateFailingTestIssueWorkflowTests.cs @@ -80,6 +80,25 @@ public async Task ParseCommandUsesTrailingUrlForCompatibilitySyntax() Assert.Equal("https://github.com/microsoft/aspire/actions/runs/123/job/456", result.SourceUrl); } + [Fact] + [RequiresTools(["node"])] + public async Task ParseCommandSupportsPositionalTestNameWithFlags() + { + var result = await InvokeHarnessAsync( + "parseCommand", + new + { + body = "/create-issue Tests.Namespace.Type.Method --force-new", + defaultSourceUrl = "https://github.com/microsoft/aspire/pull/999" + }); + + Assert.True(result.Success); + Assert.Equal("Tests.Namespace.Type.Method", result.TestQuery); + Assert.Equal("https://github.com/microsoft/aspire/pull/999", result.SourceUrl); + Assert.True(result.ForceNew); + Assert.False(result.ListOnly); + } + [Fact] [RequiresTools(["node"])] public async Task ParseCommandRejectsAmbiguousPositionalSyntax() From 85209ad281139767a95c43b499b5707dfc3876dd Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 8 Apr 2026 18:27:08 -0400 Subject: [PATCH 3/6] Revert intentional test failures after E2E validation All /create-issue variants tested successfully: - List-only (no args) - Positional test name - --test flag - Update existing issue - --force-new - --url with explicit run URL - workflow_dispatch with pr_number Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/Aspire.Hosting.Tests/ModelNameTests.cs | 2 +- tests/Aspire.Hosting.Tests/ValueSnapshotTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/ModelNameTests.cs b/tests/Aspire.Hosting.Tests/ModelNameTests.cs index df0320b628e..113bce695c4 100644 --- a/tests/Aspire.Hosting.Tests/ModelNameTests.cs +++ b/tests/Aspire.Hosting.Tests/ModelNameTests.cs @@ -23,7 +23,7 @@ public void ValidateName_Empty_Error() var exception = Assert.Throws(() => ModelName.ValidateName(nameof(Resource), "")); // Assert - Assert.Equal($"Resource name '' is BROKEN. Name must be between 1 and 64 characters long. (Parameter 'name')", exception.Message); // intentionally broken for /create-issue testing + Assert.Equal($"Resource name '' is invalid. Name must be between 1 and 64 characters long. (Parameter 'name')", exception.Message); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/ValueSnapshotTests.cs b/tests/Aspire.Hosting.Tests/ValueSnapshotTests.cs index d859b130f8b..9177b7d7bb8 100644 --- a/tests/Aspire.Hosting.Tests/ValueSnapshotTests.cs +++ b/tests/Aspire.Hosting.Tests/ValueSnapshotTests.cs @@ -135,7 +135,7 @@ public void IsValueSet_FalseBeforeAnySet() { var snapshot = new ValueSnapshot(); - Assert.True(snapshot.IsValueSet); // intentionally broken for /create-issue testing + Assert.False(snapshot.IsValueSet); } [Fact] From 36d4f4d73ed91fc673e25032aa41710d5cf9d4d1 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 8 Apr 2026 18:36:37 -0400 Subject: [PATCH 4/6] Extract formatListResponse and add tests for silent build failure Extract the list-only response logic from inline YAML into a testable formatListResponse() function in the JS helper. This covers the bug where a resolver build failure silently fell through to 'No test failures found' instead of reporting the error. New tests: - FormatListResponseReturnsErrorWhenResolverFailed - FormatListResponseReturnsTestNamesFromResult - FormatListResponseReturnsNoFailuresWhenResultIsEmpty Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/create-failing-test-issue.js | 22 +++++++ .../workflows/create-failing-test-issue.yml | 33 +++++----- .../CreateFailingTestIssueWorkflowTests.cs | 64 +++++++++++++++++++ .../create-failing-test-issue.harness.js | 3 + 4 files changed, 106 insertions(+), 16 deletions(-) diff --git a/.github/workflows/create-failing-test-issue.js b/.github/workflows/create-failing-test-issue.js index c1c5b6d41e8..b5e7e744b46 100644 --- a/.github/workflows/create-failing-test-issue.js +++ b/.github/workflows/create-failing-test-issue.js @@ -106,6 +106,27 @@ function parseCommand(body, defaultSourceUrl = null) { }; } +function formatListResponse(resolverOutcome, resultJson) { + if (resolverOutcome === 'failure' && !resultJson) { + return { error: true, message: 'The failing-test resolver failed to run.' }; + } + + const tests = resultJson?.allFailures?.tests?.map(t => t.canonicalTestName ?? t.displayTestName) + ?? resultJson?.diagnostics?.availableFailedTests + ?? []; + + if (tests.length > 0) { + return { + error: false, + message: '**Failed tests found on this PR:**\n\n' + + tests.map(name => `/create-issue ${name}`).join('\n'), + tests, + }; + } + + return { error: false, message: 'No test failures were found. Use `--url` to point to a specific workflow run.' }; +} + function buildIssueSearchQuery(owner, repo, metadataMarker) { const escapedMarker = String(metadataMarker ?? '').replaceAll('"', '\\"'); return `repo:${owner}/${repo} is:issue label:failing-test in:body "${escapedMarker}"`; @@ -123,6 +144,7 @@ function isSupportedSourceUrl(value) { module.exports = { buildIssueSearchQuery, + formatListResponse, isSupportedSourceUrl, parseCommand, }; diff --git a/.github/workflows/create-failing-test-issue.yml b/.github/workflows/create-failing-test-issue.yml index 986566d65a0..adf343e66b1 100644 --- a/.github/workflows/create-failing-test-issue.yml +++ b/.github/workflows/create-failing-test-issue.yml @@ -232,24 +232,25 @@ jobs: // List-only mode: show available failures + help when no --test was given if (process.env.LIST_ONLY === 'true') { - let body = ''; - - if (hasResultFile) { - const result = JSON.parse(fs.readFileSync(process.env.RESULT_PATH, 'utf8')); - const tests = result.allFailures?.tests?.map(t => t.canonicalTestName ?? t.displayTestName) - ?? result.diagnostics?.availableFailedTests - ?? []; - - if (tests.length > 0) { - body = '**Failed tests found on this PR:**\n\n' - + tests.map(name => `- \`/create-issue ${name}\``).join('\n') - + '\n\n'; - } + const resultJson = hasResultFile + ? JSON.parse(fs.readFileSync(process.env.RESULT_PATH, 'utf8')) + : null; + const listResult = helper.formatListResponse(process.env.RESOLVE_OUTCOME, resultJson); + + if (listResult.error) { + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const message = `❌ ${listResult.message} See the [workflow run](${runUrl}) for details.`; + await postComment(message); + core.setFailed(message); + return; } - if (!body) { - body = 'No test failures were found. Use `--url` to point to a specific workflow run.\n\n'; - } + // Format test names as clickable commands in list mode + let body = listResult.tests?.length > 0 + ? '**Failed tests found on this PR:**\n\n' + + listResult.tests.map(name => `- \`/create-issue ${name}\``).join('\n') + + '\n\n' + : listResult.message + '\n\n'; await postComment(`${body}${helpBlock}`); return; diff --git a/tests/Infrastructure.Tests/WorkflowScripts/CreateFailingTestIssueWorkflowTests.cs b/tests/Infrastructure.Tests/WorkflowScripts/CreateFailingTestIssueWorkflowTests.cs index 4286940e1c3..b0a7cbd595d 100644 --- a/tests/Infrastructure.Tests/WorkflowScripts/CreateFailingTestIssueWorkflowTests.cs +++ b/tests/Infrastructure.Tests/WorkflowScripts/CreateFailingTestIssueWorkflowTests.cs @@ -150,6 +150,68 @@ public async Task ParseCommandReturnsListOnlyWhenFlagBasedWithoutTest() Assert.Equal("custom.yml", result.Workflow); } + [Fact] + [RequiresTools(["node"])] + public async Task FormatListResponseReturnsErrorWhenResolverFailed() + { + var result = await InvokeHarnessAsync( + "formatListResponse", + new + { + resolverOutcome = "failure", + resultJson = (object?)null + }); + + Assert.True(result.Error); + Assert.Contains("resolver failed", result.Message); + } + + [Fact] + [RequiresTools(["node"])] + public async Task FormatListResponseReturnsTestNamesFromResult() + { + var result = await InvokeHarnessAsync( + "formatListResponse", + new + { + resolverOutcome = "success", + resultJson = new + { + allFailures = new + { + tests = new[] + { + new { canonicalTestName = "Namespace.Class.MethodA", displayTestName = "MethodA" }, + new { canonicalTestName = "Namespace.Class.MethodB", displayTestName = "MethodB" } + } + } + } + }); + + Assert.False(result.Error); + Assert.NotNull(result.Tests); + Assert.Equal(2, result.Tests!.Length); + Assert.Contains("Namespace.Class.MethodA", result.Tests); + Assert.Contains("Namespace.Class.MethodB", result.Tests); + } + + [Fact] + [RequiresTools(["node"])] + public async Task FormatListResponseReturnsNoFailuresWhenResultIsEmpty() + { + var result = await InvokeHarnessAsync( + "formatListResponse", + new + { + resolverOutcome = "success", + resultJson = new { allFailures = new { tests = Array.Empty() } } + }); + + Assert.False(result.Error); + Assert.Contains("No test failures", result.Message); + Assert.Null(result.Tests); + } + [Fact] [RequiresTools(["node"])] public async Task BuildIssueSearchQueryTargetsFailingTestIssuesByMetadataMarker() @@ -203,4 +265,6 @@ private static string FindRepoRoot() private sealed record HarnessResponse(T Result); private sealed record ParseCommandResult(bool Success, string TestQuery, string? SourceUrl, string Workflow, bool ForceNew, bool ListOnly, string? ErrorMessage); + + private sealed record FormatListResponseResult(bool Error, string Message, string[]? Tests); } diff --git a/tests/Infrastructure.Tests/WorkflowScripts/create-failing-test-issue.harness.js b/tests/Infrastructure.Tests/WorkflowScripts/create-failing-test-issue.harness.js index 9d8c6483047..a51356da69b 100644 --- a/tests/Infrastructure.Tests/WorkflowScripts/create-failing-test-issue.harness.js +++ b/tests/Infrastructure.Tests/WorkflowScripts/create-failing-test-issue.harness.js @@ -20,6 +20,9 @@ async function dispatch(operation, payload) { case 'buildIssueSearchQuery': return helper.buildIssueSearchQuery(payload.owner ?? 'microsoft', payload.repo ?? 'aspire', payload.metadataMarker); + case 'formatListResponse': + return helper.formatListResponse(payload.resolverOutcome, payload.resultJson ?? null); + default: throw new Error(`Unsupported operation '${operation}'.`); } From 59071ff7e7097d54a3c0e4e08d244f01d23c47c5 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 8 Apr 2026 18:41:05 -0400 Subject: [PATCH 5/6] Speed up workflow: drop restore.sh, use setup-dotnet Isolate CreateFailingTestIssue from the repo's Arcade SDK by adding Directory.Build.props/targets/Packages.props that cut inheritance. Replace ProjectReference to Aspire.TestTools with shared source links. Replace restore.sh (~2.5min) with setup-dotnet action (~5s). The tool builds in ~4s with a plain dotnet SDK, no Arcade needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/create-failing-test-issue.yml | 6 ++++-- .../CreateFailingTestIssue/CreateFailingTestIssue.csproj | 2 +- tools/CreateFailingTestIssue/Directory.Build.props | 3 +++ tools/CreateFailingTestIssue/Directory.Build.targets | 3 +++ tools/CreateFailingTestIssue/Directory.Packages.props | 9 +++++++++ 5 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 tools/CreateFailingTestIssue/Directory.Build.props create mode 100644 tools/CreateFailingTestIssue/Directory.Build.targets create mode 100644 tools/CreateFailingTestIssue/Directory.Packages.props diff --git a/.github/workflows/create-failing-test-issue.yml b/.github/workflows/create-failing-test-issue.yml index adf343e66b1..6d64215d244 100644 --- a/.github/workflows/create-failing-test-issue.yml +++ b/.github/workflows/create-failing-test-issue.yml @@ -147,9 +147,11 @@ jobs: content: 'eyes' }); - - name: Restore SDK + - name: Setup .NET SDK if: success() - run: ./restore.sh + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 + with: + global-json-file: global.json - name: Resolve failing test details if: success() diff --git a/tools/CreateFailingTestIssue/CreateFailingTestIssue.csproj b/tools/CreateFailingTestIssue/CreateFailingTestIssue.csproj index 7b37a13df28..eb3af36f183 100644 --- a/tools/CreateFailingTestIssue/CreateFailingTestIssue.csproj +++ b/tools/CreateFailingTestIssue/CreateFailingTestIssue.csproj @@ -13,7 +13,7 @@ - + diff --git a/tools/CreateFailingTestIssue/Directory.Build.props b/tools/CreateFailingTestIssue/Directory.Build.props new file mode 100644 index 00000000000..6ed2903ca80 --- /dev/null +++ b/tools/CreateFailingTestIssue/Directory.Build.props @@ -0,0 +1,3 @@ + + + diff --git a/tools/CreateFailingTestIssue/Directory.Build.targets b/tools/CreateFailingTestIssue/Directory.Build.targets new file mode 100644 index 00000000000..6ed2903ca80 --- /dev/null +++ b/tools/CreateFailingTestIssue/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/tools/CreateFailingTestIssue/Directory.Packages.props b/tools/CreateFailingTestIssue/Directory.Packages.props new file mode 100644 index 00000000000..7b709c8debe --- /dev/null +++ b/tools/CreateFailingTestIssue/Directory.Packages.props @@ -0,0 +1,9 @@ + + + true + + + + + + From f4c87b4a4989ec9bb7e499e2d2de00aad4c01544 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 8 Apr 2026 19:16:02 -0400 Subject: [PATCH 6/6] Update actions/github-script to v8 (Node 22) Replace Node 20 github-script@v7 with github-script@v8 to avoid the upcoming Node 20 deprecation on GitHub Actions runners. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/create-failing-test-issue.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/create-failing-test-issue.yml b/.github/workflows/create-failing-test-issue.yml index 6d64215d244..28403a82069 100644 --- a/.github/workflows/create-failing-test-issue.yml +++ b/.github/workflows/create-failing-test-issue.yml @@ -59,7 +59,7 @@ jobs: - name: Verify user has write access if: github.event_name == 'issue_comment' id: verify-permission - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const commentUser = context.payload.comment.user.login; @@ -91,7 +91,7 @@ jobs: - name: Extract command if: success() id: extract-command - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | if (context.eventName === 'workflow_dispatch') { @@ -137,7 +137,7 @@ jobs: - name: Add processing reaction if: success() && github.event_name == 'issue_comment' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | await github.rest.reactions.createForIssueComment({ @@ -181,7 +181,7 @@ jobs: - name: Create or update failing-test issue if: steps.resolve-failure.outcome != 'skipped' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: RESULT_PATH: ${{ runner.temp }}/failing-test-result.json FORCE_NEW: ${{ steps.extract-command.outputs.force_new }} @@ -354,7 +354,7 @@ jobs: - name: Post failure comment on unexpected error if: failure() && steps.extract-command.outcome == 'success' && (steps.verify-permission.outcome == 'success' || steps.verify-permission.outcome == 'skipped') - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: PR_NUMBER: ${{ steps.extract-command.outputs.pr_number }} with: