When a build fails in a CI/CD pipeline, the first task is to understand what went wrong. Developers usually open the workflow run, inspect the logs, download test reports, and then create an issue to track the problem. If another developer needs to investigate the same failure, they may have to repeat the process.
This becomes harder when a repository has multiple workflows, frequent deployments, and several teams working on the same codebase. A GitHub Issue might describe the failure, but the information needed to reproduce it remains scattered across workflow logs, test reports, and commit history.
GitHub Actions can help automate this process. A workflow can collect relevant logs and test results, upload them as artifacts, and create or update a GitHub Issue with a summary of the failure. The issue becomes a central place for tracking the problem without requiring developers to search through every workflow run.
In this article, we will build a practical workflow that runs tests, saves their results, and creates a GitHub Issue when a failure occurs.
Why Attach Build Logs and Test Results to Issues?
A useful issue should explain what failed, where it failed, and where developers can find the evidence. A message such as "Build failed" is not enough to begin an investigation.
For example, a .NET application might fail because a unit test expects an incorrect value, a dependency cannot be restored, or a compilation error was introduced in the latest commit. Each failure requires different debugging steps.
Automatically collecting the relevant information provides several benefits:
- Developers can start investigating without manually locating the failed workflow.
- Test reports help identify which tests failed and why.
- Artifacts preserve diagnostic files for a defined retention period.
- Issues provide a searchable record of recurring failures.
- Teams can assign, label, and prioritize problems using their existing GitHub workflow.
However, logs and reports should not be copied blindly into issue descriptions. Build logs can be large, and diagnostic output may contain sensitive information. A better approach is to include a concise summary in the issue and attach the full diagnostic files to the workflow run.
Understanding the Workflow Design
The workflow will perform five operations:
- Check out the repository.
- Set up the .NET environment and run tests.
- Generate a test report in TRX format.
- Upload the report as a workflow artifact, including when tests fail.
- Create a GitHub Issue containing the commit, workflow run, and artifact information.
The workflow separates diagnostic collection from issue creation. This matters because a test failure should not prevent the workflow from uploading the available results.
The following example uses a .NET test project. The same design can be adapted for JavaScript, Java, Python, or other environments by changing the test command and report format.
Creating a GitHub Actions Workflow
Create a file named test-and-report.yml in the .github/workflows directory.
The workflow runs when code is pushed to the main branch or a pull request targets that branch.
name: Test and Report
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
permissions:
contents: read
issues: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Run tests
id: run_tests
continue-on-error: true
run: |
mkdir -p TestResults
dotnet test \
--no-restore \
--logger "trx;LogFileName=test-results.trx" \
--results-directory TestResults
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ github.run_id }}
path: TestResults/
if-no-files-found: warn
retention-days: 14
- name: Create issue for test failure
if: steps.run_tests.outcome == 'failure' && github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
COMMIT_SHA: ${{ github.sha }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
gh issue create \
--repo "$REPOSITORY" \
--title "Automated test failure: ${COMMIT_SHA:0:7}" \
--label "bug" \
--body "$(cat <<EOF
## Automated test failure
The test workflow reported a failure.
- Commit: \`$COMMIT_SHA\`
- Workflow run: https://github.com/$REPOSITORY/actions/runs/$RUN_ID/attempts/$RUN_ATTEMPT
- Trigger: ${{ github.event_name }}
- Branch: ${{ github.ref_name }}
### Diagnostic information
Download the test-results artifact from the linked workflow run.
Review the failed test cases and their error messages before changing the application code.
This issue was created automatically by GitHub Actions.
EOF
)"
This example uses the GitHub CLI, which is available on GitHub-hosted runners. The workflow token is provided through the GH_TOKEN environment variable.
The issues: write permission allows the workflow to create issues. The contents: read permission is sufficient for checking out repository content in this example. Keeping permissions narrow reduces the impact of an incorrectly configured workflow.
The example assumes that the repository has a bug label. Create that label first or change the command to use a label that already exists in your repository.
Why use continue-on-error for the test step?
By default, a failed test step causes the job to fail and later steps with ordinary success conditions are skipped.
Setting continue-on-error: true allows subsequent steps to run while preserving the original outcome. The workflow can upload the report and then check whether the test step failed.
The condition:
if: steps.run_tests.outcome == 'failure'
checks the test step's actual result rather than the job's overall result.
The job can still finish with a failure conclusion because the test step failed. This is useful when the workflow must preserve the failure status while performing cleanup and reporting tasks.
The if: always() condition on artifact upload ensures that the upload step is considered even after a previous step fails or is cancelled. It does not guarantee that the runner will remain available or that an artifact will exist. The if-no-files-found: warn option prevents a missing report from turning the upload step into an additional failure.
Understanding Test Result Artifacts
The dotnet test command generates a TRX file through the following logger option:
--logger "trx;LogFileName=test-results.trx"
TRX is an XML-based test result format supported by Microsoft testing tools. It can contain information about test cases, execution outcomes, error messages, and timing.
The --results-directory TestResults option places the generated report in a predictable directory.
The upload step then publishes that directory as a workflow artifact:
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ github.run_id }}
path: TestResults/
if-no-files-found: warn
retention-days: 14
The artifact name includes the workflow run ID so reports from different runs are easy to distinguish.
The retention period is set to 14 days as an example. Choose a period that matches your debugging needs, storage constraints, and organizational retention policy.
Uploading an artifact does not automatically embed its contents in a GitHub Issue. Instead, the issue points developers to the workflow run, where they can download the artifact.
This approach works well for detailed XML reports, test logs, screenshots, and other diagnostic files that would make an issue difficult to read if copied directly into its description.
Attaching Build Logs to the Issue
Test reports are only one part of the investigation. Compilation errors, dependency restoration failures, and build warnings may also be important.
GitHub Actions already stores logs for individual workflow steps. The workflow run URL in the issue gives developers access to those logs without copying the entire output into the issue body.
For example, the following environment variables identify the relevant run:
env:
REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
The issue can then include the run location:
Workflow run:
https://github.com/OWNER/REPOSITORY/actions/runs/RUN_ID/attempts/RUN_ATTEMPT
The URL in the actual issue is generated from the repository and run identifiers.
Including the run attempt is useful when a developer reruns a failed workflow. It helps distinguish the attempt that produced the diagnostic output from a later attempt.
If the workflow also generates application logs or compiler output as files, those files can be uploaded alongside the test report. For example, a build script might save its output to build.log, and the artifact upload step can include both files:
- name: Upload diagnostic files
if: always()
uses: actions/upload-artifact@v4
with:
name: diagnostics-${{ github.run_id }}
path: |
TestResults/
build.log
if-no-files-found: warn
retention-days: 14
Make sure the build script actually creates build.log; otherwise, the workflow will warn that a file is missing.
Avoid uploading unrestricted environment dumps or entire workspace directories. They may contain credentials, configuration files, or unrelated data.
Preventing Duplicate Issues
The example creates a new issue for every qualifying failure on a push or manual run. That is straightforward, but it can create too many issues if the same test fails repeatedly.
A more maintainable approach is to use a stable issue title or identifier and check whether a matching open issue already exists before creating another one.
For example, a workflow could use the GitHub CLI to search open issues with a dedicated label and a stable title:
TITLE="Automated test failure: main branch"
EXISTING_ISSUE=$(gh issue list \
--repo "$REPOSITORY" \
--state open \
--search "\"$TITLE\" in:title" \
--json number,title \
--jq '.[0].number // empty')
if [ -n "$EXISTING_ISSUE" ]; then
echo "An open issue already exists: #$EXISTING_ISSUE"
else
gh issue create \
--repo "$REPOSITORY" \
--title "$TITLE" \
--label "bug" \
--body "Automated tests failed. Review the latest workflow run for diagnostic details."
fi
This is a basic example, not a complete deduplication system. The search query may match similar titles, and concurrent workflow runs can both check for an issue before either creates one.
For more reliable tracking, define what makes two failures equivalent. Possible identifiers include a test name, a normalized error signature, a component, or a workflow name. Store that identifier in the issue body or a machine-readable marker so later runs can find the correct issue.
Also decide how existing issues should be updated. A useful update can add the latest failed run, the affected commit, and the date of the most recent failure. This preserves one issue for a recurring problem while maintaining a history of relevant runs.
Handling Pull Request Failures Safely
The example intentionally avoids creating issues from pull request events:
if: steps.run_tests.outcome == 'failure' && github.event_name != 'pull_request'
For many repositories, a failed pull request is better reported through the pull request's checks and workflow logs. Creating an issue for every failed pull request can generate noise, especially when a developer is still making changes.
There is also a security consideration. Pull request workflows can execute code supplied by contributors. Granting write permissions to a workflow that processes untrusted code can expose the repository to unwanted changes if secrets or tokens are mishandled.
Keep test execution separate from privileged reporting whenever possible. If a workflow needs to create issues based on pull request results, design the reporting stage so that untrusted code cannot access a write-capable token. Use narrowly scoped permissions and review the event model before introducing privileged follow-up workflows.
Do not switch to pull_request_target simply to gain write permissions without understanding its security implications. Running untrusted pull request code in a privileged context can expose repository resources.
Common Mistakes and Troubleshooting
The test report is missing
Confirm that dotnet test runs successfully enough to produce results and that the test project is being discovered.
Check the TestResults directory and verify the actual file path. Projects with multiple test assemblies may generate several reports, so use a suitable filename pattern when necessary.
The issue is not created
Check the step condition, the workflow event, and the repository's Actions settings.
The workflow token must have permission to create issues. If repository or organization policies restrict the token's permissions, the workflow may fail even when issues: write appears in the YAML.
Also verify that the target repository allows Issues and that the configured label exists.
The artifact upload succeeds, but the issue has no report
Uploading an artifact and writing an issue description are separate operations. The example links to the workflow run, where the artifact can be downloaded; it does not attach the artifact directly to the issue.
Keep that distinction clear in the issue template so developers know where to find the files.
Repeated failures create too many issues
Add a deduplication strategy based on a stable failure identifier. For repositories with many concurrent workflows, consider how two runs might attempt to create the same issue at the same time.
Diagnostic files contain sensitive information
Review logs and reports before uploading them. Remove tokens, passwords, connection strings, personal data, and other secrets from generated output.
Artifact access and retention should follow the repository's security requirements. Do not assume that an uploaded file is appropriate for every repository collaborator to access.
Best Practices for Production Workflows
A reporting workflow should make failures easier to investigate without creating extra operational problems.
- Use least-privilege token permissions. Grant issue-writing permission only where it is needed.
- Keep issue descriptions short. Include the failure summary, commit, workflow run, and artifact location.
- Upload diagnostic artifacts even after test failures, but handle missing files gracefully.
- Use predictable artifact names and a retention period aligned with your team's needs.
- Avoid exposing secrets in logs, reports, or issue descriptions.
- Prevent duplicate issues by defining a stable failure identifier.
- Treat pull request code as untrusted unless the workflow's security model proves otherwise.
- Keep the build's failure status intact. Reporting a failure should not turn a failed test run into a successful result.
- Test the reporting path deliberately by introducing a controlled test failure in a safe branch.
Advantages and Limitations
| Approach | Advantages | Limitations |
|---|
| Link to workflow logs | Simple and preserves detailed step output | Developers must open the workflow run |
| Upload test reports as artifacts | Keeps detailed reports available for download | Artifacts have retention and access constraints |
| Summarize failures in an issue | Makes failures searchable and assignable | Poorly designed automation can create duplicate issues |
| Copy full logs into an issue | Keeps selected output visible in one place | Large output is difficult to read and may expose sensitive data |
| Update an existing issue | Supports tracking recurring failures | Requires a reliable method to identify the same problem |
For most repositories, linking to workflow logs and uploading test reports is a good starting point. Add automatic issue creation only when it supports a clear ownership or triage process.
Summary
GitHub Actions can connect build failures to the issue-tracking process by collecting test results, uploading diagnostic artifacts, and creating an issue with links to the relevant workflow run.
The important part is not simply generating an issue whenever a command fails. The automation should preserve the real build status, collect useful evidence, avoid duplicate reports, and keep credentials and diagnostic data secure.
Start with a workflow that uploads test results and makes failed runs easy to locate. Once that process is reliable, add issue creation and deduplication rules that fit the way your team handles recurring failures.
Join the conversation! Your thoughts help the community grow.