xworkspace-console/.github/workflows/offline-package-xworkspace-console-runtime.yaml
Haitao Pan fd1fb5710c ci(console-runtime): publish moving latest-runtime release
The ai-workspace role's final-deployment step downloads the console runtime
from a stable latest-runtime release (matching the bridge/qmd/litellm
convention). Have the publish job refresh a moving `latest-runtime` release
alongside the immutable `runtime-<sha>` one, carrying the same cross-compiled
assets (darwin-arm64, linux-amd64, linux-arm64) + SHA256SUMS, so consumers get
a predictable URL:
  releases/download/latest-runtime/xworkspace-console-runtime-<os>-<arch>.tar.gz

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:05:20 +08:00

478 lines
18 KiB
YAML

name: Build XWorkspace Console Runtime & Offline Package
on:
push:
branches: [main]
paths:
- api/**
- dashboard/**
- scripts/**
- .github/workflows/offline-package-xworkspace-console-runtime.yaml
workflow_dispatch:
inputs:
tag:
description: "Offline release tag. Leave empty to use offline-ai-workspace-<run_number>"
required: false
type: string
playbooks_ref:
description: "ai-workspace-infra/playbooks git ref"
required: false
default: "main"
type: string
console_ref:
description: "ai-workspace-lab/xworkspace-console git ref"
required: false
default: "main"
type: string
core_skills_ref:
description: "ai-workspace-lab/xworkspace-core-skills git ref"
required: false
default: "main"
type: string
bridge_runtime_release_tag:
description: "Bridge runtime release tag, or latest-runtime"
required: false
default: "latest-runtime"
type: string
qmd_runtime_release_tag:
description: "QMD runtime release tag, or latest-runtime"
required: false
default: "latest-runtime"
type: string
litellm_runtime_release_tag:
description: "LiteLLM runtime release tag, or latest-runtime"
required: false
default: "latest-runtime"
type: string
permissions:
contents: write
actions: write
concurrency:
group: xworkspace-console-runtime-and-offline-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
name: Build console runtime (universal dist + cross-compiled API)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version-file: api/go.mod
cache: false
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Build universal dashboard (dist) once
run: |
set -euo pipefail
# The dashboard compiles to platform-independent static assets, so it
# is built a single time and reused by every target. node_modules is a
# build-only dependency and is intentionally excluded from the runtime
# (Node is provided by the playbook nodejs role on target).
( cd dashboard && npm ci --no-audit --no-fund && npm run build )
- name: Package per-platform runtimes
run: |
set -euo pipefail
mkdir -p dist/assets
# Supported runtime targets: macOS is arm64-only; Linux (Debian and
# Ubuntu share the same binary) is amd64 + arm64. The Go API is a pure
# cross-compile (CGO disabled); the dashboard dist is the universal
# bundle built above.
for target in darwin/arm64 linux/amd64 linux/arm64; do
os="${target%/*}"
arch="${target#*/}"
root="dist/runtime/${os}-${arch}/xworkspace-console"
mkdir -p "${root}/dashboard" "${root}/bin"
cp -a scripts "${root}/"
cp -a dashboard/dist "${root}/dashboard/"
(
cd api
CGO_ENABLED=0 GOOS="${os}" GOARCH="${arch}" \
go build -buildvcs=false -trimpath -o "../${root}/bin/xworkspace-api" .
)
cat > "${root}/manifest.json" <<JSON
{
"component": "xworkspace-console",
"commit": "${GITHUB_SHA}",
"os": "${os}",
"arch": "${arch}",
"apiBinary": "bin/xworkspace-api",
"dashboard": "dashboard"
}
JSON
tar -czf "dist/assets/xworkspace-console-runtime-${os}-${arch}.tar.gz" \
-C "dist/runtime/${os}-${arch}" xworkspace-console
done
( cd dist/assets && sha256sum -- *.tar.gz > SHA256SUMS )
- uses: actions/upload-artifact@v4
with:
name: xworkspace-console-runtime
path: |
dist/assets/*.tar.gz
dist/assets/SHA256SUMS
if-no-files-found: error
publish:
name: Publish runtime release
needs: build
runs-on: ubuntu-latest
outputs:
runtime_tag: ${{ steps.publish.outputs.tag }}
steps:
- uses: actions/download-artifact@v4
with:
name: xworkspace-console-runtime
path: dist
- name: Publish assets
id: publish
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="runtime-${GITHUB_SHA::12}"
if gh release view "${tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
gh release upload "${tag}" dist/*.tar.gz dist/SHA256SUMS \
--repo "${GITHUB_REPOSITORY}" --clobber
else
gh release create "${tag}" dist/*.tar.gz dist/SHA256SUMS \
--repo "${GITHUB_REPOSITORY}" \
--target "${GITHUB_SHA}" \
--title "XWorkspace Console runtime ${GITHUB_SHA::12}" \
--notes "Prebuilt console runtime (cross-compiled Go API + universal dashboard dist). Targets: darwin-arm64, linux-amd64, linux-arm64."
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
- name: Refresh moving latest-runtime release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# Stable download URL for consumers (matches the latest-runtime
# convention used by the bridge/qmd/litellm runtimes). The
# ai-workspace role's console deployment downloads from
# releases/download/latest-runtime/xworkspace-console-runtime-<os>-<arch>.tar.gz
tag="latest-runtime"
if gh release view "${tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
gh release upload "${tag}" dist/*.tar.gz dist/SHA256SUMS \
--repo "${GITHUB_REPOSITORY}" --clobber
else
gh release create "${tag}" dist/*.tar.gz dist/SHA256SUMS \
--repo "${GITHUB_REPOSITORY}" \
--target "${GITHUB_SHA}" \
--title "XWorkspace Console runtime (latest)" \
--notes "Moving alias to the newest console runtime build (cross-compiled Go API + universal dashboard dist). Targets: darwin-arm64, linux-amd64, linux-arm64."
fi
build-offline-package:
name: Build offline ${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }}
needs: publish
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- distro: debian
version: "13"
arch: amd64
- distro: debian
version: "13"
arch: arm64
- distro: debian
version: "12"
arch: amd64
- distro: debian
version: "12"
arch: arm64
- distro: debian
version: "11"
arch: amd64
- distro: debian
version: "11"
arch: arm64
- distro: ubuntu
version: "26.04"
arch: amd64
- distro: ubuntu
version: "26.04"
arch: arm64
- distro: ubuntu
version: "24.04"
arch: amd64
- distro: ubuntu
version: "24.04"
arch: arm64
- distro: ubuntu
version: "22.04"
arch: amd64
- distro: ubuntu
version: "22.04"
arch: arm64
steps:
- uses: actions/checkout@v4
- name: Enable QEMU for cross-arch package collection
uses: docker/setup-qemu-action@v3
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update -y
sudo apt-get install -y curl git jq python3-pip
- name: Build offline package
env:
DISTRO_ID: ${{ matrix.distro }}
DISTRO_VERSION: ${{ matrix.version }}
ARCH: ${{ matrix.arch }}
PLAYBOOKS_REF: ${{ github.event.inputs.playbooks_ref || 'main' }}
CONSOLE_REF: ${{ github.event_name == 'push' && github.sha || github.event.inputs.console_ref || 'main' }}
CORE_SKILLS_REF: ${{ github.event.inputs.core_skills_ref || 'main' }}
CONSOLE_RUNTIME_RELEASE_TAG: ${{ needs.publish.outputs.runtime_tag }}
BRIDGE_RUNTIME_RELEASE_TAG: ${{ github.event.inputs.bridge_runtime_release_tag || 'latest-runtime' }}
QMD_RUNTIME_RELEASE_TAG: ${{ github.event.inputs.qmd_runtime_release_tag || 'latest-runtime' }}
LITELLM_RUNTIME_RELEASE_TAG: ${{ github.event.inputs.litellm_runtime_release_tag || 'latest-runtime' }}
GH_TOKEN: ${{ github.token }}
PACKAGE_VERSION: ${{ github.run_number }}
run: |
set -euo pipefail
chmod +x scripts/create-ai-workspace-offline-package.sh
scripts/create-ai-workspace-offline-package.sh
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ai-workspace-all-in-one-offline-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }}
path: ai-workspace-all-in-one-offline-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }}.tar.gz
if-no-files-found: error
test-offline-package:
needs: build-offline-package
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- distro: debian
version: "13"
arch: amd64
- distro: ubuntu
version: "24.04"
arch: amd64
- distro: debian
version: "13"
arch: arm64
- distro: ubuntu
version: "24.04"
arch: arm64
steps:
- uses: actions/checkout@v4
- name: Verify online bootstrap hands off to offline installer
run: |
set -euo pipefail
fixture="${RUNNER_TEMP}/offline-handoff"
mkdir -p "${fixture}/scripts" "${fixture}/metadata"
cat > "${fixture}/metadata/target.env" <<EOF
DISTRO_ID=${{ matrix.distro }}
DISTRO_VERSION=${{ matrix.version }}
ARCH=${{ matrix.arch }}
EOF
cat > "${fixture}/scripts/ai-workspace-offline-install.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
test "${AI_WORKSPACE_OFFLINE_ACTIVE:-}" = "true"
printf 'offline-handoff-ok\n'
EOF
chmod +x "${fixture}/scripts/ai-workspace-offline-install.sh"
output="$(
AI_WORKSPACE_OFFLINE_MODE=force \
AI_WORKSPACE_OFFLINE_PACKAGE="${fixture}" \
AI_WORKSPACE_OFFLINE_DISTRO_ID=${{ matrix.distro }} \
AI_WORKSPACE_OFFLINE_DISTRO_VERSION=${{ matrix.version }} \
AI_WORKSPACE_OFFLINE_ARCH=${{ matrix.arch }} \
bash scripts/setup-ai-workspace-all-in-one.sh
)"
grep -q 'offline-handoff-ok' <<<"${output}"
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: ai-workspace-all-in-one-offline-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }}
path: offline-test
- name: Verify offline package contents
run: |
set -euo pipefail
cd offline-test
tar -tzf ai-workspace-all-in-one-offline-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }}.tar.gz > contents.txt
grep -q 'scripts/ai-workspace-offline-install.sh' contents.txt
grep -q 'metadata/manifest.json' contents.txt
grep -q 'metadata/target.env' contents.txt
grep -q 'repos/playbooks' contents.txt
grep -q 'repos/xworkspace-console' contents.txt
grep -q 'packages/components/xworkspace-console-runtime-linux-${{ matrix.arch }}.tar.gz' contents.txt
grep -q 'packages/components/xworkmate-bridge-linux-${{ matrix.arch }}.tar.gz' contents.txt
grep -q 'packages/components/qmd-runtime-linux-${{ matrix.arch }}.tar.gz' contents.txt
grep -q 'packages/components/litellm-runtime-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.arch }}.tar.gz' contents.txt
grep -q 'metadata/litellm-runtime.env' contents.txt
grep -q 'metadata/components/xworkmate-bridge.tag' contents.txt
grep -q 'packages/apt/Packages.gz' contents.txt
if [[ "${{ matrix.arch }}" == "arm64" ]]; then
grep -Eq 'packages/playwright-browsers/.*/chrome-linux(64)?/chrome$' contents.txt
grep -q 'metadata/apt/browser-deb-packages.txt' contents.txt
fi
if [[ "${{ matrix.distro }}:${{ matrix.version }}" == "ubuntu:26.04" ]]; then
grep -Eq 'packages/python/.*/bin/python3.13$' contents.txt
fi
publish-release:
needs: [test-offline-package, publish]
runs-on: ubuntu-latest
env:
TAG_NAME: ${{ github.event.inputs.tag != '' && github.event.inputs.tag || format('offline-ai-workspace-{0}', github.run_number) }}
CONSOLE_RUNTIME_RELEASE_TAG: ${{ needs.publish.outputs.runtime_tag }}
RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }}
RSYNC_SSH_USER: ${{ secrets.RSYNC_SSH_USER }}
VPS_HOST: ${{ secrets.VPS_HOST }}
REMOTE_ROOT: /data/update-server/offline-package/ai-workspace
steps:
- uses: actions/checkout@v4
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
echo "Release $TAG_NAME already exists"
else
gh release create "$TAG_NAME" --title "Build $TAG_NAME" --notes "Offline AI Workspace all-in-one packages."
fi
- name: Download all package artifacts
uses: actions/download-artifact@v4
with:
path: release-artifacts
pattern: ai-workspace-all-in-one-offline-*
merge-multiple: true
- name: Download console runtime release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
mkdir -p release-artifacts
runtime_tag="$CONSOLE_RUNTIME_RELEASE_TAG"
if [[ "$runtime_tag" == "latest-runtime" || -z "$runtime_tag" ]]; then
runtime_tag="$(
gh release list \
--limit 50 \
--json tagName \
--jq '[.[].tagName | select(startswith("runtime-"))][0]'
)"
fi
if [[ -z "$runtime_tag" || "$runtime_tag" == "null" ]]; then
echo "Unable to resolve console runtime release tag" >&2
exit 1
fi
echo "Downloading console runtime assets from ${runtime_tag}"
gh release download "$runtime_tag" \
--pattern 'xworkspace-console-runtime-*.tar.gz' \
--pattern 'SHA256SUMS' \
--dir release-artifacts \
--clobber
- name: Upload packages to GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
shopt -s nullglob
# GitHub release assets are hard-capped at 2 GiB. Packages at/over the
# limit are split into <2 GiB parts plus a ".parts" manifest; the
# offline bootstrap reassembles them. Parts are 1900 MiB.
GH_ASSET_LIMIT=2147483648
SPLIT_SIZE=1900m
delete_asset_if_present() {
local name=$1
if gh release view "$TAG_NAME" --json assets --jq '.assets[].name' \
2>/dev/null | grep -Fxq "$name"; then
echo "Deleting existing release asset ${name}"
gh release delete-asset "$TAG_NAME" "$name" --yes
fi
}
upload_one() {
local file=$1 name attempt
name="$(basename "$file")"
delete_asset_if_present "$name"
for attempt in 1 2 3; do
if gh release upload "$TAG_NAME" "$file" --clobber; then
return 0
fi
if [[ "$attempt" -eq 3 ]]; then
echo "Failed to upload ${file} after ${attempt} attempts" >&2
return 1
fi
sleep $((attempt * 20))
done
}
packages=(release-artifacts/*.tar.gz)
if [[ ${#packages[@]} -eq 0 ]]; then
echo "No offline packages found in release-artifacts" >&2
exit 1
fi
for package in "${packages[@]}"; do
asset_name="$(basename "$package")"
size="$(stat -c%s "$package")"
if [[ "$size" -lt "$GH_ASSET_LIMIT" ]]; then
echo "Uploading ${asset_name} (${size} bytes)"
upload_one "$package"
else
echo "Splitting oversized ${asset_name} (${size} bytes) into ${SPLIT_SIZE} parts"
dir="$(dirname "$package")"
(
cd "$dir"
rm -f "${asset_name}".part-* "${asset_name}.parts"
split -b "$SPLIT_SIZE" -d -a 3 "$asset_name" "${asset_name}.part-"
ls "${asset_name}".part-* | LC_ALL=C sort > "${asset_name}.parts"
)
# Remove any stale whole asset, then publish the manifest + parts.
delete_asset_if_present "$asset_name"
upload_one "${dir}/${asset_name}.parts"
for part in "${dir}/${asset_name}".part-*; do
echo "Uploading $(basename "$part")"
upload_one "$part"
done
fi
done
- name: Rsync packages to remote mirror
if: ${{ env.RSYNC_SSH_KEY != '' && env.RSYNC_SSH_USER != '' && env.VPS_HOST != '' }}
run: |
set -euo pipefail
sudo apt-get update -y
sudo apt-get install -y rsync openssh-client
mkdir -p ~/.ssh
echo "$RSYNC_SSH_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts
remote_dir="${REMOTE_ROOT}/${TAG_NAME}"
ssh -i ~/.ssh/id_rsa "${RSYNC_SSH_USER}@${VPS_HOST}" "mkdir -p '${remote_dir}'"
rsync -av -e "ssh -i ~/.ssh/id_rsa" release-artifacts/*.tar.gz \
"${RSYNC_SSH_USER}@${VPS_HOST}:${remote_dir}/"