@@ -16,33 +16,53 @@ permissions:
1616 contents : read
1717 packages : write
1818
19+ # One run per ref. Two pushes to main in quick succession would otherwise race
20+ # each other to the same tag, and the winner is whichever finishes last rather
21+ # than whichever is newer.
22+ #
23+ # Cancelled only for pull requests. A run that is publishing has already pushed
24+ # per-architecture images, and killing it between that and the manifest list
25+ # leaves them in the registry with nothing pointing at them.
26+ concurrency :
27+ group : ${{ github.workflow }}-${{ github.ref }}
28+ cancel-in-progress : ${{ github.event_name == 'pull_request' }}
29+
1930env :
2031 IMAGE : ghcr.io/runonflux/flux-volume-tools
2132
33+ # Actions are pinned to commits rather than to major tags. A major tag is
34+ # mutable, and every job here runs with packages: write - so a compromised tag
35+ # would be a compromised release of the program that runs as root over an
36+ # application's volume. The comment after each is the tag it was resolved from.
2237jobs :
2338 # flux-op's own logic, reachable without a container in the way. The shell
2439 # implementation this replaced could only be exercised through one, which is
2540 # how it shipped handing every command it ran /dev/null as its standard input.
2641 unit :
2742 runs-on : ubuntu-latest
2843 steps :
29- - uses : actions/checkout@v4
44+ - uses : actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
3045
31- - uses : actions/setup-go@v5
46+ - uses : actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
3247 with :
3348 go-version-file : go.mod
3449
3550 - name : Assert the source is formatted
3651 run : |
37- unformatted="$(gofmt -l cmd)"
52+ unformatted="$(gofmt -l cmd test )"
3853 if [ -n "$unformatted" ]; then
3954 echo "not gofmt'd: $unformatted"
4055 exit 1
4156 fi
4257
4358 - run : go vet ./...
4459
45- - run : go test ./... -race
60+ # The container tests are behind a build tag, so the vet above never
61+ # compiles them. They are the half most likely to rot precisely because
62+ # nothing compiles them by default.
63+ - run : go vet -tags docker ./...
64+
65+ - run : go test ./cmd/... -race
4666
4767 # What the image DOES, exercised through a container configured exactly as the
4868 # FluxOS volume executor configures it - read-only rootfs, no network, all
@@ -52,85 +72,165 @@ jobs:
5272 #
5373 # Both architectures. arm64 is published and, before this, was never executed
5474 # here at all.
75+ #
76+ # Where this run publishes, the image is pushed BEFORE it is tested and is
77+ # tested through the registry. That ordering is the point: what gets tagged is
78+ # then the same bytes that passed, rather than a second build of the same
79+ # source. The Dockerfile pins a minor Alpine tag and installs unpinned apk
80+ # packages, so two builds minutes apart are not required to agree - and the one
81+ # that shipped used to be the one nothing had run.
82+ #
83+ # A digest with no tag on it is not published in any useful sense: nothing can
84+ # resolve to it without already knowing it. If the tests fail, no tag is ever
85+ # created and the digest is left unreferenced.
5586 image :
5687 runs-on : ubuntu-latest
5788 strategy :
5889 fail-fast : false
5990 matrix :
60- platform : [linux/amd64, linux/arm64]
91+ include :
92+ - platform : linux/amd64
93+ arch : amd64
94+ - platform : linux/arm64
95+ arch : arm64
6196 steps :
62- - uses : actions/checkout@v4
97+ - uses : actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
6398
64- - uses : actions/setup-go@v5
99+ - uses : actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
65100 with :
66101 go-version-file : go.mod
67102
68103 # Only the non-native architecture needs emulation, and only to RUN the
69104 # image: flux-op is cross-compiled in the build stage either way.
70- - uses : docker/setup-qemu-action@v3
105+ - uses : docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
71106 if : matrix.platform != 'linux/amd64'
72107
73- - uses : docker/setup-buildx-action@v3
108+ - uses : docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
109+
110+ - uses : docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
111+ if : github.event_name != 'pull_request'
112+ with :
113+ registry : ghcr.io
114+ username : ${{ github.actor }}
115+ password : ${{ secrets.GITHUB_TOKEN }}
116+
117+ - name : Build this architecture and push it by digest
118+ id : pushed
119+ if : github.event_name != 'pull_request'
120+ uses : docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
121+ with :
122+ context : .
123+ platforms : ${{ matrix.platform }}
124+ outputs : type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
74125
126+ - name : Take the image under test back out of the registry
127+ if : github.event_name != 'pull_request'
128+ run : |
129+ reference="${IMAGE}@${{ steps.pushed.outputs.digest }}"
130+ docker pull --platform '${{ matrix.platform }}' "$reference"
131+ echo "IMAGE_UNDER_TEST=$reference" >> "$GITHUB_ENV"
132+
133+ # A pull request publishes nothing, so there is nothing to push by digest
134+ # and no token to do it with on a fork. Built and kept locally instead.
75135 - name : Build the image
76- uses : docker/build-push-action@v6
136+ if : github.event_name == 'pull_request'
137+ uses : docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
77138 with :
78139 context : .
79140 platforms : ${{ matrix.platform }}
80141 push : false
81142 load : true
82143 tags : flux-volume-tools:test
83144
145+ - name : Name the image under test
146+ if : github.event_name == 'pull_request'
147+ run : echo "IMAGE_UNDER_TEST=flux-volume-tools:test" >> "$GITHUB_ENV"
148+
84149 # -count=1 because the image is an input the test cache cannot see: with a
85150 # warm cache the arm64 run would report the amd64 result and pass without
86151 # ever starting a container.
87152 - name : Assert what the image does
153+ env :
154+ FLUX_VOLUME_TOOLS_IMAGE : ${{ env.IMAGE_UNDER_TEST }}
88155 run : go test -tags docker -count=1 ./test/container/ -v
89156
157+ - name : Keep the digest that passed
158+ if : github.event_name != 'pull_request'
159+ run : |
160+ mkdir -p /tmp/digests
161+ digest='${{ steps.pushed.outputs.digest }}'
162+ touch "/tmp/digests/${digest#sha256:}"
163+
164+ - uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
165+ if : github.event_name != 'pull_request'
166+ with :
167+ name : digest-${{ matrix.arch }}
168+ path : /tmp/digests/*
169+ if-no-files-found : error
170+ retention-days : 1
171+
172+ # Assembles the manifest list from the digests that passed. This job builds
173+ # nothing: it only names images that already exist and have already been
174+ # tested, which is what makes the published image the tested one.
90175 publish :
91176 needs : [unit, image]
92177 if : github.event_name != 'pull_request'
93178 runs-on : ubuntu-latest
94179 steps :
95- - uses : actions/checkout@v4
96-
97- - uses : docker/setup-qemu-action@v3
180+ - uses : actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
181+ with :
182+ path : /tmp/digests
183+ pattern : digest-*
184+ merge-multiple : true
98185
99- - uses : docker/setup-buildx-action@v3
186+ - uses : docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
100187
101- - uses : docker/login-action@v3
188+ - uses : docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
102189 with :
103190 registry : ghcr.io
104191 username : ${{ github.actor }}
105192 password : ${{ secrets.GITHUB_TOKEN }}
106193
107194 - id : meta
108- uses : docker/metadata-action@v5
195+ uses : docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
109196 with :
110197 images : ${{ env.IMAGE }}
111198 tags : |
112199 type=ref,event=tag
113200 type=raw,value=latest,enable={{is_default_branch}}
114201
115- - id : build
116- uses : docker/build-push-action@v6
117- with :
118- context : .
119- platforms : linux/amd64,linux/arm64
120- push : true
121- tags : ${{ steps.meta.outputs.tags }}
122- labels : ${{ steps.meta.outputs.labels }}
123-
124- # This digest is the value FluxOS pins. It is the manifest LIST digest, so it
125- # resolves to the right architecture on both x86 and arm nodes .
126- - name : Publish digest to run summary
202+ - name : Assemble the manifest list from the digests that passed
203+ working-directory : /tmp/digests
204+ run : |
205+ docker buildx imagetools create \
206+ $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
207+ $(printf "${IMAGE}@sha256:%s " *)
208+
209+ # Everything FluxOS has to write down, in one place. The pin is a tag AND
210+ # a per-architecture image id, and the id is the digest of the image's own
211+ # CONFIG rather than of its manifest - so the manifest list digest alone,
212+ # which is what this job used to print, is not enough to fill it in .
213+ - name : Write the pin to the run summary
127214 run : |
215+ reference="${IMAGE}:${{ steps.meta.outputs.version }}"
128216 {
129217 echo '## Published'
130218 echo
131- echo 'Pin this in FluxOS :'
219+ echo 'ZelBack/config/volumeToolsImage.json :'
132220 echo
133- echo '```'
134- echo "${{ env.IMAGE }}@${{ steps.build.outputs.digest }}"
221+ echo '```json'
222+ echo '{'
223+ echo " \"image\": \"${reference}\","
224+ echo ' "imageIds": {'
225+ for arch in amd64 arm64; do
226+ manifest="$(docker buildx imagetools inspect "$reference" --raw |
227+ jq -r --arg a "$arch" '.manifests[] | select(.platform.architecture == $a and .platform.os == "linux") | .digest')"
228+ config="$(docker buildx imagetools inspect "${IMAGE}@${manifest}" --raw | jq -r '.config.digest')"
229+ comma=','
230+ [ "$arch" = arm64 ] && comma=''
231+ echo " \"${arch}\": \"${config}\"${comma}"
232+ done
233+ echo ' }'
234+ echo '}'
135235 echo '```'
136236 } >> "$GITHUB_STEP_SUMMARY"
0 commit comments