diff --git a/.github/workflows/patch-coverage-comment.yaml b/.github/workflows/patch-coverage-comment.yaml
new file mode 100644
index 000000000..e3c1f4268
--- /dev/null
+++ b/.github/workflows/patch-coverage-comment.yaml
@@ -0,0 +1,117 @@
+name: Patch Coverage Comment
+on:
+ workflow_run:
+ workflows: ["Patch Coverage (tests only)"] # must match the name in patch-coverage.yaml
+ types: [completed]
+
+permissions:
+ contents: read
+
+jobs:
+ comment:
+ if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion != 'cancelled' }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ actions: read # needed to download artifacts from the run
+ pull-requests: write # needed to post comment
+ steps:
+ - name: Download artifacts from triggering run
+ uses: actions/github-script@v7
+ id: dl
+ with:
+ script: |
+ const { owner, repo } = context.repo;
+ const run_id = context.payload.workflow_run.id;
+
+ // List artifacts
+ const arts = await github.rest.actions.listWorkflowRunArtifacts({ owner, repo, run_id });
+ if (!arts.data.artifacts.length) {
+ core.setOutput('found', 'false');
+ return;
+ }
+ core.setOutput('found', 'true');
+
+ const fs = require('fs');
+ const AdmZip = require('adm-zip');
+
+ // Download and extract all artifacts
+ for (const art of arts.data.artifacts) {
+ const zip = await github.rest.actions.downloadArtifact({
+ owner, repo, artifact_id: art.id, archive_format: 'zip'
+ });
+ fs.writeFileSync(`${art.name}.zip`, Buffer.from(zip.data));
+ const z = new AdmZip(`${art.name}.zip`);
+ z.extractAllTo('./artifacts', true);
+ }
+
+ - name: Prepare message
+ if: steps.dl.outputs.found == 'true'
+ id: prep
+ run: |
+ UNIT_FILE=$(ls artifacts/**/unit/diff_coverage.txt 2>/dev/null | head -n1 || true)
+ INT_FILE=$(ls artifacts/**/integration/diff_coverage.txt 2>/dev/null | head -n1 || true)
+ ALL_FILE=$(ls artifacts/**/overall/diff_coverage.txt 2>/dev/null | head -n1 || true)
+
+ unit_text="— no unit tests detected —"
+ [ -f "$UNIT_FILE" ] && unit_text="$(cat "$UNIT_FILE")"
+
+ int_text="— no integration tests detected —"
+ [ -f "$INT_FILE" ] && int_text="$(cat "$INT_FILE")"
+
+ all_text="— n/a —"
+ [ -f "$ALL_FILE" ] && all_text="$(cat "$ALL_FILE")"
+
+ # Save a markdown comment body
+ cat > comment.md <<'EOF'
+ ## Patch coverage (informational)
+
+ Unit details
+
+ ```
+ UNIT_TEXT
+ ```
+
+
+
+ Integration details
+
+ ```
+ INT_TEXT
+ ```
+
+
+
+ Overall details
+
+ ```
+ ALL_TEXT
+ ```
+
+
+
+ Download the full HTML diff reports from the workflow artifacts.
+ EOF
+
+ # Replace placeholders
+ sed -i "s|UNIT_TEXT|$(printf "%s" "$unit_text" | sed 's/[&/\]/\\&/g')|g" comment.md
+ sed -i "s|INT_TEXT|$(printf "%s" "$int_text" | sed 's/[&/\]/\\&/g')|g" comment.md
+ sed -i "s|ALL_TEXT|$(printf "%s" "$all_text" | sed 's/[&/\]/\\&/g')|g" comment.md
+
+ - name: Find PR number
+ id: findpr
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const { owner, repo } = context.repo;
+ const sha = context.payload.workflow_run.head_sha;
+ const prs = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner, repo, commit_sha: sha });
+ if (!prs.data.length) core.setFailed('No PR found for this run');
+ core.setOutput('pr', prs.data[0].number.toString());
+
+ - name: Post sticky PR comment
+ uses: marocchino/sticky-pull-request-comment@v2
+ with:
+ header: patch-coverage
+ number: ${{ steps.findpr.outputs.pr }}
+ path: comment.md
\ No newline at end of file
diff --git a/.github/workflows/patch-coverage.yaml b/.github/workflows/patch-coverage.yaml
new file mode 100644
index 000000000..04e36a923
--- /dev/null
+++ b/.github/workflows/patch-coverage.yaml
@@ -0,0 +1,28 @@
+name: Patch Coverage (tests only)
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+jobs:
+ coverage:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10
+ - run: dotnet restore
+ - run: dotnet build --no-restore -c Release
+ - name: Upload coverage artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: patch-coverage-${{ github.run_id }}
+ path: |
+ coverage/**/*.txt
+ coverage/**/*.html
+ retention-days: 7
\ No newline at end of file