diff --git a/.github/workflows/ci-feedback-events.yml b/.github/workflows/ci-feedback-events.yml index b5094d0..23db5d6 100644 --- a/.github/workflows/ci-feedback-events.yml +++ b/.github/workflows/ci-feedback-events.yml @@ -1,18 +1,25 @@ name: CI feedback on: - workflow_run: - workflows: [gds-ci] + # Trusted publisher reads completed-run metadata only; source-run code and + # artifacts are never executed. Its code is fixed by the reviewed caller/ref. + workflow_run: # zizmor: ignore[dangerous-triggers] + workflows: ["codeql", "dependabot-projection-convergence", "gds-ci", "platforms", "gds-release-bundle"] types: [completed] -permissions: - actions: read - issues: write +permissions: {} + +concurrency: + group: ci-feedback-events-${{ github.repository_id }}-${{ github.run_id }} + cancel-in-progress: false jobs: feedback: if: ${{ contains(fromJSON('["failure","timed_out","action_required","stale","startup_failure","cancelled"]'), github.event.workflow_run.conclusion) }} - uses: NDDev-OpenNetwork/github-actions/.github/workflows/ci-feedback.yml@a0dc5592333dcfdf2a3d239ed1f2b128899e5482 + permissions: + actions: read # Read exact completed run and job metadata. + issues: write # Publish bounded failure evidence in this repository. + uses: NDDev-OpenNetwork/github-actions/.github/workflows/ci-feedback.yml@a0dc5592333dcfdf2a3d239ed1f2b128899e5482 # commit:a0dc5592333dcfdf2a3d239ed1f2b128899e5482 with: run-id: ${{ format('{0}', github.event.workflow_run.id) }} run-attempt: ${{ format('{0}', github.event.workflow_run.run_attempt) }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c66b660..df7f3e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ Versioning. ## [Unreleased] +- Publish unsuccessful completed self-workflow attempts as unassigned, + repository-local CI evidence; preserve actual conclusions and exact attempts. + + - Reject ineligible module pins and invalid consumer policy before running module verification commands; eligible plans and apply still verify the exact published target and bind that evidence to the transaction. diff --git a/tests/test_ci_feedback_contract.py b/tests/test_ci_feedback_contract.py index e5d4015..bba135e 100644 --- a/tests/test_ci_feedback_contract.py +++ b/tests/test_ci_feedback_contract.py @@ -1,5 +1,8 @@ """Small wiring checks; publisher behavior belongs to its owning module.""" import pathlib +import json +import re +import yaml import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] @@ -9,20 +12,36 @@ class FeedbackContractTests(unittest.TestCase): def test_exact_completed_attempt_is_forwarded(self): text = CALLER.read_text() - self.assertIn("workflows: [gds-ci]", text) + selected = json.loads(re.search(r"workflows: (\[[^\n]+\])", text).group(1)) + expected = [] + for path in sorted((ROOT / ".github/workflows").glob("*.yml")): + if path == CALLER: + continue + document = yaml.safe_load(path.read_text()) + events = document.get("on", document.get(True)) + if isinstance(events, str): + events = [events] + if set(events) - {"workflow_call"}: + expected.append(document.get("name", path.name)) + self.assertEqual(selected, expected) + self.assertNotIn("CI feedback", selected) self.assertIn("types: [completed]", text) self.assertIn("github.event.workflow_run.id", text) self.assertIn("github.event.workflow_run.run_attempt", text) - self.assertNotIn("github.run_id", text) + self.assertNotIn("github.run_id", json.dumps(yaml.safe_load(text)["jobs"]["feedback"]["with"])) def test_reusable_reference_is_immutable(self): text = CALLER.read_text() - self.assertRegex(text, r"uses: NDDev-OpenNetwork/github-actions/\.github/workflows/ci-feedback\.yml@[0-9a-f]{40}\n") + self.assertRegex(text, r"uses: NDDev-OpenNetwork/github-actions/\.github/workflows/ci-feedback\.yml@[0-9a-f]{40}(?: +# commit:[0-9a-f]{40})?\n") self.assertNotIn("@main", text) def test_no_project_execution_or_secret_inheritance(self): text = CALLER.read_text() - self.assertIn(" actions: read\n issues: write", text) + document = yaml.safe_load(text) + self.assertEqual(document["permissions"], {}) + self.assertEqual(document["jobs"]["feedback"]["permissions"], + {"actions": "read", "issues": "write"}) + self.assertNotIn("steps", document["jobs"]["feedback"]) self.assertNotIn("checkout", text) self.assertNotIn("secrets:", text) self.assertNotIn("runs-on:", text)