-
Notifications
You must be signed in to change notification settings - Fork 0
410 lines (376 loc) · 20 KB
/
Copy pathdocker-publish.yml
File metadata and controls
410 lines (376 loc) · 20 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
name: EMS release
on:
push:
branches:
- main
tags:
- "v*"
schedule:
- cron: "0 3 * * 1"
workflow_dispatch:
permissions:
contents: read
packages: write
# Owned cache scopes, disjoint from the feature scopes so a feature build can
# never write a release cache. Bump `-v1` to reset.
env:
EMS_RELEASE_CACHE_SCOPE: ems-release-v1
ADMIN_RELEASE_CACHE_SCOPE: admin-release-v1
jobs:
publish-ghcr:
name: EMS release publish
runs-on: ubuntu-latest
if: >-
github.event_name == 'schedule' ||
(github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))) ||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.event_name == 'schedule' && 'main' || github.ref }}
- name: Verify tag commit is on main
if: startsWith(github.ref, 'refs/tags/')
run: |
git fetch origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "Tag commit is not contained in main. Refusing to publish."
exit 1
fi
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.11"
cache: pip
cache-dependency-path: |
requirements.txt
requirements-dev.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -r requirements-dev.txt
- name: Run Ruff lint
run: |
ruff check .
- name: Compile Python files
run: |
python -m compileall ems dashboard scripts tests emsctl.py ems-solarflow-api-control.py
- name: Validate example Compose file
run: |
docker compose -f docker-compose.example.yml config
- name: Install the filesystem tools the fixtures build with
run: sudo apt-get install -y --no-install-recommends mtools e2fsprogs
- name: Run full test suite (excludes real-Docker e2e, covered by CI workflow)
run: |
pytest -q -m "not docker" tests/
- name: Run paired System Build Docker/startup contract
run: |
pytest -q -rs tests/test_system_build_docker_contract.py
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve build identity
id: build_identity
run: |
# `latest` is a channel, not a version. Resolve a monotonic build
# identity so Admin can compare a running `latest` image against
# stable/rc tags by image labels, not tag name alone.
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
release_tag="${GITHUB_REF#refs/tags/}"
# Only a real release tag becomes the runtime ems_version; `latest`
# stays empty so a channel never poses as a release version.
ems_release_tag="${release_tag}"
if [[ "${release_tag}" == *-* ]]; then
channel="rc" # refs/tags/vX.Y.Z-... (pre-release)
else
channel="stable" # refs/tags/vX.Y.Z
fi
else
channel="latest" # main / schedule / workflow_dispatch
release_tag="latest"
ems_release_tag=""
fi
git_commit="$(git rev-parse --verify HEAD)"
if [[ ! "${git_commit}" =~ ^[0-9a-f]{40}$ ]]; then
echo "The checked-out release ref did not resolve to a full lowercase Git SHA." >&2
exit 1
fi
git_describe="$(git describe --tags --always --dirty 2>/dev/null || true)"
git_commit_short="${git_commit::12}"
if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
git_dirty="true"
else
git_dirty="false"
fi
{
echo "channel=${channel}"
echo "release_tag=${release_tag}"
echo "ems_release_tag=${ems_release_tag}"
echo "git_describe=${git_describe}"
echo "git_commit=${git_commit}"
echo "git_commit_short=${git_commit_short}"
echo "git_dirty=${git_dirty}"
echo "build_serial=${GITHUB_RUN_NUMBER}"
echo "build_id=${release_tag}-${git_commit_short}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
} >> "${GITHUB_OUTPUT}"
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/basecubedev/ems-solarflow-api-control
flavor: |
latest=false
tags: |
type=ref,event=tag
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
labels: |
org.opencontainers.image.title=ems-solarflow-api-control
org.opencontainers.image.description=Local EMS controller for Zendure SolarFlow. Docker images include automatic first-run config bootstrap. See README for Quick Start and docs/docker.md for full setup.
org.opencontainers.image.source=https://github.com/basecubedev/ems-solarflow-api-control
org.opencontainers.image.documentation=https://github.com/basecubedev/ems-solarflow-api-control/blob/main/docs/docker.md
org.opencontainers.image.version=${{ steps.build_identity.outputs.release_tag }}
org.opencontainers.image.revision=${{ steps.build_identity.outputs.git_commit }}
de.basecubedev.ems.channel=${{ steps.build_identity.outputs.channel }}
de.basecubedev.ems.build_serial=${{ steps.build_identity.outputs.build_serial }}
de.basecubedev.ems.build_id=${{ steps.build_identity.outputs.build_id }}
de.basecubedev.ems.release_tag=${{ steps.build_identity.outputs.release_tag }}
# Imports the release cache but never exports (the pushed build is the
# authoritative exporter). Explicit labels override the Dockerfile's
# EMS_RELEASE_TAG-derived version label, which is empty on latest.
- name: Build local Docker image for content validation
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
load: true
push: false
tags: ems-solarflow-api-control:ci
build-args: |
EMS_RELEASE_TAG=${{ steps.build_identity.outputs.ems_release_tag }}
EMS_GIT_COMMIT=${{ steps.build_identity.outputs.git_commit }}
EMS_GIT_COMMIT_SHORT=${{ steps.build_identity.outputs.git_commit_short }}
EMS_GIT_DESCRIBE=${{ steps.build_identity.outputs.git_describe }}
EMS_GIT_BRANCH=${{ github.ref_name }}
EMS_GIT_DIRTY=${{ steps.build_identity.outputs.git_dirty }}
EMS_BUILD_ID=${{ steps.build_identity.outputs.build_id }}
EMS_BUILD_SERIAL=${{ steps.build_identity.outputs.build_serial }}
EMS_CHANNEL=${{ steps.build_identity.outputs.channel }}
labels: |
org.opencontainers.image.version=${{ steps.build_identity.outputs.release_tag }}
org.opencontainers.image.revision=${{ steps.build_identity.outputs.git_commit }}
de.basecubedev.ems.channel=${{ steps.build_identity.outputs.channel }}
de.basecubedev.ems.build_id=${{ steps.build_identity.outputs.build_id }}
de.basecubedev.ems.release_tag=${{ steps.build_identity.outputs.release_tag }}
cache-from: type=gha,scope=${{ env.EMS_RELEASE_CACHE_SCOPE }}
- name: Validate Docker image runtime content
run: |
docker run --rm ems-solarflow-api-control:ci python3 emsctl.py --help
docker run --rm ems-solarflow-api-control:ci test -f /app/config.template.json
docker run --rm ems-solarflow-api-control:ci sh -c '
test -f /app/emsctl.py &&
test -f /app/docker-entrypoint.sh &&
test -f /app/docs/docker.md &&
test -f /app/ems/config.py &&
test -f /app/dashboard/server.py &&
test ! -e /app/.git &&
test ! -e /app/tests &&
test ! -e /app/__pycache__ &&
test ! -e /app/backup &&
test ! -e /app/.venv &&
test ! -e /app/deploy/docker/influxdb.env &&
test -z "$(find /app/data -mindepth 1 -maxdepth 1 -print -quit)"
'
# Authoritative EMS release cache exporter.
- name: Build and push Docker image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
EMS_RELEASE_TAG=${{ steps.build_identity.outputs.ems_release_tag }}
EMS_GIT_COMMIT=${{ steps.build_identity.outputs.git_commit }}
EMS_GIT_COMMIT_SHORT=${{ steps.build_identity.outputs.git_commit_short }}
EMS_GIT_DESCRIBE=${{ steps.build_identity.outputs.git_describe }}
EMS_GIT_BRANCH=${{ github.ref_name }}
EMS_GIT_DIRTY=${{ steps.build_identity.outputs.git_dirty }}
EMS_BUILD_ID=${{ steps.build_identity.outputs.build_id }}
EMS_BUILD_SERIAL=${{ steps.build_identity.outputs.build_serial }}
EMS_CHANNEL=${{ steps.build_identity.outputs.channel }}
cache-from: type=gha,scope=${{ env.EMS_RELEASE_CACHE_SCOPE }}
cache-to: type=gha,mode=max,scope=${{ env.EMS_RELEASE_CACHE_SCOPE }}
annotations: |
index:org.opencontainers.image.description=Local EMS controller for Zendure SolarFlow. Docker images include automatic first-run config bootstrap. See README for Quick Start and docs/docker.md for full setup.
# --- Admin Console image ------------------------------------------------
# The installable Admin Console ships as its own image so end users can run
# it without a source checkout. It reuses the same build identity as the
# EMS image; only the metadata and Dockerfile differ.
- name: Generate Admin Docker metadata
id: meta_admin
uses: docker/metadata-action@v6
with:
images: ghcr.io/basecubedev/ems-solarflow-admin
flavor: |
latest=false
tags: |
type=ref,event=tag
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
labels: |
org.opencontainers.image.title=ems-solarflow-admin
org.opencontainers.image.description=Browser-based Admin Console for EMS SolarFlow setup, maintenance, updates and backups.
org.opencontainers.image.source=https://github.com/basecubedev/ems-solarflow-api-control
org.opencontainers.image.documentation=https://github.com/basecubedev/ems-solarflow-api-control/blob/main/docs/user/admin-console.md
org.opencontainers.image.version=${{ steps.build_identity.outputs.release_tag }}
org.opencontainers.image.revision=${{ steps.build_identity.outputs.git_commit }}
de.basecubedev.ems.channel=${{ steps.build_identity.outputs.channel }}
de.basecubedev.ems.build_serial=${{ steps.build_identity.outputs.build_serial }}
de.basecubedev.ems.build_id=${{ steps.build_identity.outputs.build_id }}
de.basecubedev.ems.release_tag=${{ steps.build_identity.outputs.release_tag }}
# Imports the Admin release cache but never exports; the Dockerfile derives
# the identity labels from these build args.
- name: Build local Admin image for content validation
uses: docker/build-push-action@v7
with:
context: .
file: ./deploy/admin/Dockerfile
platforms: linux/amd64
load: true
push: false
tags: ems-solarflow-admin:ci
build-args: |
EMS_REVISION=${{ steps.build_identity.outputs.git_commit }}
EMS_BUILD_ID=${{ steps.build_identity.outputs.build_id }}
EMS_CHANNEL=${{ steps.build_identity.outputs.channel }}
EMS_RELEASE_TAG=${{ steps.build_identity.outputs.release_tag }}
EMS_SYSTEM_TAG=${{ steps.build_identity.outputs.release_tag }}
cache-from: type=gha,scope=${{ env.ADMIN_RELEASE_CACHE_SCOPE }}
- name: Validate Admin image runtime content
run: |
docker run --rm ems-solarflow-admin:ci python -m admin --help
- name: Verify Admin/EMS system-build pair metadata
run: |
# One paired system build: both images' OCI labels, EMS runtime ENV,
# and both embedded descriptors must agree on the complete identity.
# On latest, OCI/descriptors intentionally say "latest" while the EMS
# runtime release version stays empty because latest is a channel.
set -euo pipefail
expected_system_tag="${{ steps.build_identity.outputs.release_tag }}"
expected_channel="${{ steps.build_identity.outputs.channel }}"
expected_rev="${{ steps.build_identity.outputs.git_commit }}"
expected_build_id="${{ steps.build_identity.outputs.build_id }}"
expected_release_tag="${{ steps.build_identity.outputs.release_tag }}"
expected_runtime_release_tag="${{ steps.build_identity.outputs.ems_release_tag }}"
expected_admin_image="ghcr.io/basecubedev/ems-solarflow-admin:${expected_system_tag}"
expected_ems_image="ghcr.io/basecubedev/ems-solarflow-api-control:${expected_system_tag}"
fail() { echo "SYSTEM BUILD PAIR MISMATCH: $1" >&2; exit 1; }
expect() { [ "$1" = "$2" ] || fail "$3=$1 != $2"; }
label() { docker inspect --format "{{ index .Config.Labels \"$1\" }}" "$2"; }
admin_version="$(label org.opencontainers.image.version ems-solarflow-admin:ci)"
admin_rev="$(label org.opencontainers.image.revision ems-solarflow-admin:ci)"
admin_channel="$(label de.basecubedev.ems.channel ems-solarflow-admin:ci)"
admin_build_id="$(label de.basecubedev.ems.build_id ems-solarflow-admin:ci)"
admin_release_tag="$(label de.basecubedev.ems.release_tag ems-solarflow-admin:ci)"
ems_version="$(label org.opencontainers.image.version ems-solarflow-api-control:ci)"
ems_oci_rev="$(label org.opencontainers.image.revision ems-solarflow-api-control:ci)"
ems_oci_channel="$(label de.basecubedev.ems.channel ems-solarflow-api-control:ci)"
ems_oci_build_id="$(label de.basecubedev.ems.build_id ems-solarflow-api-control:ci)"
ems_oci_release_tag="$(label de.basecubedev.ems.release_tag ems-solarflow-api-control:ci)"
sb="$(docker run --rm ems-solarflow-admin:ci cat /app/release-resources/system-build.json)"
rm="$(docker run --rm ems-solarflow-admin:ci cat /app/release-resources/resource-manifest.json)"
jq_get() { printf '%s' "$1" | python3 -c "import sys,json;print(json.load(sys.stdin)$2)"; }
sb_format_version="$(jq_get "$sb" "['format_version']")"
sb_system_tag="$(jq_get "$sb" "['system_tag']")"
sb_channel="$(jq_get "$sb" "['channel']")"
sb_rev="$(jq_get "$sb" "['revision']")"
sb_build_id="$(jq_get "$sb" "['build_id']")"
sb_release_tag="$(jq_get "$sb" "['release_tag']")"
sb_admin_image="$(jq_get "$sb" "['admin_image']")"
sb_ems_image="$(jq_get "$sb" "['ems_image']")"
rm_format_version="$(jq_get "$rm" "['format_version']")"
rm_system_tag="$(jq_get "$rm" "['system_tag']")"
rm_channel="$(jq_get "$rm" "['channel']")"
rm_rev="$(jq_get "$rm" "['revision']")"
rm_build_id="$(jq_get "$rm" "['build_id']")"
rm_release_tag="$(jq_get "$rm" "['release_tag']")"
rm_admin_image="$(jq_get "$rm" "['admin_image']")"
rm_ems_image="$(jq_get "$rm" "['ems_image']")"
ems_rev="$(docker run --rm ems-solarflow-api-control:ci printenv EMS_GIT_COMMIT)"
ems_build_id="$(docker run --rm ems-solarflow-api-control:ci printenv EMS_BUILD_ID)"
ems_channel="$(docker run --rm ems-solarflow-api-control:ci printenv EMS_CHANNEL)"
ems_release_tag="$(docker run --rm ems-solarflow-api-control:ci printenv EMS_RELEASE_TAG)"
for name in admin_version ems_version sb_system_tag rm_system_tag; do
expect "${!name}" "$expected_system_tag" "$name"
done
for name in admin_rev ems_oci_rev ems_rev sb_rev rm_rev; do
expect "${!name}" "$expected_rev" "$name"
done
for name in admin_channel ems_oci_channel ems_channel sb_channel rm_channel; do
expect "${!name}" "$expected_channel" "$name"
done
for name in admin_build_id ems_oci_build_id ems_build_id sb_build_id rm_build_id; do
expect "${!name}" "$expected_build_id" "$name"
done
for name in admin_release_tag ems_oci_release_tag sb_release_tag rm_release_tag; do
expect "${!name}" "$expected_release_tag" "$name"
done
for name in sb_admin_image rm_admin_image; do
expect "${!name}" "$expected_admin_image" "$name"
done
for name in sb_ems_image rm_ems_image; do
expect "${!name}" "$expected_ems_image" "$name"
done
expect "$ems_release_tag" "$expected_runtime_release_tag" "EMS runtime release_tag"
expect "$sb_format_version" "1" "system-build format_version"
expect "$rm_format_version" "1" "resource-manifest format_version"
required_resources=(
config.template.json
docker-compose.example.yml
install-docker.sh
install-docker.ps1
)
while IFS= read -r resource; do
required_resources+=("$resource")
done < <(find deploy/docker -type f -print | sort)
for resource in "${required_resources[@]}"; do
resource_hash="$(printf '%s' "$rm" | RESOURCE="$resource" python3 -c \
'import json,os,sys; print(json.load(sys.stdin)["files"].get(os.environ["RESOURCE"], ""))')"
[[ "$resource_hash" =~ ^sha256:[0-9a-f]{64}$ ]] \
|| fail "resource-manifest hash missing or invalid for $resource"
actual_hash="$(docker run --rm ems-solarflow-admin:ci python -c \
'import hashlib,sys; print("sha256:" + hashlib.sha256(open(sys.argv[1], "rb").read()).hexdigest())' \
"/app/release-resources/$resource")"
expect "$actual_hash" "$resource_hash" "embedded resource hash ($resource)"
done
echo "Admin/EMS system-build pair verified: tag=$expected_system_tag rev=$expected_rev build_id=$expected_build_id channel=$expected_channel"
# Authoritative Admin release cache exporter (separate scope from EMS).
- name: Build and push Admin Docker image
uses: docker/build-push-action@v7
with:
context: .
file: ./deploy/admin/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta_admin.outputs.tags }}
labels: ${{ steps.meta_admin.outputs.labels }}
build-args: |
EMS_REVISION=${{ steps.build_identity.outputs.git_commit }}
EMS_BUILD_ID=${{ steps.build_identity.outputs.build_id }}
EMS_CHANNEL=${{ steps.build_identity.outputs.channel }}
EMS_RELEASE_TAG=${{ steps.build_identity.outputs.release_tag }}
EMS_SYSTEM_TAG=${{ steps.build_identity.outputs.release_tag }}
cache-from: type=gha,scope=${{ env.ADMIN_RELEASE_CACHE_SCOPE }}
cache-to: type=gha,mode=max,scope=${{ env.ADMIN_RELEASE_CACHE_SCOPE }}
annotations: |
index:org.opencontainers.image.description=Browser-based Admin Console for EMS SolarFlow setup, maintenance, updates and backups.