forked from FuzzyGrim/Yamtrack
-
-
Notifications
You must be signed in to change notification settings - Fork 55
275 lines (250 loc) · 11.1 KB
/
Copy pathdocker-image.yml
File metadata and controls
275 lines (250 loc) · 11.1 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
name: Docker Image
on:
workflow_dispatch:
inputs:
bootstrap_ghcr:
description: "Use GHCR_TOKEN once to create the renamed floppy package"
required: false
default: false
type: boolean
push:
branches:
- "release"
- "latest"
tags:
- "v*"
pull_request:
branches:
- "latest"
# A push to latest must not be overtaken by an older build that started later
# because runners were queued. Keep one publish per ref in flight and let the
# newest pending run replace intermediate commits without cancelling the run
# currently publishing.
concurrency:
group: docker-image-${{ github.ref }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_NAME: dannyvfilms/floppy
LEGACY_IMAGE_NAME: dannyvfilms/yamtrack
jobs:
test:
name: Smoke built image
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
env:
SMOKE_APP: floppy-smoke-app-${{ github.run_id }}-${{ github.run_attempt }}
SMOKE_IMAGE: floppy:ci-smoke
SMOKE_NETWORK: floppy-smoke-${{ github.run_id }}-${{ github.run_attempt }}
SMOKE_REDIS: floppy-smoke-redis-${{ github.run_id }}-${{ github.run_attempt }}
SMOKE_SHA: ${{ github.sha }}
SMOKE_VOLUME: floppy-smoke-db-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v4
- name: Build test image
uses: docker/build-push-action@v7
with:
context: .
load: true
tags: ${{ env.SMOKE_IMAGE }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
platforms: linux/amd64
build-args: |
VERSION=ci-smoke
COMMIT_SHA=${{ env.SMOKE_SHA }}
- name: Run and restart test image
run: |
set -euo pipefail
wait_for_redis() {
for _ in $(seq 1 30); do
if timeout 10 docker exec "$SMOKE_REDIS" redis-cli ping | grep -qx PONG; then
return 0
fi
sleep 2
done
echo "Redis did not become ready" >&2
return 1
}
wait_for_health() {
for _ in $(seq 1 60); do
state=$(docker inspect --format '{{.State.Status}}' "$SMOKE_APP")
health=$(docker inspect --format '{{.State.Health.Status}}' "$SMOKE_APP")
if [ "$health" = healthy ]; then
return 0
fi
if [ "$state" = exited ] || [ "$state" = dead ]; then
echo "Floppy exited before becoming healthy" >&2
return 1
fi
sleep 5
done
echo "Floppy did not become healthy" >&2
return 1
}
assert_runtime() {
curl -fsS --max-time 10 http://127.0.0.1:8000/health/ >/dev/null
login_html=$(curl -fsS --max-time 10 http://127.0.0.1:8000/accounts/login/)
grep -Fq 'alt="Floppy"' <<<"$login_html"
curl -fsS --max-time 10 http://127.0.0.1:8000/static/css/main.css >/dev/null
curl -fsS --max-time 10 http://127.0.0.1:8000/api/v1/info/ |
python3 -c 'import json, sys; assert json.load(sys.stdin)["version"] == "ci-smoke"'
test "$(docker exec "$SMOKE_APP" printenv VERSION)" = ci-smoke
test "$(docker exec "$SMOKE_APP" printenv COMMIT_SHA)" = "$SMOKE_SHA"
timeout 30 docker exec "$SMOKE_APP" test -s /floppy/db/db.sqlite3
timeout 120 docker exec "$SMOKE_APP" python manage.py migrate --check --noinput
timeout 30 docker exec "$SMOKE_APP" python manage.py shell -c \
'from django.conf import settings; import os, redis; assert settings.VERSION == os.environ["VERSION"] == "ci-smoke"; assert settings.COMMIT_SHA == os.environ["COMMIT_SHA"]; assert redis.Redis.from_url(settings.REDIS_URL).ping()'
timeout 30 docker exec "$SMOKE_APP" python -c \
'import importlib.metadata; import floppy_mcp; assert importlib.metadata.version("floppy-mcp")'
timeout 10 docker exec "$SMOKE_APP" sh -c \
'timeout 5 floppy-mcp </dev/null; status=$?; [ "$status" -eq 0 ] || [ "$status" -eq 124 ]'
# The startup-scan observability path (heartbeat, status sidecar,
# timeout marking) must actually be present in the image, not just
# in source.
timeout 10 docker exec "$SMOKE_APP" python -c \
'from config.sqlite_integrity import mark_startup_status_timeout, print_startup_heartbeat, read_startup_status, write_startup_status'
# The build-info file must exist and match this build, and a
# shadowing runtime environment override (an orchestrator's
# persisted .env, for example) must not be able to defeat it --
# this is the failure a real deployment hit.
build_info=$(docker exec "$SMOKE_APP" cat /etc/floppy-build-info)
grep -Fxq "VERSION=ci-smoke" <<<"$build_info"
grep -Fxq "COMMIT_SHA=$SMOKE_SHA" <<<"$build_info"
restored=$(docker exec -e COMMIT_SHA=stale-override -e VERSION=stale-override "$SMOKE_APP" sh -c \
'. /etc/floppy-build-info; export VERSION COMMIT_SHA; echo "$VERSION $COMMIT_SHA"')
test "$restored" = "ci-smoke $SMOKE_SHA"
# HOST_CONTAINERNAME is unset in this smoke run, so the identity
# line must fall back to "floppy" rather than erroring. Captured
# into a variable first: piping a long-lived command straight
# into "grep -q" trips pipefail on the writer's SIGPIPE once grep
# finds its match and closes the pipe early.
smoke_logs=$(docker logs "$SMOKE_APP" 2>&1)
grep -Fq "[entrypoint] Floppy runtime: version=ci-smoke commit=$SMOKE_SHA" <<<"$smoke_logs"
grep -Fq "container_name=floppy" <<<"$smoke_logs"
}
start_app() {
docker run --detach \
--name "$SMOKE_APP" \
--network "$SMOKE_NETWORK" \
--publish 127.0.0.1:8000:8000 \
--env TZ=UTC \
--env SECRET=ci-smoke-only-not-a-production-secret \
--env REDIS_URL="redis://$SMOKE_REDIS:6379" \
--env ADMIN_ENABLED=False \
--env DEMO_ACCOUNT_ENABLED=False \
--env DEBUG=False \
--env FLOPPY_RESOURCE_TIER=minimal \
--volume "$SMOKE_VOLUME:/floppy/db" \
"$SMOKE_IMAGE"
}
image_env=$(docker image inspect "$SMOKE_IMAGE" --format '{{range .Config.Env}}{{println .}}{{end}}')
grep -Fxq VERSION=ci-smoke <<<"$image_env"
grep -Fxq "COMMIT_SHA=$SMOKE_SHA" <<<"$image_env"
docker network create "$SMOKE_NETWORK"
docker volume create "$SMOKE_VOLUME"
docker run --detach --name "$SMOKE_REDIS" --network "$SMOKE_NETWORK" redis:8-alpine
wait_for_redis
start_app
wait_for_health
assert_runtime
docker logs "$SMOKE_APP" >"$RUNNER_TEMP/floppy-smoke-first.log" 2>&1
docker rm --force "$SMOKE_APP"
start_app
wait_for_health
assert_runtime
- name: Print test container logs
if: failure()
run: |
if [ -f "$RUNNER_TEMP/floppy-smoke-first.log" ]; then
echo "First Floppy boot logs:"
tail -n 500 "$RUNNER_TEMP/floppy-smoke-first.log"
fi
for container in "$SMOKE_APP" "$SMOKE_REDIS"; do
if docker inspect "$container" >/dev/null 2>&1; then
echo "$container logs:"
docker logs --tail 500 "$container" 2>&1 || true
fi
done
- name: Clean up test containers
if: always()
run: |
docker rm --force "$SMOKE_APP" "$SMOKE_REDIS" 2>/dev/null || true
docker network rm "$SMOKE_NETWORK" 2>/dev/null || true
docker volume rm "$SMOKE_VOLUME" 2>/dev/null || true
build:
needs: test
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0 # This ensures all tags are fetched for git describe
- name: Get version from git
id: get-version
run: |
VERSION=$(git describe --tags 2>/dev/null || echo "dev")
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Using version: $VERSION"
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Extract Docker metadata tags
id: meta
uses: docker/metadata-action@v6
with:
# Dual-publish during the rename grace period: the legacy path keeps
# receiving identical tags so pre-rename deployments still get
# updates. Drop the second line once traffic to it has died off.
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.REGISTRY }}/${{ env.LEGACY_IMAGE_NAME }}
tags: |
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/latest' }}
type=raw,value=release,enable=${{ github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/tags/v') }}
# Pull requests build both architectures to prove the image still
# cross-compiles, but they must never write to the registry. A pr-N tag
# has no consumer and lands in the same public package users pull from,
# where nothing distinguishes it from a release. Publishing happens on
# push to latest/release and on v* tags only.
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
# The one-time bootstrap is needed because floppy was created by a
# PAT-based push, while the existing yamtrack package is already
# associated with this repository. Normal builds stay on GITHUB_TOKEN.
username: ${{ inputs.bootstrap_ghcr == true && 'dannyvfilms' || github.actor }}
password: ${{ inputs.bootstrap_ghcr == true && secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v7
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.get-version.outputs.VERSION }}
COMMIT_SHA=${{ github.sha }}