Skip to content

Commit 3f59d79

Browse files
committed
Disambiguate release artifacts and tags by base digest.
1 parent 0d14120 commit 3f59d79

8 files changed

Lines changed: 112 additions & 50 deletions

File tree

.github/workflows/publish.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ jobs:
141141
with:
142142
image: ${{ steps.tag.outputs.image }}@${{ steps.build.outputs.digest }}
143143
format: spdx-json
144-
output-file: sbom-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}.spdx.json
144+
output-file: sbom-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_id }}-${{ matrix.arch }}.spdx.json
145145
upload-release-assets: false
146146
upload-artifact: false
147147

@@ -164,14 +164,14 @@ jobs:
164164
with:
165165
subject-name: ${{ env.REGISTRY }}
166166
subject-digest: ${{ steps.build.outputs.digest }}
167-
sbom-path: sbom-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}.spdx.json
167+
sbom-path: sbom-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_id }}-${{ matrix.arch }}.spdx.json
168168
push-to-registry: true
169169

170170
- name: write per-arch metadata
171171
if: steps.skip.outputs.skipped != 'true'
172172
run: |
173173
./scripts/write_metadata.py \
174-
--output "meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}.json" \
174+
--output "meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_id }}-${{ matrix.arch }}.json" \
175175
--arch "${{ matrix.arch }}" \
176176
--stellar-cli-version "${{ matrix.stellar_cli_version }}" \
177177
--digest "${{ steps.build.outputs.digest }}" \
@@ -184,7 +184,7 @@ jobs:
184184
if: steps.skip.outputs.skipped != 'true'
185185
run: |
186186
cp "${{ steps.attest-prov.outputs.bundle-path }}" \
187-
"prov-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}.intoto.jsonl"
187+
"prov-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_id }}-${{ matrix.arch }}.intoto.jsonl"
188188
189189
# Skipped pairs still need metadata so the release-body composer can
190190
# show the full state of every declared pair, not just the freshly
@@ -196,7 +196,7 @@ jobs:
196196
if: steps.skip.outputs.skipped == 'true'
197197
run: |
198198
./scripts/write_metadata.py \
199-
--output "meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}.json" \
199+
--output "meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_id }}-${{ matrix.arch }}.json" \
200200
--arch "${{ matrix.arch }}" \
201201
--stellar-cli-version "${{ matrix.stellar_cli_version }}" \
202202
--image "${{ steps.tag.outputs.image }}" \
@@ -207,7 +207,7 @@ jobs:
207207
- name: upload release artifacts
208208
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
209209
with:
210-
name: release-artifacts-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_key }}-${{ matrix.arch }}
210+
name: release-artifacts-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_base_id }}-${{ matrix.arch }}
211211
path: |
212212
sbom-*.spdx.json
213213
prov-*.intoto.jsonl

scripts/release_body.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,19 @@ def load_metadata(metadata_dir: Path, expected_cli: str) -> list[dict]:
3535
return rows
3636

3737

38-
def rust_keys_newest_first(rows: list[dict]) -> list[str]:
39-
"""Unique rust base keys, ordered by toolchain version descending."""
38+
def list_tag(row: dict) -> str:
39+
"""The multi-arch manifest-list tag for a row: its per-arch tag minus the arch."""
40+
return row["tag"].removesuffix(f"-{row['arch']}")
41+
42+
43+
def pins_newest_first(rows: list[dict]) -> list[str]:
44+
"""Unique multi-arch list tags (one per base pin), ordered by version descending."""
4045
seen: dict[str, tuple] = {}
4146
for row in rows:
42-
if row["rust_base_key"] not in seen:
43-
seen[row["rust_base_key"]] = semver.parse(row["rust_version"])
44-
return sorted(seen.keys(), key=lambda k: (seen[k], k), reverse=True)
47+
tag = list_tag(row)
48+
if tag not in seen:
49+
seen[tag] = semver.parse(row["rust_version"])
50+
return sorted(seen.keys(), key=lambda t: (seen[t], t), reverse=True)
4551

4652

4753
def emit_body(*, cli: str, rows: list[dict], registry: str, repo: str, stellar_ref: str) -> str:
@@ -57,36 +63,38 @@ def emit_body(*, cli: str, rows: list[dict], registry: str, repo: str, stellar_r
5763
p(f"- `{registry}:{cli}` — this cli, default Rust")
5864
p()
5965
p(f"Immutable, pinned to stellar-cli `{stellar_ref}`:\n")
60-
for key in rust_keys_newest_first(rows):
61-
p(f"- `{registry}:{cli}-{stellar_ref}-rust{key}` — multi-arch")
62-
p(f"- `{registry}:{cli}-{stellar_ref}-rust{key}-amd64`")
63-
p(f"- `{registry}:{cli}-{stellar_ref}-rust{key}-arm64`")
66+
for tag in pins_newest_first(rows):
67+
p(f"- `{registry}:{tag}` — multi-arch")
68+
for row in [r for r in rows if list_tag(r) == tag]:
69+
p(f"- `{registry}:{row['tag']}`")
6470

6571
p("\n## Per-architecture digests (for SEP-58 `bldimg`)\n")
6672
p(
6773
f"Use the per-architecture digest when recording `bldimg` in your contract "
6874
f"metadata. Never use a moving tag like `:latest` or `:{cli}`.\n"
6975
)
7076

71-
for key in rust_keys_newest_first(rows):
72-
p(f"### Rust {key}\n")
73-
key_rows = [r for r in rows if r["rust_base_key"] == key]
74-
for row in key_rows:
77+
for tag in pins_newest_first(rows):
78+
pin_rows = [r for r in rows if list_tag(r) == tag]
79+
key = pin_rows[0]["rust_base_key"]
80+
base_digest = tag.rsplit("-", 1)[1]
81+
p(f"### Rust {key} (base {base_digest})\n")
82+
for row in pin_rows:
7583
p(f"- `linux/{row['arch']}`: `{registry}@{row['digest']}`")
7684
p("\nVerify:\n")
7785
p("```sh")
78-
for row in key_rows:
86+
for row in pin_rows:
7987
p(f"gh attestation verify oci://{registry}@{row['digest']} --repo {repo}")
8088
p()
81-
for row in key_rows:
89+
for row in pin_rows:
8290
p("cosign verify-attestation \\")
8391
p(" --type slsaprovenance1 \\")
8492
identity_re = f"https://github.com/{repo}/\\.github/workflows/.*"
8593
p(f' --certificate-identity-regexp "{identity_re}" \\')
8694
p(" --certificate-oidc-issuer https://token.actions.githubusercontent.com \\")
8795
p(f" {registry}@{row['digest']}")
8896
p()
89-
for row in key_rows:
97+
for row in pin_rows:
9098
p(f"docker buildx imagetools inspect {registry}@{row['digest']}")
9199
p("```\n")
92100

scripts/resolve_matrix.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import json
1111
import sys
1212

13+
import tag_names
1314
from lib import builds, common, rust_keys
1415

1516
ARCHES = ("amd64", "arm64")
@@ -28,11 +29,13 @@ def build_matrix(data: dict, only_cli: str = "") -> dict:
2829
for pin in entry["rust_versions"]:
2930
label, digest = builds.split_entry(pin)
3031
parsed = rust_keys.parse(label)
32+
rust_base_id = f"{label}-{tag_names.short_digest(digest)}"
3133
for arch in ARCHES:
3234
rows.append(
3335
{
3436
"arch": arch,
3537
"platform": f"linux/{arch}",
38+
"rust_base_id": rust_base_id,
3639
"rust_base_key": label,
3740
"rust_base_suffix": parsed.suffix,
3841
"rust_image_digest": digest,

scripts/tag_names.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def _short_ref(ref: str) -> str:
3232
return ref[:_SHORT]
3333

3434

35-
def _short_digest(digest: str) -> str:
35+
def short_digest(digest: str) -> str:
36+
"""First 15 hex chars of an image digest, with any `sha256:` prefix stripped."""
3637
return digest.removeprefix("sha256:")[:_SHORT]
3738

3839

@@ -48,7 +49,7 @@ def compose_tag(
4849
if stellar_cli_ref:
4950
tag = f"{tag}-{_short_ref(stellar_cli_ref)}"
5051
tag = f"{tag}-rust{rust_version}"
51-
tag = f"{tag}-{_short_digest(rust_image_digest)}"
52+
tag = f"{tag}-{short_digest(rust_image_digest)}"
5253
if platform:
5354
arch = _ARCH_FOR_PLATFORM.get(platform)
5455
if arch is None:

tests/fixtures/meta_amd64.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"arch": "amd64",
33
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
4-
"image": "docker.io/stellar/stellar-cli:26.0.0-abc123-rust1.94.0-slim-trixie-amd64",
4+
"image": "docker.io/stellar/stellar-cli:26.0.0-abc123-rust1.94.0-slim-trixie-f7bf1c266d9e48c-amd64",
55
"rust_base_key": "1.94.0-slim-trixie",
66
"rust_version": "1.94.0",
77
"stellar_cli_version": "26.0.0",
8-
"tag": "26.0.0-abc123-rust1.94.0-slim-trixie-amd64"
8+
"tag": "26.0.0-abc123-rust1.94.0-slim-trixie-f7bf1c266d9e48c-amd64"
99
}

tests/fixtures/meta_arm64.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"arch": "arm64",
33
"digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
4-
"image": "docker.io/stellar/stellar-cli:26.0.0-abc123-rust1.94.0-slim-trixie-arm64",
4+
"image": "docker.io/stellar/stellar-cli:26.0.0-abc123-rust1.94.0-slim-trixie-f7bf1c266d9e48c-arm64",
55
"rust_base_key": "1.94.0-slim-trixie",
66
"rust_version": "1.94.0",
77
"stellar_cli_version": "26.0.0",
8-
"tag": "26.0.0-abc123-rust1.94.0-slim-trixie-arm64"
8+
"tag": "26.0.0-abc123-rust1.94.0-slim-trixie-f7bf1c266d9e48c-arm64"
99
}

tests/unit/test_release_body.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -68,44 +68,46 @@ def test_load_metadata_sorts_by_rust_version_then_key_then_arch(tmp_path: Path)
6868
]
6969

7070

71-
def test_rust_keys_newest_first_orders_by_version_desc() -> None:
71+
def _pin_rows(label: str, version: str, digest15: str) -> list[dict]:
72+
base = f"26.0.0-abc123-rust{label}-{digest15}"
73+
return [
74+
{
75+
"arch": arch,
76+
"digest": "sha256:" + char * 64,
77+
"rust_base_key": label,
78+
"rust_version": version,
79+
"tag": f"{base}-{arch}",
80+
}
81+
for arch, char in (("amd64", "1"), ("arm64", "2"))
82+
]
83+
84+
85+
def test_pins_newest_first_orders_by_version_desc() -> None:
7286
rows = [
73-
{"rust_base_key": "1.94.0-slim-trixie", "rust_version": "1.94.0"},
74-
{"rust_base_key": "1.100.0-slim-trixie", "rust_version": "1.100.0"},
75-
{"rust_base_key": "1.94.0-slim-trixie", "rust_version": "1.94.0"}, # dup
87+
*_pin_rows("1.94.0-slim-trixie", "1.94.0", "a" * 15),
88+
*_pin_rows("1.100.0-slim-trixie", "1.100.0", "b" * 15),
7689
]
77-
assert release_body.rust_keys_newest_first(rows) == [
78-
"1.100.0-slim-trixie",
79-
"1.94.0-slim-trixie",
90+
assert release_body.pins_newest_first(rows) == [
91+
"26.0.0-abc123-rust1.100.0-slim-trixie-" + "b" * 15,
92+
"26.0.0-abc123-rust1.94.0-slim-trixie-" + "a" * 15,
8093
]
8194

8295

8396
def test_emit_body_includes_expected_sections() -> None:
84-
rows = [
85-
{
86-
"arch": "amd64",
87-
"digest": "sha256:" + "1" * 64,
88-
"rust_base_key": "1.94.0-slim-trixie",
89-
"rust_version": "1.94.0",
90-
},
91-
{
92-
"arch": "arm64",
93-
"digest": "sha256:" + "2" * 64,
94-
"rust_base_key": "1.94.0-slim-trixie",
95-
"rust_version": "1.94.0",
96-
},
97-
]
97+
rows = _pin_rows("1.94.0-slim-trixie", "1.94.0", "f7bf1c266d9e48c")
9898
body = release_body.emit_body(
9999
cli="26.0.0",
100100
rows=rows,
101101
registry="docker.io/stellar/stellar-cli",
102102
repo="stellar/stellar-cli-docker",
103103
stellar_ref="abc123",
104104
)
105+
list_tag = "26.0.0-abc123-rust1.94.0-slim-trixie-f7bf1c266d9e48c"
105106
assert "# stellar-cli 26.0.0" in body
106107
assert "## Tags" in body
107108
assert "docker.io/stellar/stellar-cli:latest" in body
108-
assert "26.0.0-abc123-rust1.94.0-slim-trixie" in body
109+
assert f"docker.io/stellar/stellar-cli:{list_tag}" in body
110+
assert f"{list_tag}-amd64" in body
109111
assert "## Per-architecture digests" in body
110112
assert "### Rust 1.94.0-slim-trixie" in body
111113
assert "linux/amd64" in body
@@ -115,6 +117,24 @@ def test_emit_body_includes_expected_sections() -> None:
115117
assert "docker buildx imagetools inspect" in body
116118
assert "## Verification" in body
117119
assert "## Assets" in body
120+
121+
122+
def test_emit_body_two_digests_same_label_render_distinctly() -> None:
123+
rows = [
124+
*_pin_rows("1.94.0-slim-trixie", "1.94.0", "a" * 15),
125+
*_pin_rows("1.94.0-slim-trixie", "1.94.0", "c" * 15),
126+
]
127+
body = release_body.emit_body(
128+
cli="26.0.0",
129+
rows=rows,
130+
registry="docker.io/stellar/stellar-cli",
131+
repo="stellar/stellar-cli-docker",
132+
stellar_ref="abc123",
133+
)
134+
# Both pins surface as their own multi-arch tag and their own section.
135+
assert "26.0.0-abc123-rust1.94.0-slim-trixie-" + "a" * 15 in body
136+
assert "26.0.0-abc123-rust1.94.0-slim-trixie-" + "c" * 15 in body
137+
assert body.count("### Rust 1.94.0-slim-trixie") == 2
118138
# Each shell-continuation line in the cosign block must end with a single
119139
# backslash, not two — `\\` in the rendered markdown would land as a
120140
# literal `\\` in the user's terminal instead of a line continuation.

tests/unit/test_resolve_matrix.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_build_matrix_row_carries_expected_keys(minimal_builds: dict) -> None:
2929
assert set(row.keys()) == {
3030
"arch",
3131
"platform",
32+
"rust_base_id",
3233
"rust_base_key",
3334
"rust_base_suffix",
3435
"rust_image_digest",
@@ -38,6 +39,35 @@ def test_build_matrix_row_carries_expected_keys(minimal_builds: dict) -> None:
3839
}
3940

4041

42+
def test_build_matrix_row_id_carries_digest_fragment(minimal_builds: dict) -> None:
43+
matrix = resolve_matrix.build_matrix(minimal_builds)
44+
row = matrix["include"][0]
45+
# rust_base_id disambiguates two pins that share a label but differ by digest,
46+
# so downstream artifact/file names never collide.
47+
assert row["rust_base_id"] == "1.94.0-slim-trixie-f7bf1c266d9e48c"
48+
49+
50+
def test_build_matrix_same_label_two_digests_distinct_ids() -> None:
51+
data = {
52+
"default_distro": "trixie",
53+
"stellar_cli_versions": [
54+
{
55+
"ref": "a" * 40,
56+
"version": "26.0.0",
57+
"rust_versions": [
58+
"1.94.0-slim-trixie@sha256:" + "a" * 64,
59+
"1.94.0-slim-trixie@sha256:" + "b" * 64,
60+
],
61+
}
62+
],
63+
}
64+
ids = {row["rust_base_id"] for row in resolve_matrix.build_matrix(data)["include"]}
65+
assert ids == {
66+
"1.94.0-slim-trixie-" + "a" * 15,
67+
"1.94.0-slim-trixie-" + "b" * 15,
68+
}
69+
70+
4171
def test_build_matrix_parses_rust_key(minimal_builds: dict) -> None:
4272
matrix = resolve_matrix.build_matrix(minimal_builds)
4373
row = matrix["include"][0]

0 commit comments

Comments
 (0)