mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
build bridge-server on the forge, and refuse an image that quotes a mint
.github/workflows/server.yml could not ship this repo, three ways over: - Its pool `lux-build-amd64` has had no GitHub-side runner since 2026-07-30. Jobs queue and cancel at the 24h timeout, which is what every `cancelled 24h0m2s` run on this repo is. Nothing goes red; it just never happens. - It only ran `kubectl rollout restart`, which delivers NOTHING against bridge-server: the live deployment is digest-pinned with imagePullPolicy IfNotPresent. - It named no namespace, while the deployment lives in `lux-bridge`. The forge runners are alive (16 of them) and advertise both `lux-build-amd64` and `lux-build-linux-amd64` — read them back with `kubectl -n hanzo exec git-runner-0 -- cat /data/.runner`. So the build moves to .hanzo/workflows, which is where native CI belongs, and there is one lane again rather than a live one and a dead one. BUILD ≠ DEPLOY: this publishes an image and stops. bridge-server is governed by Hanzo CD, so shipping is an edit to universe/deploy/lux-bridge/bridge-server.yaml. Patching the live object from CI would be reverted ~90s later, which fails in the worst way — the pod rolls and the old digest quietly returns. The manifest-verify step prints the digest to pin. The smoke is the point. A quote is not advisory: on an exit, handlerUtilaPayoutAction reads back quotes.receive_amount as payoutAmount and bridgeMints exactly it, and bridge-server serves /api for bridge.zoo.network, bridge.zoo.ngo and bridge.pars.network — where 1000 LZOO quoted 47,142,857 ZOO for months. So the image must answer 990 to that exact request, and 400 to an unroutable pair, before a manifest is allowed to exist. Both run against the built container, not the source tree. docker.yml is left alone: it builds ghcr.io/luxfi/bridge from the root Dockerfile, a different image and a different lane. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
name: server
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- app/server/**
|
||||
- pkg/core/**
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: lux-build-amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.15.0
|
||||
run_install: false
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: Set up kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
with:
|
||||
version: v1.30.6
|
||||
- name: Deploy to Kubernetes
|
||||
env:
|
||||
KUBE_YAML: ${{ secrets.KUBE_YAML }}
|
||||
run: 'echo "$KUBE_YAML" > kubeconfig.yaml
|
||||
|
||||
export KUBECONFIG=$(pwd)/kubeconfig.yaml
|
||||
|
||||
pnpm run k8s:restart
|
||||
|
||||
'
|
||||
working-directory: app/server
|
||||
@@ -0,0 +1,171 @@
|
||||
name: Server
|
||||
# Builds ghcr.io/luxfi/bridge-server on the native runners (git.hanzo.ai →
|
||||
# git-runner), which is the only fleet that currently serves this repo: the
|
||||
# GitHub-side pool `lux-build-amd64` has had no runner since 2026-07-30, so
|
||||
# .github/workflows jobs queue and cancel at the 24h timeout without ever
|
||||
# reporting a failure. The forge runners advertise both `lux-build-amd64` and
|
||||
# `lux-build-linux-amd64`; read them back with
|
||||
# kubectl -n hanzo exec git-runner-0 -- cat /data/.runner
|
||||
#
|
||||
# BUILD ≠ DEPLOY. This publishes an image and stops. bridge-server is governed
|
||||
# by Hanzo CD (`apps.hanzo.ai/installation-id: hanzo-cd`) and runs a
|
||||
# DIGEST-PINNED image, so shipping is a separate edit to
|
||||
# universe/deploy/lux-bridge/bridge-server.yaml. A `kubectl rollout restart`
|
||||
# from CI — which is what .github/workflows/server.yml does — delivers nothing
|
||||
# against a pinned digest, and patching the live object would be reverted by CD.
|
||||
#
|
||||
# ghcr auth: GHCR_USER / GHCR_TOKEN, org Actions secrets on this forge. The
|
||||
# instance GITHUB_TOKEN is a Hanzo Git token and is NOT valid at ghcr.io.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'app/server/**'
|
||||
- 'pkg/core/**'
|
||||
- '.hanzo/workflows/server.yml'
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: server-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [lux-build-linux-amd64]
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Compute tags
|
||||
id: meta
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SHA_TAG="ghcr.io/luxfi/bridge-server:sha-${GITHUB_SHA:0:7}"
|
||||
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
{
|
||||
echo "tags<<EOF"
|
||||
echo "ghcr.io/luxfi/bridge-server:v${VERSION}"
|
||||
echo "ghcr.io/luxfi/bridge-server:${VERSION}"
|
||||
echo "$SHA_TAG"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "probe=ghcr.io/luxfi/bridge-server:v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
{
|
||||
echo "tags<<EOF"
|
||||
echo "$SHA_TAG"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "probe=$SHA_TAG" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: docker-container
|
||||
driver-opts: network=host
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.GHCR_USER }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Build (load for smoke, not yet pushed)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
# The Dockerfile installs the pnpm workspace, so the context is the
|
||||
# repo root and the file is addressed from it.
|
||||
context: .
|
||||
file: app/server/docker/Dockerfile
|
||||
platforms: linux/amd64
|
||||
load: true
|
||||
push: false
|
||||
provenance: false
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
- name: Smoke — it serves, and it does not quote a mint
|
||||
# Two things are checked, and the second is the reason this step exists.
|
||||
#
|
||||
# A quote is not advisory: on an exit, handlerUtilaPayoutAction reads
|
||||
# back quotes.receive_amount as payoutAmount and bridgeMints exactly it.
|
||||
# bridge-server serves /api for bridge.zoo.network, bridge.zoo.ngo and
|
||||
# bridge.pars.network, and it shipped for months quoting 1000 LZOO as
|
||||
# 47,142,857 ZOO because a failed price lookup answered $1.00. An image
|
||||
# that would do that again must not reach the registry.
|
||||
#
|
||||
# POSTGRES_URL only has to be SET, not reachable: PrismaClient reads it
|
||||
# at construction and would abort startup without it, but connects
|
||||
# lazily, and neither /health nor /api/quote touches the database.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
img="${{ steps.meta.outputs.probe }}"
|
||||
cid=$(docker run -d -p 3000:3000 \
|
||||
-e PORT=3000 \
|
||||
-e POSTGRES_URL="postgresql://smoke:[email protected]:5432/smoke" \
|
||||
"$img")
|
||||
trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT
|
||||
|
||||
ok=""
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:3000/health >/dev/null 2>&1; then ok=1; break; fi
|
||||
sleep 2
|
||||
done
|
||||
if [ -z "$ok" ]; then
|
||||
docker logs "$cid" 2>&1 | tail -40
|
||||
echo "::error::bridge-server never answered /health"; exit 1
|
||||
fi
|
||||
echo "health ok"
|
||||
|
||||
# 1000 LZOO out of Lux is a 1% exit fee on a wrap: exactly 990.
|
||||
got=$(curl -sf -G http://localhost:3000/api/quote \
|
||||
--data-urlencode 'source_network=LUX_MAINNET' \
|
||||
--data-urlencode 'source_token=LZOO' \
|
||||
--data-urlencode 'destination_network=ZOO_MAINNET' \
|
||||
--data-urlencode 'destination_token=ZOO' \
|
||||
--data-urlencode 'amount=1000' \
|
||||
--data-urlencode 'refuel=false' \
|
||||
--data-urlencode 'use_deposit_address=false' \
|
||||
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{process.stdout.write(String(JSON.parse(s).data.quote.receive_amount))})')
|
||||
if [ "$got" != "990" ]; then
|
||||
echo "::error::1000 LZOO quoted ${got}, want 990 — this image would mint it"; exit 1
|
||||
fi
|
||||
echo "quote ok: 1000 LZOO -> 990 ZOO"
|
||||
|
||||
# An unroutable pair must be refused, not priced.
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -G http://localhost:3000/api/quote \
|
||||
--data-urlencode 'source_network=LUX_MAINNET' \
|
||||
--data-urlencode 'source_token=LBTC' \
|
||||
--data-urlencode 'destination_network=ZOO_MAINNET' \
|
||||
--data-urlencode 'destination_token=ZETH' \
|
||||
--data-urlencode 'amount=1')
|
||||
if [ "$code" != "400" ]; then
|
||||
echo "::error::LBTC->ZETH answered ${code}, want 400"; exit 1
|
||||
fi
|
||||
echo "unroutable pair refused with 400"
|
||||
|
||||
- name: Push
|
||||
run: |
|
||||
set -euo pipefail
|
||||
while IFS= read -r t; do
|
||||
[ -n "$t" ] && docker push "$t"
|
||||
done <<< "${{ steps.meta.outputs.tags }}"
|
||||
|
||||
- name: Verify the pushed manifest resolves
|
||||
# build-push can exit 0 while the manifest is not yet resolvable, and a
|
||||
# CR pinned to a phantom tag becomes ImagePullBackOff.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
img="${{ steps.meta.outputs.probe }}"
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if docker manifest inspect "$img" >/dev/null 2>&1; then
|
||||
echo "$img is resolvable"
|
||||
echo "pin this in universe/deploy/lux-bridge/bridge-server.yaml:"
|
||||
docker manifest inspect -v "$img" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const j=JSON.parse(s);console.log(" digest: "+(j.Descriptor?.digest||j.descriptor?.digest||"(unknown)"))})'
|
||||
exit 0
|
||||
fi
|
||||
echo "manifest not visible yet (attempt ${i}/6) — retrying"; sleep 5
|
||||
done
|
||||
echo "::error::$img not pullable after push"; exit 1
|
||||
Reference in New Issue
Block a user