-
Notifications
You must be signed in to change notification settings - Fork 0
448 lines (418 loc) · 19.6 KB
/
Copy pathbuild-and-publish.yml
File metadata and controls
448 lines (418 loc) · 19.6 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
# ═══════════════════════════════════════════════════════════
# LocalPibox Devstack — CI/CD Build & Publish
# ═══════════════════════════════════════════════════════════
# Builds on GitHub (fast connection) → publishes to GHCR
# Extensions update at runtime via pi update --extensions
#
# Triggers:
# - Push to dev/main (code or VERSION changes, path-filtered)
# - Weekly cron (Monday 3am UTC) — keep image fresh
# - Manual dispatch
#
# Version model (manual tagging):
# - Single source: devstack/VERSION
# - Developer bumps VERSION with `lpb-devstack bump` (local commit + push)
# - CI never writes VERSION. Pipeline phases:
# Phase 0: VERSION_CHECK — did this commit change VERSION?
# Phase 1: test-lpb — always runs (fast validation)
# Phase 2: build-cli — only if VERSION changed
# Phase 3: build-web — only if VERSION changed
# Phase 4: tag-repos — only if VERSION changed (after build)
# Phase 4.5: docs-publish — main only, stable docs version (mike)
# Phase 5: status — always runs
# - Cron / manual dispatch always build (keep image fresh / on-demand)
# - Dev pipeline (push to dev): 0.0.x-lpb-dev, images :dev-*, :{v}-*, :{sha}-*
# - Main pipeline (push to main): 0.0.x-lpb, images :main-*, :latest-*, :{v}-*, :{sha}-*
#
# Actions: all latest major versions (Node.js 24 native)
# actions/checkout@v6 · docker/build-push-action@v7
# docker/setup-buildx-action@v4 · docker/setup-qemu-action@v4
# docker/login-action@v4 · docker/metadata-action@v6
# actions/upload-artifact@v7
name: Build & Publish Devstack
on:
push:
branches: [dev, main]
paths:
- 'Dockerfile'
- 'VERSION'
- 'support/**'
- 'scripts/**'
- '.github/workflows/*.yml'
# NOTE: VERSION changes DO trigger — manual tagging (bump = release).
# lpb.stack.env / lpb.conf.env changes ship with the next VERSION bump.
pull_request:
branches: [main]
paths:
- 'Dockerfile'
- 'VERSION'
- 'support/**'
- 'scripts/**'
- '.github/workflows/*.yml'
schedule:
- cron: '0 3 * * 1'
workflow_dispatch:
inputs:
publish_latest:
description: 'Publish as :latest tag'
type: boolean
default: true
no_cache:
description: 'Full rebuild with no cache (pick up fork-branch changes)'
type: boolean
default: false
permissions:
contents: write
packages: write
env:
IMAGE_NAME: ghcr.io/lpb-stack/devstack
jobs:
# ──────────────────────────────────────────────────────
# Phase 0: VERSION check — build only when VERSION changed
# ──────────────────────────────────────────────────────
VERSION_CHECK:
name: VERSION check
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.check.outputs.changed }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Detect VERSION change
id: check
run: |
# Cron and manual dispatch always build (keep image fresh / on-demand release)
if [ "$GITHUB_EVENT_NAME" = "schedule" ] || [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Regular commit: files changed vs its parent (root commit → full tree)
changed_files=$(git diff-tree --no-commit-id --name-only -r "$GITHUB_SHA")
# Merge commit: diff-tree shows nothing by default → diff vs first parent
if [ -z "$changed_files" ] && git rev-parse -q --verify "$GITHUB_SHA^2" >/dev/null 2>&1; then
changed_files=$(git diff --name-only "$GITHUB_SHA^1" "$GITHUB_SHA")
fi
if echo "$changed_files" | grep -q "VERSION"; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
# ──────────────────────────────────────────────────────
# Phase 1: Test (always)
# ──────────────────────────────────────────────────────
test-lpb:
name: Run lpb.py unit tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Run test suite
run: |
python3 scripts/test_lpb.py
python3 scripts/test_localpibox.py
bash scripts/test_env_bridge.sh
# ──────────────────────────────────────────────────────
# Phase 2: Build & publish images (only if VERSION changed)
# ──────────────────────────────────────────────────────
build-cli:
name: Build & publish cli image
runs-on: ubuntu-latest
timeout-minutes: 45
needs: [test-lpb, VERSION_CHECK]
if: ${{ github.event_name != 'pull_request' && needs.VERSION_CHECK.outputs.changed == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up buildx
uses: docker/setup-buildx-action@v4
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Load stack config
id: config
run: |
set -a
source lpb.stack.env
# Pipeline profile: main builds stable branches (lpb/main), dev builds lpb-dev/dev
case "${GITHUB_REF_NAME}" in
main) [ -f lpb.stack.main.env ] && source lpb.stack.main.env ;;
dev) [ -f lpb.stack.dev.env ] && source lpb.stack.dev.env ;;
esac
set +a
echo "PI_VERSION=$LPB_PI_VERSION" >> "$GITHUB_OUTPUT"
# VERSION file is the single source (manual tagging — CI never bumps)
LPB_VERSION=$(cat VERSION)
echo "LPB_VERSION=${LPB_VERSION}" >> "$GITHUB_OUTPUT"
echo "CONFIG_FORK=$LPB_CONFIG_FORK" >> "$GITHUB_OUTPUT"
echo "CONFIG_REF=$LPB_CONFIG_REF" >> "$GITHUB_OUTPUT"
echo "NODE_VERSION=$LPB_NODE_VERSION" >> "$GITHUB_OUTPUT"
echo "VSCODIUM_VERSION=$LPB_VSCODIUM_VERSION" >> "$GITHUB_OUTPUT"
sha=$(git ls-remote "https://github.com/earendil-works/pi.git" "refs/tags/v${LPB_PI_VERSION}" | awk '{print $1}')
echo "sha=${sha:-unknown}" >> "$GITHUB_OUTPUT"
echo "stack_version=${LPB_VERSION}" >> "$GITHUB_OUTPUT"
- name: Build & push cli
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
target: cli
push: ${{ github.event_name != 'pull_request' }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
PI_VERSION=${{ steps.config.outputs.PI_VERSION }}
CONFIG_FORK=${{ steps.config.outputs.CONFIG_FORK }}
CONFIG_REF=${{ steps.config.outputs.CONFIG_REF }}
NODE_VERSION=${{ steps.config.outputs.NODE_VERSION }}
VSCODIUM_VERSION=${{ steps.config.outputs.VSCODIUM_VERSION }}
PI_HEAD_SHA=${{ steps.config.outputs.sha }}
LPB_VERSION=${{ steps.config.outputs.LPB_VERSION }}
secrets: |
GIT_AUTH_TOKEN=${{ secrets.LPB_STACK_PAT }}
no-cache: ${{ github.event.inputs.no_cache == 'true' }}
tags: |
${{ env.IMAGE_NAME }}:${{ steps.config.outputs.LPB_VERSION }}-cli
${{ github.ref_name == 'main' && format('{0}:main-cli', env.IMAGE_NAME) || format('{0}:dev-cli', env.IMAGE_NAME) }}
${{ github.ref_name == 'main' && format('{0}:latest-cli', env.IMAGE_NAME) || '' }}
${{ env.IMAGE_NAME }}:${{ github.sha }}-cli
${{ github.event_name == 'schedule' && format('{0}:weekly-cli', env.IMAGE_NAME) || '' }}
${{ github.event_name == 'workflow_dispatch' && inputs.publish_latest == 'true' && format('{0}:latest-cli', env.IMAGE_NAME) || '' }}
provenance: false
platforms: linux/amd64
build-web:
name: Build & publish web image
runs-on: ubuntu-latest
timeout-minutes: 45
needs: [build-cli, VERSION_CHECK]
if: ${{ github.event_name != 'pull_request' && needs.VERSION_CHECK.outputs.changed == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up buildx
uses: docker/setup-buildx-action@v4
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Load stack config
id: config
run: |
set -a
source lpb.stack.env
# Pipeline profile: main builds stable branches (lpb/main), dev builds lpb-dev/dev
case "${GITHUB_REF_NAME}" in
main) [ -f lpb.stack.main.env ] && source lpb.stack.main.env ;;
dev) [ -f lpb.stack.dev.env ] && source lpb.stack.dev.env ;;
esac
set +a
echo "PI_VERSION=$LPB_PI_VERSION" >> "$GITHUB_OUTPUT"
echo "CONFIG_FORK=$LPB_CONFIG_FORK" >> "$GITHUB_OUTPUT"
echo "CONFIG_REF=$LPB_CONFIG_REF" >> "$GITHUB_OUTPUT"
echo "NODE_VERSION=$LPB_NODE_VERSION" >> "$GITHUB_OUTPUT"
echo "VSCODIUM_VERSION=$LPB_VSCODIUM_VERSION" >> "$GITHUB_OUTPUT"
# VERSION file is the single source (manual tagging — CI never bumps)
LPB_VERSION=$(cat VERSION)
echo "LPB_VERSION=${LPB_VERSION}" >> "$GITHUB_OUTPUT"
sha=$(git ls-remote "https://github.com/earendil-works/pi.git" "refs/tags/v${LPB_PI_VERSION}" | awk '{print $1}')
echo "sha=${sha:-unknown}" >> "$GITHUB_OUTPUT"
echo "stack_version=${LPB_VERSION}" >> "$GITHUB_OUTPUT"
- name: Build & push web
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
target: web
push: ${{ github.event_name != 'pull_request' }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
PI_VERSION=${{ steps.config.outputs.PI_VERSION }}
CONFIG_FORK=${{ steps.config.outputs.CONFIG_FORK }}
CONFIG_REF=${{ steps.config.outputs.CONFIG_REF }}
NODE_VERSION=${{ steps.config.outputs.NODE_VERSION }}
VSCODIUM_VERSION=${{ steps.config.outputs.VSCODIUM_VERSION }}
PI_HEAD_SHA=${{ steps.config.outputs.sha }}
LPB_VERSION=${{ steps.config.outputs.LPB_VERSION }}
secrets: |
GIT_AUTH_TOKEN=${{ secrets.LPB_STACK_PAT }}
no-cache: ${{ github.event.inputs.no_cache == 'true' }}
tags: |
${{ env.IMAGE_NAME }}:${{ steps.config.outputs.LPB_VERSION }}-web
${{ github.ref_name == 'main' && format('{0}:main-web', env.IMAGE_NAME) || format('{0}:dev-web', env.IMAGE_NAME) }}
${{ github.ref_name == 'main' && format('{0}:latest-web', env.IMAGE_NAME) || '' }}
${{ env.IMAGE_NAME }}:${{ github.sha }}-web
${{ github.event_name == 'schedule' && format('{0}:weekly-web', env.IMAGE_NAME) || '' }}
${{ github.event_name == 'workflow_dispatch' && inputs.publish_latest == 'true' && format('{0}:latest-web', env.IMAGE_NAME) || '' }}
provenance: false
platforms: linux/amd64
# ──────────────────────────────────────────────────────
# Phase 3: Tag repos on success (only after build passes)
# ──────────────────────────────────────────────────────
tag-repos:
name: Tag all repos
runs-on: ubuntu-latest
needs: [build-cli, build-web, VERSION_CHECK]
if: ${{ github.event_name != 'pull_request' && needs.VERSION_CHECK.outputs.changed == 'true' && needs.build-cli.result == 'success' && needs.build-web.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Create tags on all repos
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.LPB_STACK_PAT }}
run: |
set -e
VERSION=$(cat VERSION)
# repo -> branch mapping per pipeline
# dev pipeline: dev branches; main pipeline: stable branches
if [ "${GITHUB_REF_NAME}" = "main" ]; then
declare -A REPO_BRANCHS=(
["lpb-stack/lemonade-pi-plugin"]="lpb"
["lpb-stack/config"]="main"
["lpb-stack/lpb-memory"]="main"
)
else
declare -A REPO_BRANCHS=(
["lpb-stack/lemonade-pi-plugin"]="lpb-dev"
["lpb-stack/config"]="dev"
["lpb-stack/lpb-memory"]="dev"
)
fi
FAILURES=0
for repo in "${!REPO_BRANCHS[@]}"; do
branch="${REPO_BRANCHS[$repo]}"
echo "Tagging $repo@$VERSION (from $branch)"
sha=$(git ls-remote "https://github.com/$repo.git" "refs/heads/$branch" | awk '{print $1}')
if [ -z "$sha" ]; then
# A missing branch means the repo map or branch name is wrong —
# a partial tag set would desync the stack, so fail loudly.
echo " ❌ $repo:$branch not found — aborting (fail fast)"
exit 1
fi
# POST to /git/refs with {ref, sha} body (NOT /git/refs/tags/{tagname})
# 201 = created, 422 = already exists (idempotent re-run) = success.
# Anything else (503/429/...) is retried with backoff — transient
# GitHub API outages must not produce a partially-tagged stack.
status=""
for attempt in 1 2 3 4; do
status=$(curl -s -o /dev/null -w '%{http_code}' \
-X POST "https://api.github.com/repos/$repo/git/refs" \
-H "Authorization: token $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-d "{\"ref\": \"refs/tags/$VERSION\", \"sha\": \"$sha\"}")
if [ "$status" = "201" ] || [ "$status" = "422" ]; then
break
fi
if [ "$attempt" -lt 4 ]; then
sleep $((attempt * 15))
echo " ⚠️ $repo HTTP $status — retry $((attempt + 1))/4"
fi
done
if [ "$status" = "201" ] || [ "$status" = "422" ]; then
echo " ✅ $repo@$VERSION"
else
echo " ❌ $repo tag creation failed (HTTP $status after 4 attempts)"
FAILURES=$((FAILURES + 1))
fi
done
if [ "$FAILURES" -gt 0 ]; then
echo "::error::Tag creation failed for $FAILURES repo(s) at $VERSION — the stack is now misaligned; re-run this job to complete tagging"
exit 1
fi
# ──────────────────────────────────────────────────────
# Phase 3.5: Publish stable docs (main pipeline only)
# Docs are gated into the release BEFORE promotion:
# lpb-devstack release docs-ready (merge dev→docs, review, flag)
# lpb-devstack release promote (refuses unless docs are ready)
# This job re-verifies the flag (catches --force promotions), then cuts
# the immutable docs version and points the `latest` alias at it.
# Output: gh-pages branch (mike) → https://lpb-stack.github.io/devstack/<version>/
# ──────────────────────────────────────────────────────
docs-publish:
name: Publish docs (mike)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [tag-repos, VERSION_CHECK]
if: ${{ github.ref_name == 'main' && github.event_name == 'push' && needs.VERSION_CHECK.outputs.changed == 'true' && needs.tag-repos.result == 'success' }}
steps:
- name: Checkout docs branch
uses: actions/checkout@v6
with:
ref: docs
fetch-depth: 0
- name: Verify docs flag
run: |
set -e
VERSION=$(git show origin/main:VERSION | tr -d '[:space:]')
FLAG=$(git show HEAD:DOCS_READY 2>/dev/null | tr -d '[:space:]' || true)
if [ "$FLAG" != "$VERSION" ]; then
echo "::error::docs not flagged ready for $VERSION (DOCS_READY='${FLAG:-<missing>}') — run 'lpb-devstack release docs-ready' before promoting, then re-run this job"
exit 1
fi
echo "docs flag OK: $FLAG"
- name: Fetch stack repos at the release tag
run: |
set -e
VERSION=$(git show origin/main:VERSION | tr -d '[:space:]')
mkdir -p ws/git
git clone --quiet https://github.com/lpb-stack/config.git ws/config
for r in lpb-memory lemonade-pi-plugin; do
git clone --quiet "https://github.com/lpb-stack/$r.git" "ws/git/$r"
done
for d in ws/config ws/git/lpb-memory \
ws/git/lemonade-pi-plugin; do
git -C "$d" fetch --quiet --depth 1 origin tag "$VERSION"
git -C "$d" checkout -q "$VERSION"
done
- name: Install docs tooling
run: python3 -m pip install --quiet "mkdocs-material==9.7.7" mike
- name: Prepare docs tree + version stamp
env:
AGENT_DIR: ${{ github.workspace }}/ws/config
LPB_AGENT_GIT: ${{ github.workspace }}/ws/git
LPB_WORKSPACE_ROOT: ${{ github.workspace }}/ws
run: python3 scripts/generate.py --tag "$(git show origin/main:VERSION | tr -d '[:space:]')"
- name: Configure git identity for mike
run: |
git config user.name "lpb-docs"
git config user.email "ci@lpb-stack.dev"
- name: Publish tagged docs version
run: |
set -e
VERSION=$(git show origin/main:VERSION | tr -d '[:space:]')
mike deploy "$VERSION" latest --push --update-aliases
mike set-default latest --push
echo "Docs version: https://lpb-stack.github.io/devstack/$VERSION/"
status:
name: Build status
needs: [build-cli, build-web]
if: always()
runs-on: ubuntu-latest
steps:
- run: |
cli="${{ needs.build-cli.result }}"
web="${{ needs.build-web.result }}"
if [ "$cli" = "skipped" ] && [ "$web" = "skipped" ]; then
echo "Builds skipped — no VERSION change in this push (tests only)"
exit 0
fi
if [ "$cli" != "success" ] || [ "$web" != "success" ]; then
echo "Build failed!"
exit 1
fi
echo "Build complete — both cli and web images pushed"