fix: preflight compact v3 context before model calls (#1297) #85
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: Dev CI and Beta Release | |
| on: | |
| push: | |
| branches: [dev] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| env: | |
| GHCR_IMAGE: ghcr.io/${{ github.repository }} | |
| DOCKERHUB_IMAGE: digitop/goclaw | |
| INITIAL_VERSION: 3.11.3 | |
| PRERELEASE_ID: beta | |
| jobs: | |
| go: | |
| runs-on: ubuntu-latest | |
| services: | |
| pg: | |
| image: pgvector/pgvector:pg18 | |
| env: | |
| POSTGRES_PASSWORD: test | |
| POSTGRES_DB: goclaw_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 5s | |
| --health-timeout 3s | |
| --health-retries 10 | |
| env: | |
| TEST_DATABASE_URL: postgres://postgres:test@localhost:5432/goclaw_test?sslmode=disable | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| - run: go build ./... | |
| - run: go build -tags sqliteonly ./... | |
| - run: go vet ./... | |
| - name: Unit tests | |
| run: go test -race -timeout=5m -coverpkg=./... -coverprofile=coverage.out ./... | |
| - name: Invariant tests (P0) | |
| run: go test -race -timeout=90s -tags integration ./tests/invariants/... | |
| - name: Contract tests (P1) | |
| run: go test -race -timeout=90s -tags integration ./tests/contracts/... || echo "::warning::Contract tests skipped (no server configured)" | |
| continue-on-error: true | |
| - name: Integration tests | |
| run: go test -race -timeout=180s -tags integration ./tests/integration/ | |
| - name: Coverage summary | |
| run: go tool cover -func=coverage.out | tail -1 | |
| web: | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: ui/web | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| cache-dependency-path: ui/web/pnpm-lock.yaml | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm lint | |
| - run: pnpm build | |
| beta_version: | |
| needs: [go, web] | |
| if: github.ref == 'refs/heads/dev' | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: dev-beta-version-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| outputs: | |
| released: ${{ steps.version.outputs.released }} | |
| version: ${{ steps.version.outputs.version }} | |
| tag: ${{ steps.version.outputs.tag }} | |
| notes_path: ${{ steps.version.outputs.notes_path }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Release workflow tests | |
| run: node --test scripts/ci/semantic-beta-version.test.mjs scripts/ci/dev-beta-release-workflow.test.mjs | |
| - name: Fetch upstream release tags | |
| run: git fetch --force --tags https://github.com/nextlevelbuilder/goclaw.git "refs/tags/v*:refs/tags/v*" | |
| - name: Compute semantic beta version | |
| id: version | |
| run: node scripts/ci/semantic-beta-version.mjs | |
| - name: Create or verify beta tag | |
| if: steps.version.outputs.released == 'true' | |
| env: | |
| TAG: ${{ steps.version.outputs.tag }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| if ! git rev-parse "$TAG" >/dev/null 2>&1; then | |
| git tag -a "$TAG" -m "Release $TAG" | |
| fi | |
| if ! git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then | |
| git push origin "$TAG" | |
| fi | |
| - name: Upload release notes | |
| if: steps.version.outputs.released == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-notes | |
| path: ${{ steps.version.outputs.notes_path }} | |
| build_zuey_binary: | |
| needs: beta_version | |
| if: needs.beta_version.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.beta_version.outputs.tag }} | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Build web UI | |
| run: | | |
| corepack enable && corepack prepare pnpm@10.28.2 --activate | |
| cd ui/web && pnpm install --frozen-lockfile && pnpm build && cd ../.. | |
| mkdir -p internal/webui/dist | |
| cp -r ui/web/dist/* internal/webui/dist/ | |
| - name: Build binary | |
| env: | |
| GOOS: linux | |
| GOARCH: amd64 | |
| VERSION: ${{ needs.beta_version.outputs.tag }} | |
| run: | | |
| CGO_ENABLED=0 go build -tags embedui \ | |
| -ldflags="-s -w -X github.com/nextlevelbuilder/goclaw/cmd.Version=${VERSION}" \ | |
| -o goclaw . | |
| tar -czf "goclaw-${VERSION}-linux-amd64.tar.gz" goclaw migrations/ skills/ | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: binary-linux-amd64 | |
| path: goclaw-*.tar.gz | |
| build_remaining_binaries: | |
| needs: beta_version | |
| if: needs.beta_version.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - goos: linux | |
| goarch: arm64 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.beta_version.outputs.tag }} | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Build web UI | |
| run: | | |
| corepack enable && corepack prepare pnpm@10.28.2 --activate | |
| cd ui/web && pnpm install --frozen-lockfile && pnpm build && cd ../.. | |
| mkdir -p internal/webui/dist | |
| cp -r ui/web/dist/* internal/webui/dist/ | |
| - name: Build binary | |
| env: | |
| GOOS: ${{ matrix.goos }} | |
| GOARCH: ${{ matrix.goarch }} | |
| VERSION: ${{ needs.beta_version.outputs.tag }} | |
| run: | | |
| CGO_ENABLED=0 go build -tags embedui \ | |
| -ldflags="-s -w -X github.com/nextlevelbuilder/goclaw/cmd.Version=${VERSION}" \ | |
| -o goclaw . | |
| tar -czf "goclaw-${VERSION}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz" goclaw migrations/ skills/ | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: binary-${{ matrix.goos }}-${{ matrix.goarch }} | |
| path: goclaw-*.tar.gz | |
| publish_release: | |
| needs: [beta_version, build_zuey_binary] | |
| if: needs.beta_version.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download zuey release artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: binary-linux-amd64 | |
| path: artifacts | |
| - name: Download release notes | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-notes | |
| path: release-notes | |
| - name: Publish prerelease | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ needs.beta_version.outputs.tag }} | |
| run: | | |
| (cd artifacts && sha256sum goclaw-*.tar.gz > CHECKSUMS.sha256) | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| gh release edit "$TAG" \ | |
| --title "GoClaw $TAG" \ | |
| --notes-file release-notes/release-notes.md \ | |
| --prerelease | |
| else | |
| gh release create "$TAG" \ | |
| --title "GoClaw $TAG" \ | |
| --notes-file release-notes/release-notes.md \ | |
| --prerelease | |
| fi | |
| gh release upload "$TAG" artifacts/* --clobber | |
| complete_release_artifacts: | |
| needs: [beta_version, publish_release, build_zuey_binary, build_remaining_binaries, deploy_zuey_beta] | |
| if: needs.beta_version.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download all binary artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: binary-* | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Refresh prerelease assets | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ needs.beta_version.outputs.tag }} | |
| run: | | |
| (cd artifacts && sha256sum goclaw-*.tar.gz > CHECKSUMS.sha256) | |
| gh release upload "$TAG" artifacts/* --clobber | |
| docker_images: | |
| needs: beta_version | |
| if: needs.beta_version.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - variant: latest | |
| suffix: "" | |
| enable_otel: "false" | |
| enable_embedui: "true" | |
| enable_python: "true" | |
| enable_full_skills: "false" | |
| - variant: full | |
| suffix: "-full" | |
| enable_otel: "false" | |
| enable_embedui: "true" | |
| enable_python: "true" | |
| enable_full_skills: "true" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.beta_version.outputs.tag }} | |
| - uses: docker/setup-qemu-action@v3 | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| if: env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ env.DOCKERHUB_USERNAME }} | |
| password: ${{ env.DOCKERHUB_TOKEN }} | |
| - name: Resolve Docker tags | |
| id: docker_tags | |
| env: | |
| TAG: ${{ needs.beta_version.outputs.tag }} | |
| SUFFIX: ${{ matrix.suffix }} | |
| run: | | |
| { | |
| echo "tags<<EOF" | |
| echo "${GHCR_IMAGE}:${TAG}${SUFFIX}" | |
| if [[ -n "$DOCKERHUB_USERNAME" && -n "$DOCKERHUB_TOKEN" ]]; then | |
| echo "${DOCKERHUB_IMAGE}:${TAG}${SUFFIX}" | |
| fi | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| if [[ -z "$DOCKERHUB_USERNAME" || -z "$DOCKERHUB_TOKEN" ]]; then | |
| echo "::notice::Docker Hub secrets not configured; publishing GHCR only." | |
| fi | |
| - name: Build and push | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.docker_tags.outputs.tags }} | |
| build-args: | | |
| ENABLE_OTEL=${{ matrix.enable_otel }} | |
| ENABLE_EMBEDUI=${{ matrix.enable_embedui }} | |
| ENABLE_PYTHON=${{ matrix.enable_python }} | |
| ENABLE_FULL_SKILLS=${{ matrix.enable_full_skills }} | |
| VERSION=${{ needs.beta_version.outputs.tag }} | |
| cache-from: type=gha,scope=dev-beta-${{ matrix.variant }} | |
| cache-to: type=gha,mode=max,scope=dev-beta-${{ matrix.variant }} | |
| promote_beta_aliases: | |
| needs: [beta_version, docker_images] | |
| if: needs.beta_version.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: dev-beta-promote-aliases | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| steps: | |
| - name: Check latest beta tag | |
| id: beta_freshness | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.beta_version.outputs.tag }} | |
| run: | | |
| latest_beta="$( | |
| gh api "repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/v" --paginate \ | |
| --jq '.[].ref | select(test("^refs/tags/v[0-9]+\\.[0-9]+\\.[0-9]+-beta\\.[0-9]+$")) | sub("^refs/tags/"; "")' \ | |
| | sort -V \ | |
| | tail -n 1 | |
| )" | |
| if [[ -z "$latest_beta" ]]; then | |
| echo "::error::No beta tags found; refusing to promote aliases" | |
| exit 1 | |
| fi | |
| if [[ "$latest_beta" != "$TAG" ]]; then | |
| echo "::notice::Skipping stale beta alias promotion for ${TAG}; latest beta is ${latest_beta}" | |
| echo "current=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "current=true" >> "$GITHUB_OUTPUT" | |
| - uses: docker/setup-buildx-action@v3 | |
| if: steps.beta_freshness.outputs.current == 'true' | |
| - name: Log in to GHCR | |
| if: steps.beta_freshness.outputs.current == 'true' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| if: steps.beta_freshness.outputs.current == 'true' && env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ env.DOCKERHUB_USERNAME }} | |
| password: ${{ env.DOCKERHUB_TOKEN }} | |
| - name: Promote beta aliases | |
| if: steps.beta_freshness.outputs.current == 'true' | |
| env: | |
| TAG: ${{ needs.beta_version.outputs.tag }} | |
| run: | | |
| docker buildx imagetools create -t "${GHCR_IMAGE}:beta" "${GHCR_IMAGE}:${TAG}" | |
| docker buildx imagetools create -t "${GHCR_IMAGE}:beta-full" "${GHCR_IMAGE}:${TAG}-full" | |
| if [[ -n "$DOCKERHUB_USERNAME" && -n "$DOCKERHUB_TOKEN" ]]; then | |
| docker buildx imagetools create -t "${DOCKERHUB_IMAGE}:beta" "${DOCKERHUB_IMAGE}:${TAG}" | |
| docker buildx imagetools create -t "${DOCKERHUB_IMAGE}:beta-full" "${DOCKERHUB_IMAGE}:${TAG}-full" | |
| else | |
| echo "::notice::Docker Hub secrets not configured; promoted GHCR beta aliases only." | |
| fi | |
| deploy_zuey_beta: | |
| needs: [beta_version, publish_release] | |
| if: needs.beta_version.outputs.released == 'true' && github.repository == 'digitopvn/goclaw' && github.repository != 'nextlevelbuilder/goclaw' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| concurrency: | |
| group: dev-beta-zuey-deploy | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| env: | |
| GOCLAW_DEPLOY_URL: ${{ secrets.ZUEY_GOCLAW_URL }} | |
| GOCLAW_GATEWAY_TOKEN: ${{ secrets.ZUEY_GOCLAW_GATEWAY_TOKEN }} | |
| GOCLAW_UPGRADE_TOKEN: ${{ secrets.ZUEY_GOCLAW_UPGRADE_TOKEN }} | |
| GOCLAW_DEPLOY_USER_ID: ${{ vars.ZUEY_GOCLAW_USER_ID || 'system' }} | |
| TAG: ${{ needs.beta_version.outputs.tag }} | |
| ZUEY_SSH_HOST: ${{ vars.ZUEY_SSH_HOST || '82.197.71.246' }} | |
| ZUEY_SSH_PORT: ${{ vars.ZUEY_SSH_PORT || '2233' }} | |
| ZUEY_SSH_USER: ${{ vars.ZUEY_SSH_USER || 'zuey' }} | |
| steps: | |
| - name: Check latest beta tag | |
| id: beta_freshness | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| latest_beta="$( | |
| gh api "repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/v" --paginate \ | |
| --jq '.[].ref | select(test("^refs/tags/v[0-9]+\\.[0-9]+\\.[0-9]+-beta\\.[0-9]+$")) | sub("^refs/tags/"; "")' \ | |
| | sort -V \ | |
| | tail -n 1 | |
| )" | |
| if [[ -z "$latest_beta" ]]; then | |
| echo "::error::No beta tags found; refusing to deploy" | |
| exit 1 | |
| fi | |
| if [[ "$latest_beta" != "$TAG" ]]; then | |
| echo "::notice::Skipping stale zuey deploy for ${TAG}; latest beta is ${latest_beta}" | |
| echo "current=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "current=true" >> "$GITHUB_OUTPUT" | |
| - name: Validate deploy configuration | |
| if: steps.beta_freshness.outputs.current == 'true' | |
| run: | | |
| missing=0 | |
| for name in GOCLAW_DEPLOY_URL GOCLAW_GATEWAY_TOKEN GOCLAW_UPGRADE_TOKEN TAG; do | |
| if [[ -z "${!name}" ]]; then | |
| echo "::error::${name} is not configured" | |
| missing=1 | |
| fi | |
| done | |
| exit "$missing" | |
| - name: Checkout repository (for VPS script sync) | |
| if: steps.beta_freshness.outputs.current == 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.beta_version.outputs.tag }} | |
| - name: Sync zuey ops scripts to VPS | |
| if: steps.beta_freshness.outputs.current == 'true' | |
| env: | |
| ZUEY_SSH_PRIVATE_KEY_B64: ${{ secrets.ZUEY_SSH_PRIVATE_KEY_B64 }} | |
| ZUEY_SUDO_PASS: ${{ secrets.ZUEY_SUDO_PASS }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${ZUEY_SSH_PRIVATE_KEY_B64:-}" || -z "${ZUEY_SUDO_PASS:-}" ]]; then | |
| echo "::warning::ZUEY_SSH_PRIVATE_KEY_B64 or ZUEY_SUDO_PASS not configured; skipping VPS script sync. Store the private key as base64 (\`base64 -w0 < /path/to/key\`) in the ZUEY_SSH_PRIVATE_KEY_B64 secret to keep /usr/local/bin/goclaw-deploy and /usr/local/bin/goclaw-upgrade-release in sync with the repo on every beta deploy." | |
| exit 0 | |
| fi | |
| # Verify the two scripts exist in the checked-out tag | |
| for f in scripts/zuey/goclaw-deploy.sh scripts/zuey/goclaw-upgrade-release.sh; do | |
| test -f "$f" || { echo "::error::$f missing in repo"; exit 1; } | |
| bash -n "$f" || { echo "::error::$f has syntax error"; exit 1; } | |
| done | |
| # Stage SSH key: base64-decode, write with 0600, validate format. | |
| # Why base64: GitHub Secrets storage round-trips can normalize/strip | |
| # newlines in PEM blocks, producing `error in libcrypto` on load. | |
| # Base64 is a single line, immune to whitespace mangling. | |
| install -m 700 -d ~/.ssh | |
| # Create the key file with a tight umask so it is born at 0600 — | |
| # avoids the brief 0644 window between `>` redirect and `chmod`. | |
| if ! ( umask 077 && printf %s "$ZUEY_SSH_PRIVATE_KEY_B64" | base64 -d > ~/.ssh/id_zuey ) 2>/dev/null; then | |
| echo "::error::ZUEY_SSH_PRIVATE_KEY_B64 is not valid base64. Regenerate with: base64 -w0 < /path/to/private_key" | |
| exit 1 | |
| fi | |
| if ! ssh-keygen -y -f ~/.ssh/id_zuey >/dev/null 2>&1; then | |
| echo "::error::Decoded SSH key is not a valid OpenSSH/PEM private key. Verify the source file is a real private key and re-encode." | |
| rm -f ~/.ssh/id_zuey | |
| exit 1 | |
| fi | |
| ssh-keyscan -p "$ZUEY_SSH_PORT" -H "$ZUEY_SSH_HOST" >> ~/.ssh/known_hosts 2>/dev/null | |
| SSH_BASE=(-i ~/.ssh/id_zuey -o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=15) | |
| # Upload (-O selects legacy scp protocol; OpenSSH 9.x defaults to sftp | |
| # which is fine here, but -O is portable across runner image versions) | |
| scp -O -P "$ZUEY_SSH_PORT" "${SSH_BASE[@]}" \ | |
| scripts/zuey/goclaw-deploy.sh \ | |
| scripts/zuey/goclaw-upgrade-release.sh \ | |
| "${ZUEY_SSH_USER}@${ZUEY_SSH_HOST}:/tmp/" | |
| # Install on host: backup-if-changed → install (root:root 0755) → | |
| # syntax check. The sudo password is shell-quoted via printf %q and | |
| # interpolated into the remote command line; the SSH channel is | |
| # encrypted and GitHub Actions auto-masks the secret in logs. | |
| # sudo -S reads from stdin and does not echo the password. | |
| quoted_pass=$(printf %q "$ZUEY_SUDO_PASS") | |
| ssh -p "$ZUEY_SSH_PORT" "${SSH_BASE[@]}" "${ZUEY_SSH_USER}@${ZUEY_SSH_HOST}" \ | |
| "SUDOPASS=$quoted_pass bash -s" <<'REMOTE' | |
| set -euo pipefail | |
| ts=$(date +%Y%m%d-%H%M%S) | |
| for name in goclaw-deploy goclaw-upgrade-release; do | |
| src="/tmp/${name}.sh" | |
| dst="/usr/local/bin/${name}" | |
| if [ ! -f "$src" ]; then echo "::error::missing $src"; exit 1; fi | |
| if [ -f "$dst" ] && cmp -s "$src" "$dst"; then | |
| echo "no change: $name" | |
| rm -f "$src" | |
| continue | |
| fi | |
| if [ -f "$dst" ]; then | |
| echo "$SUDOPASS" | sudo -S cp -p "$dst" "${dst}.bak-${ts}" | |
| fi | |
| echo "$SUDOPASS" | sudo -S install -o root -g root -m 0755 "$src" "$dst" | |
| echo "$SUDOPASS" | sudo -S bash -n "$dst" | |
| rm -f "$src" | |
| echo "installed: $name" | |
| done | |
| REMOTE | |
| # Clean up the private key from the runner FS | |
| shred -u ~/.ssh/id_zuey 2>/dev/null || rm -f ~/.ssh/id_zuey | |
| - name: Trigger zuey gateway upgrade | |
| if: steps.beta_freshness.outputs.current == 'true' | |
| run: | | |
| base_url="${GOCLAW_DEPLOY_URL%/}" | |
| body="$(mktemp)" | |
| payload="$(printf '{"tag":"%s"}' "$TAG")" | |
| status_code="$(curl -sS --retry 3 --retry-delay 2 \ | |
| -o "$body" \ | |
| -w "%{http_code}" \ | |
| -X POST "${base_url}/v1/system/gateway/upgrade" \ | |
| -H "Authorization: Bearer ${GOCLAW_GATEWAY_TOKEN}" \ | |
| -H "X-GoClaw-Upgrade-Token: ${GOCLAW_UPGRADE_TOKEN}" \ | |
| -H "X-GoClaw-User-Id: ${GOCLAW_DEPLOY_USER_ID}" \ | |
| -H "Content-Type: application/json" \ | |
| --data "$payload")" | |
| if [[ "$status_code" != "202" ]]; then | |
| echo "::error::gateway upgrade trigger failed with HTTP ${status_code}" | |
| cat "$body" | |
| exit 1 | |
| fi | |
| cat "$body" | |
| - name: Wait for zuey gateway upgrade | |
| if: steps.beta_freshness.outputs.current == 'true' | |
| run: | | |
| base_url="${GOCLAW_DEPLOY_URL%/}" | |
| for attempt in {1..90}; do | |
| status_err="$(mktemp)" | |
| status_json="$(curl -fsS --retry 3 --retry-delay 2 \ | |
| -H "Authorization: Bearer ${GOCLAW_GATEWAY_TOKEN}" \ | |
| -H "X-GoClaw-Upgrade-Token: ${GOCLAW_UPGRADE_TOKEN}" \ | |
| -H "X-GoClaw-User-Id: ${GOCLAW_DEPLOY_USER_ID}" \ | |
| "${base_url}/v1/system/gateway/upgrade/status" 2>"$status_err" || true)" | |
| if [[ -z "$status_json" ]]; then | |
| echo "upgrade status unavailable; attempt ${attempt}/90" | |
| cat "$status_err" | |
| rm -f "$status_err" | |
| sleep 10 | |
| continue | |
| fi | |
| rm -f "$status_err" | |
| state="$(python3 -c 'import json,sys; print(json.load(sys.stdin).get("state", ""))' <<< "$status_json" 2>/dev/null || true)" | |
| if [[ "$state" == "succeeded" ]]; then | |
| echo "$status_json" | |
| exit 0 | |
| fi | |
| if [[ "$state" == "failed" ]]; then | |
| echo "::error::gateway upgrade failed" | |
| echo "$status_json" | |
| exit 1 | |
| fi | |
| echo "upgrade state=${state:-unknown}; attempt ${attempt}/90" | |
| sleep 10 | |
| done | |
| echo "::error::gateway upgrade timed out" | |
| exit 1 | |
| - name: Verify public health | |
| if: steps.beta_freshness.outputs.current == 'true' | |
| run: | | |
| base_url="${GOCLAW_DEPLOY_URL%/}" | |
| health_json="$(curl -fsS --retry 5 --retry-delay 3 "${base_url}/health")" | |
| status="$(python3 -c 'import json,sys; print(json.load(sys.stdin).get("status", ""))' <<< "$health_json")" | |
| if [[ "$status" != "ok" ]]; then | |
| echo "::error::unexpected health response" | |
| echo "$health_json" | |
| exit 1 | |
| fi | |
| echo "$health_json" |