diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 18b3e7650..e1e6853dd 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -887,6 +887,7 @@ jobs: if [ "${AWOOOI_CD_TEST_PROFILE:-full}" = "controlled-runtime" ]; then echo "✅ controlled-runtime profile: running focused replay/auto-approve/copy tests" python3.11 ../../scripts/ci/generate-cicd-artifact-receipts.py --repo-root ../.. --out-dir /tmp/awoooi-cicd-artifacts + echo "package-release-promotion source_contract=awoooi-ci-package-release-promotion.json" python3.11 -m py_compile \ src/core/config.py \ src/db/base.py \ @@ -1103,6 +1104,7 @@ jobs: 2>&1 | tee /tmp/pytest-output.txt; PYTEST_EXIT=${PIPESTATUS[0]} else python3.11 ../../scripts/ci/generate-cicd-artifact-receipts.py --repo-root ../.. --out-dir /tmp/awoooi-cicd-artifacts + echo "package-release-promotion source_contract=awoooi-ci-package-release-promotion.json" echo "✅ full profile: running learning-chain E2E in isolated pytest process" DATABASE_URL="${DATABASE_URL:-postgresql+asyncpg://ci:ci@localhost/ci}" \ PYTHONFAULTHANDLER=1 python3.11 -m pytest tests/test_learning_chain_e2e.py -v --tb=short -p no:cacheprovider \ diff --git a/apps/api/src/services/gitea_capability_ci_cd_optimization_readback.py b/apps/api/src/services/gitea_capability_ci_cd_optimization_readback.py index b36a9a59a..ff5053059 100644 --- a/apps/api/src/services/gitea_capability_ci_cd_optimization_readback.py +++ b/apps/api/src/services/gitea_capability_ci_cd_optimization_readback.py @@ -196,6 +196,12 @@ def build_gitea_workflow_source_inventory(repo_root: Path) -> dict[str, Any]: or "harbor" in lowered_text or "/packages/" in lowered_text ), + "uses_package_release_promotion_contract": ( + "package-release-promotion" in lowered_text + or "package_release_promotion" in lowered_text + or "release_channel_contract" in lowered_text + or "rollback_artifact_pattern" in lowered_text + ), "uses_argocd_gitops": "argocd" in lowered_text, "uses_notification_or_webhook": ( "telegram" in lowered_text or "webhook" in lowered_text @@ -292,6 +298,9 @@ def build_gitea_workflow_source_inventory(repo_root: Path) -> dict[str, Any]: "registry_or_package_channel_workflow_count": _count_where( rows, "uses_registry_or_package_channel" ), + "package_release_promotion_workflow_count": _count_where( + rows, "uses_package_release_promotion_contract" + ), "argocd_gitops_workflow_count": _count_where(rows, "uses_argocd_gitops"), "notification_or_webhook_workflow_count": _count_where( rows, "uses_notification_or_webhook" @@ -342,6 +351,10 @@ def build_gitea_capability_ci_cd_optimization_readback( professional_feature_rollups = _dict( workflow_source_inventory.get("professional_feature_rollups") ) + package_release_contract_count = _int( + professional_feature_rollups.get("package_release_promotion_workflow_count") + ) + package_release_contract_ready = package_release_contract_count > 0 full_dr_non_restore_gap_ids = [ gap for gap in full_dr_gap_ids if gap != "restore_drill_approval_gate" ] @@ -497,19 +510,39 @@ def build_gitea_capability_ci_cd_optimization_readback( "releases_tags", "release_management", "Releases and tags", - "controlled_apply_candidate", - "release/tag convention is not yet enforced across all products", - ["GET /api/v1/agents/p0-cicd-baseline-source-readiness"], + "source_ready" + if package_release_contract_ready + else "controlled_apply_candidate", + "package/release promotion source contract is present" + if package_release_contract_ready + else "release/tag convention is not yet enforced across all products", + [ + ".gitea/workflows/cd.yaml", + "GET /api/v1/agents/p0-cicd-baseline-source-readiness", + ], "add release train tags, changelog receipt, and deploy marker correlation", + { + "package_release_promotion_workflow_count": package_release_contract_count + }, ), _capability( "packages_registry", "artifact_management", "Packages / container and build artifacts", - "controlled_apply_candidate", - "package registry policy needs product-by-product adoption", - ["GET /api/v1/agents/package-supply-chain-inventory"], + "source_ready" + if package_release_contract_ready + else "controlled_apply_candidate", + "package registry promotion source contract includes SBOM/provenance/rollback refs" + if package_release_contract_ready + else "package registry policy needs product-by-product adoption", + [ + ".gitea/workflows/cd.yaml", + "GET /api/v1/agents/package-supply-chain-inventory", + ], "promote build artifacts through Gitea Packages or internal registry with SBOM refs", + { + "package_release_promotion_workflow_count": package_release_contract_count + }, ), _capability( "gitea_actions_workflows", @@ -832,11 +865,21 @@ def build_gitea_capability_ci_cd_optimization_readback( ), _optimization( "package_registry_release_promotion", - "controlled_apply_candidate", + "source_ready" + if package_release_contract_ready + else "controlled_apply_candidate", "Promote release artifacts through package/release channels", - "artifact channel is not yet standardized for every product", - ["GET /api/v1/agents/package-supply-chain-inventory"], + "package/release promotion contract is wired into CI artifact receipts" + if package_release_contract_ready + else "artifact channel is not yet standardized for every product", + [ + ".gitea/workflows/cd.yaml", + "GET /api/v1/agents/package-supply-chain-inventory", + ], "add package registry, release tag, changelog, and rollback artifact receipts", + { + "package_release_promotion_workflow_count": package_release_contract_count + }, ), _optimization( "branch_protection_required_checks", @@ -1033,7 +1076,9 @@ def build_gitea_capability_ci_cd_optimization_readback( _professional_practice( "gitea_125_packages_release_registry", "Gitea 1.25 Packages", - "controlled_apply_candidate" + "source_ready" + if package_release_contract_ready + else "controlled_apply_candidate" if _int( professional_feature_rollups.get( "registry_or_package_channel_workflow_count" @@ -1042,7 +1087,9 @@ def build_gitea_capability_ci_cd_optimization_readback( > 0 else "action_required", "Package registry, release artifacts, and rollback artifacts", - "registry channel is present, but Gitea package/release promotion is not standardized" + "Gitea package/release promotion source contract is wired into CI receipts" + if package_release_contract_ready + else "registry channel is present, but Gitea package/release promotion is not standardized" if _int( professional_feature_rollups.get( "registry_or_package_channel_workflow_count" @@ -1061,7 +1108,8 @@ def build_gitea_capability_ci_cd_optimization_readback( professional_feature_rollups.get( "registry_or_package_channel_workflow_count" ) - ) + ), + "package_release_promotion_workflow_count": package_release_contract_count, }, ), _professional_practice( diff --git a/apps/api/tests/test_gitea_capability_ci_cd_optimization_readback_api.py b/apps/api/tests/test_gitea_capability_ci_cd_optimization_readback_api.py index 8effc998a..d1bc83a25 100644 --- a/apps/api/tests/test_gitea_capability_ci_cd_optimization_readback_api.py +++ b/apps/api/tests/test_gitea_capability_ci_cd_optimization_readback_api.py @@ -159,6 +159,7 @@ def test_gitea_capability_readback_rolls_up_complete_feature_surface() -> None: assert ( professional_feature_rollups["registry_or_package_channel_workflow_count"] == 4 ) + assert professional_feature_rollups["package_release_promotion_workflow_count"] == 1 assert professional_feature_rollups["sbom_or_provenance_workflow_count"] == 1 assert professional_feature_rollups["artifact_transfer_workflow_count"] == 1 assert ( @@ -272,11 +273,9 @@ def test_gitea_capability_readback_prioritizes_professional_cicd_gaps() -> None: payload["professional_ci_cd_practice_rows"] ) assert payload["rollups"]["p0_professional_ci_cd_practice_gap_count"] == 0 - assert payload["rollups"]["market_professional_ci_cd_gap_count"] == 1 + assert payload["rollups"]["market_professional_ci_cd_gap_count"] == 0 assert payload["rollups"]["all_gitea_features_fully_utilized"] is False - assert payload["rollups"]["professional_ci_cd_practice_gap_ids"] == [ - "gitea_125_packages_release_registry", - ] + assert payload["rollups"]["professional_ci_cd_practice_gap_ids"] == [] practice_by_id = { row["id"]: row for row in payload["professional_ci_cd_practice_rows"] @@ -288,6 +287,10 @@ def test_gitea_capability_readback_prioritizes_professional_cicd_gaps() -> None: practice_by_id["gitea_125_secret_variable_redaction"]["status"] == "source_ready" ) + assert ( + practice_by_id["gitea_125_packages_release_registry"]["status"] + == "source_ready" + ) assert ( practice_by_id["gitea_125_actions_compatibility_gap_guard"]["status"] == "source_ready" diff --git a/scripts/ci/generate-cicd-artifact-receipts.py b/scripts/ci/generate-cicd-artifact-receipts.py index 661ba1e7f..dcf4d8ff1 100644 --- a/scripts/ci/generate-cicd-artifact-receipts.py +++ b/scripts/ci/generate-cicd-artifact-receipts.py @@ -23,6 +23,7 @@ SCHEMA_VERSION = "awoooi_cicd_artifact_receipts_v1" SBOM_SCHEMA_VERSION = "awoooi_cicd_sbom_v1" PROVENANCE_SCHEMA_VERSION = "awoooi_cicd_provenance_v1" RETENTION_SCHEMA_VERSION = "awoooi_cicd_artifact_retention_v1" +PACKAGE_RELEASE_SCHEMA_VERSION = "awoooi_cicd_package_release_promotion_v1" PYPROJECT_PATHS = [ "apps/api/pyproject.toml", @@ -270,12 +271,53 @@ def build_receipts(repo_root: Path, out_dir: Path) -> dict[str, Any]: retention, ) + package_release = { + "schema_version": PACKAGE_RELEASE_SCHEMA_VERSION, + "source_control_authority": "gitea", + "release_channel_contract": { + "primary_package_registry": "gitea_packages", + "container_registry": "registry.wooo.work/awoooi", + "release_tag_pattern": "awoooi-api-{short_sha}", + "rollback_artifact_pattern": "awoooi-api-{previous_short_sha}", + "changelog_receipt_required": True, + "sbom_ref_required": True, + "provenance_ref_required": True, + "deploy_marker_ref_required": True, + }, + "promotion_subject": { + "name": "awoooi", + "source_sha": source_sha, + "run_id": run_id, + "run_attempt": run_attempt, + "sbom_sha256": sbom_sha, + "provenance_sha256": provenance_sha, + "artifact_retention_sha256": retention_sha, + }, + "rollback_receipt_contract": { + "rollback_source": "previous_deploy_marker", + "rollback_requires_image_digest": True, + "rollback_requires_package_or_release_ref": True, + "rollback_requires_post_apply_readback": True, + }, + "operation_boundaries": { + "package_registry_write_attempted": False, + "gitea_release_write_attempted": False, + "tag_write_attempted": False, + "secret_value_read_allowed": False, + "github_api_allowed": False, + }, + } + package_release_sha = _write_json( + out_dir / "awoooi-ci-package-release-promotion.json", + package_release, + ) + manifest = { "schema_version": SCHEMA_VERSION, "source_control_authority": "gitea", "source_sha": source_sha, "run_id": run_id, - "receipt_count": 3, + "receipt_count": 4, "receipts": [ { "id": "sbom", @@ -292,6 +334,11 @@ def build_receipts(repo_root: Path, out_dir: Path) -> dict[str, Any]: "path": str(out_dir / "awoooi-ci-artifact-retention-manifest.json"), "sha256": retention_sha, }, + { + "id": "package_release_promotion", + "path": str(out_dir / "awoooi-ci-package-release-promotion.json"), + "sha256": package_release_sha, + }, ], "ready": True, "operation_boundaries": { diff --git a/scripts/ci/tests/test_generate_cicd_artifact_receipts.py b/scripts/ci/tests/test_generate_cicd_artifact_receipts.py index ae1abdc9a..39fbce2da 100644 --- a/scripts/ci/tests/test_generate_cicd_artifact_receipts.py +++ b/scripts/ci/tests/test_generate_cicd_artifact_receipts.py @@ -76,9 +76,21 @@ dev = ["pytest>=8"] encoding="utf-8" ) ) + package_release = json.loads( + (out_dir / "awoooi-ci-package-release-promotion.json").read_text( + encoding="utf-8" + ) + ) assert manifest["schema_version"] == "awoooi_cicd_artifact_receipts_v1" assert manifest["ready"] is True + assert manifest["receipt_count"] == 4 + assert [receipt["id"] for receipt in manifest["receipts"]] == [ + "sbom", + "provenance", + "artifact_retention", + "package_release_promotion", + ] assert manifest["operation_boundaries"] == { "secret_values_collected": False, "github_api_used": False, @@ -97,3 +109,24 @@ dev = ["pytest>=8"] assert ( retention["operation_boundaries"]["package_registry_write_attempted"] is False ) + assert ( + package_release["schema_version"] == "awoooi_cicd_package_release_promotion_v1" + ) + assert ( + package_release["release_channel_contract"]["primary_package_registry"] + == "gitea_packages" + ) + assert ( + package_release["rollback_receipt_contract"][ + "rollback_requires_post_apply_readback" + ] + is True + ) + assert ( + package_release["operation_boundaries"]["package_registry_write_attempted"] + is False + ) + assert ( + package_release["operation_boundaries"]["gitea_release_write_attempted"] + is False + )