Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 38 additions & 4 deletions .github/workflows/create-failing-test-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<test-name>" [--url <pr|run|job-url>] [--workflow <selector>] [--force-new].',
};
}

result.testQuery = token;
break;
}
}

Expand Down Expand Up @@ -94,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}"`;
Expand All @@ -111,6 +144,7 @@ function isSupportedSourceUrl(value) {

module.exports = {
buildIssueSearchQuery,
formatListResponse,
isSupportedSourceUrl,
parseCommand,
};
49 changes: 26 additions & 23 deletions .github/workflows/create-failing-test-issue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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({
Expand All @@ -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()
Expand Down Expand Up @@ -179,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 }}
Expand Down Expand Up @@ -232,24 +234,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;
Expand Down Expand Up @@ -351,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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ParseCommandResult>(
"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()
Expand Down Expand Up @@ -131,6 +150,68 @@ public async Task ParseCommandReturnsListOnlyWhenFlagBasedWithoutTest()
Assert.Equal("custom.yml", result.Workflow);
}

[Fact]
[RequiresTools(["node"])]
public async Task FormatListResponseReturnsErrorWhenResolverFailed()
{
var result = await InvokeHarnessAsync<FormatListResponseResult>(
"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<FormatListResponseResult>(
"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<FormatListResponseResult>(
"formatListResponse",
new
{
resolverOutcome = "success",
resultJson = new { allFailures = new { tests = Array.Empty<object>() } }
});

Assert.False(result.Error);
Assert.Contains("No test failures", result.Message);
Assert.Null(result.Tests);
}

[Fact]
[RequiresTools(["node"])]
public async Task BuildIssueSearchQueryTargetsFailingTestIssuesByMetadataMarker()
Expand Down Expand Up @@ -184,4 +265,6 @@ private static string FindRepoRoot()
private sealed record HarnessResponse<T>(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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}'.`);
}
Expand Down
2 changes: 1 addition & 1 deletion tools/CreateFailingTestIssue/CreateFailingTestIssue.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Aspire.TestTools\Aspire.TestTools.csproj" />
<Compile Include="..\Aspire.TestTools\*.cs" LinkBase="Aspire.TestTools" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions tools/CreateFailingTestIssue/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<!-- Cut inheritance from repo root to avoid Arcade SDK dependency -->
</Project>
3 changes: 3 additions & 0 deletions tools/CreateFailingTestIssue/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<!-- Cut inheritance from repo root to avoid Arcade SDK dependency -->
</Project>
9 changes: 9 additions & 0 deletions tools/CreateFailingTestIssue/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="System.CommandLine" Version="2.0.0" />
<PackageVersion Include="System.IO.Hashing" Version="10.0.3" />
</ItemGroup>
</Project>
Loading