55# Extensions update at runtime via pi update --extensions
66#
77# Triggers:
8+ # - Push to dev (code changes, NOT VERSION bumps)
89# - Push to main (Dockerfile, support/, lpb.stack.env, lpb.conf.env)
910# - Weekly cron (Monday 3am UTC) — keep image fresh
1011# - Manual dispatch
1112#
12- # Images built:
13- # ghcr.io/localpibox/devstack:cli — Base dev environment + Pi CLI
14- # ghcr.io/localpibox/devstack:web — Extends cli + VSCodium server
13+ # Version model (Option C):
14+ # - Single source: devstack/VERSION
15+ # - CI bumps version after tests pass, sets bumped value as CI output
16+ # - All 6 repos tagged together on each CI run (not pushed as a commit)
17+ # - Docker images tagged: :{version}-cli, :{version}-web
18+ # - Dev branch images: :dev-cli, :dev-web (always latest)
1519#
1620# Actions: all latest major versions (Node.js 24 native)
1721# actions/checkout@v6 · docker/build-push-action@v7
2731 paths :
2832 - ' Dockerfile'
2933 - ' support/**'
30- - ' lpb.stack.env'
31- - ' lpb.conf.env'
32- - ' .github/workflows/*.yml'
3334 - ' scripts/**'
35+ - ' .github/workflows/*.yml'
36+ # NOTE: VERSION + lpb.stack.env changes intentionally excluded
37+ # to avoid re-triggering on auto-bump commits
3438 pull_request :
3539 branches : [main]
3640 paths :
3741 - ' Dockerfile'
3842 - ' support/**'
39- - ' lpb.stack.env'
40- - ' lpb.conf.env'
41- - ' .github/workflows/*.yml'
4243 - ' scripts/**'
44+ - ' .github/workflows/*.yml'
4345 schedule :
4446 - cron : ' 0 3 * * 1'
4547 workflow_dispatch :
5456 default : false
5557
5658permissions :
57- contents : read
59+ contents : write
5860 packages : write
5961
6062env :
6163 IMAGE_NAME : ghcr.io/localpibox/devstack
6264
6365jobs :
66+ # ──────────────────────────────────────────────────────
67+ # Phase 1: Test
68+ # ──────────────────────────────────────────────────────
6469 test-lpb :
6570 name : Run lpb.py unit tests
6671 runs-on : ubuntu-latest
@@ -73,14 +78,108 @@ jobs:
7378 python3 scripts/test_lpb.py
7479 python3 scripts/test_localpibox.py
7580
81+ # ──────────────────────────────────────────────────────
82+ # Phase 2: Bump version + create tags
83+ # ──────────────────────────────────────────────────────
84+ bump-version :
85+ name : Bump version & create tags
86+ runs-on : ubuntu-latest
87+ needs : [test-lpb]
88+ if : ${{ github.event_name != 'pull_request' }}
89+ outputs :
90+ version : ${{ steps.bump.outputs.version }}
91+ steps :
92+ - name : Checkout
93+ uses : actions/checkout@v6
94+ with :
95+ fetch-depth : 0
96+
97+ - name : Set version for build jobs
98+ id : set-version
99+ run : |
100+ VERSION=$(cat VERSION 2>/dev/null || echo "0.0.0-lpb")
101+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
102+
103+ - name : Bump patch version
104+ id : bump
105+ run : |
106+ set -e
107+ # Read current VERSION from repo (set by previous CI bump)
108+ VERSION=$(cat VERSION 2>/dev/null || echo "0.0.0-lpb")
109+ PATCH=$(echo "$VERSION" | sed 's/^[0-9]*\.[0-9]*\.\([0-9]*\).*/\1/')
110+ NEW_PATCH=$((PATCH + 1))
111+
112+ # Add -dev suffix for dev branch, no suffix for main
113+ if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
114+ SUFFIX=""
115+ else
116+ SUFFIX="-dev"
117+ fi
118+ NEW_VERSION="0.0.${NEW_PATCH}-lpb${SUFFIX}"
119+
120+ echo "Bumping $VERSION → $NEW_VERSION"
121+ echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
122+
123+ # Persist bumped version back to repo (VERSION/lpb.stack.env excluded
124+ # from CI paths filter, so this won't trigger a new CI run)
125+ echo "$NEW_VERSION" > VERSION
126+ sed -i "s/^LPB_PI_REF=.*/LPB_PI_REF=$NEW_VERSION/" lpb.stack.env
127+ git config user.name "ci-localpibox"
128+ git config user.email "ci@localpibox.dev"
129+ git add VERSION lpb.stack.env
130+ git commit -m "chore: bump VERSION $NEW_VERSION" || echo "Nothing to commit"
131+ git push origin dev --force-with-lease || echo "Push failed (already up-to-date)"
132+
133+ - name : Create tags on all repos
134+ env :
135+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
136+ VERSION : ${{ steps.bump.outputs.version }}
137+ GH_TOKEN : ${{ secrets.LOCALPIBOX_PAT }}
138+ run : |
139+ set -e
140+ # repo -> default branch mapping
141+ declare -A REPO_BRANCHS=(
142+ ["localpibox/pi"]="lpb-dev"
143+ ["localpibox/pi-subagents"]="lpb-dev"
144+ ["localpibox/lemonade-pi-plugin"]="lpb-dev"
145+ ["localpibox/config"]="dev"
146+ ["localpibox/lpb-memory"]="dev"
147+ )
148+ for repo in "${!REPO_BRANCHS[@]}"; do
149+ branch="${REPO_BRANCHS[$repo]}"
150+ echo "Tagging $repo@$VERSION (from $branch)"
151+ sha=$(git ls-remote "https://github.com/$repo.git" "refs/heads/$branch" | awk '{print $1}')
152+ if [ -n "$sha" ]; then
153+ # POST to /git/refs with {ref, sha} body (NOT /git/refs/tags/{tagname})
154+ status=$(curl -s -o /dev/null -w '%{http_code}' \
155+ -X POST "https://api.github.com/repos/$repo/git/refs" \
156+ -H "Authorization: token $GH_TOKEN" \
157+ -H "Accept: application/vnd.github+json" \
158+ -d "{\"ref\": \"refs/tags/$VERSION\", \"sha\": \"$sha\"}")
159+ if [ "$status" = "201" ] || [ "$status" = "422" ]; then
160+ echo " ✅ $repo@$VERSION"
161+ else
162+ echo " ⚠️ $repo tag creation failed (HTTP $status)"
163+ fi
164+ else
165+ echo " ⚠️ $repo:$branch not found, skipping tag"
166+ fi
167+ done
168+
169+ # ──────────────────────────────────────────────────────
170+ # Phase 3: Build & publish images
171+ # ──────────────────────────────────────────────────────
76172 build-cli :
77173 name : Build & publish cli image
78174 runs-on : ubuntu-latest
79175 timeout-minutes : 45
80- needs : [test-lpb]
176+ needs : [bump-version]
177+ if : ${{ github.event_name != 'pull_request' }}
81178 steps :
82179 - name : Checkout
83180 uses : actions/checkout@v6
181+ with :
182+ persist-credentials : true
84183
85184 - name : Set up QEMU
86185 uses : docker/setup-qemu-action@v4
@@ -102,7 +201,8 @@ jobs:
102201 source lpb.stack.env
103202 set +a
104203 echo "PI_FORK=$LPB_PI_FORK" >> "$GITHUB_OUTPUT"
105- echo "PI_REF=$LPB_PI_REF" >> "$GITHUB_OUTPUT"
204+ # Use bumped version from bump-version step (workspace has old value)
205+ echo "PI_REF=${{ needs.bump-version.outputs.version }}" >> "$GITHUB_OUTPUT"
106206 echo "CONFIG_FORK=$LPB_CONFIG_FORK" >> "$GITHUB_OUTPUT"
107207 echo "CONFIG_REF=$LPB_CONFIG_REF" >> "$GITHUB_OUTPUT"
108208 echo "NODE_VERSION=$LPB_NODE_VERSION" >> "$GITHUB_OUTPUT"
@@ -111,11 +211,8 @@ jobs:
111211 MAX_TOKENS=$(grep -E '^LPB_MAX_TOKENS_CONTEXT_RATIO=' lpb.conf.env | cut -d= -f2- | tr -d '"' 2>/dev/null || true)
112212 echo "MAX_TOKENS=${MAX_TOKENS:-0.06}" >> "$GITHUB_OUTPUT"
113213 sha=$(git ls-remote "$LPB_PI_FORK" "refs/heads/$LPB_PI_REF" | awk '{print $1}')
114- # Read stack version from config repo
115- cfg_repo=$(echo "$LPB_CONFIG_FORK" | sed -E 's#https://github.com/([^/]+)/([^./]+)\.git#\1/\2#')
116- stack_ver=$(curl -sf "https://raw.githubusercontent.com/$cfg_repo/$LPB_CONFIG_REF/VERSION" 2>/dev/null || echo "unknown")
117214 echo "sha=${sha:-unknown}" >> "$GITHUB_OUTPUT"
118- echo "stack_version=${stack_ver }" >> "$GITHUB_OUTPUT"
215+ echo "stack_version=${{ needs.bump-version.outputs.version } }" >> "$GITHUB_OUTPUT"
119216
120217 - name : Build & push cli
121218 uses : docker/build-push-action@v7
@@ -136,16 +233,14 @@ jobs:
136233 PI_HEAD_SHA=${{ steps.config.outputs.sha }}
137234 LPB_VERSION=${{ steps.config.outputs.LPB_VERSION }}
138235 LPB_MAX_TOKENS_CONTEXT_RATIO=${{ steps.config.outputs.MAX_TOKENS }}
236+ secrets : |
237+ GIT_AUTH_TOKEN=${{ secrets.LOCALPIBOX_PAT }}
139238 no-cache : ${{ github.event.inputs.no_cache == 'true' }}
140239 tags : |
141- ${{ env.IMAGE_NAME }}:cli
240+ ${{ env.IMAGE_NAME }}:${{ needs.bump-version.outputs.version }}- cli
142241 ${{ env.IMAGE_NAME }}:dev-cli
143242 ${{ env.IMAGE_NAME }}:main-cli
144243 ${{ env.IMAGE_NAME }}:${{ github.sha }}-cli
145- ${{ env.IMAGE_NAME }}:${{ steps.config.outputs.stack_version }}-cli
146- ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && format('{0}:latest', env.IMAGE_NAME) || '' }}
147- ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' && format('{0}:dev', env.IMAGE_NAME) || '' }}
148- ${{ github.event.inputs.publish_latest && format('{0}:latest', env.IMAGE_NAME) || '' }}
149244 ${{ github.event_name == 'schedule' && format('{0}:weekly-cli', env.IMAGE_NAME) || '' }}
150245 provenance : false
151246 platforms : linux/amd64
@@ -154,10 +249,13 @@ jobs:
154249 name : Build & publish web image
155250 runs-on : ubuntu-latest
156251 timeout-minutes : 45
157- needs : [build-cli]
252+ needs : [build-cli, bump-version]
253+ if : ${{ github.event_name != 'pull_request' }}
158254 steps :
159255 - name : Checkout
160256 uses : actions/checkout@v6
257+ with :
258+ persist-credentials : true
161259
162260 - name : Set up QEMU
163261 uses : docker/setup-qemu-action@v4
@@ -188,11 +286,8 @@ jobs:
188286 MAX_TOKENS=$(grep -E '^LPB_MAX_TOKENS_CONTEXT_RATIO=' lpb.conf.env | cut -d= -f2- | tr -d '"' 2>/dev/null || true)
189287 echo "MAX_TOKENS=${MAX_TOKENS:-0.06}" >> "$GITHUB_OUTPUT"
190288 sha=$(git ls-remote "$LPB_PI_FORK" "refs/heads/$LPB_PI_REF" | awk '{print $1}')
191- # Read stack version from config repo
192- cfg_repo=$(echo "$LPB_CONFIG_FORK" | sed -E 's#https://github.com/([^/]+)/([^./]+)\.git#\1/\2#')
193- stack_ver=$(curl -sf "https://raw.githubusercontent.com/$cfg_repo/$LPB_CONFIG_REF/VERSION" 2>/dev/null || echo "unknown")
194289 echo "sha=${sha:-unknown}" >> "$GITHUB_OUTPUT"
195- echo "stack_version=${stack_ver }" >> "$GITHUB_OUTPUT"
290+ echo "stack_version=${{ needs.bump-version.outputs.version } }" >> "$GITHUB_OUTPUT"
196291
197292 - name : Build & push web
198293 uses : docker/build-push-action@v7
@@ -213,15 +308,14 @@ jobs:
213308 PI_HEAD_SHA=${{ steps.config.outputs.sha }}
214309 LPB_VERSION=${{ steps.config.outputs.LPB_VERSION }}
215310 LPB_MAX_TOKENS_CONTEXT_RATIO=${{ steps.config.outputs.MAX_TOKENS }}
311+ secrets : |
312+ GIT_AUTH_TOKEN=${{ secrets.LOCALPIBOX_PAT }}
216313 no-cache : ${{ github.event.inputs.no_cache == 'true' }}
217314 tags : |
218- ${{ env.IMAGE_NAME }}:web
315+ ${{ env.IMAGE_NAME }}:${{ needs.bump-version.outputs.version }}- web
219316 ${{ env.IMAGE_NAME }}:dev-web
220317 ${{ env.IMAGE_NAME }}:main-web
221318 ${{ env.IMAGE_NAME }}:${{ github.sha }}-web
222- ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && format('{0}:latest-web', env.IMAGE_NAME) || '' }}
223- ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' && format('{0}:dev-web', env.IMAGE_NAME) || '' }}
224- ${{ github.event.inputs.publish_latest && format('{0}:latest-web', env.IMAGE_NAME) || '' }}
225319 ${{ github.event_name == 'schedule' && format('{0}:weekly-web', env.IMAGE_NAME) || '' }}
226320 provenance : false
227321 platforms : linux/amd64
0 commit comments