OpenClaw Upgrade Check #6
Workflow file for this run
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
| name: OpenClaw Upgrade Check | |
| # Automated weekly upgrade pipeline: | |
| # 1. check-version — Compare current vs latest stable OpenClaw release | |
| # 2. docker-tests — Bump Dockerfile, build image, run smoke tests locally | |
| # 3. push-e2e-image — Push tested image to GHCR for App Platform E2E | |
| # 4. app-platform-e2e — Deploy to App Platform, run live health checks | |
| # 5. open-pr — Create upgrade PR if all tests pass | |
| # 6. open-issue — Create issue + trigger Copilot agent if tests fail | |
| on: | |
| schedule: | |
| - cron: '0 14 * * 1' # Every Monday at 2pm UTC | |
| workflow_dispatch: | |
| inputs: | |
| force_version: | |
| description: 'Force upgrade to this version (leave empty for latest)' | |
| required: false | |
| type: string | |
| skip_to_e2e: | |
| description: 'Skip build/Docker tests — provide existing GHCR image tag for E2E only' | |
| required: false | |
| type: string | |
| pull_request: | |
| branches: [main] | |
| # Run full pipeline (Docker + E2E) on agent fix PRs so CI validates before merge | |
| paths: | |
| - 'Dockerfile' | |
| - 'rootfs/**' | |
| - 'tests/**' | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| packages: write | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 1: Check for new version | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| jobs: | |
| check-version: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| needs_upgrade: ${{ steps.check.outputs.needs_upgrade }} | |
| current: ${{ steps.check.outputs.current }} | |
| latest: ${{ steps.check.outputs.latest }} | |
| release_notes: ${{ steps.check.outputs.release_notes }} | |
| has_regressions: ${{ steps.regressions.outputs.has_regressions }} | |
| regression_list: ${{ steps.regressions.outputs.regression_list }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Check for new stable version | |
| id: check | |
| run: | | |
| CURRENT=$(grep 'OPENCLAW_VERSION=' Dockerfile | head -1 | sed 's/.*=//') | |
| echo "current=$CURRENT" >> $GITHUB_OUTPUT | |
| # On pull_request: the PR already has the version bump — just validate it | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "PR mode: testing version already in Dockerfile ($CURRENT)" | |
| echo "latest=$CURRENT" >> $GITHUB_OUTPUT | |
| echo "needs_upgrade=true" >> $GITHUB_OUTPUT | |
| else | |
| if [ -n "${{ inputs.force_version }}" ]; then | |
| LATEST="${{ inputs.force_version }}" | |
| echo "Forced version: $LATEST" | |
| else | |
| LATEST=$(curl -s https://api.github.com/repos/openclaw/openclaw/releases | \ | |
| jq -r '[.[] | select(.prerelease == false)][0].name' | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' || echo "") | |
| fi | |
| echo "latest=$LATEST" >> $GITHUB_OUTPUT | |
| if [ -z "$LATEST" ]; then | |
| echo "needs_upgrade=false" >> $GITHUB_OUTPUT | |
| echo "Could not determine latest version" | |
| elif [ "$CURRENT" = "$LATEST" ]; then | |
| echo "needs_upgrade=false" >> $GITHUB_OUTPUT | |
| echo "Already on latest: $CURRENT" | |
| else | |
| echo "needs_upgrade=true" >> $GITHUB_OUTPUT | |
| echo "Upgrade available: $CURRENT → $LATEST" | |
| fi | |
| fi | |
| # Get release notes | |
| NOTES=$(curl -s https://api.github.com/repos/openclaw/openclaw/releases | \ | |
| jq -r '.[] | select(.prerelease == false) | "## \(.name)\n\(.body)\n"' | head -500) | |
| echo "release_notes<<EOF" >> $GITHUB_OUTPUT | |
| echo "$NOTES" | head -200 >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Check for known regressions | |
| if: steps.check.outputs.needs_upgrade == 'true' | |
| id: regressions | |
| run: | | |
| VERSION=${{ steps.check.outputs.latest }} | |
| ISSUES=$(curl -s "https://api.github.com/search/issues?q=repo:openclaw/openclaw+label:regression+$VERSION&per_page=5" | \ | |
| jq -r '.items[] | "- #\(.number): \(.title)"') | |
| if [ -n "$ISSUES" ]; then | |
| echo "has_regressions=true" >> $GITHUB_OUTPUT | |
| echo "regression_list<<EOF" >> $GITHUB_OUTPUT | |
| echo "$ISSUES" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_regressions=false" >> $GITHUB_OUTPUT | |
| fi | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| # Job 2: Build and run Docker smoke tests | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| docker-tests: | |
| needs: check-version | |
| if: needs.check-version.outputs.needs_upgrade == 'true' && inputs.skip_to_e2e == '' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| test_result: ${{ steps.test.outputs.result }} | |
| test_log: ${{ steps.test.outputs.log }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Bump version in Dockerfile | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| VERSION=${{ needs.check-version.outputs.latest }} | |
| sed -i "s/OPENCLAW_VERSION=.*/OPENCLAW_VERSION=${VERSION}/" Dockerfile | |
| echo "Bumped to $VERSION" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build Docker image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| load: true | |
| tags: openclaw-test:latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Save image for later jobs | |
| run: docker save openclaw-test:latest -o /tmp/openclaw-test.tar | |
| - name: Upload image artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: upgrade-docker-image | |
| path: /tmp/openclaw-test.tar | |
| retention-days: 1 | |
| - name: Start container | |
| run: | | |
| docker run -d --name openclaw-upgrade-test \ | |
| -e OPENCLAW_GATEWAY_TOKEN=test-token-upgrade \ | |
| openclaw-test:latest | |
| sleep 10 | |
| - name: Run upgrade verification | |
| id: test | |
| env: | |
| TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} | |
| run: | | |
| set +e | |
| chmod +x tests/minimal/04-upgrade-verify.sh | |
| OUTPUT=$(./tests/minimal/04-upgrade-verify.sh openclaw-upgrade-test 2>&1) | |
| EXIT_CODE=$? | |
| set -e | |
| echo "$OUTPUT" | |
| # Store results for downstream jobs | |
| echo "log<<EOF" >> $GITHUB_OUTPUT | |
| echo "$OUTPUT" | tail -40 >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| if [ $EXIT_CODE -eq 0 ]; then | |
| echo "result=pass" >> $GITHUB_OUTPUT | |
| else | |
| echo "result=fail" >> $GITHUB_OUTPUT | |
| fi | |
| exit $EXIT_CODE | |
| - name: Dump container state on failure | |
| if: failure() | |
| run: | | |
| echo "=== Container status ===" | |
| docker ps -a --filter name=openclaw-upgrade-test | |
| echo "" | |
| echo "=== Process list ===" | |
| docker exec openclaw-upgrade-test ps aux 2>/dev/null || echo "Container not running" | |
| echo "" | |
| echo "=== Service status ===" | |
| for svc in $(docker exec openclaw-upgrade-test ls /run/service/ 2>/dev/null); do | |
| echo -n "$svc: " | |
| docker exec openclaw-upgrade-test /command/s6-svstat "/run/service/$svc" 2>/dev/null || echo "unknown" | |
| done | |
| echo "" | |
| echo "=== Container logs (last 50 lines) ===" | |
| docker logs --tail 50 openclaw-upgrade-test 2>&1 || true | |
| - name: Cleanup container | |
| if: always() | |
| run: docker rm -f openclaw-upgrade-test 2>/dev/null || true | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| # Job 3: Push tested image to GHCR for App Platform E2E | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| push-e2e-image: | |
| needs: [check-version, docker-tests] | |
| if: inputs.skip_to_e2e == '' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| image_tag: ${{ steps.tag.outputs.tag }} | |
| steps: | |
| - name: Download image artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: upgrade-docker-image | |
| path: /tmp | |
| - name: Load image | |
| run: docker load -i /tmp/openclaw-test.tar | |
| - name: Compute tag | |
| id: tag | |
| run: echo "tag=e2e-${{ github.run_id }}" >> $GITHUB_OUTPUT | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Tag and push | |
| run: | | |
| TAG="${{ steps.tag.outputs.tag }}" | |
| docker tag openclaw-test:latest ghcr.io/${{ github.repository }}:${TAG} | |
| docker push ghcr.io/${{ github.repository }}:${TAG} | |
| echo "Pushed ghcr.io/${{ github.repository }}:${TAG}" | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| # Job 4: App Platform E2E (deploy, test, teardown) | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| app-platform-e2e: | |
| needs: [check-version, docker-tests, push-e2e-image] | |
| runs-on: ubuntu-latest | |
| # Run if docker-tests passed, OR if skip_to_e2e is set (debugging E2E directly) | |
| if: always() && (needs.docker-tests.result == 'success' || inputs.skip_to_e2e != '') | |
| outputs: | |
| e2e_result: ${{ steps.e2e.outputs.result }} | |
| e2e_log: ${{ steps.e2e.outputs.log }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install doctl | |
| uses: digitalocean/action-doctl@v2 | |
| with: | |
| token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} | |
| - name: Run App Platform E2E | |
| id: e2e | |
| env: | |
| DOCTL_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} | |
| IMAGE_TAG: ${{ inputs.skip_to_e2e || needs.push-e2e-image.outputs.image_tag }} | |
| GHCR_REPOSITORY: ${{ github.repository }} | |
| RUN_ID: ${{ github.run_id }} | |
| TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} | |
| DEPLOY_TIMEOUT: "600" | |
| run: | | |
| if [ -z "$DOCTL_TOKEN" ]; then | |
| echo "SKIP: DOCTL_TOKEN not configured — skipping App Platform E2E" | |
| echo "result=skip" >> $GITHUB_OUTPUT | |
| echo "log=Skipped: DOCTL_TOKEN not set" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| set +e | |
| chmod +x tests/e2e/deploy-and-test.sh | |
| OUTPUT=$(./tests/e2e/deploy-and-test.sh 2>&1) | |
| EXIT_CODE=$? | |
| set -e | |
| echo "$OUTPUT" | |
| echo "log<<EOF" >> $GITHUB_OUTPUT | |
| echo "$OUTPUT" | tail -40 >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| if [ $EXIT_CODE -eq 0 ]; then | |
| echo "result=pass" >> $GITHUB_OUTPUT | |
| else | |
| echo "result=fail" >> $GITHUB_OUTPUT | |
| fi | |
| exit $EXIT_CODE | |
| - name: Cleanup stale E2E apps (janitor) | |
| if: always() | |
| env: | |
| DOCTL_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} | |
| run: | | |
| if [ -n "$DOCTL_TOKEN" ]; then | |
| chmod +x tests/e2e/cleanup-stale-apps.sh | |
| MAX_AGE_HOURS=2 ./tests/e2e/cleanup-stale-apps.sh || true | |
| fi | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| # Job 5: Open PR if all tests passed | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| open-pr: | |
| needs: [check-version, docker-tests, app-platform-e2e] | |
| if: | | |
| always() && | |
| github.event_name != 'pull_request' && | |
| needs.docker-tests.result == 'success' && | |
| (needs.app-platform-e2e.result == 'success' || needs.app-platform-e2e.outputs.e2e_result == 'skip') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Create upgrade PR | |
| run: | | |
| VERSION=${{ needs.check-version.outputs.latest }} | |
| CURRENT=${{ needs.check-version.outputs.current }} | |
| BRANCH="upgrade-openclaw-${VERSION}" | |
| # Bump version | |
| sed -i "s/OPENCLAW_VERSION=.*/OPENCLAW_VERSION=${VERSION}/" Dockerfile | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Check if branch already exists (re-run scenario) | |
| if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then | |
| echo "Branch $BRANCH already exists — force-pushing updated version" | |
| git checkout -B "$BRANCH" | |
| else | |
| git checkout -b "$BRANCH" | |
| fi | |
| git add Dockerfile | |
| git commit -m "Upgrade OpenClaw to ${VERSION}" --allow-empty | |
| git push -f origin "$BRANCH" | |
| # Build PR body | |
| E2E_STATUS="✅ Passed" | |
| if [ "${{ needs.app-platform-e2e.outputs.e2e_result }}" = "skip" ]; then | |
| E2E_STATUS="⏭️ Skipped (DOCTL_TOKEN not configured)" | |
| fi | |
| REGRESSION_NOTE="" | |
| if [ "${{ needs.check-version.outputs.has_regressions }}" = "true" ]; then | |
| REGRESSION_NOTE=$(cat <<'REGEOF' | |
| ## ⚠️ Known Upstream Regressions | |
| ${{ needs.check-version.outputs.regression_list }} | |
| **Review these before merging.** | |
| REGEOF | |
| ) | |
| fi | |
| # Check if PR already exists | |
| EXISTING_PR=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "") | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "PR #$EXISTING_PR already exists — updating" | |
| exit 0 | |
| fi | |
| gh pr create \ | |
| --title "Upgrade OpenClaw to ${VERSION}" \ | |
| --body "## Automated Upgrade: ${CURRENT} → ${VERSION} | |
| ### Test Results | |
| | Test | Result | | |
| |---|---| | |
| | Docker image builds | ✅ | | |
| | Version matches Dockerfile | ✅ | | |
| | Config valid JSON + correct defaults | ✅ | | |
| | Config owned by openclaw user | ✅ | | |
| | Gateway process running | ✅ | | |
| | Gateway HTTP responds 200 | ✅ | | |
| | Tailscale binary present | ✅ | | |
| | Channel plugins loaded | ✅ | | |
| | Backup config valid | ✅ | | |
| | openclaw doctor completes | ✅ | | |
| | App Platform E2E | ${E2E_STATUS} | | |
| ${REGRESSION_NOTE} | |
| ## Release Notes | |
| ${{ needs.check-version.outputs.release_notes }} | |
| --- | |
| **This is an automated version bump. Only the Dockerfile was changed.** | |
| If release notes mention config/schema changes, manual review is required. | |
| Do not merge if there are known regressions above. | |
| <details><summary>Docker test output</summary> | |
| \`\`\` | |
| ${{ needs.docker-tests.outputs.test_log }} | |
| \`\`\` | |
| </details>" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| # Job 6: Open issue if tests failed — triggers Copilot agent investigation | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| open-issue: | |
| needs: [check-version, docker-tests, app-platform-e2e] | |
| if: | | |
| always() && | |
| github.event_name != 'pull_request' && | |
| needs.check-version.outputs.needs_upgrade == 'true' && | |
| (needs.docker-tests.result == 'failure' || (needs.app-platform-e2e.result == 'failure' && needs.app-platform-e2e.outputs.e2e_result != 'skip')) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Determine failure source | |
| id: failure | |
| run: | | |
| if [ "${{ needs.docker-tests.result }}" = "failure" ]; then | |
| echo "stage=Docker smoke tests" >> $GITHUB_OUTPUT | |
| echo "log<<EOF" >> $GITHUB_OUTPUT | |
| cat <<'LOGEOF' >> $GITHUB_OUTPUT | |
| ${{ needs.docker-tests.outputs.test_log }} | |
| LOGEOF | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| else | |
| echo "stage=App Platform E2E" >> $GITHUB_OUTPUT | |
| echo "log<<EOF" >> $GITHUB_OUTPUT | |
| cat <<'LOGEOF' >> $GITHUB_OUTPUT | |
| ${{ needs.app-platform-e2e.outputs.e2e_log }} | |
| LOGEOF | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create issue for failed upgrade | |
| run: | | |
| VERSION=${{ needs.check-version.outputs.latest }} | |
| CURRENT=${{ needs.check-version.outputs.current }} | |
| REGRESSION_NOTE="" | |
| if [ "${{ needs.check-version.outputs.has_regressions }}" = "true" ]; then | |
| REGRESSION_NOTE="### Known Regressions | |
| ${{ needs.check-version.outputs.regression_list }} | |
| " | |
| fi | |
| # Check if issue already exists for this version | |
| EXISTING=$(gh issue list --search "OpenClaw ${VERSION} upgrade failed" --json number --jq '.[0].number' 2>/dev/null || echo "") | |
| if [ -n "$EXISTING" ]; then | |
| echo "Issue #$EXISTING already exists for version $VERSION — adding comment" | |
| gh issue comment "$EXISTING" --body "Workflow re-run failed again. | |
| [Workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | |
| **Failed stage:** ${{ steps.failure.outputs.stage }} | |
| \`\`\` | |
| ${{ steps.failure.outputs.log }} | |
| \`\`\`" | |
| exit 0 | |
| fi | |
| gh issue create \ | |
| --title "⚠️ OpenClaw ${VERSION} upgrade failed automated tests" \ | |
| --body "## Automated Upgrade Failed: ${CURRENT} → ${VERSION} | |
| **Failed stage:** ${{ steps.failure.outputs.stage }} | |
| **Workflow run:** [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | |
| ### Test Output | |
| \`\`\` | |
| ${{ steps.failure.outputs.log }} | |
| \`\`\` | |
| ${REGRESSION_NOTE} | |
| ### Release Notes | |
| ${{ needs.check-version.outputs.release_notes }} | |
| --- | |
| ### For the Copilot Agent | |
| @copilot Please investigate this failed upgrade using the \`/openclaw-upgrader\` prompt. | |
| **Steps:** | |
| 1. Read \`UPGRADE-AUTOMATION-CONTEXT.md\` for full context on how upgrades work | |
| 2. Read the release notes above and the [full changelog](https://github.com/openclaw/openclaw/releases) | |
| 3. Identify the root cause of the test failure | |
| 4. Apply the **minimum necessary fix** following the rules in \`.github/copilot-instructions.md\` | |
| 5. Push a branch and open a PR — CI will run the full test suite | |
| 6. If CI passes, the PR is ready for human review | |
| 7. If CI fails after 2 attempts, comment with your analysis explaining what blocked it | |
| **Critical rules:** | |
| - Most upgrades need ONLY a Dockerfile version bump | |
| - NEVER modify \`rootfs/etc/services.d/openclaw/run\` | |
| - Use \`jq '.key //= \"value\"'\` (set-if-missing) for new defaults | |
| - Every jq write MUST be followed by \`chown openclaw:openclaw\` | |
| - If there's a known upstream regression, recommend skipping the version" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| # Cleanup: Delete GHCR E2E image after all jobs complete | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| cleanup-image: | |
| needs: [push-e2e-image, app-platform-e2e, open-pr, open-issue] | |
| if: always() && needs.push-e2e-image.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Note about GHCR cleanup | |
| run: | | |
| TAG="${{ needs.push-e2e-image.outputs.image_tag }}" | |
| echo "E2E image ghcr.io/${{ github.repository }}:${TAG} can be cleaned up." | |
| echo "GHCR image deletion requires a separate PAT or manual cleanup." | |
| echo "Images with 'e2e-' prefix and >24h age can be safely purged." |