Skip to content
Merged
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
2 changes: 0 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,12 @@ jobs:
./scripts/build_image.py \
--stellar-cli-version "${{ steps.pair.outputs.cli }}" \
--rust-version "${{ steps.pair.outputs.rust }}" \
--rust-image-digest "${{ steps.pair.outputs.digest }}"
- name: smoke test
run: |
./scripts/smoke_test_image.py \
--image "${{ steps.pair.outputs.image }}" \
--stellar-cli-version "${{ steps.pair.outputs.cli }}" \
--rust-version "${{ steps.pair.outputs.rust }}" \
--rust-image-digest "${{ steps.pair.outputs.digest }}"
- name: wasm reproducibility
run: |
./scripts/repro_test.py \
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,12 @@ compare the resulting WASM sha256.
./scripts/validate_json.py

# Build a local image for a declared (cli, rust base) pair. The rust base is
# given as the label plus its pinned digest (copy the pin from builds.json).
# given as the label; the digest is resolved from builds.json automatically.
./scripts/build_image.py --stellar-cli-version 26.0.0 \
--rust-version 1.94.0-slim-trixie \
--rust-image-digest sha256:f7bf1c266d9e48c8d724733fd97ba60464c44b743eb4f46f935577d3242d81d0
--rust-version 1.94.0-slim-trixie

# Only when a label carries more than one digest in builds.json do you need
# --rust-image-digest to say which pin to build.

# Smoke-test the built image.
docker run --rm stellar-cli:26.0.0-rust1.94.0-slim-trixie --version
Expand Down
8 changes: 4 additions & 4 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ The script prints the chosen release tag as its final stdout line. Commit and pu

```sh
./scripts/validate_json.py
# --rust-version is the label; --rust-image-digest is the pin's digest from
# builds.json (it pins the FROM and is cross-checked against the image's
# --rust-version is the label; build_image.py resolves its pinned digest from
# builds.json (the digest pins the FROM and is cross-checked against the image's
# org.opencontainers.image.base.digest label). The built tag is label-only.
./scripts/build_image.py --stellar-cli-version 26.1.0 --rust-version 1.95.0-slim-trixie \
--rust-image-digest sha256:e14e87345b4d5964ddcc3491d27ee046a0f23820f340c3c1e24da6880141f7c0
# Pass --rust-image-digest only when one label carries multiple digests.
./scripts/build_image.py --stellar-cli-version 26.1.0 --rust-version 1.95.0-slim-trixie
./scripts/smoke_test_image.py --image stellar-cli:26.1.0-rust1.95.0-slim-trixie \
--stellar-cli-version 26.1.0 --rust-version 1.95.0-slim-trixie \
--rust-image-digest sha256:e14e87345b4d5964ddcc3491d27ee046a0f23820f340c3c1e24da6880141f7c0
Expand Down
7 changes: 3 additions & 4 deletions scripts/build_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--stellar-cli-version", required=True, metavar="V")
parser.add_argument("--rust-version", required=True, metavar="KEY")
parser.add_argument("--rust-image-digest", required=True, metavar="DIGEST")
parser.add_argument("--rust-image-digest", default=None, metavar="DIGEST")
parser.add_argument("--platform", default="", metavar="P")
parser.add_argument("--tag", default="", metavar="REF")
parser.add_argument("--source-repo", default="stellar/stellar-cli-docker", metavar="SLUG")
Expand All @@ -29,10 +29,9 @@ def main(argv: list[str] | None = None) -> int:
common.preflight_checks(["buildx"])

data = builds.load()
rust_digest = args.rust_image_digest
try:
builds.assert_pair_declared(
data, args.stellar_cli_version, f"{args.rust_version}@{rust_digest}"
rust_digest = builds.resolve_rust_digest(
data, args.stellar_cli_version, args.rust_version, args.rust_image_digest
)
stellar_ref = builds.stellar_cli_ref(data, args.stellar_cli_version)
parsed = rust_keys.parse(args.rust_version)
Expand Down
29 changes: 29 additions & 0 deletions scripts/lib/builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,35 @@ def assert_pair_declared(data: dict[str, Any], cli: str, rust_pin: str) -> None:
)


def resolve_rust_digest(
data: dict[str, Any], cli: str, label: str, digest: str | None = None
) -> str:
"""Resolve the pinned base digest for a (cli, rust label) pair.

When `digest` is given, assert the exact `<label>@<digest>` pin is declared
and return it. When `digest` is omitted, return the sole digest declared for
the label; raise if the label is undeclared, or if it carries more than one
digest (the caller must then disambiguate with an explicit digest).
"""
if digest is not None:
assert_pair_declared(data, cli, f"{label}@{digest}")
return digest

entry = find_cli(data, cli)
if entry is None:
raise ValueError(f"unknown stellar-cli version: {cli}")
pins = [pin for pin in entry.get("rust_versions", []) if label_of(pin) == label]
if not pins:
raise ValueError(f"stellar-cli {cli} is not declared with rust base {label} in builds.json")
if len(pins) > 1:
choices = ", ".join(digest_of(pin) for pin in pins)
raise ValueError(
f"stellar-cli {cli} declares multiple digests for rust base {label}; "
f"pass --rust-image-digest to select one of: {choices}"
)
return digest_of(pins[0])


def derive_default_rust(data: dict[str, Any], cli: str) -> str:
distro = data.get("default_distro")
if not distro:
Expand Down
59 changes: 59 additions & 0 deletions tests/unit/test_build_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,65 @@ def test_main_respects_custom_tag(monkeypatch: pytest.MonkeyPatch, minimal_build
assert "my-local:test" in args


def test_main_infers_digest_when_unambiguous(
monkeypatch: pytest.MonkeyPatch, minimal_builds: dict
) -> None:
# minimal_builds declares 26.0.0 with a single 1.94.0-slim-trixie pin, so the
# digest can be resolved without --rust-image-digest.
monkeypatch.setattr(build_image.builds, "load", lambda: minimal_builds)
monkeypatch.setattr(build_image.common, "preflight_checks", lambda _: None)
captured = MagicMock(return_value=_completed())
monkeypatch.setattr(build_image.runner, "run", captured)

rc = build_image.main(
[
"--stellar-cli-version",
"26.0.0",
"--rust-version",
"1.94.0-slim-trixie",
]
)

# Expect the sole digest the fixture declares for this (cli, label), so the
# assertion can't drift from the digest build_image actually infers.
expected_digest = minimal_builds["stellar_cli_versions"][0]["rust_versions"][0].split("@", 1)[1]
assert rc == 0
args = captured.call_args[0][0]
assert f"RUST_IMAGE_DIGEST={expected_digest}" in args


def test_main_dies_when_digest_omitted_and_ambiguous(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Two digests share the 1.94.0-slim-trixie label → the digest must be given.
ambiguous = {
"default_distro": "trixie",
"stellar_cli_versions": [
{
"ref": "a" * 40,
"version": "26.0.0",
"rust_versions": [
"1.94.0-slim-trixie@sha256:" + "a" * 64,
"1.94.0-slim-trixie@sha256:" + "b" * 64,
],
}
],
}
monkeypatch.setattr(build_image.builds, "load", lambda: ambiguous)
monkeypatch.setattr(build_image.common, "preflight_checks", lambda _: None)
monkeypatch.setattr(build_image.runner, "run", lambda *_, **__: _completed())

with pytest.raises(SystemExit):
build_image.main(
[
"--stellar-cli-version",
"26.0.0",
"--rust-version",
"1.94.0-slim-trixie",
]
)


def test_main_dies_for_undeclared_pair(
monkeypatch: pytest.MonkeyPatch, minimal_builds: dict
) -> None:
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/test_builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,49 @@ def test_assert_pair_declared_rejects_undeclared_pair(multi_cli_builds: dict) ->
builds.assert_pair_declared(multi_cli_builds, "25.1.0", _PIN_94)


def test_resolve_rust_digest_returns_sole_digest(minimal_builds: dict) -> None:
# One pin for the label → digest is unambiguous, no explicit digest needed.
assert builds.resolve_rust_digest(minimal_builds, "26.0.0", "1.94.0-slim-trixie") == _DIGEST_94


def test_resolve_rust_digest_honors_explicit_digest(multi_cli_builds: dict) -> None:
assert (
builds.resolve_rust_digest(multi_cli_builds, "26.0.0", "1.94.0-slim-trixie", _DIGEST_94)
== _DIGEST_94
)


def test_resolve_rust_digest_rejects_explicit_undeclared_pin(multi_cli_builds: dict) -> None:
with pytest.raises(ValueError, match="not declared"):
builds.resolve_rust_digest(
multi_cli_builds, "26.0.0", "1.94.0-slim-trixie", "sha256:" + "0" * 64
)


def test_resolve_rust_digest_rejects_undeclared_label(minimal_builds: dict) -> None:
with pytest.raises(ValueError, match="not declared"):
builds.resolve_rust_digest(minimal_builds, "26.0.0", "1.99.0-slim-trixie")


def test_resolve_rust_digest_requires_digest_when_ambiguous() -> None:
# Same label carrying two digests → caller must disambiguate.
data = {
"default_distro": "trixie",
"stellar_cli_versions": [
{
"ref": "a" * 40,
"version": "26.0.0",
"rust_versions": [
"1.94.0-slim-trixie@sha256:" + "a" * 64,
"1.94.0-slim-trixie@sha256:" + "b" * 64,
],
}
],
}
with pytest.raises(ValueError, match="multiple digests"):
builds.resolve_rust_digest(data, "26.0.0", "1.94.0-slim-trixie")


def test_derive_default_rust_picks_highest_matching_suffix(multi_cli_builds: dict) -> None:
assert builds.derive_default_rust(multi_cli_builds, "26.0.0") == _PIN_94

Expand Down
Loading