Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3059ada
Encode rust+debian in builds.json keys.
fnando May 28, 2026
64429b4
Pin base by digest and trim builder packages.
fnando May 28, 2026
b25d42c
Thread composite base key through scripts.
fnando May 28, 2026
51b314e
Pass base suffix through workflows.
fnando May 28, 2026
72fd8c0
Always use the non-slim base.
fnando May 28, 2026
bb06bad
Document the base image policy.
fnando May 28, 2026
6e47a03
Mention rust base in the release PR body.
fnando May 28, 2026
73b0ce2
Sort composite keys by their rust prefix.
fnando May 28, 2026
fb468e5
Cancel in-flight build runs on PR pushes.
fnando May 28, 2026
1f35d10
Switch base keys to slim trixie.
fnando May 28, 2026
4b0451f
Install build deps that slim doesn't ship.
fnando May 28, 2026
183bb50
Refresh examples and policy for the slim base.
fnando May 28, 2026
dbc8088
Check the base image digest in smoke test.
fnando May 28, 2026
fc6d207
Drop labels that duplicate OCI standard ones.
fnando May 28, 2026
f90251c
Drop all custom labels from the image.
fnando May 28, 2026
0d9c83c
Pick rust base keys from Docker Hub tags.
fnando May 28, 2026
166958e
Refresh docs and examples after dedup.
fnando May 28, 2026
276de52
Add --stellar-cli-ref to tag-names.sh.
fnando May 28, 2026
45bde3c
Publish ref-pinned manifest list per pair.
fnando May 28, 2026
79e5898
List ref-pinned tag in release body.
fnando May 28, 2026
d210b09
Document ref-pinned multi-arch tag.
fnando May 28, 2026
adff676
Use SEP-58 registry@digest form in release body.
fnando May 28, 2026
a3397cf
Allow ref in per-arch tags.
fnando May 28, 2026
5a6cbcd
Embed ref in every published tag.
fnando May 28, 2026
b2f3da8
List every published tag in release body.
fnando May 28, 2026
9840abe
Update docs for the new tag scheme.
fnando May 28, 2026
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
17 changes: 13 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ on:
permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
smoke-build:
name: build & smoke-test newest image
Expand All @@ -20,9 +24,14 @@ jobs:
- name: resolve newest pair
id: pair
run: |
cli="$(./scripts/newest-pair.sh --stellar-cli-version)"
rust="$(./scripts/newest-pair.sh --rust-version)"
tag="$(./scripts/tag-names.sh \
--stellar-cli-version "$cli" --rust-version "$rust")"
{
echo "cli=$(./scripts/newest-pair.sh --stellar-cli-version)"
echo "rust=$(./scripts/newest-pair.sh --rust-version)"
echo "cli=$cli"
echo "rust=$rust"
echo "image=stellar-cli:$tag"
} >> "$GITHUB_OUTPUT"
- name: build image
run: |
Expand All @@ -32,13 +41,13 @@ jobs:
- name: smoke test
run: |
./scripts/smoke-test-image.sh \
--image "stellar-cli:${{ steps.pair.outputs.cli }}-rust${{ steps.pair.outputs.rust }}" \
--image "${{ steps.pair.outputs.image }}" \
--stellar-cli-version "${{ steps.pair.outputs.cli }}" \
--rust-version "${{ steps.pair.outputs.rust }}"
- name: wasm reproducibility
run: |
./scripts/repro-test.sh \
--image "stellar-cli:${{ steps.pair.outputs.cli }}-rust${{ steps.pair.outputs.rust }}"
--image "${{ steps.pair.outputs.image }}"

complete:
if: always()
Expand Down
64 changes: 38 additions & 26 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"

