chore: enforce ghcr.io/zooai registry + add service-released auto-bump (#1)

Cleans up registry drift across the live cluster:
- base/operator.yaml: 0.1.0 (non-existent in GHCR) -> :main + TODO semver
- base/bridge.yaml:   0.1.0 (non-existent in GHCR) -> :main + TODO semver
- base/explorer.yaml: blockscout :latest -> :7.0.2.commit.900ef697 (release SHA)
- zoo-system/patches.yaml: remove redundant operator image override

Adds .github/workflows/service-released.yml for auto-bump on
repository_dispatch from zoo service repos. Validates payload pins
semver (rejects :main/:latest) — forcing function for service CIs to
ship semver tags before auto-bump can land.

LLM.md: registry policy, image pin status table, auto-bump flow,
mpc-node orphan documentation, live cluster drift snapshot.
This commit is contained in:
Hanzo Dev
2026-04-30 11:56:18 -07:00
committed by GitHub
parent 2148af97b3
commit 3e51152561
6 changed files with 246 additions and 18 deletions
+144
View File
@@ -0,0 +1,144 @@
# Auto-PR on Zoo service release.
#
# When a Zoo service repo (zooai/operator, zooai/bridge, zooai/ingress,
# etc.) tags + pushes a new version, it dispatches here. This workflow:
# 1. Bumps the image tag in k8s/base/<service>.yaml
# 2. Opens a PR: auto-bump/<service>-<version>
# 3. Enables auto-merge (squash) — merges on green CI
#
# Companion to .github/workflows/update-image.yml which handles the older
# `image-published` dispatch shape (kept for repos that haven't migrated).
# New service repos should dispatch a per-service event type listed below.
name: Service Released
on:
repository_dispatch:
types:
- operator
- bridge
- ingress
- mpc
- ngo
- docs
- zips
# Explorer is upstream Blockscout — Zoo does not ship its own image.
# Listed here as a no-op acknowledgement so a stray dispatch doesn't
# 404 the sender; the validate step rejects the bump explicitly.
- explorer
permissions:
contents: write
pull-requests: write
jobs:
auto-bump:
name: "Bump ${{ github.event.action }} → ${{ github.event.client_payload.version }}"
runs-on: zoo-build-amd64
steps:
- name: Validate payload
run: |
set -euo pipefail
SERVICE="${{ github.event.action }}"
VERSION="${{ github.event.client_payload.version }}"
IMAGE="${{ github.event.client_payload.image }}"
SHA="${{ github.event.client_payload.sha }}"
if [ -z "$VERSION" ] || [ -z "$IMAGE" ]; then
echo "::error::Missing required payload fields: service=${SERVICE} version=${VERSION} image=${IMAGE}"
exit 1
fi
# Explorer is upstream Blockscout — refuse to auto-bump from
# a zooai/explorer dispatch, since k8s/base/explorer.yaml pins
# blockscout/blockscout:<sha> upstream.
if [ "$SERVICE" = "explorer" ]; then
echo "::notice::explorer pins blockscout/blockscout upstream — ignoring dispatch"
exit 0
fi
# Pinned tag must be semver (immutable). :latest and bare branch
# tags (:main/:test/:dev) are banned by policy. Today most Zoo
# service CIs only publish :main; this guard is the forcing
# function to ship semver before auto-bump can land.
IMAGE_TAG="${IMAGE##*:}"
if ! echo "$IMAGE_TAG" | grep -qE '^v?[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9.-]+)?$'; then
echo "::error::Non-semver tag '${IMAGE_TAG}' rejected — manifests must pin to vX.Y.Z"
exit 1
fi
echo "SERVICE=${SERVICE}" >> "$GITHUB_ENV"
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
echo "SHA=${SHA}" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.UNIVERSE_PAT || secrets.GITHUB_TOKEN }}
fetch-depth: 1
- name: Bump base manifest
run: |
set -euo pipefail
FILE="k8s/base/${SERVICE}.yaml"
if [ ! -f "$FILE" ]; then
echo "::error::No base manifest at ${FILE}"
exit 1
fi
# Match the existing image line for ghcr.io/zooai/<service>:<anything>
# and rewrite the tag. We don't accept arbitrary IMAGE values from
# the payload — the service name is the source of truth.
OLD="ghcr.io/zooai/${SERVICE}:[A-Za-z0-9._-]\\+"
NEW="ghcr.io/zooai/${SERVICE}:${VERSION}"
if ! grep -qE "ghcr\\.io/zooai/${SERVICE}:" "$FILE"; then
echo "::error::No ghcr.io/zooai/${SERVICE}: line in ${FILE}"
exit 1
fi
sed -i.bak "s|ghcr.io/zooai/${SERVICE}:[A-Za-z0-9._-]\\+|${NEW}|g" "$FILE"
rm -f "${FILE}.bak"
echo "Updated ${FILE}: ${SERVICE} → ${VERSION}"
git diff --stat -- "$FILE"
- name: Open PR
env:
GH_TOKEN: ${{ secrets.UNIVERSE_PAT || secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
BRANCH="auto-bump/${SERVICE}-${VERSION}"
git config user.name "zoo-bot[bot]"
git config user.email "[email protected]"
git checkout -b "$BRANCH"
git add -A
if git diff --cached --quiet; then
echo "::notice::No changes — manifest already at ${VERSION}"
exit 0
fi
git commit -m "chore(deps): bump ${SERVICE} to ${VERSION}"
git push -f origin "$BRANCH"
PR_URL=$(gh pr create \
--title "chore(deps): bump ${SERVICE} to ${VERSION}" \
--body "Auto-bump from zooai/${SERVICE} release.
| field | value |
|---------|-------|
| service | \`${SERVICE}\` |
| version | \`${VERSION}\` |
| image | \`${IMAGE}\` |
| sha | \`${SHA}\` |
" \
--label "auto-bump" \
--base main 2>&1 | tail -1)
echo "PR_URL=${PR_URL}" >> "$GITHUB_ENV"
# Auto-merge (squash) on green CI
gh pr merge "$PR_URL" --squash --auto || true
+87 -2
View File
@@ -129,12 +129,97 @@ Located in `zoo.fund/contracts/`:
- **ZooGovernor.sol** - OpenZeppelin Governor
- **TimelockController.sol** - 2-day governance timelock
## K8s Manifests (`k8s/`)
Kustomize-based: one `base/` + per-env overlays under `zoo-{system,devnet,testnet,mainnet}/`.
### Registry Policy
All Zoo-built workloads MUST pull from `ghcr.io/zooai/*`. No exceptions:
- `registry.digitalocean.com/hanzo/*` — banned. Old DOCR images live in
the cluster as drift; re-applying base evicts them.
- `us-docker.pkg.dev/*` — banned for Zoo workloads.
- `:latest` — banned. Pin a real tag (semver if available, immutable
branch tag like `:main` only as fallback while CI ships semver).
Upstream OSS images are fine where Zoo doesn't fork the binary:
- `blockscout/blockscout:<sha>` for explorer
- `traefik:v3.3` for ingress
- `ghcr.io/hanzoai/sql:18`, `ghcr.io/hanzoai/kv:9` — Zoo consumes the
Hanzo OSS infra images unmodified
- `hashicorp/consul:1.20` for service discovery
### Image Pin Status
| Manifest | Image | Pin | Notes |
|----------|-------|-----|-------|
| `base/operator.yaml` | `ghcr.io/zooai/operator:main` | branch tag | TODO: pin semver |
| `base/bridge.yaml` | `ghcr.io/zooai/bridge:main` | branch tag | TODO: pin semver |
| `base/ingress.yaml` | `traefik:v3.3` | semver | upstream |
| `base/explorer.yaml` | `blockscout/blockscout:7.0.2.commit.900ef697` | release SHA | upstream Blockscout |
| `base/sql.yaml` | `ghcr.io/hanzoai/sql:18` | semver | OSS infra |
| `base/kv.yaml` | `ghcr.io/hanzoai/kv:9` | semver | OSS infra |
| `base/ngo.yaml` | `ghcr.io/zooai/ngo:latest` | latest | image not built; manifest scaffolding |
| `base/docs.yaml` | `ghcr.io/zooai/docs:latest` | latest | image not built |
| `base/zips.yaml` | `ghcr.io/zooai/zips:latest` | latest | image not built |
### Auto-bump (`service-released.yml`)
Zoo service repos (`zooai/operator`, `zooai/bridge`, `zooai/ingress`,
`zooai/mpc`, `zooai/ngo`, `zooai/docs`, `zooai/zips`) dispatch a
`repository_dispatch` event with type=`<service>` to this repo on
release. The workflow:
1. Validates the payload pins a semver tag (rejects `:main`/`:latest`).
2. Edits the matching `k8s/base/<service>.yaml` image line.
3. Opens an `auto-bump/<service>-<version>` PR + enables auto-merge.
Until service CIs publish semver tags the dispatch is rejected — the
guard is the forcing function. `update-image.yml` (older
`image-published` shape) is kept for repos that still emit that.
### Orphan Workloads
`mpc-node` runs as a StatefulSet in `zoo-{system,devnet,testnet,mainnet}`
pulling `registry.digitalocean.com/hanzo/zoo-mpc:latest`. There is NO
universe manifest for it — the `# mpc.yaml removed — threshold signing
is chain-native via T-Chain` note in `base/kustomization.yaml` makes the
intent explicit: **threshold signing is chain-native (T-Chain VM), not a
sidecar**. The `mpc-node` StatefulSets are leftover from the pre-T-Chain
architecture and should be deleted from the cluster. Do NOT re-add an
mpc.yaml manifest.
```sh
# Cleanup (run once it's confirmed nothing else references mpc-node):
for ns in zoo-system zoo-devnet zoo-testnet zoo-mainnet; do
kubectl --context do-sfo3-zoo-k8s -n "$ns" delete statefulset mpc-node || true
done
```
### Live Cluster Drift (snapshot at PR time)
Deployments still on banned registries:
- `zoo-{system,devnet,testnet,mainnet}/zoo-operator` → `registry.digitalocean.com/hanzo/zoo-operator`
- `zoo-{system,devnet,testnet,mainnet}/zoo-bridge` → `registry.digitalocean.com/hanzo/zoo-bridge`
- `zoo-{devnet,testnet,mainnet}/zoo-explorer` → `us-docker.pkg.dev/liquidity-devnet/backend/zoo-explorer`
- `zoo-system/zoo-ingress` → `ghcr.io/zooai/ingress:latest` (latest pin only — base says `traefik:v3.3`)
Re-applying the kustomize base after this PR merges fixes all four:
```sh
kubectl --context do-sfo3-zoo-k8s apply -k k8s/zoo-system/
kubectl --context do-sfo3-zoo-k8s apply -k k8s/zoo-devnet/
kubectl --context do-sfo3-zoo-k8s apply -k k8s/zoo-testnet/
kubectl --context do-sfo3-zoo-k8s apply -k k8s/zoo-mainnet/
```
## Rules for AI Assistants
1. **ALWAYS** update LLM.md with significant discoveries
2. **NEVER** commit symlinked files (.AGENTS.md, CLAUDE.md, etc.)
3. **NEVER** create random summary files - update THIS file
4. See ECOSYSTEM_SUMMARY.md for detailed architecture
4. **NEVER** push Zoo workloads to any registry other than `ghcr.io/zooai/*`
5. See ECOSYSTEM_SUMMARY.md for detailed architecture
---
*Last Updated: 2026-03-24*
*Last Updated: 2026-04-30*
+4 -1
View File
@@ -23,7 +23,10 @@ spec:
- name: hanzo
containers:
- name: bridge
image: ghcr.io/zooai/bridge:0.1.0
# TODO: pin semver once zooai/bridge CI publishes vX.Y.Z tags.
# Today GHCR only has :main + :latest (the v1.0.6 git tag never
# produced an image tag — CI only writes :main). Pinning :main.
image: ghcr.io/zooai/bridge:main
imagePullPolicy: Always
ports:
- containerPort: 3000
+5 -1
View File
@@ -22,7 +22,11 @@ spec:
- name: hanzo
containers:
- name: explorer
image: blockscout/blockscout:latest
# Blockscout upstream image. zooai/explorer is the source repo
# (Blockscout fork) and does not publish to ghcr.io/zooai/explorer
# — Zoo runs the upstream binary unmodified. Pinned to a real
# release SHA tag instead of :latest.
image: blockscout/blockscout:7.0.2.commit.900ef697
ports:
- containerPort: 4000
name: http
+5 -1
View File
@@ -23,7 +23,11 @@ spec:
- name: hanzo
containers:
- name: operator
image: ghcr.io/zooai/operator:0.1.0
# TODO: pin semver once zooai/operator CI publishes vX.Y.Z tags.
# Today GHCR only has :main + :latest. :main is the immutable
# branch tag CI pushes per build (per-arch manifests under main-amd64/main-arm64),
# which is the closest thing to a semver pin we have.
image: ghcr.io/zooai/operator:main
imagePullPolicy: Always
args: ["--namespace", ""]
ports:
+1 -13
View File
@@ -1,17 +1,5 @@
# zoo-system: operator only, scale everything else to 0
apiVersion: apps/v1
kind: Deployment
metadata:
name: zoo-operator
spec:
template:
spec:
containers:
- name: operator
image: ghcr.io/zooai/operator:main
args: ["--namespace", ""]
---
# Scale down services that don't belong in zoo-system
# Operator image is pinned in base/operator.yaml — no override needed.
apiVersion: zoo.network/v1alpha1
kind: ZooNetwork
metadata: