Skip to content

Create Failing-Test Issue #170

Create Failing-Test Issue

Create Failing-Test Issue #170

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_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:
create_failing_test_issue:
name: Create failing-test issue
if: >-
github.repository == 'microsoft/aspire' &&
(github.event_name == 'workflow_dispatch' ||
startsWith(github.event.comment.body, '/create-issue'))
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read # checkout repository
issues: write # create/reopen/comment on issues
pull-requests: write # comment on PRs
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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const commentUser = context.payload.comment.user.login;
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: commentUser
});
const writePermissions = ['admin', 'maintain', 'write'];
if (!writePermissions.includes(permission.permission)) {
const message = `@${commentUser} The \`/create-issue\` command requires write, maintain, or admin access to this repository.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
core.setOutput('error_message', message);
core.setFailed(message);
return;
}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
if: success()
with:
persist-credentials: false
- name: Extract command
if: success()
id: extract-command
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
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}`
: null;
const parsed = helper.parseCommand(context.payload.comment.body, defaultSourceUrl);
if (!parsed.success) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `@${context.payload.comment.user.login} ❌ ${parsed.errorMessage}`
});
core.setOutput('error_message', parsed.errorMessage);
core.setFailed(parsed.errorMessage);
return;
}
core.setOutput('test_query', parsed.testQuery);
core.setOutput('source_url', parsed.sourceUrl ?? '');
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() && github.event_name == 'issue_comment'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes'
});
- name: Setup .NET SDK
if: success()
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
global-json-file: global.json
- name: Resolve failing test details
if: success()
id: resolve-failure
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
TEST_QUERY: ${{ steps.extract-command.outputs.test_query }}
SOURCE_URL: ${{ steps.extract-command.outputs.source_url }}
WORKFLOW_SELECTOR: ${{ steps.extract-command.outputs.workflow }}
FORCE_NEW: ${{ steps.extract-command.outputs.force_new }}
LIST_ONLY: ${{ steps.extract-command.outputs.list_only }}
run: |
args=(--workflow "$WORKFLOW_SELECTOR" --repo "${GITHUB_REPOSITORY}")
if [ "$LIST_ONLY" != "true" ]; then
args+=(--test "$TEST_QUERY")
fi
if [ -n "$SOURCE_URL" ]; then
args+=(--url "$SOURCE_URL")
fi
if [ "$FORCE_NEW" = "true" ]; then
args+=(--force-new)
fi
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
if: steps.resolve-failure.outcome != 'skipped'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
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**',
'',
'Creates or updates a failing-test issue from CI failures.',
'',
'```',
'/create-issue <test-name>',
'/create-issue <test-name> <pr|run|job-url>',
'/create-issue --test "<test-name>"',
'/create-issue --test "<test-name>" --url <pr|run|job-url>',
'/create-issue --test "<test-name>" --force-new',
'```',
].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') {
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;
}
await postComment(`${listResult.message}${helpBlock}`);
return;
}
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;
}
const result = JSON.parse(fs.readFileSync(process.env.RESULT_PATH, 'utf8'));
if (!result.success) {
const candidates = result.diagnostics?.availableFailedTests?.length
? `\n\n**Available failed tests:**\n${result.diagnostics.availableFailedTests.map(name => `- \`${name}\``).join('\n')}`
: '';
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;
}
let targetIssue = null;
if (process.env.FORCE_NEW !== 'true') {
const query = helper.buildIssueSearchQuery(context.repo.owner, context.repo.repo, result.issue.metadataMarker);
const { data: search } = await github.rest.search.issuesAndPullRequests({
q: query,
per_page: 20
});
const issues = search.items.filter(item => !item.pull_request);
targetIssue = issues.find(item => item.state === 'open') ?? issues.find(item => item.state === 'closed') ?? null;
}
let action;
let issueNumber;
let issueUrl;
if (targetIssue && targetIssue.state === 'open') {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: targetIssue.number,
body: result.issue.commentBody
});
action = 'updated';
issueNumber = targetIssue.number;
issueUrl = targetIssue.html_url;
} else if (targetIssue && targetIssue.state === 'closed') {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: targetIssue.number,
state: 'open'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: targetIssue.number,
body: result.issue.commentBody
});
action = 'reopened';
issueNumber = targetIssue.number;
issueUrl = targetIssue.html_url;
} else {
const { data: issue } = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: result.issue.title,
body: result.issue.body,
labels: result.issue.labels
});
action = 'created';
issueNumber = issue.number;
issueUrl = issue.html_url;
}
const testName = result.match?.canonicalTestName ?? result.match?.displayTestName ?? '';
let disableHint = '';
if (testName) {
disableHint = `\n\nTo disable this test on your PR, comment:\n\`\`\`\n/disable-test ${testName} ${issueUrl}\n\`\`\``;
}
await postComment(`✅ ${action[0].toUpperCase()}${action.slice(1)} failing-test issue #${issueNumber}: ${issueUrl}${disableHint}`);
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' || steps.verify-permission.outcome == 'skipped')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
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 = `❌ 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);
}