build:
name: ${{ matrix.stellar_cli_version }} rust${{ matrix.rust_version }} ${{ matrix.arch }}
name: ${{ matrix.stellar_cli_version }} rust${{ matrix.rust_base_key }} ${{ matrix.arch }}
needs: matrix
strategy:
matrix: ${{ fromJson(needs.matrix.outputs.matrix) }}
Expand All @@ -72,8 +72,9 @@ jobs:
run: |
tag="$(./scripts/tag-names.sh \
--stellar-cli-version ${{ matrix.stellar_cli_version }} \
--rust-version ${{ matrix.rust_version }} \
--platform ${{ matrix.platform }})"
--rust-version ${{ matrix.rust_base_key }} \
--platform ${{ matrix.platform }} \
--stellar-cli-ref ${{ matrix.stellar_cli_ref }})"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "image=$REGISTRY:$tag" >> "$GITHUB_OUTPUT"

Expand Down Expand Up @@ -104,10 +105,7 @@ jobs:
id: meta
if: steps.skip.outputs.skipped != 'true'
run: |
{
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "builds_json_sha=$(sha256sum builds.json | awk '{print $1}')"
} >> "$GITHUB_OUTPUT"
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"

- name: build and push
id: build
Expand All @@ -122,11 +120,11 @@ jobs:
sbom: true
build-args: |
RUST_VERSION=${{ matrix.rust_version }}
RUST_BASE_SUFFIX=${{ matrix.rust_base_suffix }}
RUST_IMAGE_DIGEST=${{ matrix.rust_image_digest }}
STELLAR_CLI_REV=${{ matrix.stellar_cli_ref }}
STELLAR_CLI_VERSION=${{ matrix.stellar_cli_version }}
BUILD_DATE=${{ steps.meta.outputs.build_date }}
BUILDS_JSON_SHA=${{ steps.meta.outputs.builds_json_sha }}
SOURCE_REPO=${{ github.repository }}

- name: generate SBOM file
Expand All @@ -135,7 +133,7 @@ jobs:
with:
image: ${{ steps.tag.outputs.image }}@${{ steps.build.outputs.digest }}
format: spdx-json
output-file: sbom-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.spdx.json
output-file: sbom-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}.spdx.json
upload-release-assets: false
upload-artifact: false

Expand All @@ -158,28 +156,29 @@ jobs:
with:
subject-name: ${{ env.REGISTRY }}
subject-digest: ${{ steps.build.outputs.digest }}
sbom-path: sbom-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.spdx.json
sbom-path: sbom-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}.spdx.json
push-to-registry: true

- name: write per-arch metadata
if: steps.skip.outputs.skipped != 'true'
run: |
out="meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.json"
out="meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}.json"
jq -n \
--arg arch "${{ matrix.arch }}" \
--arg cli "${{ matrix.stellar_cli_version }}" \
--arg digest "${{ steps.build.outputs.digest }}" \
--arg image "${{ steps.tag.outputs.image }}" \
--arg rust "${{ matrix.rust_version }}" \
--arg rust_base_key "${{ matrix.rust_base_key }}" \
--arg rust_version "${{ matrix.rust_version }}" \
--arg tag "${{ steps.tag.outputs.tag }}" \
'{arch: $arch, digest: $digest, image: $image, rust_version: $rust, stellar_cli_version: $cli, tag: $tag}' \
'{arch: $arch, digest: $digest, image: $image, rust_base_key: $rust_base_key, rust_version: $rust_version, stellar_cli_version: $cli, tag: $tag}' \
> "$out"

- name: rename provenance bundle
if: steps.skip.outputs.skipped != 'true'
run: |
cp "${{ steps.attest-prov.outputs.bundle-path }}" \
"prov-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.intoto.jsonl"
"prov-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}.intoto.jsonl"

# Skipped pairs still need metadata so the release-body composer can
# show the full state of every declared pair, not just the freshly
Expand All @@ -193,21 +192,22 @@ jobs:
existing_digest="$(docker buildx imagetools inspect \
"${{ steps.tag.outputs.image }}" \
--format '{{.Manifest.Digest}}')"
out="meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.json"
out="meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}.json"
jq -n \
--arg arch "${{ matrix.arch }}" \
--arg cli "${{ matrix.stellar_cli_version }}" \
--arg digest "$existing_digest" \
--arg image "${{ steps.tag.outputs.image }}" \
--arg rust "${{ matrix.rust_version }}" \
--arg rust_base_key "${{ matrix.rust_base_key }}" \
--arg rust_version "${{ matrix.rust_version }}" \
--arg tag "${{ steps.tag.outputs.tag }}" \
'{arch: $arch, digest: $digest, image: $image, rust_version: $rust, stellar_cli_version: $cli, tag: $tag}' \
'{arch: $arch, digest: $digest, image: $image, rust_base_key: $rust_base_key, rust_version: $rust_version, stellar_cli_version: $cli, tag: $tag}' \
> "$out"

