feat: add system upgrade action #129
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 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 |