diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5a85d5..9c0eeb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,6 +100,7 @@ jobs: tests/test_decision_engine_replay.py \ tests/test_evaluate_agent_routing.py \ tests/test_gcp_artifact_workflow.py \ + tests/test_gcp_leoclean_nosend_release_workflow.py \ tests/test_capture_vps_canonical_postgres_snapshot.py \ tests/test_gcp_infra_execute_canary.py \ tests/test_gcp_infra_readiness_checker.py \ diff --git a/.github/workflows/gcp-leoclean-nosend-release.yml b/.github/workflows/gcp-leoclean-nosend-release.yml new file mode 100644 index 0000000..a953203 --- /dev/null +++ b/.github/workflows/gcp-leoclean-nosend-release.yml @@ -0,0 +1,284 @@ +name: gcp-leoclean-nosend-release + +on: + workflow_dispatch: + +permissions: + contents: read + id-token: write + +concurrency: + group: gcp-leoclean-nosend-release-main + cancel-in-progress: false + +env: + PROJECT_ID: teleo-501523 + REGION: europe-west6 + ARTIFACT_REPOSITORY: teleo + IMAGE_REPOSITORY: europe-west6-docker.pkg.dev/teleo-501523/teleo/leoclean-nosend-staging + WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github + ARTIFACT_SERVICE_ACCOUNT: sa-artifact-builder@teleo-501523.iam.gserviceaccount.com + +jobs: + publish: + name: Build, smoke, and publish exact no-send release + runs-on: ubuntu-latest + timeout-minutes: 90 + env: + RELEASE_ROOT: ${{ runner.temp }}/leoclean-nosend-release + RUNTIME_ARTIFACT: ${{ runner.temp }}/leoclean-nosend-release/runtime-artifact + RUNTIME_RECEIPT: ${{ runner.temp }}/leoclean-nosend-release/evidence/runtime-receipt.json + OCI_SMOKE_RECEIPT: ${{ runner.temp }}/leoclean-nosend-release/evidence/oci-smoke.json + IMAGE_CONTEXT: ${{ runner.temp }}/leoclean-nosend-release/image-context + BUILD_PUSH_RECEIPT: ${{ runner.temp }}/leoclean-nosend-release/receipts/build-push-receipt.json + RELEASE_BUNDLE: ${{ runner.temp }}/leoclean-nosend-release/final/release-v3 + DOCKER_CONFIG: ${{ runner.temp }}/leoclean-nosend-release/docker-config + steps: + - name: Require manual main dispatch + shell: bash + run: | + set -euo pipefail + test "${GITHUB_EVENT_NAME}" = "workflow_dispatch" + test "${GITHUB_REF}" = "refs/heads/main" + test "${GITHUB_REF_TYPE}" = "branch" + + - name: Check out exact dispatched revision + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.sha }} + + - name: Verify exact clean main revision + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse --verify HEAD)" = "${GITHUB_SHA}" + test "$(git rev-parse --verify refs/remotes/origin/main^{commit})" = "${GITHUB_SHA}" + test -z "$(git status --porcelain=v1 --untracked-files=all)" + + - name: Set up exact Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: "3.11.9" + + - name: Install exact no-send build tools + shell: bash + run: | + set -euo pipefail + python -m pip install --disable-pip-version-check 'uv==0.9.30' 'PyYAML==6.0.3' + test "$(uv --version | awk '{print $2}')" = "0.9.30" + test "$(command -v docker)" = "/usr/bin/docker" + + - name: Verify fixed staging package target + shell: bash + run: | + set -euo pipefail + python - <<'PY' + import os + + from ops import gcp_leoclean_nosend_package as package + + assert package.PROJECT == "teleo-501523" + assert package.PLATFORM == "linux/amd64" + assert os.environ["IMAGE_REPOSITORY"] == package.IMAGE_REPOSITORY + assert package.IMAGE_REPOSITORY == "europe-west6-docker.pkg.dev/teleo-501523/teleo/leoclean-nosend-staging" + PY + + - name: Pull digest-pinned OCI bases + shell: bash + run: | + set -euo pipefail + docker pull --platform=linux/amd64 \ + "ghcr.io/astral-sh/uv:0.9.30@sha256:538e0b39736e7feae937a65983e49d2ab75e1559d35041f9878b7b7e51de91e4" + docker pull --platform=linux/amd64 \ + "docker.io/library/python:3.11.9-slim-bookworm@sha256:8fb099199b9f2d70342674bd9dbccd3ed03a258f26bbd1d556822c6dfc60c317" + docker pull --platform=linux/amd64 \ + "docker.io/library/postgres:16.14-bookworm@sha256:92620daddcd947f8d5ab5ba66e848702fe443d87fed30c4cea8e389fd78dfc55" + + - name: Compile and release-verify exact runtime artifact + shell: bash + run: | + set -euo pipefail + install -d -m 0700 "${RELEASE_ROOT}" "${RELEASE_ROOT}/evidence" + LEOCLEAN_NOSEND_RESULTS_DIR="${RELEASE_ROOT}/evidence" \ + LEOCLEAN_NOSEND_RECEIPT="${RUNTIME_RECEIPT}" \ + LEOCLEAN_NOSEND_ARTIFACT_OUTPUT="${RUNTIME_ARTIFACT}" \ + scripts/run_leoclean_nosend_runtime_canary.sh + + - name: Smoke exact no-send OCI surface + shell: bash + run: | + set -euo pipefail + python scripts/run_gcp_leoclean_nosend_oci_smoke.py \ + --repo-root "${GITHUB_WORKSPACE}" \ + --artifact "${RUNTIME_ARTIFACT}" \ + --runtime-receipt "${RUNTIME_RECEIPT}" \ + --output "${OCI_SMOKE_RECEIPT}" + + - name: Prepare exact candidate context + shell: bash + run: | + set -euo pipefail + identity_contract="${RELEASE_ROOT}/identity-runtime-contract.json" + identity_manifest="${RELEASE_ROOT}/identity-manifest.json" + identity_bundle="${RELEASE_ROOT}/identity" + + python ops/gcp_leoclean_nosend_package.py identity-contract \ + --repo-root "${GITHUB_WORKSPACE}" \ + --artifact "${RUNTIME_ARTIFACT}" \ + --receipt "${RUNTIME_RECEIPT}" \ + --output "${identity_contract}" + python ops/gcp_leoclean_nosend_package.py generate-identity-manifest \ + --repo-root "${GITHUB_WORKSPACE}" \ + --artifact "${RUNTIME_ARTIFACT}" \ + --receipt "${RUNTIME_RECEIPT}" \ + --database-fingerprint fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json \ + --constitution fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json \ + --database-identity fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json \ + --identity-source-root "${GITHUB_WORKSPACE}" \ + --output "${identity_manifest}" + python ops/gcp_leoclean_nosend_package.py compile-identity \ + --repo-root "${GITHUB_WORKSPACE}" \ + --artifact "${RUNTIME_ARTIFACT}" \ + --receipt "${RUNTIME_RECEIPT}" \ + --identity-manifest "${identity_manifest}" \ + --identity-source-root "${GITHUB_WORKSPACE}" \ + --output "${identity_bundle}" + python ops/gcp_leoclean_nosend_package.py prepare \ + --repo-root "${GITHUB_WORKSPACE}" \ + --artifact "${RUNTIME_ARTIFACT}" \ + --receipt "${RUNTIME_RECEIPT}" \ + --identity "${identity_bundle}" \ + --identity-source-root "${GITHUB_WORKSPACE}" \ + --output "${IMAGE_CONTEXT}" + + python - "${OCI_SMOKE_RECEIPT}" "${IMAGE_CONTEXT}/image-input.json" "${GITHUB_SHA}" <<'PY' + import json + import sys + from pathlib import Path + + smoke = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + image_input = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8")) + revision = sys.argv[3] + assert smoke["status"] == "pass" + assert smoke["cleanup"]["status"] == "pass" + assert smoke["teleo_git_head"] == revision + assert image_input["runtime"]["teleo_git_head"] == revision + assert smoke["input_sha256"] == image_input["input_sha256"] + assert smoke["identity_sha256"] == image_input["identity"]["bundle_sha256"] + PY + + - name: Authenticate to Google Cloud through WIF + id: auth + uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed + with: + workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ env.ARTIFACT_SERVICE_ACCOUNT }} + create_credentials_file: true + export_environment_variables: true + + - name: Set up exact gcloud + uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f + with: + version: "550.0.0" + + - name: Require immutable fixed Artifact Registry repository + shell: bash + run: | + set -euo pipefail + repository_readback="${RUNNER_TEMP}/leoclean-nosend-repository.json" + umask 077 + gcloud artifacts repositories describe "${ARTIFACT_REPOSITORY}" \ + --project "${PROJECT_ID}" \ + --location "${REGION}" \ + --format json >"${repository_readback}" + python - "${repository_readback}" <<'PY' + import json + import sys + from pathlib import Path + + repository = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + assert repository["name"] == ( + "projects/teleo-501523/locations/europe-west6/repositories/teleo" + ) + assert repository["format"] == "DOCKER" + assert repository["dockerConfig"]["immutableTags"] is True + PY + rm -f -- "${repository_readback}" + + - name: Configure private Artifact Registry Docker auth + shell: bash + run: | + set -euo pipefail + install -d -m 0700 "${DOCKER_CONFIG}" + gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet + test -f "${DOCKER_CONFIG}/config.json" + test ! -L "${DOCKER_CONFIG}/config.json" + chmod 0600 "${DOCKER_CONFIG}/config.json" + + - name: Build and push one immutable candidate + shell: bash + run: | + set -euo pipefail + install -d -m 0700 "$(dirname "${BUILD_PUSH_RECEIPT}")" + python ops/gcp_leoclean_nosend_package.py build-push \ + --repo-root "${GITHUB_WORKSPACE}" \ + --image-context "${IMAGE_CONTEXT}" \ + --docker-binary /usr/bin/docker \ + --docker-host unix:///var/run/docker.sock \ + --docker-config "${DOCKER_CONFIG}" \ + --output-receipt "${BUILD_PUSH_RECEIPT}" \ + --execute-staging-push + + - name: Finalize and validate release v3 bundle + shell: bash + run: | + set -euo pipefail + install -d -m 0700 "$(dirname "${RELEASE_BUNDLE}")" + python ops/gcp_leoclean_nosend_package.py finalize \ + --image-input "${IMAGE_CONTEXT}/image-input.json" \ + --build-push-receipt "${BUILD_PUSH_RECEIPT}" \ + --docker-binary /usr/bin/docker \ + --docker-host unix:///var/run/docker.sock \ + --docker-config "${DOCKER_CONFIG}" \ + --output-bundle "${RELEASE_BUNDLE}" + python ops/gcp_leoclean_nosend_package.py validate \ + --release "${RELEASE_BUNDLE}/release.json" + + python - "${BUILD_PUSH_RECEIPT}" "${RELEASE_BUNDLE}/release.json" "${OCI_SMOKE_RECEIPT}" "${GITHUB_SHA}" <<'PY' + import json + import sys + from pathlib import Path + + receipt = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + release = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8")) + smoke = json.loads(Path(sys.argv[3]).read_text(encoding="utf-8")) + revision = sys.argv[4] + assert receipt["schema"] == "livingip.leocleanNoSendBuildPushReceipt.v1" + assert release["schema"] == "livingip.leocleanNoSendRelease.v3" + assert release["build_push_receipt"] == receipt + assert release["image_input"]["runtime"]["teleo_git_head"] == revision + assert release["image_input"]["input_sha256"] == smoke["input_sha256"] + assert release["image"]["reference"].startswith( + "europe-west6-docker.pkg.dev/teleo-501523/teleo/leoclean-nosend-staging@sha256:" + ) + PY + + - name: Upload exact build and release evidence + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: leoclean-nosend-release-${{ github.sha }} + path: | + ${{ env.RUNTIME_RECEIPT }} + ${{ env.OCI_SMOKE_RECEIPT }} + ${{ env.BUILD_PUSH_RECEIPT }} + ${{ env.RELEASE_BUNDLE }}/release.json + ${{ env.RELEASE_BUNDLE }}/leoclean-gcp-nosend.service + if-no-files-found: error + retention-days: 30 + + - name: Remove ephemeral Docker authentication + if: always() + shell: bash + run: rm -rf -- "${DOCKER_CONFIG}" diff --git a/tests/test_gcp_leoclean_nosend_release_workflow.py b/tests/test_gcp_leoclean_nosend_release_workflow.py new file mode 100644 index 0000000..2bce3ca --- /dev/null +++ b/tests/test_gcp_leoclean_nosend_release_workflow.py @@ -0,0 +1,146 @@ +from pathlib import Path + +import yaml + +from ops import gcp_leoclean_nosend_package as package + +WORKFLOW = Path(".github/workflows/gcp-leoclean-nosend-release.yml") + + +def load_workflow() -> dict[str, object]: + return yaml.load(WORKFLOW.read_text(encoding="utf-8"), Loader=yaml.BaseLoader) + + +def workflow_step(name: str) -> dict[str, object]: + workflow = load_workflow() + jobs = workflow["jobs"] + assert isinstance(jobs, dict) + publish = jobs["publish"] + assert isinstance(publish, dict) + steps = publish["steps"] + assert isinstance(steps, list) + return next(step for step in steps if step.get("name") == name) + + +def test_release_workflow_is_manual_main_only_and_serialized() -> None: + workflow = load_workflow() + + assert set(workflow["on"]) == {"workflow_dispatch"} + assert workflow["permissions"] == {"contents": "read", "id-token": "write"} + assert workflow["concurrency"] == { + "group": "gcp-leoclean-nosend-release-main", + "cancel-in-progress": "false", + } + guard = workflow_step("Require manual main dispatch")["run"] + assert 'test "${GITHUB_EVENT_NAME}" = "workflow_dispatch"' in guard + assert 'test "${GITHUB_REF}" = "refs/heads/main"' in guard + assert 'test "${GITHUB_REF_TYPE}" = "branch"' in guard + + +def test_release_workflow_checks_out_exact_clean_revision_without_persisted_token() -> None: + step = workflow_step("Check out exact dispatched revision") + + assert step["uses"] == "actions/checkout@11d5960a326750d5838078e36cf38b85af677262" + assert step["with"] == { + "fetch-depth": "0", + "persist-credentials": "false", + "ref": "${{ github.sha }}", + } + revision_check = workflow_step("Verify exact clean main revision")["run"] + assert 'test "$(git rev-parse --verify HEAD)" = "${GITHUB_SHA}"' in revision_check + assert 'test "$(git rev-parse --verify refs/remotes/origin/main^{commit})" = "${GITHUB_SHA}"' in revision_check + assert "git status --porcelain=v1 --untracked-files=all" in revision_check + + +def test_release_workflow_smokes_before_requesting_cloud_identity() -> None: + workflow = WORKFLOW.read_text(encoding="utf-8") + + smoke = workflow.index("Smoke exact no-send OCI surface") + auth = workflow.index("Authenticate to Google Cloud through WIF") + push = workflow.index("Build and push one immutable candidate") + finalize = workflow.index("Finalize and validate release v3 bundle") + upload = workflow.index("Upload exact build and release evidence") + assert smoke < auth < push < finalize < upload + assert "scripts/run_leoclean_nosend_runtime_canary.sh" in workflow + assert "scripts/run_gcp_leoclean_nosend_oci_smoke.py" in workflow + assert 'assert smoke["input_sha256"] == image_input["input_sha256"]' in workflow + assert 'assert smoke["cleanup"]["status"] == "pass"' in workflow + + +def test_release_workflow_uses_exact_wif_artifact_builder_without_repository_secrets() -> None: + workflow = load_workflow() + text = WORKFLOW.read_text(encoding="utf-8") + env = workflow["env"] + + assert env["PROJECT_ID"] == "teleo-501523" + assert env["REGION"] == "europe-west6" + assert env["ARTIFACT_REPOSITORY"] == "teleo" + assert env["IMAGE_REPOSITORY"] == package.IMAGE_REPOSITORY + assert env["WORKLOAD_IDENTITY_PROVIDER"] == ( + "projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github" + ) + assert env["ARTIFACT_SERVICE_ACCOUNT"] == "sa-artifact-builder@teleo-501523.iam.gserviceaccount.com" + assert "${{ secrets." not in text + assert "password" not in text.casefold() + + auth = workflow_step("Authenticate to Google Cloud through WIF") + assert auth["uses"] == "google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed" + assert auth["with"] == { + "workload_identity_provider": "${{ env.WORKLOAD_IDENTITY_PROVIDER }}", + "service_account": "${{ env.ARTIFACT_SERVICE_ACCOUNT }}", + "create_credentials_file": "true", + "export_environment_variables": "true", + } + + +def test_release_workflow_requires_immutable_repository_and_package_owned_candidate_push() -> None: + workflow = WORKFLOW.read_text(encoding="utf-8") + immutable = workflow_step("Require immutable fixed Artifact Registry repository")["run"] + push = workflow_step("Build and push one immutable candidate")["run"] + + assert "gcloud artifacts repositories describe" in immutable + assert 'repository["dockerConfig"]["immutableTags"] is True' in immutable + assert "ops/gcp_leoclean_nosend_package.py build-push" in push + assert "--execute-staging-push" in push + assert "--docker-binary /usr/bin/docker" in push + assert "--docker-host unix:///var/run/docker.sock" in push + assert "docker push" not in workflow + assert "docker tag" not in workflow + assert "candidate-" not in workflow + + +def test_release_workflow_finalizes_v3_and_uploads_only_allowlisted_evidence() -> None: + finalize = workflow_step("Finalize and validate release v3 bundle")["run"] + upload = workflow_step("Upload exact build and release evidence") + paths = upload["with"]["path"].splitlines() + + assert "ops/gcp_leoclean_nosend_package.py finalize" in finalize + assert "ops/gcp_leoclean_nosend_package.py validate" in finalize + assert 'assert release["schema"] == "livingip.leocleanNoSendRelease.v3"' in finalize + assert paths == [ + "${{ env.RUNTIME_RECEIPT }}", + "${{ env.OCI_SMOKE_RECEIPT }}", + "${{ env.BUILD_PUSH_RECEIPT }}", + "${{ env.RELEASE_BUNDLE }}/release.json", + "${{ env.RELEASE_BUNDLE }}/leoclean-gcp-nosend.service", + ] + assert upload["with"]["if-no-files-found"] == "error" + assert upload["with"]["retention-days"] == "30" + assert "*" not in upload["with"]["path"] + assert "google-github-actions" not in upload["with"]["path"] + + +def test_release_workflow_has_no_deployment_or_runtime_mutation_surface() -> None: + workflow = WORKFLOW.read_text(encoding="utf-8").casefold() + forbidden = ( + "--execute-restart", + "install_gcp_leoclean_nosend_release.py", + "gcloud compute", + "gcloud sql", + "gcloud secrets", + "kubectl", + "systemctl", + "terraform apply", + ) + + assert all(marker not in workflow for marker in forbidden)