mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
327 lines
14 KiB
YAML
327 lines
14 KiB
YAML
name: Blacksmith Build Artifacts Testbox
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
testbox_id:
|
|
type: string
|
|
description: "Testbox session ID"
|
|
required: true
|
|
pull_request:
|
|
types: [opened, reopened, synchronize, ready_for_review]
|
|
paths:
|
|
- "scripts/ci-testbox-budget.mjs"
|
|
- ".github/workflows/ci-build-artifacts-testbox.yml"
|
|
- ".github/actions/prepare-testbox-shell/**"
|
|
- ".github/actions/setup-node-env/**"
|
|
- ".github/actions/setup-pnpm-store-cache/**"
|
|
- ".github/actions/ensure-base-commit/**"
|
|
- ".github/actions/git-owner/**"
|
|
- "scripts/ci-hydrate-testbox-env.sh"
|
|
- "scripts/ci-hydrate-live-auth.sh"
|
|
- "scripts/lib/merge-head-diff-base.mjs"
|
|
- "scripts/lib/pnpm-lockfile-documents.mjs"
|
|
- "package.json"
|
|
- "pnpm-lock.yaml"
|
|
- "pnpm-workspace.yaml"
|
|
- "patches/**"
|
|
- ".npmrc"
|
|
- ".pnpmfile.*"
|
|
- "pnpmfile.cjs"
|
|
- "node-version.mjs"
|
|
- "**/package.json"
|
|
- "scripts/check-install-dependency-ownership.mjs"
|
|
- "scripts/postinstall-bundled-plugins.mjs"
|
|
- "scripts/lib/package-dist-imports.mjs"
|
|
- "scripts/lib/javascript-statements.mjs"
|
|
- "scripts/lib/package-lifecycle-marker.mjs"
|
|
- "scripts/lib/fs-safe-prebuild.mjs"
|
|
- "scripts/windows-cmd-helpers.mjs"
|
|
- "scripts/preinstall-package-manager-warning.mjs"
|
|
- "scripts/prepare-git-hooks.mjs"
|
|
# The build wrappers invoke other scripts and shared helpers.
|
|
- "scripts/**"
|
|
- "ui/scripts/**"
|
|
- "ui/src/build-info-normalizers.ts"
|
|
- "ui/package.json"
|
|
- "ui/vite.config.ts"
|
|
- "tsdown.config.ts"
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
# Admission precedes job skips: isolate passive drafts before they can cancel useful work.
|
|
# Keep conversion events in the PR group where subscribed, as an intentional cancellation signal.
|
|
group: >-
|
|
${{ github.event_name == 'pull_request' && github.event.pull_request.draft && github.event.action != 'converted_to_draft'
|
|
&& format('{0}-passive-draft-{1}', github.workflow, github.run_id)
|
|
|| (github.event_name == 'pull_request' && format('{0}-pr-v1-{1}', github.workflow, github.event.pull_request.number) || format('{0}-manual-v1-{1}', github.workflow, github.run_id)) }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
|
|
jobs:
|
|
admission:
|
|
if: github.event_name == 'workflow_dispatch'
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 3
|
|
permissions:
|
|
contents: read
|
|
actions: read
|
|
outputs:
|
|
group: ${{ steps.budget.outputs.group }}
|
|
runner: ${{ steps.budget.outputs.runner }}
|
|
minutes: ${{ steps.budget.outputs.minutes }}
|
|
expires_at: ${{ steps.budget.outputs.expires_at }}
|
|
steps:
|
|
- name: Checkout admission policy
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
sparse-checkout: scripts/ci-testbox-budget.mjs
|
|
sparse-checkout-cone-mode: false
|
|
persist-credentials: false
|
|
- name: Admit Testbox within the shared budget
|
|
id: budget
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TESTBOX_PROFILE: build
|
|
TESTBOX_ID: ${{ inputs.testbox_id }}
|
|
run: node scripts/ci-testbox-budget.mjs admit
|
|
|
|
build-artifacts:
|
|
needs: admission
|
|
if: ${{ !cancelled() && ((github.event_name == 'pull_request' && !github.event.pull_request.draft) || needs.admission.result == 'success') }}
|
|
concurrency:
|
|
group: ${{ github.event_name == 'workflow_dispatch' && needs.admission.outputs.group || format('testbox-validation-{0}-{1}', github.workflow, github.run_id) }}
|
|
cancel-in-progress: false
|
|
permissions:
|
|
contents: read
|
|
name: "build-artifacts"
|
|
# Pull requests only validate the artifact build; real Testbox leases
|
|
# arrive via dispatch. Hosted PR runs keep this landing gate satisfiable
|
|
# during Blacksmith outages (mirrors ci-check-testbox.yml).
|
|
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-24.04' || needs.admission.outputs.runner }}
|
|
timeout-minutes: ${{ fromJSON(needs.admission.outputs.minutes || '35') }}
|
|
steps:
|
|
# Testbox lifecycle actions require Blacksmith VM metadata; PRs validate the build only.
|
|
- name: Begin Testbox
|
|
if: github.event_name == 'workflow_dispatch'
|
|
uses: useblacksmith/begin-testbox@233448af4bfdc6fca509a7f0974411ac6d8a8043
|
|
with:
|
|
testbox_id: ${{ inputs.testbox_id }}
|
|
|
|
- name: Reject expired Testbox admission
|
|
if: github.event_name == 'workflow_dispatch'
|
|
shell: bash
|
|
env:
|
|
TESTBOX_EXPIRES_AT: ${{ needs.admission.outputs.expires_at }}
|
|
run: |
|
|
if [[ ! "$TESTBOX_EXPIRES_AT" =~ ^[0-9]+$ ]] ||
|
|
(( $(date +%s) >= TESTBOX_EXPIRES_AT / 1000 )); then
|
|
echo "Testbox waited more than 10 minutes; request a fresh lease." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Checkout
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
fetch-depth: ${{ github.event_name == 'pull_request' && '2' || '0' }}
|
|
persist-credentials: false
|
|
|
|
- name: Bound Testbox idle lifetime
|
|
if: github.event_name == 'workflow_dispatch'
|
|
run: node scripts/ci-testbox-budget.mjs configure
|
|
|
|
- name: Setup Node environment
|
|
id: setup-node-env
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
cache-mode: ${{ github.event_name == 'workflow_dispatch' && 'read-write' || 'restore' }}
|
|
install-bun: "false"
|
|
|
|
- name: Resolve release dist cache seeds
|
|
id: dist-cache-seeds
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
cache_prefix="${RUNNER_OS}-dist-build-v2-"
|
|
declare -A seen=()
|
|
|
|
resolve_tag_sha() {
|
|
local tag="$1"
|
|
local direct=""
|
|
local peeled=""
|
|
|
|
while read -r sha ref; do
|
|
if [[ "$ref" == "refs/tags/${tag}^{}" ]]; then
|
|
peeled="$sha"
|
|
elif [[ "$ref" == "refs/tags/${tag}" ]]; then
|
|
direct="$sha"
|
|
fi
|
|
done < <(git ls-remote --tags origin "refs/tags/${tag}" "refs/tags/${tag}^{}")
|
|
|
|
printf '%s\n' "${peeled:-$direct}"
|
|
}
|
|
|
|
{
|
|
echo "restore-keys<<EOF"
|
|
for dist_tag in beta latest; do
|
|
version="$(npm view "openclaw@${dist_tag}" version 2>/dev/null || true)"
|
|
if [[ -z "$version" ]]; then
|
|
echo "Could not resolve npm dist-tag ${dist_tag}; skipping cache seed." >&2
|
|
continue
|
|
fi
|
|
|
|
sha="$(resolve_tag_sha "v${version}")"
|
|
if [[ -z "$sha" ]]; then
|
|
echo "Could not resolve git tag v${version}; skipping cache seed." >&2
|
|
continue
|
|
fi
|
|
|
|
key="${cache_prefix}${sha}"
|
|
if [[ -z "${seen[$key]+x}" ]]; then
|
|
echo "$key"
|
|
seen[$key]=1
|
|
fi
|
|
done
|
|
echo "${cache_prefix}"
|
|
echo "EOF"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Restore dist build cache
|
|
id: dist-cache
|
|
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
.artifacts/build-all-cache/
|
|
dist/
|
|
dist-runtime/
|
|
packages/*/dist/
|
|
key: ${{ runner.os }}-dist-build-v2-${{ github.sha }}
|
|
restore-keys: ${{ steps.dist-cache-seeds.outputs.restore-keys }}
|
|
|
|
- name: Build dist on cache miss
|
|
if: steps.dist-cache.outputs.cache-hit != 'true'
|
|
env:
|
|
# Hosted PR runners have 16GB total; the 16GB heap fits only the
|
|
# dispatch-time 16vcpu Blacksmith box.
|
|
NODE_OPTIONS: ${{ github.event_name == 'pull_request' && '--max-old-space-size=8192' || '--max-old-space-size=16384' }}
|
|
run: pnpm build:ci-artifacts
|
|
|
|
- name: Build Control UI on cache miss
|
|
if: steps.dist-cache.outputs.cache-hit != 'true'
|
|
run: pnpm ui:build
|
|
|
|
- name: Verify build artifacts
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
test -d dist
|
|
test -d dist-runtime
|
|
test -f packages/ai/dist/internal/runtime.mjs
|
|
if [[ ! -f dist/index.js && ! -f dist/index.mjs ]]; then
|
|
echo "Missing dist/index.js or dist/index.mjs" >&2
|
|
exit 1
|
|
fi
|
|
test -f dist/build-info.json
|
|
test -f dist/control-ui/index.html
|
|
|
|
- name: Save dist build cache
|
|
if: steps.setup-node-env.outputs.cache-mode == 'read-write' && steps.dist-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
.artifacts/build-all-cache/
|
|
dist/
|
|
dist-runtime/
|
|
packages/*/dist/
|
|
key: ${{ runner.os }}-dist-build-v2-${{ github.sha }}
|
|
|
|
- name: Prepare Testbox shell
|
|
uses: ./.github/actions/prepare-testbox-shell
|
|
with:
|
|
base-ref: ${{ github.event.pull_request.base.sha || 'refs/remotes/origin/main' }}
|
|
|
|
- name: Hydrate Testbox provider env helper
|
|
shell: bash
|
|
env:
|
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
|
|
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
|
|
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
|
|
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
|
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
|
|
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
|
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
|
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
|
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
|
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
|
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
|
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
|
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
|
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
|
|
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
|
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
|
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
|
|
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
|
|
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
|
|
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
|
|
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
|
|
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
|
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
|
FAL_KEY: ${{ secrets.FAL_KEY }}
|
|
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
|
|
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
|
|
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
|
|
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
|
|
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
|
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
|
|
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
|
|
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
|
|
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
|
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
|
|
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
|
|
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
|
|
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
|
|
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
|
|
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
|
|
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
|
|
run: bash scripts/ci-hydrate-testbox-env.sh
|
|
|
|
- name: Run Testbox
|
|
# Temporary local-listener fix: https://github.com/useblacksmith/run-testbox/pull/15
|
|
# Return to useblacksmith/run-testbox after the fix lands upstream.
|
|
uses: steipete/run-testbox@2b6b1be536ec7f3c73757fedf5460a27ab4856b4
|
|
if: github.event_name == 'workflow_dispatch' && always()
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
|
|
- name: Close Testbox SSH sessions
|
|
if: github.event_name == 'workflow_dispatch' && always()
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Testbox state stores Blacksmith's external forwarded port. Resolve
|
|
# sshd's VM-local listener because that is the sport visible to ss.
|
|
runner_ssh_local_port="$(sudo sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }')"
|
|
if [[ ! "$runner_ssh_local_port" =~ ^[0-9]+$ ]] ||
|
|
(( runner_ssh_local_port < 1 || runner_ssh_local_port > 65535 )); then
|
|
echo "No valid local SSH listener port found; skipping session cleanup"
|
|
exit 0
|
|
fi
|
|
|
|
# run-testbox has no post hook. Close only Testbox client sockets so
|
|
# Blacksmith's runner teardown does not wait for its 290-second grace.
|
|
timeout --signal=KILL 5s sudo ss -K state established \
|
|
"( sport = :${runner_ssh_local_port} )" || true
|