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
73 changes: 73 additions & 0 deletions .dagger/src/ci/fleet_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@
re.IGNORECASE,
)
DYNAMIC_SECRET_REFERENCE: Final = re.compile(r"secrets\s*\[\s*(?!['\"])")
# Oldest hseshadr/ci commit each central module may be pinned at. Raise a floor in the same PR
# that ships a fix every consumer must run. cloudflare-pages and python-package embed
# portfolio-foundation at their own revision, so the foundation floor covers them too.
REQUIRED_MINIMUM: Final[Mapping[str, str]] = MappingProxyType(
{"portfolio-foundation": "dd19871486588b1582e432b7bc1f2cfffb296340"}
)
DESCENDANT_STATUSES: Final = frozenset(("ahead", "identical"))
APPROVED_PUBLISHER_MODULES: Final = frozenset(("github.com/hseshadr/ci/modules/npm-publisher",))
type Scalar = str | bool | int
type RemoteIdentity = tuple[str, str, str, str]
Expand Down Expand Up @@ -284,6 +291,20 @@ class RepositoryExpectation:
grandfathered_until: date | None = None


@validated_dataclass(config=BOUNDARY_CONFIG)
class PinAncestry:
"""GitHub compare evidence for one central module pin.

``floor_status`` is ``compare/<floor>...<pin>`` and ``main_status`` is
``compare/<pin>...main``; ``unrelated`` records a compare without a common ancestor.
"""

floor: str
pin: str
floor_status: str
main_status: str


@validated_dataclass(config=BOUNDARY_CONFIG)
class RepositorySnapshot:
"""All authoritative source, protection, and integration evidence."""
Expand All @@ -301,6 +322,7 @@ class RepositorySnapshot:
missing_dagger_configs: tuple[str, ...] = Field(default_factory=tuple)
environments: tuple[DeploymentEnvironment, ...] = Field(default_factory=tuple)
repository_secret_names: tuple[str, ...] = Field(default_factory=tuple)
pin_ancestry: tuple[PinAncestry, ...] = Field(default_factory=tuple)


