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
53 changes: 53 additions & 0 deletions .github/workflows/attestation-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Attestation Guard

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

# E2E attestation receipts are append-only: each one records that a branch's
# e2e suite ran, and nothing can regenerate a merged branch's receipt. The
# `e2e-verify` gate only asks whether a branch adds or updates ITS OWN receipt,
# so a diff that deletes fifteen other branches' receipts passes it green
# (#1036 merged exactly that, 28 receipts, fully green). This lane closes that
# hole (#1041, #1043).
#
# Deliberately its own workflow rather than a job in an existing lane: receipts
# live under packages/, plugins/ and internals/, so no single package lane sees
# them all, and changelog-check.yml explicitly filters e2e-attestations out.
# The `**` path filter is the point -- a deletion can ride in any diff, and the
# only reliable trigger is the receipt path itself.
on:
pull_request:
paths:
- '**/e2e-attestations/**'
- 'internals/checks/src/checks/attestation_guard/**'
- '.github/workflows/attestation-guard.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
attestation-guard:
name: Attestation guard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.13"

# Logic (receipt-path detection, bypass-line scanning, the restore
# instructions) lives in internals/checks' attestation_guard check,
# colocated-tested at 100% coverage, per AGENTS.md "CI Workflows".
- name: Fail on any deleted e2e attestation receipt
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: uv run --project internals/checks dirsql-checks attestation-guard
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "uv run python -m pytest tests/e2e -x -q",
"ran_at": 1788570542,
"exit_code": 0,
"commit": "4e22dbe0a374458ef28c6165579000cd551bce63",
"branch": "claude/1043-receipt-deletion-guard"
}
Empty file.
19 changes: 19 additions & 0 deletions internals/checks/src/checks/attestation_guard/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""The attestation-guard check -- repo-only (#1043).

Backs the `dirsql-checks attestation-guard` subcommand: fails a PR that deletes
an e2e attestation receipt. Reads BASE_SHA/HEAD_SHA from the environment (set by
`.github/workflows/attestation-guard.yml`) so the workflow step stays a one-liner.
"""

from __future__ import annotations

import click

from checks.attestation_guard.gate import run


@click.command()
@click.option("--base-sha", envvar="BASE_SHA", required=True)
@click.option("--head-sha", envvar="HEAD_SHA", required=True)
def cli(base_sha: str, head_sha: str) -> None:
raise SystemExit(run(base_sha, head_sha))
34 changes: 34 additions & 0 deletions internals/checks/src/checks/attestation_guard/cli_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Colocated unit tests for the attestation-guard command (isolation -- no `CliRunner`).

Driven through `.callback`; `run` is mocked at its import site in this module.
"""

from unittest import mock

import pytest

from checks.attestation_guard.cli import cli


def test_exits_with_runs_return_code():
with mock.patch("checks.attestation_guard.cli.run", return_value=0) as run:
with pytest.raises(SystemExit) as exc_info:
cli.callback(base_sha="abc", head_sha="def")
run.assert_called_once_with("abc", "def")
assert exc_info.value.code == 0


def test_propagates_a_nonzero_return_code():
with mock.patch("checks.attestation_guard.cli.run", return_value=1) as run:
with pytest.raises(SystemExit) as exc_info:
cli.callback(base_sha="abc", head_sha="def")
run.assert_called_once_with("abc", "def")
assert exc_info.value.code == 1


def test_declares_base_sha_and_head_sha_options_reading_the_matching_envvars():
by_name = {option.name: option for option in cli.params}
assert by_name["base_sha"].envvar == "BASE_SHA"
assert by_name["base_sha"].required is True
assert by_name["head_sha"].envvar == "HEAD_SHA"
assert by_name["head_sha"].required is True
52 changes: 52 additions & 0 deletions internals/checks/src/checks/attestation_guard/decide.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Pure decision logic for the attestation-guard check (#1043).

E2E attestation receipts are append-only by convention
(``agents/reference/e2e-attestation.md``): a merged branch's receipt stays in
the directory as the record that its suite ran. The rule here is therefore
**total** -- any deletion under an ``e2e-attestations/`` directory fails, with
no carve-out for "the branch's own" receipt. A branch that re-attests
*modifies* its receipt; it never deletes one. Slug-keyed ownership would also
mis-handle the real case from #1036, where a merged child branch's receipt
rode in under a different slug and looked foreign.

An ``allow-receipt-deletion: <reason>`` commit-body line is the bypass,
mirroring changelog-gate's ``skip-changelog:``. The reason is mandatory so a
bare word cannot disarm the gate by accident.
"""

from __future__ import annotations

import re

# Any file inside a directory named `e2e-attestations`, at any depth, so a new
# package's receipt directory is covered the day it is created.
_RECEIPT = re.compile(r"(?:.+/)?e2e-attestations/.+")

# Scanned over raw commit bodies rather than git's trailer parser, matching
# changelog-gate: a blank line must not split the bypass off the message.
_ALLOW = re.compile(r"(?im)^[ \t]*allow-receipt-deletion:[ \t]*\S.*$")

# Deliberately loose, so a bypass that was attempted but misspelled (wrong
# verb, wrong noun, pluralised, reason omitted) is named instead of ignored.
_NEAR = re.compile(
r"(?im)^[ \t]*(?:skip|allow)[-_ ]?(?:receipt|attestation)s?[-_ ]?deletions?[ \t]*:?.*$"
)


def deleted_receipts(deleted) -> list[str]:
"""Sorted paths the diff deletes from an ``e2e-attestations/`` directory."""
return sorted(path for path in deleted if _RECEIPT.fullmatch(path))


def has_allow_line(commit_messages: str) -> bool:
"""True if a commit body carries ``allow-receipt-deletion: <reason>``."""
return bool(_ALLOW.search(commit_messages))


def near_miss_lines(commit_messages: str) -> list[str]:
"""Bypass-shaped commit lines that are not the accepted spelling."""
return [
line.strip()
for line in _NEAR.findall(commit_messages)
if not _ALLOW.fullmatch(line)
]
91 changes: 91 additions & 0 deletions internals/checks/src/checks/attestation_guard/decide_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from checks.attestation_guard.decide import (
deleted_receipts,
has_allow_line,
near_miss_lines,
)


def describe_deleted_receipts():
def keeps_only_paths_under_an_e2e_attestations_directory():
assert deleted_receipts(
[
"packages/python/e2e-attestations/claude-a.json",
"packages/python/dirsql/core.py",
"README.md",
]
) == ["packages/python/e2e-attestations/claude-a.json"]

def spans_every_package_root_that_holds_receipts():
paths = [
"internals/checks/e2e-attestations/a.json",
"plugins/dirsql-plugin-embeddings/e2e-attestations/b.json",
"packages/ts/e2e-attestations/c.json",
]
assert deleted_receipts(paths) == sorted(paths)

def sorts_the_result():
assert deleted_receipts(
["packages/ts/e2e-attestations/b.json", "packages/ts/e2e-attestations/a.json"]
) == [
"packages/ts/e2e-attestations/a.json",
"packages/ts/e2e-attestations/b.json",
]

def ignores_the_directory_entry_itself():
assert deleted_receipts(["packages/ts/e2e-attestations/"]) == []

def ignores_a_lookalike_directory_name():
assert deleted_receipts(["packages/ts/e2e-attestations-old/a.json"]) == []

def returns_nothing_for_an_empty_diff():
assert deleted_receipts([]) == []


def describe_has_allow_line():
def accepts_the_bypass_with_a_reason():
assert has_allow_line("chore: x\n\nallow-receipt-deletion: retiring pkg") is True

def accepts_it_regardless_of_case():
assert has_allow_line("Allow-Receipt-Deletion: retiring pkg") is True

def accepts_it_from_any_line_not_only_a_formal_trailer():
body = "allow-receipt-deletion: retiring pkg\n\nchore: x\n"
assert has_allow_line(body) is True

def rejects_the_bypass_with_no_reason():
assert has_allow_line("chore: x\n\nallow-receipt-deletion:") is False

def rejects_the_bypass_with_only_whitespace_for_a_reason():
assert has_allow_line("chore: x\n\nallow-receipt-deletion: ") is False

def rejects_an_ordinary_message():
assert has_allow_line("fix: delete a stale thing\n") is False


def describe_near_miss_lines():
def names_a_bypass_spelled_with_the_wrong_verb():
assert near_miss_lines("skip-receipt-deletion: cleanup") == [
"skip-receipt-deletion: cleanup"
]

def names_a_bypass_spelled_with_the_wrong_noun():
assert near_miss_lines("allow-attestation-deletion: cleanup") == [
"allow-attestation-deletion: cleanup"
]

def names_a_bypass_missing_its_reason():
assert near_miss_lines("allow-receipt-deletion:") == ["allow-receipt-deletion:"]

def names_a_bypass_missing_its_colon():
assert near_miss_lines("allow-receipt-deletion cleanup") == [
"allow-receipt-deletion cleanup"
]

def does_not_name_the_accepted_spelling():
assert near_miss_lines("allow-receipt-deletion: retiring pkg") == []

def strips_surrounding_whitespace():
assert near_miss_lines(" skip-receipt-deletion: ") == ["skip-receipt-deletion:"]

def returns_nothing_for_an_ordinary_message():
assert near_miss_lines("fix: a thing\n") == []
38 changes: 38 additions & 0 deletions internals/checks/src/checks/attestation_guard/gate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Orchestration for the attestation-guard check (#1043).

Fails a PR whose `base...HEAD` diff deletes any e2e attestation receipt. The
git plumbing is injected so the orchestration unit-tests without a real repo.
"""

from __future__ import annotations

from checks.attestation_guard import git_ops
from checks.attestation_guard.decide import (
deleted_receipts,
has_allow_line,
near_miss_lines,
)
from checks.attestation_guard.report import report


def verdict(deleted, messages: str, base_sha: str) -> int:
"""0 when a well-formed bypass line is present, else 1 with diagnostics."""
if has_allow_line(messages):
print("allow-receipt-deletion line present; permitting receipt deletion.")
return 0
report(deleted, near_miss_lines(messages), base_sha)
return 1


def run(
base_sha: str,
head_sha: str,
*,
deleted_files=git_ops.deleted_files,
commit_messages=git_ops.commit_messages,
) -> int:
deleted = deleted_receipts(deleted_files(base_sha, head_sha))
if not deleted:
print("No e2e attestation receipts deleted.")
return 0
return verdict(deleted, commit_messages(base_sha, head_sha), base_sha)
94 changes: 94 additions & 0 deletions internals/checks/src/checks/attestation_guard/gate_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from unittest import mock

import pytest

from checks.attestation_guard.gate import run, verdict


def _run(deleted, *, messages="fix: a thing\n"):
return run(
"base",
"head",
deleted_files=mock.Mock(return_value=deleted),
commit_messages=mock.Mock(return_value=messages),
)


def describe_run():
def passes_when_the_diff_deletes_nothing(capsys):
assert _run([]) == 0
assert "No e2e attestation receipts deleted." in capsys.readouterr().out

def passes_when_the_diff_deletes_no_receipt(capsys):
assert _run(["packages/python/dirsql/core.py"]) == 0
assert "No e2e attestation receipts deleted." in capsys.readouterr().out

def does_not_read_commit_messages_when_nothing_was_deleted():
messages = mock.Mock()
run(
"base",
"head",
deleted_files=mock.Mock(return_value=[]),
commit_messages=messages,
)
messages.assert_not_called()

def fails_when_the_diff_deletes_a_receipt(capsys):
assert _run(["packages/ts/e2e-attestations/other-branch.json"]) == 1
assert "other-branch.json is an e2e attestation receipt" in capsys.readouterr().out

def names_the_restore_command_with_the_base_sha(capsys):
assert _run(["packages/ts/e2e-attestations/a.json"]) == 1
out = capsys.readouterr().out
assert "git checkout base -- packages/ts/e2e-attestations/a.json" in out

def passes_when_a_bypass_line_is_present(capsys):
rc = _run(
["packages/ts/e2e-attestations/a.json"],
messages="chore: retire pkg\n\nallow-receipt-deletion: package removed",
)
assert rc == 0
assert "permitting receipt deletion" in capsys.readouterr().out

def fails_and_names_a_near_miss_bypass(capsys):
rc = _run(
["packages/ts/e2e-attestations/a.json"],
messages="chore: retire pkg\n\nskip-receipt-deletion: package removed",
)
assert rc == 1
assert "is not the bypass line" in capsys.readouterr().out

def keeps_the_injected_collaborators_keyword_only():
# The `*` in the signature is the seam's contract: a positional third
# argument must not silently bind to `deleted_files`.
with pytest.raises(TypeError):
run("base", "head", mock.Mock(return_value=[]), mock.Mock())

def queries_git_with_the_supplied_shas():
deleted = mock.Mock(return_value=[])
run("b1", "h1", deleted_files=deleted, commit_messages=mock.Mock())
deleted.assert_called_once_with("b1", "h1")

def reads_commit_messages_over_the_same_range():
messages = mock.Mock(return_value="fix: x\n")
run(
"b1",
"h1",
deleted_files=mock.Mock(return_value=["a/e2e-attestations/x.json"]),
commit_messages=messages,
)
messages.assert_called_once_with("b1", "h1")


def describe_verdict():
def reports_every_deleted_receipt(capsys):
rc = verdict(
["a/e2e-attestations/x.json", "b/e2e-attestations/y.json"], "fix: x", "sha"
)
assert rc == 1
assert capsys.readouterr().out.count("::error file=") == 2

def does_not_report_when_bypassed(capsys):
rc = verdict(["a/e2e-attestations/x.json"], "allow-receipt-deletion: why", "sha")
assert rc == 0
assert "::error file=" not in capsys.readouterr().out
Loading