Repository navigation
353 lines (326 loc) · 13.2 KB
/
Copy pathrelease-container.yml
File metadata and controls
353 lines (326 loc) · 13.2 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
name: Release container
run-name: Release container
on:
schedule:
# 13:00 GMT+8 (05:00 UTC) every day. GitHub Actions cron uses UTC.
- cron: "0 5 * * *"
release:
types: [published]
workflow_call:
inputs:
source_ref:
description: "Git tag to build"
required: true
type: string
image_tag:
description: "Docker image tag"
required: true
type: string
release_build:
description: "Require source_ref to resolve as a Git tag"
required: true
type: boolean
workflow_dispatch:
inputs:
source_ref:
description: "Git tag, commit SHA, or branch to build"
required: true
type: string
default: main
image_tag:
description: "Docker image tag override (defaults to the source ref)"
required: false
type: string
concurrency:
group: release-container
cancel-in-progress: false
permissions:
contents: read
jobs:
authorize-publication:
name: Authorize publication
runs-on: ubuntu-latest
steps:
- name: Verify manual publisher permission
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
permission="$(gh api \
"repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission" \
--jq '.role_name')"
case "$permission" in
admin|maintain)
echo "$GITHUB_ACTOR has $permission permission"
;;
*)
echo "::error::Manual container publication requires admin or maintain permission"
exit 1
;;
esac
publish:
name: Build and publish Docker image
needs: authorize-publication
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
statuses: write
environment: docker-publish
steps:
- name: Verify configuration
env:
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
set -euo pipefail
if [ -z "$DOCKERHUB_IMAGE" ]; then
echo "::error::The DOCKERHUB_IMAGE environment variable is not configured"
exit 1
fi
if [ -z "$DOCKERHUB_USERNAME" ] || [ -z "$DOCKERHUB_TOKEN" ]; then
echo "::error::The Docker Hub environment secrets are not configured"
exit 1
fi
if [[ ! "$DOCKERHUB_IMAGE" =~ ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$ ]]; then
echo "::error::DOCKERHUB_IMAGE must be a Docker Hub repository such as vllm/agentic-api"
exit 1
fi
- name: Select source
id: release
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
REQUESTED_REF: ${{ inputs.source_ref }}
REQUESTED_IMAGE_TAG: ${{ inputs.image_tag }}
RELEASE_BUILD: ${{ inputs.release_build }}
run: |
set -euo pipefail
# Reusable workflows inherit the caller's event name.
if [ "$RELEASE_BUILD" = "true" ]; then
source_ref="refs/tags/$REQUESTED_REF"
elif [ "$EVENT_NAME" = "schedule" ]; then
source_ref="main"
elif [ "$EVENT_NAME" = "release" ]; then
source_ref="refs/tags/$RELEASE_TAG"
else
source_ref="$REQUESTED_REF"
fi
if [[ "$source_ref" =~ ^[0-9a-fA-F]{40}$ ]]; then
:
elif [[ "$source_ref" =~ ^[0-9a-fA-F]{7,39}$ ]]; then
echo "::error::Commit SHAs must contain all 40 hexadecimal characters"
exit 1
elif ! git check-ref-format --allow-onelevel "$source_ref"; then
echo "::error::Invalid Git source ref"
exit 1
fi
echo "source_ref=$source_ref" >> "$GITHUB_OUTPUT"
- name: Check out release source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ steps.release.outputs.source_ref }}
- name: Resolve source commit
id: source
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
REQUESTED_IMAGE_TAG: ${{ inputs.image_tag }}
REQUESTED_REF: ${{ inputs.source_ref }}
RELEASE_BUILD: ${{ inputs.release_build }}
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE }}
run: |
set -euo pipefail
if [[ -n "$REQUESTED_IMAGE_TAG" \
&& ! "$REQUESTED_IMAGE_TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then
echo "::error::Invalid Docker image tag override"
exit 1
fi
sha="$(git rev-parse HEAD)"
git fetch --no-tags origin main
main_sha="$(git rev-parse FETCH_HEAD)"
image_tags=()
nightly=false
if [ "$RELEASE_BUILD" = "true" ]; then
image_tags+=("$REQUESTED_IMAGE_TAG")
elif [ "$EVENT_NAME" = "release" ]; then
image_tags+=("$RELEASE_TAG")
elif [ "$EVENT_NAME" = "schedule" ] \
|| { [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$sha" = "$main_sha" ]; }; then
nightly=true
image_tags+=("nightly-$sha" "nightly")
if [ -n "$REQUESTED_IMAGE_TAG" ]; then
image_tags+=("$REQUESTED_IMAGE_TAG")
fi
else
# Normalize slashes only in the default tag derived from a Git ref.
image_tags+=("${REQUESTED_IMAGE_TAG:-${REQUESTED_REF//\//-}}")
fi
tags=()
for image_tag in "${image_tags[@]}"; do
if [[ ! "$image_tag" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then
echo "::error::Invalid Docker image tag"
exit 1
fi
tags+=("$DOCKERHUB_IMAGE:$image_tag")
done
version="${image_tags[0]}"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "nightly=$nightly" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
{
echo "tags<<EOF"
printf '%s\n' "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
printf 'Publishing %s\n' "${tags[@]}"
- name: Check for an already published nightly commit
id: nightly-check
if: steps.source.outputs.nightly == 'true'
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ steps.source.outputs.sha }}
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE }}
run: |
set -euo pipefail
status_context="container/nightly/$DOCKERHUB_IMAGE"
# Read GitHub's publication record, without querying Docker Hub. API
# failures must stop the run rather than be interpreted as a cache miss.
statuses="$(gh api --paginate --method GET \
"repos/$GITHUB_REPOSITORY/commits/$SOURCE_SHA/statuses" \
-f per_page=100 --jq '.[] | [.context, .state] | @tsv')"
skip=false
# The API returns newest statuses first; use the latest for this image.
while IFS=$'\t' read -r context state; do
if [ "$context" = "$status_context" ]; then
if [ "$state" = "success" ]; then
skip=true
fi
break
fi
done <<< "$statuses"
echo "skip=$skip" >> "$GITHUB_OUTPUT"
if [ "$skip" = "true" ]; then
message="Skipping nightly: $DOCKERHUB_IMAGE at $SOURCE_SHA was already published."
echo "$message"
echo "$message" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Set up Docker Buildx
if: steps.nightly-check.outputs.skip != 'true'
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Generate image metadata
if: steps.nightly-check.outputs.skip != 'true'
id: image-metadata
run: echo "created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
# Pull public build dependencies anonymously. The publishing token may only
# have access to the destination repository, not docker/dockerfile or base images.
- name: Build runtime image
if: steps.nightly-check.outputs.skip != 'true'
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: false
load: true
tags: ${{ steps.source.outputs.tags }}
build-args: |
OCI_BUILD_PIPELINE=${{ github.workflow }}
OCI_BUILD_URL=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
OCI_CREATED=${{ steps.image-metadata.outputs.created }}
OCI_REVISION=${{ steps.source.outputs.sha }}
OCI_VERSION=${{ steps.source.outputs.version }}
cache-from: type=gha,scope=agentic-api-container
cache-to: type=gha,mode=max,scope=agentic-api-container,ignore-error=true
- name: Log in to Docker Hub for publication
if: steps.nightly-check.outputs.skip != 'true'
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push runtime image
if: steps.nightly-check.outputs.skip != 'true'
env:
IMAGE_TAGS: ${{ steps.source.outputs.tags }}
run: |
set -euo pipefail
while IFS= read -r tag; do
docker push "$tag"
done <<< "$IMAGE_TAGS"
- name: Record successful nightly publication
if: steps.source.outputs.nightly == 'true' && steps.nightly-check.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ steps.source.outputs.sha }}
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE }}
run: |
set -euo pipefail
gh api --method POST "repos/$GITHUB_REPOSITORY/statuses/$SOURCE_SHA" \
-f state=success \
-f context="container/nightly/$DOCKERHUB_IMAGE" \
-f description='All nightly image tags successfully pushed' \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
--silent
- name: Report disabled nightly cleanup
if: vars.DOCKERHUB_CLEANUP_ENABLED != 'true'
run: |
echo "::warning::Nightly cleanup is disabled; 30-day retention is not enforced."
echo "Enable DOCKERHUB_CLEANUP_ENABLED only with tag-read and tag-delete permissions."
- name: Delete nightly tags older than 30 days
if: vars.DOCKERHUB_CLEANUP_ENABLED == 'true'
env:
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
set +x
set -euo pipefail
namespace="${DOCKERHUB_IMAGE%%/*}"
repository="${DOCKERHUB_IMAGE#*/}"
cutoff="$(date -u --date='30 days ago' '+%Y-%m-%dT%H:%M:%SZ')"
# Personal access tokens are exchanged for an API bearer token.
# Organization access tokens can be used directly as bearer tokens.
auth_payload="$(jq --null-input \
'{identifier: env.DOCKERHUB_USERNAME, secret: env.DOCKERHUB_TOKEN}')"
if api_token="$(printf '%s' "$auth_payload" \
| curl --fail --silent --show-error \
--request POST 'https://hub.docker.com/v2/auth/token' \
--header 'Content-Type: application/json' \
--data-binary @- \
| jq --exit-status --raw-output '.access_token')"; then
:
else
api_token="$DOCKERHUB_TOKEN"
fi
printf '::add-mask::%s\n' "$api_token"
unset auth_payload
api_base="https://hub.docker.com/v2/namespaces/$namespace/repositories/$repository"
url="$api_base/tags?page_size=100&name=nightly-"
candidates=()
while [ "$url" != "null" ] && [ -n "$url" ]; do
case "$url" in
"$api_base/tags?"*) ;;
*)
echo "::error::Docker Hub returned an unexpected pagination URL"
exit 1
;;
esac
response="$(curl --fail --silent --show-error \
--header "Authorization: Bearer $api_token" "$url")"
mapfile -t page_candidates < <(jq --raw-output --arg cutoff "$cutoff" '
.results[]
| select(.name | startswith("nightly-"))
| select(.last_updated < $cutoff)
| .name
' <<< "$response")
candidates+=("${page_candidates[@]}")
url="$(jq --raw-output '.next' <<< "$response")"
done
for tag in "${candidates[@]}"; do
echo "Deleting expired Docker tag $DOCKERHUB_IMAGE:$tag"
curl --fail --silent --show-error \
--output /dev/null \
--request DELETE \
--header "Authorization: Bearer $api_token" \
"$api_base/tags/$tag"
done