diff --git a/.dagger/scripts/github-hosted.sh b/.dagger/scripts/github-hosted.sh deleted file mode 100644 index 82b2ded..0000000 --- a/.dagger/scripts/github-hosted.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -set -eu - -exec uv run python scripts/release_contract.py github \ - --repository "$3" --sha "$1" --tag "$2" diff --git a/.dagger/src/edgeproc_core/main.py b/.dagger/src/edgeproc_core/main.py index 99e99a0..e1719ae 100644 --- a/.dagger/src/edgeproc_core/main.py +++ b/.dagger/src/edgeproc_core/main.py @@ -1,4 +1,4 @@ -"""EdgeProc Core's complete quality, security, and release-candidate graph.""" +"""EdgeProc Core's composed quality, security, and candidate graph.""" from __future__ import annotations @@ -14,18 +14,11 @@ "ghcr.io/astral-sh/uv:0.11.32@sha256:" "df4cae8f3a96d175e2e5f992e597550000edbe78fdc2594d5cd8de1a217f504c" ) -ACTIONLINT_IMAGE: Final = ( - "rhysd/actionlint:1.7.10@sha256:" - "ef8299f97635c4c30e2298f48f30763ab782a4ad2c95b744649439a039421e36" -) -GITLEAKS_IMAGE: Final = ( - "ghcr.io/gitleaks/gitleaks:v8.29.1@sha256:" - "aa036a2f4bdfe3cc3c55fa4326308efabb4a6be498c883c864fd1d0d5585438a" -) GIT_PACKAGE: Final = "git=1:2.47.3-0+deb13u1" REPOSITORY: Final = "hseshadr/edgeproc-core" REPOSITORY_URL: Final = f"https://github.com/{REPOSITORY}.git" -SHA_LENGTH: Final = 40 +PROJECT_NAME: Final = "edgeproc-core" +CENTRAL_MODULE_SHA: Final = "95c72573fc11ea6732abb7f7fe8b59c7d245d927" SOURCE_EXCLUDES: Final = [ ".git", ".venv", @@ -37,26 +30,71 @@ "**/__pycache__", "dist", ] -GITLEAKS_SNAPSHOT: Final = [ - "gitleaks", - "detect", - "--source", - "/snapshot", - "--no-git", - "--redact", - "--no-banner", -] -GITLEAKS_HISTORY: Final = [ - "gitleaks", - "detect", - "--source", - "/repo", - "--log-opts=--all", - "--redact", - "--no-banner", +CANDIDATE_TAG_CHECK: Final = """ +import json +import os +from pathlib import Path + +manifest = Path('/candidate/artifact/metadata/python-candidate.json') +actual = json.loads(manifest.read_text(encoding='utf-8'))['tag'] +if actual != os.environ['EXPECTED_TAG']: + raise SystemExit('requested tag differs from the verified candidate') +""" +WORK_DIRECTORIES: Final = ["mkdir", "-p", "/opt/venv", "/opt/home", "/opt/model-cache", "/opt/tmp"] +OWNERSHIP_COMMAND: Final = [ + "chown", + "-R", + "65532:65532", + "/opt/venv", + "/opt/home", + "/opt/model-cache", + "/opt/tmp", ] +def _foundation() -> dagger.Foundation: + """Return the exact-SHA generated Foundation dependency.""" + return dag.foundation() + + +def _python_package() -> dagger.PythonPackage: + """Return the exact-SHA generated Python package dependency.""" + return dag.python_package() + + +def _history(commit_sha: str) -> dagger.Directory: + """Return canonical Git metadata for the already-verified commit.""" + return dag.git(REPOSITORY_URL).commit(commit_sha).tree(depth=0, include_tags=True) + + +def _with_history(source: dagger.Directory, commit_sha: str) -> dagger.Directory: + """Overlay the verified caller snapshot onto exact-commit Git metadata.""" + metadata = _history(commit_sha).filter(include=[".git", ".git/**"]) + return metadata.with_directory("/", source) + + +# fmt: off +def _create_candidate( + source: dagger.Directory, token: dagger.Secret, commit_sha: str, + workflow_run_id: str, run_attempt: int, +) -> dagger.PythonPackageCandidate: + return _python_package().candidate( + source, token, REPOSITORY, commit_sha, PROJECT_NAME, + CENTRAL_MODULE_SHA, workflow_run_id, run_attempt, + ) + + +def _verify_candidate( + envelope: dagger.Directory, commit_sha: str, workflow_run_id: str, run_attempt: int, +) -> dagger.Directory: + verified = _python_package().verify_candidate( + envelope, REPOSITORY, commit_sha, PROJECT_NAME, + CENTRAL_MODULE_SHA, workflow_run_id, run_attempt, + ) + return verified.envelope() +# fmt: on + + @object_type class EdgeprocCore: """Run the same typed EdgeProc Core release graph locally and on GitHub.""" @@ -71,203 +109,102 @@ def create(cls, workspace: dagger.Workspace) -> Self: return instance @function - def quality(self) -> dagger.Container: - """Run lint, format, strict typing, Grade A complexity, and 90%+ tests.""" - return self._quality(self._source_with_history(self.source)) + async def quality(self, commit_sha: str) -> dagger.Container: + """Return product quality after exact source binding and the shared guard.""" + complete = await self._verified_source(self.source, commit_sha) + return self._quality(complete) @function - def dependency_audit(self) -> dagger.Container: - """Audit the exact frozen dependency graph without suppressions.""" - return self._dependency_audit(self.source) - - def _dependency_audit(self, source: dagger.Directory) -> dagger.Container: - export = [ - "uv", - "export", - "--frozen", - "--all-extras", - "--no-emit-project", - "--no-hashes", - "-o", - "/opt/tmp/audit.txt", - ] - audit = [ - "uv", - "run", - "pip-audit", - "-r", - "/opt/tmp/audit.txt", - "--disable-pip", - "--no-deps", - ] - return self._python(source).with_exec(export).with_exec(audit) - - @function - def secret_scan(self, commit_sha: str = "") -> dagger.Container: - """Scan the exact snapshot and complete canonical history with Gitleaks.""" - return self._secret_scan(self.source, commit_sha) - - def _secret_scan(self, source: dagger.Directory, commit_sha: str = "") -> dagger.Container: - if commit_sha: - self._require_sha(commit_sha) - history = dag.git(REPOSITORY_URL).commit(commit_sha).tree(depth=0, include_tags=True) - else: - history = dag.git(REPOSITORY_URL).branch("main").tree(depth=0, include_tags=True) - scan = self._gitleaks().with_directory("/snapshot", source) - scan = scan.with_exec(["sh", "-ceu", 'test -n "$(find /snapshot -type f -print -quit)"']) - scan = scan.with_exec(GITLEAKS_SNAPSHOT).with_directory("/repo", history) - return scan.with_exec(GITLEAKS_HISTORY) - - @function - def workflow_security(self) -> dagger.Container: - """Validate every GitHub ingress workflow with pinned actionlint.""" - return self._workflow_security(self.source) - - def _workflow_security(self, source: dagger.Directory) -> dagger.Container: - workflows = source.directory(".github/workflows") - command = ( - "find .github/workflows -type f " - "\\( -name '*.yml' -o -name '*.yaml' \\) -exec actionlint {} +" - ) - return ( - self._actionlint() - .with_directory("/repo/.github/workflows", workflows) - .with_exec(["sh", "-ceu", command]) - ) + def dependency_audit(self, commit_sha: str) -> dagger.Container: + """Audit the bound frozen graph through the shared Python package Lego.""" + return self._dependency_audit(commit_sha) @function @check - async def ci(self, commit_sha: str = "") -> str: + async def ci(self, commit_sha: str) -> str: """Run the canonical release gate sequentially to bound runner memory.""" - await self._run_ci(self.source, commit_sha) + complete = await self._verified_source(self.source, commit_sha) + await self._run_product_gate(complete) + await self._dependency_audit(commit_sha).sync() return "EdgeProc Core canonical Dagger gate passed" - async def _run_ci(self, source: dagger.Directory, commit_sha: str = "") -> None: - complete = self._source_with_history(source, commit_sha) - tested = self._quality(complete).with_exec(["bash", "examples/run_loop.sh"]) - await tested.with_exec(["uv", "run", "python", "benchmarks/benchmark.py"]).sync() - await self._dependency_audit(complete).sync() - await self._secret_scan(source, commit_sha).sync() - await self._workflow_security(complete).sync() + def _dependency_audit(self, commit_sha: str) -> dagger.Container: + return _python_package().dependency_audit(self.source, REPOSITORY, commit_sha) - def _source_with_history( - self, source: dagger.Directory, commit_sha: str = "" - ) -> dagger.Directory: - history = self._release_source(commit_sha) if commit_sha else self._main_source() - git_metadata = history.filter(include=[".git", ".git/**"]) - return git_metadata.with_directory("/", source) - - @function + # fmt: off + @function(cache="never") # type: ignore[call-overload,untyped-decorator] # SDK stub gap async def release_candidate( - self, tag: str, commit_sha: str, github_token: dagger.Secret + self, tag: str, commit_sha: str, github_token: dagger.Secret, ) -> dagger.Directory: - """Build one exact, Dagger-proven candidate without publishing it.""" - self._require_sha(commit_sha) - await self._hosted(commit_sha, tag, github_token).sync() - source = self._release_source(commit_sha) - await self._identity(source, tag).sync() - await self._run_ci(source, commit_sha) - return self._candidate(source, tag).directory("/candidate") - - def _identity(self, source: dagger.Directory, tag: str) -> dagger.Container: - command = [ - "uv", - "run", - "python", - "scripts/release_contract.py", - "identity", - "--root", - ".", - "--tag", - tag, - ] - return self._python(source).with_exec(command) - - def _candidate(self, source: dagger.Directory, tag: str) -> dagger.Container: - built = self._python(source).with_exec( - ["uv", "build", "--no-build-isolation", "--out-dir", "dist"] + """Build one exact, verified Foundation envelope without publishing it.""" + complete = await self._verified_source(self.source, commit_sha) + await self._run_product_gate(complete) + run_id, attempt = await self._green_identity(github_token) + envelope = self._candidate_envelope( + self.source, github_token, commit_sha, run_id, attempt, ) - built = built.with_exec(self._distribution_command(tag)) - built = built.with_exec(self._checksum_command()) - copy = "mkdir /candidate && cp -R dist /candidate/dist && cp SHA256SUMS /candidate/" - return built.with_exec(["sh", "-ceu", copy]) + checked = self._require_requested_tag(envelope, tag) + return checked.directory("artifact") + # fmt: on - @staticmethod - def _release_source(commit_sha: str) -> dagger.Directory: - return dag.git(REPOSITORY_URL).commit(commit_sha).tree(depth=0, include_tags=True) + async def _run_product_gate(self, source: dagger.Directory) -> None: + tested = self._quality(source).with_exec(["bash", "examples/run_loop.sh"]) + benchmark = tested.with_exec(["uv", "run", "python", "benchmarks/benchmark.py"]) + await benchmark.sync() @staticmethod - def _main_source() -> dagger.Directory: - return dag.git(REPOSITORY_URL).branch("main").tree(depth=0, include_tags=True) + async def _verified_source(source: dagger.Directory, commit_sha: str) -> dagger.Directory: + foundation = _foundation() + bound = foundation.source(source, REPOSITORY, commit_sha) + await foundation.guard(source, REPOSITORY, commit_sha).sync() + return _with_history(bound, commit_sha) @staticmethod - def _distribution_command(tag: str) -> list[str]: - return [ - "uv", - "run", - "python", - "scripts/release_contract.py", - "distributions", - "--root", - ".", - "--dist", - "dist", - "--tag", - tag, - ] + async def _green_identity(token: dagger.Secret) -> tuple[str, int]: + evidence = _foundation().green_main(token, REPOSITORY) + run_id = await evidence.workflow_run_id() + attempt = await evidence.run_attempt() + return run_id, attempt @staticmethod - def _checksum_command() -> list[str]: - return [ - "uv", - "run", - "python", - "scripts/release_contract.py", - "checksums", - "--dist", - "dist", - "--output", - "SHA256SUMS", - ] + def _candidate_envelope( + source: dagger.Directory, + token: dagger.Secret, + commit_sha: str, + workflow_run_id: str, + run_attempt: int, + ) -> dagger.Directory: + candidate = _create_candidate(source, token, commit_sha, workflow_run_id, run_attempt) + return _verify_candidate(candidate.envelope(), commit_sha, workflow_run_id, run_attempt) - def _hosted(self, commit: str, tag: str, token: dagger.Secret) -> dagger.Container: - container = self._python(self.source).with_secret_variable("GITHUB_TOKEN", token) - return container.with_exec( - ["sh", ".dagger/scripts/github-hosted.sh", commit, tag, REPOSITORY] - ) + @staticmethod + def _require_requested_tag(envelope: dagger.Directory, tag: str) -> dagger.Directory: + checked = dag.container().from_(PYTHON_IMAGE).with_directory("/candidate", envelope) + checked = checked.with_env_variable("EXPECTED_TAG", tag) + return checked.with_exec(["python", "-c", CANDIDATE_TAG_CHECK]).directory("/candidate") def _python(self, source: dagger.Directory) -> dagger.Container: - base = ( - self._python_toolchain() - .with_directory("/src", source, owner="65532:65532") - .with_workdir("/src") + configured = self._configured_python(source) + prepared = self._prepared_python(configured) + return prepared.with_user("65532:65532").with_exec( + ["uv", "sync", "--frozen", "--all-extras"] ) - base = base.with_env_variable("UV_PROJECT_ENVIRONMENT", "/opt/venv") + + def _configured_python(self, source: dagger.Directory) -> dagger.Container: + base = self._python_toolchain().with_directory("/src", source, owner="65532:65532") + base = base.with_workdir("/src").with_env_variable("UV_PROJECT_ENVIRONMENT", "/opt/venv") base = base.with_env_variable("UV_CACHE_DIR", "/opt/uv-cache") - base = base.with_env_variable("UV_LINK_MODE", "copy") - base = base.with_env_variable("HOME", "/opt/home") + base = base.with_env_variable("UV_LINK_MODE", "copy").with_env_variable("HOME", "/opt/home") base = base.with_env_variable("XDG_CACHE_HOME", "/opt/model-cache") - base = base.with_env_variable("HF_HOME", "/opt/model-cache/huggingface") + return base.with_env_variable("HF_HOME", "/opt/model-cache/huggingface") + + @staticmethod + def _prepared_python(base: dagger.Container) -> dagger.Container: base = base.with_env_variable("TMPDIR", "/opt/tmp") - base = base.with_mounted_cache( - "/opt/uv-cache", dag.cache_volume("edgeproc-core-uv-nonroot"), owner="65532:65532" - ) - base = base.with_exec( - ["mkdir", "-p", "/opt/venv", "/opt/home", "/opt/model-cache", "/opt/tmp"] - ) - base = base.with_exec( - [ - "chown", - "-R", - "65532:65532", - "/opt/venv", - "/opt/home", - "/opt/model-cache", - "/opt/tmp", - ] - ) - unprivileged = base.with_user("65532:65532") - return unprivileged.with_exec(["uv", "sync", "--frozen", "--all-extras"]) + cache = dag.cache_volume("edgeproc-core-uv-nonroot") + base = base.with_mounted_cache("/opt/uv-cache", cache, owner="65532:65532") + base = base.with_exec(WORK_DIRECTORIES) + return base.with_exec(OWNERSHIP_COMMAND) def _quality(self, source: dagger.Directory) -> dagger.Container: return self._python(source).with_exec(["uv", "run", "poe", "gate"]) @@ -284,18 +221,3 @@ def _python_toolchain() -> dagger.Container: ] base = dag.container().from_(PYTHON_IMAGE).with_exec(install) return base.with_file("/usr/local/bin/uv", uv) - - @staticmethod - def _actionlint() -> dagger.Container: - return dag.container().from_(ACTIONLINT_IMAGE).with_entrypoint([]).with_workdir("/repo") - - @staticmethod - def _gitleaks() -> dagger.Container: - return dag.container().from_(GITLEAKS_IMAGE).with_entrypoint([]) - - @staticmethod - def _require_sha(commit: str) -> None: - valid_length = len(commit) == SHA_LENGTH - valid = valid_length and all(character in "0123456789abcdef" for character in commit) - if not valid: - raise ValueError("commit_sha must be a lowercase 40-character Git SHA") diff --git a/.dagger/tests/test_public_contracts.py b/.dagger/tests/test_public_contracts.py index 244729c..0cb2f60 100644 --- a/.dagger/tests/test_public_contracts.py +++ b/.dagger/tests/test_public_contracts.py @@ -1,15 +1,23 @@ -"""Behavioral contracts for EdgeProc Core's typed Dagger release graph.""" +"""Behavioral contracts for EdgeProc Core's composed Dagger release graph.""" from __future__ import annotations +import asyncio import inspect +import json +from pathlib import Path from typing import cast import dagger import pytest +from edgeproc_core import main from edgeproc_core.main import EdgeprocCore +ROOT = Path(__file__).parents[2] +COMMIT_SHA = "a" * 40 +EXPECTED_CENTRAL_SHA = "95c72573fc11ea6732abb7f7fe8b59c7d245d927" + class RecordingWorkspace: """Record the explicit source directory selected by the constructor.""" @@ -22,22 +30,101 @@ def directory(self, path: str, **_options: object) -> dagger.Directory: return cast(dagger.Directory, object()) -class RecordingDirectory: - """Record history filtering and typed-source overlay behavior.""" +class RecordingContainer: + """Record when one lazy Dagger security or audit graph is evaluated.""" def __init__(self) -> None: + self.synced = False + + async def sync(self) -> RecordingContainer: + self.synced = True + return self + + +class RecordingDirectory: + """Record the exact Git metadata overlay applied to one bound snapshot.""" + + def __init__(self, guard: RecordingContainer) -> None: + self.guard = guard self.includes: list[str] = [] - self.overlay: object | None = None + self.overlay: tuple[str, object] | None = None + self.guard_synced_when_filtered = False def filter(self, *, include: list[str]) -> RecordingDirectory: self.includes = include + self.guard_synced_when_filtered = self.guard.synced return self - def with_directory(self, _path: str, source: object) -> RecordingDirectory: - self.overlay = source + def with_directory(self, path: str, directory: object) -> RecordingDirectory: + self.overlay = (path, directory) return self +class RecordingFoundation: + """Record exact Foundation source and guard identities.""" + + def __init__(self) -> None: + self.calls: list[tuple[str, object, str, str]] = [] + self.bound = cast(dagger.Directory, object()) + self.security = RecordingContainer() + + def source( + self, source: dagger.Directory, repository: str, commit_sha: str + ) -> dagger.Directory: + self.calls.append(("source", source, repository, commit_sha)) + return self.bound + + def guard(self, source: dagger.Directory, repository: str, commit_sha: str) -> dagger.Container: + self.calls.append(("guard", source, repository, commit_sha)) + return cast(dagger.Container, self.security) + + +class RecordingCandidate: + """Expose one verified envelope in the generated candidate shape.""" + + def __init__(self, envelope: dagger.Directory) -> None: + self._envelope = envelope + + def envelope(self) -> dagger.Directory: + return self._envelope + + +class RecordingArtifactEnvelope: + """Record projection of one authenticated Foundation artifact subtree.""" + + def __init__(self) -> None: + self.artifact = cast(dagger.Directory, object()) + self.requested: list[str] = [] + + def directory(self, path: str) -> dagger.Directory: + self.requested.append(path) + return self.artifact + + +class RecordingPythonPackage: + """Record the closed reusable package operations selected by the adapter.""" + + def __init__(self) -> None: + self.audit = RecordingContainer() + self.calls: list[tuple[str, tuple[object, ...]]] = [] + self.created = cast(dagger.Directory, object()) + self.verified = cast(dagger.Directory, object()) + + def dependency_audit( + self, source: dagger.Directory, repository: str, commit_sha: str + ) -> dagger.Container: + self.calls.append(("dependency_audit", (source, repository, commit_sha))) + return cast(dagger.Container, self.audit) + + def candidate(self, *arguments: object) -> RecordingCandidate: + self.calls.append(("candidate", arguments)) + return RecordingCandidate(self.created) + + def verify_candidate(self, *arguments: object) -> RecordingCandidate: + self.calls.append(("verify_candidate", arguments)) + return RecordingCandidate(self.verified) + + def test_should_select_explicit_root_when_constructing_release_graph() -> None: # Given workspace = RecordingWorkspace() @@ -61,64 +148,178 @@ def test_should_require_typed_workspace_when_constructing_release_graph() -> Non assert workspace.annotation is dagger.Workspace -def test_should_expose_one_canonical_check_and_release_boundaries() -> None: +def test_should_expose_only_composed_quality_and_release_boundaries() -> None: # Given - expected = { - "ci", - "quality", - "dependency_audit", - "secret_scan", - "workflow_security", - "release_candidate", - } + expected = {"ci", "quality", "dependency_audit", "release_candidate"} # When available = {name for name in expected if hasattr(EdgeprocCore, name)} # Then assert available == expected + assert not hasattr(EdgeprocCore, "secret_scan") + assert not hasattr(EdgeprocCore, "workflow_security") -def test_should_require_typed_secret_for_hosted_release_eligibility() -> None: +def test_should_require_bound_sha_for_every_unprivileged_entrypoint() -> None: + # Given / When + names = ("ci", "quality", "dependency_audit") + signatures = [inspect.signature(getattr(EdgeprocCore, name), eval_str=True) for name in names] + + # Then + assert all( + item.parameters["commit_sha"].default is inspect.Parameter.empty for item in signatures + ) + + +def test_should_bind_snapshot_before_product_quality( + monkeypatch: pytest.MonkeyPatch, +) -> None: # Given - signature = inspect.signature(EdgeprocCore.release_candidate, eval_str=True) + foundation = RecordingFoundation() + history = RecordingDirectory(foundation.security) + requested_history: list[str] = [] + source = cast(dagger.Directory, object()) + monkeypatch.setattr(main, "_foundation", lambda: foundation) + monkeypatch.setattr( + main, + "_history", + lambda commit_sha: requested_history.append(commit_sha) or history, + ) # When - token = signature.parameters.get("github_token") - result = signature.return_annotation + actual = asyncio.run(EdgeprocCore._verified_source(source, COMMIT_SHA)) # Then - assert token is not None - assert token.annotation is dagger.Secret - assert result is dagger.Directory + assert actual is history + assert foundation.security.synced + assert requested_history == [COMMIT_SHA] + assert history.guard_synced_when_filtered + assert history.includes == [".git", ".git/**"] + assert history.overlay == ("/", foundation.bound) + assert foundation.calls == [ + ("source", source, "hseshadr/edgeproc-core", COMMIT_SHA), + ("guard", source, "hseshadr/edgeproc-core", COMMIT_SHA), + ] -def test_should_actionlint_both_github_workflow_extensions() -> None: - # Given / When - command = inspect.getsource(EdgeprocCore._workflow_security) +def test_should_delegate_dependency_audit_to_shared_python_package( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Given + package = RecordingPythonPackage() + source = cast(dagger.Directory, object()) + graph = EdgeprocCore.__new__(EdgeprocCore) + graph.source = source + monkeypatch.setattr(main, "_python_package", lambda: package) + + # When + actual = graph.dependency_audit(COMMIT_SHA) # Then - assert "*.yml" in command - assert "*.yaml" in command + assert actual is package.audit + assert package.calls == [("dependency_audit", (source, "hseshadr/edgeproc-core", COMMIT_SHA))] -def test_should_take_only_git_metadata_from_remote_history( +def test_should_create_then_verify_closed_candidate_with_same_identity( monkeypatch: pytest.MonkeyPatch, ) -> None: - # Given a remote commit with files that may have been deleted in the typed source - history = RecordingDirectory() - source = object() + # Given + package = RecordingPythonPackage() + source = cast(dagger.Directory, object()) + token = cast(dagger.Secret, object()) + monkeypatch.setattr(main, "_python_package", lambda: package) + + # When + actual = EdgeprocCore._candidate_envelope(source, token, COMMIT_SHA, "6100", 2) + + # Then + identity = ( + source, + token, + "hseshadr/edgeproc-core", + COMMIT_SHA, + "edgeproc-core", + EXPECTED_CENTRAL_SHA, + "6100", + 2, + ) + assert package.calls == [ + ("candidate", identity), + ("verify_candidate", (package.created, *identity[2:])), + ] + assert actual is package.verified + + +def test_should_project_authenticated_artifact_into_existing_publisher_shape( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Given + graph = EdgeprocCore.__new__(EdgeprocCore) + graph.source = cast(dagger.Directory, object()) + envelope = RecordingArtifactEnvelope() + checked_tags: list[str] = [] + + async def verified(source: dagger.Directory, _commit_sha: str) -> dagger.Directory: + return source + + async def product_gate(_graph: EdgeprocCore, _source: dagger.Directory) -> None: + return None + + async def green_identity(_token: dagger.Secret) -> tuple[str, int]: + return "6100", 2 + + monkeypatch.setattr(EdgeprocCore, "_verified_source", staticmethod(verified)) + monkeypatch.setattr(EdgeprocCore, "_run_product_gate", product_gate) + monkeypatch.setattr(EdgeprocCore, "_green_identity", staticmethod(green_identity)) monkeypatch.setattr( EdgeprocCore, - "_release_source", - staticmethod(lambda _commit: cast(dagger.Directory, history)), + "_candidate_envelope", + staticmethod(lambda *_arguments: cast(dagger.Directory, envelope)), + ) + monkeypatch.setattr( + EdgeprocCore, + "_require_requested_tag", + staticmethod(lambda value, tag: checked_tags.append(tag) or value), ) - graph = EdgeprocCore.__new__(EdgeprocCore) - # When the exact source is composed with its usable Git history - result = graph._source_with_history(cast(dagger.Directory, source), "a" * 40) + # When + actual = asyncio.run( + graph.release_candidate("v0.4.2", COMMIT_SHA, cast(dagger.Secret, object())) + ) - # Then no remote working-tree file can survive a typed-source deletion - assert history.includes == [".git", ".git/**"] - assert history.overlay is source - assert result is history + # Then + assert checked_tags == ["v0.4.2"] + assert envelope.requested == ["artifact"] + assert actual is envelope.artifact + + +def test_should_pin_both_shared_modules_to_same_reviewed_commit() -> None: + # Given / When + config = json.loads((ROOT / "dagger.json").read_text(encoding="utf-8")) + dependencies = {item["name"]: item for item in config["dependencies"]} + + # Then + assert set(dependencies) == {"foundation", "python-package"} + assert main.CENTRAL_MODULE_SHA == EXPECTED_CENTRAL_SHA + assert {item["pin"] for item in dependencies.values()} == {EXPECTED_CENTRAL_SHA} + assert dependencies["foundation"]["source"].endswith( + f"/modules/portfolio-foundation@{EXPECTED_CENTRAL_SHA}" + ) + assert dependencies["python-package"]["source"].endswith( + f"/modules/python-package@{EXPECTED_CENTRAL_SHA}" + ) + + +def test_should_require_typed_secret_for_hosted_release_eligibility() -> None: + # Given + signature = inspect.signature(EdgeprocCore.release_candidate, eval_str=True) + + # When + token = signature.parameters.get("github_token") + result = signature.return_annotation + + # Then + assert token is not None + assert token.annotation is dagger.Secret + assert result is dagger.Directory diff --git a/.github/workflows/dagger.yml b/.github/workflows/dagger.yml index ecf299d..d38b400 100644 --- a/.github/workflows/dagger.yml +++ b/.github/workflows/dagger.yml @@ -15,9 +15,10 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.sha }} fetch-depth: 0 persist-credentials: false - - uses: dagger/dagger-for-github@496f1b3d8b0d823834c13e67cf8a8e08ca3b9602 # v8.4.0 + - uses: dagger/dagger-for-github@27b130bf0f79a7f6fbbbe0fbca6760dc9bb40a77 # v8.4.1 with: version: "0.21.8" verb: call diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index 4946307..72b5199 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -15,10 +15,11 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.sha }} fetch-depth: 0 persist-credentials: false - - uses: dagger/dagger-for-github@496f1b3d8b0d823834c13e67cf8a8e08ca3b9602 # v8.4.0 + - uses: dagger/dagger-for-github@27b130bf0f79a7f6fbbbe0fbca6760dc9bb40a77 # v8.4.1 with: version: "0.21.8" verb: call - args: dependency-audit + args: dependency-audit --commit-sha=${{ github.sha }} diff --git a/dagger.json b/dagger.json index a6afe2e..ce02826 100644 --- a/dagger.json +++ b/dagger.json @@ -4,5 +4,17 @@ "sdk": { "source": "python" }, + "dependencies": [ + { + "name": "foundation", + "source": "github.com/hseshadr/ci/modules/portfolio-foundation@95c72573fc11ea6732abb7f7fe8b59c7d245d927", + "pin": "95c72573fc11ea6732abb7f7fe8b59c7d245d927" + }, + { + "name": "python-package", + "source": "github.com/hseshadr/ci/modules/python-package@95c72573fc11ea6732abb7f7fe8b59c7d245d927", + "pin": "95c72573fc11ea6732abb7f7fe8b59c7d245d927" + } + ], "source": ".dagger" } diff --git a/tests/test_workflow_security.py b/tests/test_workflow_security.py index 6a52e1d..efa5c2f 100644 --- a/tests/test_workflow_security.py +++ b/tests/test_workflow_security.py @@ -63,6 +63,7 @@ def _assert_thin_dagger(job: Mapping[str, object], args: str) -> None: assert [_action(step) for step in steps] == [CHECKOUT_ACTION, DAGGER_ACTION] assert all(PINNED.fullmatch(str(step.get("uses"))) for step in steps) checkout = _mapping(steps[0].get("with")) + assert checkout.get("ref") == "${{ github.sha }}" invocation = _mapping(steps[1].get("with")) assert checkout.get("fetch-depth") == 0 assert checkout.get("persist-credentials") is False @@ -92,7 +93,10 @@ def test_should_route_pull_request_and_main_ci_only_through_dagger() -> None: def test_should_route_scheduled_dependency_audit_only_through_dagger() -> None: document = _workflow("security-audit.yml") - _assert_thin_dagger(_job(document, "dependency-audit"), "dependency-audit") + _assert_thin_dagger( + _job(document, "dependency-audit"), + "dependency-audit --commit-sha=${{ github.sha }}", + ) def test_should_make_release_manual_and_dagger_proven() -> None: @@ -103,12 +107,16 @@ def test_should_make_release_manual_and_dagger_proven() -> None: assert set(triggers) == {"workflow_dispatch"} assert [_action(step) for step in steps] == [CHECKOUT_ACTION, DAGGER_ACTION, UPLOAD_ACTION] assert all(PINNED.fullmatch(str(step.get("uses"))) for step in steps) + checkout = _mapping(steps[0].get("with")) + assert "ref" not in checkout invocation = _mapping(steps[1].get("with")) assert invocation.get("verb") == "call" assert str(invocation.get("args", "")).startswith("release-candidate ") assert "--commit-sha=${{ github.sha }}" in str(invocation.get("args")) assert "--github-token=env:GITHUB_TOKEN" in str(invocation.get("args")) assert "export --path=release" in str(invocation.get("args")) + upload = _mapping(steps[2].get("with")) + assert upload.get("name") == "edgeproc-core-${{ github.sha }}" def test_should_keep_oidc_publisher_source_free_and_shell_free() -> None: @@ -131,6 +139,7 @@ def test_should_keep_oidc_publisher_source_free_and_shell_free() -> None: assert [_action(step) for step in steps] == [DOWNLOAD_ACTION, PUBLISH_ACTION] assert all("run" not in step for step in steps) download = _mapping(steps[0].get("with")) + assert download.get("name") == "edgeproc-core-${{ github.event.workflow_run.head_sha }}" assert download.get("run-id") == "${{ github.event.workflow_run.id }}" assert download.get("github-token") == "${{ github.token }}" settings = _mapping(steps[1].get("with"))