mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
One label per runner, spelled <os>-<arch>. The aliases these jobs named were removed, and a job naming a label no runner carries waits instead of failing. Co-authored-by: Hanzo Dev <[email protected]>
180 lines
7.8 KiB
YAML
180 lines
7.8 KiB
YAML
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.
|
|
|
|
# No `paths:` filter, deliberately. luxfi/bridge reaches this forge as a MIRROR
|
|
# of GitHub, and a path filter has to be evaluated against a diff the mirror sync
|
|
# does not necessarily compute — the first version of this file carried one, the
|
|
# forge synced the commit, and no job was ever dispatched. luxfi/kms, which does
|
|
# build here, filters on nothing. Matching that is worth more than skipping the
|
|
# occasional build of an untouched tree.
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
tags: ['v*']
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: server-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: [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.
|
|
# Probing runs INSIDE the container, via `docker exec`, not over a
|
|
# published port. The daemon here is a DinD sidecar, so `-p 3000:3000`
|
|
# publishes into the daemon's network namespace rather than this job's,
|
|
# and `curl localhost:3000` from the step would miss a perfectly healthy
|
|
# container. It uses node's global fetch rather than curl because the
|
|
# image is node:20-alpine and has no curl.
|
|
run: |
|
|
set -euo pipefail
|
|
img="${{ steps.meta.outputs.probe }}"
|
|
cid=$(docker run -d \
|
|
-e PORT=3000 \
|
|
-e POSTGRES_URL="postgresql://smoke:[email protected]:5432/smoke" \
|
|
"$img")
|
|
trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT
|
|
|
|
# `get` prints "<status> <body>" for a path, or exits non-zero.
|
|
get() {
|
|
docker exec "$cid" node -e '
|
|
fetch("http://127.0.0.1:3000" + process.argv[1])
|
|
.then(async (r) => { console.log(r.status + " " + (await r.text())) })
|
|
.catch((e) => { console.error(e.message); process.exit(1) })
|
|
' "$1"
|
|
}
|
|
|
|
ok=""
|
|
for i in $(seq 1 30); do
|
|
if get /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.
|
|
q='/api/quote?source_network=LUX_MAINNET&source_token=LZOO&destination_network=ZOO_MAINNET&destination_token=ZOO&amount=1000&refuel=false&use_deposit_address=false'
|
|
out=$(get "$q")
|
|
got=$(printf '%s' "${out#* }" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(String(JSON.parse(s).data.quote.receive_amount))}catch(e){process.stdout.write("unparseable")}})')
|
|
if [ "$got" != "990" ]; then
|
|
echo "::error::1000 LZOO quoted ${got}, want 990 — this image would mint it"
|
|
echo "response: $out"; exit 1
|
|
fi
|
|
echo "quote ok: 1000 LZOO -> 990 ZOO"
|
|
|
|
# An unroutable pair must be refused, not priced.
|
|
bad='/api/quote?source_network=LUX_MAINNET&source_token=LBTC&destination_network=ZOO_MAINNET&destination_token=ZETH&amount=1'
|
|
code=$(get "$bad" | cut -d' ' -f1)
|
|
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
|