mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-09-30 20:46:32 +00:00
Merge pull request #12 from bootnode/chore/hanzo-cicd
ci: hanzo.yml + one arc/KMS workflow, kill .platform.yml
This commit is contained in:
@@ -1,146 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test-api:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
POSTGRES_USER: bootnode
|
||||
POSTGRES_PASSWORD: bootnode
|
||||
POSTGRES_DB: bootnode_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.12
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./api
|
||||
run: |
|
||||
uv venv
|
||||
uv pip install -e ".[dev]"
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./api
|
||||
env:
|
||||
DATABASE_URL: postgresql://bootnode:bootnode@localhost:5432/bootnode_test
|
||||
REDIS_URL: redis://localhost:6379
|
||||
JWT_SECRET: test-secret-key-for-ci
|
||||
API_KEY_SALT: test-salt-for-ci
|
||||
run: |
|
||||
.venv/bin/pytest -v --tb=short || echo "No tests yet"
|
||||
|
||||
test-web:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./web
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check & Build
|
||||
working-directory: ./web
|
||||
run: pnpm run build
|
||||
|
||||
e2e-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-api, test-web]
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
POSTGRES_USER: bootnode
|
||||
POSTGRES_PASSWORD: bootnode
|
||||
POSTGRES_DB: bootnode
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.12
|
||||
|
||||
- name: Install API dependencies
|
||||
working-directory: ./api
|
||||
run: |
|
||||
uv venv
|
||||
uv pip install -e ".[dev]"
|
||||
|
||||
- name: Start API server
|
||||
working-directory: ./api
|
||||
env:
|
||||
DATABASE_URL: postgresql+asyncpg://bootnode:bootnode@localhost:5432/bootnode
|
||||
REDIS_URL: redis://localhost:6379
|
||||
JWT_SECRET: test-secret-key-for-ci
|
||||
API_KEY_SALT: test-salt-for-ci
|
||||
run: |
|
||||
.venv/bin/python -c "from bootnode.db import init_db; import asyncio; asyncio.run(init_db())"
|
||||
.venv/bin/uvicorn bootnode.main:app --host 0.0.0.0 --port 8100 &
|
||||
sleep 10
|
||||
|
||||
- name: Run E2E tests
|
||||
run: bash test-e2e.sh
|
||||
|
||||
# Build, push, and deploy are handled by docker-release.yml
|
||||
# which triggers on CI completion via workflow_run
|
||||
@@ -0,0 +1,104 @@
|
||||
# Hanzo CI/CD — build + test + deploy, driven by the repo's root hanzo.yml.
|
||||
#
|
||||
# Runs on our self-hosted arc runners ([self-hosted, linux, amd64]) — offloaded
|
||||
# from GitHub-hosted compute. Credentials (GHCR push token, cluster kubeconfig)
|
||||
# are pulled from KMS at run time via Universal Auth; the only GitHub secrets are
|
||||
# KMS_CLIENT_ID / KMS_CLIENT_SECRET, and the repo var KMS_WORKSPACE. Self-
|
||||
# contained (no cross-org reusable workflow) so it works from any org.
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: cicd-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
cicd:
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch deploy credentials from KMS
|
||||
id: kms
|
||||
env:
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_WORKSPACE: ${{ vars.KMS_WORKSPACE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TOKEN=$(curl -sf "$KMS_ENDPOINT/api/v1/auth/universal-auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"clientId\":\"$KMS_CLIENT_ID\",\"clientSecret\":\"$KMS_CLIENT_SECRET\"}" | jq -r '.accessToken')
|
||||
ENV="$(python3 -c "import yaml;print((yaml.safe_load(open('hanzo.yml')).get('kms') or {}).get('environment','prod'))")"
|
||||
PATHQ="$(python3 -c "import yaml;print((yaml.safe_load(open('hanzo.yml')).get('kms') or {}).get('path','/deploy'))")"
|
||||
SECRETS=$(curl -sf "$KMS_ENDPOINT/api/v3/secrets/raw?workspaceId=$KMS_WORKSPACE&secretPath=$PATHQ&environment=$ENV" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
get() { echo "$SECRETS" | jq -r ".secrets[]|select(.secretKey==\"$1\")|.secretValue // empty"; }
|
||||
GHCR_USER=$(get GHCR_USER); GHCR_TOKEN=$(get GHCR_TOKEN); KUBECONFIG_B64=$(get KUBECONFIG)
|
||||
: "${GHCR_USER:=hanzoai}"
|
||||
if [ -z "$GHCR_TOKEN" ]; then echo "::error::GHCR_TOKEN not found in KMS"; exit 1; fi
|
||||
echo "::add-mask::$GHCR_TOKEN"
|
||||
echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USER" --password-stdin
|
||||
if [ -n "$KUBECONFIG_B64" ]; then echo "$KUBECONFIG_B64" | base64 -d > "$RUNNER_TEMP/kubeconfig"; echo "kubeconfig=$RUNNER_TEMP/kubeconfig" >> "$GITHUB_OUTPUT"; fi
|
||||
|
||||
- name: Build & push images (per hanzo.yml)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
IS_TAG=$([ "${{ github.ref_type }}" = "tag" ] && echo 1 || echo 0)
|
||||
VER="${{ github.ref_name }}"; VER="${VER#v}"
|
||||
python3 -c "import yaml,json;print(json.dumps(yaml.safe_load(open('hanzo.yml'))['images']))" | jq -c '.[]' | while read -r img; do
|
||||
name=$(echo "$img"|jq -r .name); ctx=$(echo "$img"|jq -r .context)
|
||||
df=$(echo "$img"|jq -r '.dockerfile // (.context+"/Dockerfile")'); repo=$(echo "$img"|jq -r .repo)
|
||||
sfx=$(echo "$img"|jq -r '."tag-suffix"')
|
||||
TAGS="-t $repo:sha-${SHORT}-amd64-${sfx} -t $repo:${sfx}-latest"
|
||||
[ "$IS_TAG" = 1 ] && TAGS="$TAGS -t $repo:${VER}-amd64-${sfx}"
|
||||
echo "::group::build $name → $repo (${sfx})"
|
||||
docker buildx build --platform linux/amd64 --push $TAGS -f "$df" "$ctx"
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
- name: Test (per hanzo.yml)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 -c "import yaml,json;print(json.dumps(yaml.safe_load(open('hanzo.yml')).get('test') or []))" | jq -c '.[]' | while read -r t; do
|
||||
name=$(echo "$t"|jq -r .name); cmd=$(echo "$t"|jq -r .run)
|
||||
echo "::group::test $name"; bash -c "$cmd"; echo "::endgroup::"
|
||||
done
|
||||
|
||||
- name: Deploy (per hanzo.yml)
|
||||
if: github.event_name != 'pull_request' && steps.kms.outputs.kubeconfig != ''
|
||||
env:
|
||||
KUBECONFIG: ${{ steps.kms.outputs.kubeconfig }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REF="${{ github.ref_name }}"
|
||||
ON=$(python3 -c "import yaml,json;print(json.dumps((yaml.safe_load(open('hanzo.yml')).get('deploy') or {}).get('on') or []))")
|
||||
IS_TAG=$([ "${{ github.ref_type }}" = "tag" ] && echo 1 || echo 0)
|
||||
if [ "$IS_TAG" != 1 ] && ! echo "$ON" | jq -e --arg b "$REF" 'index($b)' >/dev/null; then
|
||||
echo "branch $REF not in deploy.on — skipping deploy"; exit 0
|
||||
fi
|
||||
SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
NS=$(python3 -c "import yaml;print(yaml.safe_load(open('hanzo.yml'))['deploy']['namespace'])")
|
||||
# map image name → repo + suffix
|
||||
python3 -c "import yaml,json;print(json.dumps(yaml.safe_load(open('hanzo.yml'))['images']))" > /tmp/imgs.json
|
||||
python3 -c "import yaml,json;print(json.dumps(yaml.safe_load(open('hanzo.yml'))['deploy']['services']))" | jq -c '.[]' | while read -r s; do
|
||||
svc=$(echo "$s"|jq -r .name); imgname=$(echo "$s"|jq -r .image)
|
||||
repo=$(jq -r --arg n "$imgname" '.[]|select(.name==$n)|.repo' /tmp/imgs.json)
|
||||
sfx=$(jq -r --arg n "$imgname" '.[]|select(.name==$n)|."tag-suffix"' /tmp/imgs.json)
|
||||
ref="$repo:sha-${SHORT}-amd64-${sfx}"
|
||||
echo "rolling $svc → $ref"
|
||||
kubectl -n "$NS" set image "deployment/$svc" "*=$ref"
|
||||
kubectl -n "$NS" rollout status "deployment/$svc" --timeout=180s
|
||||
done
|
||||
@@ -1,67 +0,0 @@
|
||||
# Self-contained image build on the self-hosted arcd pools.
|
||||
#
|
||||
# Previously this delegated to hanzoai/.github's reusable docker-build.yml, but
|
||||
# bootnode is in a separate GitHub org and that workflow's access is org-scoped,
|
||||
# so every run failed "workflow was not found" (broken since ~2026-05-19). The
|
||||
# build steps are inlined here — same self-hosted runners, no cross-org
|
||||
# reusable dependency. (platform.hanzo.ai via .platform.yml is the canonical
|
||||
# path; this is the retained GHA fallback. No GitHub-hosted runners.)
|
||||
name: Docker Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: docker-release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { app: api, context: ./api }
|
||||
- { app: web, context: ./web }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Compute tags
|
||||
id: tags
|
||||
run: |
|
||||
BASE=ghcr.io/hanzoai/bootnode
|
||||
SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
# *-latest is what hanzo/universe pins; sha tag is for traceability.
|
||||
TAGS="$BASE:${{ matrix.app }}-latest,$BASE:sha-${SHORT}-amd64-${{ matrix.app }}"
|
||||
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||
V="${{ github.ref_name }}"; V="${V#v}"
|
||||
TAGS="$TAGS,$BASE:${V}-amd64-${{ matrix.app }}"
|
||||
fi
|
||||
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push ${{ matrix.app }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.context }}/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
platforms: linux/amd64
|
||||
provenance: false
|
||||
@@ -1,94 +0,0 @@
|
||||
name: Platform Build (arcd executor)
|
||||
|
||||
# Dispatched by platform.hanzo.ai (NOT by GitHub on push). Platform receives
|
||||
# the repo webhook, schedules the build, and triggers this workflow via
|
||||
# workflow_dispatch with the inputs below — pinning the arcd runner pool per
|
||||
# build target. On completion this workflow calls back into platform, which
|
||||
# records the outcome and (on success) rolls the image out via the operator.
|
||||
#
|
||||
# This is the per-repo half of the platform-native CI/CD escape from GHA as a
|
||||
# single point of failure. The build muscle still runs on self-hosted arcd.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build-job-id:
|
||||
description: Platform buildJob id (echoed back on completion)
|
||||
required: true
|
||||
target:
|
||||
description: "Build target, e.g. linux/amd64"
|
||||
required: true
|
||||
runner-pool:
|
||||
description: "arcd pool label, e.g. hanzoai-linux-amd64"
|
||||
required: true
|
||||
image:
|
||||
description: "Fully-resolved image ref, e.g. ghcr.io/hanzoai/zip:<sha>"
|
||||
required: true
|
||||
dockerfile:
|
||||
description: Path to the Dockerfile
|
||||
required: true
|
||||
default: ./Dockerfile
|
||||
context:
|
||||
description: Build context
|
||||
required: true
|
||||
default: .
|
||||
push:
|
||||
description: Whether to push the built image
|
||||
required: true
|
||||
default: "true"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ inputs.runner-pool }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
if: ${{ inputs.push == 'true' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ inputs.context }}
|
||||
file: ${{ inputs.dockerfile }}
|
||||
push: ${{ inputs.push }}
|
||||
tags: ${{ inputs.image }}
|
||||
provenance: false
|
||||
|
||||
- name: Report success to platform
|
||||
if: success()
|
||||
env:
|
||||
TOKEN: ${{ secrets.PLATFORM_BUILD_CALLBACK_TOKEN }}
|
||||
run: |
|
||||
curl -fsS -X POST "https://platform.hanzo.ai/v1/build-callback" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -nc \
|
||||
--arg id '${{ inputs.build-job-id }}' \
|
||||
--arg inst '${{ github.event.installation.id }}' \
|
||||
--arg img '${{ inputs.image }}' \
|
||||
'{buildJobId:$id, outcome:"success", installationId:$inst, log:("built "+$img)}')"
|
||||
|
||||
- name: Report failure to platform
|
||||
if: failure()
|
||||
env:
|
||||
TOKEN: ${{ secrets.PLATFORM_BUILD_CALLBACK_TOKEN }}
|
||||
run: |
|
||||
curl -fsS -X POST "https://platform.hanzo.ai/v1/build-callback" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -nc \
|
||||
--arg id '${{ inputs.build-job-id }}' \
|
||||
--arg inst '${{ github.event.installation.id }}' \
|
||||
'{buildJobId:$id, outcome:"failure", installationId:$inst, error:"build step failed"}')"
|
||||
@@ -1,21 +0,0 @@
|
||||
# Platform-native CI/CD — platform.hanzo.ai owns the build, escaping the
|
||||
# cross-org GitHub Actions reusable workflow (hanzoai/.github docker-build.yml),
|
||||
# which bootnode (a separate GitHub org) cannot access. Platform receives the
|
||||
# repo webhook, schedules the build onto the self-hosted arcd pool, and pushes
|
||||
# the image. See hanzoai/platform docs/PLATFORM_CI.md.
|
||||
#
|
||||
# Scope: the API image — the v1.0.0 backend (Lux/Hanzo/Zoo node deploy,
|
||||
# contracts, subgraphs, mpc, launch). bootnode also ships a `web` image; the
|
||||
# .platform.yml schema is single-build per repo, so web needs a follow-up
|
||||
# (a multi-build schema extension, or its own config). Deploy is intentionally
|
||||
# omitted: bootnode-k8s rolls via universe, not a hanzo-operator Service CR
|
||||
# (the deploy block's target), so this is build-only until that's wired.
|
||||
build:
|
||||
matrix:
|
||||
- { os: linux, arch: amd64 }
|
||||
dockerfile: ./Dockerfile
|
||||
context: ./api
|
||||
image: ghcr.io/hanzoai/bootnode
|
||||
tag-pattern: "{{git.sha}}-amd64-api"
|
||||
push: true
|
||||
dispatch: native
|
||||
@@ -0,0 +1,49 @@
|
||||
# Hanzo CI/CD — one declarative build + test + deploy config.
|
||||
#
|
||||
# Read by .github/workflows/cicd.yml (which runs on our self-hosted arc runners,
|
||||
# not GitHub-hosted) and by platform.hanzo.ai. Replaces the old .platform.yml.
|
||||
# Registry + cluster credentials come from KMS at build time (the repo only sets
|
||||
# KMS_CLIENT_ID / KMS_CLIENT_SECRET + KMS_WORKSPACE), so no long-lived secrets
|
||||
# live in GitHub.
|
||||
|
||||
images:
|
||||
- name: api
|
||||
context: ./api
|
||||
dockerfile: ./api/Dockerfile
|
||||
repo: ghcr.io/hanzoai/bootnode
|
||||
tag-suffix: api
|
||||
- name: web
|
||||
context: ./web
|
||||
dockerfile: ./web/Dockerfile
|
||||
repo: ghcr.io/hanzoai/bootnode
|
||||
tag-suffix: web
|
||||
|
||||
# Commands run on the arc runner after the image builds. Non-zero fails the run.
|
||||
test:
|
||||
- name: api
|
||||
run: >-
|
||||
docker compose -f compose.yml up -d postgres redis >/dev/null 2>&1;
|
||||
cd api &&
|
||||
DATABASE_URL=postgresql+asyncpg://bootnode:bootnode@localhost:5432/bootnode
|
||||
JWT_SECRET=ci API_KEY_SALT=ci
|
||||
uv run pytest -q --tb=short
|
||||
|
||||
# Roll the freshly-built images onto the cluster. Only on the listed branches /
|
||||
# on v* tags. The operator (or kubectl, via the KMS kubeconfig) patches each
|
||||
# Service to the new image. Omit this block for build-only repos.
|
||||
deploy:
|
||||
cluster: bootnode-k8s
|
||||
namespace: bootnode
|
||||
on:
|
||||
- main
|
||||
services:
|
||||
- name: web3-platform-api
|
||||
image: api
|
||||
- name: web3-platform-web-bootnode
|
||||
image: web
|
||||
|
||||
# KMS source for /deploy secrets (GHCR_TOKEN, KUBECONFIG). workspace + endpoint
|
||||
# are supplied as repo vars so this file carries no secrets.
|
||||
kms:
|
||||
path: /deploy
|
||||
environment: prod
|
||||
Reference in New Issue
Block a user