Repository navigation
1074 lines (1015 loc) · 53.5 KB
/
Copy pathdocker.yml
File metadata and controls
1074 lines (1015 loc) · 53.5 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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: Build and Push Docker Image
on:
push:
# Run on every branch so the required `gate` context attaches to every
# push, but only long-lived branches BUILD and PUSH to GHCR — see the
# "Push policy" step below. Feature branches used to build all six images
# here as a compile gate; that duplicated v2-ci.yml's `docker` job (which
# builds every Dockerfile on pull_request) and cost ~20 hosted-runner
# minutes per push, starving the shared ubuntu-latest queue so the LAST
# check on a PR landed 25-30 minutes after the tests finished. Exclude bot
# branches so dependency PRs don't consume image CI; they build on the base
# branch when merged.
branches:
- '**'
- '!dependabot/**'
- '!copilot/**'
# `gate` is the ONLY status context v4 branch protection requires, and a
# push-only workflow cannot produce it for a pull request from a FORK: the
# push event fires in the contributor's repo, not here, so `gate` never
# attaches to the head SHA this repo evaluates and protection blocks the
# merge waiting for a check that cannot arrive (#4965). Six green fork PRs
# were structurally unmergeable; maintainers never saw it because
# enforce_admins is false, so an admin merge silently bypasses the same
# missing check.
#
# This trigger exists to produce that one context. It deliberately runs
# NOTHING else on a pull request — every build job below is skipped for this
# event — so it adds no image CI. The heavyweight builds stay push-only,
# exactly as the comment above describes.
#
# No `paths:` or `branches:` filter, on purpose. A required context must
# attach to EVERY pull request; a filtered one silently never runs on the
# changes it does not match, and protection then waits forever. That is also
# why the already PR-triggered `build-and-test` is not a drop-in replacement
# for `gate` in the required list — v2-ci.yml path-filters it.
#
# Leaving the filter off also keeps this workflow's `unpinned: '**'` contract
# in .github/release-lines.yml intact: that guard requires every `branches:`
# filter here to stay a wildcard, so a fixed base-branch list would fail it.
pull_request:
workflow_dispatch:
inputs:
release_sha:
description: >-
Exact release commit to build. Tagged Release supplies this after its
GITHUB_TOKEN-authenticated merge because that merge cannot fire the
push trigger (#6380). Leave empty for an ordinary manual branch build.
required: false
type: string
release_version:
description: >-
Exact git tag being published for release_sha (for example v5.35.2).
Tagged Release supplies this so release images self-report the tag
even though the tag is pushed after the image build is dispatched.
required: false
type: string
# Serialize Docker builds per branch so concurrent merges don't race. Do NOT
# cancel in-progress builds: on a fast-merging release branch (v2 saw ~34 merges
# in 2h), cancel-in-progress meant every build was killed by the next merge and
# NONE ever completed, freezing <branch>-latest and blocking all deploys. With
# cancel-in-progress:false the builds queue and each runs to completion. Each
# completed build publishes an immutable SHA tag; publish-image-tags.sh uses a
# per-tag workflow generation to advance moving tags without either starving
# during a merge burst or regressing when queued runs execute out of order.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
packages: write
jobs:
# Decide once whether this run pushes images to GHCR, and expose it as an
# output every other job consumes (needs.gate.outputs.push). Only long-lived
# branches that a hive can be assigned to via the hub's branch switcher push:
# the release lines and standing experimental branches. Every OTHER branch
# still BUILDS (the image is a per-PR CI gate) but does not push — otherwise a
# temporary PR branch leaves an orphaned <branch>-latest tag on GHCR when it
# merges and is deleted. workflow_dispatch always pushes so a throwaway branch
# can be published for a hive on demand before merge.
#
# EXCEPTION: `release-gate/*` (#5072). tagged-release.yml pushes its
# release commit to a throwaway `release-gate/v<version>` branch to earn a
# `gate` check on
# that exact SHA before pushing to protected `v4` (see docs/releases.md,
# "Satisfying branch protection"). That push uses the job's default
# GITHUB_TOKEN, and GitHub's documented recursive-workflow guard means a
# GITHUB_TOKEN push never fires another workflow's `push` trigger — so
# tagged-release.yml has to fall back to explicitly dispatching this
# workflow via `gh workflow run docker.yml --ref release-gate/v<version>`
# instead. Without
# this exception, that dispatch would hit the `workflow_dispatch` branch
# below and force push=true on a branch nothing should ever publish images
# for: a full two-platform GHCR push under a one-off scratch branch name,
# solely to obtain a status check that only needs `gate` (a five-second job)
# to run. `release-gate/*` branches are deliberately never in LONG_LIVED and
# are deleted immediately after use (tagged-release.yml's `trap ... EXIT`),
# so this
# exception cannot leave a stray moving tag behind — it only ever prevents
# one from being created in the first place.
#
# The long-lived branch set is defined ONCE below (LONG_LIVED). To add or
# remove one, edit only that list — the whole workflow derives from this job's
# output, so nothing else changes.
#
# It is not a trigger filter, so it hand-writes the release lines a second
# time, and going stale here fails GREEN: the image would keep building on a
# new release line and never publish its <branch>-latest tag (#4462). The
# release-line guard asserts this list against .github/release-lines.yml —
# see the `env_lists` entry there and src/docs/release-line-guard.md.
#
# NOTE (#5339/#5356): do not special-case `release-gate/*` pull requests in
# this job. GITHUB_TOKEN-opened release PR runs may be recursion-blocked
# before any job starts, and workflow_dispatch check-runs are not associated
# with a PR even when dispatched after it exists. tagged-release.yml handles
# that release-only gap by mirroring its verified check-run as a SHA-scoped
# commit status. Ordinary pull requests still need this job unchanged so
# their head SHA receives the required `gate` check-run (#4965).
gate:
# Required on v5: keep on the self-hosted pool so a GitHub-hosted runner
# outage does not make otherwise-mergeable PRs impossible to merge.
runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON(vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') || 'ubuntu-latest' }}
timeout-minutes: 10
outputs:
push: ${{ steps.decide.outputs.push }}
sha: ${{ steps.decide.outputs.sha }}
release_version: ${{ steps.decide.outputs.release_version }}
ref_name: ${{ steps.decide.outputs.ref_name }}
cross_org: ${{ steps.registries.outputs.cross_org }}
cross_secret: ${{ steps.registries.outputs.cross_secret }}
steps:
- name: Decide push policy
id: decide
env:
# v5 added (RFC #4000/#4001/#4002 implementation line, branched from v4
# HEAD): without it here the '**' trigger above still BUILDS v5 as a CI
# gate, but never PUSHES, so v5-latest would never exist on GHCR. This
# Channel ownership (#7721 Phase 1): v5 publishes candidate + :latest
# (stable moves only via promote-stable.yml after soak), v6 publishes
# edge, v4 is maintenance and publishes no channel — see the
# RELEASE_BRANCH/CHANNELS args on each publish step below.
LONG_LIVED: "v4 v5 mk dd v6"
EVENT: ${{ github.event_name }}
TARGET_SHA: ${{ inputs.release_sha || github.sha }}
RELEASE_VERSION: ${{ inputs.release_version || '' }}
run: |
set -euo pipefail
: "${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}"
REF_NAME=$GITHUB_REF_NAME
if ! [[ "$TARGET_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::release_sha must be a full 40-character lowercase commit SHA, got '${TARGET_SHA}'" >&2
exit 1
fi
if [ -n "$RELEASE_VERSION" ] && ! [[ "$RELEASE_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::release_version must be a v-prefixed semver tag, got '${RELEASE_VERSION}'" >&2
exit 1
fi
push=false
case "$REF_NAME" in
release-gate/*)
# See the EXCEPTION comment above this job: this scratch branch
# only ever exists to earn a `gate` check for
# tagged-release.yml, never to publish an image, regardless of
# trigger event.
push=false
;;
*)
if [ "$EVENT" = "workflow_dispatch" ]; then
push=true
else
for b in $LONG_LIVED; do
if [ "$b" = "$REF_NAME" ]; then push=true; break; fi
done
fi
;;
esac
echo "push=$push" >> "$GITHUB_OUTPUT"
echo "sha=$TARGET_SHA" >> "$GITHUB_OUTPUT"
echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
echo "ref_name=$REF_NAME" >> "$GITHUB_OUTPUT"
echo "Push to GHCR: $push (branch=$REF_NAME sha=$TARGET_SHA event=$EVENT release_version=${RELEASE_VERSION:-<git describe>})"
# #6419: well-formed 40-hex is NOT "reachable from this branch". Without
# this, any principal with `actions: write` could dispatch
# `release_sha=<any commit object in the repo>` — including an
# unreviewed PR head — and every job below would build and publish it
# under the dispatched branch's moving tags (`<branch>-latest`,
# `candidate`, ...), laundered through the normal pipeline with
# legitimate provenance labels. tagged-release.yml, the only intended
# caller, already runs this exact `git merge-base --is-ancestor` check
# before it ever tags a commit (see its "push_v4" step) — the gate job
# re-verified nothing on the receiving end. Only fetch full history when
# a release_sha was actually supplied; the ordinary push/PR path (99% of
# runs) pays nothing extra.
- name: Checkout dispatched ref for release_sha ancestry check (#6419)
if: inputs.release_sha != ''
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Verify release_sha is an ancestor of the dispatched branch (#6419)
if: inputs.release_sha != ''
env:
TARGET_SHA: ${{ inputs.release_sha }}
run: bash src/scripts/check-release-sha-ancestry.sh "$TARGET_SHA" HEAD .
# DUAL-PUBLISH (kubestellar -> hivecommons org transfer). During the
# transfer window every image publish is mirrored into the OTHER org's
# GHCR namespace, so a hive pinned to either registry path keeps
# resolving fresh tags no matter which side of the transfer the repo is
# on. The NATIVE org is wherever this repo currently lives
# (github.repository_owner — the only org this job's GITHUB_TOKEN can
# write packages to); the CROSS org and the classic-PAT secret that can
# write to it are derived here once, direction-agnostically, so the
# workflow needs no edit when the transfer actually happens:
# owner=kubestellar -> cross=hivecommons, secret GHCR_HIVECOMMONS_PAT
# owner=hivecommons -> cross=kubestellar, secret GHCR_KUBESTELLAR_PAT
# The merge jobs consume these outputs. Whether a MISSING secret is a
# warning or a hard failure is decided there — see the asymmetry
# comment on each "Check cross-registry credential" step.
- name: Decide cross-registry mirror target
id: registries
env:
OWNER: ${{ github.repository_owner }}
run: |
set -euo pipefail
if [ "$OWNER" = "kubestellar" ]; then
cross_org=hivecommons
cross_secret=GHCR_HIVECOMMONS_PAT
else
cross_org=kubestellar
cross_secret=GHCR_KUBESTELLAR_PAT
fi
echo "cross_org=${cross_org}" >> "$GITHUB_OUTPUT"
echo "cross_secret=${cross_secret}" >> "$GITHUB_OUTPUT"
echo "Native org: ${OWNER}; mirroring publishes to ghcr.io/${cross_org} using secret ${cross_secret}."
build:
needs: gate
# Runs when `gate` decided push=true, i.e. a long-lived branch or a
# workflow_dispatch release, and on same-repo PRs so Dockerfile/platform
# changes prove both release architectures before merge. Fork PRs still
# get only the required `gate` context here (#4965); they cannot safely use
# the self-hosted fleet. Feature-branch pushes are skipped because the PR
# event supplies the image compile gate.
# `release-gate/*` is excluded for the same reason `merge*` already skips
# it (gate forces push=false there): these builds publish NOTHING for the
# scratch branch. They are not merely wasted — branch protection evaluates
# the whole check SUITE, so while any job in it runs the `gate` context is
# not treated as satisfied even though its check-run is already `success`.
# The release job's merge waits 120s and a multi-arch build takes ~10min,
# so the merge could never win that race (#5339, 8th recurrence).
#
# No coverage is lost. A release commit modifies CHANGELOG.md and nothing
# else — no source, no Dockerfile — so its tree is byte-identical to the v4
# tip whose images this workflow already built, published and freshness-
# checked minutes earlier. The `gate` job itself still runs on the scratch
# branch; that is the whole reason the branch exists.
# push=true already implies not release-gate/* (gate forces push=false
# there); the explicit clause is kept because test-release-push-retry.sh
# asserts it (#5339) and it makes the intent readable.
if: >-
(needs.gate.outputs.push == 'true' ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository)) &&
!startsWith(github.ref, 'refs/heads/release-gate/')
strategy:
matrix:
include:
- platform: linux/amd64
self_hosted: true
qemu: false
timeout_minutes: 60
- platform: linux/arm64
self_hosted: true
qemu: true
timeout_minutes: 75
runs-on: ${{ matrix.qemu && fromJSON(vars.HIVE_ARM64_RUNNER || vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') || ((github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON(vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') || 'ubuntu-latest') }}
timeout-minutes: ${{ matrix.timeout_minutes }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.gate.outputs.sha }}
# fetch-depth: 0 brings the full tag history into the .git this job
# copies into the image build, so src/Dockerfile's
# `git describe --tags` can stamp a real semver (#7092).
fetch-depth: 0
fetch-tags: true
- name: Set up QEMU for arm64 emulation
if: matrix.qemu
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with:
driver-opts: network=host
- name: Log in to GHCR
if: needs.gate.outputs.push == 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Platform slug
id: slug
run: echo "slug=${PLATFORM//\//-}" >> "$GITHUB_OUTPUT"
env:
PLATFORM: ${{ matrix.platform }}
# NOTE on Go compile caching (removed actions/cache transport, #4114):
# measurement showed the Go compile in this image is only ~25-50s, and
# the inject/extract transport never worked — every extract found the
# cache mounts empty (each actions/cache save was 273 bytes), so every
# "warm" run was actually cold while paying the transport's fixed cost.
# src/Dockerfile keeps its BuildKit cache mounts (free, and a no-op when
# empty) so any future runner-local BuildKit state still benefits, but we
# no longer round-trip them through actions/cache.
#
# LAYER CACHE STRATEGY: `no-cache-filters: builder,runtime` (the #3760
# stale-binary defense, see below) forces every layer of the builder and
# runtime stages to re-execute on every build — so a whole-image
# `cache-to: type=gha,mode=max` export bought almost nothing while
# costing 2-4 minutes per job to upload multi-GB layer sets. The ONLY
# stage that can legitimately be served from cache is `tmux-builder`
# (pinned base image + pinned tmux tarball; not listed in
# no-cache-filters). Warm exactly that stage: a targeted `--target
# tmux-builder` build seeds the builder's local layer cache from a small
# dedicated gha scope (and refreshes it with mode=max — cheap, tmux-stage
# layers only). The main build then hits those layers locally and does
# NO cache export at all. continue-on-error: pure optimization — if the
# gha cache service hiccups, the main build just recompiles tmux.
- name: Warm tmux-builder layer cache
continue-on-error: true
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: src/Dockerfile
platforms: ${{ matrix.platform }}
target: tmux-builder
provenance: false
sbom: false
cache-from: type=gha,scope=tmux-${{ steps.slug.outputs.slug }}
cache-to: type=gha,scope=tmux-${{ steps.slug.outputs.slug }},mode=max
- name: Build and push by digest
id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: src/Dockerfile
platforms: ${{ matrix.platform }}
build-args: |
GIT_HASH=${{ needs.gate.outputs.sha }}
GIT_BRANCH=${{ needs.gate.outputs.ref_name }}
VERSION=${{ needs.gate.outputs.release_version }}
# Monotonic moving-tag guard metadata (#4804). This config label is
# inherited by each platform manifest without changing its media type.
labels: |
io.kubestellar.hive.github-actions-run-number=${{ github.run_number }}
org.opencontainers.image.revision=${{ needs.gate.outputs.sha }}
# STALE-BINARY FIX (unblocks the #3760 fix in the SHIPPED image).
# `no-cache-filters: builder` alone rebuilt
# a fresh /hive in the `builder` stage but let BuildKit serve the FINAL
# (`runtime`) stage's `COPY --from=builder /hive` layer AND the
# subsequent fresh-inode rewrite RUN from that persistent cache — so the
# rebuilt binary was thrown away and the OLD binary (e.g. commit
# 4d9c11c) shipped even though the manifest digest advanced. Listing the
# `runtime` stage too forces those binary-bearing layers to re-run every
# build, so the published /usr/local/bin/hive always matches HEAD.
no-cache-filters: builder,runtime
# #3760: DO NOT attach provenance/SBOM attestations. build-push-action
# adds them by DEFAULT, and an attestation can only be carried by an
# OCI image *index* — so the per-arch digest we push becomes
# application/vnd.oci.image.index.v1+json instead of a plain image
# manifest. That is exactly the OCI-index form the reporter measured on
# the crash-looping image (his working fork build, same v4 code, is a
# plain gzip-layer manifest and execs everywhere). The attestation/OCI-
# index build path is what lets a COPY --from layer ship an overlayfs
# metacopy redirect for /usr/local/bin/hive, which containerd/k3s and
# rootless podman present as non-executable at the image path until
# copy-up (a plain `cp` to any other path clears it). Forcing plain
# image manifests removes that trigger. See the in-final-stage rewrite
# in src/Dockerfile for the belt-and-suspenders defense.
provenance: false
sbom: false
# Push the per-arch digest only for long-lived branches; on a PR
# branch build without pushing so the image is still validated (CI
# gate) but nothing lands on GHCR.
# The image name is owner-derived (dual-publish, see the gate job):
# GITHUB_TOKEN can only push packages in the org that hosts the
# repo, so the build always targets the NATIVE org and the merge
# job mirrors the finished manifest list into the cross org.
outputs: type=image,name=ghcr.io/${{ github.repository_owner }}/hive,push-by-digest=true,name-canonical=true,push=${{ needs.gate.outputs.push }},oci-mediatypes=false
# tmux-builder layers come from the local cache warmed above; every
# other stage is forced to re-execute by no-cache-filters, so there
# is nothing worth exporting — deliberately NO cache-to here (the
# whole-image mode=max export measured 130-225s per job for ~0 reuse)
cache-from: type=gha,scope=tmux-${{ steps.slug.outputs.slug }}
# FRESHNESS ASSERTION (#3760 regression guard): run the image this job
# just produced and assert the ldflags-embedded commit equals the commit
# being built. `hive --version` prints "hive 3.0.0 (commit <short7>,
# branch <b>)" where <short7> is baked from /repo/.git at compile time.
#
# - push builds: probe the EXACT pushed artifact by digest — the most
# direct possible evidence that what landed on GHCR is fresh.
# - PR builds (push=false): nothing is pushed and push-by-digest output
# doesn't load into the local daemon, so re-run the build with --load.
# On this same builder every layer of the forced-fresh build above is
# in local cache, so the rebuild is seconds and yields those exact
# layers. If BuildKit ever resolved a STALE binary layer instead, the
# embedded hash would differ from the validated target SHA and this step fails loudly
# — a stale layer can make this check fail, never silently pass.
- name: Assert embedded commit matches build (freshness guard)
env:
EXPECTED_SHA: ${{ needs.gate.outputs.sha }}
REF_NAME: ${{ needs.gate.outputs.ref_name }}
run: |
set -euo pipefail
if [ "${{ needs.gate.outputs.push }}" = "true" ]; then
IMG="ghcr.io/${{ github.repository_owner }}/hive@${{ steps.build.outputs.digest }}"
docker pull "$IMG"
else
IMG=hive-freshness-probe:local
docker buildx build --load \
-f src/Dockerfile \
--build-arg GIT_HASH="$EXPECTED_SHA" \
--build-arg GIT_BRANCH="$REF_NAME" \
--build-arg VERSION="${{ needs.gate.outputs.release_version }}" \
-t "$IMG" .
fi
ACTUAL=$(docker run --rm --entrypoint /usr/local/bin/hive "$IMG" --version)
echo "image reports: $ACTUAL"
echo "expected commit: ${EXPECTED_SHA::7} (from $EXPECTED_SHA)"
case "$ACTUAL" in
*"commit ${EXPECTED_SHA::7},"*)
echo "Freshness OK: embedded commit matches the built SHA"
;;
*)
echo "::error::STALE BINARY: image reports '$ACTUAL' but this build is $EXPECTED_SHA. The published/validated image does not contain the code of this commit."
exit 1
;;
esac
# SECURITY (#3866, CWE-250/732): assert the REAL built image ships exactly
# one setuid/setgid binary, the su-exec launcher helper.
#
# src/deploy/test_image_suid_inventory.sh runs this same assertion against
# a fixture derived from src/Dockerfile on every PR that touches it (the
# image-suid-inventory job in suid-contract.yml). That fixture covers the
# base + apt layers, which is where the eleven world-executable
# setuid/setgid binaries came from. This copy is the one that sees the
# WHOLE image — every npm, pip and COPY layer included — so nothing can
# slip a setuid bit in below the layers the fixture reproduces.
#
# The image is already local here: pushed builds pulled it by digest in
# the step above, PR builds loaded it as hive-freshness-probe:local.
- name: Assert image SUID/SGID inventory
run: |
set -euo pipefail
if [ "${{ needs.gate.outputs.push }}" = "true" ]; then
IMG="ghcr.io/${{ github.repository_owner }}/hive@${{ steps.build.outputs.digest }}"
else
IMG=hive-freshness-probe:local
fi
bash src/deploy/test_image_suid_inventory.sh --image "$IMG"
- name: Export digest
if: needs.gate.outputs.push == 'true'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
if: needs.gate.outputs.push == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: digests-${{ steps.slug.outputs.slug }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# The merge job exists only to create and push the <branch>-latest / <sha>
# manifest tags, which only happens for long-lived branches. On a PR branch
# the build job above still runs (validating the image builds) and this job
# is skipped entirely.
merge:
if: needs.gate.outputs.push == 'true'
runs-on: ${{ fromJSON(vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') }}
timeout-minutes: 20
needs: [gate, build]
steps:
- name: Checkout tag publisher
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.gate.outputs.sha }}
- name: Download digests
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with:
driver-opts: network=host
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
env:
REF_NAME: ${{ needs.gate.outputs.ref_name }}
GATE_SHA: ${{ needs.gate.outputs.sha }}
RUN_NUMBER: ${{ github.run_number }}
run: |
src/scripts/publish-image-tags.sh \
ghcr.io/${{ github.repository_owner }}/hive /tmp/digests \
"$REF_NAME" "$GATE_SHA" "$RUN_NUMBER" \
v5 true candidate
# ---- Cross-org mirror (kubestellar <-> hivecommons dual-publish) ----
# Copy the tags just published above into the other org's GHCR
# namespace BY DIGEST (`imagetools create -t <cross>:<tag>
# <native>@<digest>`): both registries then serve the byte-identical,
# digest-identical manifest list — no second build, no drift. The gate
# job computed which org is "cross" and which secret can write to it.
#
# ASYMMETRY of the missing-secret handling, on purpose:
# - owner=hivecommons (post-transfer): a missing PAT would mean the
# OLD kubestellar registry silently stops receiving images while
# every spoke still pinned there keeps pulling stale tags — the
# dangerous one-sided publish — so it is a hard ::error failure.
# - owner=kubestellar (pre-transfer staging): the PAT may simply not
# exist yet; the mirror is purely additive and nothing that exists
# today degrades, so it is only a ::warning and the run stays green.
- name: Check cross-registry credential
id: crosspat
env:
# Env indirection: `if:` cannot read the secrets context, and the
# secret NAME itself is computed by the gate job.
CROSS_PAT: ${{ secrets[needs.gate.outputs.cross_secret] }}
CROSS_ORG: ${{ needs.gate.outputs.cross_org }}
CROSS_SECRET_NAME: ${{ needs.gate.outputs.cross_secret }}
OWNER: ${{ github.repository_owner }}
run: |
set -euo pipefail
if [ -n "$CROSS_PAT" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "available=false" >> "$GITHUB_OUTPUT"
if [ "$OWNER" = "hivecommons" ]; then
echo "::error::secret ${CROSS_SECRET_NAME} is not set, so this run cannot mirror to ghcr.io/${CROSS_ORG}. Post-transfer that is a silent one-sided publish (spokes pinned to the old org keep pulling stale tags), so this fails hard. Create a classic PAT with write:packages from a ${CROSS_ORG} member and store it as ${CROSS_SECRET_NAME}."
exit 1
fi
echo "::warning::secret ${CROSS_SECRET_NAME} is not set — skipping the ghcr.io/${CROSS_ORG} mirror. Pre-transfer this is expected until that PAT is created; nothing that exists today degrades."
# ghcr.io is ONE registry host and docker keeps one credential per
# host, so this login REPLACES the GITHUB_TOKEN login above. Every
# native-org write is already done (the publish step above was the
# last one). Reading the native image back for the digest copy still
# works: these packages are public, and the PAT is a valid ghcr.io
# credential for public reads regardless of org.
- name: Log in to GHCR as the cross-org publisher
if: steps.crosspat.outputs.available == 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets[needs.gate.outputs.cross_secret] }}
- name: Mirror published tags into the cross-org registry (by digest)
if: steps.crosspat.outputs.available == 'true'
env:
NATIVE_IMAGE: ghcr.io/${{ github.repository_owner }}/hive
CROSS_IMAGE: ghcr.io/${{ needs.gate.outputs.cross_org }}/hive
BRANCH: ${{ needs.gate.outputs.ref_name }}
GIT_SHA: ${{ needs.gate.outputs.sha }}
# These three mirror publish-image-tags.sh's positional args in the
# publish step above — keep them in lockstep with that call.
INCLUDE_LATEST: "true"
RELEASE_BRANCH: "v5"
CHANNELS: "candidate"
run: |
set -euo pipefail
# Same tag set publish-image-tags.sh manages for this image. Each
# tag is copied from whatever the NATIVE tag currently points at,
# so the monotonic moving-tag guard's decisions carry over: the
# cross org can never regress a tag the native org did not.
branch_tag="${BRANCH//\//-}"
tags=("${GIT_SHA:0:7}" "${branch_tag}-latest")
if [ "$INCLUDE_LATEST" = "true" ]; then tags+=("latest"); fi
if [ "$BRANCH" = "$RELEASE_BRANCH" ] && [ -n "$CHANNELS" ]; then
IFS=, read -ra channel_tags <<<"$CHANNELS"
for ch in "${channel_tags[@]}"; do [ -n "$ch" ] && tags+=("$ch"); done
fi
for tag in "${tags[@]}"; do
if ! digest=$(docker buildx imagetools inspect \
"${NATIVE_IMAGE}:${tag}" --format '{{.Manifest.Digest}}' 2>&1); then
# Distinguish "tag does not exist" (fine: e.g. the monotonic
# guard published nothing this run) from a transport/auth
# failure (red, same policy as publish-image-tags.sh).
if grep -Eqi 'manifest unknown|manifest.*not found|not found.*manifest|no such manifest|(^|[[:space:]:])not found$' <<<"$digest"; then
echo "native tag ${NATIVE_IMAGE}:${tag} does not exist — nothing to mirror for it"
continue
fi
echo "::error::could not inspect ${NATIVE_IMAGE}:${tag}; refusing to silently skip a mirror on a transport/auth failure" >&2
echo "$digest" >&2
exit 1
fi
echo "Mirroring ${CROSS_IMAGE}:${tag} <- ${NATIVE_IMAGE}@${digest}"
docker buildx imagetools create -t "${CROSS_IMAGE}:${tag}" "${NATIVE_IMAGE}@${digest}"
done
build-contributor:
needs: gate
# Only when gate says push=true, same as `build` above (#4965).
# `release-gate/*` is excluded for the same reason `merge*` already skips
# it (gate forces push=false there): these builds publish NOTHING for the
# scratch branch. They are not merely wasted — branch protection evaluates
# the whole check SUITE, so while any job in it runs the `gate` context is
# not treated as satisfied even though its check-run is already `success`.
# The release job's merge waits 120s and a multi-arch build takes ~10min,
# so the merge could never win that race (#5339, 8th recurrence).
# push=true already implies not release-gate/* (gate forces push=false
# there); the explicit clause is kept because test-release-push-retry.sh
# asserts it (#5339) and it makes the intent readable.
if: >-
(needs.gate.outputs.push == 'true' ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository)) &&
!startsWith(github.ref, 'refs/heads/release-gate/')
strategy:
matrix:
include:
- platform: linux/amd64
self_hosted: true
qemu: false
timeout_minutes: 60
- platform: linux/arm64
self_hosted: true
qemu: true
timeout_minutes: 75
runs-on: ${{ matrix.qemu && fromJSON(vars.HIVE_ARM64_RUNNER || vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') || ((github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON(vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') || 'ubuntu-latest') }}
timeout-minutes: ${{ matrix.timeout_minutes }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.gate.outputs.sha }}
- name: Set up QEMU for arm64 emulation
if: matrix.qemu
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with:
driver-opts: network=host
- name: Log in to GHCR
if: needs.gate.outputs.push == 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Platform slug
id: slug
run: echo "slug=${PLATFORM//\//-}" >> "$GITHUB_OUTPUT"
env:
PLATFORM: ${{ matrix.platform }}
- name: Resolve contributor base manifests with retry
shell: bash
env:
CONTRIBUTOR_BASE_IMAGES: |
mirror.gcr.io/library/golang:1.27-alpine@sha256:f92b6ef800e499660581efdabdf25d9d817a9d124eaf900924f0504e7e27e12d
mirror.gcr.io/library/debian:bookworm-slim@sha256:7c7b2c966bc9ee8cedfeef67e0e279108992c77681fa595db4a9d65c06ccc587
run: |
set -euo pipefail
for image in $CONTRIBUTOR_BASE_IMAGES; do
for attempt in 1 2 3; do
if docker buildx imagetools inspect "$image" >/dev/null; then
continue 2
fi
if [ "$attempt" -lt 3 ]; then
sleep 90
fi
done
docker buildx imagetools inspect "$image" >/dev/null
done
- name: Build and push contributor image by digest
id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: src/Dockerfile.contributor
platforms: ${{ matrix.platform }}
labels: |
io.kubestellar.hive.github-actions-run-number=${{ github.run_number }}
org.opencontainers.image.revision=${{ needs.gate.outputs.sha }}
# #3760: no default provenance/SBOM attestations → plain image
# manifests, not an OCI index. See the note on the main build above.
provenance: false
sbom: false
# Owner-derived image name (dual-publish) — see the main build step.
outputs: type=image,name=ghcr.io/${{ github.repository_owner }}/hive-contributor,push-by-digest=true,name-canonical=true,push=${{ needs.gate.outputs.push }},oci-mediatypes=false
cache-from: type=gha,scope=contributor-${{ steps.slug.outputs.slug }}
cache-to: type=gha,scope=contributor-${{ steps.slug.outputs.slug }},mode=max
- name: Export digest
if: needs.gate.outputs.push == 'true'
run: |
mkdir -p /tmp/contrib-digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/contrib-digests/${digest#sha256:}"
- name: Upload digest
if: needs.gate.outputs.push == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: contrib-digests-${{ steps.slug.outputs.slug }}
path: /tmp/contrib-digests/*
if-no-files-found: error
retention-days: 1
merge-contributor:
if: needs.gate.outputs.push == 'true'
runs-on: ${{ fromJSON(vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') }}
timeout-minutes: 20
needs: [gate, build-contributor]
steps:
- name: Checkout tag publisher
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.gate.outputs.sha }}
- name: Download digests
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
path: /tmp/contrib-digests
pattern: contrib-digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with:
driver-opts: network=host
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create contributor manifest list and push
env:
REF_NAME: ${{ needs.gate.outputs.ref_name }}
GATE_SHA: ${{ needs.gate.outputs.sha }}
RUN_NUMBER: ${{ github.run_number }}
run: |
src/scripts/publish-image-tags.sh \
ghcr.io/${{ github.repository_owner }}/hive-contributor /tmp/contrib-digests \
"$REF_NAME" "$GATE_SHA" "$RUN_NUMBER" \
v5 true candidate
# Cross-org mirror — same mechanics, same missing-secret asymmetry, and
# same "one credential per registry host" ordering constraint as the
# fully commented block in the `merge` job above.
- name: Check cross-registry credential
id: crosspat
env:
CROSS_PAT: ${{ secrets[needs.gate.outputs.cross_secret] }}
CROSS_ORG: ${{ needs.gate.outputs.cross_org }}
CROSS_SECRET_NAME: ${{ needs.gate.outputs.cross_secret }}
OWNER: ${{ github.repository_owner }}
run: |
set -euo pipefail
if [ -n "$CROSS_PAT" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "available=false" >> "$GITHUB_OUTPUT"
if [ "$OWNER" = "hivecommons" ]; then
echo "::error::secret ${CROSS_SECRET_NAME} is not set, so this run cannot mirror to ghcr.io/${CROSS_ORG}. Post-transfer that is a silent one-sided publish (spokes pinned to the old org keep pulling stale tags), so this fails hard. Create a classic PAT with write:packages from a ${CROSS_ORG} member and store it as ${CROSS_SECRET_NAME}."
exit 1
fi
echo "::warning::secret ${CROSS_SECRET_NAME} is not set — skipping the ghcr.io/${CROSS_ORG} mirror. Pre-transfer this is expected until that PAT is created; nothing that exists today degrades."
- name: Log in to GHCR as the cross-org publisher
if: steps.crosspat.outputs.available == 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets[needs.gate.outputs.cross_secret] }}
- name: Mirror published tags into the cross-org registry (by digest)
if: steps.crosspat.outputs.available == 'true'
env:
NATIVE_IMAGE: ghcr.io/${{ github.repository_owner }}/hive-contributor
CROSS_IMAGE: ghcr.io/${{ needs.gate.outputs.cross_org }}/hive-contributor
BRANCH: ${{ needs.gate.outputs.ref_name }}
GIT_SHA: ${{ needs.gate.outputs.sha }}
# Lockstep with the publish-image-tags.sh args above.
INCLUDE_LATEST: "true"
RELEASE_BRANCH: "v5"
CHANNELS: "candidate"
run: |
set -euo pipefail
branch_tag="${BRANCH//\//-}"
tags=("${GIT_SHA:0:7}" "${branch_tag}-latest")
if [ "$INCLUDE_LATEST" = "true" ]; then tags+=("latest"); fi
if [ "$BRANCH" = "$RELEASE_BRANCH" ] && [ -n "$CHANNELS" ]; then
IFS=, read -ra channel_tags <<<"$CHANNELS"
for ch in "${channel_tags[@]}"; do [ -n "$ch" ] && tags+=("$ch"); done
fi
for tag in "${tags[@]}"; do
if ! digest=$(docker buildx imagetools inspect \
"${NATIVE_IMAGE}:${tag}" --format '{{.Manifest.Digest}}' 2>&1); then
if grep -Eqi 'manifest unknown|manifest.*not found|not found.*manifest|no such manifest|(^|[[:space:]:])not found$' <<<"$digest"; then
echo "native tag ${NATIVE_IMAGE}:${tag} does not exist — nothing to mirror for it"
continue
fi
echo "::error::could not inspect ${NATIVE_IMAGE}:${tag}; refusing to silently skip a mirror on a transport/auth failure" >&2
echo "$digest" >&2
exit 1
fi
echo "Mirroring ${CROSS_IMAGE}:${tag} <- ${NATIVE_IMAGE}@${digest}"
docker buildx imagetools create -t "${CROSS_IMAGE}:${tag}" "${NATIVE_IMAGE}@${digest}"
done
build-hub:
needs: gate
# Only when gate says push=true, same as `build` above (#4965).
# `release-gate/*` is excluded for the same reason `merge*` already skips
# it (gate forces push=false there): these builds publish NOTHING for the
# scratch branch. They are not merely wasted — branch protection evaluates
# the whole check SUITE, so while any job in it runs the `gate` context is
# not treated as satisfied even though its check-run is already `success`.
# The release job's merge waits 120s and a multi-arch build takes ~10min,
# so the merge could never win that race (#5339, 8th recurrence).
# push=true already implies not release-gate/* (gate forces push=false
# there); the explicit clause is kept because test-release-push-retry.sh
# asserts it (#5339) and it makes the intent readable.
if: >-
(needs.gate.outputs.push == 'true' ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository)) &&
!startsWith(github.ref, 'refs/heads/release-gate/')
strategy:
matrix:
include:
- platform: linux/amd64
self_hosted: true
qemu: false
timeout_minutes: 60
- platform: linux/arm64
self_hosted: true
qemu: true
timeout_minutes: 75
runs-on: ${{ matrix.qemu && fromJSON(vars.HIVE_ARM64_RUNNER || vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') || ((github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON(vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') || 'ubuntu-latest') }}
timeout-minutes: ${{ matrix.timeout_minutes }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.gate.outputs.sha }}
# fetch-depth: 0 brings the full tag history into the .git this job
# copies into the image build, so src/Dockerfile.hub's
# `git describe --tags` can stamp a real semver (#7092).
fetch-depth: 0
fetch-tags: true
- name: Set up QEMU for arm64 emulation
if: matrix.qemu
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with:
driver-opts: network=host
- name: Log in to GHCR
if: needs.gate.outputs.push == 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Platform slug
id: slug
run: echo "slug=${PLATFORM//\//-}" >> "$GITHUB_OUTPUT"
env:
PLATFORM: ${{ matrix.platform }}
- name: Build and push hub image by digest
id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: src/Dockerfile.hub
platforms: ${{ matrix.platform }}
build-args: |
GIT_HASH=${{ needs.gate.outputs.sha }}
GIT_BRANCH=${{ needs.gate.outputs.ref_name }}
VERSION=${{ needs.gate.outputs.release_version }}
labels: |
io.kubestellar.hive.github-actions-run-number=${{ github.run_number }}
org.opencontainers.image.revision=${{ needs.gate.outputs.sha }}
# STALE-BINARY FIX (see the main build step). The hub image also does
# `COPY --from=builder /hive` into a `runtime` final stage, and its GHA
# cache scope (hub-<slug>) is likewise branch-persistent — so without
# busting `runtime` the hub could ship an old hive that self-upgrades
# against the wrong SHA. Bust both binary-bearing stages.
no-cache-filters: builder,runtime
# #3760: no default provenance/SBOM attestations → plain image
# manifests, not an OCI index. See the note on the main build above.
provenance: false
sbom: false
# Owner-derived image name (dual-publish) — see the main build step.
outputs: type=image,name=ghcr.io/${{ github.repository_owner }}/hive-hub,push-by-digest=true,name-canonical=true,push=${{ needs.gate.outputs.push }},oci-mediatypes=false
cache-from: type=gha,scope=hub-${{ steps.slug.outputs.slug }}
cache-to: type=gha,scope=hub-${{ steps.slug.outputs.slug }},mode=max
- name: Export digest
if: needs.gate.outputs.push == 'true'
run: |
mkdir -p /tmp/hub-digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/hub-digests/${digest#sha256:}"
- name: Upload digest
if: needs.gate.outputs.push == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: hub-digests-${{ steps.slug.outputs.slug }}
path: /tmp/hub-digests/*
if-no-files-found: error
retention-days: 1
merge-hub:
if: needs.gate.outputs.push == 'true'
runs-on: ${{ fromJSON(vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') }}
timeout-minutes: 20
needs: [gate, build-hub]
steps:
- name: Checkout tag publisher
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.gate.outputs.sha }}
- name: Download digests
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
path: /tmp/hub-digests
pattern: hub-digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with:
driver-opts: network=host
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create hub manifest list and push
env: