Skip to content

XUnit3 Only External #530

XUnit3 Only External

XUnit3 Only External #530

name: XUnit3 Only External
on:
schedule:
- cron: "0 2,8,14,20 * * *"
push:
paths:
- ".github/workflows/xunit3-pipeline-external.yml"
- "ExternalMerlin.XUnit3/**"
- "ExternalMerlin.XUnit3.RealDevices/**"
branches:
- master
- "releases/**"
- "feat/**"
- "feature/**"
- "fix/**"
- "test/**"
workflow_dispatch:
env:
PROJECT_NAME: Saucery
SOLN_FILE: Saucery.sln
EM_XUNIT3_PROJECT: ExternalMerlin.XUnit3/ExternalMerlin.XUnit3.csproj
EM_RD_XUNIT3_PROJECT: ExternalMerlin.XUnit3.RealDevices/ExternalMerlin.XUnit3.RealDevices.csproj
SAUCE_USER_NAME: ${{ secrets.SAUCE_USER_NAME }}
SAUCE_API_KEY: ${{ secrets.SAUCE_API_KEY }}
CONFIG: Release
# IMPORTANT: use literal block so each glob is its own token
WATCHED_GLOBS: |
.github/workflows/xunit3-pipeline-external.yml
ExternalMerlin.XUnit3/**
ExternalMerlin.XUnit3.RealDevices/**
permissions:
contents: write
jobs:
detect-changes:
name: Detect if branch HEAD changed since last completed run
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
changed: ${{ steps.decide.outputs.changed }}
previous_sha: ${{ steps.decide.outputs.previous_sha }}
paths_changed: ${{ steps.decide.outputs.paths_changed }}
steps:
- name: Resolve last completed run head_sha for this workflow+branch AND check paths
id: decide
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const branch = context.ref.replace('refs/heads/', '');
const workflow_id = ".github/workflows/xunit3-pipeline-external.yml";
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id,
branch,
status: "completed",
per_page: 1
});
const prevSha = runs.data.workflow_runs[0]?.head_sha || "";
const curSha = context.sha;
core.info(`Current SHA: ${curSha}`);
core.info(`Previous SHA: ${prevSha || "<none>"}`);
let changed = "true";
let pathsChanged = "true";
// Robust parse: supports newline-separated (recommended) and/or commas
// and tolerates folded whitespace accidents.
const raw = process.env.WATCHED_GLOBS || "";
const watchedGlobs = raw
.split(/[\n,]+/g)
.map(s => s.trim())
.filter(Boolean)
.flatMap(s => s.split(/\s+/g))
.map(s => s.trim())
.filter(Boolean);
core.info(`WATCHED_GLOBS (parsed): [${watchedGlobs.join(", ")}]`);
function globToRegexSource(pattern) {
let out = "";
for (let i = 0; i < pattern.length; i++) {
const ch = pattern[i];
if (ch === "*") {
const nextIsStar = i + 1 < pattern.length && pattern[i + 1] === "*";
if (nextIsStar) {
out += ".*";
i++;
} else {
out += "[^/]*";
}
} else if (ch === "/") {
out += "\\/";
} else if ("\\.[]{}()+-?^$|".includes(ch)) {
out += "\\" + ch;
} else {
out += ch;
}
}
return out;
}
function matchesGlob(path, pattern) {
const regex = new RegExp("^" + globToRegexSource(pattern) + "$");
return regex.test(path);
}
if (!prevSha) {
core.info("No previous completed run for this workflow+branch. Treating as changed.");
// First ever completed run: always run downstream.
pathsChanged = "true";
changed = "true";
} else if (prevSha === curSha) {
core.info("No new commits since the last completed run on this branch.");
changed = "false";
pathsChanged = "false";
} else {
const comparison = await github.rest.repos.compareCommits({
owner,
repo,
base: prevSha,
head: curSha
});
const files = comparison.data.files || [];
const changedPaths = files.map(f => f.filename);
core.info("Changed files since last run:");
if (changedPaths.length === 0) {
core.info(" (none reported by compareCommits)");
} else {
for (const p of changedPaths) core.info(` - ${p}`);
}
if (watchedGlobs.length === 0) {
core.info("No watched globs defined; treating paths_changed as true.");
pathsChanged = "true";
} else {
const matches = changedPaths.some(path =>
watchedGlobs.some(pattern => matchesGlob(path, pattern))
);
pathsChanged = matches ? "true" : "false";
if (pathsChanged === "true") {
core.info("Detected changes in watched paths. Downstream jobs are eligible to run.");
} else {
core.info("No changes in watched paths. Downstream jobs will be skipped.");
}
}
// CRITICAL FIX: if watched paths didn't change, force overall skip
if (pathsChanged === "false") {
core.info("No watched-path changes detected since previous run; marking overall changed as false.");
changed = "false";
}
}
core.setOutput("changed", changed);
core.setOutput("paths_changed", pathsChanged);
core.setOutput("previous_sha", prevSha);
xunit3-external-tests:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.paths_changed == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Compile solution
run: dotnet build -c ${{ env.CONFIG }}
- name: Run External XUnit3 Integration Tests
run: dotnet run -c ${{ env.CONFIG }} --project ${{ env.EM_XUNIT3_PROJECT }} --no-build
xunit3-real-external-tests:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.paths_changed == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Compile solution
run: dotnet build -c ${{ env.CONFIG }}
- name: Run External XUnit3 Real Integration Tests
run: dotnet run -c ${{ env.CONFIG }} --project ${{ env.EM_RD_XUNIT3_PROJECT }} --no-build
tag:
needs: [detect-changes, xunit3-external-tests, xunit3-real-external-tests]
if: ${{ needs.detect-changes.outputs.paths_changed == 'true' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && success() }}
runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: tag-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Bump version and push tag
id: tag_version
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.ACTIONS_PUSH_PAT }}