44 push :
55 branches :
66 - main
7- # Manual backfill: publish an already-cut release to npm (e.g. after the
8- # npm side of trusted publishing is configured, or a publish was missed).
9- # Only the npm-publish job runs; binaries are downloaded from the existing
10- # GitHub release for the given tag.
7+ # Manual backfill: republish an already-cut release to a channel that
8+ # missed it. Fill in only the channel(s) you want — an empty input leaves
9+ # that channel's jobs skipped. npm reuses the existing release's binary
10+ # assets; docker rebuilds the image from the tag and re-tags the manifest .
1111 workflow_dispatch :
1212 inputs :
1313 npm_backfill_tag :
1414 description : " Existing release tag to publish to npm (e.g. v0.6.0)"
15- required : true
15+ required : false
16+ type : string
17+ docker_backfill_tag :
18+ description : " Existing release tag to publish to ghcr (e.g. v0.6.0)"
19+ required : false
1620 type : string
1721
1822concurrency :
@@ -44,24 +48,57 @@ jobs:
4448 with :
4549 token : ${{ secrets.GITHUB_TOKEN }}
4650
51+ # Each architecture builds NATIVELY (amd64 on ubuntu-latest, arm64 on
52+ # ubuntu-24.04-arm) and pushes by digest; docker-manifest below stitches the
53+ # digests into the tagged multi-arch image. No QEMU: emulating the arm64
54+ # Rust release build wedged the v0.6.0 run for 3+ hours and, via the
55+ # workflow concurrency group, blocked every queued release run behind it.
4756 docker :
48- name : Build & Push Docker Image
57+ name : Build & Push Docker Image (${{ matrix.arch }})
4958 needs : release-please
50- if : ${{ needs.release-please.outputs.release_created == 'true' }}
51- runs-on : ubuntu-latest
59+ # Runs on a fresh release, or on manual dispatch for an existing tag
60+ # (release-please is skipped on dispatch, hence !cancelled()).
61+ if : >-
62+ ${{ !cancelled() && (
63+ needs.release-please.outputs.release_created == 'true' ||
64+ (github.event_name == 'workflow_dispatch' && inputs.docker_backfill_tag != '')
65+ ) }}
66+ strategy :
67+ fail-fast : false
68+ matrix :
69+ include :
70+ - arch : amd64
71+ os : ubuntu-latest
72+ platform : linux/amd64
73+ - arch : arm64
74+ os : ubuntu-24.04-arm
75+ platform : linux/arm64
76+ runs-on : ${{ matrix.os }}
5277 permissions :
5378 contents : read
5479 packages : write
5580 steps :
81+ - name : Resolve release tag
82+ id : rel
83+ env :
84+ DISPATCH_TAG : ${{ inputs.docker_backfill_tag }}
85+ RELEASE_TAG : ${{ needs.release-please.outputs.tag_name }}
86+ run : |
87+ set -euo pipefail
88+ TAG="${DISPATCH_TAG:-$RELEASE_TAG}"
89+ case "$TAG" in
90+ v[0-9]*) ;;
91+ *) echo "::error::'$TAG' does not look like a release tag (vX.Y.Z)"; exit 1 ;;
92+ esac
93+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
94+ echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
95+
5696 - name : Checkout release tag
5797 uses : actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
5898 with :
59- ref : ${{ needs.release-please .outputs.tag_name }}
99+ ref : ${{ steps.rel .outputs.tag }}
60100 persist-credentials : false
61101
62- - name : Set up QEMU
63- uses : docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
64-
65102 - name : Set up Docker Buildx
66103 uses : docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
67104
@@ -72,45 +109,130 @@ jobs:
72109 username : ${{ github.actor }}
73110 password : ${{ secrets.GITHUB_TOKEN }}
74111
75- - name : Extract metadata
76- id : meta
77- uses : docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
78- with :
79- images : ghcr.io/${{ github.repository }}
80- tags : |
81- type=semver,pattern={{version}},value=${{ needs.release-please.outputs.version }}
82- type=semver,pattern={{major}}.{{minor}},value=${{ needs.release-please.outputs.version }}
83- type=raw,value=latest
84-
85112 - name : Build and load locally (smoke)
86113 uses : docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
87114 with :
88115 context : .
116+ platforms : ${{ matrix.platform }}
89117 load : true
90118 tags : gitlawb-node:smoke
91- cache-from : type=gha
119+ cache-from : type=gha,scope=docker-${{ matrix.arch }}
92120
121+ # Native runner on both arches, so the built image can always execute.
122+ # Assert the released version so a stale artifact fails here, not in prod.
93123 - name : Smoke test
94- run : docker run --rm gitlawb-node:smoke --version
124+ env :
125+ VERSION : ${{ steps.rel.outputs.version }}
126+ run : |
127+ set -euo pipefail
128+ out="$(docker run --rm gitlawb-node:smoke --version)"
129+ echo "$out"
130+ grep -qF "$VERSION" <<<"$out" || {
131+ echo "::error::image --version did not report expected version $VERSION (got: $out)"
132+ exit 1
133+ }
95134
96- - name : Build and push (multi-arch)
135+ # Push by digest only — tags are applied once by docker-manifest so a
136+ # half-finished matrix can never publish a partially-tagged image.
137+ # provenance:false keeps each push a plain single-arch manifest, which is
138+ # what imagetools create expects to merge.
139+ - name : Build and push by digest
140+ id : push
97141 uses : docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
98142 with :
99143 context : .
100- platforms : linux/amd64,linux/arm64
101- push : true
102- tags : ${{ steps.meta.outputs.tags }}
103- labels : ${{ steps.meta.outputs.labels }}
104- cache-from : type=gha
105- cache-to : type=gha,mode=max
144+ platforms : ${{ matrix.platform }}
145+ provenance : false
146+ outputs : type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true
147+ cache-from : type=gha,scope=docker-${{ matrix.arch }}
148+ cache-to : type=gha,mode=max,scope=docker-${{ matrix.arch }}
149+
150+ - name : Export digest
151+ env :
152+ DIGEST : ${{ steps.push.outputs.digest }}
153+ run : |
154+ set -euo pipefail
155+ mkdir -p /tmp/digests
156+ touch "/tmp/digests/${DIGEST#sha256:}"
157+
158+ - name : Upload digest
159+ uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
160+ with :
161+ name : docker-digest-${{ matrix.arch }}
162+ path : /tmp/digests/*
163+ if-no-files-found : error
164+ retention-days : 1
165+
166+ docker-manifest :
167+ name : Publish Docker manifest
168+ needs : [release-please, docker]
169+ if : ${{ !cancelled() && needs.docker.result == 'success' }}
170+ runs-on : ubuntu-latest
171+ permissions :
172+ packages : write
173+ steps :
174+ - name : Resolve release tag
175+ id : rel
176+ env :
177+ DISPATCH_TAG : ${{ inputs.docker_backfill_tag }}
178+ RELEASE_TAG : ${{ needs.release-please.outputs.tag_name }}
179+ run : |
180+ set -euo pipefail
181+ TAG="${DISPATCH_TAG:-$RELEASE_TAG}"
182+ case "$TAG" in
183+ v[0-9]*) ;;
184+ *) echo "::error::'$TAG' does not look like a release tag (vX.Y.Z)"; exit 1 ;;
185+ esac
186+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
187+ echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
188+
189+ - name : Download digests
190+ uses : actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
191+ with :
192+ pattern : docker-digest-*
193+ path : /tmp/digests
194+ merge-multiple : true
195+
196+ - name : Set up Docker Buildx
197+ uses : docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
198+
199+ - name : Log in to GitHub Container Registry
200+ uses : docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
201+ with :
202+ registry : ghcr.io
203+ username : ${{ github.actor }}
204+ password : ${{ secrets.GITHUB_TOKEN }}
205+
206+ - name : Create and push multi-arch manifest
207+ env :
208+ VERSION : ${{ steps.rel.outputs.version }}
209+ run : |
210+ set -euo pipefail
211+ # ghcr requires a lowercase repository path.
212+ IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}"
213+ MAJOR_MINOR="${VERSION%.*}"
214+ digests=""
215+ for f in /tmp/digests/*; do
216+ digests="$digests $IMAGE@sha256:$(basename "$f")"
217+ done
218+ # shellcheck disable=SC2086
219+ docker buildx imagetools create \
220+ -t "$IMAGE:$VERSION" \
221+ -t "$IMAGE:$MAJOR_MINOR" \
222+ -t "$IMAGE:latest" \
223+ $digests
224+ docker buildx imagetools inspect "$IMAGE:$VERSION"
106225
107226 - name : Release summary
227+ env :
228+ VERSION : ${{ steps.rel.outputs.version }}
229+ TAG : ${{ steps.rel.outputs.tag }}
108230 run : |
109231 {
110- echo "## Released ${{ needs.release-please.outputs.tag_name }} "
232+ echo "## Released $TAG "
111233 echo
112- echo "- Image: \`ghcr.io/${{ github.repository }}:${{ needs.release-please.outputs.version }}\` "
113- echo "- GitHub: https://github.com/${{ github.repository }} /releases/tag/${{ needs.release-please.outputs.tag_name }} "
234+ echo "- Image: \`ghcr.io/${GITHUB_REPOSITORY,,}:$VERSION\` (linux/amd64 + linux/arm64) "
235+ echo "- GitHub: https://github.com/$GITHUB_REPOSITORY /releases/tag/$TAG "
114236 } >> "$GITHUB_STEP_SUMMARY"
115237
116238 release-binaries :
0 commit comments