Skip to content

feat: add system upgrade action #132

feat: add system upgrade action

feat: add system upgrade action #132

Workflow file for this run

name: Charm Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
workflow_dispatch:
jobs:
lint:
name: Lint Charm Code
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y shellcheck
- name: Lint shell scripts
run: |
find hooks -type f -executable -exec shellcheck {} +
- name: Check hook permissions
run: |
for hook in hooks/*; do
if [ -f "$hook" ] && [ ! -x "$hook" ]; then
echo "ERROR: Hook $hook is not executable"
exit 1
fi
done
- name: Validate metadata
run: |
if [ ! -f metadata.yaml ]; then
echo "ERROR: metadata.yaml not found"
exit 1
fi
# Check required fields
for field in name summary description; do
if ! grep -q "^${field}:" metadata.yaml; then
echo "ERROR: Required field '${field}' not found in metadata.yaml"
exit 1
fi
done
- name: Validate config
run: |
if [ ! -f config.yaml ]; then
echo "ERROR: config.yaml not found"
exit 1
fi
build:
name: Build Charm
runs-on: ubuntu-24.04
needs: lint
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install charmcraft
run: |
sudo snap install charmcraft --classic
- name: Pack charm
run: |
charmcraft pack --destructive-mode
- name: Upload charm artifact
uses: actions/upload-artifact@v7
with:
name: openclaw-charm
path: openclaw_*.charm
retention-days: 1
test-install:
name: Test Charm Installation
runs-on: ubuntu-24.04
needs: build
strategy:
matrix:
install-method: [npm, pnpm, bun]
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
with:
channel: latest/stable
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-openclaw
- name: Deploy charm
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config install-method=${{ matrix.install-method }} \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key-placeholder"
- name: Wait for deployment
run: |
# Wait up to 15 minutes for installation to complete
juju-wait -v -m test-openclaw -t 900
- name: Verify installation
run: |
# Get unit IP
UNIT_IP=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units | to_entries[0].value."public-address"')
echo "Unit IP: $UNIT_IP"
# Check if openclaw command exists
juju ssh openclaw/0 'which openclaw'
# Check openclaw version
juju ssh openclaw/0 'openclaw --version' || echo "Version check skipped (may require config)"
# Check systemd service
juju ssh openclaw/0 'systemctl status openclaw.service' || true
# Check if port is open
juju ssh openclaw/0 'ss -tulpn | grep 18789' || echo "Port not bound (expected without valid API key)"
- name: Test configuration changes
run: |
# Record gateway token before any config changes
TOKEN_BEFORE=$(juju ssh openclaw/0 'jq -r .gateway.auth.token /home/ubuntu/.openclaw/openclaw.json')
echo "Gateway token before config change: $TOKEN_BEFORE"
# Change gateway port
juju config openclaw gateway-port=18790
# Wait for config change to apply
sleep 30
# Verify port change
juju ssh openclaw/0 'ss -tulpn | grep 18790' || echo "Port change test (may not bind without valid key)"
# Verify gateway token is unchanged after config-changed
TOKEN_AFTER=$(juju ssh openclaw/0 'jq -r .gateway.auth.token /home/ubuntu/.openclaw/openclaw.json')
echo "Gateway token after config change: $TOKEN_AFTER"
if [ "$TOKEN_AFTER" = "$TOKEN_BEFORE" ]; then
echo "✓ Gateway token preserved across config-changed"
else
echo "✗ Gateway token changed after config-changed"
exit 1
fi
# Change back
juju config openclaw gateway-port=18789
- name: Collect logs on failure
if: failure()
run: |
echo "=== Juju Status ==="
juju status --format=yaml
echo "=== Debug Log ==="
juju debug-log --replay --no-tail --limit 500
echo "=== Unit Logs ==="
juju ssh openclaw/0 'journalctl -u openclaw.service -n 200' || true
echo "=== OpenClaw Config ==="
juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' || true
- name: Cleanup
if: always()
run: |
juju destroy-model test-openclaw -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-channels:
name: Test Messaging Channel Configuration
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-channels
- name: Deploy with channel config
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key" \
--config telegram-bot-token="123456:TEST" \
--config control-ui-allowed-origins="http://192.168.1.50:18789"
- name: Wait for deployment
run: |
juju-wait -v -m test-channels -t 900
- name: Verify channel configuration
run: |
# Check that config file contains channel configuration
juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' > config.json
if grep -q "telegram" config.json; then
echo "✓ Channel configuration verified"
else
echo "✗ Channel configuration missing"
exit 1
fi
# Verify AI model configuration
PRIMARY_MODEL=$(jq -r '.agents.defaults.model.primary' config.json)
if [ "$PRIMARY_MODEL" = "anthropic/claude-sonnet-3-5" ]; then
echo "✓ Primary model configuration verified"
else
echo "✗ Primary model configuration mismatch (got: $PRIMARY_MODEL)"
exit 1
fi
MODEL_CONFIG=$(jq -c '.agents.defaults.models["anthropic/claude-sonnet-3-5"]' config.json)
if [ "$MODEL_CONFIG" = "{}" ]; then
echo "✓ Model dictionary configuration verified"
else
echo "✗ Model dictionary configuration mismatch (got: $MODEL_CONFIG)"
exit 1
fi
origins=$(jq -r '.gateway.controlUi.allowedOrigins[0]' config.json)
echo "allowedOrigins[0]: $origins"
if [ "$origins" = "http://192.168.1.50:18789" ]; then
echo "✓ control-ui-allowed-origins written correctly"
else
echo "✗ control-ui-allowed-origins mismatch (got: $origins)"
exit 1
fi
- name: Cleanup
if: always()
run: |
juju destroy-model test-channels -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-multi-model-config:
name: Test Multi-Model Configuration
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-models
- name: Deploy with multi-model config
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5,claude-opus-4-5" \
--config ai-api-key="test-key-1" \
--config ai0-provider="openai" \
--config ai0-model="gpt-4o,gpt-4-turbo" \
--config ai0-api-key="test-key-2" \
--config ai1-provider="google" \
--config ai1-model="gemini-2.5-flash" \
--config ai1-api-key="test-key-3"
- name: Wait for deployment
run: |
juju-wait -v -m test-models -t 900
- name: Verify multi-model configuration
run: |
juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' > config.json
# Verify AI model primary
PRIMARY_MODEL=$(jq -r ".agents.defaults.model.primary" config.json)
if [ "$PRIMARY_MODEL" = "anthropic/claude-sonnet-3-5" ]; then
echo "✓ Primary model configuration verified"
else
echo "✗ Primary model configuration mismatch (got: $PRIMARY_MODEL)"
exit 1
fi
# Verify AI model fallbacks
FALLBACKS_LEN=$(jq ".agents.defaults.model.fallbacks | length" config.json)
if [ "$FALLBACKS_LEN" = "4" ]; then
echo "✓ Fallbacks count verified ($FALLBACKS_LEN)"
else
echo "✗ Fallbacks count mismatch (got: $FALLBACKS_LEN)"
exit 1
fi
# Verify models dictionary
MODELS_LEN=$(jq ".agents.defaults.models | length" config.json)
if [ "$MODELS_LEN" = "5" ]; then
echo "✓ Models dictionary count verified ($MODELS_LEN)"
else
echo "✗ Models dictionary count mismatch (got: $MODELS_LEN)"
jq ".agents.defaults.models" config.json
exit 1
fi
# Verify model dictionary properties
SONNET_CONFIG=$(jq -c ".agents.defaults.models[\"anthropic/claude-sonnet-3-5\"]" config.json)
if [ "$SONNET_CONFIG" = "{}" ]; then
echo "✓ Model dictionary contains anthropic/claude-sonnet-3-5"
else
echo "✗ Model dictionary missing anthropic/claude-sonnet-3-5 (got: $SONNET_CONFIG)"
exit 1
fi
GPT4O_CONFIG=$(jq -c ".agents.defaults.models[\"openai/gpt-4o\"]" config.json)
if [ "$GPT4O_CONFIG" = "{}" ]; then
echo "✓ Model dictionary contains openai/gpt-4o"
else
echo "✗ Model dictionary missing openai/gpt-4o (got: $GPT4O_CONFIG)"
exit 1
fi
- name: Cleanup
if: always()
run: |
juju destroy-model test-models -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-config-preservation:
name: Test Config Preservation
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
with:
channel: latest/stable
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-config-preservation
- name: Deploy charm
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key" \
--config control-ui-allowed-origins="http://10.0.0.1:18789,http://10.0.0.2:18789"
- name: Wait for deployment
run: |
juju-wait -v -m test-config-preservation -t 900
- name: Verify control-ui-allowed-origins written to openclaw.json
run: |
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
origins=$(echo "$config" | jq -r '.gateway.controlUi.allowedOrigins | length')
echo "allowedOrigins count: $origins"
if [ "$origins" = "2" ]; then
echo "✓ gateway.controlUi.allowedOrigins contains 2 entries"
else
echo "✗ Expected 2 entries, got: $origins"
echo "$config" | jq '.gateway.controlUi'
exit 1
fi
origin0=$(echo "$config" | jq -r '.gateway.controlUi.allowedOrigins[0]')
origin1=$(echo "$config" | jq -r '.gateway.controlUi.allowedOrigins[1]')
echo "Origin 0: $origin0"
echo "Origin 1: $origin1"
if [ "$origin0" = "http://10.0.0.1:18789" ] && [ "$origin1" = "http://10.0.0.2:18789" ]; then
echo "✓ Origins match expected values"
else
echo "✗ Origins mismatch"
exit 1
fi
- name: Record gateway token before config change
run: |
TOKEN=$(juju ssh openclaw/0 'jq -r .gateway.auth.token /home/ubuntu/.openclaw/openclaw.json')
echo "token_before=$TOKEN" >> "$GITHUB_ENV"
echo "Gateway token recorded: $TOKEN"
- name: Inject user-managed field and channel property into openclaw.json
run: |
juju ssh openclaw/0 'jq '\''.user_custom_field = "preserve-me-12345" | .channels.telegram.custom_prop = "preserve-me-6789"'\'' \
/home/ubuntu/.openclaw/openclaw.json > /tmp/oc.tmp && \
mv /tmp/oc.tmp /home/ubuntu/.openclaw/openclaw.json'
echo "✓ Injected user_custom_field and telegram.custom_prop into openclaw.json"
- name: Trigger config-changed via juju config update
run: |
juju config openclaw log-level=debug telegram-bot-token="654321:NEW-TOKEN"
sleep 30
juju config openclaw log-level=info
sleep 30
- name: Verify user field, channel property and gateway token survive config-changed
run: |
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
custom=$(echo "$config" | jq -r '.user_custom_field')
echo "user_custom_field after config-changed: $custom"
if [ "$custom" = "preserve-me-12345" ]; then
echo "✓ User-managed field preserved across config-changed"
else
echo "✗ User-managed field was lost (got: $custom)"
exit 1
fi
chan_custom=$(echo "$config" | jq -r '.channels.telegram.custom_prop')
echo "channels.telegram.custom_prop after config-changed: $chan_custom"
if [ "$chan_custom" = "preserve-me-6789" ]; then
echo "✓ Custom channel property preserved across config-changed"
else
echo "✗ Custom channel property was lost (got: $chan_custom)"
exit 1
fi
token_after=$(echo "$config" | jq -r '.gateway.auth.token')
echo "Gateway token after config-changed: $token_after"
if [ "$token_after" = "${{ env.token_before }}" ]; then
echo "✓ Gateway token preserved across config-changed"
else
echo "✗ Gateway token changed (before: ${{ env.token_before }}, after: $token_after)"
exit 1
fi
new_tg_token=$(echo "$config" | jq -r '.channels.telegram.botToken')
if [ "$new_tg_token" = "654321:NEW-TOKEN" ]; then
echo "✓ Telegram botToken updated correctly"
else
echo "✗ Telegram botToken update failed (got: $new_tg_token)"
exit 1
fi
- name: Clear control-ui-allowed-origins and verify default gateway URL
run: |
juju config openclaw control-ui-allowed-origins=""
sleep 30
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
origins=$(echo "$config" | jq -r '.gateway.controlUi.allowedOrigins[0]')
port=$(juju config openclaw gateway-port)
echo "allowedOrigins after clearing: $origins"
if [ "$origins" = "http://127.0.0.1:${port}" ]; then
echo "✓ gateway.controlUi.allowedOrigins defaults to loopback gateway URL when config is empty"
else
echo "✗ expected http://127.0.0.1:${port}, got: $origins"
exit 1
fi
- name: Restart container and verify persistence
run: |
unit_machine=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units | to_entries[0].value["machine"]')
container_name=$(juju status --format=json | jq -r --arg m "$unit_machine" '.machines[$m]["instance-id"]')
echo "Restarting container: $container_name"
lxc restart "$container_name"
sleep 45
timeout 120 bash -c '
while true; do
s=$(juju status openclaw --format=json 2>/dev/null | jq -r ".applications.openclaw.units | to_entries[0].value[\"workload-status\"].current" 2>/dev/null || echo "unknown")
echo "Status: $s"
[ "$s" = "active" ] && break
sleep 5
done
'
sleep 10
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
token_restart=$(echo "$config" | jq -r '.gateway.auth.token')
echo "Gateway token after restart: $token_restart"
if [ "$token_restart" = "${{ env.token_before }}" ]; then
echo "✓ Gateway token preserved across container restart"
else
echo "✗ Gateway token changed after restart"
exit 1
fi
custom_restart=$(echo "$config" | jq -r '.user_custom_field')
echo "user_custom_field after restart: $custom_restart"
if [ "$custom_restart" = "preserve-me-12345" ]; then
echo "✓ User-managed field preserved across container restart"
else
echo "✗ User-managed field lost after restart (got: $custom_restart)"
exit 1
fi
chan_custom_restart=$(echo "$config" | jq -r '.channels.telegram.custom_prop')
echo "channels.telegram.custom_prop after restart: $chan_custom_restart"
if [ "$chan_custom_restart" = "preserve-me-6789" ]; then
echo "✓ Custom channel property preserved across container restart"
else
echo "✗ Custom channel property lost after restart (got: $chan_custom_restart)"
exit 1
fi
- name: Collect logs on failure
if: failure()
run: |
echo "=== Juju Status ==="
juju status --format=yaml
echo "=== Debug Log ==="
juju debug-log --replay --no-tail --limit 500
echo "=== OpenClaw Config ==="
juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' || true
- name: Cleanup
if: always()
run: |
juju destroy-model test-config-preservation -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-upgrade:
name: Test Charm Upgrade
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-upgrade
- name: Deploy v1
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key"
- name: Wait for initial deployment
run: |
juju-wait -v -m test-upgrade -t 900
- name: Refresh charm (simulate upgrade)
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju refresh openclaw --path ./$CHARM_FILE
sleep 60 # Give upgrade-charm hook time to complete
- name: Verify upgrade success
run: |
# Get unit workload status (field is "current", not "status")
status=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units[]["workload-status"].current' | head -1)
echo "Current workload status: $status"
if [ "$status" = "active" ] || [ "$status" = "blocked" ]; then
echo "✓ Upgrade successful (status: $status)"
else
echo "✗ Upgrade failed - unexpected status: $status"
echo "Full juju status:"
juju status openclaw
exit 1
fi
- name: Cleanup
if: always()
run: |
juju destroy-model test-upgrade -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-auto-update:
name: Test Auto-Update Functionality
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-auto-update
- name: Deploy with auto-update disabled and specific version
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config install-method=npm \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key" \
--config auto-update=false \
--config version="2026.2.6-3"
- name: Wait for initial deployment
run: |
juju-wait -v -m test-auto-update -t 900
- name: Verify initial version installed
run: |
# Check openclaw version
initial_version=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && openclaw --version 2>&1 | tail -1"' || echo "unknown")
echo "Initial OpenClaw version: $initial_version"
if [ "$initial_version" = "2026.2.6-3" ]; then
echo "✓ Specific version installed correctly"
else
echo "⚠ Version mismatch (got: $initial_version, expected: 2026.2.6-3)"
echo "This may be OK if the version was unavailable"
fi
# Check symlink points to correct Node.js version
symlink=$(juju ssh openclaw/0 'ls -l /usr/local/bin/openclaw')
echo "Symlink: $symlink"
- name: Enable auto-update with latest version
run: |
# Enable auto-update and change to latest
juju config openclaw auto-update=true version=latest
# Wait for config-changed hook to complete
sleep 90
- name: Verify auto-update applied
run: |
# Wait for unit to become active/blocked again (not in maintenance)
timeout 300 bash -c '
while true; do
status=$(juju status openclaw --format=json | jq -r ".applications.openclaw.units[\"openclaw/0\"][\"workload-status\"].current")
echo "Current status: $status"
if [ "$status" = "active" ] || [ "$status" = "blocked" ]; then
break
fi
sleep 10
done
'
# Get updated version
updated_version=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && openclaw --version 2>&1 | tail -1"' || echo "unknown")
echo "Updated OpenClaw version: $updated_version"
# Verify it's newer than initial version (or at least different)
if [ "$updated_version" != "2026.2.6-3" ] && [ "$updated_version" != "unknown" ]; then
echo "✓ Auto-update successfully updated OpenClaw (from 2026.2.6-3 to $updated_version)"
else
echo "✗ Auto-update did not update version (still: $updated_version)"
echo "Checking logs..."
juju debug-log --replay --no-tail --limit 100
exit 1
fi
- name: Verify symlink updated correctly
run: |
# Check symlink points to latest Node.js version
symlink=$(juju ssh openclaw/0 'ls -l /usr/local/bin/openclaw')
echo "Updated symlink: $symlink"
# Get latest Node.js version from nvm
latest_node=$(juju ssh openclaw/0 'ls -1 /home/ubuntu/.nvm/versions/node | sort -V | tail -1')
echo "Latest Node.js version: $latest_node"
# Verify symlink points to latest version
if echo "$symlink" | grep -q "$latest_node"; then
echo "✓ Symlink correctly points to latest Node.js version"
else
echo "⚠ Symlink may not point to latest Node.js version"
echo "This could be expected if openclaw binary doesn't exist in latest version yet"
fi
- name: Test auto-update on charm upgrade
run: |
# Refresh charm with auto-update enabled (simulates charm upgrade scenario)
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju refresh openclaw --path ./$CHARM_FILE
# Wait for upgrade-charm hook to complete
sleep 90
# Verify status is still healthy
status=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units["openclaw/0"]["workload-status"].current')
echo "Status after charm upgrade: $status"
if [ "$status" = "active" ] || [ "$status" = "blocked" ]; then
echo "✓ Charm upgrade with auto-update successful"
else
echo "✗ Unexpected status after upgrade: $status"
juju status openclaw
exit 1
fi
- name: Test Node.js version change with auto-update
run: |
echo "=== Testing Node.js version change from 24 to 22 ==="
# Get current Node.js version
current_node=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm version default"' | tr -d '\n')
echo "Current Node.js version: $current_node"
# Change Node.js version from 24 to 22
juju config openclaw node-version=22
# Wait for config-changed to complete
sleep 90
# Verify status is healthy
status=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units["openclaw/0"]["workload-status"].current')
echo "Status after node-version change: $status"
if [ "$status" != "active" ] && [ "$status" != "blocked" ]; then
echo "✗ Unexpected status after node-version change: $status"
juju status openclaw
juju debug-log --replay --no-tail --limit 200
exit 1
fi
# Verify nvm default changed to v22
new_node=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm version default"' | tr -d '\n')
echo "New Node.js version: $new_node"
if echo "$new_node" | grep -q "^v22\."; then
echo "✓ nvm default correctly changed to v22"
else
echo "✗ nvm default did not change to v22 (got: $new_node)"
exit 1
fi
# Verify openclaw is installed in the new Node.js version
openclaw_v22=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 22 && npm list -g openclaw 2>&1"' || echo "not found")
echo "OpenClaw in v22: $openclaw_v22"
if echo "$openclaw_v22" | grep -q "openclaw@"; then
echo "✓ OpenClaw installed in Node.js v22"
else
echo "✗ OpenClaw not found in Node.js v22"
echo "Checking all Node versions for openclaw..."
juju ssh openclaw/0 'find /home/ubuntu/.nvm/versions/node -name openclaw -type f'
exit 1
fi
# Verify symlink points to v22
symlink=$(juju ssh openclaw/0 'ls -l /usr/local/bin/openclaw')
echo "Symlink after node-version change: $symlink"
if echo "$symlink" | grep -q "v22\."; then
echo "✓ Symlink correctly points to Node.js v22"
else
echo "✗ Symlink does not point to v22"
echo "Symlink: $symlink"
exit 1
fi
echo "=== Testing Node.js version change from 22 to 24 ==="
# Change back to v24
juju config openclaw node-version=24
# Wait for config-changed to complete
sleep 90
# Verify nvm default changed back to v24
final_node=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm version default"' | tr -d '\n')
echo "Final Node.js version: $final_node"
if echo "$final_node" | grep -q "^v24\."; then
echo "✓ nvm default correctly changed back to v24"
else
echo "✗ nvm default did not change back to v24 (got: $final_node)"
exit 1
fi
# Verify openclaw is installed in v24
openclaw_v24=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && npm list -g openclaw 2>&1"' || echo "not found")
echo "OpenClaw in v24: $openclaw_v24"
if echo "$openclaw_v24" | grep -q "openclaw@"; then
echo "✓ OpenClaw installed in Node.js v24"
else
echo "✗ OpenClaw not found in Node.js v24"
exit 1
fi
# Verify symlink points to v24
final_symlink=$(juju ssh openclaw/0 'ls -l /usr/local/bin/openclaw')
echo "Final symlink: $final_symlink"
if echo "$final_symlink" | grep -q "v24\."; then
echo "✓ Symlink correctly points to Node.js v24"
else
echo "✗ Symlink does not point to v24"
echo "Symlink: $final_symlink"
exit 1
fi
echo "✓ Node.js version change test completed successfully"
- name: Test status message shows Gateway URL after upgrade
run: |
echo "=== Testing status message after charm upgrade ==="
# Refresh charm again to test status message
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju refresh openclaw --path ./$CHARM_FILE
# Wait for upgrade to complete
sleep 90
# Get status message
status_msg=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units["openclaw/0"]["workload-status"].message')
echo "Status message after upgrade: $status_msg"
# Verify status message contains Gateway URL (not just "Configuration updated")
if echo "$status_msg" | grep -qE "Gateway: http://"; then
echo "✓ Status message correctly shows Gateway URL"
else
echo "⚠ Status message does not show Gateway URL (got: $status_msg)"
echo "This is a cosmetic issue - checking if service is actually running..."
service_status=$(juju ssh openclaw/0 'systemctl --user is-active openclaw-gateway.service' || echo "inactive")
if [ "$service_status" = "active" ]; then
echo "✓ Gateway service is running (status message issue is cosmetic)"
else
echo "✗ Gateway service is not running"
exit 1
fi
fi
- name: Collect logs on failure
if: failure()
run: |
echo "=== Juju Status ==="
juju status --format=yaml
echo "=== Debug Log ==="
juju debug-log --replay --no-tail --limit 500
echo "=== OpenClaw Version ==="
juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && openclaw --version"' || true
echo "=== Node.js Versions ==="
juju ssh openclaw/0 'ls -la /home/ubuntu/.nvm/versions/node/' || true
echo "=== Symlink Status ==="
juju ssh openclaw/0 'ls -l /usr/local/bin/openclaw' || true
echo "=== OpenClaw Installation Paths ==="
juju ssh openclaw/0 'find /home/ubuntu/.nvm/versions/node -name openclaw -type f -o -name openclaw -type l' || true
- name: Cleanup
if: always()
run: |
juju destroy-model test-auto-update -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-browser:
name: Test Browser Functionality (Chrome)
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-browser
- name: Deploy with install-pkgs=chrome,tailscale,homebrew
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key" \
--config install-pkgs="chrome,tailscale,homebrew"
- name: Wait for deployment
run: |
juju-wait -v -m test-browser -t 900
- name: Verify Chrome installation
run: |
echo "=== Verifying Chrome installation ==="
chrome_version=$(juju ssh openclaw/0 'google-chrome --version' 2>&1 || echo "NOT_INSTALLED")
echo "Chrome version: $chrome_version"
if echo "$chrome_version" | grep -q "Google Chrome"; then
echo "✓ Chrome is installed"
else
echo "✗ Chrome is not installed"
exit 1
fi
- name: Verify Tailscale installation
run: |
echo "=== Verifying Tailscale installation ==="
tailscale_version=$(juju ssh openclaw/0 'tailscale version' 2>&1 || echo "NOT_INSTALLED")
echo "Tailscale version: $tailscale_version"
if echo "$tailscale_version" | grep -q "NOT_INSTALLED"; then
echo "✗ Tailscale is not installed"
exit 1
else
echo "✓ Tailscale is installed"
fi
- name: Verify Homebrew installation
run: |
echo "=== Verifying Homebrew installation ==="
brew_version=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c "brew --version"' 2>&1 || echo "NOT_INSTALLED")
echo "Homebrew version: $brew_version"
if echo "$brew_version" | grep -q "Homebrew"; then
echo "✓ Homebrew is installed"
else
echo "✗ Homebrew is not installed"
exit 1
fi
- name: Verify AppArmor is disabled for Chrome
run: |
echo "=== Verifying AppArmor status ==="
# Check if Chrome profile is disabled
if juju ssh openclaw/0 'test -L /etc/apparmor.d/disable/chrome'; then
echo "✓ Chrome AppArmor profile is disabled (symlink exists)"
else
echo "✗ Chrome AppArmor profile is not disabled"
exit 1
fi
# Check if Chrome is not loaded in AppArmor
aa_status=$(juju ssh openclaw/0 'sudo aa-status 2>&1' || echo "")
if echo "$aa_status" | grep -q "chrome"; then
echo "✗ Chrome profile is still active in AppArmor"
echo "$aa_status"
exit 1
else
echo "✓ Chrome profile is not active in AppArmor"
fi
- name: Verify browser settings in openclaw.json
run: |
echo "=== Verifying browser configuration ==="
# Get browser config from openclaw.json
browser_config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' | jq '.browser')
echo "Browser config: $browser_config"
# Verify browser.enabled is true
enabled=$(echo "$browser_config" | jq -r '.enabled')
if [ "$enabled" = "true" ]; then
echo "✓ browser.enabled is true"
else
echo "✗ browser.enabled is not true (got: $enabled)"
exit 1
fi
# Verify browser.headless is true
headless=$(echo "$browser_config" | jq -r '.headless')
if [ "$headless" = "true" ]; then
echo "✓ browser.headless is true"
else
echo "✗ browser.headless is not true (got: $headless)"
exit 1
fi
# Verify browser.defaultProfile is "openclaw"
defaultProfile=$(echo "$browser_config" | jq -r '.defaultProfile')
if [ "$defaultProfile" = "openclaw" ]; then
echo "✓ browser.defaultProfile is 'openclaw'"
else
echo "✗ browser.defaultProfile is not 'openclaw' (got: $defaultProfile)"
exit 1
fi
echo "✓ All browser settings verified"
- name: Test browser start and status
run: |
echo "=== Testing browser start ==="
# Start the browser
browser_start_output=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && openclaw browser start"' 2>&1 || echo "FAILED")
echo "Browser start output: $browser_start_output"
if echo "$browser_start_output" | grep -qE "(Browser started|already running|successfully)"; then
echo "✓ Browser start command succeeded"
else
echo "⚠ Browser start output: $browser_start_output"
echo "Continuing to check status..."
fi
# Wait a moment for browser to initialize
sleep 5
# Check browser status
browser_status=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && openclaw browser status"' 2>&1 || echo "FAILED")
echo "Browser status output: $browser_status"
if echo "$browser_status" | grep -qE "(running|active|started|Browser:.*running)"; then
echo "✓ Browser status shows browser is running"
elif echo "$browser_status" | grep -qE "(stopped|inactive|not running)"; then
echo "✗ Browser is not running"
echo "Full status: $browser_status"
# Check for common issues
echo "=== Debugging browser start failure ==="
juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && which google-chrome"' || echo "Chrome binary not found"
juju ssh openclaw/0 'sudo -u ubuntu bash -l -c "google-chrome --version"' || echo "Chrome version check failed"
juju ssh openclaw/0 'ls -la /etc/apparmor.d/disable/' || echo "AppArmor disable dir check failed"
exit 1
else
echo "⚠ Unexpected browser status output: $browser_status"
echo "Cannot determine if browser is running - treating as failure"
exit 1
fi
- name: Test disabling browser
run: |
echo "=== Testing browser disable ==="
# Disable browser
juju config openclaw install-pkgs=""
# Wait for config to apply
sleep 30
# Verify browser object is null (not present) when install-pkgs is empty
browser_config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' | jq '.browser')
if [ "$browser_config" = "null" ]; then
echo "✓ browser object is null when install-pkgs is empty"
else
echo "✗ browser object is not null (got: $browser_config)"
exit 1
fi
# Verify browser status shows it's stopped or not running
browser_status_disabled=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && openclaw browser status"' 2>&1 || echo "NOT_RUNNING")
echo "Browser status after disable: $browser_status_disabled"
if echo "$browser_status_disabled" | grep -qE "(stopped|inactive|not running|disabled)"; then
echo "✓ Browser is stopped after disabling"
else
echo "⚠ Browser status after disable: $browser_status_disabled (may still be running)"
fi
- name: Test re-enabling browser
run: |
echo "=== Testing browser re-enable ==="
# Re-enable browser
juju config openclaw install-pkgs="chrome"
# Wait for config to apply
sleep 30
# Verify browser.enabled is true again
enabled=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' | jq -r '.browser.enabled')
if [ "$enabled" = "true" ]; then
echo "✓ browser.enabled is true after re-enabling"
else
echo "✗ browser.enabled is not true (got: $enabled)"
exit 1
fi
# Verify service is still active
status=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units[]["workload-status"].current' | head -1)
if [ "$status" = "active" ]; then
echo "✓ Service is still active after browser toggle"
else
echo "⚠ Service status is: $status"
fi
# Verify browser can start again after re-enabling
browser_start_again=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && openclaw browser start"' 2>&1 || echo "FAILED")
echo "Browser start after re-enable: $browser_start_again"
sleep 5
browser_status_reenabled=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && openclaw browser status"' 2>&1 || echo "FAILED")
echo "Browser status after re-enable: $browser_status_reenabled"
if echo "$browser_status_reenabled" | grep -qE "(running|active|started|Browser:.*running)"; then
echo "✓ Browser is running again after re-enabling"
else
echo "⚠ Browser status after re-enable: $browser_status_reenabled"
fi
- name: Test browser config persistence after container restart
run: |
echo "=== Testing browser config persistence after container restart ==="
# Verify browser is currently enabled and configured
enabled_before=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' | jq -r '.browser.enabled')
if [ "$enabled_before" != "true" ]; then
echo "✗ Browser not enabled before restart (got: $enabled_before)"
exit 1
fi
echo "✓ Browser is enabled before restart"
# Get the LXD container name from juju machine mapping
unit_machine=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units | to_entries[0].value["machine"]')
container_name=$(juju status --format=json | jq -r --arg machine "$unit_machine" '.machines[$machine]["instance-id"]')
echo "Container name: $container_name"
# Restart the LXD container to simulate system reboot
echo "Restarting container $container_name..."
lxc restart "$container_name"
# Wait for container to come back online and service to stabilize
echo "Waiting for service to come back online..."
sleep 45
# Wait for juju agent to reconnect (check unit status)
timeout=120
elapsed=0
while [ $elapsed -lt $timeout ]; do
status=$(juju status openclaw --format=json 2>/dev/null | jq -r '.applications.openclaw.units | to_entries[0].value["workload-status"].current' 2>/dev/null || echo "unknown")
if [ "$status" = "active" ]; then
echo "✓ Service is active after restart"
break
fi
echo "Waiting for service (status: $status)... $elapsed/$timeout seconds"
sleep 5
elapsed=$((elapsed + 5))
done
if [ $elapsed -ge $timeout ]; then
echo "✗ Service did not become active after $timeout seconds"
juju status openclaw
exit 1
fi
# Additional wait for openclaw.json to be regenerated
sleep 10
# Verify browser config is still present in openclaw.json
browser_config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' | jq '.browser')
enabled_after=$(echo "$browser_config" | jq -r '.enabled')
headless_after=$(echo "$browser_config" | jq -r '.headless')
profile_after=$(echo "$browser_config" | jq -r '.defaultProfile')
echo "Browser config after restart: $browser_config"
# Validate all browser settings persisted
if [ "$enabled_after" = "true" ]; then
echo "✓ browser.enabled=true persisted after restart"
else
echo "✗ browser.enabled not true after restart (got: $enabled_after)"
exit 1
fi
if [ "$headless_after" = "true" ]; then
echo "✓ browser.headless=true persisted after restart"
else
echo "✗ browser.headless not true after restart (got: $headless_after)"
exit 1
fi
if [ "$profile_after" = "openclaw" ]; then
echo "✓ browser.defaultProfile='openclaw' persisted after restart"
else
echo "✗ browser.defaultProfile incorrect after restart (got: $profile_after)"
exit 1
fi
echo "✅ All browser settings successfully persisted after container restart"
- name: Collect logs on failure
if: failure()
run: |
echo "=== Juju Status ==="
juju status --format=yaml
echo "=== Debug Log ==="
juju debug-log --replay --no-tail --limit 500
echo "=== Chrome Installation Status ==="
juju ssh openclaw/0 'dpkg -l | grep chrome' || true
echo "=== AppArmor Status ==="
juju ssh openclaw/0 'sudo aa-status' || true
echo "=== Browser Config ==="
juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json | jq .browser' || true
echo "=== OpenClaw Service Logs ==="
juju ssh openclaw/0 'journalctl --user -u openclaw-gateway.service -n 100 --no-pager' || true
- name: Cleanup
if: always()
run: |
juju destroy-model test-browser -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-manual-mode:
name: Test Manual Configuration Mode
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-manual
- name: Deploy with manual=true
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config manual=true \
--config install-method=npm
- name: Wait for deployment
run: |
juju-wait -v -m test-manual -t 900
- name: Verify manual mode behavior
run: |
# Check status message indicates manual mode
status_msg=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units[]["workload-status"].message' | head -1)
echo "Status message: $status_msg"
if echo "$status_msg" | grep -qi "manual"; then
echo "✓ Manual mode status verified"
else
echo "✗ Status does not indicate manual mode: $status_msg"
exit 1
fi
# Verify OpenClaw is installed
juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && which openclaw"'
echo "✓ OpenClaw binary installed"
# Verify version
version=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && openclaw --version 2>&1 | tail -1"')
echo "OpenClaw version: $version"
# Check that config files were NOT auto-generated by charm
if juju ssh openclaw/0 'test -f /home/ubuntu/.openclaw/openclaw.json'; then
echo "⚠ Config file exists (may be from OpenClaw itself, not charm)"
# Verify it doesn't contain charm-managed settings
if juju ssh openclaw/0 'grep -q "ai-provider\|ai-model" /home/ubuntu/.openclaw/openclaw.json 2>/dev/null'; then
echo "✗ Config contains charm-managed settings in manual mode"
exit 1
fi
fi
echo "✓ No charm-managed config generated"
- name: Test user config preservation
run: |
# Create a custom config file
juju ssh openclaw/0 'cat > /tmp/custom-config.json << "EOF"
{
"user_custom_field": "test-value-12345",
"gateway": {
"mode": "local",
"port": 18789
}
}
EOF'
# Copy it to OpenClaw directory
juju ssh openclaw/0 'sudo -u ubuntu cp /tmp/custom-config.json /home/ubuntu/.openclaw/openclaw.json'
# Trigger a config change (this would normally regenerate config in automatic mode)
juju config openclaw gateway-port=18790
sleep 30
# Verify custom field still exists
if juju ssh openclaw/0 'grep -q "user_custom_field.*test-value-12345" /home/ubuntu/.openclaw/openclaw.json'; then
echo "✓ User config preserved after charm config change"
else
echo "✗ User config was overwritten"
juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json'
exit 1
fi
- name: Test multi-unit with manual mode
run: |
# Add a second unit
juju add-unit openclaw
# Wait for it to deploy
timeout 900 bash -c 'until juju status openclaw --format=json | jq -e ".applications.openclaw.units[\"openclaw/1\"]" > /dev/null 2>&1; do sleep 10; done'
sleep 60 # Additional wait for installation
# Check second unit has manual mode
unit1_status=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units["openclaw/1"]["workload-status"].message' 2>/dev/null || echo "not-ready")
echo "Unit 1 status: $unit1_status"
# Verify OpenClaw installed on second unit
juju ssh openclaw/1 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm use 24 && which openclaw"' || echo "Unit 1 still installing"
# Verify peer relation node config exists on Node unit
if juju ssh openclaw/1 'test -f /home/ubuntu/.openclaw/node.json'; then
echo "✓ Node peer relation config exists"
juju ssh openclaw/1 'cat /home/ubuntu/.openclaw/node.json'
else
echo "⚠ Node config not yet created (unit may still be starting)"
fi
- name: Collect logs on failure
if: failure()
run: |
echo "=== Juju Status ==="
juju status --format=yaml
echo "=== Debug Log ==="
juju debug-log --replay --no-tail --limit 500
echo "=== Unit 0 Config ==="
juju ssh openclaw/0 'ls -la /home/ubuntu/.openclaw/' || true
juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json 2>/dev/null' || true
echo "=== Unit 1 Config ==="
juju ssh openclaw/1 'ls -la /home/ubuntu/.openclaw/' || true
juju ssh openclaw/1 'cat /home/ubuntu/.openclaw/node.json 2>/dev/null' || true
- name: Cleanup
if: always()
run: |
juju destroy-model test-manual -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-version-config:
name: Test Version Config
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
with:
channel: latest/stable
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-version-config
- name: Deploy with pinned version
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config install-method=npm \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key" \
--config version="2026.2.6-3"
- name: Wait for deployment
run: |
juju-wait -v -m test-version-config -t 900
- name: Verify pinned version installed
run: |
installed=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && openclaw --version 2>&1 | tail -1"' || echo "unknown")
echo "Installed OpenClaw version: $installed"
if [ "$installed" = "2026.2.6-3" ]; then
echo "✓ Pinned version 2026.2.6-3 installed correctly"
else
echo "✗ Expected 2026.2.6-3, got: $installed"
exit 1
fi
- name: Switch to a different pinned version
run: |
juju config openclaw version=2026.2.24
- name: Wait for pinned version reinstall to complete
run: |
juju-wait -v -m test-version-config -t 300
- name: Verify new pinned version installed after config change
run: |
installed=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && openclaw --version 2>&1 | tail -1"' || echo "unknown")
echo "Installed OpenClaw version: $installed"
if [ "$installed" = "2026.2.24" ]; then
echo "✓ Pinned version 2026.2.24 installed correctly after config change"
else
echo "✗ Expected 2026.2.24, got: $installed"
exit 1
fi
- name: Switch to version=latest with auto-update=true
run: |
juju config openclaw version=latest auto-update=true
- name: Wait for latest install to complete
run: |
juju-wait -v -m test-version-config -t 300
- name: Verify latest version is installed
run: |
installed=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && openclaw --version 2>&1 | tail -1"' || echo "unknown")
echo "Installed OpenClaw version: $installed"
if [ -z "$installed" ] || [ "$installed" = "unknown" ]; then
echo "✗ Could not determine installed version after switching to latest"
exit 1
fi
echo "✓ version=latest installed: $installed"
- name: Simulate daily auto-update check via update-status hook
run: |
juju ssh openclaw/0 'sudo bash -c "echo 0 > /home/ubuntu/.openclaw/.last-update-check && chown ubuntu:ubuntu /home/ubuntu/.openclaw/.last-update-check"'
juju model-config update-status-hook-interval=10s
sleep 30
juju model-config update-status-hook-interval=5m
sleep 30
stamp=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/.last-update-check 2>/dev/null || echo "missing"' | tr -d '\r\n')
echo "Timestamp file contents after update-status: $stamp"
now=$(date +%s)
if [ "$stamp" = "missing" ] || [ "$stamp" = "0" ]; then
echo "✗ Timestamp file was not updated by update-status hook"
exit 1
fi
age=$(( now - stamp ))
if [ "$age" -gt 120 ]; then
echo "✗ Timestamp too old (${age}s) - check_and_update_openclaw may not have run"
exit 1
fi
echo "✓ Daily auto-update check ran (timestamp updated ${age}s ago)"
- name: Collect logs on failure
if: failure()
run: |
echo "=== Juju Status ==="
juju status --format=yaml
echo "=== Debug Log ==="
juju debug-log --replay --no-tail --limit 500
echo "=== OpenClaw version ==="
juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && openclaw --version"' || true
- name: Cleanup
if: always()
run: |
juju destroy-model test-version-config -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-node-version-config:
name: Test Node Version Config
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
with:
channel: latest/stable
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-node-version-config
- name: Deploy with node-version=22
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config install-method=npm \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key" \
--config node-version="22"
- name: Wait for deployment
run: |
juju-wait -v -m test-node-version-config -t 900
- name: Verify Node.js v22 is active
run: |
node_ver=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm version default"' | tr -d '\r\n')
echo "Active Node.js version: $node_ver"
if echo "$node_ver" | grep -q "^v22\."; then
echo "✓ Node.js v22 is the nvm default"
else
echo "✗ Expected v22.x, got: $node_ver"
exit 1
fi
symlink=$(juju ssh openclaw/0 'ls -l /usr/local/bin/openclaw')
echo "openclaw symlink: $symlink"
if echo "$symlink" | grep -q "v22\."; then
echo "✓ openclaw symlink points to Node.js v22"
else
echo "✗ openclaw symlink does not point to v22"
exit 1
fi
- name: Switch to node-version=24
run: |
juju config openclaw node-version=24
- name: Wait for node-version change to complete
run: |
juju-wait -v -m test-node-version-config -t 300
- name: Verify Node.js v24 is active after switch
run: |
node_ver=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && nvm version default"' | tr -d '\r\n')
echo "Active Node.js version: $node_ver"
if echo "$node_ver" | grep -q "^v24\."; then
echo "✓ Node.js v24 is the nvm default after node-version change"
else
echo "✗ Expected v24.x, got: $node_ver"
exit 1
fi
symlink=$(juju ssh openclaw/0 'ls -l /usr/local/bin/openclaw')
echo "openclaw symlink: $symlink"
if echo "$symlink" | grep -q "v24\."; then
echo "✓ openclaw symlink points to Node.js v24"
else
echo "✗ openclaw symlink does not point to v24"
exit 1
fi
- name: Collect logs on failure
if: failure()
run: |
echo "=== Juju Status ==="
juju status --format=yaml
echo "=== Debug Log ==="
juju debug-log --replay --no-tail --limit 500
echo "=== Node.js Versions ==="
juju ssh openclaw/0 'ls -la /home/ubuntu/.nvm/versions/node/' || true
echo "=== openclaw symlink ==="
juju ssh openclaw/0 'ls -l /usr/local/bin/openclaw' || true
- name: Cleanup
if: always()
run: |
juju destroy-model test-node-version-config -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-default-allowed-origins:
name: Test Default control-ui-allowed-origins
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-default-origins
- name: Deploy charm without control-ui-allowed-origins
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key"
- name: Wait for deployment
run: |
juju-wait -v -m test-default-origins -t 900
- name: Verify default allowedOrigins uses loopback gateway URL
run: |
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
port=$(juju config openclaw gateway-port)
origins=$(echo "$config" | jq -r '.gateway.controlUi.allowedOrigins[0]')
count=$(echo "$config" | jq '.gateway.controlUi.allowedOrigins | length')
echo "allowedOrigins: $origins (count: $count)"
if [ "$origins" = "http://127.0.0.1:${port}" ] && [ "$count" = "1" ]; then
echo "✓ Default allowedOrigins is loopback gateway URL (gateway-bind=loopback)"
else
echo "✗ Expected [\"http://127.0.0.1:${port}\"], got: $(echo "$config" | jq '.gateway.controlUi.allowedOrigins')"
exit 1
fi
- name: Switch to LAN bind and verify default allowedOrigins updates
run: |
juju config openclaw gateway-bind=lan
sleep 30
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
unit_ip=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units | to_entries[0].value["public-address"]')
port=$(juju config openclaw gateway-port)
origins=$(echo "$config" | jq -r '.gateway.controlUi.allowedOrigins[0]')
echo "allowedOrigins after gateway-bind=lan: $origins (unit IP: $unit_ip)"
if [ "$origins" = "http://${unit_ip}:${port}" ]; then
echo "✓ Default allowedOrigins uses unit private IP when gateway-bind=lan"
else
echo "✗ Expected http://${unit_ip}:${port}, got: $origins"
exit 1
fi
- name: Verify explicit control-ui-allowed-origins still overrides default
run: |
juju config openclaw control-ui-allowed-origins="http://192.168.99.1:18789,http://192.168.99.2:18789"
sleep 30
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
count=$(echo "$config" | jq '.gateway.controlUi.allowedOrigins | length')
first=$(echo "$config" | jq -r '.gateway.controlUi.allowedOrigins[0]')
second=$(echo "$config" | jq -r '.gateway.controlUi.allowedOrigins[1]')
echo "allowedOrigins after explicit config: count=$count first=$first second=$second"
if [ "$count" = "2" ] && [ "$first" = "http://192.168.99.1:18789" ] && [ "$second" = "http://192.168.99.2:18789" ]; then
echo "✓ Explicit control-ui-allowed-origins overrides default correctly"
else
echo "✗ Explicit override failed: $(echo "$config" | jq '.gateway.controlUi.allowedOrigins')"
exit 1
fi
- name: Clear explicit config and verify default is restored
run: |
juju config openclaw control-ui-allowed-origins=""
sleep 30
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
unit_ip=$(juju status openclaw --format=json | jq -r '.applications.openclaw.units | to_entries[0].value["public-address"]')
port=$(juju config openclaw gateway-port)
origins=$(echo "$config" | jq -r '.gateway.controlUi.allowedOrigins[0]')
count=$(echo "$config" | jq '.gateway.controlUi.allowedOrigins | length')
echo "allowedOrigins after clearing explicit config: $origins (count: $count)"
if [ "$origins" = "http://${unit_ip}:${port}" ] && [ "$count" = "1" ]; then
echo "✓ Default gateway URL restored after clearing explicit control-ui-allowed-origins"
else
echo "✗ Expected [\"http://${unit_ip}:${port}\"], got: $(echo "$config" | jq '.gateway.controlUi.allowedOrigins')"
exit 1
fi
- name: Collect logs on failure
if: failure()
run: |
echo "=== Juju Status ==="
juju status --format=yaml
echo "=== Debug Log ==="
juju debug-log --replay --no-tail --limit 500
echo "=== Unit Config ==="
juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json 2>/dev/null' || true
- name: Cleanup
if: always()
run: |
juju destroy-model test-default-origins -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-context-window:
name: Test AI Context Window Configuration
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
with:
channel: latest/stable
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-context-window
- name: Deploy with ai-context-window=16384
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config ai-provider="openai" \
--config ai-model="qwen3.5:2b" \
--config ai-api-key="test-key" \
--config ai-base-url="http://localhost:11434/v1" \
--config ai-context-window=16384
- name: Wait for deployment
run: |
juju-wait -v -m test-context-window -t 900
- name: Verify models.providers.openai structure with context window
run: |
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
# Verify baseUrl
base_url=$(echo "$config" | jq -r '.models.providers.openai.baseUrl')
if [ "$base_url" = "http://localhost:11434/v1" ]; then
echo "✓ models.providers.openai.baseUrl is correct"
else
echo "✗ baseUrl mismatch (got: $base_url)"
exit 1
fi
# Verify models array has exactly one entry
models_len=$(echo "$config" | jq '.models.providers.openai.models | length')
if [ "$models_len" = "1" ]; then
echo "✓ models array has 1 entry"
else
echo "✗ models array length mismatch (got: $models_len)"
echo "$config" | jq '.models'
exit 1
fi
# Verify model id (provider prefix must be stripped: openai/qwen3.5:2b → qwen3.5:2b)
model_id=$(echo "$config" | jq -r '.models.providers.openai.models[0].id')
if [ "$model_id" = "qwen3.5:2b" ]; then
echo "✓ model.id is correct (provider prefix stripped)"
else
echo "✗ model.id mismatch (got: $model_id)"
exit 1
fi
# Verify model name
model_name=$(echo "$config" | jq -r '.models.providers.openai.models[0].name')
if [ "$model_name" = "qwen3.5:2b" ]; then
echo "✓ model.name is correct"
else
echo "✗ model.name mismatch (got: $model_name)"
exit 1
fi
# Verify api field is openai-completions (required for OpenAI-compat inference)
model_api=$(echo "$config" | jq -r '.models.providers.openai.models[0].api')
if [ "$model_api" = "openai-completions" ]; then
echo "✓ model.api is 'openai-completions'"
else
echo "✗ model.api mismatch (got: $model_api)"
exit 1
fi
# Verify contextWindow value
ctx=$(echo "$config" | jq -r '.models.providers.openai.models[0].contextWindow')
if [ "$ctx" = "16384" ]; then
echo "✓ model.contextWindow is 16384"
else
echo "✗ model.contextWindow mismatch (got: $ctx)"
exit 1
fi
- name: Update ai-context-window to 32768
run: |
juju config openclaw ai-context-window=32768
juju-wait -v -m test-context-window -t 120
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
ctx=$(echo "$config" | jq -r '.models.providers.openai.models[0].contextWindow')
if [ "$ctx" = "32768" ]; then
echo "✓ contextWindow updated to 32768"
else
echo "✗ contextWindow update failed (got: $ctx)"
exit 1
fi
- name: Reset ai-context-window to default (verify models array is cleared)
run: |
juju config openclaw --reset ai-context-window
juju-wait -v -m test-context-window -t 120
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
# models array must be empty when ai-context-window is reset to default (0)
models_len=$(echo "$config" | jq '.models.providers.openai.models | length')
if [ "$models_len" = "0" ]; then
echo "✓ models array is empty when ai-context-window is reset to default"
else
echo "✗ models array should be empty (got length: $models_len)"
echo "$config" | jq '.models'
exit 1
fi
- name: Configure slot 0 with ai0-context-window=32768
run: |
juju config openclaw \
ai0-provider="openai" \
ai0-model="mistral:7b" \
ai0-api-key="test-key" \
ai0-base-url="http://localhost:11434/v1" \
ai0-context-window=32768
juju-wait -v -m test-context-window -t 120
- name: Verify slot 0 models.providers.openai structure with context window
run: |
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
# Verify baseUrl for slot 0 provider
base_url=$(echo "$config" | jq -r '.models.providers.openai.baseUrl')
if [ "$base_url" = "http://localhost:11434/v1" ]; then
echo "✓ models.providers.openai.baseUrl is correct for slot 0"
else
echo "✗ baseUrl mismatch (got: $base_url)"
exit 1
fi
# Verify models array has exactly one entry
models_len=$(echo "$config" | jq '.models.providers.openai.models | length')
if [ "$models_len" = "1" ]; then
echo "✓ models array has 1 entry"
else
echo "✗ models array length mismatch (got: $models_len)"
echo "$config" | jq '.models'
exit 1
fi
# Verify model id (provider prefix must be stripped: openai/mistral:7b → mistral:7b)
model_id=$(echo "$config" | jq -r '.models.providers.openai.models[0].id')
if [ "$model_id" = "mistral:7b" ]; then
echo "✓ slot 0 model.id is correct (provider prefix stripped)"
else
echo "✗ slot 0 model.id mismatch (got: $model_id)"
exit 1
fi
# Verify api field
model_api=$(echo "$config" | jq -r '.models.providers.openai.models[0].api')
if [ "$model_api" = "openai-completions" ]; then
echo "✓ slot 0 model.api is 'openai-completions'"
else
echo "✗ slot 0 model.api mismatch (got: $model_api)"
exit 1
fi
# Verify contextWindow value
ctx=$(echo "$config" | jq -r '.models.providers.openai.models[0].contextWindow')
if [ "$ctx" = "32768" ]; then
echo "✓ slot 0 model.contextWindow is 32768"
else
echo "✗ slot 0 model.contextWindow mismatch (got: $ctx)"
exit 1
fi
- name: Update ai0-context-window to 16384
run: |
juju config openclaw ai0-context-window=16384
juju-wait -v -m test-context-window -t 120
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
ctx=$(echo "$config" | jq -r '.models.providers.openai.models[0].contextWindow')
if [ "$ctx" = "16384" ]; then
echo "✓ slot 0 contextWindow updated to 16384"
else
echo "✗ slot 0 contextWindow update failed (got: $ctx)"
exit 1
fi
- name: Reset ai0-context-window to default (verify models array is cleared)
run: |
juju config openclaw --reset ai0-context-window
juju-wait -v -m test-context-window -t 120
config=$(juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json')
# models array must be empty when ai0-context-window is reset to default (0)
models_len=$(echo "$config" | jq '.models.providers.openai.models | length')
if [ "$models_len" = "0" ]; then
echo "✓ slot 0 models array is empty when ai0-context-window is reset to default"
else
echo "✗ slot 0 models array should be empty (got length: $models_len)"
echo "$config" | jq '.models'
exit 1
fi
- name: Collect logs on failure
if: failure()
run: |
echo "=== Juju Status ==="
juju status --format=yaml
echo "=== Debug Log ==="
juju debug-log --replay --no-tail --limit 500
echo "=== OpenClaw Config ==="
juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' || true
- name: Cleanup
if: always()
run: |
juju destroy-model test-context-window -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-ai-providers-order:
name: Test AI Providers Order
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-order
- name: Deploy with custom order
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key-primary" \
--config ai3-provider="openai" \
--config ai3-model="gpt-4o" \
--config ai3-api-key="test-key-3" \
--config ai2-provider="google" \
--config ai2-model="gemini-2.5-flash" \
--config ai2-api-key="test-key-2" \
--config ai-providers-order="3,2,"
- name: Wait for deployment
run: |
juju-wait -v -m test-order -t 900
- name: Verify ordered configuration
run: |
juju ssh openclaw/0 'cat /home/ubuntu/.openclaw/openclaw.json' > config.json
# Verify AI model primary (should be ai3-model)
PRIMARY_MODEL=$(jq -r ".agents.defaults.model.primary" config.json)
if [ "$PRIMARY_MODEL" = "openai/gpt-4o" ]; then
echo "✓ Primary model configuration verified (ai3-model is first)"
else
echo "✗ Primary model configuration mismatch (got: $PRIMARY_MODEL, expected: openai/gpt-4o)"
exit 1
fi
# Verify AI model fallbacks order
# Order should be: ai3 (primary), ai2, ai (empty slot)
FB0=$(jq -r ".agents.defaults.model.fallbacks[0]" config.json)
if [ "$FB0" = "google/gemini-2.5-flash" ]; then
echo "✓ First fallback verified (ai2-model)"
else
echo "✗ First fallback mismatch (got: $FB0, expected: google/gemini-2.5-flash)"
exit 1
fi
FB1=$(jq -r ".agents.defaults.model.fallbacks[1]" config.json)
if [ "$FB1" = "anthropic/claude-sonnet-3-5" ]; then
echo "✓ Second fallback verified (ai-model)"
else
echo "✗ Second fallback mismatch (got: $FB1, expected: anthropic/claude-sonnet-3-5)"
exit 1
fi
- name: Cleanup
if: always()
run: |
juju destroy-model test-order -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true
test-system-upgrade-action:
name: Test System Upgrade Action
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download charm artifact
uses: actions/download-artifact@v8
with:
name: openclaw-charm
- name: Set up LXD
uses: canonical/setup-lxd@main
with:
channel: latest/stable
- name: Install Juju
run: |
sudo snap install juju --channel=3/stable
sudo snap install juju-wait --classic
- name: Bootstrap Juju
run: |
lxc network set lxdbr0 ipv6.address none
juju bootstrap localhost test-controller
- name: Create test model
run: |
juju add-model test-system-upgrade
- name: Deploy with specific version
run: |
CHARM_FILE=$(ls openclaw_*.charm | head -1)
juju deploy ./$CHARM_FILE \
--config install-method=npm \
--config ai-provider="anthropic" \
--config ai-model="claude-sonnet-3-5" \
--config ai-api-key="test-key" \
--config version="2026.3.8"
- name: Wait for deployment
run: |
juju-wait -v -m test-system-upgrade -t 900
- name: Verify initial version
run: |
installed=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && openclaw --version 2>&1 | tail -1"' || echo "unknown")
version_num=$(echo "$installed" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+\(-[a-zA-Z0-9.-]\+\)\?' | head -1)
if [ "$version_num" = "2026.3.8" ]; then
echo "✓ Initial version 2026.3.8 installed correctly"
else
echo "✗ Expected 2026.3.8, got: $installed (parsed as: $version_num)"
exit 1
fi
- name: Run upgrade action
run: |
juju config openclaw version=""
sleep 15
juju run openclaw/leader upgrade
# Wait for the system to reboot and Juju to reconnect
echo "Waiting for reboot and service recovery..."
sleep 60
juju-wait -v -m test-system-upgrade -t 300
- name: Verify upgraded version
run: |
# Give the node some time to finish its restart
sleep 30
installed=$(juju ssh openclaw/0 'sudo -u ubuntu bash -l -c ". ~/.nvm/nvm.sh && openclaw --version 2>&1 | tail -1"' || echo "unknown")
echo "Installed version after upgrade action: $installed"
# Extract version number
version_num=$(echo "$installed" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+\(-[a-zA-Z0-9.-]\+\)\?' | head -1)
echo "Parsed version: $version_num"
# Compare versions
if dpkg --compare-versions "$version_num" "gt" "2026.3.8"; then
echo "✓ Upgraded successfully to $version_num (greater than 2026.3.8)"
else
echo "✗ Version $version_num is not greater than 2026.3.8"
exit 1
fi
- name: Collect logs on failure
if: failure()
run: |
echo "=== Juju Status ==="
juju status --format=yaml
echo "=== Debug Log ==="
juju debug-log --replay --no-tail --limit 500
- name: Cleanup
if: always()
run: |
juju destroy-model test-system-upgrade -y --force --no-wait || true
juju destroy-controller test-controller -y --destroy-all-models --force --no-wait || true