Repository navigation
338 lines (319 loc) · 13.6 KB
/
Copy pathrelease-repeatability.yml
File metadata and controls
338 lines (319 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
name: RegistryStack Release Repeatability
on:
schedule:
- cron: "37 3 * * 1"
workflow_dispatch:
inputs:
tag:
description: Exact published vMAJOR.MINOR.PATCH tag; newest release when omitted
required: false
type: string
permissions:
contents: read
concurrency:
group: release-repeatability
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
CRANE_VERSION: v0.21.2
CRANE_LINUX_AMD64_SHA256: 897e7c342db072ba76531246fc18fbf3e8e298688b6ecf98916770984b263866
RELEASE_BUILDX_VERSION: v0.33.0
RELEASE_BUILDKIT_IMAGE: ghcr.io/registrystack/buildkit:v0.31.2@sha256:2f5adac4ecd194d9f8c10b7b5d7bceb5186853db1b26e5abd3a657af0b7e26ec
jobs:
rebuild:
name: Clean rebuild of published release
runs-on: ubuntu-24.04
timeout-minutes: 90
permissions:
contents: read
steps:
- name: Checkout protected repeatability tooling
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
path: verifier
fetch-depth: 0
persist-credentials: false
submodules: false
- name: Resolve exact published tag
id: release
env:
GH_TOKEN: ${{ github.token }}
REQUESTED_TAG: ${{ inputs.tag || '' }}
shell: bash
run: |
set -euo pipefail
tag="${REQUESTED_TAG}"
if [[ -z "${tag}" ]]; then
tag="$(
gh release list --repo "${GITHUB_REPOSITORY}" --limit 100 \
--exclude-drafts --json tagName \
--jq '[.[].tagName | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+$"))][0]'
)"
fi
if [[ ! "${tag}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "tag must be an exact published vMAJOR.MINOR.PATCH tag" >&2
exit 1
fi
if (( BASH_REMATCH[1] == 0 && BASH_REMATCH[2] < 19 )); then
echo "pre-v0.19 releases are immutable historical evidence; use the corresponding ${tag} Git tag and archived assets" >&2
exit 1
fi
gh release view "${tag}" --repo "${GITHUB_REPOSITORY}" \
--json isDraft,tagName \
| jq -e --arg tag "${tag}" \
'select(.isDraft == false and .tagName == $tag)' >/dev/null
{
echo "tag=${tag}"
echo "version=${tag#v}"
} >> "${GITHUB_OUTPUT}"
- name: Checkout exact release source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.release.outputs.tag }}
path: source
fetch-depth: 0
persist-credentials: false
submodules: false
- name: Verify tag source and download published subjects
id: source
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.release.outputs.tag }}
shell: bash
run: |
set -euo pipefail
git -C source fetch --force origin \
"refs/heads/main:refs/remotes/origin/main" \
"refs/tags/${TAG}:refs/tags/${TAG}"
source_sha="$(git -C source rev-parse --verify "refs/tags/${TAG}^{}")"
test "$(git -C source rev-parse HEAD)" = "${source_sha}"
git -C source merge-base --is-ancestor \
"${source_sha}" refs/remotes/origin/main
mkdir -p published
release_manifest="registry-stack-${TAG}-release-manifest.json"
release_assets="$(
gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" \
--json assets --jq '.assets[].name'
)"
image_metadata=""
image_metadata_kind=""
if grep -Fqx -- "${release_manifest}" <<<"${release_assets}"; then
image_metadata="${release_manifest}"
image_metadata_kind="release-manifest"
else
echo "${TAG} requires release asset ${release_manifest}" >&2
exit 1
fi
gh release download "${TAG}" --repo "${GITHUB_REPOSITORY}" \
--dir published \
--pattern SHA256SUMS \
--pattern "*-${TAG}-linux-amd64" \
--pattern "${image_metadata}"
test -s "published/${image_metadata}"
(
cd published
sha256sum --check --strict --ignore-missing SHA256SUMS
)
{
echo "source_sha=${source_sha}"
echo "image_metadata=published/${image_metadata}"
echo "image_metadata_kind=${image_metadata_kind}"
} >> "${GITHUB_OUTPUT}"
- name: Build canonical Linux payload from clean state
env:
VERSION: ${{ steps.release.outputs.version }}
shell: bash
run: |
set -euo pipefail
cargo_home="${RUNNER_TEMP}/repeatability-cargo-home"
target_dir="${RUNNER_TEMP}/repeatability-target"
test ! -e "${cargo_home}"
test ! -e "${target_dir}"
mkdir -p "${cargo_home}" "${target_dir}"
RELEASE_CARGO_HOME="${cargo_home}" \
RELEASE_TARGET_DIR="${target_dir}" \
source/release/scripts/build-release-binaries.sh "${VERSION}"
- name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: "0.11.16"
- name: Compare published binary hashes
env:
TAG: ${{ steps.release.outputs.tag }}
shell: bash
run: |
set -euo pipefail
mkdir -p proof
mapfile -t expected < <(
uv run --no-project --with PyYAML==6.0.2 python3 - "source" "${TAG}" <<'PY'
import glob
import sys
import yaml
sys.path.insert(0, "verifier/release/scripts")
import release_roster
root, tag = sys.argv[1:]
version = tag.removeprefix("v")
matches = []
for path in glob.glob(f"{root}/release/manifests/registry-stack-*.yaml"):
with open(path, encoding="utf-8") as handle:
data = yaml.safe_load(handle)
if str(data.get("stack", {}).get("version", "")) == version:
matches.append(data)
if len(matches) != 1:
raise SystemExit(f"expected one release manifest for {version}")
artifacts = matches[0].get("artifacts", {})
parsed_version = release_roster.parse_version(version)
binary_names = {
"discovery": "discovery",
"discoveryctl": "discoveryctl",
"evidence": "evidence",
"evidencectl": "evidencectl",
"evidence-oid4vci": "evidence-oid4vci",
"mint": "mint",
"registry-manifest": "registry-manifest",
"registry-manifest-cli": "registry-manifest",
"registry-render": "registry-render",
"breg": "breg",
"bregctl": "bregctl",
"breg-mcp": "breg-mcp",
"breg-review": "breg-review",
"casework": "casework",
"caseworkctl": "caseworkctl",
"schedulingctl": "schedulingctl",
"messaging": "messaging",
"messagingctl": "messagingctl",
"coordinator": "coordinator",
"coordinatorctl": "coordinatorctl",
}
# Historical repeatability still compares the immutable Relay bytes,
# while current manifests cannot reintroduce them.
if release_roster.relay_in_release(parsed_version):
binary_names.update({"relay": "relay", "relayctl": "relayctl"})
if release_roster.scheduling_binary_in_release(parsed_version):
binary_names["scheduling"] = "scheduling"
for artifact, asset in sorted(binary_names.items()):
if artifact in artifacts:
print(f"{asset}-{tag}-linux-amd64")
PY
)
for asset in "${expected[@]}"; do
test -s "published/${asset}"
cmp "published/${asset}" "source/dist/bin/${asset}"
done
(
cd source/dist/bin
sha256sum -- "${expected[@]}"
) | sort > proof/rebuilt-linux-amd64.sha256
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with:
version: ${{ env.RELEASE_BUILDX_VERSION }}
driver-opts: image=${{ env.RELEASE_BUILDKIT_IMAGE }}
- name: Install pinned image inspector
shell: bash
run: |
set -euo pipefail
archive="${RUNNER_TEMP}/go-containerregistry_Linux_x86_64.tar.gz"
curl -sSfL \
"https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" \
-o "${archive}"
echo "${CRANE_LINUX_AMD64_SHA256} ${archive}" \
| sha256sum --check --strict
mkdir -p "${HOME}/.local/bin"
tar -xzf "${archive}" -C "${HOME}/.local/bin" crane
echo "${HOME}/.local/bin" >> "${GITHUB_PATH}"
- name: Compare published image config and layers
env:
BUILDER_NAME: ${{ steps.buildx.outputs.name }}
IMAGE_METADATA: ${{ steps.source.outputs.image_metadata }}
IMAGE_METADATA_KIND: ${{ steps.source.outputs.image_metadata_kind }}
SOURCE_SHA: ${{ steps.source.outputs.source_sha }}
TAG: ${{ steps.release.outputs.tag }}
VERSION: ${{ steps.release.outputs.version }}
shell: bash
run: |
set -euo pipefail
mkdir -p proof/image-layouts
image_rows="${RUNNER_TEMP}/repeatability-images.tsv"
case "${IMAGE_METADATA_KIND}" in
release-manifest)
jq -er '.images[] | [.name,.final_ref,.digest] | @tsv' \
"${IMAGE_METADATA}" > "${image_rows}"
;;
*)
echo "unsupported repeatability image metadata kind: ${IMAGE_METADATA_KIND}" >&2
exit 1
;;
esac
test -s "${image_rows}"
# The rows arrive on descriptor 3 so no command in the loop body can
# read the remaining image list from standard input.
while IFS=$'\t' read -r -u 3 name published_ref published_digest; do
test -n "${name}"
[[ "${published_ref}" == "ghcr.io/registrystack/${name}:${TAG}" ]]
[[ "${published_digest}" =~ ^sha256:[0-9a-f]{64}$ ]]
published_repository="ghcr.io/registrystack/${name}"
published_layout="proof/image-layouts/${name}-published"
rebuilt_layout="${GITHUB_WORKSPACE}/proof/image-layouts/${name}-rebuilt"
# The tag must still name the recorded digest; the pull then uses
# the digest itself. crane writes a layout whose index.json names
# that digest, an image manifest or an image index, and the
# comparator resolves it from there. Never rewrite index.json.
test "$(crane digest "${published_ref}")" = "${published_digest}"
crane pull "${published_repository}@${published_digest}" \
"${published_layout}" --format=oci
RELEASE_BUILDX_BUILDER="${BUILDER_NAME}" \
RELEASE_IMAGE_CONTEXT="${GITHUB_WORKSPACE}/source" \
RELEASE_IMAGE_NO_CACHE=true \
RELEASE_IMAGE_OCI_LAYOUT="${rebuilt_layout}" \
source/release/scripts/build-release-image.sh \
"${name}" \
"ghcr.io/registrystack/${name}:${TAG}" \
"https://github.com/${GITHUB_REPOSITORY}" \
"${SOURCE_SHA}" \
"${VERSION}" \
"${GITHUB_WORKSPACE}/proof/image-layouts/${name}.metadata.json"
python3 verifier/release/scripts/compare-release-image-layouts.py \
--published-digest "${published_digest}" \
"${published_layout}" "${rebuilt_layout}"
done 3< "${image_rows}"
- name: Record successful 30-day proof
env:
SOURCE_SHA: ${{ steps.source.outputs.source_sha }}
TAG: ${{ steps.release.outputs.tag }}
shell: bash
run: |
set -euo pipefail
jq -n \
--arg repository "${GITHUB_REPOSITORY}" \
--arg tag "${TAG}" \
--arg source_sha "${SOURCE_SHA}" \
--arg completed_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg run_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
'{
schema_version: "registry-stack.release-repeatability-result.v2",
repository: $repository,
tag: $tag,
source_sha: $source_sha,
result: "passed",
scope: {
linux_amd64_binaries: "byte-identical",
images: "config-and-ordered-rootfs-layers-identical",
other_platforms: "not-tested"
},
completed_at: $completed_at,
silver_claim_valid_through:
(($completed_at | fromdateiso8601) + (30 * 24 * 60 * 60) | todateiso8601),
run_url: $run_url
}' > proof/release-repeatability-result.json
- name: Upload 30-day repeatability evidence
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-repeatability-${{ steps.release.outputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
proof/rebuilt-linux-amd64.sha256
proof/release-repeatability-result.json
if-no-files-found: error
retention-days: 30