Skip to content

Commit cfe917f

Browse files
committed
CI: record per-target build identity (OID) on images
Foundation for skipping rebuilds of unchanged shell versions. Builds are byte-reproducible and committed build checksums are the authoritative output identity, so a content-addressed OID can decide when a target cannot have changed and its prior-push image can be reused. shvr.sh (inert — no caller behavior change): - shvr_toolchain_fingerprint: hash of the global pins (musl-cross-make commit, RUST_TOOLCHAIN, DEBIAN_SNAPSHOT, TOOLCHAIN_BASE digest, ncurses, rustup-init) + an OIDV scheme tag; folded into every OID so a pin bump forces a full rebuild. - shvr_build_identity <target>: sha256(fingerprint + the target's committed build checksums); prints MISSING (always rebuild) when none exist. - shvr_build_identities, shvr_plan_matrix <file>: emit identities and the dynamic build-matrix JSON of missing/forced/mismatched targets. Verified to reproduce the committed static matrix exactly (250/250, no field drift). CI (Step 1 — annotate while still building all 250): - build-push-retry forwards new annotations/labels/provenance inputs. - build job computes the OID and pushes each image with provenance:false + an org.shvr.oid annotation (and config label) so a later plan job can read it from the registry without pulling layers. No skipping yet.
1 parent 438c610 commit cfe917f

3 files changed

Lines changed: 147 additions & 0 deletions

File tree

.github/actions/build-push-retry/action.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ inputs:
3535
description: 'Cache export'
3636
required: false
3737
default: ''
38+
labels:
39+
description: 'Image labels (config-level); empty adds none'
40+
required: false
41+
default: ''
42+
annotations:
43+
description: 'OCI annotations (manifest-level); empty adds none'
44+
required: false
45+
default: ''
46+
provenance:
47+
description: 'Provenance attestation (true|false|mode=...); empty uses the action default'
48+
required: false
49+
default: ''
3850
runs:
3951
using: composite
4052
steps:
@@ -51,6 +63,9 @@ runs:
5163
build-args: ${{ inputs.build-args }}
5264
cache-from: ${{ inputs.cache-from }}
5365
cache-to: ${{ inputs.cache-to }}
66+
labels: ${{ inputs.labels }}
67+
annotations: ${{ inputs.annotations }}
68+
provenance: ${{ inputs.provenance }}
5469
- name: Build and push (retry)
5570
if: steps.attempt1.outcome == 'failure'
5671
uses: docker/build-push-action@v7
@@ -63,3 +78,6 @@ runs:
6378
build-args: ${{ inputs.build-args }}
6479
cache-from: ${{ inputs.cache-from }}
6580
cache-to: ${{ inputs.cache-to }}
81+
labels: ${{ inputs.labels }}
82+
annotations: ${{ inputs.annotations }}
83+
provenance: ${{ inputs.provenance }}

.github/workflows/docker.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,15 @@ jobs:
162162
with:
163163
driver-opts: image=${{ needs.mirror.outputs.buildkit_ref }}
164164

165+
# Content-addressed build identity for this target. Recorded on the image
166+
# as an OCI annotation (+ config label) so a future `plan` job can skip
167+
# rebuilding targets whose OID is unchanged. See shvr_build_identity.
168+
- name: Compute build identity (OID)
169+
id: oid
170+
env:
171+
TARGET: ${{ matrix.target }}
172+
run: echo "oid=$(sh shvr.sh build_identity "$TARGET")" >> "$GITHUB_OUTPUT"
173+
165174
- name: Build and push shell image
166175
uses: ./.github/actions/build-push-retry
167176
with:
@@ -182,6 +191,13 @@ jobs:
182191
${{ github.event_name == 'push'
183192
&& format('type=registry,ref=ghcr.io/{0}:buildcache-{1},mode=max', github.repository, matrix.target)
184193
|| '' }}
194+
# Single-manifest image so the OID annotation sits at the manifest top
195+
# level, readable via `imagetools inspect --raw` without pulling layers.
196+
provenance: false
197+
annotations: |
198+
manifest:org.shvr.oid=${{ steps.oid.outputs.oid }}
199+
labels: |
200+
org.shvr.oid=${{ steps.oid.outputs.oid }}
185201
186202
- name: Verify build checksums
187203
uses: ./.github/actions/verify-build-checksums

shvr.sh

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,4 +724,117 @@ shvr_github_regen_all ()
724724
(shvr_github_regen_docker_workflow)
725725
}
726726

727+
# Toolchain fingerprint: a hash of every globally-pinned reproducibility input
728+
# (musl-cross-make commit, Rust toolchain, Debian snapshot, base image digest,
729+
# ncurses, rustup-init). It is folded into every target's build identity, so
730+
# bumping any pin changes all identities and forces a full rebuild. Bump the
731+
# OIDV scheme tag to force a rebuild without touching a pin.
732+
shvr_toolchain_fingerprint ()
733+
{
734+
. "${SHVR_DIR_SELF}/common/musl-cross-make.sh"
735+
. "${SHVR_DIR_SELF}/common/ncurses.sh"
736+
737+
dockerfile="${SHVR_DIR_SELF}/Dockerfile"
738+
fp_rust="$(grep -m1 'ARG RUST_TOOLCHAIN=' "$dockerfile" | sed 's/.*=//')"
739+
fp_snap="$(grep -m1 'ARG DEBIAN_SNAPSHOT=' "$dockerfile" | sed 's/.*=//')"
740+
fp_tbase="$(grep -m1 'ARG TOOLCHAIN_BASE=' "$dockerfile" | sed 's/.*=//')"
741+
fp_rustup="$(grep -oE 'rustup-init-[0-9][0-9.]*\.sh' "$dockerfile" | head -n1)"
742+
743+
{
744+
printf 'OIDV=1\n'
745+
printf 'MCM=%s\n' "$SHVR_MCM_COMMIT"
746+
printf 'RUST=%s\n' "$fp_rust"
747+
printf 'SNAP=%s\n' "$fp_snap"
748+
printf 'TBASE=%s\n' "$fp_tbase"
749+
printf 'NCURSES=%s\n' "$SHVR_NCURSES_VERSION"
750+
printf 'RUSTUP=%s\n' "$fp_rustup"
751+
} | sha256sum | cut -d' ' -f1
752+
}
753+
754+
# Build identity (OID) for one target: a hash of the toolchain fingerprint plus
755+
# the target's committed build checksums. Because builds are byte-reproducible
756+
# and the build-checksum verify gate keeps committed checksums in sync with the
757+
# intended binary, the OID changes if and only if the binary should change.
758+
# Prints MISSING (and warns) when no committed checksums exist, so such a target
759+
# is always (re)built and never wrongly skipped. Pass a precomputed fingerprint
760+
# as $2 to avoid recomputing it per target.
761+
shvr_build_identity ()
762+
{
763+
target="$1"
764+
fingerprint="${2:-$(shvr_toolchain_fingerprint)}"
765+
dir="${SHVR_CHECKSUMS_DIR}/build/${target}"
766+
767+
if ! test -d "$dir"
768+
then
769+
echo "shvr_build_identity: no build checksums for ${target} at ${dir}" >&2
770+
echo MISSING
771+
return 0
772+
fi
773+
774+
{
775+
printf '%s\n' "$fingerprint"
776+
find "$dir" -name '*.sha256sums' | LC_ALL=C sort | while IFS= read -r f
777+
do cat "$f"
778+
done
779+
} | sha256sum | cut -d' ' -f1
780+
}
781+
782+
# Emit "<target> <oid>" for every target (or the targets passed as arguments).
783+
shvr_build_identities ()
784+
{
785+
if test -z "$*"
786+
then set -- $(printf '%s ' $(shvr_targets))
787+
fi
788+
fingerprint="$(shvr_toolchain_fingerprint)"
789+
for bi_target in "$@"
790+
do
791+
printf '%s %s\n' "$bi_target" "$(shvr_build_identity "$bi_target" "$fingerprint")"
792+
done
793+
}
794+
795+
# Print the dynamic GitHub Actions build matrix (JSON) of targets that must be
796+
# (re)built. Reads "<target> <current-oid>" lines from <registry_oid_file> (the
797+
# OID currently published for each target, or MISSING/FORCE), compares against
798+
# the freshly computed desired OID, and includes any target that is missing,
799+
# forced, or mismatched. Does no network I/O: the registry read happens in the
800+
# workflow and is passed in as a file.
801+
shvr_plan_matrix ()
802+
(
803+
registry_file="$1"
804+
fingerprint="$(shvr_toolchain_fingerprint)"
805+
806+
# Slurp to a real file so it is re-readable per target (handles /dev/stdin).
807+
reg="${TMPDIR:-/tmp}/shvr_plan.$$"
808+
trap 'rm -f "$reg"' EXIT INT TERM
809+
cat "$registry_file" > "$reg"
810+
811+
printf '{"include":['
812+
first=1
813+
for target in $(shvr_targets)
814+
do
815+
desired="$(shvr_build_identity "$target" "$fingerprint")"
816+
current="$(awk -v t="$target" '$1 == t {print $2; exit}' "$reg")"
817+
818+
# Skip only when the published OID matches and is a real identity.
819+
if test "$desired" != MISSING && test "x$current" = "x$desired"
820+
then continue
821+
fi
822+
823+
shvr_clear_versioninfo
824+
interpreter="${target%%_*}"
825+
version="${target#*_}"
826+
. "${SHVR_DIR_SELF}/variants/${interpreter}.sh"
827+
"shvr_versioninfo_${interpreter}" "$version"
828+
cache_path="${build_srcdir#${SHVR_DIR_SRC}/}"
829+
830+
if test "$first" = 1
831+
then first=0
832+
else printf ','
833+
fi
834+
printf '{"target":"%s","shell":"%s","version":"%s","cache_path":"%s","oid":"%s"}' \
835+
"$target" "$interpreter" "$version" "$cache_path" "$desired"
836+
done
837+
printf ']}\n'
838+
)
839+
727840
shvr "${@:-}"

0 commit comments

Comments
 (0)