ci: speed up zuey beta deploy (#127)

* ci: speed up zuey beta deploy

* ci: mark issue 88 plan complete
This commit is contained in:
Duy /zuey/ authored and GitHub committed 2026-05-31 21:47:28 +07:00
1 parent 94cbaa9f0f
commit 64c02ea5a6
9 files changed
+460 -15

No files matched your search

+135 -14
View File
@@ -8,10 +8,6 @@ on:
permissions:
contents: read
concurrency:
group: dev-beta-release-${{ github.ref }}
cancel-in-progress: false
env:
GHCR_IMAGE: ghcr.io/${{ github.repository }}
DOCKERHUB_IMAGE: digitop/goclaw
@@ -80,6 +76,9 @@ jobs:
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:
@@ -91,8 +90,8 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Release versioning tests
run: node --test scripts/ci/semantic-beta-version.test.mjs
- 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
@@ -118,7 +117,49 @@ jobs:
name: release-notes
path: ${{ steps.version.outputs.notes_path }}
build_binaries:
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
@@ -126,8 +167,6 @@ jobs:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
steps:
@@ -169,18 +208,17 @@ jobs:
path: goclaw-*.tar.gz
publish_release:
needs: [beta_version, build_binaries, promote_beta_aliases]
needs: [beta_version, build_zuey_binary]
if: needs.beta_version.outputs.released == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all artifacts
- name: Download zuey release artifact
uses: actions/download-artifact@v4
with:
pattern: binary-*
name: binary-linux-amd64
path: artifacts
merge-multiple: true
- name: Download release notes
uses: actions/download-artifact@v4
@@ -208,6 +246,29 @@ jobs:
fi
gh release upload "$TAG" artifacts/* --clobber
complete_release_artifacts:
needs: [beta_version, publish_release, build_zuey_binary, build_remaining_binaries]
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'
@@ -295,6 +356,9 @@ jobs:
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
@@ -302,9 +366,34 @@ jobs:
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
@@ -312,13 +401,14 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
if: env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != ''
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: |
@@ -336,6 +426,9 @@ jobs:
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:
@@ -348,7 +441,30 @@ jobs:
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
@@ -360,11 +476,13 @@ jobs:
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 }}
@@ -441,6 +559,7 @@ jobs:
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)"
@@ -462,6 +581,7 @@ jobs:
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
@@ -496,6 +616,7 @@ jobs:
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")"
+3 -1
View File
@@ -190,7 +190,9 @@ The remote trigger endpoint fails closed unless `GOCLAW_UPGRADE_TRIGGER_TOKEN` i
### Automatic Beta Deploy From `dev`
Pushing or merging into `dev` runs `.github/workflows/dev-beta-release.yaml`. After Go/Web checks pass, the workflow creates the next semantic beta tag, publishes the prerelease assets, promotes beta Docker aliases, then deploys that exact beta tag to the zuey VPS through the gateway upgrade endpoint.
Pushing or merging into `dev` runs `.github/workflows/dev-beta-release.yaml`. After Go/Web checks pass, the workflow creates the next semantic beta tag, publishes the linux amd64 prerelease asset and checksum, then deploys that exact beta tag to the zuey VPS through the gateway upgrade endpoint.
Linux arm64 release assets, full checksums, multi-arch Docker images, and beta Docker aliases continue after the fast zuey deploy path. Those completion jobs are still required to pass; the workflow remains failed if later artifact completion breaks. Zuey deploy and Docker beta alias promotion both skip stale beta tags so older runs cannot roll back a newer beta.
Required GitHub Actions configuration:
+14
View File
@@ -6,6 +6,20 @@ Significant changes, features, and fixes in reverse chronological order.
## 2026-05-31
### CI/CD: fast zuey beta deploy (issue #88)
**Changes**
- Split the dev beta release workflow so zuey deploy starts after the linux amd64 prerelease asset is published, instead of waiting for Docker multi-arch builds and beta alias promotion.
- Added stale beta tag guards for zuey deploy and Docker beta alias promotion so older runs cannot roll back a newer beta.
- Kept linux arm64 binaries, refreshed checksums, multi-arch Docker images, and beta aliases as required post-deploy completion jobs.
**Tests**
- Added a workflow-structure test covering the fast deploy graph, artifact completion graph, and stale-tag guards.
---
### Provider fallback content-policy recovery
**Fixes**
@@ -0,0 +1,42 @@
---
phase: 1
title: Scout and design
status: completed
priority: P1
effort: 30m
dependencies: []
---
# Phase 1: Scout and design
## Overview
Verify current workflow graph, zuey asset contract, and overlapping plans before editing CI.
## Requirements
- Functional: document the exact current blockers for zuey deploy.
- Non-functional: plan must be grounded in live workflow code and recent run timing.
## Architecture
Current path: `go/web -> beta_version -> build_binaries + docker_images -> promote_beta_aliases -> publish_release -> deploy_zuey_beta`.
Fast path target: `go/web -> beta_version -> build_zuey_binary -> publish_release -> deploy_zuey_beta`.
## Implementation Steps
1. Read `README.md`, `CLAUDE.md`, `.github/workflows/dev-beta-release.yaml`, and `scripts/zuey/goclaw-upgrade-release.sh`.
2. Check issue #88 and recent workflow job timings with `gh`.
3. Check unfinished plans for overlapping CI release work.
4. Verify zuey only requires linux amd64 release tarball and checksum/digest.
## Success Criteria
- [ ] Workflow dependency bottleneck identified with file/line refs.
- [ ] Zuey asset requirement identified with file/line refs.
- [ ] No overlapping active plan blocks this work.
## Risk Assessment
Risk: publishing partial prerelease could confuse consumers. Mitigation: follow-up job refreshes release assets/checksums and workflow stays failed if completion breaks.
@@ -0,0 +1,61 @@
---
phase: 2
title: Refactor dev beta workflow
status: completed
priority: P1
effort: 1h
dependencies:
- 1
---
# Phase 2: Refactor dev beta workflow
## Overview
Refactor `.github/workflows/dev-beta-release.yaml` so the release needed by zuey is published before slow Docker multi-arch jobs finish.
## Requirements
- Functional: split amd64 release publishing from full artifact completion.
- Non-functional: keep all current artifact families and registry publishing.
## Architecture
Use two binary jobs:
- `build_zuey_binary`: linux amd64 only, feeds the first prerelease publish.
- `build_remaining_binaries`: linux arm64, feeds final release artifact completion.
Keep Docker jobs parallel after `beta_version`. Keep `promote_beta_aliases` after Docker. Add a release completion job that re-uploads all binary assets and full `CHECKSUMS.sha256`.
Move from workflow-level concurrency to job-level concurrency only where mutation ordering matters. Add stale beta tag guards before zuey deploy and beta alias promotion.
## Related Code Files
- Modify: `.github/workflows/dev-beta-release.yaml`
- Modify: `docs/deployment-guide.md`
- Modify: `docs/project-changelog.md`
- Create: `scripts/ci/dev-beta-release-workflow.test.mjs`
- Delete: none
## Implementation Steps
1. Rename/split existing `build_binaries` into amd64 fast job and remaining arch job.
2. Change `publish_release.needs` to `[beta_version, build_zuey_binary]`.
3. Download only `binary-linux-amd64` in `publish_release`.
4. Add `complete_release_artifacts` needing `[beta_version, publish_release, build_zuey_binary, build_remaining_binaries]`.
5. Keep `docker_images` and `promote_beta_aliases` unchanged except not gating deploy.
6. Keep `deploy_zuey_beta.needs` as `[beta_version, publish_release]`.
7. Remove top-level workflow concurrency; add job-level concurrency and stale-tag guard for deploy/alias mutation jobs.
8. Update deployment docs and changelog for the new order.
## Success Criteria
- [ ] `deploy_zuey_beta` critical path excludes Docker jobs.
- [ ] Full release upload still includes amd64 + arm64 tarballs and refreshed checksum.
- [ ] Docker beta aliases still promote after multi-arch images.
- [ ] Stale beta deploy/alias promotion is skipped.
- [ ] No secret, branch, or repository guard is weakened.
## Risk Assessment
Risk: two jobs uploading to same release can race. Mitigation: `complete_release_artifacts` depends on `publish_release` and uses `gh release upload --clobber` after the fast publish.
@@ -0,0 +1,48 @@
---
phase: 3
title: Validate workflow
status: completed
priority: P1
effort: 30m
dependencies:
- 2
---
# Phase 3: Validate workflow
## Overview
Validate YAML syntax, dependency graph intent, and changed workflow blast radius.
## Requirements
- Functional: prove workflow file is syntactically valid and dependency graph matches acceptance.
- Non-functional: avoid running heavy Docker builds locally.
## Architecture
Local validation is static because GitHub-hosted runner context/secrets are required for full execution.
## Related Code Files
- Read: `.github/workflows/dev-beta-release.yaml`
- Read: `docs/deployment-guide.md`
## Implementation Steps
1. Parse workflow YAML with Ruby `YAML.safe_load` or available equivalent.
2. Inspect changed job `needs` and artifact names.
3. Add and run workflow-structure test for fast path, completion path, and stale guard.
4. Run lightweight repo checks that do not require services when feasible.
5. Ask tester/reviewer agents to validate regression risks.
## Success Criteria
- [ ] YAML parser accepts workflow.
- [ ] Static grep confirms deploy depends only on fast release publish.
- [ ] Node workflow-structure test passes.
- [ ] Review finds no blocker for release/deploy contract.
## Risk Assessment
Risk: static validation misses GitHub expression mistakes. Mitigation: keep expressions simple and reuse existing artifact/action patterns.
@@ -0,0 +1,47 @@
---
phase: 4
title: Ship beta PR
status: completed
priority: P2
effort: 30m
dependencies:
- 3
---
# Phase 4: Ship beta PR
## Overview
Ship the workflow refactor to `dev` via PR and preserve beta release observability.
## Requirements
- Functional: commit, push branch, create PR into `dev`, link issue #88.
- Non-functional: do not merge automatically; beta workflow runs after PR merge to `dev`.
## Architecture
Use normal PR shipping. After merge, the first `dev` push is the real validation for zuey time-to-deploy and full artifact completion.
## Related Code Files
- Commit: `.github/workflows/dev-beta-release.yaml`
- Commit: `plans/260531-2101-issue-88-fast-zuey-beta-deploy-ci/*`
## Implementation Steps
1. Run final `git diff --check` and workflow validation.
2. Commit with conventional message.
3. Push `codex/issue-88-fast-zuey-beta-deploy-ci`.
4. Create PR to `dev`, mention `Fixes #88`.
5. Report post-merge watch command for first real beta run.
## Success Criteria
- [ ] Branch pushed to origin.
- [ ] PR opened against `dev`.
- [ ] PR body explains fast path and artifact completion path.
## Risk Assessment
Risk: `dev` workflow behavior cannot be fully proven before merge. Mitigation: PR includes expected job-order evidence and first-run watch steps.
@@ -0,0 +1,49 @@
---
title: Issue 88 fast zuey beta deploy CI
description: >-
Refactor dev beta release workflow so zuey deploy starts after the linux amd64
release asset is published, while arm64 and Docker artifacts still complete
afterward.
status: completed
priority: P2
branch: codex/issue-88-fast-zuey-beta-deploy-ci
tags: []
blockedBy: []
blocks: []
created: '2026-05-31T14:01:31.321Z'
createdBy: 'ck:plan'
source: skill
---
# Issue 88 fast zuey beta deploy CI
## Overview
Issue #88 reports 30-40m dev beta CI. Recent run `26713221199` spent ~24m in Docker multi-arch while zuey deploy waited behind Docker promotion. Zuey deploy only needs the GitHub Release linux amd64 tarball, verified by `scripts/zuey/goclaw-upgrade-release.sh`.
Goal: optimize time-to-zuey deployed. Do not weaken Go/Web gates. Do not drop arm64, Docker `latest/full`, beta aliases, or checksums; only move them off the zuey critical path.
## Phases
| Phase | Name | Status |
|-------|------|--------|
| 1 | [Scout and design](./phase-01-scout-and-design.md) | Completed |
| 2 | [Refactor dev beta workflow](./phase-02-refactor-dev-beta-workflow.md) | Completed |
| 3 | [Validate workflow](./phase-03-validate-workflow.md) | Completed |
| 4 | [Ship beta PR](./phase-04-ship-beta-pr.md) | Completed |
## Dependencies
- Base branch: `origin/dev`
- Primary files: `.github/workflows/dev-beta-release.yaml`, this plan
- GitHub issue: `digitopvn/goclaw#88`
## Acceptance Criteria
- `deploy_zuey_beta` no longer waits for `docker_images` or `promote_beta_aliases`.
- Workflow-level concurrency no longer serializes a new dev push behind slow artifact completion from the previous push.
- Zuey deploy and Docker beta alias promotion skip stale beta tags instead of rolling back to an older tag.
- The prerelease is published with `goclaw-${TAG}-linux-amd64.tar.gz` and `CHECKSUMS.sha256` before deploy.
- Linux arm64 binary, multi-arch Docker `latest/full`, and beta aliases still run after `beta_version` and keep workflow failure visible if broken.
- Workflow YAML parses and preserves repo guard `github.repository == 'digitopvn/goclaw'`.
- PR targets `dev` and links issue #88.
@@ -0,0 +1,61 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, "../..");
const workflow = readFileSync(join(repoRoot, ".github/workflows/dev-beta-release.yaml"), "utf8");
function jobBlock(name) {
const lines = workflow.split("\n");
const start = lines.findIndex((line) => line === ` ${name}:`);
assert.notEqual(start, -1, `missing job ${name}`);
let end = lines.length;
for (let index = start + 1; index < lines.length; index += 1) {
if (/^ [A-Za-z0-9_]+:$/.test(lines[index])) {
end = index;
break;
}
}
return lines.slice(start + 1, end).join("\n");
}
test("dev beta workflow does not serialize fast deploy behind full artifact completion", () => {
assert.doesNotMatch(workflow, /^concurrency:\n/m);
const publishRelease = jobBlock("publish_release");
assert.match(publishRelease, /needs: \[beta_version, build_zuey_binary\]/);
assert.doesNotMatch(publishRelease, /promote_beta_aliases|docker_images|build_remaining_binaries/);
const deployZuey = jobBlock("deploy_zuey_beta");
assert.match(deployZuey, /needs: \[beta_version, publish_release\]/);
assert.doesNotMatch(deployZuey, /promote_beta_aliases|docker_images|complete_release_artifacts/);
});
test("dev beta workflow publishes zuey amd64 asset before completing all release assets", () => {
const buildZuey = jobBlock("build_zuey_binary");
assert.match(buildZuey, /GOARCH: amd64/);
assert.match(buildZuey, /name: binary-linux-amd64/);
const remainingBinaries = jobBlock("build_remaining_binaries");
assert.match(remainingBinaries, /goarch: arm64/);
assert.match(remainingBinaries, /name: binary-\$\{\{ matrix\.goos \}\}-\$\{\{ matrix\.goarch \}\}/);
const completion = jobBlock("complete_release_artifacts");
assert.match(completion, /needs: \[beta_version, publish_release, build_zuey_binary, build_remaining_binaries\]/);
assert.match(completion, /pattern: binary-\*/);
assert.match(completion, /sha256sum goclaw-\*\.tar\.gz > CHECKSUMS\.sha256/);
});
test("dev beta workflow skips stale deploy and alias mutations", () => {
for (const name of ["deploy_zuey_beta", "promote_beta_aliases"]) {
const block = jobBlock(name);
assert.match(block, /id: beta_freshness/);
assert.match(block, /latest_beta/);
assert.match(block, /Skipping stale/);
assert.match(block, /steps\.beta_freshness\.outputs\.current == 'true'/);
}
});