Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions .github/workflows/ci-feedback-events.yml
Original file line number Diff line number Diff line change
@@ -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) }}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 23 additions & 4 deletions tests/test_ci_feedback_contract.py
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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)
Expand Down