- name: upload release artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-artifacts-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}
name: release-artifacts-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}
path: |
sbom-*.spdx.json
prov-*.intoto.jsonl
Expand All @@ -233,17 +233,25 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: create manifest list per (cli, rust) pair
- name: create manifest list per (cli, rust base) pair
run: |
while IFS= read -r rust; do
stellar_cli_ref="$(jq -r --arg v "$STELLAR_CLI_VERSION" \
'.stellar_cli_versions[] | select(.version == $v) | .ref' builds.json \
| head -n1)"
test -n "$stellar_cli_ref" \
|| { echo "::error::no stellar_cli_versions entry for $STELLAR_CLI_VERSION"; exit 1; }
while IFS= read -r key; do
list_tag="$(./scripts/tag-names.sh \
--stellar-cli-version "$STELLAR_CLI_VERSION" --rust-version "$rust")"
--stellar-cli-version "$STELLAR_CLI_VERSION" --rust-version "$key" \
--stellar-cli-ref "$stellar_cli_ref")"
amd64_tag="$(./scripts/tag-names.sh \
--stellar-cli-version "$STELLAR_CLI_VERSION" --rust-version "$rust" \
--platform linux/amd64)"
--stellar-cli-version "$STELLAR_CLI_VERSION" --rust-version "$key" \
--platform linux/amd64 \
--stellar-cli-ref "$stellar_cli_ref")"
arm64_tag="$(./scripts/tag-names.sh \
--stellar-cli-version "$STELLAR_CLI_VERSION" --rust-version "$rust" \
--platform linux/arm64)"
--stellar-cli-version "$STELLAR_CLI_VERSION" --rust-version "$key" \
--platform linux/arm64 \
--stellar-cli-ref "$stellar_cli_ref")"
if docker buildx imagetools inspect "$REGISTRY:$list_tag" >/dev/null 2>&1; then
echo "::warning::manifest list $REGISTRY:$list_tag already exists; skipping (lists are immutable)"
{
Expand Down Expand Up @@ -286,9 +294,13 @@ jobs:
default_rust="$(jq -r --arg v "$STELLAR_CLI_VERSION" \
'.stellar_cli_versions[] | select(.version == $v) | .default_rust' \
builds.json | head -n1)"
stellar_cli_ref="$(jq -r --arg v "$STELLAR_CLI_VERSION" \
'.stellar_cli_versions[] | select(.version == $v) | .ref' builds.json \
| head -n1)"
target_tag="$(./scripts/tag-names.sh \
--stellar-cli-version "$STELLAR_CLI_VERSION" \
--rust-version "$default_rust")"
--rust-version "$default_rust" \
--stellar-cli-ref "$stellar_cli_ref")"
target="$REGISTRY:$target_tag"

echo "::group::alias $REGISTRY:$STELLAR_CLI_VERSION -> $target"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
https://github.com/${REPO}/releases/new?tag=${RELEASE_TAG}&title=${RELEASE_TAG#v}&target=${{ github.event.repository.default_branch }}

