fix: self_update respects --tag for branch selection #245
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ═══════════════════════════════════════════════════════════ | |
| # 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 (code changes, NOT VERSION bumps) | |
| # - Push to main (Dockerfile, support/, lpb.stack.env, lpb.conf.env) | |
| # - Weekly cron (Monday 3am UTC) — keep image fresh | |
| # - Manual dispatch | |
| # | |
| # Version model (Option C): | |
| # - Single source: devstack/VERSION | |
| # - CI bumps version after tests pass, sets bumped value as CI output | |
| # - All 6 repos tagged together on each CI run (not pushed as a commit) | |
| # - Docker images tagged: :{version}-cli, :{version}-web | |
| # - Dev branch images: :dev-cli, :dev-web (always latest) | |
| # | |
| # 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' | |
| - 'support/**' | |
| - 'scripts/**' | |
| - '.github/workflows/*.yml' | |
| # NOTE: VERSION + lpb.stack.env changes intentionally excluded | |
| # to avoid re-triggering on auto-bump commits | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - 'Dockerfile' | |
| - '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/localpibox/devstack | |
| jobs: | |
| # ────────────────────────────────────────────────────── | |
| # Phase 1: Test | |
| # ────────────────────────────────────────────────────── | |
| 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 | |
| # ────────────────────────────────────────────────────── | |
| # Phase 2: Bump version + create tags | |
| # ────────────────────────────────────────────────────── | |
| bump-version: | |
| name: Bump version & create tags | |
| runs-on: ubuntu-latest | |
| needs: [test-lpb] | |
| if: ${{ github.event_name != 'pull_request' }} | |
| outputs: | |
| version: ${{ steps.bump.outputs.version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set version for build jobs | |
| id: set-version | |
| run: | | |
| VERSION=$(cat VERSION 2>/dev/null || echo "0.0.0-lpb") | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Bump patch version | |
| id: bump | |
| run: | | |
| set -e | |
| # Read current VERSION from repo (set by previous CI bump) | |
| VERSION=$(cat VERSION 2>/dev/null || echo "0.0.0-lpb") | |
| PATCH=$(echo "$VERSION" | sed 's/^[0-9]*\.[0-9]*\.\([0-9]*\).*/\1/') | |
| NEW_PATCH=$((PATCH + 1)) | |
| # Add -dev suffix for dev branch, no suffix for main | |
| if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then | |
| SUFFIX="" | |
| else | |
| SUFFIX="-dev" | |
| fi | |
| NEW_VERSION="0.0.${NEW_PATCH}-lpb${SUFFIX}" | |
| echo "Bumping $VERSION → $NEW_VERSION" | |
| echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT" | |
| # Persist bumped version back to repo (VERSION/lpb.stack.env excluded | |
| # from CI paths filter, so this won't trigger a new CI run) | |
| echo "$NEW_VERSION" > VERSION | |
| sed -i "s/^LPB_PI_REF=.*/LPB_PI_REF=$NEW_VERSION/" lpb.stack.env | |
| git config user.name "ci-localpibox" | |
| git config user.email "ci@localpibox.dev" | |
| git add VERSION lpb.stack.env | |
| git commit -m "chore: bump VERSION $NEW_VERSION" || echo "Nothing to commit" | |
| git push origin dev --force-with-lease || echo "Push failed (already up-to-date)" | |
| - name: Create tags on all repos | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ steps.bump.outputs.version }} | |
| GH_TOKEN: ${{ secrets.LOCALPIBOX_PAT }} | |
| run: | | |
| set -e | |
| # repo -> default branch mapping | |
| declare -A REPO_BRANCHS=( | |
| ["localpibox/pi"]="lpb-dev" | |
| ["localpibox/pi-subagents"]="lpb-dev" | |
| ["localpibox/lemonade-pi-plugin"]="lpb-dev" | |
| ["localpibox/config"]="dev" | |
| ["localpibox/lpb-memory"]="dev" | |
| ) | |
| 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 [ -n "$sha" ]; then | |
| # POST to /git/refs with {ref, sha} body (NOT /git/refs/tags/{tagname}) | |
| 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 | |
| echo " ✅ $repo@$VERSION" | |
| else | |
| echo " ⚠️ $repo tag creation failed (HTTP $status)" | |
| fi | |
| else | |
| echo " ⚠️ $repo:$branch not found, skipping tag" | |
| fi | |
| done | |
| # ────────────────────────────────────────────────────── | |
| # Phase 3: Build & publish images | |
| # ────────────────────────────────────────────────────── | |
| build-cli: | |
| name: Build & publish cli image | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| needs: [bump-version] | |
| if: ${{ github.event_name != 'pull_request' }} | |
| 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 | |
| set +a | |
| echo "PI_FORK=$LPB_PI_FORK" >> "$GITHUB_OUTPUT" | |
| # Use bumped version from bump-version step (workspace has old value) | |
| echo "PI_REF=${{ needs.bump-version.outputs.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" | |
| echo "LPB_VERSION=$(cat VERSION 2>/dev/null || echo unknown)" >> "$GITHUB_OUTPUT" | |
| MAX_TOKENS=$(grep -E '^LPB_MAX_TOKENS_CONTEXT_RATIO=' lpb.conf.env | cut -d= -f2- | tr -d '"' 2>/dev/null || true) | |
| echo "MAX_TOKENS=${MAX_TOKENS:-0.06}" >> "$GITHUB_OUTPUT" | |
| sha=$(git ls-remote "$LPB_PI_FORK" "refs/heads/$LPB_PI_REF" | awk '{print $1}') | |
| echo "sha=${sha:-unknown}" >> "$GITHUB_OUTPUT" | |
| echo "stack_version=${{ needs.bump-version.outputs.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_FORK=${{ steps.config.outputs.PI_FORK }} | |
| PI_REF=${{ steps.config.outputs.PI_REF }} | |
| 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 }} | |
| LPB_MAX_TOKENS_CONTEXT_RATIO=${{ steps.config.outputs.MAX_TOKENS }} | |
| secrets: | | |
| GIT_AUTH_TOKEN=${{ secrets.LOCALPIBOX_PAT }} | |
| no-cache: ${{ github.event.inputs.no_cache == 'true' }} | |
| tags: | | |
| ${{ env.IMAGE_NAME }}:${{ needs.bump-version.outputs.version }}-cli | |
| ${{ env.IMAGE_NAME }}:dev-cli | |
| ${{ env.IMAGE_NAME }}:main-cli | |
| ${{ env.IMAGE_NAME }}:${{ github.sha }}-cli | |
| ${{ github.event_name == 'schedule' && format('{0}:weekly-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, bump-version] | |
| if: ${{ github.event_name != 'pull_request' }} | |
| 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 | |
| set +a | |
| echo "PI_FORK=$LPB_PI_FORK" >> "$GITHUB_OUTPUT" | |
| echo "PI_REF=$LPB_PI_REF" >> "$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" | |
| echo "LPB_VERSION=$(cat VERSION 2>/dev/null || echo unknown)" >> "$GITHUB_OUTPUT" | |
| MAX_TOKENS=$(grep -E '^LPB_MAX_TOKENS_CONTEXT_RATIO=' lpb.conf.env | cut -d= -f2- | tr -d '"' 2>/dev/null || true) | |
| echo "MAX_TOKENS=${MAX_TOKENS:-0.06}" >> "$GITHUB_OUTPUT" | |
| sha=$(git ls-remote "$LPB_PI_FORK" "refs/heads/$LPB_PI_REF" | awk '{print $1}') | |
| echo "sha=${sha:-unknown}" >> "$GITHUB_OUTPUT" | |
| echo "stack_version=${{ needs.bump-version.outputs.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_FORK=${{ steps.config.outputs.PI_FORK }} | |
| PI_REF=${{ steps.config.outputs.PI_REF }} | |
| 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 }} | |
| LPB_MAX_TOKENS_CONTEXT_RATIO=${{ steps.config.outputs.MAX_TOKENS }} | |
| secrets: | | |
| GIT_AUTH_TOKEN=${{ secrets.LOCALPIBOX_PAT }} | |
| no-cache: ${{ github.event.inputs.no_cache == 'true' }} | |
| tags: | | |
| ${{ env.IMAGE_NAME }}:${{ needs.bump-version.outputs.version }}-web | |
| ${{ env.IMAGE_NAME }}:dev-web | |
| ${{ env.IMAGE_NAME }}:main-web | |
| ${{ env.IMAGE_NAME }}:${{ github.sha }}-web | |
| ${{ github.event_name == 'schedule' && format('{0}:weekly-web', env.IMAGE_NAME) || '' }} | |
| provenance: false | |
| platforms: linux/amd64 | |
| status: | |
| name: Build status | |
| needs: [build-cli, build-web] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - run: | | |
| if [ "${{ needs.build-cli.result }}" != "success" ] || [ "${{ needs.build-web.result }}" != "success" ]; then | |
| echo "Build failed!" | |
| exit 1 | |
| fi | |
| echo "Build complete — both cli and web images pushed" |