diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d51dc3d9..27bd7a3f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -34,6 +34,8 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Post Coverage Started Comment + if: github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: header: ${{ matrix.comment_header }} @@ -200,14 +202,6 @@ jobs: RUST_LOG: trace shell: bash - - name: Upload PR Coverage Artifact - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: coverage-pr-${{ matrix.os }} - path: lcov.info - if-no-files-found: ignore - - name: Wait for Exact PR Base Coverage id: wait_for_base_coverage if: always() @@ -246,8 +240,21 @@ jobs: --summary "$GITHUB_STEP_SUMMARY" shell: bash - - name: Post Coverage Comment + - name: Upload PR Coverage Artifact if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-pr-${{ matrix.os }} + path: | + lcov.info + coverage-report.md + if-no-files-found: ignore + + - name: Post Coverage Comment + if: >- + always() && + github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: header: ${{ matrix.comment_header }} diff --git a/.github/workflows/perf-tests.yml b/.github/workflows/perf-tests.yml index aaed25e9..a8f1f7be 100644 --- a/.github/workflows/perf-tests.yml +++ b/.github/workflows/perf-tests.yml @@ -39,7 +39,10 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Post In-Progress Comment - if: github.event_name == 'pull_request' + if: >- + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: header: ${{ matrix.comment_header }} @@ -120,14 +123,6 @@ jobs: cat metrics.json shell: bash - - name: Upload PR Performance Results - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: perf-pr-${{ matrix.os }} - path: metrics.json - if-no-files-found: ignore - - name: Wait for Exact PR Base Performance id: wait_for_base_performance if: always() && github.event_name == 'pull_request' @@ -180,8 +175,21 @@ jobs: --summary "$GITHUB_STEP_SUMMARY" shell: bash + - name: Upload PR Performance Results + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: perf-pr-${{ matrix.os }} + path: | + metrics.json + performance-report.md + if-no-files-found: ignore + - name: Post Performance Comment - if: always() && github.event_name == 'pull_request' + if: >- + always() && github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: header: ${{ matrix.comment_header }} diff --git a/scripts/tests/test_quality_workflows.py b/scripts/tests/test_quality_workflows.py new file mode 100644 index 00000000..ce85d32b --- /dev/null +++ b/scripts/tests/test_quality_workflows.py @@ -0,0 +1,104 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import re +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +SAME_REPOSITORY_PR = ( + "github.event.pull_request.head.repo.full_name == github.repository" +) + + +def workflow_steps(path: str) -> dict[str, str]: + workflow = (ROOT / path).read_text(encoding="utf-8") + chunks = re.split(r"(?m)^ - name: ", workflow)[1:] + return {chunk.splitlines()[0]: chunk for chunk in chunks} + + +def step_property(step: str, name: str) -> str | None: + lines = step.splitlines()[1:] + prefix = f" {name}:" + for index, line in enumerate(lines): + if not line.startswith(prefix): + continue + + value = line.removeprefix(prefix).strip() + if value not in {">", ">-", "|", "|-"}: + return value + + continuation = [] + for next_line in lines[index + 1 :]: + if not next_line.startswith(" "): + break + continuation.append(next_line.strip()) + return " ".join(continuation) + return None + + +class QualityWorkflowTests(unittest.TestCase): + def test_comment_steps_are_fork_safe_and_non_gating(self) -> None: + comment_steps = [] + for path in (ROOT / ".github" / "workflows").glob("*.yml"): + workflow = path.relative_to(ROOT).as_posix() + text = path.read_text(encoding="utf-8") + if "marocchino/sticky-pull-request-comment" not in text: + continue + self.assertNotIn("pull_request_target:", text) + + for step_name, step in workflow_steps(workflow).items(): + if "marocchino/sticky-pull-request-comment" not in step: + continue + comment_steps.append((workflow, step_name)) + with self.subTest(workflow=workflow, step=step_name): + condition = step_property(step, "if") + self.assertIsNotNone(condition) + assert condition is not None + self.assertIn(SAME_REPOSITORY_PR, condition) + self.assertEqual(step_property(step, "continue-on-error"), "true") + + self.assertEqual( + sorted(comment_steps), + sorted( + ( + (".github/workflows/coverage.yml", "Post Coverage Comment"), + (".github/workflows/coverage.yml", "Post Coverage Started Comment"), + (".github/workflows/perf-tests.yml", "Post In-Progress Comment"), + (".github/workflows/perf-tests.yml", "Post Performance Comment"), + ) + ), + ) + + def test_report_artifacts_are_uploaded_after_comparison(self) -> None: + cases = ( + ( + ".github/workflows/coverage.yml", + "Compare Coverage Snapshot", + "Upload PR Coverage Artifact", + "coverage-report.md", + ), + ( + ".github/workflows/perf-tests.yml", + "Compare Performance Snapshot", + "Upload PR Performance Results", + "performance-report.md", + ), + ) + + for workflow, comparison, upload, report in cases: + with self.subTest(workflow=workflow): + text = (ROOT / workflow).read_text(encoding="utf-8") + steps = workflow_steps(workflow) + self.assertLess( + text.index(f"- name: {comparison}"), + text.index(f"- name: {upload}"), + ) + self.assertIn(report, steps[upload]) + self.assertEqual(step_property(steps[upload], "if"), "always()") + self.assertIsNone(step_property(steps[comparison], "continue-on-error")) + + +if __name__ == "__main__": + unittest.main()