The publish workflow fires on the release-published event and:
- Builds and pushes per-arch images for any new (cli, rust) pairs; existing pairs are skipped with a warning (per-arch tags are immutable)
- Builds and pushes per-arch images for any new (cli, rust base) pairs; existing pairs are skipped with a warning (per-arch tags are immutable)
- Generates SLSA build provenance + SPDX SBOM attestations on each newly-built image (buildx-native + GitHub-native chains)
- Re-points the \`:${VERSION}\` and (if newest) \`:latest\` aliases
- Attaches the SBOM and provenance files to the new GitHub Release, with per-arch digests in the body
Expand Down
32 changes: 14 additions & 18 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
# Reproducible stellar-cli image. See SEP-58 for the full contract:
# https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0058.md
#
# Every input is pinned. The Debian base via the official Rust image's
# multi-arch index digest, the Rust toolchain by version, and stellar-cli to
# a specific upstream commit SHA. Build args are not optional; the build
# scripts and CI workflows always supply them.
# Every input is pinned. The base image is referenced by its multi-arch
# index digest exclusively — FROM never carries a tag — so a drifting tag
# cannot silently change what we build against. RUST_VERSION and
# RUST_BASE_SUFFIX are surfaced in labels (and RUST_VERSION drives
# RUSTUP_TOOLCHAIN) but are metadata, not load-bearing on FROM. The Rust
# toolchain is pinned by version, and stellar-cli by a specific upstream
# commit SHA. Build args are not optional; the build scripts and CI
# workflows always supply them.

ARG RUST_VERSION
ARG RUST_BASE_SUFFIX
ARG RUST_IMAGE_DIGEST
ARG STELLAR_CLI_REV
ARG STELLAR_CLI_VERSION
ARG BUILD_DATE
ARG BUILDS_JSON_SHA
ARG SOURCE_REPO

FROM rust:${RUST_VERSION}-slim-bookworm@${RUST_IMAGE_DIGEST} AS builder
FROM rust@${RUST_IMAGE_DIGEST} AS builder
ARG STELLAR_CLI_REV
ARG STELLAR_CLI_VERSION
SHELL ["/bin/bash", "-eo", "pipefail", "-c"]
Expand Down Expand Up @@ -46,16 +50,15 @@ RUN installed_version="$(/out/bin/stellar version --only-version)" \
&& test "$installed_rev" = "${STELLAR_CLI_REV}" \
|| { echo "stellar-cli mismatch: binary reports version='$installed_version' rev='$installed_rev', expected version='${STELLAR_CLI_VERSION}' rev='${STELLAR_CLI_REV}'" >&2; exit 1; }

FROM rust:${RUST_VERSION}-slim-bookworm@${RUST_IMAGE_DIGEST}
FROM rust@${RUST_IMAGE_DIGEST}
SHELL ["/bin/bash", "-eo", "pipefail", "-c"]
ARG RUST_VERSION
ARG RUST_BASE_SUFFIX
ARG RUST_IMAGE_DIGEST
ARG STELLAR_CLI_REV
ARG STELLAR_CLI_VERSION
ARG BUILD_DATE
ARG BUILDS_JSON_SHA
ARG SOURCE_REPO
ARG TARGETARCH

# RUSTUP_TOOLCHAIN is baked in so an in-source `rust-toolchain.toml` in a
# consumer's contract can't silently swap our pinned toolchain at build
Expand Down Expand Up @@ -93,12 +96,5 @@ LABEL org.opencontainers.image.title="stellar-cli" \
org.opencontainers.image.version="${STELLAR_CLI_VERSION}" \
org.opencontainers.image.revision="${STELLAR_CLI_REV}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.base.name="docker.io/library/rust:${RUST_VERSION}-slim-bookworm" \
org.opencontainers.image.base.digest="${RUST_IMAGE_DIGEST}" \
org.stellar.rust-version="${RUST_VERSION}" \
org.stellar.rust-image-digest="${RUST_IMAGE_DIGEST}" \
org.stellar.stellar-cli-ref="${STELLAR_CLI_REV}" \
org.stellar.stellar-cli-version="${STELLAR_CLI_VERSION}" \
org.stellar.wasm-target="wasm32v1-none" \
org.stellar.build-arch="${TARGETARCH}" \
org.stellar.builds-json-sha="${BUILDS_JSON_SHA}"
org.opencontainers.image.base.name="docker.io/library/rust:${RUST_VERSION}-${RUST_BASE_SUFFIX}" \
org.opencontainers.image.base.digest="${RUST_IMAGE_DIGEST}"
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ Also compatible as a [SEP-58](https://github.com/stellar/stellar-protocol/blob/m

Each image:

- Pins its Debian base via the official `rust:<version>-slim-bookworm`
multi-arch index digest.
- Pins its base via the official `rust:<version>-<suffix>` multi-arch
index digest. See
[`RELEASE.md` → Base image policy](./RELEASE.md#base-image-policy) for
how the version + suffix are chosen per release.
- Pins the Rust toolchain via `RUSTUP_TOOLCHAIN`, baked in so an in-source
`rust-toolchain.toml` cannot silently swap it.
- Pins `stellar-cli` to a specific upstream commit, installed with
Expand All @@ -32,12 +34,14 @@ docker run --rm -v "$PWD:/source" docker.io/stellar/stellar-cli:latest contract
## Verifiable builds ([SEP-58](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0058.md))

For verifiable references, **always pin to a per-arch single-architecture
digest**, never to a moving tag like `:latest` or to a multi-arch manifest
list digest:
digest**, never to a moving tag like `:latest` or `:<cli>`, and never to a
multi-arch manifest list digest:

```sh
# Find the per-arch digest for the architecture you used to build:
docker buildx imagetools inspect docker.io/stellar/stellar-cli:26.0.0-rust1.94.0
# Find the per-arch digest for the architecture you used to build.
# Pick any of the immutable manifest-list tags from the release notes,
# e.g. :26.0.0-<ref>-rust1.94.0-slim-trixie, or the :26.0.0 alias:
docker buildx imagetools inspect docker.io/stellar/stellar-cli:26.0.0
```

Record the per-arch digest in your contract's `bldimg` metadata. A verifier
Expand All @@ -49,11 +53,11 @@ compare the resulting WASM sha256.
| Path | What |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Dockerfile` | Two-stage builder + runtime, args-driven. |
| `builds.json` | Source of truth for which (stellar-cli, rust) pairs we publish. |
| `builds.json` | Source of truth for which (stellar-cli, rust base key) pairs we publish. |
| `builds.schema.json` | JSON Schema for `builds.json`. |
| `scripts/build-image.sh` | Local single-image build. |
| `scripts/validate-json.sh` | Validates every `*.json` for sorted keys and `builds.json` for schema + cross-field constraints. |
| `scripts/refresh-rust-digests.sh` | Fills blank `rust_image_digests` entries by inspecting `rust:<v>-slim-bookworm` upstream. Does not touch already-pinned digests unless asked per-version. |
| `scripts/refresh-rust-digests.sh` | Fills blank `rust_image_digests` entries by inspecting `rust:<key>` upstream (where `<key>` is the composite `<rust>-<suffix>` form). Does not touch already-pinned digests unless asked per-key. |
| `scripts/refresh-stellar-cli-digests.sh` | Fills blank `stellar_cli_versions[].ref` entries by resolving the matching `v<version>` git tag in `stellar/stellar-cli`. Same per-target opt-in shape as the rust refresher. |
| `scripts/verify-image.sh` | Consumer-facing verifier. Wraps `gh attestation verify` for both the SLSA build provenance and the SPDX SBOM attestations against a per-arch image digest. |
| `scripts/lib/common.sh` | Shared helpers sourced by the other scripts. |
Expand All @@ -64,12 +68,12 @@ compare the resulting WASM sha256.
# Validate builds.json.
./scripts/validate-json.sh

# Build a local image for a declared (cli, rust) pair.
./scripts/build-image.sh --stellar-cli-version 26.0.0 --rust-version 1.94.0
# Build a local image for a declared (cli, rust base) pair.
./scripts/build-image.sh --stellar-cli-version 26.0.0 --rust-version 1.94.0-slim-trixie

# Smoke-test the built image.
docker run --rm stellar-cli:26.0.0-rust1.94.0 --version
docker run --rm stellar-cli:26.0.0-rust1.94.0 contract build --help
docker run --rm stellar-cli:26.0.0-rust1.94.0-slim-trixie --version
docker run --rm stellar-cli:26.0.0-rust1.94.0-slim-trixie contract build --help

# Resolve blank rust base image digests (maintainer task).
./scripts/refresh-rust-digests.sh --dry-run
Expand Down
Loading