diff --git a/.github/workflows/app-ci.yml b/.github/workflows/app-ci.yml index 5995ec817..23be03e58 100644 --- a/.github/workflows/app-ci.yml +++ b/.github/workflows/app-ci.yml @@ -33,6 +33,7 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Set up Python @@ -84,6 +85,7 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Install pnpm @@ -133,3 +135,11 @@ jobs: NARUON_FULL_PRODUCT_BASE_URL: "http://127.0.0.1:3001" NARUON_FULL_PRODUCT_SCREENSHOT_DIR: "/tmp/naruon-full-product-smoke" run: cd frontend && pnpm run full:smoke + + - name: Retain full product smoke PNG evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: naruon-full-product-smoke-${{ github.event.pull_request.number || github.run_id }}-${{ github.event.pull_request.head.sha || github.sha }} + path: /tmp/naruon-full-product-smoke-*/*.png + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml index 0e250389c..81fe5ad42 100644 --- a/.github/workflows/bandit.yml +++ b/.github/workflows/bandit.yml @@ -23,6 +23,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Set up Python diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 21e607f7b..2d968e5b3 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -27,6 +27,7 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Log dependency review policy diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index dd1015812..f69f35e37 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -55,6 +55,7 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Set up QEMU @@ -208,6 +209,7 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Read release version diff --git a/backend/tests/test_application_ci_evidence.py b/backend/tests/test_application_ci_evidence.py new file mode 100644 index 000000000..7d60ce731 --- /dev/null +++ b/backend/tests/test_application_ci_evidence.py @@ -0,0 +1,43 @@ +"""Guard hosted buyer-facing evidence retention in Application CI.""" + +from __future__ import annotations + +from pathlib import Path + +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[2] +APPLICATION_CI_PATH = REPO_ROOT / ".github/workflows/app-ci.yml" +UPLOAD_ARTIFACT_SHA = "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" + + +def test_application_ci_retains_full_product_smoke_screenshot_evidence() -> None: + """A passing browser smoke must publish PR-head-bound PNG evidence instead of discarding it.""" + workflow_text = APPLICATION_CI_PATH.read_text(encoding="utf-8") + workflow = yaml.load(workflow_text, Loader=yaml.BaseLoader) + frontend_steps = workflow["jobs"]["frontend"]["steps"] + + smoke_indices = [ + index + for index, step in enumerate(frontend_steps) + if step.get("name") == "Run full product smoke" + ] + upload_indices = [ + index + for index, step in enumerate(frontend_steps) + if step.get("uses", "").startswith("actions/upload-artifact@") + ] + + assert len(smoke_indices) == 1 + assert len(upload_indices) == 1 + assert upload_indices[0] == smoke_indices[0] + 1 + + upload_step = frontend_steps[upload_indices[0]] + assert upload_step["uses"] == f"actions/upload-artifact@{UPLOAD_ARTIFACT_SHA}" + assert upload_step["with"] == { + "name": "naruon-full-product-smoke-${{ github.event.pull_request.number || github.run_id }}-${{ github.event.pull_request.head.sha || github.sha }}", + "path": "/tmp/naruon-full-product-smoke-*/*.png", + "if-no-files-found": "error", + "retention-days": "14", + } diff --git a/backend/tests/test_repo_local_pr_exact_head.py b/backend/tests/test_repo_local_pr_exact_head.py new file mode 100644 index 000000000..ed2374df5 --- /dev/null +++ b/backend/tests/test_repo_local_pr_exact_head.py @@ -0,0 +1,35 @@ +"""Guard exact-head source checkout in repository-owned pull-request validation.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[2] +PR_VALIDATION_WORKFLOWS = ( + ".github/workflows/app-ci.yml", + ".github/workflows/bandit.yml", + ".github/workflows/dependency-review.yml", + ".github/workflows/docker-publish.yml", +) +EXPECTED_EXACT_REF = "${{ github.event.pull_request.head.sha || github.sha }}" + + +@pytest.mark.parametrize("workflow_path", PR_VALIDATION_WORKFLOWS) +def test_repo_local_validation_checks_out_exact_pr_head(workflow_path: str) -> None: + """Do not report pull-request merge-tree execution as exact-head validation.""" + workflow_text = (REPO_ROOT / workflow_path).read_text(encoding="utf-8") + workflow = yaml.load(workflow_text, Loader=yaml.BaseLoader) + checkout_steps = [ + step + for job in workflow["jobs"].values() + for step in job.get("steps", []) + if step.get("uses", "").startswith("actions/checkout@") + ] + + assert checkout_steps + for checkout_step in checkout_steps: + assert checkout_step.get("with", {}).get("ref") == EXPECTED_EXACT_REF