From 27de73fbdd1b75ab044a252ad5478d70109899f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn=20Kyne?= Date: Wed, 8 Jul 2026 20:11:40 +0100 Subject: [PATCH] feat: update ci, build image for job buyer --- .github/scripts/repin-x402-images.sh | 22 ++++++++++-------- .github/scripts/verify-x402-pins.sh | 23 +++++++++++-------- .github/workflows/docker-publish-x402.yml | 21 +++++++++++++---- .github/workflows/release-prep.yml | 11 +++++---- .github/workflows/release.yml | 23 +++++++++++++------ docs/release-x402-pins.md | 6 ++--- internal/defaults/defaults.go | 10 ++++---- internal/defaults/defaults_test.go | 11 +++++---- .../infrastructure/base/templates/x402.yaml | 3 +++ 9 files changed, 82 insertions(+), 48 deletions(-) diff --git a/.github/scripts/repin-x402-images.sh b/.github/scripts/repin-x402-images.sh index 5f6d93e4..70e70456 100755 --- a/.github/scripts/repin-x402-images.sh +++ b/.github/scripts/repin-x402-images.sh @@ -1,16 +1,16 @@ #!/usr/bin/env bash # Repin the embedded x402 image references (x402-verifier, -# serviceoffer-controller, x402-buyer) to the images built from a given -# commit. +# serviceoffer-controller, x402-buyer, job-broker) to the images built from +# a given commit. # # .github/scripts/repin-x402-images.sh # -# CI runs this from the repin-embedded-pins job in docker-publish-x402.yml -# after every successful branch build, so the embedded manifests track the -# images built from the same source. Operators can run it manually for -# ad-hoc repins (the rc11/rc14 pattern, cf. 8fb1553 / 2db429b) — the images -# must already exist on GHCR at the 7-char short-SHA tag, which the -# docker-publish-x402 workflow publishes for every build. +# CI runs this from release-prep.yml (workflow_dispatch), which builds the +# images for the release commit and opens a reviewed repin PR — see +# docs/release-x402-pins.md. Operators can run it manually for ad-hoc +# repins (the rc11/rc14 pattern, cf. 8fb1553 / 2db429b) — the images must +# already exist on GHCR at the 7-char short-SHA tag, which release-prep and +# every main-push docker-publish-x402 build publish. # # The script rewrites image lines in the two embedded templates and nothing # else. The digest written is the multi-arch index digest (amd64+arm64) — @@ -38,9 +38,12 @@ repin_image() { return 1 } ref="ghcr.io/obolnetwork/${image}:${SHORT_SHA}@${digest}" + # The digest suffix is optional so a component's FIRST repin converts a + # plain `:latest` (or bare short-SHA) pin into the digest-pinned form — + # job-broker shipped as `:latest` before it joined the pin regime. # BSD and GNU sed both accept -i with an attached suffix. sed -E -i.repin-bak \ - "s|image: ghcr\.io/obolnetwork/${image}:[A-Za-z0-9._-]+@sha256:[0-9a-f]{64}|image: ${ref}|" \ + "s|image: ghcr\.io/obolnetwork/${image}:[A-Za-z0-9._-]+(@sha256:[0-9a-f]{64})?|image: ${ref}|" \ "${REPO_ROOT}/${file}" rm -f "${REPO_ROOT}/${file}.repin-bak" if ! grep -q "image: ${ref}" "${REPO_ROOT}/${file}"; then @@ -52,6 +55,7 @@ repin_image() { repin_image "${X402_YAML}" "x402-verifier" repin_image "${X402_YAML}" "serviceoffer-controller" +repin_image "${X402_YAML}" "job-broker" repin_image "${LLM_YAML}" "x402-buyer" # Keep the human-readability note in llm.yaml in sync with the tag. diff --git a/.github/scripts/verify-x402-pins.sh b/.github/scripts/verify-x402-pins.sh index 5a2f6d9e..c57bc6db 100755 --- a/.github/scripts/verify-x402-pins.sh +++ b/.github/scripts/verify-x402-pins.sh @@ -5,9 +5,9 @@ # # "Stale" means: some commit after the pinned build commit changed source # that compiles into the x402-verifier / serviceoffer-controller / -# x402-buyer binaries (computed live from `go list -deps`, plus their -# Dockerfiles and go.mod/go.sum), so the pinned images no longer match the -# source being released. This is the gate that makes the rc14 trap — +# x402-buyer / job-broker binaries (computed live from `go list -deps`, +# plus their Dockerfiles and go.mod/go.sum), so the pinned images no longer +# match the source being released. This is the gate that makes the rc14 trap — # tagging a release whose embedded pins predate the release's own # verifier/buyer changes — structurally impossible. # @@ -57,15 +57,17 @@ extract_pin() { # (read -r x <<<"$(cmd)" would swallow cmd's exit status). verifier_pin="$(extract_pin "${X402_YAML}" "x402-verifier")" controller_pin="$(extract_pin "${X402_YAML}" "serviceoffer-controller")" +broker_pin="$(extract_pin "${X402_YAML}" "job-broker")" buyer_pin="$(extract_pin "${LLM_YAML}" "x402-buyer")" read -r VERIFIER_TAG VERIFIER_DIGEST <<<"${verifier_pin}" read -r CONTROLLER_TAG CONTROLLER_DIGEST <<<"${controller_pin}" +read -r BROKER_TAG BROKER_DIGEST <<<"${broker_pin}" read -r BUYER_TAG BUYER_DIGEST <<<"${buyer_pin}" -if [[ ! ("${VERIFIER_TAG}" == "${CONTROLLER_TAG}" && "${VERIFIER_TAG}" == "${BUYER_TAG}") ]]; then +if [[ ! ("${VERIFIER_TAG}" == "${CONTROLLER_TAG}" && "${VERIFIER_TAG}" == "${BUYER_TAG}" && "${VERIFIER_TAG}" == "${BROKER_TAG}") ]]; then echo "error: embedded x402 pins do not share one build commit:" >&2 - echo " x402-verifier=${VERIFIER_TAG} serviceoffer-controller=${CONTROLLER_TAG} x402-buyer=${BUYER_TAG}" >&2 - echo " Repin all three from one commit: .github/scripts/repin-x402-images.sh " >&2 + echo " x402-verifier=${VERIFIER_TAG} serviceoffer-controller=${CONTROLLER_TAG} x402-buyer=${BUYER_TAG} job-broker=${BROKER_TAG}" >&2 + echo " Repin all four from one commit: .github/scripts/repin-x402-images.sh " >&2 exit 1 fi @@ -89,6 +91,7 @@ else for entry in \ "x402-verifier ${VERIFIER_DIGEST}" \ "serviceoffer-controller ${CONTROLLER_DIGEST}" \ + "job-broker ${BROKER_DIGEST}" \ "x402-buyer ${BUYER_DIGEST}"; do read -r image embedded_digest <<<"${entry}" live_digest="$(fetch_index_digest "${image}" "${PIN_TAG}")" @@ -112,7 +115,7 @@ fi # Computed FAIL-CLOSED: a process substitution would swallow a go-list # failure under set -e and silently gut the gate down to the static paths, # turning any toolchain hiccup into a false PASS on stale pins. -if ! deps_out="$(go list -deps ./cmd/x402-verifier ./cmd/x402-buyer ./cmd/serviceoffer-controller)"; then +if ! deps_out="$(go list -deps ./cmd/x402-verifier ./cmd/x402-buyer ./cmd/serviceoffer-controller ./cmd/job-broker)"; then echo "error: 'go list -deps' failed; cannot compute the component import graph." >&2 echo " Refusing to pass the gate on a partial path set." >&2 exit 1 @@ -120,7 +123,7 @@ fi graph="$(grep '^github.com/ObolNetwork/obol-stack/' <<<"${deps_out}" \ | sed 's|^github.com/ObolNetwork/obol-stack/||' \ | sort -u)" -for must in cmd/x402-verifier cmd/x402-buyer cmd/serviceoffer-controller; do +for must in cmd/x402-verifier cmd/x402-buyer cmd/serviceoffer-controller cmd/job-broker; do if ! grep -qx "${must}" <<<"${graph}"; then echo "error: import graph is missing ${must} (module path changed?); refusing to pass." >&2 exit 1 @@ -128,7 +131,7 @@ for must in cmd/x402-verifier cmd/x402-buyer cmd/serviceoffer-controller; do done # (while-read instead of mapfile: macOS ships bash 3.2.) -paths=(go.mod go.sum Dockerfile.x402-verifier Dockerfile.x402-buyer Dockerfile.serviceoffer-controller) +paths=(go.mod go.sum Dockerfile.x402-verifier Dockerfile.x402-buyer Dockerfile.serviceoffer-controller Dockerfile.job-broker) while IFS= read -r dir; do paths+=("${dir}") done <<<"${graph}" @@ -143,7 +146,7 @@ stale_files="$(git diff --name-only "${PIN_COMMIT}..${RELEASE_REF}" -- "${paths[ pin_template_drift="$(git diff --unified=0 "${PIN_COMMIT}..${RELEASE_REF}" -- "${X402_YAML}" "${LLM_YAML}" \ | grep -E '^[+-]' \ | grep -Ev '^(\+\+\+ |--- )' \ - | grep -Ev '^[+-][[:space:]]*image: ghcr\.io/obolnetwork/(x402-verifier|serviceoffer-controller|x402-buyer):' \ + | grep -Ev '^[+-][[:space:]]*image: ghcr\.io/obolnetwork/(x402-verifier|serviceoffer-controller|x402-buyer|job-broker):' \ | grep -Ev '^[+-][[:space:]]*# hosts\. The :[0-9a-f]{7,40} tag is preserved' \ || true)" diff --git a/.github/workflows/docker-publish-x402.yml b/.github/workflows/docker-publish-x402.yml index 288b0f89..ddb90364 100644 --- a/.github/workflows/docker-publish-x402.yml +++ b/.github/workflows/docker-publish-x402.yml @@ -2,11 +2,15 @@ name: Build and Publish x402 Images on: push: + # main only — release images are built from main (or via the manual + # release-prep.yml dispatch); feature/integration branches never publish. branches: - main - # Release trains get the same build + auto-repin treatment, so a tag - # cut on the branch never needs a manual workflow_dispatch first. - - 'release/**' + # Tag pushes MUST keep building: path filters below do not apply to tags, + # and internal/images.Resolve commit-pins demo-server (and the release + # binary's own components) to the tag commit's short SHA — the tag build + # is what guarantees those SHA tags exist even when the same commit's + # main-push build was skipped by the paths filter (e.g. a docs-only merge). tags: - 'v*' # Approximates the import graph of the built binaries (verifier, @@ -39,14 +43,17 @@ on: - 'internal/walletbackup/**' - 'internal/x402/**' - 'internal/demo/**' + - 'internal/jobbroker/**' - 'cmd/x402-verifier/**' - 'cmd/x402-buyer/**' - 'cmd/serviceoffer-controller/**' - 'cmd/demo-server/**' + - 'cmd/job-broker/**' - 'Dockerfile.x402-verifier' - 'Dockerfile.x402-buyer' - 'Dockerfile.serviceoffer-controller' - 'Dockerfile.demo-server' + - 'Dockerfile.job-broker' - 'go.mod' - 'go.sum' - '.github/workflows/docker-publish-x402.yml' @@ -89,6 +96,10 @@ jobs: image: obolnetwork/demo-server dockerfile: Dockerfile.demo-server description: Demo HTTP services for Obol Stack sell demo + - component: job-broker + image: obolnetwork/job-broker + dockerfile: Dockerfile.job-broker + description: Async job delivery broker for x402-paid ServiceOffers steps: - name: Checkout @@ -125,7 +136,7 @@ jobs: # it through internal/images.Resolve to commit-pin the deployments # it manages. Without this, binary upgrades wouldn't roll the pods. type=sha,format=short,prefix= - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feat/secure-enclave-inference' }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} labels: | org.opencontainers.image.title=${{ matrix.component }} org.opencontainers.image.description=${{ matrix.description }} @@ -167,6 +178,8 @@ jobs: image: obolnetwork/serviceoffer-controller - component: demo-server image: obolnetwork/demo-server + - component: job-broker + image: obolnetwork/job-broker steps: - name: Run Trivy vulnerability scanner diff --git a/.github/workflows/release-prep.yml b/.github/workflows/release-prep.yml index fb007208..f0ad62e6 100644 --- a/.github/workflows/release-prep.yml +++ b/.github/workflows/release-prep.yml @@ -8,7 +8,7 @@ # # Run this once when preparing a release (the natural moment the pins need to be # fresh — release.yml's verify-x402-pins gate is also release-time). It: -# 1. builds the four x402 images for the release commit, and +# 1. builds the x402 component images for the release commit, and # 2. opens an auto-merging PR that repins the embedded manifests to them. # # The pin bump lands through the normal review path — NO ruleset bypass, no @@ -51,10 +51,10 @@ env: jobs: # --------------------------------------------------------------------------- - # Build the four x402 images for the release commit so the pins can reference - # them by digest. Mirrors docker-publish-x402.yml's build matrix; images are - # tagged by the release commit's short and long SHA (repin-x402-images.sh - # resolves the digest from the short-SHA tag). + # Build the x402 component images for the release commit so the pins can + # reference them by digest. Mirrors docker-publish-x402.yml's build matrix; + # images are tagged by the release commit's short and long SHA + # (repin-x402-images.sh resolves the digest from the short-SHA tag). # --------------------------------------------------------------------------- build-images: runs-on: ubuntu-latest @@ -69,6 +69,7 @@ jobs: - { image: obolnetwork/x402-buyer, dockerfile: Dockerfile.x402-buyer } - { image: obolnetwork/serviceoffer-controller, dockerfile: Dockerfile.serviceoffer-controller } - { image: obolnetwork/demo-server, dockerfile: Dockerfile.demo-server } + - { image: obolnetwork/job-broker, dockerfile: Dockerfile.job-broker } steps: - name: Checkout release ref uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b75a4fbf..6f19924c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,14 @@ # # How to use: # -# 1. Create and push a release tag: +# 0. Refresh the embedded x402 image pins first: run the +# "Release Prep - repin x402 pins" workflow against the release commit +# and merge the PR it opens (docs/release-x402-pins.md). Skipping this +# fails the verify-image-pins gate below and NO assets get built — +# the v0.13.0-rc0/rc1 empty-release trap. +# +# 1. Create and push a release tag ON MAIN (tags cut on integration or +# feature branches fail the pin gate — images are only built from main): # $ git tag v0.1.0 # $ git push origin v0.1.0 # @@ -51,12 +58,14 @@ permissions: jobs: # Gate: the embedded x402 image pins (x402-verifier, serviceoffer-controller, - # x402-buyer) must have been built from source that contains every change to - # those binaries' import graph. Without this, a tag can ship manifests that - # deploy images predating the release's own payment-path changes (the - # v0.10.0-rc14 stale-pin trap). The repin-embedded-pins job in - # docker-publish-x402.yml normally lands the bump automatically; this gate - # makes sure the tag was cut after it. + # x402-buyer, job-broker) must have been built from source that contains + # every change to those binaries' import graph. Without this, a tag can ship + # manifests that deploy images predating the release's own payment-path + # changes (the v0.10.0-rc14 stale-pin trap, repeated by v0.13.0-rc0/rc1). + # Repinning is MANUAL: run the "Release Prep - repin x402 pins" workflow + # (release-prep.yml) against the release commit and merge its PR BEFORE + # tagging — see docs/release-x402-pins.md. This gate makes sure that + # happened. verify-image-pins: name: Verify embedded x402 image pins are fresh runs-on: ubuntu-latest diff --git a/docs/release-x402-pins.md b/docs/release-x402-pins.md index 3c933bac..a1ed4343 100644 --- a/docs/release-x402-pins.md +++ b/docs/release-x402-pins.md @@ -1,8 +1,8 @@ # Releasing: x402 image pins The embedded infrastructure manifests pin the x402 images -(`x402-verifier`, `serviceoffer-controller`, `x402-buyer`) by tag **and** -digest, in: +(`x402-verifier`, `serviceoffer-controller`, `job-broker`, `x402-buyer`) by +tag **and** digest, in: - `internal/embed/infrastructure/base/templates/x402.yaml` - `internal/embed/infrastructure/base/templates/llm.yaml` @@ -17,7 +17,7 @@ pins must point at images built from the release commit before you tag. Run the **Release Prep - repin x402 pins** workflow (`release-prep.yml`, `workflow_dispatch`) with `ref` = the release commit (usually `main`). It: -1. builds the four x402 images for that commit (tagged by its SHA), then +1. builds the x402 component images for that commit (tagged by its SHA), then 2. opens an **auto-merging PR** that repins `x402.yaml` / `llm.yaml` to them. The pin commit is made with the GraphQL `createCommitOnBranch` API onto a diff --git a/internal/defaults/defaults.go b/internal/defaults/defaults.go index 72bb5d31..2b0fb294 100644 --- a/internal/defaults/defaults.go +++ b/internal/defaults/defaults.go @@ -164,11 +164,11 @@ func rewriteDevDigestPins(defaultsDir, devTag string) error { // :<7-40 hex>@sha256:<64 hex> tag + digest combo // @sha256:<64 hex> digest-only pin // :<7-40 hex> short-SHA tag pin (e.g. b13254e) - // :latest unpinned (e.g. job-broker, an - // image not yet published to ghcr, - // so it ships as :latest and has - // no digest for the release - // pipeline to stamp) + // :latest unpinned (a component that has + // not been through its first + // release repin yet — job-broker + // shipped like this before it + // joined the pin regime) // The combo form MUST come first so the engine doesn't stop at the // shorter `:` match and leave a stray `@sha256:` suffix, // which Docker still resolves to the immutable registry image and diff --git a/internal/defaults/defaults_test.go b/internal/defaults/defaults_test.go index 5544011a..0512d260 100644 --- a/internal/defaults/defaults_test.go +++ b/internal/defaults/defaults_test.go @@ -41,11 +41,12 @@ func TestCopyInfrastructure_DevModeRewritesDigestPins(t *testing.T) { for _, base := range []string{ "ghcr.io/obolnetwork/x402-verifier", "ghcr.io/obolnetwork/serviceoffer-controller", - // job-broker ships as :latest (unpublished to ghcr, so no digest for - // the release pipeline to stamp). The dev rewrite must still repoint - // it at the local build's :dev- tag, or the pod ImagePullBackOffs - // on the missing :latest ref. Regression guard for the smoke-surfaced - // bug where the pin regex matched only digest/short-SHA forms. + // job-broker shipped as :latest before its first release repin (any + // new component will again). The dev rewrite must repoint an + // unpinned :latest at the local build's :dev- tag too, or the + // pod ImagePullBackOffs on the unpublished ref. Regression guard for + // the smoke-surfaced bug where the pin regex matched only + // digest/short-SHA forms. "ghcr.io/obolnetwork/job-broker", } { want := base + ":" + devTag diff --git a/internal/embed/infrastructure/base/templates/x402.yaml b/internal/embed/infrastructure/base/templates/x402.yaml index 5eaac0f5..4b6775a9 100644 --- a/internal/embed/infrastructure/base/templates/x402.yaml +++ b/internal/embed/infrastructure/base/templates/x402.yaml @@ -518,6 +518,9 @@ spec: type: RuntimeDefault containers: - name: job-broker + # Digest-pinned by release-prep.yml (repin-x402-images.sh) like the + # verifier/controller/buyer; OBOL_DEVELOPMENT rewrites it to the + # local dev tag. `:latest` only survives until the first repin. image: ghcr.io/obolnetwork/job-broker:latest imagePullPolicy: IfNotPresent securityContext: