Skip to content

Commit d8ed15f

Browse files
authored
Add the trusted publish system (#3)
1 parent eb07b2c commit d8ed15f

23 files changed

Lines changed: 1573 additions & 107 deletions

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ jobs:
3535
--image "stellar-cli:${{ steps.pair.outputs.cli }}-rust${{ steps.pair.outputs.rust }}" \
3636
--stellar-cli-version "${{ steps.pair.outputs.cli }}" \
3737
--rust-version "${{ steps.pair.outputs.rust }}"
38+
- name: wasm reproducibility
39+
run: |
40+
./scripts/repro-test.sh \
41+
--image "stellar-cli:${{ steps.pair.outputs.cli }}-rust${{ steps.pair.outputs.rust }}"
3842
3943
complete:
4044
if: always()

.github/workflows/lint.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ jobs:
4646
scandir: scripts
4747
severity: style
4848

49+
shell:
50+
name: validate shell
51+
runs-on: ubuntu-24.04
52+
steps:
53+
- name: checkout
54+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
55+
- name: validate shell scripts
56+
run: ./scripts/validate-shell.sh
57+
4958
matrix-smoke:
5059
name: resolve-matrix smoke
5160
runs-on: ubuntu-24.04
@@ -61,6 +70,7 @@ jobs:
6170
- json
6271
- dockerfile
6372
- shellcheck
73+
- shell
6474
- matrix-smoke
6575
runs-on: ubuntu-24.04
6676
steps:

.github/workflows/publish.yml

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
---
2+
name: publish
3+
4+
on:
5+
release:
6+
types:
7+
- published
8+
9+
permissions:
10+
contents: write # release job creates/updates the GitHub Release
11+
attestations: write # actions/attest-build-provenance + actions/attest publish to the repo's attestation store
12+
id-token: write # OIDC token used by buildx provenance and the attest actions
13+
artifact-metadata: write # actions/attest creates an artifact storage record
14+
15+
env:
16+
# Override per-repo via Settings → Variables → Actions (vars.REGISTRY).
17+
# Useful for forks that want to publish to a personal registry for
18+
# testing without forking the workflow itself.
19+
REGISTRY: ${{ vars.REGISTRY || 'docker.io/stellar/stellar-cli' }}
20+
21+
jobs:
22+
matrix:
23+
name: resolve matrix
24+
runs-on: ubuntu-24.04
25+
outputs:
26+
matrix: ${{ steps.resolve.outputs.matrix }}
27+
stellar_cli_version: ${{ steps.scope.outputs.version }}
28+
steps:
29+
- name: checkout
30+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
31+
- name: install check-jsonschema
32+
run: pipx install check-jsonschema
33+
- name: validate builds.json
34+
run: ./scripts/validate-json.sh
35+
- name: scope to one cli version
36+
id: scope
37+
env:
38+
RELEASE_TAG: ${{ github.event.release.tag_name }}
39+
run: |
40+
# Tag is "v<version>" or "v<version>-<N>" (refresh iterations).
41+
# Strip leading "v" and trailing "-<N>" if present to derive
42+
# the stellar-cli version.
43+
no_prefix="${RELEASE_TAG#v}"
44+
version="${no_prefix%%-*}"
45+
test -n "$version" || { echo "::error::could not determine stellar_cli_version from release tag '$RELEASE_TAG'"; exit 1; }
46+
echo "version=$version" >> "$GITHUB_OUTPUT"
47+
- name: resolve matrix
48+
id: resolve
49+
env:
50+
STELLAR_CLI_VERSION: ${{ steps.scope.outputs.version }}
51+
run: |
52+
matrix="$(./scripts/resolve-matrix.sh \
53+
--stellar-cli-version "$STELLAR_CLI_VERSION")"
54+
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
55+
56+
build:
57+
name: ${{ matrix.stellar_cli_version }} rust${{ matrix.rust_version }} ${{ matrix.arch }}
58+
needs: matrix
59+
strategy:
60+
matrix: ${{ fromJson(needs.matrix.outputs.matrix) }}
61+
fail-fast: false
62+
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
63+
steps:
64+
- name: checkout
65+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
66+
67+
- name: set up buildx
68+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
69+
70+
- name: resolve tag
71+
id: tag
72+
run: |
73+
tag="$(./scripts/tag-names.sh \
74+
--stellar-cli-version ${{ matrix.stellar_cli_version }} \
75+
--rust-version ${{ matrix.rust_version }} \
76+
--platform ${{ matrix.platform }})"
77+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
78+
echo "image=$REGISTRY:$tag" >> "$GITHUB_OUTPUT"
79+
80+
- name: login to Docker Hub
81+
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
82+
with:
83+
username: ${{ secrets.DOCKERHUB_USERNAME }}
84+
password: ${{ secrets.DOCKERHUB_TOKEN }}
85+
86+
- name: check whether already published
87+
id: skip
88+
run: |
89+
if docker buildx imagetools inspect "${{ steps.tag.outputs.image }}" >/dev/null 2>&1; then
90+
echo "::warning::${{ steps.tag.outputs.image }} already exists in the registry; skipping build (per-arch tags are immutable)"
91+
{
92+
echo "## ⚠️ Skipped — already published"
93+
echo ""
94+
echo "\`${{ steps.tag.outputs.image }}\` was already in the registry."
95+
echo ""
96+
echo "Per-arch tags are immutable, so no build or push happened for this row. If this is a corrupt-push recovery, delete the tag in Docker Hub by hand and re-run the workflow."
97+
} >> "$GITHUB_STEP_SUMMARY"
98+
echo "skipped=true" >> "$GITHUB_OUTPUT"
99+
else
100+
echo "skipped=false" >> "$GITHUB_OUTPUT"
101+
fi
102+
103+
- name: build metadata
104+
id: meta
105+
if: steps.skip.outputs.skipped != 'true'
106+
run: |
107+
{
108+
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
109+
echo "builds_json_sha=$(sha256sum builds.json | awk '{print $1}')"
110+
} >> "$GITHUB_OUTPUT"
111+
112+
- name: build and push
113+
id: build
114+
if: steps.skip.outputs.skipped != 'true'
115+
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
116+
with:
117+
context: .
118+
push: true
119+
platforms: ${{ matrix.platform }}
120+
tags: ${{ steps.tag.outputs.image }}
121+
provenance: mode=max
122+
sbom: true
123+
build-args: |
124+
RUST_VERSION=${{ matrix.rust_version }}
125+
RUST_IMAGE_DIGEST=${{ matrix.rust_image_digest }}
126+
STELLAR_CLI_REV=${{ matrix.stellar_cli_ref }}
127+
STELLAR_CLI_VERSION=${{ matrix.stellar_cli_version }}
128+
BUILD_DATE=${{ steps.meta.outputs.build_date }}
129+
BUILDS_JSON_SHA=${{ steps.meta.outputs.builds_json_sha }}
130+
SOURCE_REPO=${{ github.repository }}
131+
132+
- name: generate SBOM file
133+
if: steps.skip.outputs.skipped != 'true'
134+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
135+
with:
136+
image: ${{ steps.tag.outputs.image }}@${{ steps.build.outputs.digest }}
137+
format: spdx-json
138+
output-file: sbom-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.spdx.json
139+
upload-release-assets: false
140+
upload-artifact: false
141+
142+
- name: attest build provenance
143+
id: attest-prov
144+
if: steps.skip.outputs.skipped != 'true'
145+
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
146+
with:
147+
subject-name: ${{ env.REGISTRY }}
148+
subject-digest: ${{ steps.build.outputs.digest }}
149+
# Also push the signed Sigstore attestation alongside the image
150+
# in the registry, so consumers can verify with cosign without
151+
# needing the gh CLI or access to the GitHub attestations API.
152+
push-to-registry: true
153+
154+
- name: attest SBOM
155+
id: attest-sbom
156+
if: steps.skip.outputs.skipped != 'true'
157+
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
158+
with:
159+
subject-name: ${{ env.REGISTRY }}
160+
subject-digest: ${{ steps.build.outputs.digest }}
161+
sbom-path: sbom-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.spdx.json
162+
push-to-registry: true
163+
164+
- name: write per-arch metadata
165+
if: steps.skip.outputs.skipped != 'true'
166+
run: |
167+
out="meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.json"
168+
jq -n \
169+
--arg arch "${{ matrix.arch }}" \
170+
--arg cli "${{ matrix.stellar_cli_version }}" \
171+
--arg digest "${{ steps.build.outputs.digest }}" \
172+
--arg image "${{ steps.tag.outputs.image }}" \
173+
--arg rust "${{ matrix.rust_version }}" \
174+
--arg tag "${{ steps.tag.outputs.tag }}" \
175+
'{arch: $arch, digest: $digest, image: $image, rust_version: $rust, stellar_cli_version: $cli, tag: $tag}' \
176+
> "$out"
177+
178+
- name: rename provenance bundle
179+
if: steps.skip.outputs.skipped != 'true'
180+
run: |
181+
cp "${{ steps.attest-prov.outputs.bundle-path }}" \
182+
"prov-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.intoto.jsonl"
183+
184+
# Skipped pairs still need metadata so the release-body composer can
185+
# show the full state of every declared pair, not just the freshly
186+
# built ones. Queries the existing tag's manifest digest from the
187+
# registry and writes the same meta-*.json shape we'd write on a
188+
# fresh build (just without the SBOM/provenance files — those stay
189+
# attached to the previously-published image's attestation store).
190+
- name: write per-arch metadata (skipped pair)
191+
if: steps.skip.outputs.skipped == 'true'
192+
run: |
193+
existing_digest="$(docker buildx imagetools inspect \
194+
"${{ steps.tag.outputs.image }}" \
195+
--format '{{.Manifest.Digest}}')"
196+
out="meta-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}.json"
197+
jq -n \
198+
--arg arch "${{ matrix.arch }}" \
199+
--arg cli "${{ matrix.stellar_cli_version }}" \
200+
--arg digest "$existing_digest" \
201+
--arg image "${{ steps.tag.outputs.image }}" \
202+
--arg rust "${{ matrix.rust_version }}" \
203+
--arg tag "${{ steps.tag.outputs.tag }}" \
204+
'{arch: $arch, digest: $digest, image: $image, rust_version: $rust, stellar_cli_version: $cli, tag: $tag}' \
205+
> "$out"
206+
207+
- name: upload release artifacts
208+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
209+
with:
210+
name: release-artifacts-${{ matrix.stellar_cli_version }}-rust${{ matrix.rust_version }}-${{ matrix.arch }}
211+
path: |
212+
sbom-*.spdx.json
213+
prov-*.intoto.jsonl
214+
meta-*.json
215+
retention-days: 7
216+
# warn (not error) because the skipped path only produces meta-*.json;
217+
# the sbom-*/prov-* globs find nothing in that case.
218+
if-no-files-found: warn
219+
220+
manifest:
221+
name: assemble manifest lists
222+
needs: [matrix, build]
223+
runs-on: ubuntu-24.04
224+
env:
225+
STELLAR_CLI_VERSION: ${{ needs.matrix.outputs.stellar_cli_version }}
226+
steps:
227+
- name: checkout
228+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
229+
- name: set up buildx
230+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
231+
- name: login to Docker Hub
232+
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
233+
with:
234+
username: ${{ secrets.DOCKERHUB_USERNAME }}
235+
password: ${{ secrets.DOCKERHUB_TOKEN }}
236+
- name: create manifest list per (cli, rust) pair
237+
run: |
238+
while IFS= read -r rust; do
239+
list_tag="$(./scripts/tag-names.sh \
240+
--stellar-cli-version "$STELLAR_CLI_VERSION" --rust-version "$rust")"
241+
amd64_tag="$(./scripts/tag-names.sh \
242+
--stellar-cli-version "$STELLAR_CLI_VERSION" --rust-version "$rust" \
243+
--platform linux/amd64)"
244+
arm64_tag="$(./scripts/tag-names.sh \
245+
--stellar-cli-version "$STELLAR_CLI_VERSION" --rust-version "$rust" \
246+
--platform linux/arm64)"
247+
if docker buildx imagetools inspect "$REGISTRY:$list_tag" >/dev/null 2>&1; then
248+
echo "::warning::manifest list $REGISTRY:$list_tag already exists; skipping (lists are immutable)"
249+
{
250+
echo "## ⚠️ Manifest list skipped — already published"
251+
echo ""
252+
echo "\`$REGISTRY:$list_tag\` was already in the registry."
253+
} >> "$GITHUB_STEP_SUMMARY"
254+
continue
255+
fi
256+
echo "::group::manifest $REGISTRY:$list_tag"
257+
docker buildx imagetools create \
258+
--tag "$REGISTRY:$list_tag" \
259+
"$REGISTRY:$amd64_tag" \
260+
"$REGISTRY:$arm64_tag"
261+
echo "::endgroup::"
262+
done < <(jq -r --arg v "$STELLAR_CLI_VERSION" '
263+
.stellar_cli_versions[]
264+
| select(.version == $v)
265+
| .rust_versions[]
266+
' builds.json)
267+
268+
aliases:
269+
name: publish moving aliases
270+
needs: [matrix, manifest]
271+
runs-on: ubuntu-24.04
272+
env:
273+
STELLAR_CLI_VERSION: ${{ needs.matrix.outputs.stellar_cli_version }}
274+
steps:
275+
- name: checkout
276+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
277+
- name: set up buildx
278+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
279+
- name: login to Docker Hub
280+
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
281+
with:
282+
username: ${{ secrets.DOCKERHUB_USERNAME }}
283+
password: ${{ secrets.DOCKERHUB_TOKEN }}
284+
- name: publish :<cli> and :latest aliases
285+
run: |
286+
default_rust="$(jq -r --arg v "$STELLAR_CLI_VERSION" \
287+
'.stellar_cli_versions[] | select(.version == $v) | .default_rust' \
288+
builds.json | head -n1)"
289+
target_tag="$(./scripts/tag-names.sh \
290+
--stellar-cli-version "$STELLAR_CLI_VERSION" \
291+
--rust-version "$default_rust")"
292+
target="$REGISTRY:$target_tag"
293+
294+
echo "::group::alias $REGISTRY:$STELLAR_CLI_VERSION -> $target"
295+
docker buildx imagetools create --tag "$REGISTRY:$STELLAR_CLI_VERSION" "$target"
296+
echo "::endgroup::"
297+
298+
newest_cli="$(./scripts/newest-pair.sh --stellar-cli-version)"
299+
if [ "$STELLAR_CLI_VERSION" = "$newest_cli" ]; then
300+
echo "::group::alias $REGISTRY:latest -> $target"
301+
docker buildx imagetools create --tag "$REGISTRY:latest" "$target"
302+
echo "::endgroup::"
303+
else
304+
echo "cli $STELLAR_CLI_VERSION is not the newest ($newest_cli); skipping :latest"
305+
fi
306+
307+
release:
308+
name: enrich github release with sbom and provenance
309+
needs: [matrix, aliases]
310+
runs-on: ubuntu-24.04
311+
env:
312+
STELLAR_CLI_VERSION: ${{ needs.matrix.outputs.stellar_cli_version }}
313+
steps:
314+
- name: checkout
315+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
316+
- name: download per-arch release artifacts
317+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
318+
with:
319+
path: release-artifacts
320+
pattern: release-artifacts-*
321+
merge-multiple: true
322+
- name: compose structural body section
323+
run: |
324+
./scripts/release-body.sh \
325+
--stellar-cli-version "$STELLAR_CLI_VERSION" \
326+
--metadata-dir release-artifacts \
327+
--registry "$REGISTRY" \
328+
--repo "${{ github.repository }}" \
329+
> release-body.md
330+
- name: update the github release
331+
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
332+
with:
333+
tag_name: ${{ github.event.release.tag_name }}
334+
body_path: release-body.md
335+
append_body: true
336+
files: |
337+
release-artifacts/sbom-*.spdx.json
338+
release-artifacts/prov-*.intoto.jsonl
339+
340+
complete:
341+
if: always()
342+
needs:
343+
- matrix
344+
- build
345+
- manifest
346+
- aliases
347+
- release
348+
runs-on: ubuntu-24.04
349+
steps:
350+
- name: check upstream jobs
351+
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
352+
run: exit 1

0 commit comments

Comments
 (0)