@dataclass(frozen=True)
Expand Down Expand Up @@ -431,6 +453,7 @@ def validate_dagger_graph(
if graph_has_cycle(snapshot.dagger_configs):
findings.append(finding("dagger-dependency-cycle", "dagger.json", "dependency cycle"))
findings.extend(validate_shared_requirement(snapshot.dagger_configs, expectation))
findings.extend(validate_minimum_pins(snapshot.dagger_configs, snapshot.pin_ancestry))
return tuple(findings)


Expand Down Expand Up @@ -1820,3 +1843,53 @@ def validate_control_plane(snapshot: RepositorySnapshot) -> tuple[PolicyFinding,
def allowed_check_apps() -> frozenset[str]:
"""Return execution ownership plus the reviewed advisory-only integration."""
return frozenset(("github-actions", "gitguardian"))


def required_minimum_pins(configs: tuple[DaggerConfig, ...]) -> tuple[tuple[str, str], ...]:
"""Return each distinct (floor, pin) pair that needs ancestry evidence."""
pairs = (floored_pin(config) for config in configs)
return tuple(dict.fromkeys(pair for pair in pairs if pair is not None))


def floored_pin(config: DaggerConfig) -> tuple[str, str] | None:
"""Return (floor, pin) for one exact hseshadr/ci module config with a floor."""
remote = parse_pinned_remote(config.identity)
if remote is None or remote[:2] != ("hseshadr", "ci"):
return None
floor = REQUIRED_MINIMUM.get(remote[2].removeprefix("modules/"))
return None if floor is None else (floor, remote[3])


def validate_minimum_pins(
configs: tuple[DaggerConfig, ...], ancestry: tuple[PinAncestry, ...]
) -> tuple[PolicyFinding, ...]:
"""Require every floored central pin to be on main and descend from its floor."""
evidence = {(item.floor, item.pin): item for item in ancestry}
pairs = required_minimum_pins(configs)
return tuple(
minimum_finding(floor, pin, evidence.get((floor, pin)))
for floor, pin in pairs
if not pin_meets_floor(evidence.get((floor, pin)))
)


def minimum_finding(floor: str, pin: str, item: PinAncestry | None) -> PolicyFinding:
"""Name the stale central pin and the ancestry fact that failed."""
return finding("pin-below-required-minimum", pin, minimum_message(floor, item))


def pin_meets_floor(item: PinAncestry | None) -> bool:
"""Accept only a pin at or after its floor that main also contains."""
if item is None:
return False
return {item.floor_status, item.main_status} <= DESCENDANT_STATUSES


def minimum_message(floor: str, item: PinAncestry | None) -> str:
"""Explain which ancestry fact failed without guessing missing evidence."""
if item is None:
return f"no ancestry evidence against required minimum {floor}"
return (
f"must descend from required minimum {floor} on main "
f"(floor...pin={item.floor_status}, pin...main={item.main_status})"
)
45 changes: 43 additions & 2 deletions .dagger/src/ci/github_fleet.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,22 @@
DaggerConfig,
DaggerDependency,
DeploymentEnvironment,
PinAncestry,
Protection,
RepositorySnapshot,
RequiredCheck,
SourceFile,
local_dependency_path,
parse_pinned_remote,
required_minimum_pins,
)

BOUNDARY_CONFIG: Final = ConfigDict(frozen=True, extra="ignore", strict=True)
WORKFLOW_PREFIX: Final = ".github/workflows/"
MODULE_PREFIXES: Final = (".dagger/src/", "dagger/src/")
HTTP_OK: Final = 200
HTTP_NOT_FOUND: Final = 404
CENTRAL_REPOSITORY: Final = "repos/hseshadr/ci"


@dataclass(frozen=True)
Expand Down Expand Up @@ -155,6 +158,7 @@ class SourceEvidence:
sources: tuple[SourceFile, ...]
configs: tuple[DaggerConfig, ...]
missing: tuple[str, ...]
ancestry: tuple[PinAncestry, ...]


@dataclass(frozen=True)
Expand All @@ -175,6 +179,13 @@ class CommitPayload:
sha: str


@validated_dataclass(config=BOUNDARY_CONFIG)
class ComparePayload:
"""GitHub compare ancestry status (ahead, behind, identical, or diverged)."""

status: str


@validated_dataclass(config=BOUNDARY_CONFIG)
class TreeEntry:
"""One recursive Git tree entry."""
Expand Down Expand Up @@ -380,7 +391,32 @@ def read_source_evidence(
identity = f"github.com/{owner}/{name}@{sha}"
location = ModuleLocation(base, sha, "dagger.json", identity)
configs, missing = read_dagger_graph(transport, location, tree)
return SourceEvidence(name, sha, sources, configs, missing)
ancestry = read_pin_ancestry(transport, configs)
return SourceEvidence(name, sha, sources, configs, missing, ancestry)


def read_pin_ancestry(
transport: GitHubTransport, configs: tuple[DaggerConfig, ...]
) -> tuple[PinAncestry, ...]:
"""Compare every floored central pin against its floor and central main."""
return tuple(
PinAncestry(
floor=floor,
pin=pin,
floor_status=read_compare_status(transport, floor, pin),
main_status=read_compare_status(transport, pin, "main"),
)
for floor, pin in required_minimum_pins(configs)
)


def read_compare_status(transport: GitHubTransport, base: str, head: str) -> str:
"""Return GitHub's ancestry status; a 404 means no common history."""
path = f"{CENTRAL_REPOSITORY}/compare/{base}...{head}?per_page=1"
response = transport.get(path)
if response.status == HTTP_NOT_FOUND:
return "unrelated"
return parse_model(response, path, ComparePayload).status


def assert_main_stable(transport: GitHubTransport, base: str, expected_sha: str) -> None:
Expand All @@ -404,7 +440,11 @@ def read_snapshot_parts(

def read_model[T](transport: GitHubTransport, path: str, model: type[T]) -> T:
"""Validate one successful GitHub response against its exact schema."""
response = transport.get(path)
return parse_model(transport.get(path), path, model)


def parse_model[T](response: HttpResponse, path: str, model: type[T]) -> T:
"""Validate one already-read GitHub response against its exact schema."""
if response.status != HTTP_OK:
raise access_error(path, response.status)
try:
Expand Down Expand Up @@ -751,6 +791,7 @@ def create_snapshot(parts: SnapshotParts, projection: SnapshotProjection) -> Rep
missing_dagger_configs=evidence.missing,
environments=parts.environments,
repository_secret_names=parts.repository_secrets,
pin_ancestry=evidence.ancestry,
)


Expand Down
97 changes: 97 additions & 0 deletions .dagger/tests/test_fleet_minimum_pin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Required-minimum floors for central Dagger module pins."""

from __future__ import annotations

import pytest

from ci.fleet_policy import (
REQUIRED_MINIMUM,
DaggerConfig,
PinAncestry,
validate_minimum_pins,
)

FLOOR = "dd19871486588b1582e432b7bc1f2cfffb296340"
NEWER = "9d491851" + "0" * 32
OLDER = "1963264" + "0" * 33
FOUNDATION = "github.com/hseshadr/ci/modules/portfolio-foundation@"


def _config(identity: str) -> DaggerConfig:
path = identity.partition("hseshadr/ci/")[2].rpartition("@")[0] + "/dagger.json"
return DaggerConfig(identity=identity, path=path, name="shared", engine_version="v0.21.8")


def _codes(pin: str, *ancestry: PinAncestry) -> tuple[str, ...]:
configs = (_config(FOUNDATION + pin),)
return tuple(item.code for item in validate_minimum_pins(configs, ancestry))


def test_should_floor_foundation_at_rerun_skew_fix_when_reviewed() -> None:
# Given the reviewed mandatory fix hseshadr/ci#46 (dd19871)
# Then the floor is that exact literal commit and no other module has one
assert dict(REQUIRED_MINIMUM) == {"portfolio-foundation": FLOOR}


def test_should_accept_pin_when_equal_to_floor() -> None:
# Given a consumer pinned exactly at the floor, which is on main
evidence = PinAncestry(floor=FLOOR, pin=FLOOR, floor_status="identical", main_status="ahead")

# When the floor is evaluated, then no finding is reported
assert _codes(FLOOR, evidence) == ()


def test_should_accept_pin_when_descended_from_floor() -> None:
# Given a consumer pinned at a main commit newer than the floor
evidence = PinAncestry(floor=FLOOR, pin=NEWER, floor_status="ahead", main_status="identical")

# When the floor is evaluated, then no finding is reported
assert _codes(NEWER, evidence) == ()


def test_should_reject_pin_when_older_than_floor() -> None:
# Given a consumer pinned at a main commit that predates the mandatory fix
evidence = PinAncestry(floor=FLOOR, pin=OLDER, floor_status="behind", main_status="ahead")

# Then the stale release gate is a failing finding
assert _codes(OLDER, evidence) == ("pin-below-required-minimum",)


@pytest.mark.parametrize(
("floor_status", "main_status"),
[("ahead", "diverged"), ("diverged", "diverged"), ("unrelated", "unrelated")],
)
def test_should_reject_pin_when_off_main_or_unrelated(floor_status: str, main_status: str) -> None:
# Given a pin on an unmerged branch, a diverged branch, or unrelated history
pin = "e" * 40
evidence = PinAncestry(floor=FLOOR, pin=pin, floor_status=floor_status, main_status=main_status)

# Then it cannot satisfy the floor
assert _codes(pin, evidence) == ("pin-below-required-minimum",)


def test_should_fail_closed_when_ancestry_evidence_is_absent() -> None:
# Given a floored module pin whose ancestry was never read
# Then the missing evidence is itself the finding
assert _codes("e" * 40) == ("pin-below-required-minimum",)


def test_should_ignore_module_when_it_has_no_floor() -> None:
# Given a central module with no reviewed floor and no ancestry evidence
configs = (_config("github.com/hseshadr/ci/modules/unknown-module@" + "e" * 40),)

# Then there is nothing to compare and no finding
assert validate_minimum_pins(configs, ()) == ()


def test_should_ignore_config_when_consumer_owned() -> None:
# Given the consumer's own root config shares the module directory name
config = DaggerConfig(
identity="github.com/hseshadr/example/modules/portfolio-foundation@" + "e" * 40,
path="modules/portfolio-foundation/dagger.json",
name="example",
engine_version="v0.21.8",
)

# Then only hseshadr/ci modules are floored
assert validate_minimum_pins((config,), ()) == ()
32 changes: 32 additions & 0 deletions .dagger/tests/test_fleet_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import pytest

from ci.fleet_policy import (
REQUIRED_MINIMUM,
CheckRun,
DaggerConfig,
DaggerDependency,
DeploymentEnvironment,
PinAncestry,
Protection,
RepositoryExpectation,
RepositorySnapshot,
Expand All @@ -26,6 +28,15 @@
DOWNLOAD = "4" * 40
PYPI = "5" * 40
HEAD_SHA = "${{ github.event.workflow_run.head_sha }}"
CURRENT_PINS = tuple(
PinAncestry(
floor=REQUIRED_MINIMUM["portfolio-foundation"],
pin=pin,
floor_status="ahead",
main_status="ahead",
)
for pin in ("b" * 40, SHA)
)

MODULE = """
@object_type
Expand Down Expand Up @@ -190,6 +201,7 @@ def _snapshot(
check_apps=("github-actions",),
codeql_default_state="not-configured",
legacy_references=(),
pin_ancestry=CURRENT_PINS,
)


Expand Down Expand Up @@ -1761,3 +1773,23 @@ def test_should_grandfather_only_missing_shared_module_until_expiry() -> None:
assert "missing-shared-module" not in active
assert "mutable-action" in active
assert "missing-shared-module" in expired


def test_should_report_stale_foundation_pin_through_repository_contract() -> None:
# Given an otherwise valid consumer whose foundation pin predates the required floor
source = "github.com/hseshadr/ci/modules/portfolio-foundation@" + "b" * 40
stale = PinAncestry(
floor=REQUIRED_MINIMUM["portfolio-foundation"],
pin="b" * 40,
floor_status="behind",
main_status="ahead",
)
snapshot = replace(
_snapshot(INGRESS), dagger_configs=_shared_configs(source), pin_ancestry=(stale,)
)

# When the complete repository contract is evaluated
codes = _shared_codes(snapshot)

# Then the stale release gate is the only failure
assert codes == ("pin-below-required-minimum",)
Loading
Loading