fix(chain-live): correct CRV4 call shape; fail closed without tlock #78
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: images | |
| # Build service images, push by commit SHA + branch tag, record digests as an artifact. | |
| # Digests-only pins are consumed by deploy/scripts/promote.sh. | |
| # After merge: promote pin services into deploy/pins/staging.json and commit so | |
| # deploy-prod preflight can match staging.json.commit_sha to the image SHA. | |
| # Registry: GHCR only (never AWS ECR). | |
| on: | |
| push: | |
| branches: [dev] | |
| paths: | |
| - "crates/**" | |
| - "bins/**" | |
| - "deploy/Dockerfile" | |
| - "deploy/attest-helper/**" | |
| - "Cargo.toml" | |
| - "Cargo.lock" | |
| - ".github/workflows/images.yml" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| REGISTRY: ghcr.io | |
| concurrency: | |
| group: images-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| build-push-digests: | |
| name: build · push · record digests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 90 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: validator | |
| image_suffix: validator | |
| dockerfile: deploy/Dockerfile | |
| - target: gateway | |
| image_suffix: gateway | |
| dockerfile: deploy/Dockerfile | |
| - target: updater | |
| image_suffix: updater | |
| dockerfile: deploy/Dockerfile | |
| - target: prism-challenge | |
| image_suffix: prism-challenge | |
| dockerfile: deploy/Dockerfile | |
| - target: design-challenge | |
| image_suffix: design-challenge | |
| dockerfile: deploy/Dockerfile | |
| - target: design-egress-proxy | |
| image_suffix: design-egress-proxy | |
| dockerfile: deploy/Dockerfile | |
| - target: design-runtime | |
| image_suffix: design-runtime | |
| dockerfile: deploy/Dockerfile | |
| - target: "" | |
| image_suffix: base-attest-helper | |
| dockerfile: deploy/attest-helper/Dockerfile | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Lowercase GHCR image prefix | |
| run: | | |
| set -euo pipefail | |
| # GHCR rejects mixed-case repository paths (e.g. BaseIntelligence). | |
| owner_repo="${GITHUB_REPOSITORY,,}" | |
| echo "IMAGE_PREFIX=ghcr.io/${owner_repo}" >> "$GITHUB_ENV" | |
| echo "IMAGE_PREFIX=ghcr.io/${owner_repo}" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push | |
| id: build | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: ${{ matrix.image_suffix == 'base-attest-helper' && 'deploy/attest-helper' || '.' }} | |
| file: ${{ matrix.dockerfile }} | |
| target: ${{ matrix.target }} | |
| push: ${{ github.event_name != 'pull_request' }} | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/${{ matrix.image_suffix }}:${{ github.sha }} | |
| ${{ env.IMAGE_PREFIX }}/${{ matrix.image_suffix }}:${{ github.ref_name }} | |
| build-args: | | |
| BUILD_FROM=source | |
| provenance: false | |
| - name: Write digest fragment | |
| run: | | |
| set -euo pipefail | |
| mkdir -p digests-out | |
| DIGEST="${{ steps.build.outputs.digest }}" | |
| test -n "$DIGEST" | |
| test "${DIGEST#sha256:}" != "$DIGEST" | |
| REPO="${{ env.IMAGE_PREFIX }}/${{ matrix.image_suffix }}" | |
| SVC="${{ matrix.image_suffix }}" | |
| cat > "digests-out/${SVC}.json" <<EOF | |
| { | |
| "service": "${SVC}", | |
| "repository": "${REPO}", | |
| "tag_sha": "${{ github.sha }}", | |
| "digest": "${DIGEST}", | |
| "image": "${REPO}@${DIGEST}", | |
| "commit_sha": "${{ github.sha }}" | |
| } | |
| EOF | |
| cat "digests-out/${SVC}.json" | |
| - name: Upload digest fragment | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: digest-${{ matrix.image_suffix }} | |
| path: digests-out/${{ matrix.image_suffix }}.json | |
| if-no-files-found: error | |
| merge-digests: | |
| name: merge digest manifest | |
| needs: build-push-digests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Download fragments | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: digest-* | |
| path: digests-in | |
| merge-multiple: true | |
| - name: Merge + validate | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' | |
| import json, pathlib, os | |
| from datetime import datetime, timezone | |
| root = pathlib.Path("digests-in") | |
| images = {} | |
| for p in sorted(root.glob("*.json")): | |
| d = json.loads(p.read_text()) | |
| assert d["digest"].startswith("sha256:") and len(d["digest"]) == 71 | |
| assert "@sha256:" in d["image"] | |
| images[d["service"]] = d | |
| expected = { | |
| "validator", | |
| "gateway", | |
| "updater", | |
| "prism-challenge", | |
| "design-challenge", | |
| "design-egress-proxy", | |
| "design-runtime", | |
| "base-attest-helper", | |
| } | |
| assert set(images) == expected, (set(images), expected) | |
| out = { | |
| "commit_sha": os.environ.get("GITHUB_SHA", ""), | |
| "created_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), | |
| "registry": "ghcr.io", | |
| "images": images, | |
| } | |
| pathlib.Path("deploy/digests").mkdir(parents=True, exist_ok=True) | |
| path = pathlib.Path("deploy/digests") / f"{out['commit_sha']}.json" | |
| path.write_text(json.dumps(out, indent=2, sort_keys=True) + "\n") | |
| print(path.read_text()) | |
| PY | |
| - name: Upload merged digests | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: base-image-digests-${{ github.sha }} | |
| path: deploy/digests/${{ github.sha }}.json | |
| if-no-files-found: error | |
| # --------------------------------------------------------------------------- | |
| # Close the staging → prod ladder: write deploy/digests/<sha>.json and promote | |
| # pin services into deploy/pins/staging.json, then commit/push to origin/dev. | |
| # Staging droplet deploy remains --build-from source (deploy-staging / ci.yml). | |
| # --------------------------------------------------------------------------- | |
| update-staging-pins: | |
| name: update · commit staging pins | |
| needs: merge-digests | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/dev' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: dev | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download merged digests | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: base-image-digests-${{ github.sha }} | |
| path: /tmp/digests-in | |
| - name: Record digests + promote staging pins | |
| run: | | |
| set -euo pipefail | |
| SHA="${{ github.sha }}" | |
| mkdir -p deploy/digests | |
| # Artifact may be the file itself or nested under deploy/digests/ | |
| SRC="" | |
| if [[ -f "/tmp/digests-in/${SHA}.json" ]]; then | |
| SRC="/tmp/digests-in/${SHA}.json" | |
| elif [[ -f "/tmp/digests-in/deploy/digests/${SHA}.json" ]]; then | |
| SRC="/tmp/digests-in/deploy/digests/${SHA}.json" | |
| else | |
| SRC=$(find /tmp/digests-in -name "${SHA}.json" -type f | head -1) | |
| fi | |
| test -n "$SRC" && test -f "$SRC" | |
| cp "$SRC" "deploy/digests/${SHA}.json" | |
| echo "Recorded digest manifest: deploy/digests/${SHA}.json" | |
| chmod +x deploy/scripts/promote.sh | |
| # Pin services only (validator/gateway/updater/prism-challenge/design-challenge). | |
| # --skip-backup: this job records CI digests; Spaces/PG backup runs at | |
| # prod promote (deploy-prod.yml, fail-closed). | |
| for svc in validator gateway updater prism-challenge design-challenge; do | |
| IMAGE=$(python3 -c ' | |
| import json, sys | |
| d = json.load(open(sys.argv[1])) | |
| meta = d["images"][sys.argv[2]] | |
| print(meta["image"]) | |
| ' "deploy/digests/${SHA}.json" "$svc") | |
| echo "staging promote $svc → $IMAGE" | |
| ./deploy/scripts/promote.sh \ | |
| --env staging \ | |
| --service "$svc" \ | |
| --image "$IMAGE" \ | |
| --commit "$SHA" \ | |
| --skip-backup | |
| done | |
| python3 -c ' | |
| import json, sys | |
| p = json.load(open("deploy/pins/staging.json")) | |
| assert p["commit_sha"] == sys.argv[1], (p["commit_sha"], sys.argv[1]) | |
| for s in ("validator", "gateway", "updater", "prism-challenge", "design-challenge"): | |
| assert s in p["services"], s | |
| assert p["services"][s]["digest"].startswith("sha256:") | |
| print("staging pins ok", p["commit_sha"]) | |
| ' "$SHA" | |
| - name: Commit and push staging pins | |
| run: | | |
| set -euo pipefail | |
| SHA="${{ github.sha }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add "deploy/digests/${SHA}.json" deploy/pins/staging.json | |
| if git diff --cached --quiet; then | |
| echo "No pin/digest changes to commit" | |
| exit 0 | |
| fi | |
| git commit -m "deploy: staging pins for ${SHA}" | |
| # Rebase onto latest dev in case another pin commit landed. | |
| git pull --rebase origin dev | |
| git push origin HEAD:dev | |
| # --------------------------------------------------------------------------- | |
| # Make GHCR packages public so unauthenticated / Phala pulls work. Lives here | |
| # rather than in ghcr-public.yml because a `workflow_run` trigger is only read | |
| # from the default branch (main) and these workflows live on dev. | |
| # --------------------------------------------------------------------------- | |
| ghcr-public: | |
| needs: merge-digests | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/dev' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Set package visibility public | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| OWNER: ${{ github.repository_owner }} | |
| run: | | |
| set -euo pipefail | |
| # Nested image names: ghcr.io/<owner>/base/<suffix> → package name "base/<suffix>" | |
| pkgs=( | |
| "base/base-attest-helper" | |
| "base/gateway" | |
| "base/validator" | |
| "base/updater" | |
| "base/prism-challenge" | |
| "base/design-challenge" | |
| "base/design-egress-proxy" | |
| "base/design-runtime" | |
| ) | |
| ok=0 | |
| # First list packages to Learn exact names (debug) | |
| curl -sS -H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json" \ | |
| "https://api.github.com/orgs/${OWNER}/packages?package_type=container&per_page=100" \ | |
| | tee /tmp/pkg-list.json | head -c 2000 || true | |
| echo | |
| for pkg in "${pkgs[@]}"; do | |
| enc=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$pkg") | |
| code=$(curl -sS -o "/tmp/vis-${enc}.json" -w "%{http_code}" \ | |
| -X PUT \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/orgs/${OWNER}/packages/container/${enc}/visibility" \ | |
| -d '{"visibility":"public"}' || true) | |
| echo "pkg=${pkg} http=${code} body=$(head -c 200 /tmp/vis-${enc}.json || true)" | |
| if [ "$code" = "204" ] || [ "$code" = "200" ]; then | |
| ok=$((ok + 1)) | |
| fi | |
| done | |
| echo "publicized_ok=${ok}/${#pkgs[@]}" | |
| # Soft fail while packages may not exist yet on first base build | |
| if [ "$ok" -lt 1 ]; then | |
| echo "WARNING: no packages publicized yet (may be missing until images run)" | |
| exit 0 | |
| fi |