fix(billing): carry the ledger to every surface that shares the balan… #598
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
| # Deployment managed by hanzoai/universe — this workflow only builds and pushes images | |
| name: Build | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - test | |
| - dev | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| # Private cross-org modules (hanzoai/*, zap-proto/*, git.hanzo.ai/*) are fetched via | |
| # authenticated git, bypassing the public proxy — the SAME set the Dockerfile uses; | |
| # the per-job git-auth step (token insteadOf) lets `go` reach them. | |
| # | |
| # luxfi/* is PUBLIC and lives on the Go module proxy, but its git tags are transient | |
| # — the remote re-tags, so a pinned tag (geth v1.17.12) later 404s on a direct git | |
| # fetch ("unknown revision"). Resolve luxfi through the proxy (immutable versions); | |
| # GONOSUMDB skips the transparency log (whose stale pre-re-tag hashes would | |
| # mismatch), and go.sum still verifies the download. | |
| env: | |
| GOPRIVATE: github.com/hanzoai/*,github.com/zap-proto/*,git.hanzo.ai/* | |
| GONOSUMDB: github.com/hanzoai/*,github.com/luxfi/*,github.com/zap-proto/*,git.hanzo.ai/* | |
| jobs: | |
| policy-check: | |
| name: Enforce Hanzo Cloud production policy | |
| runs-on: hanzo-build-linux-amd64 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Enforce code-level invariants | |
| run: | | |
| set -euo pipefail | |
| # Deployment manifests now live in hanzoai/cloud + universe (ai is no | |
| # longer the image source), so k8s/ policy checks moved there. Keep the | |
| # code-level invariants that still belong to this repo. | |
| grep -q 'recordUsage' controllers/openai_api.go | |
| go-tests: | |
| name: Running Go tests | |
| runs-on: hanzo-build-linux-amd64 | |
| needs: [ policy-check ] | |
| # SQLite-only — Hanzo canon is Base/SQLite, never MySQL/Postgres. cloud-api is | |
| # single-pod (the balance-ledger invariant pins replicas:1), so SQLite is the | |
| # CORRECT driver here, not a CI stub. conf.GetConfigString reads env before | |
| # app.conf, so this overrides the driver for the test run; DB-touching tests | |
| # use a hermetic in-memory DB. No service container — the self-hosted runner | |
| # can't start them, which is exactly what the cargo-culted mysql:5.7 broke. | |
| env: | |
| # Match the shipped build (Dockerfile: CGO_ENABLED=0). ai's ONE sqlite | |
| # driver, github.com/hanzoai/sqlite, is pure-Go modernc under !cgo — so the | |
| # test run exercises the exact backend prod uses and never needs a C | |
| # toolchain. (Under cgo the fork is mattn/SQLCipher, which prod does not use.) | |
| CGO_ENABLED: "0" | |
| driverName: sqlite | |
| dataSourceName: "file:cloud_ci?mode=memory&cache=shared" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.26.4' | |
| cache-dependency-path: ./go.mod | |
| - name: Auth private modules (hanzoai/*, zap-proto/*) | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT || secrets.UNIVERSE_DISPATCH_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| # hanzoai/* private modules resolve from github.com with a token; the | |
| # git.hanzo.ai mirror 403s / lags for some repos (dashscope-go-sdk, | |
| # money, decimal). luxfi/*, zap-proto/* are public on github.com and | |
| # resolve directly. go.sum verifies content, so downloads stay pinned. | |
| git config --global url."https://x-access-token:${GH_TOKEN}@github.com/hanzoai/".insteadOf "https://github.com/hanzoai/" | |
| - name: Tests | |
| run: go test -v $(go list ./...) -tags skipCi | |
| # NOTE: there is intentionally no standalone Front-end job. The deployable | |
| # image's Dockerfile builds web/ in its `front` stage (node:22-alpine → | |
| # `pnpm install --frozen-lockfile && pnpm build` → COPY --from=front | |
| # /web/build), so the Docker Release stage IS the authoritative frontend gate | |
| # — a broken web/ build fails the image. A second GHA build of the same web/ | |
| # was a redundant duplicate that OOM-killed the self-hosted runner (heap >8GB) | |
| # and gated nothing. One way to build the frontend: the Dockerfile. | |
| backend: | |
| name: Back-end | |
| runs-on: hanzo-build-linux-amd64 | |
| needs: [ go-tests ] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.26.4' | |
| cache-dependency-path: ./go.mod | |
| - name: Auth private modules (hanzoai/*, zap-proto/*) | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT || secrets.UNIVERSE_DISPATCH_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| # hanzoai/* private modules resolve from github.com with a token; the | |
| # git.hanzo.ai mirror 403s / lags for some repos (dashscope-go-sdk, | |
| # money, decimal). luxfi/*, zap-proto/* are public on github.com and | |
| # resolve directly. go.sum verifies content, so downloads stay pinned. | |
| git config --global url."https://x-access-token:${GH_TOKEN}@github.com/hanzoai/".insteadOf "https://github.com/hanzoai/" | |
| - run: go version | |
| - name: Build | |
| # Verify the SAME build config the deployable image uses (Dockerfile: | |
| # CGO_ENABLED=0 go build ./cmd/aid). The prior `-race -extldflags -static` | |
| # check was unreachable for months (go-tests blocked it) and is broken | |
| # anyway: `-race` requires cgo, and static+cgo+luxcpp doesn't link. Compile | |
| # every package against the real shipped config instead. | |
| run: CGO_ENABLED=0 go build ./... | |
| working-directory: ./ | |
| linter: | |
| name: Go-Linter | |
| runs-on: hanzo-build-linux-amd64 | |
| needs: [ go-tests ] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.26.4' | |
| cache: false | |
| - name: gofumpt | |
| # One formatting gate, one tool: gofumpt pinned, built with THIS repo's | |
| # Go toolchain (go.mod: go 1.26.4). The prebuilt golangci-lint binary | |
| # lags the Go release (its latest is built with go1.24) and refuses to | |
| # analyze a newer target — "the Go language version (go1.24) used to | |
| # build golangci-lint is lower than the targeted Go version (1.26.4)" — | |
| # so a downloaded linter can NEVER run here. gofumpt is exactly what the | |
| # old `-E=gofumpt` config enforced; `go install` builds it with the | |
| # runner's go1.26.4, eliminating the version mismatch permanently. No | |
| # private-module auth needed — gofumpt parses source, it doesn't resolve | |
| # dependencies. | |
| run: | | |
| go install mvdan.cc/gofumpt@v0.10.0 | |
| dirty="$(gofumpt -l .)" | |
| if [ -n "$dirty" ]; then | |
| echo "::error::gofumpt: the following files are not formatted (run 'gofumpt -w .'):" | |
| echo "$dirty" | |
| exit 1 | |
| fi | |
| tag-release: | |
| name: Create Tag | |
| runs-on: hanzo-build-linux-amd64 | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| if: github.repository == 'hanzoai/ai' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') | |
| # Gate the release on the PROVEN code checks: go-tests (the full suite) + | |
| # backend (compiles against the shipped CGO_ENABLED=0 config). The frontend is | |
| # built+verified authoritatively by the Dockerfile's `front` stage (the GHA | |
| # frontend job is a redundant duplicate that OOMs the runner); golangci is a | |
| # non-blocking style gate. The Docker stage fails the image if frontend breaks. | |
| needs: [ go-tests, backend ] | |
| outputs: | |
| new-release-published: ${{ steps.semantic.outputs.new_release_published }} | |
| new-release-version: ${{ steps.semantic.outputs.new_release_version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| # semantic-release-action shells out to `npm ci` to install semantic-release, | |
| # but the hanzo-build-linux-amd64 runner has no Node/npm on PATH by default | |
| # (it's a Go+Docker builder). Without this the action prints | |
| # "npm: not found", swallows the error, and exits 0 having published NOTHING | |
| # — so tag-release went green while new_release_published stayed empty and | |
| # Docker Release was skipped. setup-node puts npm on PATH so the release runs. | |
| - name: Set up Node (semantic-release needs npm) | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Create Tag with Semantic Release | |
| id: semantic | |
| uses: cycjimmy/semantic-release-action@v6 | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| github-release: | |
| name: GitHub Release | |
| runs-on: hanzo-build-linux-amd64 | |
| permissions: | |
| contents: write | |
| issues: write | |
| if: github.repository == 'hanzoai/ai' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.tag-release.outputs.new-release-published == 'true' | |
| needs: [ tag-release ] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@v1.3.1 | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| swap-storage: true | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.26.4' | |
| cache-dependency-path: ./go.mod | |
| - name: Auth private modules (hanzoai/*, zap-proto/*) | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT || secrets.UNIVERSE_DISPATCH_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Same rewrite as go-tests/backend: GoReleaser's go-mod hook fetches | |
| # the private hanzoai/* modules and dies (git exit 128) without it. | |
| git config --global url."https://x-access-token:${GH_TOKEN}@github.com/hanzoai/".insteadOf "https://github.com/hanzoai/" | |
| - name: Prepare Go caches | |
| run: | | |
| echo "GOMODCACHE=$RUNNER_TEMP/gomod" >> $GITHUB_ENV | |
| echo "GOCACHE=$RUNNER_TEMP/gocache" >> $GITHUB_ENV | |
| go clean -cache -modcache -testcache -fuzzcache | |
| - name: Run GoReleaser | |
| uses: goreleaser/goreleaser-action@v6 | |
| with: | |
| distribution: goreleaser | |
| version: '~> v2' | |
| # -p 1 serialises the cross-compiles. GoReleaser defaults parallelism to | |
| # nCPU and builds ALL six GOOS/GOARCH targets at once; on this 8Gi runner | |
| # the concurrent COLD `go build`s of the full tree (it imports luxfi/geth) | |
| # burst past the memory limit and the kernel OOM-kills the runner — the | |
| # "self-hosted runner lost communication" death at ~27min. The `backend` | |
| # job proves ONE `go build ./...` fits in 8Gi, so one target at a time is | |
| # the minimal, capacity-preserving fix (no arch dropped, no memory bump). | |
| args: release --clean -p 1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |