Skip to content

Bump Microsoft.NET.Test.Sdk from 18.5.1 to 18.6.0 #9

Bump Microsoft.NET.Test.Sdk from 18.5.1 to 18.6.0

Bump Microsoft.NET.Test.Sdk from 18.5.1 to 18.6.0 #9

Workflow file for this run

name: Bug Reproduction Video
on:
pull_request:
types:
- labeled
workflow_dispatch:
inputs:
issue_number:
description: Issue or PR number to reproduce against
required: true
type: number
require_bug_label:
description: Fail run if target does not have bug label
required: true
default: true
type: boolean
target_ref:
description: Git ref to checkout when issue_number is not a PR
required: false
default: main
type: string
permissions:
contents: read
issues: write
actions: read
pull-requests: read
defaults:
run:
shell: bash
env:
DOTNET_VERSION: 10.0.x
DOTNET_CONFIGURATION: Release
# Set one or both.
SOLUTION_FILE: Clean.Architecture.slnx
PROJECT_FILE: src/Clean.Architecture.Web/Clean.Architecture.Web.csproj
APP_URL: http://127.0.0.1:5010
APP_HEALTH_PATH: /health
APP_STARTUP_COMMAND: dotnet run --project src/Clean.Architecture.Web/Clean.Architecture.Web.csproj --configuration Release --no-build --no-launch-profile
jobs:
reproduce-bug:
# Only run on:
# - workflow_dispatch (maintainer-initiated), OR
# - pull_request labeled 'bug' from a branch in THIS repo (never a fork —
# we build & run PR code, so external PRs would be a code-execution risk).
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
github.event.label.name == 'bug' &&
github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: repro-bug-${{ github.event.pull_request.number || github.event.inputs.issue_number }}
cancel-in-progress: true
steps:
- name: Resolve target context
id: target
# Pinned to v7.0.1 commit SHA for supply-chain safety.
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
with:
script: |
const isDispatch = context.eventName === 'workflow_dispatch';
const inputs = context.payload.inputs || {};
// Truncate untrusted text so it can't blow past env-var size limits
// or overwhelm downstream consumers.
const MAX_LEN = 8000;
const truncate = (s) => {
const v = (s || '').toString();
return v.length > MAX_LEN ? v.slice(0, MAX_LEN) + '\n…[truncated]' : v;
};
let issueNumber;
let issueTitle;
let issueBody;
let issueUrl;
let checkoutRef = context.ref;
let labels = [];
if (isDispatch) {
issueNumber = Number(inputs.issue_number);
if (!Number.isFinite(issueNumber) || issueNumber <= 0) {
core.setFailed('workflow_dispatch requires a valid issue_number input.');
return;
}
const issueResponse = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber
});
const issue = issueResponse.data;
issueTitle = issue.title || '';
issueBody = issue.body || '';
issueUrl = issue.html_url;
labels = (issue.labels || [])
.map((label) => typeof label === 'string' ? label : label.name)
.filter(Boolean);
if (issue.pull_request) {
const prResponse = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: issueNumber
});
// Refuse to build code from a fork via dispatch as well.
const expected = `${context.repo.owner}/${context.repo.repo}`;
if (prResponse.data.head.repo.full_name !== expected) {
core.setFailed(`PR #${issueNumber} is from a fork; refusing to check out and build untrusted code.`);
return;
}
checkoutRef = prResponse.data.head.sha;
} else {
checkoutRef = inputs.target_ref || 'main';
}
const requireBugLabel = String(inputs.require_bug_label).toLowerCase() === 'true';
if (requireBugLabel && !labels.includes('bug')) {
core.setFailed(`Issue/PR #${issueNumber} is missing bug label.`);
return;
}
} else {
const pr = context.payload.pull_request;
issueNumber = pr.number;
issueTitle = pr.title || '';
issueBody = pr.body || '';
issueUrl = pr.html_url;
checkoutRef = pr.head.sha;
labels = (pr.labels || []).map((label) => label.name).filter(Boolean);
if (!labels.includes('bug')) {
core.setFailed(`PR #${issueNumber} is missing bug label.`);
return;
}
}
core.exportVariable('ISSUE_NUMBER', String(issueNumber));
core.exportVariable('ISSUE_TITLE', truncate(issueTitle));
core.exportVariable('ISSUE_BODY', truncate(issueBody));
core.exportVariable('ISSUE_URL', issueUrl);
core.exportVariable('REPO_NAME', context.repo.owner + '/' + context.repo.repo);
core.setOutput('checkout_ref', checkoutRef);
core.setOutput('issue_number', String(issueNumber));
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.target.outputs.checkout_ref }}
# Don't leave the GITHUB_TOKEN in .git/config — we only need to read.
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: 22
- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-v1
- name: Initialize Playwright project
timeout-minutes: 5
run: |
set -euo pipefail
npm init -y >/dev/null
npm install --no-fund --no-audit @playwright/test wait-on
if [ "${{ steps.playwright-cache.outputs.cache-hit }}" = "true" ]; then
npx playwright install-deps
else
npx playwright install --with-deps
fi
- name: Restore .NET dependencies
timeout-minutes: 10
run: |
set -euo pipefail
if [ -n "${SOLUTION_FILE}" ] && [ -f "${SOLUTION_FILE}" ]; then
dotnet restore "${SOLUTION_FILE}"
elif [ -n "${PROJECT_FILE}" ] && [ -f "${PROJECT_FILE}" ]; then
dotnet restore "${PROJECT_FILE}"
else
dotnet restore
fi
- name: Build application
timeout-minutes: 15
run: |
set -euo pipefail
if [ -n "${SOLUTION_FILE}" ] && [ -f "${SOLUTION_FILE}" ]; then
dotnet build "${SOLUTION_FILE}" --configuration "${DOTNET_CONFIGURATION}" --no-restore
elif [ -n "${PROJECT_FILE}" ] && [ -f "${PROJECT_FILE}" ]; then
dotnet build "${PROJECT_FILE}" --configuration "${DOTNET_CONFIGURATION}" --no-restore
else
dotnet build --configuration "${DOTNET_CONFIGURATION}" --no-restore
fi
- name: Start application
run: |
set -euo pipefail
export ASPNETCORE_URLS="${APP_URL}"
export ASPNETCORE_ENVIRONMENT="Development"
# setsid puts the app in its own process group so cleanup can kill
# the whole tree (dotnet often spawns child processes).
setsid bash -c "${APP_STARTUP_COMMAND} > app.log 2>&1" &
echo $! > app.pid
echo "Started app with PGID $(cat app.pid)"
- name: Wait for application startup
timeout-minutes: 3
run: |
set -uo pipefail
# Prefer a real health endpoint over a bare HTTP check; wait-on with
# http-get:// considers only 2xx as ready.
TARGET="http-get://127.0.0.1:5010${APP_HEALTH_PATH}"
if ! npx wait-on "${TARGET}" --timeout 120000 --interval 1000; then
echo "::warning::Health endpoint not ready at ${TARGET}; falling back to base URL."
if ! npx wait-on "http-get://127.0.0.1:5010/" --timeout 30000 --interval 1000; then
echo "::error::Application failed to become ready."
if [ -f app.pid ]; then
echo "Process status:"
ps -p "$(cat app.pid)" -f || true
fi
echo "Last 200 lines of app.log:"
tail -n 200 app.log || true
exit 1
fi
fi
- name: Generate reproduction script
run: |
set -euo pipefail
mkdir -p tests
# NOTE: ISSUE_TITLE / ISSUE_BODY are UNTRUSTED user input.
# Only log them — never pass them to page.goto(), eval(), or shell.
cat << 'EOF' > tests/repro.spec.js
const { test } = require('@playwright/test');
test.use({
video: 'on',
trace: 'on',
screenshot: 'only-on-failure'
});
test('attempt reproduction from github pull request', async ({ page }) => {
await page.goto(process.env.APP_URL, {
waitUntil: 'networkidle'
});
await page.waitForTimeout(5000);
console.log('Issue Title:');
console.log(process.env.ISSUE_TITLE);
console.log('Issue Body:');
console.log(process.env.ISSUE_BODY);
});
EOF
- name: Run Playwright reproduction
timeout-minutes: 10
run: npx playwright test --reporter=list
- name: Upload Playwright artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: repro-artifacts-${{ steps.target.outputs.issue_number }}
path: |
app.log
test-results
playwright-report
if-no-files-found: warn
retention-days: 14
- name: Comment on issue or PR with artifact link
if: always() && env.ISSUE_NUMBER != ''
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const runUrl =
`https://github.com/${context.repo.owner}/${context.repo.repo}` +
`/actions/runs/${context.runId}`;
const body = [
'🤖 Automated reproduction attempt completed.',
'',
'## Target',
`#${process.env.ISSUE_NUMBER}`,
'',
'## Configuration',
`- Solution: \`${process.env.SOLUTION_FILE || '(not set)'}\``,
`- Project: \`${process.env.PROJECT_FILE || '(not set)'}\``,
`- App URL: \`${process.env.APP_URL}\``,
'',
'## Artifacts',
'- 🎥 Playwright video',
'- 📷 Screenshots',
'- 🧭 Trace files',
'- 📄 Console output',
'',
'Download artifacts from the workflow run:',
'',
runUrl
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(process.env.ISSUE_NUMBER),
body
});
- name: Cleanup app
if: always()
run: |
if [ -f app.pid ]; then
PGID="$(cat app.pid)"
# Kill the entire process group started via setsid.
kill -TERM -"${PGID}" 2>/dev/null || true
sleep 2
kill -KILL -"${PGID}" 2>/dev/null || true
fi