XUnit3 Only External #530
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |