diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 60af4644d..de71875a4 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -1733,6 +1733,25 @@ jobs: docker push ${{ env.HARBOR }}/awoooi/api:${{ github.sha }} run_docker_push_step api_push_latest "${DOCKER_API_PUSH_TIMEOUT_SECONDS:-600}" \ docker push ${{ env.HARBOR }}/awoooi/api:latest + API_IMAGE_DIGEST="$( + docker manifest inspect --verbose \ + "${{ env.HARBOR }}/awoooi/api:${{ github.sha }}" | \ + python3 -c ' + import json + import re + import sys + + payload = json.load(sys.stdin) + digest = payload.get("Descriptor", {}).get("digest", "") + if not re.fullmatch(r"sha256:[0-9a-f]{64}", digest): + raise SystemExit("api_harbor_digest_invalid") + print(digest) + ' + )" + docker manifest inspect \ + "${{ env.HARBOR }}/awoooi/api@${API_IMAGE_DIGEST}" >/dev/null + echo "API_IMAGE_DIGEST=${API_IMAGE_DIGEST}" >> "${GITHUB_ENV}" + echo "api_harbor_digest_verified=${API_IMAGE_DIGEST}" # 2026-03-31 ogt: 移除中間通知,減少訊息雜訊 @@ -1848,6 +1867,25 @@ jobs: docker push ${{ env.HARBOR }}/awoooi/web:${{ github.sha }} run_docker_push_step web_push_latest "${DOCKER_WEB_PUSH_TIMEOUT_SECONDS:-600}" \ docker push ${{ env.HARBOR }}/awoooi/web:latest + WEB_IMAGE_DIGEST="$( + docker manifest inspect --verbose \ + "${{ env.HARBOR }}/awoooi/web:${{ github.sha }}" | \ + python3 -c ' + import json + import re + import sys + + payload = json.load(sys.stdin) + digest = payload.get("Descriptor", {}).get("digest", "") + if not re.fullmatch(r"sha256:[0-9a-f]{64}", digest): + raise SystemExit("web_harbor_digest_invalid") + print(digest) + ' + )" + docker manifest inspect \ + "${{ env.HARBOR }}/awoooi/web@${WEB_IMAGE_DIGEST}" >/dev/null + echo "WEB_IMAGE_DIGEST=${WEB_IMAGE_DIGEST}" >> "${GITHUB_ENV}" + echo "web_harbor_digest_verified=${WEB_IMAGE_DIGEST}" - name: Release Docker Build Lock if: always() @@ -2611,6 +2649,16 @@ jobs: export IMAGE_TAG="${{ github.sha }}" HARBOR=192.168.0.110:5000 + if ! printf '%s\n' "${API_IMAGE_DIGEST:-}" | \ + grep -Eq '^sha256:[0-9a-f]{64}$'; then + echo "BLOCKER api_harbor_digest_missing" + exit 1 + fi + if ! printf '%s\n' "${WEB_IMAGE_DIGEST:-}" | \ + grep -Eq '^sha256:[0-9a-f]{64}$'; then + echo "BLOCKER web_harbor_digest_missing" + exit 1 + fi # ─── Step 1: Apply NetworkPolicy + ConfigMap + ServiceRegistry ─── # Preserve only live Deployment specs and Secret object existence. @@ -2789,7 +2837,7 @@ jobs: "KUBECTL='sudo kubectl --kubeconfig=/etc/rancher/k3s/k3s.yaml --server=${{ env.K8S_API_SERVER }}'; \$KUBECTL apply -f -" echo "✅ Service Registry ConfigMap 已更新" - # ─── Step 2: 更新 kustomization.yaml image tag ─── + # ─── Step 2: 更新 kustomization.yaml immutable image digest ─── # kustomize is an immutable dependency of the internal runner image. # GitHub is frozen, so CD must never download tools from public GitHub. export PATH="${HOME}/.local/bin:${PATH}" @@ -2799,11 +2847,12 @@ jobs: } cd k8s/awoooi-prod - # kustomize edit set image 更新 tag + # Source SHA remains in AWOOOI_BUILD_COMMIT_SHA; workload identity is + # pinned to the independently read-back Harbor manifest digest. kustomize edit set image \ - 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER=${HARBOR}/awoooi/api:${IMAGE_TAG} + 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER=${HARBOR}/awoooi/api@${API_IMAGE_DIGEST} kustomize edit set image \ - 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER=${HARBOR}/awoooi/web:${IMAGE_TAG} + 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER=${HARBOR}/awoooi/web@${WEB_IMAGE_DIGEST} cd ../.. # 2026-06-29 Codex: API deploy readback uses AWOOOI_BUILD_COMMIT_SHA diff --git a/ops/runner/test_cd_controlled_runtime_profile.py b/ops/runner/test_cd_controlled_runtime_profile.py index 54ff89fa0..321cd1dad 100644 --- a/ops/runner/test_cd_controlled_runtime_profile.py +++ b/ops/runner/test_cd_controlled_runtime_profile.py @@ -469,6 +469,27 @@ def test_cd_docker_build_and_push_steps_are_bounded_and_classified() -> None: assert marker in block +def test_cd_deploys_api_and_web_by_verified_harbor_digest() -> None: + text = _workflow_text() + expected = [ + '"${{ env.HARBOR }}/awoooi/api:${{ github.sha }}"', + '"${{ env.HARBOR }}/awoooi/web:${{ github.sha }}"', + 'echo "API_IMAGE_DIGEST=${API_IMAGE_DIGEST}" >> "${GITHUB_ENV}"', + 'echo "WEB_IMAGE_DIGEST=${WEB_IMAGE_DIGEST}" >> "${GITHUB_ENV}"', + "api_harbor_digest_missing", + "web_harbor_digest_missing", + "${HARBOR}/awoooi/api@${API_IMAGE_DIGEST}", + "${HARBOR}/awoooi/web@${WEB_IMAGE_DIGEST}", + ] + for marker in expected: + assert marker in text + + deploy_block = text.split("- name: Deploy to K8s (ArgoCD GitOps)", 1)[1] + deploy_block = deploy_block.split("- name: Apply Runtime Image Mirror Policy", 1)[0] + assert "${HARBOR}/awoooi/api:${IMAGE_TAG}" not in deploy_block + assert "${HARBOR}/awoooi/web:${IMAGE_TAG}" not in deploy_block + + def test_host_pressure_gate_ignores_stale_docker_stats_for_cpu_attribution() -> None: text = WAIT_HOST_PRESSURE.read_text(encoding="utf-8")