Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions .github/scripts/repin-x402-images.sh
Original file line number Diff line number Diff line change
@@ -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 <commit-ish>
#
# 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) —
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
23 changes: 13 additions & 10 deletions .github/scripts/verify-x402-pins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down Expand Up @@ -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 <commit>" >&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 <commit>" >&2
exit 1
fi

Expand All @@ -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}")"
Expand All @@ -112,23 +115,23 @@ 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
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
fi
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}"
Expand All @@ -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)"

Expand Down
21 changes: 17 additions & 4 deletions .github/workflows/docker-publish-x402.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/release-prep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
23 changes: 16 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/release-x402-pins.md
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions internal/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@ func rewriteDevDigestPins(defaultsDir, devTag string) error {
// <base>:<7-40 hex>@sha256:<64 hex> tag + digest combo
// <base>@sha256:<64 hex> digest-only pin
// <base>:<7-40 hex> short-SHA tag pin (e.g. b13254e)
// <base>: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)
// <base>: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 `:<hex>` match and leave a stray `@sha256:<digest>` suffix,
// which Docker still resolves to the immutable registry image and
Expand Down
11 changes: 6 additions & 5 deletions internal/defaults/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-<sha> 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-<sha> 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
Expand Down
3 changes: 3 additions & 0 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading