Skip to content

Commit 172e1ca

Browse files
Yolean k8s-qa (buckety maintainer)claude
andcommitted
release: per-commit timestamp tag + pinned digest workflow
scripts/bump-release.sh mints a UTC-seconds ISO tag, builds the OCI deterministically with that tag baked into main.version, pins the manifest digest in deploy/kustomize/release/kustomization.yaml. CI's e2e job reads the same tag+digest via yq, rebuilds, and asserts the produced digest matches the pinned one. The publish job (gated to main on the canonical Yolean org) rebuilds, re-asserts, and craned- pushes to ghcr.io. No commit-back: the digest is pinned in the commit being built. -buildvcs=false + go-version-file: go.mod are the reproducibility levers - without them the binary depends on whether the tree had uncommitted files at build time and on which patch of Go was on PATH. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent edc8eb0 commit 172e1ca

3 files changed

Lines changed: 160 additions & 17 deletions

File tree

.github/workflows/e2e.yaml

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@ jobs:
1616
e2e:
1717
runs-on: ubuntu-latest
1818
timeout-minutes: 30
19+
outputs:
20+
tag: ${{ steps.release.outputs.tag }}
21+
digest: ${{ steps.release.outputs.digest }}
1922
steps:
2023
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2124

2225
- name: Set up Go
2326
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
2427
with:
25-
go-version: stable
28+
go-version-file: go.mod # reproducibility: same Go as local builds
2629

27-
- name: Install k3d, kubectl, crane
30+
- name: Install k3d, kubectl, crane, yq
2831
run: |
2932
set -euxo pipefail
3033
# k3d is the only provisioner that works in GHA; kind, minikube
@@ -37,8 +40,23 @@ jobs:
3740
# crane converts our OCI layout to a registry push without a
3841
# docker daemon round-trip.
3942
go install github.com/google/go-containerregistry/cmd/crane@v0.20.6
43+
# yq parses the pinned tag + digest out of the release kustomization.
44+
go install github.com/mikefarah/yq/v4@v4.46.1
4045
echo "$HOME/go/bin" >>"$GITHUB_PATH"
4146
"$HOME/go/bin/crane" version
47+
"$HOME/go/bin/yq" --version
48+
49+
- name: Read pinned release
50+
id: release
51+
run: |
52+
set -euxo pipefail
53+
K=deploy/kustomize/release/kustomization.yaml
54+
T=$(yq '.images[] | select(.name == "ghcr.io/yolean/buckety-controller") | .newTag' "$K")
55+
D=$(yq '.images[] | select(.name == "ghcr.io/yolean/buckety-controller") | .digest' "$K")
56+
[[ -n "$T" && "$T" != null ]] || { echo "$K: no newTag" >&2; exit 1; }
57+
[[ "$D" == sha256:* ]] || { echo "$K: bad digest $D" >&2; exit 1; }
58+
echo "tag=$T" >> "$GITHUB_OUTPUT"
59+
echo "digest=$D" >> "$GITHUB_OUTPUT"
4260
4361
- name: Start k3d cluster with local registry
4462
run: |
@@ -66,26 +84,44 @@ jobs:
6684
- name: Set up contain
6785
uses: solsson/setup-contain@49cc4cce1498df8a59e88fff52c1a9b747f11f08 # v1
6886

69-
- name: Build controller image (contain)
87+
- name: Build controller image (contain) with pinned tag
88+
env:
89+
T: ${{ steps.release.outputs.tag }}
7090
run: |
7191
set -euxo pipefail
7292
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
7393
-trimpath \
74-
-ldflags "-s -w -X main.version=$(git describe --always --dirty)" \
94+
-buildvcs=false \
95+
-ldflags "-s -w -X main.version=${T}" \
7596
-o target/linux/amd64/buckety \
7697
./cmd/buckety
77-
IMAGE=ghcr.io/yolean/buckety-controller:${{ github.sha }} \
98+
IMAGE="ghcr.io/yolean/buckety-controller:${T}" \
7899
contain build --output ./oci --push=false
79100
101+
- name: Assert produced digest matches pinned
102+
env:
103+
PINNED: ${{ steps.release.outputs.digest }}
104+
run: |
105+
set -euxo pipefail
106+
BUILT=$(jq -r '.manifests[0].digest' oci/index.json)
107+
if [[ "$BUILT" != "$PINNED" ]]; then
108+
echo "Pinned digest in deploy/kustomize/release/kustomization.yaml" >&2
109+
echo " ${PINNED}" >&2
110+
echo "does not match the digest produced by this build:" >&2
111+
echo " ${BUILT}" >&2
112+
echo "Re-run scripts/bump-release.sh and commit the result." >&2
113+
exit 1
114+
fi
115+
80116
- name: Run e2e harness
81117
env:
82118
IMPLEMENTATIONS: redpanda,versitygw,minio
83119
OCI_DIR: ./oci
84120
# PUSH_AS reaches the k3d local registry via its host-published
85121
# port; CONTROLLER_IMAGE uses the same registry's in-cluster
86122
# hostname so node containerd can pull it.
87-
PUSH_AS: localhost:5000/yolean/buckety-controller:${{ github.sha }}
88-
CONTROLLER_IMAGE: k3d-buckety-registry:5000/yolean/buckety-controller:${{ github.sha }}
123+
PUSH_AS: localhost:5000/yolean/buckety-controller:${{ steps.release.outputs.tag }}
124+
CONTROLLER_IMAGE: k3d-buckety-registry:5000/yolean/buckety-controller:${{ steps.release.outputs.tag }}
89125
run: ./test/e2e/run.sh
90126

91127
- name: Surface controller logs on failure
@@ -98,11 +134,10 @@ jobs:
98134
kubectl -n blobs describe pod -l app=minio || true
99135
100136
# The image push job runs only on main-branch builds in the canonical
101-
# Yolean org with a successful e2e; it pins by digest into
102-
# deploy/kustomize/release/ so consumers vendor a self-consistent base.
103-
# The repository_owner gate means YoleanAgents (and forks) can iterate
104-
# on the e2e job without publishing images or committing release pins.
105-
# Stubbed for phase 1.
137+
# Yolean org with a successful e2e. It rebuilds (proves reproducibility),
138+
# asserts the same digest the e2e job verified, then crane pushes to
139+
# ghcr.io. No commit-back: the digest is pinned in the commit being
140+
# built. repository_owner gates YoleanAgents and forks from publishing.
106141
publish:
107142
if: >-
108143
github.ref == 'refs/heads/main'
@@ -111,13 +146,61 @@ jobs:
111146
needs: e2e
112147
runs-on: ubuntu-latest
113148
permissions:
114-
contents: write
149+
contents: read
115150
packages: write
116151
steps:
117152
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
118-
- name: contain push (stub)
153+
154+
- name: Set up Go
155+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
156+
with:
157+
go-version-file: go.mod # reproducibility: same Go as local builds
158+
159+
- name: Install crane
160+
run: |
161+
set -euxo pipefail
162+
go install github.com/google/go-containerregistry/cmd/crane@v0.20.6
163+
echo "$HOME/go/bin" >>"$GITHUB_PATH"
164+
165+
- name: Set up contain
166+
uses: solsson/setup-contain@49cc4cce1498df8a59e88fff52c1a9b747f11f08 # v1
167+
168+
- name: Rebuild controller image with pinned tag
169+
env:
170+
T: ${{ needs.e2e.outputs.tag }}
119171
run: |
120-
echo "TODO contain build --push --image ghcr.io/yolean/buckety-controller@..."
121-
- name: template release base (stub)
172+
set -euxo pipefail
173+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
174+
-trimpath \
175+
-buildvcs=false \
176+
-ldflags "-s -w -X main.version=${T}" \
177+
-o target/linux/amd64/buckety \
178+
./cmd/buckety
179+
IMAGE="ghcr.io/yolean/buckety-controller:${T}" \
180+
contain build --output ./oci --push=false
181+
182+
- name: Assert publish-time digest matches the e2e-tested digest
183+
env:
184+
PINNED: ${{ needs.e2e.outputs.digest }}
185+
run: |
186+
set -euxo pipefail
187+
BUILT=$(jq -r '.manifests[0].digest' oci/index.json)
188+
if [[ "$BUILT" != "$PINNED" ]]; then
189+
echo "publish rebuild produced ${BUILT}, e2e verified ${PINNED}" >&2
190+
exit 1
191+
fi
192+
193+
- name: Login to ghcr.io
194+
env:
195+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
122196
run: |
123-
echo "TODO template deploy/kustomize/release/ with the pushed digest, commit back"
197+
set -euo pipefail
198+
echo "$GITHUB_TOKEN" | crane auth login ghcr.io \
199+
-u "${{ github.actor }}" --password-stdin
200+
201+
- name: Push image to ghcr.io
202+
env:
203+
T: ${{ needs.e2e.outputs.tag }}
204+
run: |
205+
set -euxo pipefail
206+
crane push ./oci "ghcr.io/yolean/buckety-controller:${T}"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apiVersion: kustomize.config.k8s.io/v1beta1
2+
kind: Kustomization
3+
# Generated kustomize bundle vendored downstream. The `images:` entry
4+
# is the only thing that changes between releases; bump it via
5+
# scripts/bump-release.sh which also rebuilds and pins the digest.
6+
# CI asserts the pinned digest matches a fresh `contain` build.
7+
resources:
8+
- ../base
9+
images:
10+
- digest: sha256:e7de9d78243f90b1b58d6176d3e15422c335e465b73d1fc9c97f6a93bf835ce2
11+
name: ghcr.io/yolean/buckety-controller
12+
newName: ghcr.io/yolean/buckety-controller
13+
newTag: 20260531T201852Z

scripts/bump-release.sh

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Bump the release pin in deploy/kustomize/release/kustomization.yaml.
4+
# Mints a UTC-seconds ISO tag, builds the OCI deterministically with
5+
# that tag baked into main.version, extracts the manifest digest from
6+
# the OCI layout, and writes both back into the kustomization. CI's
7+
# e2e job rebuilds with the same tag and asserts the produced digest
8+
# matches; the publish job craned-pushes the same OCI to ghcr.io.
9+
#
10+
# Run this before committing whenever the controller binary or its
11+
# base image changes; the assertion in CI catches you otherwise.
12+
13+
set -euo pipefail
14+
15+
here() { cd "$(dirname "${BASH_SOURCE[0]}")" && pwd; }
16+
REPO="$(cd "$(here)/.." && pwd)"
17+
18+
command -v contain >/dev/null || { echo "contain not on PATH" >&2; exit 1; }
19+
command -v jq >/dev/null || { echo "jq not on PATH" >&2; exit 1; }
20+
command -v kustomize >/dev/null || { echo "kustomize not on PATH" >&2; exit 1; }
21+
22+
cd "$REPO"
23+
TAG="$(date -u +%Y%m%dT%H%M%SZ)"
24+
25+
rm -rf target/linux/amd64 oci
26+
# -buildvcs=false is the reproducibility lever: with it on (the default),
27+
# Go embeds the commit SHA and a "modified" flag, which makes the digest
28+
# depend on whether the tree had uncommitted files at build time. Off,
29+
# the binary is a pure function of source + flags + Go version.
30+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
31+
-trimpath \
32+
-buildvcs=false \
33+
-ldflags "-s -w -X main.version=${TAG}" \
34+
-o target/linux/amd64/buckety \
35+
./cmd/buckety
36+
37+
IMAGE="ghcr.io/yolean/buckety-controller:${TAG}" \
38+
contain build --output ./oci --push=false >/dev/null
39+
40+
DIGEST="$(jq -r '.manifests[0].digest' oci/index.json)"
41+
[[ "$DIGEST" == sha256:* ]] || { echo "unexpected digest: $DIGEST" >&2; exit 1; }
42+
43+
( cd deploy/kustomize/release \
44+
&& kustomize edit set image \
45+
"ghcr.io/yolean/buckety-controller=ghcr.io/yolean/buckety-controller:${TAG}@${DIGEST}" )
46+
47+
printf 'release bumped: ghcr.io/yolean/buckety-controller:%s@%s\n' "$TAG" "$DIGEST"

0 commit comments

Comments
 (0)