fix: make no-send release publication repeatable
This commit is contained in:
parent
d36a6d8cc1
commit
7aabfa4ab8
4 changed files with 1408 additions and 117 deletions
|
|
@ -24,15 +24,6 @@ jobs:
|
||||||
name: Build, smoke, and publish exact no-send release
|
name: Build, smoke, and publish exact no-send release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 90
|
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:
|
steps:
|
||||||
- name: Require manual main dispatch
|
- name: Require manual main dispatch
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
@ -41,6 +32,9 @@ jobs:
|
||||||
test "${GITHUB_EVENT_NAME}" = "workflow_dispatch"
|
test "${GITHUB_EVENT_NAME}" = "workflow_dispatch"
|
||||||
test "${GITHUB_REF}" = "refs/heads/main"
|
test "${GITHUB_REF}" = "refs/heads/main"
|
||||||
test "${GITHUB_REF_TYPE}" = "branch"
|
test "${GITHUB_REF_TYPE}" = "branch"
|
||||||
|
test "${GITHUB_REPOSITORY}" = "living-ip/teleo-infrastructure"
|
||||||
|
test "${GITHUB_WORKFLOW_REF}" = \
|
||||||
|
"living-ip/teleo-infrastructure/.github/workflows/gcp-leoclean-nosend-release.yml@refs/heads/main"
|
||||||
|
|
||||||
- name: Check out exact dispatched revision
|
- name: Check out exact dispatched revision
|
||||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||||
|
|
@ -57,6 +51,24 @@ jobs:
|
||||||
test "$(git rev-parse --verify refs/remotes/origin/main^{commit})" = "${GITHUB_SHA}"
|
test "$(git rev-parse --verify refs/remotes/origin/main^{commit})" = "${GITHUB_SHA}"
|
||||||
test -z "$(git status --porcelain=v1 --untracked-files=all)"
|
test -z "$(git status --porcelain=v1 --untracked-files=all)"
|
||||||
|
|
||||||
|
- name: Initialize private release paths
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
release_root="${RUNNER_TEMP}/leoclean-nosend-release"
|
||||||
|
install -d -m 0700 "${release_root}"
|
||||||
|
{
|
||||||
|
echo "RELEASE_ROOT=${release_root}"
|
||||||
|
echo "RUNTIME_ARTIFACT=${release_root}/runtime-artifact"
|
||||||
|
echo "RUNTIME_RECEIPT=${release_root}/evidence/runtime-receipt.json"
|
||||||
|
echo "OCI_SMOKE_RECEIPT=${release_root}/evidence/oci-smoke.json"
|
||||||
|
echo "IMAGE_CONTEXT=${release_root}/image-context"
|
||||||
|
echo "BUILD_PUSH_RECEIPT=${release_root}/receipts/build-push-receipt.json"
|
||||||
|
echo "PUBLISH_OUTCOME=${release_root}/evidence/publish-outcome.json"
|
||||||
|
echo "RELEASE_BUNDLE=${release_root}/final/release-v3"
|
||||||
|
echo "DOCKER_CONFIG=${release_root}/docker-config"
|
||||||
|
} >>"${GITHUB_ENV}"
|
||||||
|
|
||||||
- name: Set up exact Python
|
- name: Set up exact Python
|
||||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||||
with:
|
with:
|
||||||
|
|
@ -169,6 +181,16 @@ jobs:
|
||||||
assert smoke["identity_sha256"] == image_input["identity"]["bundle_sha256"]
|
assert smoke["identity_sha256"] == image_input["identity"]["bundle_sha256"]
|
||||||
PY
|
PY
|
||||||
|
|
||||||
|
- name: Initialize pre-mutation publish outcome
|
||||||
|
id: init_outcome
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
install -d -m 0700 "$(dirname "${PUBLISH_OUTCOME}")"
|
||||||
|
python ops/gcp_leoclean_nosend_package.py init-outcome \
|
||||||
|
--image-input "${IMAGE_CONTEXT}/image-input.json" \
|
||||||
|
--output-journal "${PUBLISH_OUTCOME}"
|
||||||
|
|
||||||
- name: Authenticate to Google Cloud through WIF
|
- name: Authenticate to Google Cloud through WIF
|
||||||
id: auth
|
id: auth
|
||||||
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed
|
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed
|
||||||
|
|
@ -189,6 +211,8 @@ jobs:
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
repository_readback="${RUNNER_TEMP}/leoclean-nosend-repository.json"
|
repository_readback="${RUNNER_TEMP}/leoclean-nosend-repository.json"
|
||||||
umask 077
|
umask 077
|
||||||
|
test "$(gcloud auth list --filter=status:ACTIVE --format='value(account)')" = \
|
||||||
|
"${ARTIFACT_SERVICE_ACCOUNT}"
|
||||||
gcloud artifacts repositories describe "${ARTIFACT_REPOSITORY}" \
|
gcloud artifacts repositories describe "${ARTIFACT_REPOSITORY}" \
|
||||||
--project "${PROJECT_ID}" \
|
--project "${PROJECT_ID}" \
|
||||||
--location "${REGION}" \
|
--location "${REGION}" \
|
||||||
|
|
@ -218,6 +242,7 @@ jobs:
|
||||||
chmod 0600 "${DOCKER_CONFIG}/config.json"
|
chmod 0600 "${DOCKER_CONFIG}/config.json"
|
||||||
|
|
||||||
- name: Build and push one immutable candidate
|
- name: Build and push one immutable candidate
|
||||||
|
id: build_push
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -228,10 +253,13 @@ jobs:
|
||||||
--docker-binary /usr/bin/docker \
|
--docker-binary /usr/bin/docker \
|
||||||
--docker-host unix:///var/run/docker.sock \
|
--docker-host unix:///var/run/docker.sock \
|
||||||
--docker-config "${DOCKER_CONFIG}" \
|
--docker-config "${DOCKER_CONFIG}" \
|
||||||
|
--gcloud-binary "$(command -v gcloud)" \
|
||||||
--output-receipt "${BUILD_PUSH_RECEIPT}" \
|
--output-receipt "${BUILD_PUSH_RECEIPT}" \
|
||||||
|
--outcome-journal "${PUBLISH_OUTCOME}" \
|
||||||
--execute-staging-push
|
--execute-staging-push
|
||||||
|
|
||||||
- name: Finalize and validate release v3 bundle
|
- name: Finalize and validate release v3 bundle
|
||||||
|
id: finalize
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -265,20 +293,41 @@ jobs:
|
||||||
)
|
)
|
||||||
PY
|
PY
|
||||||
|
|
||||||
- name: Upload exact build and release evidence
|
- name: Remove ephemeral Docker authentication
|
||||||
|
id: cleanup
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rm -rf -- "${DOCKER_CONFIG}"
|
||||||
|
test ! -e "${DOCKER_CONFIG}"
|
||||||
|
test ! -L "${DOCKER_CONFIG}"
|
||||||
|
|
||||||
|
- name: Finalize publish outcome
|
||||||
|
if: always() && steps.init_outcome.outcome == 'success'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python ops/gcp_leoclean_nosend_package.py complete-outcome \
|
||||||
|
--image-input "${IMAGE_CONTEXT}/image-input.json" \
|
||||||
|
--outcome-journal "${PUBLISH_OUTCOME}" \
|
||||||
|
--build-push-receipt "${BUILD_PUSH_RECEIPT}" \
|
||||||
|
--release "${RELEASE_BUNDLE}/release.json" \
|
||||||
|
--build-outcome "${{ steps.build_push.outcome }}" \
|
||||||
|
--finalize-outcome "${{ steps.finalize.outcome }}" \
|
||||||
|
--cleanup-outcome "${{ steps.cleanup.outcome }}"
|
||||||
|
|
||||||
|
- name: Upload exact available build and release evidence
|
||||||
|
if: always()
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||||
with:
|
with:
|
||||||
name: leoclean-nosend-release-${{ github.sha }}
|
name: leoclean-nosend-release-${{ github.sha }}
|
||||||
path: |
|
path: |
|
||||||
${{ env.RUNTIME_RECEIPT }}
|
${{ runner.temp }}/leoclean-nosend-release/evidence/runtime-receipt.json
|
||||||
${{ env.OCI_SMOKE_RECEIPT }}
|
${{ runner.temp }}/leoclean-nosend-release/evidence/oci-smoke.json
|
||||||
${{ env.BUILD_PUSH_RECEIPT }}
|
${{ runner.temp }}/leoclean-nosend-release/evidence/publish-outcome.json
|
||||||
${{ env.RELEASE_BUNDLE }}/release.json
|
${{ runner.temp }}/leoclean-nosend-release/receipts/build-push-receipt.json
|
||||||
${{ env.RELEASE_BUNDLE }}/leoclean-gcp-nosend.service
|
${{ runner.temp }}/leoclean-nosend-release/final/release-v3/release.json
|
||||||
if-no-files-found: error
|
${{ runner.temp }}/leoclean-nosend-release/final/release-v3/leoclean-gcp-nosend.service
|
||||||
|
if-no-files-found: warn
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- name: Remove ephemeral Docker authentication
|
|
||||||
if: always()
|
|
||||||
shell: bash
|
|
||||||
run: rm -rf -- "${DOCKER_CONFIG}"
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import copy
|
||||||
import ctypes
|
import ctypes
|
||||||
import errno
|
import errno
|
||||||
import fcntl
|
import fcntl
|
||||||
|
|
@ -15,6 +16,7 @@ import shutil
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import tomllib
|
import tomllib
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
|
@ -34,6 +36,7 @@ from scripts import leo_identity_manifest as strict_identity_manifest # noqa: E
|
||||||
IMAGE_INPUT_SCHEMA = "livingip.leocleanNoSendImageInput.v2"
|
IMAGE_INPUT_SCHEMA = "livingip.leocleanNoSendImageInput.v2"
|
||||||
IMAGE_INSPECTION_SCHEMA = "livingip.leocleanNoSendImageInspection.v1"
|
IMAGE_INSPECTION_SCHEMA = "livingip.leocleanNoSendImageInspection.v1"
|
||||||
BUILD_PUSH_RECEIPT_SCHEMA = "livingip.leocleanNoSendBuildPushReceipt.v1"
|
BUILD_PUSH_RECEIPT_SCHEMA = "livingip.leocleanNoSendBuildPushReceipt.v1"
|
||||||
|
PUBLISH_OUTCOME_SCHEMA = "livingip.leocleanNoSendPublishOutcome.v1"
|
||||||
RELEASE_SCHEMA = "livingip.leocleanNoSendRelease.v3"
|
RELEASE_SCHEMA = "livingip.leocleanNoSendRelease.v3"
|
||||||
ARTIFACT_MANIFEST_SCHEMA = "livingip.leocleanNoSendArtifactManifest.v1"
|
ARTIFACT_MANIFEST_SCHEMA = "livingip.leocleanNoSendArtifactManifest.v1"
|
||||||
ARTIFACT_RECEIPT_SCHEMA = "livingip.leocleanNoSendArtifactVerification.v1"
|
ARTIFACT_RECEIPT_SCHEMA = "livingip.leocleanNoSendArtifactVerification.v1"
|
||||||
|
|
@ -50,6 +53,9 @@ SERVICE = "leoclean-gcp-nosend.service"
|
||||||
SERVICE_ACCOUNT = "sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com"
|
SERVICE_ACCOUNT = "sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com"
|
||||||
PLATFORM = "linux/amd64"
|
PLATFORM = "linux/amd64"
|
||||||
IMAGE_REPOSITORY = "europe-west6-docker.pkg.dev/teleo-501523/teleo/leoclean-nosend-staging"
|
IMAGE_REPOSITORY = "europe-west6-docker.pkg.dev/teleo-501523/teleo/leoclean-nosend-staging"
|
||||||
|
ARTIFACT_PACKAGE_RESOURCE = (
|
||||||
|
"projects/teleo-501523/locations/europe-west6/repositories/teleo/packages/leoclean-nosend-staging"
|
||||||
|
)
|
||||||
CONTAINER_NAME = "livingip-leoclean-nosend"
|
CONTAINER_NAME = "livingip-leoclean-nosend"
|
||||||
LOCAL_BUILD_REPOSITORY = "livingip-leoclean-nosend-build"
|
LOCAL_BUILD_REPOSITORY = "livingip-leoclean-nosend-build"
|
||||||
DOCKER_BINARY = "/usr/bin/docker"
|
DOCKER_BINARY = "/usr/bin/docker"
|
||||||
|
|
@ -83,7 +89,9 @@ RUNTIME_UID = 65532
|
||||||
RUNTIME_GID = 65532
|
RUNTIME_GID = 65532
|
||||||
MAX_DOCKER_INSPECTION_BYTES = 1024 * 1024
|
MAX_DOCKER_INSPECTION_BYTES = 1024 * 1024
|
||||||
MAX_DOCKER_COMMAND_OUTPUT_BYTES = 1024 * 1024
|
MAX_DOCKER_COMMAND_OUTPUT_BYTES = 1024 * 1024
|
||||||
|
MAX_GCLOUD_COMMAND_OUTPUT_BYTES = 1024 * 1024
|
||||||
MAX_BUILD_PUSH_RECEIPT_BYTES = 256 * 1024
|
MAX_BUILD_PUSH_RECEIPT_BYTES = 256 * 1024
|
||||||
|
MAX_PUBLISH_OUTCOME_BYTES = 256 * 1024
|
||||||
IMAGE_RUNTIME_CONFIG = {
|
IMAGE_RUNTIME_CONFIG = {
|
||||||
"user": None,
|
"user": None,
|
||||||
"environment": [
|
"environment": [
|
||||||
|
|
@ -138,6 +146,8 @@ IMAGE_RUNTIME_CONFIG = {
|
||||||
HEX_40 = re.compile(r"^[0-9a-f]{40}$")
|
HEX_40 = re.compile(r"^[0-9a-f]{40}$")
|
||||||
HEX_64 = re.compile(r"^[0-9a-f]{64}$")
|
HEX_64 = re.compile(r"^[0-9a-f]{64}$")
|
||||||
IMAGE_REFERENCE = re.compile(rf"^{re.escape(IMAGE_REPOSITORY)}@sha256:([0-9a-f]{{64}})$")
|
IMAGE_REFERENCE = re.compile(rf"^{re.escape(IMAGE_REPOSITORY)}@sha256:([0-9a-f]{{64}})$")
|
||||||
|
CANDIDATE_TAG = re.compile(r"^candidate-([0-9a-f]{64})$")
|
||||||
|
CANDIDATE_REFERENCE = re.compile(rf"^{re.escape(IMAGE_REPOSITORY)}:candidate-([0-9a-f]{{64}})$")
|
||||||
FORBIDDEN_DESCRIPTOR_MARKERS = (
|
FORBIDDEN_DESCRIPTOR_MARKERS = (
|
||||||
"77.42.65.182",
|
"77.42.65.182",
|
||||||
"gcp-teleo-pgvector-standby-postgres-password",
|
"gcp-teleo-pgvector-standby-postgres-password",
|
||||||
|
|
@ -1821,6 +1831,244 @@ def build_build_push_receipt(
|
||||||
return receipt
|
return receipt
|
||||||
|
|
||||||
|
|
||||||
|
def _seal_publish_outcome(stable: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
value = {**stable, "outcome_sha256": canonical_sha256(stable)}
|
||||||
|
validate_publish_outcome(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_publish_outcome(value: dict[str, Any], *, image_input: dict[str, Any] | None = None) -> None:
|
||||||
|
_require(
|
||||||
|
set(value)
|
||||||
|
== {
|
||||||
|
"schema",
|
||||||
|
"status",
|
||||||
|
"source_revision",
|
||||||
|
"input_sha256",
|
||||||
|
"candidate",
|
||||||
|
"registry",
|
||||||
|
"phase",
|
||||||
|
"failure_phase",
|
||||||
|
"cleanup",
|
||||||
|
"release",
|
||||||
|
"events",
|
||||||
|
"outcome_sha256",
|
||||||
|
},
|
||||||
|
"publish outcome fields are not exact",
|
||||||
|
)
|
||||||
|
_require(value.get("schema") == PUBLISH_OUTCOME_SCHEMA, "publish outcome schema is invalid")
|
||||||
|
_require(value.get("status") in {"in_progress", "pass", "fail"}, "publish outcome status is invalid")
|
||||||
|
_require(_is_revision(value.get("source_revision")), "publish outcome source revision is invalid")
|
||||||
|
_require(_is_sha256(value.get("input_sha256")), "publish outcome input hash is invalid")
|
||||||
|
if image_input is not None:
|
||||||
|
validate_image_input(image_input)
|
||||||
|
_require(
|
||||||
|
value["source_revision"] == image_input["runtime"]["teleo_git_head"]
|
||||||
|
and value["input_sha256"] == image_input["input_sha256"],
|
||||||
|
"publish outcome image-input binding drifted",
|
||||||
|
)
|
||||||
|
|
||||||
|
candidate = value.get("candidate")
|
||||||
|
_require(
|
||||||
|
isinstance(candidate, dict) and set(candidate) == {"tag", "reference", "local_build_config_digest"},
|
||||||
|
"publish outcome candidate fields are not exact",
|
||||||
|
)
|
||||||
|
candidate_values = (
|
||||||
|
candidate.get("tag"),
|
||||||
|
candidate.get("reference"),
|
||||||
|
candidate.get("local_build_config_digest"),
|
||||||
|
)
|
||||||
|
if candidate_values == (None, None, None):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
tag, reference, local_build_config_digest = candidate_values
|
||||||
|
_require(
|
||||||
|
isinstance(tag, str)
|
||||||
|
and CANDIDATE_TAG.fullmatch(tag) is not None
|
||||||
|
and isinstance(reference, str)
|
||||||
|
and CANDIDATE_REFERENCE.fullmatch(reference) is not None
|
||||||
|
and isinstance(local_build_config_digest, str)
|
||||||
|
and local_build_config_digest.startswith("sha256:")
|
||||||
|
and _is_sha256(local_build_config_digest.removeprefix("sha256:")),
|
||||||
|
"publish outcome candidate is invalid",
|
||||||
|
)
|
||||||
|
_require(
|
||||||
|
tag == f"candidate-{value['input_sha256']}" and reference == f"{IMAGE_REPOSITORY}:{tag}",
|
||||||
|
"candidate binding drifted",
|
||||||
|
)
|
||||||
|
|
||||||
|
registry = value.get("registry")
|
||||||
|
_require(
|
||||||
|
isinstance(registry, dict)
|
||||||
|
and set(registry) == {"repository", "preexisting", "push_attempted", "action", "observed_digest"},
|
||||||
|
"publish outcome registry fields are not exact",
|
||||||
|
)
|
||||||
|
_require(registry.get("repository") == IMAGE_REPOSITORY, "publish outcome repository is not exact")
|
||||||
|
_require(registry.get("preexisting") in {None, True, False}, "publish outcome preexisting state is invalid")
|
||||||
|
_require(type(registry.get("push_attempted")) is bool, "publish outcome push-attempt state is invalid")
|
||||||
|
_require(
|
||||||
|
registry.get("action")
|
||||||
|
in {"none", "observed", "fresh_push", "reused", "indeterminate", "reconciled_after_attempt"},
|
||||||
|
"publish outcome registry action is invalid",
|
||||||
|
)
|
||||||
|
observed_digest = registry.get("observed_digest")
|
||||||
|
_require(
|
||||||
|
observed_digest is None
|
||||||
|
or (
|
||||||
|
isinstance(observed_digest, str)
|
||||||
|
and observed_digest.startswith("sha256:")
|
||||||
|
and _is_sha256(observed_digest.removeprefix("sha256:"))
|
||||||
|
),
|
||||||
|
"publish outcome observed digest is invalid",
|
||||||
|
)
|
||||||
|
candidate_is_set = candidate_values != (None, None, None)
|
||||||
|
registry_state = (
|
||||||
|
registry["preexisting"],
|
||||||
|
registry["push_attempted"],
|
||||||
|
registry["action"],
|
||||||
|
observed_digest is not None,
|
||||||
|
)
|
||||||
|
_require(
|
||||||
|
registry_state
|
||||||
|
in {
|
||||||
|
(None, False, "none", False),
|
||||||
|
(False, False, "none", False),
|
||||||
|
(True, False, "observed", True),
|
||||||
|
(True, False, "reused", True),
|
||||||
|
(False, True, "indeterminate", False),
|
||||||
|
(False, True, "indeterminate", True),
|
||||||
|
(False, True, "fresh_push", True),
|
||||||
|
(False, True, "reconciled_after_attempt", True),
|
||||||
|
},
|
||||||
|
"publish outcome registry state is inconsistent",
|
||||||
|
)
|
||||||
|
_require(
|
||||||
|
candidate_is_set or registry_state == (None, False, "none", False),
|
||||||
|
"publish outcome registry activity has no candidate",
|
||||||
|
)
|
||||||
|
|
||||||
|
phase = value.get("phase")
|
||||||
|
failure_phase = value.get("failure_phase")
|
||||||
|
_require(isinstance(phase, str) and bool(re.fullmatch(r"[a-z][a-z0-9_]{0,63}", phase)), "publish phase is invalid")
|
||||||
|
_require(
|
||||||
|
failure_phase is None
|
||||||
|
or (isinstance(failure_phase, str) and bool(re.fullmatch(r"[a-z][a-z0-9_]{0,63}", failure_phase))),
|
||||||
|
"publish failure phase is invalid",
|
||||||
|
)
|
||||||
|
_require(
|
||||||
|
(value["status"] == "fail") == (failure_phase is not None),
|
||||||
|
"publish failure status and phase disagree",
|
||||||
|
)
|
||||||
|
|
||||||
|
cleanup = value.get("cleanup")
|
||||||
|
_require(
|
||||||
|
isinstance(cleanup, dict)
|
||||||
|
and cleanup
|
||||||
|
== {
|
||||||
|
"docker_config": cleanup.get("docker_config"),
|
||||||
|
"local_daemon": "runner_ephemeral_references_retained",
|
||||||
|
"remote_candidate": "immutable_evidence_retained",
|
||||||
|
}
|
||||||
|
and cleanup.get("docker_config") in {"pending", "pass", "fail"},
|
||||||
|
"publish cleanup state is invalid",
|
||||||
|
)
|
||||||
|
release = value.get("release")
|
||||||
|
_require(
|
||||||
|
isinstance(release, dict)
|
||||||
|
and set(release) == {"build_push_receipt_present", "bundle_present", "release_sha256"}
|
||||||
|
and type(release.get("build_push_receipt_present")) is bool
|
||||||
|
and type(release.get("bundle_present")) is bool,
|
||||||
|
"publish release state is invalid",
|
||||||
|
)
|
||||||
|
release_sha256 = release.get("release_sha256")
|
||||||
|
_require(release_sha256 is None or _is_sha256(release_sha256), "publish release hash is invalid")
|
||||||
|
if value["status"] == "pass":
|
||||||
|
_require(
|
||||||
|
registry["action"] in {"fresh_push", "reused", "reconciled_after_attempt"}
|
||||||
|
and observed_digest is not None
|
||||||
|
and cleanup["docker_config"] == "pass"
|
||||||
|
and release["build_push_receipt_present"] is True
|
||||||
|
and release["bundle_present"] is True
|
||||||
|
and _is_sha256(release_sha256),
|
||||||
|
"passing publish outcome is incomplete",
|
||||||
|
)
|
||||||
|
|
||||||
|
events = value.get("events")
|
||||||
|
_require(isinstance(events, list) and bool(events), "publish outcome events are missing")
|
||||||
|
for event in events:
|
||||||
|
_require(
|
||||||
|
isinstance(event, dict)
|
||||||
|
and set(event) == {"phase", "status"}
|
||||||
|
and isinstance(event.get("phase"), str)
|
||||||
|
and bool(re.fullmatch(r"[a-z][a-z0-9_]{0,63}", event["phase"]))
|
||||||
|
and event.get("status")
|
||||||
|
in {"initialized", "attempted", "absent", "present", "pass", "fail", "recovered", "reused", "skipped"},
|
||||||
|
"publish outcome event is invalid",
|
||||||
|
)
|
||||||
|
stable = {key: item for key, item in value.items() if key != "outcome_sha256"}
|
||||||
|
_require(value.get("outcome_sha256") == canonical_sha256(stable), "publish outcome self-hash drifted")
|
||||||
|
_assert_no_forbidden_descriptor_markers(value)
|
||||||
|
|
||||||
|
|
||||||
|
def build_initial_publish_outcome(image_input: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
validate_image_input(image_input)
|
||||||
|
stable = {
|
||||||
|
"schema": PUBLISH_OUTCOME_SCHEMA,
|
||||||
|
"status": "in_progress",
|
||||||
|
"source_revision": image_input["runtime"]["teleo_git_head"],
|
||||||
|
"input_sha256": image_input["input_sha256"],
|
||||||
|
"candidate": {"tag": None, "reference": None, "local_build_config_digest": None},
|
||||||
|
"registry": {
|
||||||
|
"repository": IMAGE_REPOSITORY,
|
||||||
|
"preexisting": None,
|
||||||
|
"push_attempted": False,
|
||||||
|
"action": "none",
|
||||||
|
"observed_digest": None,
|
||||||
|
},
|
||||||
|
"phase": "initialized",
|
||||||
|
"failure_phase": None,
|
||||||
|
"cleanup": {
|
||||||
|
"docker_config": "pending",
|
||||||
|
"local_daemon": "runner_ephemeral_references_retained",
|
||||||
|
"remote_candidate": "immutable_evidence_retained",
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"build_push_receipt_present": False,
|
||||||
|
"bundle_present": False,
|
||||||
|
"release_sha256": None,
|
||||||
|
},
|
||||||
|
"events": [{"phase": "initialized", "status": "initialized"}],
|
||||||
|
}
|
||||||
|
return _seal_publish_outcome(stable)
|
||||||
|
|
||||||
|
|
||||||
|
def advance_publish_outcome(
|
||||||
|
value: dict[str, Any],
|
||||||
|
*,
|
||||||
|
phase: str,
|
||||||
|
event_status: str,
|
||||||
|
status: str | None = None,
|
||||||
|
failure_phase: str | None = None,
|
||||||
|
candidate: dict[str, Any] | None = None,
|
||||||
|
registry: dict[str, Any] | None = None,
|
||||||
|
cleanup: dict[str, Any] | None = None,
|
||||||
|
release: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
validate_publish_outcome(value)
|
||||||
|
_require(value["status"] != "pass", "completed publish outcome cannot advance")
|
||||||
|
stable = copy.deepcopy({key: item for key, item in value.items() if key != "outcome_sha256"})
|
||||||
|
stable["phase"] = phase
|
||||||
|
stable["events"].append({"phase": phase, "status": event_status})
|
||||||
|
if status is not None:
|
||||||
|
stable["status"] = status
|
||||||
|
if failure_phase is not None:
|
||||||
|
stable["failure_phase"] = failure_phase
|
||||||
|
for key, updates in (("candidate", candidate), ("registry", registry), ("cleanup", cleanup), ("release", release)):
|
||||||
|
if updates is not None:
|
||||||
|
stable[key].update(updates)
|
||||||
|
return _seal_publish_outcome(stable)
|
||||||
|
|
||||||
|
|
||||||
def build_image_inspection(
|
def build_image_inspection(
|
||||||
image_input: dict[str, Any],
|
image_input: dict[str, Any],
|
||||||
image_reference: str,
|
image_reference: str,
|
||||||
|
|
@ -2133,6 +2381,197 @@ def _bounded_docker_output(result: subprocess.CompletedProcess[bytes], label: st
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_gcloud_binary(gcloud_binary: Path) -> None:
|
||||||
|
_require(gcloud_binary.is_absolute() and "\x00" not in str(gcloud_binary), "gcloud binary must be absolute")
|
||||||
|
try:
|
||||||
|
observed = gcloud_binary.stat(follow_symlinks=False)
|
||||||
|
except OSError as exc:
|
||||||
|
raise PackageError("gcloud binary is unavailable") from exc
|
||||||
|
_require(
|
||||||
|
stat.S_ISREG(observed.st_mode)
|
||||||
|
and observed.st_uid in {0, os.geteuid()}
|
||||||
|
and bool(observed.st_mode & 0o111)
|
||||||
|
and observed.st_mode & 0o022 == 0,
|
||||||
|
"gcloud binary posture is unsafe",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_gcloud(gcloud_binary: Path, arguments: list[str], *, timeout: int = 60) -> subprocess.CompletedProcess[bytes]:
|
||||||
|
_validate_gcloud_binary(gcloud_binary)
|
||||||
|
environment = os.environ.copy()
|
||||||
|
environment["CLOUDSDK_CORE_DISABLE_PROMPTS"] = "1"
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(gcloud_binary), *arguments],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
timeout=timeout,
|
||||||
|
env=environment,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError) as exc:
|
||||||
|
raise PackageError("registry tag lookup failed") from exc
|
||||||
|
_require(
|
||||||
|
len(result.stdout) <= MAX_GCLOUD_COMMAND_OUTPUT_BYTES and len(result.stderr) <= MAX_GCLOUD_COMMAND_OUTPUT_BYTES,
|
||||||
|
"registry tag lookup output is too large",
|
||||||
|
)
|
||||||
|
_require(result.returncode == 0, "registry tag lookup failed")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _registry_candidate_digest(gcloud_binary: Path, candidate_tag: str) -> str | None:
|
||||||
|
_require(CANDIDATE_TAG.fullmatch(candidate_tag) is not None, "registry candidate tag is invalid")
|
||||||
|
expected_tag = f"{ARTIFACT_PACKAGE_RESOURCE}/tags/{candidate_tag}"
|
||||||
|
expected_version_prefix = f"{ARTIFACT_PACKAGE_RESOURCE}/versions/sha256:"
|
||||||
|
result = _run_gcloud(
|
||||||
|
gcloud_binary,
|
||||||
|
[
|
||||||
|
"artifacts",
|
||||||
|
"docker",
|
||||||
|
"tags",
|
||||||
|
"list",
|
||||||
|
IMAGE_REPOSITORY,
|
||||||
|
"--project",
|
||||||
|
PROJECT,
|
||||||
|
f"--filter=tag={expected_tag}",
|
||||||
|
"--format=json(tag,version)",
|
||||||
|
"--limit=2",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
rows = _strict_json_loads(result.stdout.decode("utf-8"), "registry candidate lookup")
|
||||||
|
except UnicodeError as exc:
|
||||||
|
raise PackageError("registry candidate lookup is invalid") from exc
|
||||||
|
_require(isinstance(rows, list), "registry candidate lookup is invalid")
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
_require(
|
||||||
|
len(rows) == 1
|
||||||
|
and isinstance(rows[0], dict)
|
||||||
|
and set(rows[0]) == {"tag", "version"}
|
||||||
|
and rows[0].get("tag") == expected_tag,
|
||||||
|
"registry candidate lookup returned an unexpected tag",
|
||||||
|
)
|
||||||
|
row = rows[0]
|
||||||
|
version = row.get("version")
|
||||||
|
digest = version.removeprefix(expected_version_prefix) if isinstance(version, str) else ""
|
||||||
|
_require(
|
||||||
|
version == f"{expected_version_prefix}{digest}" and _is_sha256(digest), "registry candidate version is invalid"
|
||||||
|
)
|
||||||
|
return f"sha256:{digest}"
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_registry_candidate_digest(
|
||||||
|
gcloud_binary: Path,
|
||||||
|
candidate_tag: str,
|
||||||
|
*,
|
||||||
|
attempts: int = 6,
|
||||||
|
interval_seconds: float = 2.0,
|
||||||
|
) -> str | None:
|
||||||
|
_require(type(attempts) is int and attempts > 0, "registry lookup attempts are invalid")
|
||||||
|
for attempt in range(attempts):
|
||||||
|
digest = _registry_candidate_digest(gcloud_binary, candidate_tag)
|
||||||
|
if digest is not None:
|
||||||
|
return digest
|
||||||
|
if attempt + 1 < attempts:
|
||||||
|
time.sleep(interval_seconds)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _pull_validated_registry_candidate(
|
||||||
|
image_input: dict[str, Any],
|
||||||
|
expected_local: dict[str, Any],
|
||||||
|
manifest_digest: str,
|
||||||
|
*,
|
||||||
|
docker_binary: Path,
|
||||||
|
docker_host: str,
|
||||||
|
docker_config: Path,
|
||||||
|
authority: _DockerCacheLock,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
_require(
|
||||||
|
isinstance(manifest_digest, str)
|
||||||
|
and manifest_digest.startswith("sha256:")
|
||||||
|
and _is_sha256(manifest_digest.removeprefix("sha256:")),
|
||||||
|
"registry candidate digest is invalid",
|
||||||
|
)
|
||||||
|
reference = f"{IMAGE_REPOSITORY}@{manifest_digest}"
|
||||||
|
pulled = _run_docker(
|
||||||
|
docker_binary,
|
||||||
|
docker_host,
|
||||||
|
docker_config,
|
||||||
|
["pull", "--quiet", "--platform", PLATFORM, reference],
|
||||||
|
check=True,
|
||||||
|
timeout=600,
|
||||||
|
authority=authority,
|
||||||
|
)
|
||||||
|
_bounded_docker_output(pulled, "registry candidate pull")
|
||||||
|
remote_image = _inspect_local_image(
|
||||||
|
docker_binary,
|
||||||
|
docker_host,
|
||||||
|
docker_config,
|
||||||
|
reference,
|
||||||
|
authority=authority,
|
||||||
|
)
|
||||||
|
remote_configuration = validate_image_configuration(image_input, remote_image)
|
||||||
|
for field in ("platform", "labels", "runtime_config"):
|
||||||
|
_require(
|
||||||
|
remote_configuration[field] == expected_local[field],
|
||||||
|
"existing registry candidate runtime differs from the exact input",
|
||||||
|
)
|
||||||
|
_require(
|
||||||
|
_target_repository_digests(remote_image) == [reference],
|
||||||
|
"existing registry candidate digest binding is not exact",
|
||||||
|
)
|
||||||
|
verified = _run_docker(
|
||||||
|
docker_binary,
|
||||||
|
docker_host,
|
||||||
|
docker_config,
|
||||||
|
[
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"--pull=never",
|
||||||
|
"--platform",
|
||||||
|
PLATFORM,
|
||||||
|
"--network=none",
|
||||||
|
"--read-only",
|
||||||
|
"--cap-drop=ALL",
|
||||||
|
"--security-opt=no-new-privileges:true",
|
||||||
|
"--pids-limit=64",
|
||||||
|
f"--user={RUNTIME_UID}:{RUNTIME_GID}",
|
||||||
|
reference,
|
||||||
|
"verify-build",
|
||||||
|
image_input["runtime"]["teleo_git_head"],
|
||||||
|
image_input["runtime"]["artifact_sha256"],
|
||||||
|
image_input["identity"]["bundle_sha256"],
|
||||||
|
image_input["input_sha256"],
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
timeout=120,
|
||||||
|
authority=authority,
|
||||||
|
)
|
||||||
|
_bounded_docker_output(verified, "registry candidate runtime verification")
|
||||||
|
try:
|
||||||
|
verification = _strict_json_loads(verified.stdout.decode("utf-8"), "registry candidate verification")
|
||||||
|
except UnicodeError as exc:
|
||||||
|
raise PackageError("registry candidate verification is invalid") from exc
|
||||||
|
_require(
|
||||||
|
verification
|
||||||
|
== {
|
||||||
|
"schema": "livingip.leocleanNoSendContainerVerification.v1",
|
||||||
|
"status": "pass",
|
||||||
|
"teleo_git_head": image_input["runtime"]["teleo_git_head"],
|
||||||
|
"artifact_sha256": image_input["runtime"]["artifact_sha256"],
|
||||||
|
"identity_sha256": image_input["identity"]["bundle_sha256"],
|
||||||
|
"input_sha256": image_input["input_sha256"],
|
||||||
|
"python_version": PYTHON_VERSION,
|
||||||
|
"postgres_version": POSTGRES_VERSION,
|
||||||
|
"cloudsql_ca_sha256": CA_SHA256,
|
||||||
|
},
|
||||||
|
"registry candidate verification differs from the exact input",
|
||||||
|
)
|
||||||
|
return remote_image
|
||||||
|
|
||||||
|
|
||||||
def _inspect_local_image(
|
def _inspect_local_image(
|
||||||
docker_binary: Path,
|
docker_binary: Path,
|
||||||
docker_host: str,
|
docker_host: str,
|
||||||
|
|
@ -2282,22 +2721,56 @@ def build_and_push_staging_image(
|
||||||
docker_binary: Path,
|
docker_binary: Path,
|
||||||
docker_host: str,
|
docker_host: str,
|
||||||
docker_config: Path,
|
docker_config: Path,
|
||||||
|
gcloud_binary: Path,
|
||||||
output_receipt: Path,
|
output_receipt: Path,
|
||||||
|
outcome_journal: Path,
|
||||||
execute_staging_push: bool,
|
execute_staging_push: bool,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Build one reviewed context, push one content-bound candidate, and receipt the result."""
|
"""Build and reconcile one content-bound candidate, then receipt the exact digest."""
|
||||||
|
|
||||||
_require(execute_staging_push is True, "staging image push requires explicit execution authorization")
|
_require(execute_staging_push is True, "staging image push requires explicit execution authorization")
|
||||||
image_input, build_args = validate_prepared_image_context(context)
|
image_input, build_args = validate_prepared_image_context(context)
|
||||||
validate_reviewed_context_source(repo_root, context, image_input)
|
outcome = load_publish_outcome(outcome_journal, image_input=image_input)
|
||||||
_validate_docker_authority(docker_binary, docker_host, docker_config)
|
_require(outcome["status"] == "in_progress", "publish outcome is not active")
|
||||||
_validate_receipt_output_path(output_receipt)
|
current_phase = "preflight"
|
||||||
_validate_receipt_context_separation(context, output_receipt)
|
push_attempted = False
|
||||||
|
observed_digest: str | None = None
|
||||||
|
|
||||||
|
def record(
|
||||||
|
phase: str,
|
||||||
|
event_status: str,
|
||||||
|
*,
|
||||||
|
status: str | None = None,
|
||||||
|
failure_phase: str | None = None,
|
||||||
|
candidate: dict[str, Any] | None = None,
|
||||||
|
registry: dict[str, Any] | None = None,
|
||||||
|
release: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
nonlocal outcome, current_phase
|
||||||
|
current_phase = phase
|
||||||
|
outcome = advance_publish_outcome(
|
||||||
|
outcome,
|
||||||
|
phase=phase,
|
||||||
|
event_status=event_status,
|
||||||
|
status=status,
|
||||||
|
failure_phase=failure_phase,
|
||||||
|
candidate=candidate,
|
||||||
|
registry=registry,
|
||||||
|
release=release,
|
||||||
|
)
|
||||||
|
publish_publish_outcome(outcome_journal, outcome, create=False)
|
||||||
|
|
||||||
build_tag = f"input-{image_input['input_sha256']}"
|
build_tag = f"input-{image_input['input_sha256']}"
|
||||||
build_reference = f"{LOCAL_BUILD_REPOSITORY}:{build_tag}"
|
build_reference = f"{LOCAL_BUILD_REPOSITORY}:{build_tag}"
|
||||||
|
validate_reviewed_context_source(repo_root, context, image_input)
|
||||||
|
_validate_docker_authority(docker_binary, docker_host, docker_config)
|
||||||
|
_validate_gcloud_binary(gcloud_binary)
|
||||||
|
_validate_receipt_output_path(output_receipt)
|
||||||
|
_validate_receipt_context_separation(context, output_receipt)
|
||||||
|
record("preflight", "pass")
|
||||||
try:
|
try:
|
||||||
with _DockerCacheLock(docker_config) as authority:
|
with _DockerCacheLock(docker_config) as authority:
|
||||||
|
current_phase = "local_build"
|
||||||
_require(
|
_require(
|
||||||
_local_tag_config_id(
|
_local_tag_config_id(
|
||||||
docker_binary,
|
docker_binary,
|
||||||
|
|
@ -2366,8 +2839,44 @@ def build_and_push_staging_image(
|
||||||
)
|
)
|
||||||
validate_reviewed_context_source(repo_root, context, post_build_input)
|
validate_reviewed_context_source(repo_root, context, post_build_input)
|
||||||
|
|
||||||
candidate_tag = f"candidate-{build_config_id.removeprefix('sha256:')}"
|
candidate_tag = f"candidate-{image_input['input_sha256']}"
|
||||||
candidate_reference = f"{IMAGE_REPOSITORY}:{candidate_tag}"
|
candidate_reference = f"{IMAGE_REPOSITORY}:{candidate_tag}"
|
||||||
|
record(
|
||||||
|
"candidate_derived",
|
||||||
|
"pass",
|
||||||
|
candidate={
|
||||||
|
"tag": candidate_tag,
|
||||||
|
"reference": candidate_reference,
|
||||||
|
"local_build_config_digest": build_config_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
current_phase = "remote_reconcile"
|
||||||
|
existing_digest = _registry_candidate_digest(gcloud_binary, candidate_tag)
|
||||||
|
if existing_digest is not None:
|
||||||
|
observed_digest = existing_digest
|
||||||
|
record(
|
||||||
|
"remote_reconcile",
|
||||||
|
"present",
|
||||||
|
registry={
|
||||||
|
"preexisting": True,
|
||||||
|
"action": "observed",
|
||||||
|
"observed_digest": existing_digest,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
remote_image = _pull_validated_registry_candidate(
|
||||||
|
image_input,
|
||||||
|
observed,
|
||||||
|
existing_digest,
|
||||||
|
docker_binary=docker_binary,
|
||||||
|
docker_host=docker_host,
|
||||||
|
docker_config=docker_config,
|
||||||
|
authority=authority,
|
||||||
|
)
|
||||||
|
receipt = build_build_push_receipt(image_input, post_push_image=remote_image)
|
||||||
|
record("remote_readback", "reused", registry={"action": "reused"})
|
||||||
|
else:
|
||||||
|
record("remote_reconcile", "absent", registry={"preexisting": False})
|
||||||
_require(
|
_require(
|
||||||
_local_tag_config_id(
|
_local_tag_config_id(
|
||||||
docker_binary,
|
docker_binary,
|
||||||
|
|
@ -2417,6 +2926,14 @@ def build_and_push_staging_image(
|
||||||
_target_repository_digests(pre_push_image) == [],
|
_target_repository_digests(pre_push_image) == [],
|
||||||
"staging candidate already has a repository digest before push",
|
"staging candidate already has a repository digest before push",
|
||||||
)
|
)
|
||||||
|
push_attempted = True
|
||||||
|
record(
|
||||||
|
"push",
|
||||||
|
"attempted",
|
||||||
|
registry={"push_attempted": True, "action": "indeterminate"},
|
||||||
|
)
|
||||||
|
push_error: BaseException | None = None
|
||||||
|
try:
|
||||||
pushed = _run_docker(
|
pushed = _run_docker(
|
||||||
docker_binary,
|
docker_binary,
|
||||||
docker_host,
|
docker_host,
|
||||||
|
|
@ -2427,31 +2944,58 @@ def build_and_push_staging_image(
|
||||||
authority=authority,
|
authority=authority,
|
||||||
)
|
)
|
||||||
_bounded_docker_output(pushed, "staging image push")
|
_bounded_docker_output(pushed, "staging image push")
|
||||||
post_push_image = _inspect_local_image(
|
except (OSError, subprocess.SubprocessError, PackageError) as exc:
|
||||||
docker_binary,
|
push_error = exc
|
||||||
docker_host,
|
observed_digest = _wait_registry_candidate_digest(gcloud_binary, candidate_tag)
|
||||||
docker_config,
|
_require(observed_digest is not None, "staging push produced no observable immutable candidate")
|
||||||
candidate_reference,
|
remote_image = _pull_validated_registry_candidate(
|
||||||
|
image_input,
|
||||||
|
observed,
|
||||||
|
observed_digest,
|
||||||
|
docker_binary=docker_binary,
|
||||||
|
docker_host=docker_host,
|
||||||
|
docker_config=docker_config,
|
||||||
authority=authority,
|
authority=authority,
|
||||||
)
|
)
|
||||||
_require(
|
record(
|
||||||
validate_image_configuration(image_input, post_push_image) == observed,
|
"remote_readback",
|
||||||
"staging candidate configuration drifted across push",
|
"recovered" if push_error is not None else "pass",
|
||||||
|
registry={
|
||||||
|
"action": "reconciled_after_attempt" if push_error is not None else "fresh_push",
|
||||||
|
"observed_digest": observed_digest,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
repository_digests = _target_repository_digests(post_push_image)
|
receipt = build_build_push_receipt(image_input, post_push_image=remote_image)
|
||||||
_require(len(repository_digests) == 1, "staging push did not produce one exact repository digest")
|
|
||||||
registry_reference = repository_digests[0]
|
current_phase = "receipt"
|
||||||
match = IMAGE_REFERENCE.fullmatch(registry_reference)
|
|
||||||
_require(match is not None, "staging push repository digest is invalid")
|
|
||||||
receipt = build_build_push_receipt(image_input, post_push_image=post_push_image)
|
|
||||||
except (OSError, subprocess.SubprocessError, PackageError) as exc:
|
|
||||||
raise PackageError("staging image build/push failed") from exc
|
|
||||||
publish_build_push_receipt(
|
publish_build_push_receipt(
|
||||||
output_receipt,
|
output_receipt,
|
||||||
receipt,
|
receipt,
|
||||||
image_input=image_input,
|
image_input=image_input,
|
||||||
outside_context=context,
|
outside_context=context,
|
||||||
)
|
)
|
||||||
|
record("receipt", "pass", release={"build_push_receipt_present": True})
|
||||||
|
except (OSError, subprocess.SubprocessError, PackageError) as exc:
|
||||||
|
registry_updates: dict[str, Any] = {}
|
||||||
|
if observed_digest is not None:
|
||||||
|
registry_updates["observed_digest"] = observed_digest
|
||||||
|
if push_attempted and outcome["registry"]["action"] not in {"fresh_push", "reconciled_after_attempt"}:
|
||||||
|
registry_updates["action"] = "indeterminate"
|
||||||
|
try:
|
||||||
|
outcome = advance_publish_outcome(
|
||||||
|
outcome,
|
||||||
|
phase=current_phase,
|
||||||
|
event_status="fail",
|
||||||
|
status="fail",
|
||||||
|
failure_phase=current_phase,
|
||||||
|
registry=registry_updates or None,
|
||||||
|
)
|
||||||
|
publish_publish_outcome(outcome_journal, outcome, create=False)
|
||||||
|
except (OSError, PackageError) as journal_error:
|
||||||
|
raise PackageError("staging image build/push failed and outcome journaling failed") from journal_error
|
||||||
|
if current_phase == "receipt" and isinstance(exc, PackageError):
|
||||||
|
raise
|
||||||
|
raise PackageError("staging image build/push failed") from exc
|
||||||
return receipt
|
return receipt
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3065,6 +3609,235 @@ def publish_build_push_receipt(
|
||||||
os.close(parent_descriptor)
|
os.close(parent_descriptor)
|
||||||
|
|
||||||
|
|
||||||
|
def publish_publish_outcome(path: Path, value: dict[str, Any], *, create: bool) -> None:
|
||||||
|
"""Atomically create or replace the non-authority publish outcome journal."""
|
||||||
|
|
||||||
|
validate_publish_outcome(value)
|
||||||
|
_require(path.is_absolute() and path.name not in {"", ".", ".."}, "publish outcome path is invalid")
|
||||||
|
try:
|
||||||
|
payload = (json.dumps(value, allow_nan=False, indent=2, sort_keys=True) + "\n").encode("utf-8")
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise PackageError("cannot encode publish outcome") from exc
|
||||||
|
_require(len(payload) <= MAX_PUBLISH_OUTCOME_BYTES, "publish outcome is too large")
|
||||||
|
parent_descriptor = _open_private_operator_directory(path.parent, "publish outcome parent")
|
||||||
|
staged_name = f".{path.name}.{uuid.uuid4().hex}.tmp"
|
||||||
|
descriptor: int | None = None
|
||||||
|
try:
|
||||||
|
existing = _directory_entry_stat(parent_descriptor, path.name)
|
||||||
|
if create:
|
||||||
|
_require(existing is None, "publish outcome already exists")
|
||||||
|
else:
|
||||||
|
_require(
|
||||||
|
existing is not None
|
||||||
|
and stat.S_ISREG(existing.st_mode)
|
||||||
|
and existing.st_uid == os.geteuid()
|
||||||
|
and stat.S_IMODE(existing.st_mode) == 0o600,
|
||||||
|
"publish outcome update target is unsafe",
|
||||||
|
)
|
||||||
|
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||||
|
if hasattr(os, "O_NOFOLLOW"):
|
||||||
|
flags |= os.O_NOFOLLOW
|
||||||
|
descriptor = os.open(staged_name, flags, 0o600, dir_fd=parent_descriptor)
|
||||||
|
os.fchmod(descriptor, 0o600)
|
||||||
|
with os.fdopen(descriptor, "wb", closefd=False) as handle:
|
||||||
|
handle.write(payload)
|
||||||
|
handle.flush()
|
||||||
|
os.fsync(descriptor)
|
||||||
|
if create:
|
||||||
|
os.link(
|
||||||
|
staged_name,
|
||||||
|
path.name,
|
||||||
|
src_dir_fd=parent_descriptor,
|
||||||
|
dst_dir_fd=parent_descriptor,
|
||||||
|
follow_symlinks=False,
|
||||||
|
)
|
||||||
|
os.unlink(staged_name, dir_fd=parent_descriptor)
|
||||||
|
else:
|
||||||
|
os.replace(
|
||||||
|
staged_name,
|
||||||
|
path.name,
|
||||||
|
src_dir_fd=parent_descriptor,
|
||||||
|
dst_dir_fd=parent_descriptor,
|
||||||
|
)
|
||||||
|
observed = os.stat(path.name, dir_fd=parent_descriptor, follow_symlinks=False)
|
||||||
|
_require(
|
||||||
|
stat.S_ISREG(observed.st_mode)
|
||||||
|
and observed.st_uid == os.geteuid()
|
||||||
|
and stat.S_IMODE(observed.st_mode) == 0o600
|
||||||
|
and observed.st_size == len(payload),
|
||||||
|
"published outcome posture drifted",
|
||||||
|
)
|
||||||
|
_fsync_directory_descriptor(parent_descriptor)
|
||||||
|
except OSError as exc:
|
||||||
|
raise PackageError("cannot publish outcome journal") from exc
|
||||||
|
finally:
|
||||||
|
if descriptor is not None:
|
||||||
|
os.close(descriptor)
|
||||||
|
staged = _directory_entry_stat(parent_descriptor, staged_name)
|
||||||
|
if staged is not None:
|
||||||
|
try:
|
||||||
|
os.unlink(staged_name, dir_fd=parent_descriptor)
|
||||||
|
_fsync_directory_descriptor(parent_descriptor)
|
||||||
|
except OSError as exc:
|
||||||
|
os.close(parent_descriptor)
|
||||||
|
raise PackageError("cannot clean staged outcome journal") from exc
|
||||||
|
os.close(parent_descriptor)
|
||||||
|
|
||||||
|
|
||||||
|
def load_publish_outcome(path: Path, *, image_input: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
_require(path.is_absolute() and path.name not in {"", ".", ".."}, "publish outcome path is invalid")
|
||||||
|
parent_descriptor = _open_private_operator_directory(path.parent, "publish outcome parent")
|
||||||
|
descriptor: int | None = None
|
||||||
|
try:
|
||||||
|
flags = os.O_RDONLY
|
||||||
|
if hasattr(os, "O_NOFOLLOW"):
|
||||||
|
flags |= os.O_NOFOLLOW
|
||||||
|
descriptor = os.open(path.name, flags, dir_fd=parent_descriptor)
|
||||||
|
before = os.fstat(descriptor)
|
||||||
|
_require(
|
||||||
|
stat.S_ISREG(before.st_mode)
|
||||||
|
and before.st_uid == os.geteuid()
|
||||||
|
and stat.S_IMODE(before.st_mode) == 0o600
|
||||||
|
and before.st_size <= MAX_PUBLISH_OUTCOME_BYTES,
|
||||||
|
"publish outcome file posture is unsafe",
|
||||||
|
)
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
remaining = MAX_PUBLISH_OUTCOME_BYTES + 1
|
||||||
|
while remaining > 0:
|
||||||
|
chunk = os.read(descriptor, min(64 * 1024, remaining))
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
chunks.append(chunk)
|
||||||
|
remaining -= len(chunk)
|
||||||
|
payload = b"".join(chunks)
|
||||||
|
after = os.fstat(descriptor)
|
||||||
|
named = os.stat(path.name, dir_fd=parent_descriptor, follow_symlinks=False)
|
||||||
|
_require(
|
||||||
|
len(payload) <= MAX_PUBLISH_OUTCOME_BYTES
|
||||||
|
and (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns)
|
||||||
|
== (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns)
|
||||||
|
and (after.st_dev, after.st_ino) == (named.st_dev, named.st_ino)
|
||||||
|
and len(payload) == after.st_size,
|
||||||
|
"publish outcome changed while it was read",
|
||||||
|
)
|
||||||
|
except (OSError, UnicodeError) as exc:
|
||||||
|
raise PackageError("cannot read publish outcome") from exc
|
||||||
|
finally:
|
||||||
|
if descriptor is not None:
|
||||||
|
os.close(descriptor)
|
||||||
|
os.close(parent_descriptor)
|
||||||
|
try:
|
||||||
|
value = _strict_json_loads(payload.decode("utf-8"), "publish outcome")
|
||||||
|
except UnicodeError as exc:
|
||||||
|
raise PackageError("cannot decode publish outcome") from exc
|
||||||
|
_require(isinstance(value, dict), "publish outcome must be a JSON object")
|
||||||
|
validate_publish_outcome(value, image_input=image_input)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def complete_publish_outcome(
|
||||||
|
*,
|
||||||
|
image_input_path: Path,
|
||||||
|
outcome_journal: Path,
|
||||||
|
build_push_receipt_path: Path,
|
||||||
|
release_path: Path,
|
||||||
|
build_outcome: str,
|
||||||
|
finalize_outcome: str,
|
||||||
|
cleanup_outcome: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Finalize the mutation journal without masking an earlier workflow failure."""
|
||||||
|
|
||||||
|
allowed_step_outcomes = {"success", "failure", "cancelled", "skipped"}
|
||||||
|
_require(
|
||||||
|
{build_outcome, finalize_outcome, cleanup_outcome} <= allowed_step_outcomes,
|
||||||
|
"workflow step outcome is invalid",
|
||||||
|
)
|
||||||
|
image_input = load_json(image_input_path, "image input")
|
||||||
|
outcome = load_publish_outcome(outcome_journal, image_input=image_input)
|
||||||
|
_require(outcome["status"] != "pass", "publish outcome is already complete")
|
||||||
|
|
||||||
|
release_updates = {
|
||||||
|
"build_push_receipt_present": False,
|
||||||
|
"bundle_present": False,
|
||||||
|
"release_sha256": None,
|
||||||
|
}
|
||||||
|
evidence_failure: str | None = None
|
||||||
|
receipt: dict[str, Any] | None = None
|
||||||
|
release: dict[str, Any] | None = None
|
||||||
|
if build_push_receipt_path.is_file() and not build_push_receipt_path.is_symlink():
|
||||||
|
try:
|
||||||
|
receipt = load_private_json(build_push_receipt_path, "build/push receipt")
|
||||||
|
validate_build_push_receipt(receipt, image_input=image_input)
|
||||||
|
_require(
|
||||||
|
receipt["registry"]["manifest_digest"] == outcome["registry"]["observed_digest"],
|
||||||
|
"build/push receipt differs from publish outcome",
|
||||||
|
)
|
||||||
|
release_updates["build_push_receipt_present"] = True
|
||||||
|
except PackageError:
|
||||||
|
receipt = None
|
||||||
|
evidence_failure = "receipt_validation"
|
||||||
|
|
||||||
|
if release_path.is_file() and not release_path.is_symlink():
|
||||||
|
try:
|
||||||
|
release = load_json(release_path, "release descriptor")
|
||||||
|
validate_release_descriptor(release)
|
||||||
|
_require(
|
||||||
|
receipt is not None
|
||||||
|
and release["build_push_receipt"] == receipt
|
||||||
|
and release["image_input"] == image_input
|
||||||
|
and release["image"]["digest"] == outcome["registry"]["observed_digest"],
|
||||||
|
"release descriptor differs from publish outcome",
|
||||||
|
)
|
||||||
|
release_updates.update(
|
||||||
|
{
|
||||||
|
"bundle_present": True,
|
||||||
|
"release_sha256": release["release_sha256"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except PackageError:
|
||||||
|
release = None
|
||||||
|
evidence_failure = evidence_failure or "release_validation"
|
||||||
|
|
||||||
|
if build_outcome == "success" and release_updates["build_push_receipt_present"] is not True:
|
||||||
|
evidence_failure = evidence_failure or "receipt_missing"
|
||||||
|
if finalize_outcome == "success" and release_updates["bundle_present"] is not True:
|
||||||
|
evidence_failure = evidence_failure or "release_missing"
|
||||||
|
|
||||||
|
failure_phase = outcome["failure_phase"]
|
||||||
|
if failure_phase is None and build_outcome != "success":
|
||||||
|
failure_phase = {
|
||||||
|
"failure": "build_push",
|
||||||
|
"cancelled": "workflow_cancelled",
|
||||||
|
"skipped": "pre_publish",
|
||||||
|
}[build_outcome]
|
||||||
|
if failure_phase is None and finalize_outcome != "success":
|
||||||
|
failure_phase = "finalize"
|
||||||
|
if failure_phase is None and evidence_failure is not None:
|
||||||
|
failure_phase = evidence_failure
|
||||||
|
if failure_phase is None and cleanup_outcome != "success":
|
||||||
|
failure_phase = "cleanup"
|
||||||
|
passing = (
|
||||||
|
failure_phase is None
|
||||||
|
and build_outcome == "success"
|
||||||
|
and finalize_outcome == "success"
|
||||||
|
and cleanup_outcome == "success"
|
||||||
|
and release_updates["build_push_receipt_present"] is True
|
||||||
|
and release_updates["bundle_present"] is True
|
||||||
|
and _is_sha256(release_updates["release_sha256"])
|
||||||
|
)
|
||||||
|
completed = advance_publish_outcome(
|
||||||
|
outcome,
|
||||||
|
phase="complete",
|
||||||
|
event_status="pass" if passing else "fail",
|
||||||
|
status="pass" if passing else "fail",
|
||||||
|
failure_phase=None if passing else failure_phase or "completion",
|
||||||
|
cleanup={"docker_config": "pass" if cleanup_outcome == "success" else "fail"},
|
||||||
|
release=release_updates,
|
||||||
|
)
|
||||||
|
publish_publish_outcome(outcome_journal, completed, create=False)
|
||||||
|
return completed
|
||||||
|
|
||||||
|
|
||||||
def _remove_published_bundle(output_bundle: Path, expected_identity: tuple[int, int]) -> None:
|
def _remove_published_bundle(output_bundle: Path, expected_identity: tuple[int, int]) -> None:
|
||||||
try:
|
try:
|
||||||
observed = output_bundle.stat(follow_symlinks=False)
|
observed = output_bundle.stat(follow_symlinks=False)
|
||||||
|
|
@ -3289,8 +4062,21 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
build_push.add_argument("--docker-binary", type=Path, required=True)
|
build_push.add_argument("--docker-binary", type=Path, required=True)
|
||||||
build_push.add_argument("--docker-host", required=True)
|
build_push.add_argument("--docker-host", required=True)
|
||||||
build_push.add_argument("--docker-config", type=Path, required=True)
|
build_push.add_argument("--docker-config", type=Path, required=True)
|
||||||
|
build_push.add_argument("--gcloud-binary", type=Path, required=True)
|
||||||
build_push.add_argument("--output-receipt", type=Path, required=True)
|
build_push.add_argument("--output-receipt", type=Path, required=True)
|
||||||
|
build_push.add_argument("--outcome-journal", type=Path, required=True)
|
||||||
build_push.add_argument("--execute-staging-push", action="store_true")
|
build_push.add_argument("--execute-staging-push", action="store_true")
|
||||||
|
init_outcome = subparsers.add_parser("init-outcome")
|
||||||
|
init_outcome.add_argument("--image-input", type=Path, required=True)
|
||||||
|
init_outcome.add_argument("--output-journal", type=Path, required=True)
|
||||||
|
complete_outcome = subparsers.add_parser("complete-outcome")
|
||||||
|
complete_outcome.add_argument("--image-input", type=Path, required=True)
|
||||||
|
complete_outcome.add_argument("--outcome-journal", type=Path, required=True)
|
||||||
|
complete_outcome.add_argument("--build-push-receipt", type=Path, required=True)
|
||||||
|
complete_outcome.add_argument("--release", type=Path, required=True)
|
||||||
|
complete_outcome.add_argument("--build-outcome", required=True)
|
||||||
|
complete_outcome.add_argument("--finalize-outcome", required=True)
|
||||||
|
complete_outcome.add_argument("--cleanup-outcome", required=True)
|
||||||
finalize = subparsers.add_parser("finalize")
|
finalize = subparsers.add_parser("finalize")
|
||||||
finalize.add_argument("--image-input", type=Path, required=True)
|
finalize.add_argument("--image-input", type=Path, required=True)
|
||||||
finalize.add_argument("--build-push-receipt", type=Path, required=True)
|
finalize.add_argument("--build-push-receipt", type=Path, required=True)
|
||||||
|
|
@ -3375,9 +4161,38 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
docker_binary=args.docker_binary,
|
docker_binary=args.docker_binary,
|
||||||
docker_host=args.docker_host,
|
docker_host=args.docker_host,
|
||||||
docker_config=args.docker_config,
|
docker_config=args.docker_config,
|
||||||
|
gcloud_binary=args.gcloud_binary,
|
||||||
output_receipt=args.output_receipt.absolute(),
|
output_receipt=args.output_receipt.absolute(),
|
||||||
|
outcome_journal=args.outcome_journal.absolute(),
|
||||||
execute_staging_push=args.execute_staging_push,
|
execute_staging_push=args.execute_staging_push,
|
||||||
)
|
)
|
||||||
|
elif args.command == "init-outcome":
|
||||||
|
image_input = load_json(args.image_input, "image input")
|
||||||
|
result = build_initial_publish_outcome(image_input)
|
||||||
|
publish_publish_outcome(args.output_journal.absolute(), result, create=True)
|
||||||
|
elif args.command == "complete-outcome":
|
||||||
|
result = complete_publish_outcome(
|
||||||
|
image_input_path=args.image_input,
|
||||||
|
outcome_journal=args.outcome_journal.absolute(),
|
||||||
|
build_push_receipt_path=args.build_push_receipt.absolute(),
|
||||||
|
release_path=args.release.absolute(),
|
||||||
|
build_outcome=args.build_outcome,
|
||||||
|
finalize_outcome=args.finalize_outcome,
|
||||||
|
cleanup_outcome=args.cleanup_outcome,
|
||||||
|
)
|
||||||
|
if result["status"] != "pass":
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"status": "fail",
|
||||||
|
"schema": result["schema"],
|
||||||
|
"failure_phase": result["failure_phase"],
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 65
|
||||||
elif args.command == "finalize":
|
elif args.command == "finalize":
|
||||||
image_input = load_json(args.image_input, "image input")
|
image_input = load_json(args.image_input, "image input")
|
||||||
build_push_receipt = load_private_json(
|
build_push_receipt = load_private_json(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ from ops import gcp_leoclean_nosend_package as package
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
REVISION = package._git_commit(ROOT)
|
REVISION = package._git_commit(ROOT)
|
||||||
IMAGE_DIGEST = "2" * 64
|
IMAGE_DIGEST = "2" * 64
|
||||||
|
LOCAL_CONFIG_DIGEST = "sha256:" + "3" * 64
|
||||||
|
REMOTE_CONFIG_DIGEST = "sha256:" + "6" * 64
|
||||||
|
ROOTFS_LAYERS = ["sha256:" + "7" * 64, "sha256:" + "8" * 64]
|
||||||
CLAIM_CEILING = "Offline package proof only; live GCP identity, database access, restart, and parity remain unproven."
|
CLAIM_CEILING = "Offline package proof only; live GCP identity, database access, restart, and parity remain unproven."
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -394,7 +397,8 @@ def docker_inspection_fixture(
|
||||||
image_input: dict[str, object],
|
image_input: dict[str, object],
|
||||||
reference: str,
|
reference: str,
|
||||||
*,
|
*,
|
||||||
config_digest: str = "sha256:" + "3" * 64,
|
config_digest: str = LOCAL_CONFIG_DIGEST,
|
||||||
|
rootfs_layers: list[str] | None = None,
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
runtime = copy.deepcopy(package.IMAGE_RUNTIME_CONFIG)
|
runtime = copy.deepcopy(package.IMAGE_RUNTIME_CONFIG)
|
||||||
healthcheck = runtime["healthcheck"]
|
healthcheck = runtime["healthcheck"]
|
||||||
|
|
@ -404,6 +408,7 @@ def docker_inspection_fixture(
|
||||||
"Architecture": "amd64",
|
"Architecture": "amd64",
|
||||||
"Os": "linux",
|
"Os": "linux",
|
||||||
"Variant": None,
|
"Variant": None,
|
||||||
|
"RootFS": {"Type": "layers", "Layers": list(rootfs_layers or ROOTFS_LAYERS)},
|
||||||
"Config": {
|
"Config": {
|
||||||
"Labels": package.expected_image_labels(image_input),
|
"Labels": package.expected_image_labels(image_input),
|
||||||
"User": runtime["user"],
|
"User": runtime["user"],
|
||||||
|
|
@ -447,6 +452,32 @@ def docker_authority(tmp_path: Path) -> tuple[Path, str, Path]:
|
||||||
return docker_binary, "unix:///tmp/livingip-docker.sock", docker_config
|
return docker_binary, "unix:///tmp/livingip-docker.sock", docker_config
|
||||||
|
|
||||||
|
|
||||||
|
def publish_outcome_authority(
|
||||||
|
tmp_path: Path,
|
||||||
|
image_input: dict[str, object],
|
||||||
|
) -> tuple[Path, Path]:
|
||||||
|
gcloud_binary = tmp_path / "bin" / "gcloud"
|
||||||
|
gcloud_binary.parent.mkdir(exist_ok=True)
|
||||||
|
gcloud_binary.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
||||||
|
gcloud_binary.chmod(0o755)
|
||||||
|
outcome_journal = tmp_path / "publish-outcome.json"
|
||||||
|
package.publish_publish_outcome(
|
||||||
|
outcome_journal,
|
||||||
|
package.build_initial_publish_outcome(image_input),
|
||||||
|
create=True,
|
||||||
|
)
|
||||||
|
return gcloud_binary, outcome_journal
|
||||||
|
|
||||||
|
|
||||||
|
def mock_fresh_registry_reconcile(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
*,
|
||||||
|
manifest_digest: str = "sha256:" + IMAGE_DIGEST,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(package, "_registry_candidate_digest", lambda *_args: None)
|
||||||
|
monkeypatch.setattr(package, "_wait_registry_candidate_digest", lambda *_args: manifest_digest)
|
||||||
|
|
||||||
|
|
||||||
def docker_inventory_fixture(
|
def docker_inventory_fixture(
|
||||||
reference: str,
|
reference: str,
|
||||||
*,
|
*,
|
||||||
|
|
@ -505,19 +536,28 @@ def mocked_build_push_docker(
|
||||||
*,
|
*,
|
||||||
post_push_references: list[str] | None = None,
|
post_push_references: list[str] | None = None,
|
||||||
fail_after_side_effect: str | None = None,
|
fail_after_side_effect: str | None = None,
|
||||||
|
remote_config_digest: str = LOCAL_CONFIG_DIGEST,
|
||||||
|
remote_rootfs_layers: list[str] | None = None,
|
||||||
|
remote_verification_valid: bool = True,
|
||||||
) -> tuple[object, dict[str, object], list[tuple[list[str], dict[str, object]]]]:
|
) -> tuple[object, dict[str, object], list[tuple[list[str], dict[str, object]]]]:
|
||||||
config_digest = "sha256:" + "3" * 64
|
config_digest = LOCAL_CONFIG_DIGEST
|
||||||
exact_reference = f"{package.IMAGE_REPOSITORY}@sha256:{IMAGE_DIGEST}"
|
exact_reference = f"{package.IMAGE_REPOSITORY}@sha256:{IMAGE_DIGEST}"
|
||||||
references = list(post_push_references if post_push_references is not None else [exact_reference])
|
references = list(post_push_references if post_push_references is not None else [exact_reference])
|
||||||
build_tag = f"input-{image_input['input_sha256']}"
|
build_tag = f"input-{image_input['input_sha256']}"
|
||||||
build_reference = f"{package.LOCAL_BUILD_REPOSITORY}:{build_tag}"
|
build_reference = f"{package.LOCAL_BUILD_REPOSITORY}:{build_tag}"
|
||||||
candidate_tag = f"candidate-{config_digest.removeprefix('sha256:')}"
|
candidate_tag = f"candidate-{image_input['input_sha256']}"
|
||||||
candidate_reference = f"{package.IMAGE_REPOSITORY}:{candidate_tag}"
|
candidate_reference = f"{package.IMAGE_REPOSITORY}:{candidate_tag}"
|
||||||
state: dict[str, object] = {"build": False, "candidate": False, "digests": False, "pushes": 0}
|
state: dict[str, object] = {"build": False, "candidate": False, "digests": False, "pushes": 0}
|
||||||
calls: list[tuple[list[str], dict[str, object]]] = []
|
calls: list[tuple[list[str], dict[str, object]]] = []
|
||||||
|
|
||||||
def inspection(reference: str) -> bytes:
|
def inspection(reference: str) -> bytes:
|
||||||
value = docker_inspection_fixture(image_input, reference, config_digest=config_digest)
|
is_remote = "@sha256:" in reference
|
||||||
|
value = docker_inspection_fixture(
|
||||||
|
image_input,
|
||||||
|
reference,
|
||||||
|
config_digest=remote_config_digest if is_remote else config_digest,
|
||||||
|
rootfs_layers=remote_rootfs_layers if is_remote else ROOTFS_LAYERS,
|
||||||
|
)
|
||||||
value["RepoDigests"] = references if state["digests"] else []
|
value["RepoDigests"] = references if state["digests"] else []
|
||||||
return json.dumps([value]).encode()
|
return json.dumps([value]).encode()
|
||||||
|
|
||||||
|
|
@ -576,6 +616,52 @@ def mocked_build_push_docker(
|
||||||
stdout=("sha256:" + "9" * 64 + "\n").encode(),
|
stdout=("sha256:" + "9" * 64 + "\n").encode(),
|
||||||
stderr=b"",
|
stderr=b"",
|
||||||
)
|
)
|
||||||
|
if operation[0] == "pull":
|
||||||
|
assert operation[1:4] == ["--quiet", "--platform", package.PLATFORM]
|
||||||
|
assert operation[4] in references
|
||||||
|
state["digests"] = True
|
||||||
|
return subprocess.CompletedProcess(arguments, 0, stdout=b"pulled\n", stderr=b"")
|
||||||
|
if operation[0] == "run":
|
||||||
|
assert operation == [
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"--pull=never",
|
||||||
|
"--platform",
|
||||||
|
package.PLATFORM,
|
||||||
|
"--network=none",
|
||||||
|
"--read-only",
|
||||||
|
"--cap-drop=ALL",
|
||||||
|
"--security-opt=no-new-privileges:true",
|
||||||
|
"--pids-limit=64",
|
||||||
|
f"--user={package.RUNTIME_UID}:{package.RUNTIME_GID}",
|
||||||
|
exact_reference,
|
||||||
|
"verify-build",
|
||||||
|
image_input["runtime"]["teleo_git_head"],
|
||||||
|
image_input["runtime"]["artifact_sha256"],
|
||||||
|
image_input["identity"]["bundle_sha256"],
|
||||||
|
image_input["input_sha256"],
|
||||||
|
]
|
||||||
|
verification = (
|
||||||
|
{
|
||||||
|
"schema": "livingip.leocleanNoSendContainerVerification.v1",
|
||||||
|
"status": "pass",
|
||||||
|
"teleo_git_head": image_input["runtime"]["teleo_git_head"],
|
||||||
|
"artifact_sha256": image_input["runtime"]["artifact_sha256"],
|
||||||
|
"identity_sha256": image_input["identity"]["bundle_sha256"],
|
||||||
|
"input_sha256": image_input["input_sha256"],
|
||||||
|
"python_version": package.PYTHON_VERSION,
|
||||||
|
"postgres_version": package.POSTGRES_VERSION,
|
||||||
|
"cloudsql_ca_sha256": package.CA_SHA256,
|
||||||
|
}
|
||||||
|
if remote_verification_valid
|
||||||
|
else {"status": "fail"}
|
||||||
|
)
|
||||||
|
return subprocess.CompletedProcess(
|
||||||
|
arguments,
|
||||||
|
0,
|
||||||
|
stdout=(json.dumps(verification) + "\n").encode(),
|
||||||
|
stderr=b"",
|
||||||
|
)
|
||||||
if operation[:2] == ["image", "inspect"]:
|
if operation[:2] == ["image", "inspect"]:
|
||||||
reference = operation[2]
|
reference = operation[2]
|
||||||
present = (
|
present = (
|
||||||
|
|
@ -602,6 +688,98 @@ def mocked_build_push_docker(
|
||||||
return fake_run, state, calls
|
return fake_run, state, calls
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_candidate_lookup_uses_exact_resource_shape_and_command(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
candidate_tag = "candidate-" + "a" * 64
|
||||||
|
digest = "b" * 64
|
||||||
|
expected_tag = f"{package.ARTIFACT_PACKAGE_RESOURCE}/tags/{candidate_tag}"
|
||||||
|
fixture = [
|
||||||
|
{
|
||||||
|
"tag": expected_tag,
|
||||||
|
"version": f"{package.ARTIFACT_PACKAGE_RESOURCE}/versions/sha256:{digest}",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
observed: dict[str, object] = {}
|
||||||
|
|
||||||
|
def fake_gcloud(
|
||||||
|
binary: Path,
|
||||||
|
arguments: list[str],
|
||||||
|
*,
|
||||||
|
timeout: int = 60,
|
||||||
|
) -> subprocess.CompletedProcess[bytes]:
|
||||||
|
observed.update({"binary": binary, "arguments": arguments, "timeout": timeout})
|
||||||
|
return subprocess.CompletedProcess(arguments, 0, stdout=json.dumps(fixture).encode(), stderr=b"")
|
||||||
|
|
||||||
|
monkeypatch.setattr(package, "_run_gcloud", fake_gcloud)
|
||||||
|
gcloud_binary = tmp_path / "gcloud"
|
||||||
|
|
||||||
|
assert package._registry_candidate_digest(gcloud_binary, candidate_tag) == f"sha256:{digest}"
|
||||||
|
assert observed == {
|
||||||
|
"binary": gcloud_binary,
|
||||||
|
"arguments": [
|
||||||
|
"artifacts",
|
||||||
|
"docker",
|
||||||
|
"tags",
|
||||||
|
"list",
|
||||||
|
package.IMAGE_REPOSITORY,
|
||||||
|
"--project",
|
||||||
|
package.PROJECT,
|
||||||
|
f"--filter=tag={expected_tag}",
|
||||||
|
"--format=json(tag,version)",
|
||||||
|
"--limit=2",
|
||||||
|
],
|
||||||
|
"timeout": 60,
|
||||||
|
}
|
||||||
|
assert "--location" not in observed["arguments"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_candidate_lookup_rejects_mixed_exact_and_unexpected_rows(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
candidate_tag = "candidate-" + "a" * 64
|
||||||
|
digest = "b" * 64
|
||||||
|
expected_tag = f"{package.ARTIFACT_PACKAGE_RESOURCE}/tags/{candidate_tag}"
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"tag": expected_tag,
|
||||||
|
"version": f"{package.ARTIFACT_PACKAGE_RESOURCE}/versions/sha256:{digest}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": f"{package.ARTIFACT_PACKAGE_RESOURCE}/tags/candidate-{'c' * 64}",
|
||||||
|
"version": f"{package.ARTIFACT_PACKAGE_RESOURCE}/versions/sha256:{'d' * 64}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
monkeypatch.setattr(
|
||||||
|
package,
|
||||||
|
"_run_gcloud",
|
||||||
|
lambda *_args, **_kwargs: subprocess.CompletedProcess(
|
||||||
|
[],
|
||||||
|
0,
|
||||||
|
stdout=json.dumps(rows).encode(),
|
||||||
|
stderr=b"",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(package.PackageError, match="unexpected tag"):
|
||||||
|
package._registry_candidate_digest(tmp_path / "gcloud", candidate_tag)
|
||||||
|
|
||||||
|
|
||||||
|
def test_publish_outcome_loader_handles_short_reads(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
image_input = image_input_fixture(tmp_path)
|
||||||
|
_gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
|
expected = package.build_initial_publish_outcome(image_input)
|
||||||
|
original_read = os.read
|
||||||
|
monkeypatch.setattr(package.os, "read", lambda descriptor, size: original_read(descriptor, min(size, 7)))
|
||||||
|
|
||||||
|
assert package.load_publish_outcome(outcome_journal, image_input=image_input) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_image_input_binds_exact_runtime_identity_and_staging_target(tmp_path: Path) -> None:
|
def test_image_input_binds_exact_runtime_identity_and_staging_target(tmp_path: Path) -> None:
|
||||||
value = image_input_fixture(tmp_path)
|
value = image_input_fixture(tmp_path)
|
||||||
package.validate_image_input(value)
|
package.validate_image_input(value)
|
||||||
|
|
@ -1065,6 +1243,7 @@ def test_build_push_derives_receipt_and_retains_all_daemon_references(
|
||||||
repo_root.mkdir()
|
repo_root.mkdir()
|
||||||
output = tmp_path / "build-push-receipt.json"
|
output = tmp_path / "build-push-receipt.json"
|
||||||
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
||||||
|
gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
fake_run, state, calls = mocked_build_push_docker(image_input)
|
fake_run, state, calls = mocked_build_push_docker(image_input)
|
||||||
reviewed: list[tuple[Path, Path, dict[str, object]]] = []
|
reviewed: list[tuple[Path, Path, dict[str, object]]] = []
|
||||||
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
||||||
|
|
@ -1074,6 +1253,7 @@ def test_build_push_derives_receipt_and_retains_all_daemon_references(
|
||||||
lambda root, observed_context, value: reviewed.append((root, observed_context, value)),
|
lambda root, observed_context, value: reviewed.append((root, observed_context, value)),
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
||||||
|
mock_fresh_registry_reconcile(monkeypatch)
|
||||||
monkeypatch.setenv("DOCKER_TLS_VERIFY", "1")
|
monkeypatch.setenv("DOCKER_TLS_VERIFY", "1")
|
||||||
monkeypatch.setenv("BUILDKIT_PROGRESS", "plain")
|
monkeypatch.setenv("BUILDKIT_PROGRESS", "plain")
|
||||||
monkeypatch.setenv("BUILDX_BUILDER", "unreviewed")
|
monkeypatch.setenv("BUILDX_BUILDER", "unreviewed")
|
||||||
|
|
@ -1084,13 +1264,24 @@ def test_build_push_derives_receipt_and_retains_all_daemon_references(
|
||||||
docker_binary=docker_binary,
|
docker_binary=docker_binary,
|
||||||
docker_host=docker_host,
|
docker_host=docker_host,
|
||||||
docker_config=docker_config,
|
docker_config=docker_config,
|
||||||
|
gcloud_binary=gcloud_binary,
|
||||||
output_receipt=output,
|
output_receipt=output,
|
||||||
|
outcome_journal=outcome_journal,
|
||||||
execute_staging_push=True,
|
execute_staging_push=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert json.loads(output.read_text(encoding="utf-8")) == receipt
|
assert json.loads(output.read_text(encoding="utf-8")) == receipt
|
||||||
assert receipt["registry"]["manifest_digest"] == f"sha256:{IMAGE_DIGEST}"
|
assert receipt["registry"]["manifest_digest"] == f"sha256:{IMAGE_DIGEST}"
|
||||||
assert receipt["registry"]["manifest_digest"] != "sha256:" + "9" * 64
|
assert receipt["registry"]["manifest_digest"] != "sha256:" + "9" * 64
|
||||||
|
outcome = package.load_publish_outcome(outcome_journal, image_input=image_input)
|
||||||
|
assert outcome["candidate"]["tag"] == f"candidate-{image_input['input_sha256']}"
|
||||||
|
assert outcome["registry"] == {
|
||||||
|
"repository": package.IMAGE_REPOSITORY,
|
||||||
|
"preexisting": False,
|
||||||
|
"push_attempted": True,
|
||||||
|
"action": "fresh_push",
|
||||||
|
"observed_digest": f"sha256:{IMAGE_DIGEST}",
|
||||||
|
}
|
||||||
assert reviewed == [(repo_root, context, image_input), (repo_root, context, image_input)]
|
assert reviewed == [(repo_root, context, image_input), (repo_root, context, image_input)]
|
||||||
assert state == {"build": True, "candidate": True, "digests": True, "pushes": 1}
|
assert state == {"build": True, "candidate": True, "digests": True, "pushes": 1}
|
||||||
build = next(operation for operation, _kwargs in calls if operation and operation[0] == "build")
|
build = next(operation for operation, _kwargs in calls if operation and operation[0] == "build")
|
||||||
|
|
@ -1134,7 +1325,9 @@ def test_build_push_requires_explicit_execution_flag_before_validation(tmp_path:
|
||||||
docker_binary=tmp_path / "docker",
|
docker_binary=tmp_path / "docker",
|
||||||
docker_host="unix:///tmp/docker.sock",
|
docker_host="unix:///tmp/docker.sock",
|
||||||
docker_config=tmp_path,
|
docker_config=tmp_path,
|
||||||
|
gcloud_binary=tmp_path / "gcloud",
|
||||||
output_receipt=tmp_path / "receipt.json",
|
output_receipt=tmp_path / "receipt.json",
|
||||||
|
outcome_journal=tmp_path / "outcome.json",
|
||||||
execute_staging_push=False,
|
execute_staging_push=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1156,6 +1349,7 @@ def test_build_push_rejects_writable_docker_config_before_docker(
|
||||||
repo_root.mkdir()
|
repo_root.mkdir()
|
||||||
output = tmp_path / "writable-config-build-push-receipt.json"
|
output = tmp_path / "writable-config-build-push-receipt.json"
|
||||||
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
||||||
|
gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
docker_config.chmod(0o777)
|
docker_config.chmod(0o777)
|
||||||
docker_calls: list[list[str]] = []
|
docker_calls: list[list[str]] = []
|
||||||
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
||||||
|
|
@ -1173,7 +1367,9 @@ def test_build_push_rejects_writable_docker_config_before_docker(
|
||||||
docker_binary=docker_binary,
|
docker_binary=docker_binary,
|
||||||
docker_host=docker_host,
|
docker_host=docker_host,
|
||||||
docker_config=docker_config,
|
docker_config=docker_config,
|
||||||
|
gcloud_binary=gcloud_binary,
|
||||||
output_receipt=output,
|
output_receipt=output,
|
||||||
|
outcome_journal=outcome_journal,
|
||||||
execute_staging_push=True,
|
execute_staging_push=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1282,6 +1478,7 @@ def test_build_push_rejects_replaced_docker_config_before_mutation(
|
||||||
repo_root.mkdir()
|
repo_root.mkdir()
|
||||||
output = tmp_path / "replaced-config-build-push-receipt.json"
|
output = tmp_path / "replaced-config-build-push-receipt.json"
|
||||||
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
||||||
|
gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
retained_config = tmp_path / "retained-docker-config"
|
retained_config = tmp_path / "retained-docker-config"
|
||||||
docker_calls: list[list[str]] = []
|
docker_calls: list[list[str]] = []
|
||||||
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
||||||
|
|
@ -1306,7 +1503,9 @@ def test_build_push_rejects_replaced_docker_config_before_mutation(
|
||||||
docker_binary=docker_binary,
|
docker_binary=docker_binary,
|
||||||
docker_host=docker_host,
|
docker_host=docker_host,
|
||||||
docker_config=docker_config,
|
docker_config=docker_config,
|
||||||
|
gcloud_binary=gcloud_binary,
|
||||||
output_receipt=output,
|
output_receipt=output,
|
||||||
|
outcome_journal=outcome_journal,
|
||||||
execute_staging_push=True,
|
execute_staging_push=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1335,6 +1534,7 @@ def test_build_push_rejects_receipt_parent_symlink_alias_before_docker(
|
||||||
repo_root = tmp_path / "repo"
|
repo_root = tmp_path / "repo"
|
||||||
repo_root.mkdir()
|
repo_root.mkdir()
|
||||||
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
||||||
|
gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
docker_calls: list[list[str]] = []
|
docker_calls: list[list[str]] = []
|
||||||
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
||||||
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
||||||
|
|
@ -1351,7 +1551,9 @@ def test_build_push_rejects_receipt_parent_symlink_alias_before_docker(
|
||||||
docker_binary=docker_binary,
|
docker_binary=docker_binary,
|
||||||
docker_host=docker_host,
|
docker_host=docker_host,
|
||||||
docker_config=docker_config,
|
docker_config=docker_config,
|
||||||
|
gcloud_binary=gcloud_binary,
|
||||||
output_receipt=output,
|
output_receipt=output,
|
||||||
|
outcome_journal=outcome_journal,
|
||||||
execute_staging_push=True,
|
execute_staging_push=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1380,11 +1582,13 @@ def test_build_push_rechecks_receipt_context_separation_at_publication(
|
||||||
retained_parent = tmp_path / "retained-receipts"
|
retained_parent = tmp_path / "retained-receipts"
|
||||||
output = output_parent / "receipt.json"
|
output = output_parent / "receipt.json"
|
||||||
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
||||||
|
gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
fake_run, state, calls = mocked_build_push_docker(image_input)
|
fake_run, state, calls = mocked_build_push_docker(image_input)
|
||||||
original_publish = package.publish_build_push_receipt
|
original_publish = package.publish_build_push_receipt
|
||||||
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
||||||
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
||||||
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
||||||
|
mock_fresh_registry_reconcile(monkeypatch)
|
||||||
|
|
||||||
def alias_parent_before_publish(*args: object, **kwargs: object) -> None:
|
def alias_parent_before_publish(*args: object, **kwargs: object) -> None:
|
||||||
output_parent.rename(retained_parent)
|
output_parent.rename(retained_parent)
|
||||||
|
|
@ -1400,7 +1604,9 @@ def test_build_push_rechecks_receipt_context_separation_at_publication(
|
||||||
docker_binary=docker_binary,
|
docker_binary=docker_binary,
|
||||||
docker_host=docker_host,
|
docker_host=docker_host,
|
||||||
docker_config=docker_config,
|
docker_config=docker_config,
|
||||||
|
gcloud_binary=gcloud_binary,
|
||||||
output_receipt=output,
|
output_receipt=output,
|
||||||
|
outcome_journal=outcome_journal,
|
||||||
execute_staging_push=True,
|
execute_staging_push=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1437,6 +1643,7 @@ def test_build_push_fails_closed_without_one_exact_post_push_digest(
|
||||||
repo_root = tmp_path / "repo"
|
repo_root = tmp_path / "repo"
|
||||||
repo_root.mkdir()
|
repo_root.mkdir()
|
||||||
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
||||||
|
gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
fake_run, state, _calls = mocked_build_push_docker(
|
fake_run, state, _calls = mocked_build_push_docker(
|
||||||
image_input,
|
image_input,
|
||||||
post_push_references=references,
|
post_push_references=references,
|
||||||
|
|
@ -1444,6 +1651,12 @@ def test_build_push_fails_closed_without_one_exact_post_push_digest(
|
||||||
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
||||||
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
||||||
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
||||||
|
monkeypatch.setattr(package, "_registry_candidate_digest", lambda *_args: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
package,
|
||||||
|
"_wait_registry_candidate_digest",
|
||||||
|
lambda *_args: f"sha256:{IMAGE_DIGEST}" if references else None,
|
||||||
|
)
|
||||||
|
|
||||||
output = tmp_path / "failed-build-push-receipt.json"
|
output = tmp_path / "failed-build-push-receipt.json"
|
||||||
with pytest.raises(package.PackageError, match="build/push failed"):
|
with pytest.raises(package.PackageError, match="build/push failed"):
|
||||||
|
|
@ -1453,7 +1666,9 @@ def test_build_push_fails_closed_without_one_exact_post_push_digest(
|
||||||
docker_binary=docker_binary,
|
docker_binary=docker_binary,
|
||||||
docker_host=docker_host,
|
docker_host=docker_host,
|
||||||
docker_config=docker_config,
|
docker_config=docker_config,
|
||||||
|
gcloud_binary=gcloud_binary,
|
||||||
output_receipt=output,
|
output_receipt=output,
|
||||||
|
outcome_journal=outcome_journal,
|
||||||
execute_staging_push=True,
|
execute_staging_push=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1462,7 +1677,7 @@ def test_build_push_fails_closed_without_one_exact_post_push_digest(
|
||||||
assert state["candidate"] is True
|
assert state["candidate"] is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("failure", ["build", "tag", "push"])
|
@pytest.mark.parametrize("failure", ["build", "tag"])
|
||||||
def test_build_push_retains_daemon_references_when_cli_fails_after_side_effect(
|
def test_build_push_retains_daemon_references_when_cli_fails_after_side_effect(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
|
@ -1480,6 +1695,7 @@ def test_build_push_retains_daemon_references_when_cli_fails_after_side_effect(
|
||||||
repo_root = tmp_path / "repo"
|
repo_root = tmp_path / "repo"
|
||||||
repo_root.mkdir()
|
repo_root.mkdir()
|
||||||
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
||||||
|
gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
fake_run, state, calls = mocked_build_push_docker(
|
fake_run, state, calls = mocked_build_push_docker(
|
||||||
image_input,
|
image_input,
|
||||||
fail_after_side_effect=failure,
|
fail_after_side_effect=failure,
|
||||||
|
|
@ -1487,6 +1703,7 @@ def test_build_push_retains_daemon_references_when_cli_fails_after_side_effect(
|
||||||
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
||||||
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
||||||
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
||||||
|
mock_fresh_registry_reconcile(monkeypatch)
|
||||||
output = tmp_path / "failed-side-effect-receipt.json"
|
output = tmp_path / "failed-side-effect-receipt.json"
|
||||||
|
|
||||||
with pytest.raises(package.PackageError, match="build/push failed") as raised:
|
with pytest.raises(package.PackageError, match="build/push failed") as raised:
|
||||||
|
|
@ -1496,7 +1713,9 @@ def test_build_push_retains_daemon_references_when_cli_fails_after_side_effect(
|
||||||
docker_binary=docker_binary,
|
docker_binary=docker_binary,
|
||||||
docker_host=docker_host,
|
docker_host=docker_host,
|
||||||
docker_config=docker_config,
|
docker_config=docker_config,
|
||||||
|
gcloud_binary=gcloud_binary,
|
||||||
output_receipt=output,
|
output_receipt=output,
|
||||||
|
outcome_journal=outcome_journal,
|
||||||
execute_staging_push=True,
|
execute_staging_push=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1504,14 +1723,188 @@ def test_build_push_retains_daemon_references_when_cli_fails_after_side_effect(
|
||||||
assert not output.exists()
|
assert not output.exists()
|
||||||
assert state == {
|
assert state == {
|
||||||
"build": True,
|
"build": True,
|
||||||
"candidate": failure in {"tag", "push"},
|
"candidate": failure == "tag",
|
||||||
"digests": failure == "push",
|
"digests": False,
|
||||||
"pushes": 1 if failure == "push" else 0,
|
"pushes": 0,
|
||||||
}
|
}
|
||||||
removals = [operation for operation, _kwargs in calls if operation[:2] == ["image", "rm"]]
|
removals = [operation for operation, _kwargs in calls if operation[:2] == ["image", "rm"]]
|
||||||
assert removals == []
|
assert removals == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_push_reuses_verified_candidate_despite_nondeterministic_rootfs(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
image_input = image_input_fixture(tmp_path)
|
||||||
|
build_args = {
|
||||||
|
"ARTIFACT_SHA256": image_input["runtime"]["artifact_sha256"],
|
||||||
|
"IDENTITY_SHA256": image_input["identity"]["bundle_sha256"],
|
||||||
|
"INPUT_SHA256": image_input["input_sha256"],
|
||||||
|
"TELEO_REVISION": image_input["runtime"]["teleo_git_head"],
|
||||||
|
}
|
||||||
|
context = tmp_path / "context"
|
||||||
|
context.mkdir()
|
||||||
|
repo_root = tmp_path / "repo"
|
||||||
|
repo_root.mkdir()
|
||||||
|
output = tmp_path / "reused-build-push-receipt.json"
|
||||||
|
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
||||||
|
gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
|
fake_run, state, calls = mocked_build_push_docker(
|
||||||
|
image_input,
|
||||||
|
remote_config_digest=REMOTE_CONFIG_DIGEST,
|
||||||
|
remote_rootfs_layers=["sha256:" + "9" * 64],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
||||||
|
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
||||||
|
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
package,
|
||||||
|
"_registry_candidate_digest",
|
||||||
|
lambda *_args: f"sha256:{IMAGE_DIGEST}",
|
||||||
|
)
|
||||||
|
|
||||||
|
receipt = package.build_and_push_staging_image(
|
||||||
|
repo_root,
|
||||||
|
context,
|
||||||
|
docker_binary=docker_binary,
|
||||||
|
docker_host=docker_host,
|
||||||
|
docker_config=docker_config,
|
||||||
|
gcloud_binary=gcloud_binary,
|
||||||
|
output_receipt=output,
|
||||||
|
outcome_journal=outcome_journal,
|
||||||
|
execute_staging_push=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert receipt["local_image"]["config_digest"] == REMOTE_CONFIG_DIGEST
|
||||||
|
assert state == {"build": True, "candidate": False, "digests": True, "pushes": 0}
|
||||||
|
assert not any(operation[:2] == ["image", "push"] for operation, _kwargs in calls)
|
||||||
|
assert any(operation and operation[0] == "pull" for operation, _kwargs in calls)
|
||||||
|
assert any(operation and operation[0] == "run" for operation, _kwargs in calls)
|
||||||
|
outcome = package.load_publish_outcome(outcome_journal, image_input=image_input)
|
||||||
|
assert outcome["candidate"] == {
|
||||||
|
"tag": f"candidate-{image_input['input_sha256']}",
|
||||||
|
"reference": f"{package.IMAGE_REPOSITORY}:candidate-{image_input['input_sha256']}",
|
||||||
|
"local_build_config_digest": LOCAL_CONFIG_DIGEST,
|
||||||
|
}
|
||||||
|
assert outcome["registry"] == {
|
||||||
|
"repository": package.IMAGE_REPOSITORY,
|
||||||
|
"preexisting": True,
|
||||||
|
"push_attempted": False,
|
||||||
|
"action": "reused",
|
||||||
|
"observed_digest": f"sha256:{IMAGE_DIGEST}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_push_rejects_preexisting_failed_runtime_verification_without_claiming_reuse(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
image_input = image_input_fixture(tmp_path)
|
||||||
|
build_args = {
|
||||||
|
"ARTIFACT_SHA256": image_input["runtime"]["artifact_sha256"],
|
||||||
|
"IDENTITY_SHA256": image_input["identity"]["bundle_sha256"],
|
||||||
|
"INPUT_SHA256": image_input["input_sha256"],
|
||||||
|
"TELEO_REVISION": image_input["runtime"]["teleo_git_head"],
|
||||||
|
}
|
||||||
|
context = tmp_path / "context"
|
||||||
|
context.mkdir()
|
||||||
|
repo_root = tmp_path / "repo"
|
||||||
|
repo_root.mkdir()
|
||||||
|
output = tmp_path / "mismatched-build-push-receipt.json"
|
||||||
|
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
||||||
|
gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
|
fake_run, state, calls = mocked_build_push_docker(
|
||||||
|
image_input,
|
||||||
|
remote_config_digest=REMOTE_CONFIG_DIGEST,
|
||||||
|
remote_verification_valid=False,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
||||||
|
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
||||||
|
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
package,
|
||||||
|
"_registry_candidate_digest",
|
||||||
|
lambda *_args: f"sha256:{IMAGE_DIGEST}",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(package.PackageError, match="build/push failed"):
|
||||||
|
package.build_and_push_staging_image(
|
||||||
|
repo_root,
|
||||||
|
context,
|
||||||
|
docker_binary=docker_binary,
|
||||||
|
docker_host=docker_host,
|
||||||
|
docker_config=docker_config,
|
||||||
|
gcloud_binary=gcloud_binary,
|
||||||
|
output_receipt=output,
|
||||||
|
outcome_journal=outcome_journal,
|
||||||
|
execute_staging_push=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not output.exists()
|
||||||
|
assert state == {"build": True, "candidate": False, "digests": True, "pushes": 0}
|
||||||
|
assert not any(operation[:2] == ["image", "push"] for operation, _kwargs in calls)
|
||||||
|
outcome = package.load_publish_outcome(outcome_journal, image_input=image_input)
|
||||||
|
assert outcome["status"] == "fail"
|
||||||
|
assert outcome["registry"]["preexisting"] is True
|
||||||
|
assert outcome["registry"]["observed_digest"] == f"sha256:{IMAGE_DIGEST}"
|
||||||
|
assert outcome["registry"]["action"] == "observed"
|
||||||
|
assert all(event["status"] != "reused" for event in outcome["events"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_push_validation_failure_retains_mutation_evidence(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
image_input = image_input_fixture(tmp_path)
|
||||||
|
build_args = {
|
||||||
|
"ARTIFACT_SHA256": image_input["runtime"]["artifact_sha256"],
|
||||||
|
"IDENTITY_SHA256": image_input["identity"]["bundle_sha256"],
|
||||||
|
"INPUT_SHA256": image_input["input_sha256"],
|
||||||
|
"TELEO_REVISION": image_input["runtime"]["teleo_git_head"],
|
||||||
|
}
|
||||||
|
context = tmp_path / "context"
|
||||||
|
context.mkdir()
|
||||||
|
repo_root = tmp_path / "repo"
|
||||||
|
repo_root.mkdir()
|
||||||
|
output = tmp_path / "post-push-failure-receipt.json"
|
||||||
|
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
|
||||||
|
gcloud_binary, outcome_journal = publish_outcome_authority(tmp_path, image_input)
|
||||||
|
fake_run, state, calls = mocked_build_push_docker(
|
||||||
|
image_input,
|
||||||
|
remote_verification_valid=False,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
|
||||||
|
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
|
||||||
|
monkeypatch.setattr(package.subprocess, "run", fake_run)
|
||||||
|
mock_fresh_registry_reconcile(monkeypatch)
|
||||||
|
|
||||||
|
with pytest.raises(package.PackageError, match="build/push failed"):
|
||||||
|
package.build_and_push_staging_image(
|
||||||
|
repo_root,
|
||||||
|
context,
|
||||||
|
docker_binary=docker_binary,
|
||||||
|
docker_host=docker_host,
|
||||||
|
docker_config=docker_config,
|
||||||
|
gcloud_binary=gcloud_binary,
|
||||||
|
output_receipt=output,
|
||||||
|
outcome_journal=outcome_journal,
|
||||||
|
execute_staging_push=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not output.exists()
|
||||||
|
assert state == {"build": True, "candidate": True, "digests": True, "pushes": 1}
|
||||||
|
assert any(operation[:2] == ["image", "push"] for operation, _kwargs in calls)
|
||||||
|
outcome = package.load_publish_outcome(outcome_journal, image_input=image_input)
|
||||||
|
assert outcome["status"] == "fail"
|
||||||
|
assert outcome["registry"] == {
|
||||||
|
"repository": package.IMAGE_REPOSITORY,
|
||||||
|
"preexisting": False,
|
||||||
|
"push_attempted": True,
|
||||||
|
"action": "indeterminate",
|
||||||
|
"observed_digest": f"sha256:{IMAGE_DIGEST}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_build_push_receipt_publication_is_atomic_repeatable_and_no_clobber(tmp_path: Path) -> None:
|
def test_build_push_receipt_publication_is_atomic_repeatable_and_no_clobber(tmp_path: Path) -> None:
|
||||||
image_input = image_input_fixture(tmp_path)
|
image_input = image_input_fixture(tmp_path)
|
||||||
receipt = build_push_receipt_fixture(image_input)
|
receipt = build_push_receipt_fixture(image_input)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ def test_release_workflow_is_manual_main_only_and_serialized() -> None:
|
||||||
assert 'test "${GITHUB_EVENT_NAME}" = "workflow_dispatch"' in guard
|
assert 'test "${GITHUB_EVENT_NAME}" = "workflow_dispatch"' in guard
|
||||||
assert 'test "${GITHUB_REF}" = "refs/heads/main"' in guard
|
assert 'test "${GITHUB_REF}" = "refs/heads/main"' in guard
|
||||||
assert 'test "${GITHUB_REF_TYPE}" = "branch"' in guard
|
assert 'test "${GITHUB_REF_TYPE}" = "branch"' in guard
|
||||||
|
assert 'test "${GITHUB_REPOSITORY}" = "living-ip/teleo-infrastructure"' in guard
|
||||||
|
assert '"living-ip/teleo-infrastructure/.github/workflows/gcp-leoclean-nosend-release.yml@refs/heads/main"' in guard
|
||||||
|
|
||||||
|
|
||||||
def test_release_workflow_checks_out_exact_clean_revision_without_persisted_token() -> None:
|
def test_release_workflow_checks_out_exact_clean_revision_without_persisted_token() -> None:
|
||||||
|
|
@ -56,11 +58,15 @@ def test_release_workflow_smokes_before_requesting_cloud_identity() -> None:
|
||||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||||
|
|
||||||
smoke = workflow.index("Smoke exact no-send OCI surface")
|
smoke = workflow.index("Smoke exact no-send OCI surface")
|
||||||
|
prepare = workflow.index("Prepare exact candidate context")
|
||||||
|
initialize = workflow.index("Initialize pre-mutation publish outcome")
|
||||||
auth = workflow.index("Authenticate to Google Cloud through WIF")
|
auth = workflow.index("Authenticate to Google Cloud through WIF")
|
||||||
push = workflow.index("Build and push one immutable candidate")
|
push = workflow.index("Build and push one immutable candidate")
|
||||||
finalize = workflow.index("Finalize and validate release v3 bundle")
|
finalize = workflow.index("Finalize and validate release v3 bundle")
|
||||||
upload = workflow.index("Upload exact build and release evidence")
|
cleanup = workflow.index("Remove ephemeral Docker authentication")
|
||||||
assert smoke < auth < push < finalize < upload
|
complete = workflow.index("Finalize publish outcome")
|
||||||
|
upload = workflow.index("Upload exact available build and release evidence")
|
||||||
|
assert smoke < prepare < initialize < auth < push < finalize < cleanup < complete < upload
|
||||||
assert "scripts/run_leoclean_nosend_runtime_canary.sh" in workflow
|
assert "scripts/run_leoclean_nosend_runtime_canary.sh" in workflow
|
||||||
assert "scripts/run_gcp_leoclean_nosend_oci_smoke.py" 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["input_sha256"] == image_input["input_sha256"]' in workflow
|
||||||
|
|
@ -93,6 +99,22 @@ def test_release_workflow_uses_exact_wif_artifact_builder_without_repository_sec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_release_workflow_keeps_runner_temp_out_of_job_environment() -> None:
|
||||||
|
workflow = load_workflow()
|
||||||
|
jobs = workflow["jobs"]
|
||||||
|
assert isinstance(jobs, dict)
|
||||||
|
publish = jobs["publish"]
|
||||||
|
assert isinstance(publish, dict)
|
||||||
|
root_env = workflow["env"]
|
||||||
|
job_env = publish.get("env", {})
|
||||||
|
|
||||||
|
assert isinstance(root_env, dict)
|
||||||
|
assert isinstance(job_env, dict)
|
||||||
|
assert all("runner.temp" not in value for value in [*root_env.values(), *job_env.values()])
|
||||||
|
initialize = workflow_step("Initialize private release paths")["run"]
|
||||||
|
assert 'release_root="${RUNNER_TEMP}/leoclean-nosend-release"' in initialize
|
||||||
|
|
||||||
|
|
||||||
def test_release_workflow_requires_immutable_repository_and_package_owned_candidate_push() -> None:
|
def test_release_workflow_requires_immutable_repository_and_package_owned_candidate_push() -> None:
|
||||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||||
immutable = workflow_step("Require immutable fixed Artifact Registry repository")["run"]
|
immutable = workflow_step("Require immutable fixed Artifact Registry repository")["run"]
|
||||||
|
|
@ -104,27 +126,39 @@ def test_release_workflow_requires_immutable_repository_and_package_owned_candid
|
||||||
assert "--execute-staging-push" in push
|
assert "--execute-staging-push" in push
|
||||||
assert "--docker-binary /usr/bin/docker" in push
|
assert "--docker-binary /usr/bin/docker" in push
|
||||||
assert "--docker-host unix:///var/run/docker.sock" in push
|
assert "--docker-host unix:///var/run/docker.sock" in push
|
||||||
|
assert '--gcloud-binary "$(command -v gcloud)"' in push
|
||||||
|
assert '--outcome-journal "${PUBLISH_OUTCOME}"' in push
|
||||||
assert "docker push" not in workflow
|
assert "docker push" not in workflow
|
||||||
assert "docker tag" not in workflow
|
assert "docker tag" not in workflow
|
||||||
assert "candidate-" not in workflow
|
assert "candidate-" not in workflow
|
||||||
|
|
||||||
|
|
||||||
def test_release_workflow_finalizes_v3_and_uploads_only_allowlisted_evidence() -> None:
|
def test_release_workflow_always_finalizes_journal_and_uploads_only_allowlisted_evidence() -> None:
|
||||||
finalize = workflow_step("Finalize and validate release v3 bundle")["run"]
|
finalize = workflow_step("Finalize and validate release v3 bundle")["run"]
|
||||||
upload = workflow_step("Upload exact build and release evidence")
|
cleanup = workflow_step("Remove ephemeral Docker authentication")
|
||||||
|
complete = workflow_step("Finalize publish outcome")
|
||||||
|
upload = workflow_step("Upload exact available build and release evidence")
|
||||||
paths = upload["with"]["path"].splitlines()
|
paths = upload["with"]["path"].splitlines()
|
||||||
|
|
||||||
assert "ops/gcp_leoclean_nosend_package.py finalize" in finalize
|
assert "ops/gcp_leoclean_nosend_package.py finalize" in finalize
|
||||||
assert "ops/gcp_leoclean_nosend_package.py validate" in finalize
|
assert "ops/gcp_leoclean_nosend_package.py validate" in finalize
|
||||||
assert 'assert release["schema"] == "livingip.leocleanNoSendRelease.v3"' in finalize
|
assert 'assert release["schema"] == "livingip.leocleanNoSendRelease.v3"' in finalize
|
||||||
|
assert cleanup["if"] == "always()"
|
||||||
|
assert complete["if"] == "always() && steps.init_outcome.outcome == 'success'"
|
||||||
|
assert "ops/gcp_leoclean_nosend_package.py complete-outcome" in complete["run"]
|
||||||
|
assert '--build-outcome "${{ steps.build_push.outcome }}"' in complete["run"]
|
||||||
|
assert '--finalize-outcome "${{ steps.finalize.outcome }}"' in complete["run"]
|
||||||
|
assert '--cleanup-outcome "${{ steps.cleanup.outcome }}"' in complete["run"]
|
||||||
|
assert upload["if"] == "always()"
|
||||||
assert paths == [
|
assert paths == [
|
||||||
"${{ env.RUNTIME_RECEIPT }}",
|
"${{ runner.temp }}/leoclean-nosend-release/evidence/runtime-receipt.json",
|
||||||
"${{ env.OCI_SMOKE_RECEIPT }}",
|
"${{ runner.temp }}/leoclean-nosend-release/evidence/oci-smoke.json",
|
||||||
"${{ env.BUILD_PUSH_RECEIPT }}",
|
"${{ runner.temp }}/leoclean-nosend-release/evidence/publish-outcome.json",
|
||||||
"${{ env.RELEASE_BUNDLE }}/release.json",
|
"${{ runner.temp }}/leoclean-nosend-release/receipts/build-push-receipt.json",
|
||||||
"${{ env.RELEASE_BUNDLE }}/leoclean-gcp-nosend.service",
|
"${{ runner.temp }}/leoclean-nosend-release/final/release-v3/release.json",
|
||||||
|
"${{ runner.temp }}/leoclean-nosend-release/final/release-v3/leoclean-gcp-nosend.service",
|
||||||
]
|
]
|
||||||
assert upload["with"]["if-no-files-found"] == "error"
|
assert upload["with"]["if-no-files-found"] == "warn"
|
||||||
assert upload["with"]["retention-days"] == "30"
|
assert upload["with"]["retention-days"] == "30"
|
||||||
assert "*" not in upload["with"]["path"]
|
assert "*" not in upload["with"]["path"]
|
||||||
assert "google-github-actions" not in upload["with"]["path"]
|
assert "google-github-actions" not in upload["with"]["path"]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue