Skip to content
Draft
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
93 changes: 92 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ on:
- v*

permissions:
contents: read
contents: write # release job creates/updates the GitHub Release
attestations: write # actions/attest-* publish to the repo's attestation store
id-token: write # OIDC token used by buildx provenance and the attest actions

env:
REGISTRY: docker.io/stellar/stellar-cli
Expand Down Expand Up @@ -96,12 +98,15 @@ jobs:
} >> "$GITHUB_OUTPUT"

- name: build and push
id: build
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: true
platforms: ${{ matrix.platform }}
tags: ${{ steps.tag.outputs.image }}
provenance: mode=max
sbom: true
build-args: |
RUST_VERSION=${{ matrix.rust_version }}
RUST_IMAGE_DIGEST=${{ matrix.rust_image_digest }}
Expand All @@ -110,6 +115,61 @@ jobs:
BUILD_DATE=${{ steps.meta.outputs.build_date }}
BUILDS_JSON_SHA=${{ steps.meta.outputs.builds_json_sha }}

- name: generate SBOM file
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
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
upload-release-assets: false
upload-artifact: false

- name: attest build provenance
id: attest-prov
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-name: ${{ env.REGISTRY }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: false

- name: attest SBOM
id: attest-sbom
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
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
push-to-registry: false

- name: write per-arch metadata
run: |
out="meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ 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 tag "${{ steps.tag.outputs.tag }}" \
'{arch: $arch, digest: $digest, image: $image, rust_version: $rust, stellar_cli_version: $cli, tag: $tag}' \
> "$out"

- name: rename provenance bundle
run: |
cp "${{ steps.attest-prov.outputs.bundle-path }}" \
"prov-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.intoto.jsonl"

- 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 }}
path: |
sbom-*.spdx.json
prov-*.intoto.jsonl
meta-*.json
retention-days: 7
if-no-files-found: error

manifest:
name: assemble manifest lists
needs: [matrix, build]
Expand Down Expand Up @@ -192,13 +252,44 @@ jobs:
echo "cli $STELLAR_CLI_VERSION is not the newest ($newest_cli); skipping :latest"
fi

release:
name: github release with sbom and provenance
if: startsWith(github.ref, 'refs/tags/v')
needs: [matrix, aliases]
runs-on: ubuntu-24.04
env:
STELLAR_CLI_VERSION: ${{ needs.matrix.outputs.stellar_cli_version }}
steps:
- name: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: download per-arch release artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: release-artifacts
pattern: release-artifacts-*
merge-multiple: true
- name: compose release body
run: |
./scripts/release-body.sh \
--stellar-cli-version "$STELLAR_CLI_VERSION" \
--metadata-dir release-artifacts \
> release-body.md
- name: create / update the github release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
body_path: release-body.md
files: |
release-artifacts/sbom-*.spdx.json
release-artifacts/prov-*.intoto.jsonl

complete:
if: always()
needs:
- matrix
- build
- manifest
- aliases
- release
runs-on: ubuntu-24.04
steps:
- name: check upstream jobs
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ compare the resulting WASM sha256.
| `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-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. |

## Local development
Expand Down
8 changes: 4 additions & 4 deletions scripts/build-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ main() {

while [ $# -gt 0 ]; do
case "$1" in
--stellar-cli-version) cli="$2"; shift 2;;
--rust-version) rust="$2"; shift 2;;
--platform) platform="$2"; shift 2;;
--tag) tag="$2"; shift 2;;
--stellar-cli-version) require_value "$1" "${2:-}"; cli="$2"; shift 2;;
--rust-version) require_value "$1" "${2:-}"; rust="$2"; shift 2;;
--platform) require_value "$1" "${2:-}"; platform="$2"; shift 2;;
--tag) require_value "$1" "${2:-}"; tag="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) err "unknown argument: $1"; usage; exit 1;;
esac
Expand Down
10 changes: 10 additions & 0 deletions scripts/lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ require_cmd() {
done
}

# require_value <flag> <value>
# Aborts with a clear error if <value> is empty. Use at the top of each
# --flag case arm: require_value "$1" "${2:-}"
# Prevents the unhelpful "$2: unbound variable" crash that `set -u`
# emits when a user passes a flag with no value (e.g. `--image` at EOL).
require_value() {
local flag="$1" value="${2:-}"
test -n "$value" || die "missing value for $flag"
}

# Minimum bash version this project's scripts rely on. Bump in one place.
# 4.3 is what `local -n` (used by the apply_updates helpers) requires;
# 4.0 covers `declare -A`. macOS ships bash 3.2 by default.
Expand Down
2 changes: 1 addition & 1 deletion scripts/refresh-rust-digests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ main() {

while [ $# -gt 0 ]; do
case "$1" in
--rust-version) only_version="$2"; shift 2;;
--rust-version) require_value "$1" "${2:-}"; only_version="$2"; shift 2;;
--dry-run) dry_run=1; shift;;
-h|--help) usage; exit 0;;
*) err "unknown argument: $1"; usage; exit 1;;
Expand Down
2 changes: 1 addition & 1 deletion scripts/refresh-stellar-cli-digests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ main() {

while [ $# -gt 0 ]; do
case "$1" in
--stellar-cli-version) only_version="$2"; shift 2;;
--stellar-cli-version) require_value "$1" "${2:-}"; only_version="$2"; shift 2;;
--dry-run) dry_run=1; shift;;
-h|--help) usage; exit 0;;
*) err "unknown argument: $1"; usage; exit 1;;
Expand Down
136 changes: 136 additions & 0 deletions scripts/release-body.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# Compose the markdown body for a GitHub Release, given a directory of
# per-arch metadata files (meta-<cli>-rust<rust>-<arch>.json) written by
# the publish workflow's build job.
#
# Each metadata file has the shape:
# {"arch": "...", "digest": "sha256:...", "image": "...", "rust_version": "...",
# "stellar_cli_version": "...", "tag": "..."}
#
# Output goes to stdout.

set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/common.sh
source "$script_dir/lib/common.sh"

usage() {
cat <<'EOF'
Usage: scripts/release-body.sh --stellar-cli-version <v> --metadata-dir <path> [--help]

Required:
--stellar-cli-version <v> The release this body is for (e.g. 26.0.0).
Must match the cli in every metadata file.
--metadata-dir <path> Directory containing meta-*.json files.

Options:
--help Show this message.

Prints the release body markdown to stdout.
EOF
}

main() {
local cli="" metadata_dir=""

while [ $# -gt 0 ]; do
case "$1" in
--stellar-cli-version) require_value "$1" "${2:-}"; cli="$2"; shift 2;;
--metadata-dir) require_value "$1" "${2:-}"; metadata_dir="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) err "unknown argument: $1"; usage; exit 1;;
esac
done

test -n "$cli" || { err "--stellar-cli-version is required"; usage; exit 1; }
test -n "$metadata_dir" || { err "--metadata-dir is required"; usage; exit 1; }
test -d "$metadata_dir" || die "$metadata_dir is not a directory"

preflight_checks jq

# Aggregate all meta-*.json files under the metadata dir, validating
# each one individually before merging. A mismatched or missing
# stellar_cli_version is a hard error — silently dropping would let a
# misconfigured run produce a release body with arches omitted.
local -a meta_files=()
while IFS= read -r -d '' f; do
meta_files+=("$f")
done < <(find "$metadata_dir" -type f -name 'meta-*.json' -print0)
test "${#meta_files[@]}" -gt 0 \
|| die "no meta-*.json files under $metadata_dir"

local f entry_cli
for f in "${meta_files[@]}"; do
entry_cli="$(jq -r '.stellar_cli_version // empty' "$f")"
test -n "$entry_cli" \
|| die "metadata file $f is missing the stellar_cli_version field"
test "$entry_cli" = "$cli" \
|| die "metadata file $f has stellar_cli_version='$entry_cli', expected '$cli'"
done

local rows
rows="$(jq -s 'sort_by(.rust_version, .arch)' "${meta_files[@]}")"

emit_body "$cli" "$rows"
}

emit_body() {
local cli="$1" rows="$2"

printf '# stellar-cli %s\n\n' "$cli"

printf 'Trusted, SEP-58-compatible build images for the Stellar CLI.\n\n'

printf '## Convenience tags\n\n'
printf -- '- `docker.io/stellar/stellar-cli:%s` — multi-arch, default Rust for this release\n' "$cli"
local rust
while IFS= read -r rust; do
printf -- '- `docker.io/stellar/stellar-cli:%s-rust%s` — multi-arch\n' "$cli" "$rust"
done < <(jq -r '. | map(.rust_version) | unique | .[]' <<<"$rows")

printf '\n## Per-architecture digests (for SEP-58 `bldimg`)\n\n'
printf 'Use the per-architecture digest when recording `bldimg` in your contract metadata. Never use a moving tag like `:latest` or `:%s`.\n\n' "$cli"

while IFS= read -r rust; do
printf '### Rust %s\n\n' "$rust"
while IFS= read -r row; do
printf -- '- `%s@%s`\n' \
"$(jq -r '.image' <<<"$row")" \
"$(jq -r '.digest' <<<"$row")"
done < <(jq -c --arg r "$rust" 'map(select(.rust_version == $r)) | .[]' <<<"$rows")
printf '\n'
done < <(jq -r '. | map(.rust_version) | unique | .[]' <<<"$rows")

cat <<'EOF'
## Verification

Each per-architecture image carries two independent attestation chains.

### GitHub-native (recommended)

```sh
gh attestation verify oci://<image>@<digest> \
--repo stellar/stellar-cli-docker
```

The repo includes `scripts/verify-image.sh` that wraps this for both provenance and SBOM:

```sh
./scripts/verify-image.sh --image <image>@<digest>
```

### Registry-attached (cosign / docker buildx)

```sh
cosign verify-attestation --type slsaprovenance <image>@<digest>
docker buildx imagetools inspect <image>@<digest>
```

## Assets

This release attaches one SBOM file (`.spdx.json`) and one provenance bundle (`.intoto.jsonl`) per per-architecture image.
EOF
}

main "$@"
2 changes: 1 addition & 1 deletion scripts/resolve-matrix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ main() {

while [ $# -gt 0 ]; do
case "$1" in
--stellar-cli-version) only_cli="$2"; shift 2;;
--stellar-cli-version) require_value "$1" "${2:-}"; only_cli="$2"; shift 2;;
--compact) mode="compact"; shift;;
--pretty) mode="pretty"; shift;;
-h|--help) usage; exit 0;;
Expand Down
6 changes: 3 additions & 3 deletions scripts/smoke-test-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ main() {

while [ $# -gt 0 ]; do
case "$1" in
--image) image="$2"; shift 2;;
--stellar-cli-version) cli="$2"; shift 2;;
--rust-version) rust="$2"; shift 2;;
--image) require_value "$1" "${2:-}"; image="$2"; shift 2;;
--stellar-cli-version) require_value "$1" "${2:-}"; cli="$2"; shift 2;;
--rust-version) require_value "$1" "${2:-}"; rust="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) err "unknown argument: $1"; usage; exit 1;;
esac
Expand Down
6 changes: 3 additions & 3 deletions scripts/tag-names.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ main() {

while [ $# -gt 0 ]; do
case "$1" in
--stellar-cli-version) cli="$2"; shift 2;;
--rust-version) rust="$2"; shift 2;;
--platform) platform="$2"; shift 2;;
--stellar-cli-version) require_value "$1" "${2:-}"; cli="$2"; shift 2;;
--rust-version) require_value "$1" "${2:-}"; rust="$2"; shift 2;;
--platform) require_value "$1" "${2:-}"; platform="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) err "unknown argument: $1"; usage; exit 1;;
esac
Expand Down
Loading