mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
39 Commits
v2026.5.3
...
fix/parall
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed6f489b72 | ||
|
|
634c2fcb78 | ||
|
|
574b748372 | ||
|
|
d286eadda3 | ||
|
|
de8a55b1c4 | ||
|
|
6b9dfafdc5 | ||
|
|
26737271ac | ||
|
|
bab6ce5fae | ||
|
|
71d8d6aa49 | ||
|
|
56ec00a69f | ||
|
|
6ac79f5009 | ||
|
|
e9412d8e02 | ||
|
|
07f6b17ed4 | ||
|
|
7c43ee3f94 | ||
|
|
d2a1078007 | ||
|
|
27e8c8866f | ||
|
|
bb1f876eb9 | ||
|
|
3a63182dd7 | ||
|
|
a76b6987fb | ||
|
|
bc724a86cb | ||
|
|
0382544c64 | ||
|
|
bb90c72f1b | ||
|
|
7d8592eacf | ||
|
|
5ccd1facf7 | ||
|
|
65b7399f4a | ||
|
|
6ce4b02e22 | ||
|
|
d1618353ac | ||
|
|
ea6bfc76b7 | ||
|
|
6ea8ea7477 | ||
|
|
1ff168b2f0 | ||
|
|
a24f9b6c7d | ||
|
|
19d369c750 | ||
|
|
074ce1d8a7 | ||
|
|
38081f8f90 | ||
|
|
a59c5f41ad | ||
|
|
559556f437 | ||
|
|
e9fec40af7 | ||
|
|
d35a10e6e0 | ||
|
|
acbb6bff44 |
177
scripts/e2e/lib/parallels-discord-common.sh
Normal file
177
scripts/e2e/lib/parallels-discord-common.sh
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
discord_python_bin() {
|
||||
if [[ -n "${PYTHON_BIN:-}" ]]; then
|
||||
printf '%s\n' "$PYTHON_BIN"
|
||||
return
|
||||
fi
|
||||
command -v python3
|
||||
}
|
||||
|
||||
discord_smoke_enabled() {
|
||||
[[ -n "${DISCORD_TOKEN_VALUE:-}" && -n "${DISCORD_GUILD_ID:-}" && -n "${DISCORD_CHANNEL_ID:-}" ]]
|
||||
}
|
||||
|
||||
discord_api_request() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
local payload="${3:-}"
|
||||
local url="https://discord.com/api/v10$path"
|
||||
if [[ -n "$payload" ]]; then
|
||||
curl -fsS -X "$method" \
|
||||
-H "Authorization: Bot $DISCORD_TOKEN_VALUE" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "$payload" \
|
||||
"$url"
|
||||
return
|
||||
fi
|
||||
curl -fsS -X "$method" \
|
||||
-H "Authorization: Bot $DISCORD_TOKEN_VALUE" \
|
||||
"$url"
|
||||
}
|
||||
|
||||
json_contains_string() {
|
||||
local needle="$1"
|
||||
"$(discord_python_bin)" - "$needle" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
needle = sys.argv[1]
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
|
||||
def contains(value):
|
||||
if isinstance(value, str):
|
||||
return needle in value
|
||||
if isinstance(value, list):
|
||||
return any(contains(item) for item in value)
|
||||
if isinstance(value, dict):
|
||||
return any(contains(item) for item in value.values())
|
||||
return False
|
||||
|
||||
raise SystemExit(0 if contains(payload) else 1)
|
||||
PY
|
||||
}
|
||||
|
||||
build_discord_guilds_json() {
|
||||
DISCORD_GUILD_ID="$DISCORD_GUILD_ID" DISCORD_CHANNEL_ID="$DISCORD_CHANNEL_ID" "$(discord_python_bin)" - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
os.environ["DISCORD_GUILD_ID"]: {
|
||||
"channels": {
|
||||
os.environ["DISCORD_CHANNEL_ID"]: {
|
||||
"allow": True,
|
||||
"requireMention": False,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
PY
|
||||
}
|
||||
|
||||
discord_message_id_from_send_log() {
|
||||
local path="$1"
|
||||
"$(discord_python_bin)" - "$path" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||
message_id = payload.get("payload", {}).get("messageId")
|
||||
if not message_id:
|
||||
message_id = payload.get("payload", {}).get("result", {}).get("messageId")
|
||||
if not message_id:
|
||||
raise SystemExit("messageId missing from send output")
|
||||
print(message_id)
|
||||
PY
|
||||
}
|
||||
|
||||
wait_for_discord_host_visibility() {
|
||||
local nonce="$1"
|
||||
local timeout_s="${2:-${TIMEOUT_DISCORD_S:-180}}"
|
||||
local response
|
||||
local deadline=$((SECONDS + timeout_s))
|
||||
while (( SECONDS < deadline )); do
|
||||
set +e
|
||||
response="$(discord_api_request GET "/channels/$DISCORD_CHANNEL_ID/messages?limit=20")"
|
||||
local rc=$?
|
||||
set -e
|
||||
if [[ $rc -eq 0 ]] && [[ -n "$response" ]] && printf '%s' "$response" | json_contains_string "$nonce"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
post_host_discord_message() {
|
||||
local nonce="$1"
|
||||
local prefix_or_id_file="$2"
|
||||
local maybe_id_file="${3:-}"
|
||||
local prefix id_file payload response
|
||||
if [[ -n "$maybe_id_file" ]]; then
|
||||
prefix="$prefix_or_id_file"
|
||||
id_file="$maybe_id_file"
|
||||
else
|
||||
prefix="parallels-smoke"
|
||||
id_file="$prefix_or_id_file"
|
||||
fi
|
||||
payload="$(
|
||||
NONCE="$nonce" PREFIX="$prefix" "$(discord_python_bin)" - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"content": f"{os.environ['PREFIX']}-inbound-{os.environ['NONCE']}",
|
||||
"flags": 4096,
|
||||
}
|
||||
)
|
||||
)
|
||||
PY
|
||||
)"
|
||||
response="$(discord_api_request POST "/channels/$DISCORD_CHANNEL_ID/messages" "$payload")"
|
||||
printf '%s' "$response" | "$(discord_python_bin)" - "$id_file" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
payload = json.load(sys.stdin)
|
||||
message_id = payload.get("id")
|
||||
if not isinstance(message_id, str) or not message_id:
|
||||
raise SystemExit("host Discord post missing message id")
|
||||
pathlib.Path(sys.argv[1]).write_text(f"{message_id}\n", encoding="utf-8")
|
||||
PY
|
||||
}
|
||||
|
||||
discord_delete_message_id_file() {
|
||||
local path="$1"
|
||||
[[ -f "$path" ]] || return 0
|
||||
[[ -s "$path" ]] || return 0
|
||||
discord_smoke_enabled || return 0
|
||||
|
||||
local message_id
|
||||
message_id="$(tr -d '\r\n' <"$path")"
|
||||
[[ -n "$message_id" ]] || return 0
|
||||
|
||||
set +e
|
||||
discord_api_request DELETE "/channels/$DISCORD_CHANNEL_ID/messages/$message_id" >/dev/null
|
||||
set -e
|
||||
}
|
||||
|
||||
cleanup_discord_message_files() {
|
||||
local path
|
||||
discord_smoke_enabled || return 0
|
||||
for path in "$@"; do
|
||||
discord_delete_message_id_file "$path"
|
||||
done
|
||||
}
|
||||
49
scripts/e2e/lib/parallels-permissions-common.sh
Normal file
49
scripts/e2e/lib/parallels-permissions-common.sh
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
parallels_macos_permission_check_snippet() {
|
||||
cat <<'EOF'
|
||||
root="$("/opt/homebrew/bin/npm" root -g)"
|
||||
check_path() {
|
||||
local path="$1"
|
||||
[ -e "$path" ] || return 0
|
||||
local perm perm_oct
|
||||
perm="$(/usr/bin/stat -f '%OLp' "$path")"
|
||||
perm_oct=$((8#$perm))
|
||||
if (( perm_oct & 0002 )); then
|
||||
echo "world-writable install artifact: $path ($perm)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
check_path "$root/openclaw"
|
||||
check_path "$root/openclaw/extensions"
|
||||
if [ -d "$root/openclaw/extensions" ]; then
|
||||
while IFS= read -r -d '' extension_dir; do
|
||||
check_path "$extension_dir"
|
||||
done < <(/usr/bin/find "$root/openclaw/extensions" -mindepth 1 -maxdepth 1 -type d -print0)
|
||||
fi
|
||||
EOF
|
||||
}
|
||||
|
||||
parallels_linux_permission_check_snippet() {
|
||||
cat <<'EOF'
|
||||
root="$(npm root -g)"
|
||||
check_path() {
|
||||
local path="$1"
|
||||
[ -e "$path" ] || return 0
|
||||
local perm perm_oct
|
||||
perm="$(stat -c '%a' "$path")"
|
||||
perm_oct=$((8#$perm))
|
||||
if (( perm_oct & 0002 )); then
|
||||
echo "world-writable install artifact: $path ($perm)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
check_path "$root/openclaw"
|
||||
check_path "$root/openclaw/extensions"
|
||||
if [ -d "$root/openclaw/extensions" ]; then
|
||||
while IFS= read -r -d '' extension_dir; do
|
||||
check_path "$extension_dir"
|
||||
done < <(find "$root/openclaw/extensions" -mindepth 1 -maxdepth 1 -type d -print0)
|
||||
fi
|
||||
EOF
|
||||
}
|
||||
215
scripts/e2e/lib/parallels-summary-common.sh
Normal file
215
scripts/e2e/lib/parallels-summary-common.sh
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
parallels_seed_fresh_child_summary() {
|
||||
local prefix="$1"
|
||||
local discord_status="skip"
|
||||
if discord_smoke_enabled; then
|
||||
discord_status="fail"
|
||||
fi
|
||||
eval "${prefix}_GATEWAY_STATUS='fail'"
|
||||
eval "${prefix}_PERMISSION_STATUS='fail'"
|
||||
eval "${prefix}_CHANNELS_STATUS='fail'"
|
||||
eval "${prefix}_DASHBOARD_STATUS='fail'"
|
||||
eval "${prefix}_AGENT_STATUS='fail'"
|
||||
eval "${prefix}_DISCORD_STATUS='${discord_status}'"
|
||||
}
|
||||
|
||||
parallels_load_fresh_child_summary() {
|
||||
local prefix="$1"
|
||||
local log_path="$2"
|
||||
[[ -f "$log_path" ]] || return 0
|
||||
|
||||
local assignments
|
||||
set +e
|
||||
assignments="$(
|
||||
PREFIX="$prefix" "$PYTHON_BIN" - "$log_path" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
prefix = os.environ["PREFIX"]
|
||||
text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").strip()
|
||||
if not text:
|
||||
raise SystemExit(0)
|
||||
|
||||
try:
|
||||
payload = json.loads(text)
|
||||
except Exception:
|
||||
decoder = json.JSONDecoder()
|
||||
payload = None
|
||||
for index, char in enumerate(text):
|
||||
if char != "{":
|
||||
continue
|
||||
try:
|
||||
candidate, _ = decoder.raw_decode(text[index:])
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(candidate, dict) and isinstance(candidate.get("freshMain"), dict):
|
||||
payload = candidate
|
||||
if payload is None:
|
||||
raise SystemExit(0)
|
||||
|
||||
fresh = payload.get("freshMain")
|
||||
if not isinstance(fresh, dict):
|
||||
raise SystemExit(0)
|
||||
|
||||
field_map = {
|
||||
"STATUS": "status",
|
||||
"VERSION": "version",
|
||||
"GATEWAY_STATUS": "gateway",
|
||||
"PERMISSION_STATUS": "permissions",
|
||||
"CHANNELS_STATUS": "channels",
|
||||
"DASHBOARD_STATUS": "dashboard",
|
||||
"AGENT_STATUS": "agent",
|
||||
"DISCORD_STATUS": "discord",
|
||||
}
|
||||
|
||||
for key, source_key in field_map.items():
|
||||
value = fresh.get(source_key)
|
||||
if isinstance(value, str):
|
||||
print(f"{prefix}_{key}={shlex.quote(value)}")
|
||||
PY
|
||||
)"
|
||||
local rc=$?
|
||||
set -e
|
||||
if [[ $rc -eq 0 && -n "$assignments" ]]; then
|
||||
eval "$assignments"
|
||||
fi
|
||||
}
|
||||
|
||||
parallels_backfill_fresh_child_summary_from_log() {
|
||||
local prefix="$1"
|
||||
local log_path="$2"
|
||||
[[ -f "$log_path" ]] || return 0
|
||||
|
||||
local assignments
|
||||
set +e
|
||||
assignments="$(
|
||||
PREFIX="$prefix" "$PYTHON_BIN" - "$log_path" <<'PY'
|
||||
import os
|
||||
import pathlib
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
prefix = os.environ["PREFIX"]
|
||||
text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace")
|
||||
field_map = {
|
||||
"GATEWAY_STATUS": "fresh.gateway.ok",
|
||||
"PERMISSION_STATUS": "fresh.permissions.ok",
|
||||
"CHANNELS_STATUS": "fresh.channels.ok",
|
||||
"DASHBOARD_STATUS": "fresh.dashboard.ok",
|
||||
"AGENT_STATUS": "fresh.agent.ok",
|
||||
"DISCORD_STATUS": "fresh.discord.ok",
|
||||
}
|
||||
|
||||
for key, marker in field_map.items():
|
||||
if f"==> {marker}" in text:
|
||||
print(f"{prefix}_{key}={shlex.quote('pass')}")
|
||||
PY
|
||||
)"
|
||||
local rc=$?
|
||||
set -e
|
||||
if [[ $rc -eq 0 && -n "$assignments" ]]; then
|
||||
eval "$assignments"
|
||||
fi
|
||||
}
|
||||
|
||||
parallels_update_status_path() {
|
||||
local run_dir="$1"
|
||||
local os_name="$2"
|
||||
printf '%s/%s-update-status.json\n' "$run_dir" "$os_name"
|
||||
}
|
||||
|
||||
parallels_write_update_status_summary() {
|
||||
local run_dir="$1"
|
||||
local os_name="$2"
|
||||
local gateway_status="$3"
|
||||
local permission_status="$4"
|
||||
local channels_status="$5"
|
||||
local dashboard_status="$6"
|
||||
local agent_status="$7"
|
||||
local discord_status="$8"
|
||||
local status_path
|
||||
status_path="$(parallels_update_status_path "$run_dir" "$os_name")"
|
||||
UPDATE_STATUS_PATH="$status_path" \
|
||||
UPDATE_GATEWAY_STATUS="$gateway_status" \
|
||||
UPDATE_PERMISSION_STATUS="$permission_status" \
|
||||
UPDATE_CHANNELS_STATUS="$channels_status" \
|
||||
UPDATE_DASHBOARD_STATUS="$dashboard_status" \
|
||||
UPDATE_AGENT_STATUS="$agent_status" \
|
||||
UPDATE_DISCORD_STATUS="$discord_status" \
|
||||
"$PYTHON_BIN" - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
payload = {
|
||||
"gateway": os.environ["UPDATE_GATEWAY_STATUS"],
|
||||
"permissions": os.environ["UPDATE_PERMISSION_STATUS"],
|
||||
"channels": os.environ["UPDATE_CHANNELS_STATUS"],
|
||||
"dashboard": os.environ["UPDATE_DASHBOARD_STATUS"],
|
||||
"agent": os.environ["UPDATE_AGENT_STATUS"],
|
||||
"discord": os.environ["UPDATE_DISCORD_STATUS"],
|
||||
}
|
||||
with open(os.environ["UPDATE_STATUS_PATH"], "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, indent=2, sort_keys=True)
|
||||
PY
|
||||
}
|
||||
|
||||
parallels_seed_update_status_summary() {
|
||||
local run_dir="$1"
|
||||
local os_name="$2"
|
||||
local discord_status="skip"
|
||||
if discord_smoke_enabled; then
|
||||
discord_status="fail"
|
||||
fi
|
||||
parallels_write_update_status_summary "$run_dir" "$os_name" "fail" "fail" "fail" "fail" "fail" "$discord_status"
|
||||
}
|
||||
|
||||
parallels_load_update_status_summary() {
|
||||
local prefix="$1"
|
||||
local status_path="$2"
|
||||
[[ -f "$status_path" ]] || return 0
|
||||
|
||||
local assignments
|
||||
set +e
|
||||
assignments="$(
|
||||
PREFIX="$prefix" "$PYTHON_BIN" - "$status_path" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
prefix = os.environ["PREFIX"]
|
||||
text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").strip()
|
||||
if not text:
|
||||
raise SystemExit(0)
|
||||
|
||||
try:
|
||||
payload = json.loads(text)
|
||||
except Exception:
|
||||
raise SystemExit(0)
|
||||
|
||||
field_map = {
|
||||
"GATEWAY_STATUS": "gateway",
|
||||
"PERMISSION_STATUS": "permissions",
|
||||
"CHANNELS_STATUS": "channels",
|
||||
"DASHBOARD_STATUS": "dashboard",
|
||||
"AGENT_STATUS": "agent",
|
||||
"DISCORD_STATUS": "discord",
|
||||
}
|
||||
|
||||
for key, source_key in field_map.items():
|
||||
value = payload.get(source_key)
|
||||
if isinstance(value, str):
|
||||
print(f"{prefix}_{key}={shlex.quote(value)}")
|
||||
PY
|
||||
)"
|
||||
local rc=$?
|
||||
set -e
|
||||
if [[ $rc -eq 0 && -n "$assignments" ]]; then
|
||||
eval "$assignments"
|
||||
fi
|
||||
}
|
||||
59
scripts/e2e/lib/parallels-windows-common.sh
Normal file
59
scripts/e2e/lib/parallels-windows-common.sh
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
parallels_windows_permission_helpers_ps() {
|
||||
cat <<'EOF'
|
||||
function Assert-NonBroadWritableInstall {
|
||||
$broadSidValues = @(
|
||||
([Security.Principal.SecurityIdentifier]::new([Security.Principal.WellKnownSidType]::WorldSid, $null)).Value,
|
||||
([Security.Principal.SecurityIdentifier]::new([Security.Principal.WellKnownSidType]::BuiltinUsersSid, $null)).Value,
|
||||
([Security.Principal.SecurityIdentifier]::new([Security.Principal.WellKnownSidType]::AuthenticatedUserSid, $null)).Value
|
||||
)
|
||||
$writeMask =
|
||||
[Security.AccessControl.FileSystemRights]::Write -bor
|
||||
[Security.AccessControl.FileSystemRights]::Modify -bor
|
||||
[Security.AccessControl.FileSystemRights]::FullControl -bor
|
||||
[Security.AccessControl.FileSystemRights]::CreateFiles -bor
|
||||
[Security.AccessControl.FileSystemRights]::CreateDirectories -bor
|
||||
[Security.AccessControl.FileSystemRights]::AppendData -bor
|
||||
[Security.AccessControl.FileSystemRights]::WriteAttributes -bor
|
||||
[Security.AccessControl.FileSystemRights]::WriteExtendedAttributes -bor
|
||||
[Security.AccessControl.FileSystemRights]::Delete -bor
|
||||
[Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
|
||||
[Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
||||
[Security.AccessControl.FileSystemRights]::TakeOwnership
|
||||
|
||||
function Assert-NonBroadWritablePath {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
return
|
||||
}
|
||||
|
||||
$acl = Get-Acl -LiteralPath $Path
|
||||
foreach ($rule in $acl.Access) {
|
||||
if ($rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
$sid = $rule.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if ($broadSidValues -notcontains $sid) {
|
||||
continue
|
||||
}
|
||||
if (($rule.FileSystemRights -band $writeMask) -ne 0) {
|
||||
throw "broad writable install artifact: $Path ($($rule.IdentityReference.Value): $($rule.FileSystemRights))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$root = (& npm.cmd root -g).Trim()
|
||||
Assert-NonBroadWritablePath -Path (Join-Path $root 'openclaw')
|
||||
Assert-NonBroadWritablePath -Path (Join-Path $root 'openclaw\extensions')
|
||||
Get-ChildItem -LiteralPath (Join-Path $root 'openclaw\extensions') -Directory -ErrorAction SilentlyContinue |
|
||||
ForEach-Object {
|
||||
Assert-NonBroadWritablePath -Path $_.FullName
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/parallels-discord-common.sh"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/parallels-permissions-common.sh"
|
||||
|
||||
VM_NAME="Ubuntu 24.04.3 ARM64"
|
||||
VM_NAME_EXPLICIT=0
|
||||
SNAPSHOT_HINT="fresh"
|
||||
@@ -19,6 +23,11 @@ INSTALL_VERSION=""
|
||||
TARGET_PACKAGE_SPEC=""
|
||||
JSON_OUTPUT=0
|
||||
KEEP_SERVER=0
|
||||
CHECK_LATEST_REF=1
|
||||
DISCORD_TOKEN_ENV=""
|
||||
DISCORD_TOKEN_VALUE=""
|
||||
DISCORD_GUILD_ID=""
|
||||
DISCORD_CHANNEL_ID=""
|
||||
SNAPSHOT_ID=""
|
||||
SNAPSHOT_STATE=""
|
||||
SNAPSHOT_NAME=""
|
||||
@@ -34,19 +43,31 @@ TIMEOUT_SNAPSHOT_S=180
|
||||
TIMEOUT_BOOTSTRAP_S=600
|
||||
TIMEOUT_INSTALL_S=1200
|
||||
TIMEOUT_VERIFY_S=90
|
||||
TIMEOUT_PERMISSION_S=60
|
||||
TIMEOUT_ONBOARD_S=180
|
||||
TIMEOUT_AGENT_S=180
|
||||
TIMEOUT_GATEWAY_S=90
|
||||
TIMEOUT_DASHBOARD_S=60
|
||||
TIMEOUT_DISCORD_S=180
|
||||
|
||||
FRESH_MAIN_STATUS="skip"
|
||||
FRESH_MAIN_VERSION="skip"
|
||||
FRESH_GATEWAY_STATUS="skip"
|
||||
FRESH_PERMISSION_STATUS="skip"
|
||||
FRESH_CHANNELS_STATUS="skip"
|
||||
FRESH_AGENT_STATUS="skip"
|
||||
FRESH_DASHBOARD_STATUS="skip"
|
||||
FRESH_DISCORD_STATUS="skip"
|
||||
UPGRADE_STATUS="skip"
|
||||
UPGRADE_PRECHECK_STATUS="skip"
|
||||
LATEST_INSTALLED_VERSION="skip"
|
||||
UPGRADE_MAIN_VERSION="skip"
|
||||
UPGRADE_GATEWAY_STATUS="skip"
|
||||
UPGRADE_PERMISSION_STATUS="skip"
|
||||
UPGRADE_CHANNELS_STATUS="skip"
|
||||
UPGRADE_AGENT_STATUS="skip"
|
||||
UPGRADE_DASHBOARD_STATUS="skip"
|
||||
UPGRADE_DISCORD_STATUS="skip"
|
||||
DAEMON_STATUS="systemd-user-unavailable"
|
||||
|
||||
say() {
|
||||
@@ -75,6 +96,9 @@ die() {
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if command -v cleanup_discord_smoke_messages >/dev/null 2>&1; then
|
||||
cleanup_discord_smoke_messages
|
||||
fi
|
||||
if [[ -n "${SERVER_PID:-}" ]]; then
|
||||
kill "$SERVER_PID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
@@ -110,6 +134,10 @@ Options:
|
||||
--target-package-spec <npm-spec>
|
||||
Install this npm package tarball instead of packing current main.
|
||||
Example: openclaw@2026.3.13-beta.1
|
||||
--skip-latest-ref-check Skip latest-release ref-mode precheck.
|
||||
--discord-token-env <var> Host env var name for Discord bot token.
|
||||
--discord-guild-id <id> Discord guild ID for smoke roundtrip.
|
||||
--discord-channel-id <id> Discord channel ID for smoke roundtrip.
|
||||
--keep-server Leave temp host HTTP server running.
|
||||
--json Print machine-readable JSON summary.
|
||||
-h, --help Show help.
|
||||
@@ -167,6 +195,22 @@ while [[ $# -gt 0 ]]; do
|
||||
TARGET_PACKAGE_SPEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-latest-ref-check)
|
||||
CHECK_LATEST_REF=0
|
||||
shift
|
||||
;;
|
||||
--discord-token-env)
|
||||
DISCORD_TOKEN_ENV="$2"
|
||||
shift 2
|
||||
;;
|
||||
--discord-guild-id)
|
||||
DISCORD_GUILD_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--discord-channel-id)
|
||||
DISCORD_CHANNEL_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--keep-server)
|
||||
KEEP_SERVER=1
|
||||
shift
|
||||
@@ -219,6 +263,24 @@ esac
|
||||
API_KEY_VALUE="${!API_KEY_ENV:-}"
|
||||
[[ -n "$API_KEY_VALUE" ]] || die "$API_KEY_ENV is required"
|
||||
|
||||
if [[ -n "$DISCORD_TOKEN_ENV" || -n "$DISCORD_GUILD_ID" || -n "$DISCORD_CHANNEL_ID" ]]; then
|
||||
[[ -n "$DISCORD_TOKEN_ENV" ]] || die "--discord-token-env is required when Discord smoke args are set"
|
||||
[[ -n "$DISCORD_GUILD_ID" ]] || die "--discord-guild-id is required when Discord smoke args are set"
|
||||
[[ -n "$DISCORD_CHANNEL_ID" ]] || die "--discord-channel-id is required when Discord smoke args are set"
|
||||
DISCORD_TOKEN_VALUE="${!DISCORD_TOKEN_ENV:-}"
|
||||
[[ -n "$DISCORD_TOKEN_VALUE" ]] || die "$DISCORD_TOKEN_ENV is required for Discord smoke"
|
||||
fi
|
||||
|
||||
cleanup_discord_smoke_messages() {
|
||||
discord_smoke_enabled || return 0
|
||||
[[ -d "$RUN_DIR" ]] || return 0
|
||||
cleanup_discord_message_files \
|
||||
"$RUN_DIR/fresh.discord-sent-message-id" \
|
||||
"$RUN_DIR/fresh.discord-host-message-id" \
|
||||
"$RUN_DIR/upgrade.discord-sent-message-id" \
|
||||
"$RUN_DIR/upgrade.discord-host-message-id"
|
||||
}
|
||||
|
||||
resolve_vm_name() {
|
||||
local json requested explicit
|
||||
json="$(prlctl list --all --json)"
|
||||
@@ -596,6 +658,18 @@ install_main_tgz() {
|
||||
guest_exec openclaw --version
|
||||
}
|
||||
|
||||
run_main_package_update() {
|
||||
local host_ip="$1"
|
||||
local tgz_url="http://$host_ip:$HOST_PORT/$(basename "$MAIN_TGZ_PATH")"
|
||||
local status_json
|
||||
guest_exec openclaw update --tag "$tgz_url" --yes --json
|
||||
status_json="$(guest_exec openclaw update status --json)"
|
||||
printf '%s\n' "$status_json"
|
||||
printf '%s\n' "$status_json" | grep -F '"installKind": "package"'
|
||||
printf '%s\n' "$status_json" | grep -F '"packageManager": "npm"'
|
||||
guest_exec openclaw --version
|
||||
}
|
||||
|
||||
verify_version_contains() {
|
||||
local needle="$1"
|
||||
local version
|
||||
@@ -667,6 +741,127 @@ verify_local_turn() {
|
||||
--json
|
||||
}
|
||||
|
||||
verify_bundle_permissions() {
|
||||
local cmd
|
||||
cmd="$(cat <<EOF
|
||||
set -eu
|
||||
set -o pipefail
|
||||
$(parallels_linux_permission_check_snippet)
|
||||
EOF
|
||||
)"
|
||||
guest_exec bash -lc "$cmd"
|
||||
}
|
||||
|
||||
verify_dashboard_load() {
|
||||
local cmd
|
||||
cmd="$(cat <<'EOF'
|
||||
set -eu
|
||||
deadline=$((SECONDS + 30))
|
||||
dashboard_ready=0
|
||||
while [ $SECONDS -lt $deadline ]; do
|
||||
if curl -fsSL --connect-timeout 2 --max-time 5 http://127.0.0.1:18789/ >/tmp/openclaw-dashboard-smoke.html 2>/dev/null; then
|
||||
if grep -F '<title>OpenClaw Control</title>' /tmp/openclaw-dashboard-smoke.html >/dev/null; then
|
||||
if grep -F '<openclaw-app></openclaw-app>' /tmp/openclaw-dashboard-smoke.html >/dev/null; then
|
||||
dashboard_ready=1
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
[ "$dashboard_ready" = "1" ] || {
|
||||
echo "dashboard HTML did not become ready at http://127.0.0.1:18789/" >&2
|
||||
exit 1
|
||||
}
|
||||
EOF
|
||||
)"
|
||||
guest_exec bash -lc "$cmd"
|
||||
}
|
||||
|
||||
verify_channels_probe() {
|
||||
guest_exec openclaw channels status --probe --json
|
||||
}
|
||||
|
||||
configure_discord_smoke() {
|
||||
local guilds_json cmd
|
||||
guilds_json="$(build_discord_guilds_json)"
|
||||
cmd="$(cat <<EOF
|
||||
cat >/tmp/openclaw-discord-token <<'__OPENCLAW_TOKEN__'
|
||||
$DISCORD_TOKEN_VALUE
|
||||
__OPENCLAW_TOKEN__
|
||||
cat >/tmp/openclaw-discord-guilds.json <<'__OPENCLAW_GUILDS__'
|
||||
$guilds_json
|
||||
__OPENCLAW_GUILDS__
|
||||
token="\$(tr -d '\n' </tmp/openclaw-discord-token)"
|
||||
guilds_json="\$(cat /tmp/openclaw-discord-guilds.json)"
|
||||
openclaw config set channels.discord.token "\$token"
|
||||
openclaw config set channels.discord.enabled true
|
||||
openclaw config set channels.discord.groupPolicy allowlist
|
||||
openclaw config set channels.discord.guilds "\$guilds_json" --strict-json
|
||||
rm -f /tmp/openclaw-discord-token /tmp/openclaw-discord-guilds.json
|
||||
EOF
|
||||
)"
|
||||
guest_exec bash -lc "$cmd"
|
||||
start_gateway_background
|
||||
show_gateway_status_compat
|
||||
guest_exec openclaw channels status --probe --json
|
||||
}
|
||||
|
||||
wait_for_guest_discord_readback() {
|
||||
local nonce="$1"
|
||||
local response rc
|
||||
local last_response_path="$RUN_DIR/discord-last-readback.json"
|
||||
local deadline=$((SECONDS + TIMEOUT_DISCORD_S))
|
||||
while (( SECONDS < deadline )); do
|
||||
set +e
|
||||
response="$(
|
||||
guest_exec \
|
||||
openclaw \
|
||||
message read \
|
||||
--channel discord \
|
||||
--target "channel:$DISCORD_CHANNEL_ID" \
|
||||
--limit 20 \
|
||||
--json
|
||||
)"
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ -n "$response" ]]; then
|
||||
printf '%s' "$response" >"$last_response_path"
|
||||
fi
|
||||
if [[ $rc -eq 0 ]] && [[ -n "$response" ]] && printf '%s' "$response" | json_contains_string "$nonce"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
run_discord_roundtrip_smoke() {
|
||||
local phase="$1"
|
||||
local nonce outbound_nonce inbound_nonce outbound_message outbound_log sent_id_file host_id_file
|
||||
nonce="$(date +%s)-$RANDOM"
|
||||
outbound_nonce="$phase-out-$nonce"
|
||||
inbound_nonce="$phase-in-$nonce"
|
||||
outbound_message="parallels-linux-smoke-outbound-$outbound_nonce"
|
||||
outbound_log="$RUN_DIR/$phase.discord-send.json"
|
||||
sent_id_file="$RUN_DIR/$phase.discord-sent-message-id"
|
||||
host_id_file="$RUN_DIR/$phase.discord-host-message-id"
|
||||
|
||||
guest_exec \
|
||||
openclaw \
|
||||
message send \
|
||||
--channel discord \
|
||||
--target "channel:$DISCORD_CHANNEL_ID" \
|
||||
--message "$outbound_message" \
|
||||
--silent \
|
||||
--json >"$outbound_log"
|
||||
|
||||
discord_message_id_from_send_log "$outbound_log" >"$sent_id_file"
|
||||
wait_for_discord_host_visibility "$outbound_nonce"
|
||||
post_host_discord_message "$inbound_nonce" "$host_id_file"
|
||||
wait_for_guest_discord_readback "$inbound_nonce"
|
||||
}
|
||||
|
||||
phase_log_path() {
|
||||
printf '%s/%s.log\n' "$RUN_DIR" "$1"
|
||||
}
|
||||
@@ -761,14 +956,23 @@ summary = {
|
||||
"status": os.environ["SUMMARY_FRESH_MAIN_STATUS"],
|
||||
"version": os.environ["SUMMARY_FRESH_MAIN_VERSION"],
|
||||
"gateway": os.environ["SUMMARY_FRESH_GATEWAY_STATUS"],
|
||||
"permissions": os.environ["SUMMARY_FRESH_PERMISSION_STATUS"],
|
||||
"channels": os.environ["SUMMARY_FRESH_CHANNELS_STATUS"],
|
||||
"agent": os.environ["SUMMARY_FRESH_AGENT_STATUS"],
|
||||
"dashboard": os.environ["SUMMARY_FRESH_DASHBOARD_STATUS"],
|
||||
"discord": os.environ["SUMMARY_FRESH_DISCORD_STATUS"],
|
||||
},
|
||||
"upgrade": {
|
||||
"precheck": os.environ["SUMMARY_UPGRADE_PRECHECK_STATUS"],
|
||||
"status": os.environ["SUMMARY_UPGRADE_STATUS"],
|
||||
"latestVersionInstalled": os.environ["SUMMARY_LATEST_INSTALLED_VERSION"],
|
||||
"mainVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"],
|
||||
"gateway": os.environ["SUMMARY_UPGRADE_GATEWAY_STATUS"],
|
||||
"permissions": os.environ["SUMMARY_UPGRADE_PERMISSION_STATUS"],
|
||||
"channels": os.environ["SUMMARY_UPGRADE_CHANNELS_STATUS"],
|
||||
"agent": os.environ["SUMMARY_UPGRADE_AGENT_STATUS"],
|
||||
"dashboard": os.environ["SUMMARY_UPGRADE_DASHBOARD_STATUS"],
|
||||
"discord": os.environ["SUMMARY_UPGRADE_DISCORD_STATUS"],
|
||||
},
|
||||
}
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
||||
@@ -786,12 +990,51 @@ run_fresh_main_lane() {
|
||||
phase_run "fresh.install-main" "$TIMEOUT_INSTALL_S" install_main_tgz "$host_ip" "openclaw-main-fresh.tgz"
|
||||
FRESH_MAIN_VERSION="$(extract_last_version "$(phase_log_path fresh.install-main)")"
|
||||
phase_run "fresh.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
|
||||
FRESH_PERMISSION_STATUS="fail"
|
||||
phase_run "fresh.verify-bundle-permissions" "$TIMEOUT_PERMISSION_S" verify_bundle_permissions
|
||||
FRESH_PERMISSION_STATUS="pass"
|
||||
say "fresh.permissions.ok"
|
||||
phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
|
||||
phase_run "fresh.gateway-start" "$TIMEOUT_GATEWAY_S" start_gateway_background
|
||||
FRESH_GATEWAY_STATUS="fail"
|
||||
phase_run "fresh.gateway-status" "$TIMEOUT_VERIFY_S" show_gateway_status_compat
|
||||
FRESH_GATEWAY_STATUS="pass"
|
||||
say "fresh.gateway.ok"
|
||||
FRESH_CHANNELS_STATUS="fail"
|
||||
phase_run "fresh.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe
|
||||
FRESH_CHANNELS_STATUS="pass"
|
||||
say "fresh.channels.ok"
|
||||
FRESH_DASHBOARD_STATUS="fail"
|
||||
phase_run "fresh.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load
|
||||
FRESH_DASHBOARD_STATUS="pass"
|
||||
say "fresh.dashboard.ok"
|
||||
FRESH_AGENT_STATUS="fail"
|
||||
phase_run "fresh.first-local-agent-turn" "$TIMEOUT_AGENT_S" verify_local_turn
|
||||
FRESH_AGENT_STATUS="pass"
|
||||
say "fresh.agent.ok"
|
||||
if discord_smoke_enabled; then
|
||||
FRESH_DISCORD_STATUS="fail"
|
||||
phase_run "fresh.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke
|
||||
phase_run "fresh.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "fresh"
|
||||
FRESH_DISCORD_STATUS="pass"
|
||||
say "fresh.discord.ok"
|
||||
fi
|
||||
}
|
||||
|
||||
capture_latest_ref_failure() {
|
||||
set +e
|
||||
run_ref_onboard
|
||||
local rc=$?
|
||||
set -e
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
say "Latest release ref-mode onboard passed"
|
||||
return 0
|
||||
fi
|
||||
warn "Latest release ref-mode onboard failed pre-upgrade"
|
||||
set +e
|
||||
show_gateway_status_compat || true
|
||||
set -e
|
||||
return 1
|
||||
}
|
||||
|
||||
run_upgrade_lane() {
|
||||
@@ -802,15 +1045,47 @@ run_upgrade_lane() {
|
||||
phase_run "upgrade.install-latest" "$TIMEOUT_INSTALL_S" install_latest_release
|
||||
LATEST_INSTALLED_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-latest)")"
|
||||
phase_run "upgrade.verify-latest-version" "$TIMEOUT_VERIFY_S" verify_version_contains "$LATEST_VERSION"
|
||||
phase_run "upgrade.install-main" "$TIMEOUT_INSTALL_S" install_main_tgz "$host_ip" "openclaw-main-upgrade.tgz"
|
||||
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-main)")"
|
||||
if [[ "$CHECK_LATEST_REF" -eq 1 ]]; then
|
||||
if phase_run "upgrade.latest-ref-precheck" "$TIMEOUT_ONBOARD_S" capture_latest_ref_failure; then
|
||||
UPGRADE_PRECHECK_STATUS="latest-ref-pass"
|
||||
else
|
||||
UPGRADE_PRECHECK_STATUS="latest-ref-fail"
|
||||
fi
|
||||
else
|
||||
UPGRADE_PRECHECK_STATUS="skipped"
|
||||
fi
|
||||
phase_run "upgrade.update-main" "$TIMEOUT_INSTALL_S" run_main_package_update "$host_ip"
|
||||
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.update-main)")"
|
||||
phase_run "upgrade.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
|
||||
UPGRADE_PERMISSION_STATUS="fail"
|
||||
phase_run "upgrade.verify-bundle-permissions" "$TIMEOUT_PERMISSION_S" verify_bundle_permissions
|
||||
UPGRADE_PERMISSION_STATUS="pass"
|
||||
say "upgrade.permissions.ok"
|
||||
phase_run "upgrade.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
|
||||
phase_run "upgrade.gateway-start" "$TIMEOUT_GATEWAY_S" start_gateway_background
|
||||
UPGRADE_GATEWAY_STATUS="fail"
|
||||
phase_run "upgrade.gateway-status" "$TIMEOUT_VERIFY_S" show_gateway_status_compat
|
||||
UPGRADE_GATEWAY_STATUS="pass"
|
||||
say "upgrade.gateway.ok"
|
||||
UPGRADE_CHANNELS_STATUS="fail"
|
||||
phase_run "upgrade.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe
|
||||
UPGRADE_CHANNELS_STATUS="pass"
|
||||
say "upgrade.channels.ok"
|
||||
UPGRADE_DASHBOARD_STATUS="fail"
|
||||
phase_run "upgrade.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load
|
||||
UPGRADE_DASHBOARD_STATUS="pass"
|
||||
say "upgrade.dashboard.ok"
|
||||
UPGRADE_AGENT_STATUS="fail"
|
||||
phase_run "upgrade.first-local-agent-turn" "$TIMEOUT_AGENT_S" verify_local_turn
|
||||
UPGRADE_AGENT_STATUS="pass"
|
||||
say "upgrade.agent.ok"
|
||||
if discord_smoke_enabled; then
|
||||
UPGRADE_DISCORD_STATUS="fail"
|
||||
phase_run "upgrade.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke
|
||||
phase_run "upgrade.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "upgrade"
|
||||
UPGRADE_DISCORD_STATUS="pass"
|
||||
say "upgrade.discord.ok"
|
||||
fi
|
||||
}
|
||||
|
||||
RESOLVED_VM_NAME="$(resolve_vm_name)"
|
||||
@@ -880,12 +1155,21 @@ SUMMARY_JSON_PATH="$(
|
||||
SUMMARY_FRESH_MAIN_STATUS="$FRESH_MAIN_STATUS" \
|
||||
SUMMARY_FRESH_MAIN_VERSION="$FRESH_MAIN_VERSION" \
|
||||
SUMMARY_FRESH_GATEWAY_STATUS="$FRESH_GATEWAY_STATUS" \
|
||||
SUMMARY_FRESH_PERMISSION_STATUS="$FRESH_PERMISSION_STATUS" \
|
||||
SUMMARY_FRESH_CHANNELS_STATUS="$FRESH_CHANNELS_STATUS" \
|
||||
SUMMARY_FRESH_AGENT_STATUS="$FRESH_AGENT_STATUS" \
|
||||
SUMMARY_FRESH_DASHBOARD_STATUS="$FRESH_DASHBOARD_STATUS" \
|
||||
SUMMARY_FRESH_DISCORD_STATUS="$FRESH_DISCORD_STATUS" \
|
||||
SUMMARY_UPGRADE_PRECHECK_STATUS="$UPGRADE_PRECHECK_STATUS" \
|
||||
SUMMARY_UPGRADE_STATUS="$UPGRADE_STATUS" \
|
||||
SUMMARY_LATEST_INSTALLED_VERSION="$LATEST_INSTALLED_VERSION" \
|
||||
SUMMARY_UPGRADE_MAIN_VERSION="$UPGRADE_MAIN_VERSION" \
|
||||
SUMMARY_UPGRADE_GATEWAY_STATUS="$UPGRADE_GATEWAY_STATUS" \
|
||||
SUMMARY_UPGRADE_PERMISSION_STATUS="$UPGRADE_PERMISSION_STATUS" \
|
||||
SUMMARY_UPGRADE_CHANNELS_STATUS="$UPGRADE_CHANNELS_STATUS" \
|
||||
SUMMARY_UPGRADE_AGENT_STATUS="$UPGRADE_AGENT_STATUS" \
|
||||
SUMMARY_UPGRADE_DASHBOARD_STATUS="$UPGRADE_DASHBOARD_STATUS" \
|
||||
SUMMARY_UPGRADE_DISCORD_STATUS="$UPGRADE_DISCORD_STATUS" \
|
||||
write_summary_json
|
||||
)"
|
||||
|
||||
@@ -900,8 +1184,9 @@ else
|
||||
printf ' baseline-install-version: %s\n' "$INSTALL_VERSION"
|
||||
fi
|
||||
printf ' daemon: %s\n' "$DAEMON_STATUS"
|
||||
printf ' fresh-main: %s (%s)\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION"
|
||||
printf ' latest->main: %s (%s)\n' "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION"
|
||||
printf ' fresh-main: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION" "$FRESH_GATEWAY_STATUS" "$FRESH_PERMISSION_STATUS" "$FRESH_CHANNELS_STATUS" "$FRESH_DASHBOARD_STATUS" "$FRESH_AGENT_STATUS" "$FRESH_DISCORD_STATUS"
|
||||
printf ' latest->main precheck: %s (%s)\n' "$UPGRADE_PRECHECK_STATUS" "$LATEST_INSTALLED_VERSION"
|
||||
printf ' latest->main: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION" "$UPGRADE_GATEWAY_STATUS" "$UPGRADE_PERMISSION_STATUS" "$UPGRADE_CHANNELS_STATUS" "$UPGRADE_DASHBOARD_STATUS" "$UPGRADE_AGENT_STATUS" "$UPGRADE_DISCORD_STATUS"
|
||||
printf ' logs: %s\n' "$RUN_DIR"
|
||||
printf ' summary: %s\n' "$SUMMARY_JSON_PATH"
|
||||
fi
|
||||
|
||||
@@ -3,6 +3,8 @@ set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/parallels-macos-common.sh"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/parallels-discord-common.sh"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/parallels-permissions-common.sh"
|
||||
|
||||
VM_NAME="macOS Tahoe"
|
||||
SNAPSHOT_HINT="macOS 26.3.1 latest"
|
||||
@@ -63,6 +65,10 @@ LATEST_INSTALLED_VERSION="skip"
|
||||
UPGRADE_MAIN_VERSION="skip"
|
||||
FRESH_GATEWAY_STATUS="skip"
|
||||
UPGRADE_GATEWAY_STATUS="skip"
|
||||
FRESH_PERMISSION_STATUS="skip"
|
||||
UPGRADE_PERMISSION_STATUS="skip"
|
||||
FRESH_CHANNELS_STATUS="skip"
|
||||
UPGRADE_CHANNELS_STATUS="skip"
|
||||
FRESH_AGENT_STATUS="skip"
|
||||
UPGRADE_AGENT_STATUS="skip"
|
||||
FRESH_DASHBOARD_STATUS="skip"
|
||||
@@ -320,72 +326,14 @@ upgrade_summary_label() {
|
||||
printf 'latest->dev'
|
||||
}
|
||||
|
||||
discord_api_request() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
local payload="${3:-}"
|
||||
local url="https://discord.com/api/v10$path"
|
||||
if [[ -n "$payload" ]]; then
|
||||
curl -fsS -X "$method" \
|
||||
-H "Authorization: Bot $DISCORD_TOKEN_VALUE" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "$payload" \
|
||||
"$url"
|
||||
return
|
||||
fi
|
||||
curl -fsS -X "$method" \
|
||||
-H "Authorization: Bot $DISCORD_TOKEN_VALUE" \
|
||||
"$url"
|
||||
}
|
||||
|
||||
json_contains_string() {
|
||||
local needle="$1"
|
||||
python3 - "$needle" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
needle = sys.argv[1]
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
|
||||
def contains(value):
|
||||
if isinstance(value, str):
|
||||
return needle in value
|
||||
if isinstance(value, list):
|
||||
return any(contains(item) for item in value)
|
||||
if isinstance(value, dict):
|
||||
return any(contains(item) for item in value.values())
|
||||
return False
|
||||
|
||||
raise SystemExit(0 if contains(payload) else 1)
|
||||
PY
|
||||
}
|
||||
|
||||
discord_delete_message_id_file() {
|
||||
local path="$1"
|
||||
[[ -f "$path" ]] || return 0
|
||||
[[ -s "$path" ]] || return 0
|
||||
discord_smoke_enabled || return 0
|
||||
|
||||
local message_id
|
||||
message_id="$(tr -d '\r\n' <"$path")"
|
||||
[[ -n "$message_id" ]] || return 0
|
||||
|
||||
set +e
|
||||
discord_api_request DELETE "/channels/$DISCORD_CHANNEL_ID/messages/$message_id" >/dev/null
|
||||
set -e
|
||||
}
|
||||
|
||||
cleanup_discord_smoke_messages() {
|
||||
discord_smoke_enabled || return 0
|
||||
[[ -d "$RUN_DIR" ]] || return 0
|
||||
|
||||
discord_delete_message_id_file "$RUN_DIR/fresh.discord-sent-message-id"
|
||||
discord_delete_message_id_file "$RUN_DIR/fresh.discord-host-message-id"
|
||||
discord_delete_message_id_file "$RUN_DIR/upgrade.discord-sent-message-id"
|
||||
discord_delete_message_id_file "$RUN_DIR/upgrade.discord-host-message-id"
|
||||
cleanup_discord_message_files \
|
||||
"$RUN_DIR/fresh.discord-sent-message-id" \
|
||||
"$RUN_DIR/fresh.discord-host-message-id" \
|
||||
"$RUN_DIR/upgrade.discord-sent-message-id" \
|
||||
"$RUN_DIR/upgrade.discord-host-message-id"
|
||||
}
|
||||
|
||||
resolve_snapshot_info() {
|
||||
@@ -1228,25 +1176,7 @@ verify_bundle_permissions() {
|
||||
cmd="$(cat <<EOF
|
||||
set -eu
|
||||
set -o pipefail
|
||||
root=\$($npm_q root -g)
|
||||
check_path() {
|
||||
local path="\$1"
|
||||
[ -e "\$path" ] || return 0
|
||||
local perm perm_oct
|
||||
perm=\$(/usr/bin/stat -f '%OLp' "\$path")
|
||||
perm_oct=\$((8#\$perm))
|
||||
if (( perm_oct & 0002 )); then
|
||||
echo "world-writable install artifact: \$path (\$perm)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
check_path "\$root/openclaw"
|
||||
check_path "\$root/openclaw/extensions"
|
||||
if [ -d "\$root/openclaw/extensions" ]; then
|
||||
while IFS= read -r -d '' extension_dir; do
|
||||
check_path "\$extension_dir"
|
||||
done < <(/usr/bin/find "\$root/openclaw/extensions" -mindepth 1 -maxdepth 1 -type d -print0)
|
||||
fi
|
||||
$(parallels_macos_permission_check_snippet | sed "s|/opt/homebrew/bin/npm|$npm_q|g")
|
||||
EOF
|
||||
)"
|
||||
guest_current_user_exec /bin/bash -lc "$cmd"
|
||||
@@ -1321,6 +1251,10 @@ EOF
|
||||
)"
|
||||
}
|
||||
|
||||
verify_channels_probe() {
|
||||
guest_current_user_exec "$GUEST_OPENCLAW_BIN" channels status --probe --json
|
||||
}
|
||||
|
||||
resolve_dashboard_url() {
|
||||
local dashboard_url
|
||||
dashboard_url="$(
|
||||
@@ -1406,27 +1340,7 @@ EOF
|
||||
|
||||
configure_discord_smoke() {
|
||||
local guilds_json script
|
||||
guilds_json="$(
|
||||
DISCORD_GUILD_ID="$DISCORD_GUILD_ID" DISCORD_CHANNEL_ID="$DISCORD_CHANNEL_ID" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
os.environ["DISCORD_GUILD_ID"]: {
|
||||
"channels": {
|
||||
os.environ["DISCORD_CHANNEL_ID"]: {
|
||||
"allow": True,
|
||||
"requireMention": False,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
PY
|
||||
)"
|
||||
guilds_json="$(build_discord_guilds_json)"
|
||||
script="$(cat <<EOF
|
||||
cat >/tmp/openclaw-discord-token <<'__OPENCLAW_TOKEN__'
|
||||
$DISCORD_TOKEN_VALUE
|
||||
@@ -1454,73 +1368,6 @@ EOF
|
||||
guest_current_user_sh "$script"
|
||||
}
|
||||
|
||||
discord_message_id_from_send_log() {
|
||||
local path="$1"
|
||||
python3 - "$path" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||
message_id = payload.get("payload", {}).get("messageId")
|
||||
if not message_id:
|
||||
message_id = payload.get("payload", {}).get("result", {}).get("messageId")
|
||||
if not message_id:
|
||||
raise SystemExit("messageId missing from send output")
|
||||
print(message_id)
|
||||
PY
|
||||
}
|
||||
|
||||
wait_for_discord_host_visibility() {
|
||||
local nonce="$1"
|
||||
local response
|
||||
local deadline=$((SECONDS + TIMEOUT_DISCORD_S))
|
||||
while (( SECONDS < deadline )); do
|
||||
set +e
|
||||
response="$(discord_api_request GET "/channels/$DISCORD_CHANNEL_ID/messages?limit=20")"
|
||||
local rc=$?
|
||||
set -e
|
||||
if [[ $rc -eq 0 ]] && [[ -n "$response" ]] && printf '%s' "$response" | json_contains_string "$nonce"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
post_host_discord_message() {
|
||||
local nonce="$1"
|
||||
local id_file="$2"
|
||||
local payload response
|
||||
payload="$(
|
||||
NONCE="$nonce" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"content": f"parallels-macos-smoke-inbound-{os.environ['NONCE']}",
|
||||
"flags": 4096,
|
||||
}
|
||||
)
|
||||
)
|
||||
PY
|
||||
)"
|
||||
response="$(discord_api_request POST "/channels/$DISCORD_CHANNEL_ID/messages" "$payload")"
|
||||
printf '%s' "$response" | python3 - "$id_file" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
payload = json.load(sys.stdin)
|
||||
message_id = payload.get("id")
|
||||
if not isinstance(message_id, str) or not message_id:
|
||||
raise SystemExit("host Discord post missing message id")
|
||||
pathlib.Path(sys.argv[1]).write_text(f"{message_id}\n", encoding="utf-8")
|
||||
PY
|
||||
}
|
||||
|
||||
wait_for_guest_discord_readback() {
|
||||
local nonce="$1"
|
||||
local response rc
|
||||
@@ -1679,6 +1526,8 @@ summary = {
|
||||
"status": os.environ["SUMMARY_FRESH_MAIN_STATUS"],
|
||||
"version": os.environ["SUMMARY_FRESH_MAIN_VERSION"],
|
||||
"gateway": os.environ["SUMMARY_FRESH_GATEWAY_STATUS"],
|
||||
"permissions": os.environ["SUMMARY_FRESH_PERMISSION_STATUS"],
|
||||
"channels": os.environ["SUMMARY_FRESH_CHANNELS_STATUS"],
|
||||
"agent": os.environ["SUMMARY_FRESH_AGENT_STATUS"],
|
||||
"dashboard": os.environ["SUMMARY_FRESH_DASHBOARD_STATUS"],
|
||||
"discord": os.environ["SUMMARY_FRESH_DISCORD_STATUS"],
|
||||
@@ -1691,6 +1540,8 @@ summary = {
|
||||
"devVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"],
|
||||
"mainVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"],
|
||||
"gateway": os.environ["SUMMARY_UPGRADE_GATEWAY_STATUS"],
|
||||
"permissions": os.environ["SUMMARY_UPGRADE_PERMISSION_STATUS"],
|
||||
"channels": os.environ["SUMMARY_UPGRADE_CHANNELS_STATUS"],
|
||||
"agent": os.environ["SUMMARY_UPGRADE_AGENT_STATUS"],
|
||||
"dashboard": os.environ["SUMMARY_UPGRADE_DASHBOARD_STATUS"],
|
||||
"discord": os.environ["SUMMARY_UPGRADE_DISCORD_STATUS"],
|
||||
@@ -1725,20 +1576,34 @@ run_fresh_main_lane() {
|
||||
phase_run "fresh.install-main" "$(install_main_timeout)" install_main_tgz "$host_ip" "openclaw-main-fresh.tgz"
|
||||
FRESH_MAIN_VERSION="$(extract_last_version "$(phase_log_path fresh.install-main)")"
|
||||
phase_run "fresh.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
|
||||
FRESH_PERMISSION_STATUS="fail"
|
||||
phase_run "fresh.verify-bundle-permissions" "$TIMEOUT_PERMISSION_S" verify_bundle_permissions
|
||||
FRESH_PERMISSION_STATUS="pass"
|
||||
say "fresh.permissions.ok"
|
||||
phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
|
||||
phase_run "fresh.gateway-start" "$TIMEOUT_GATEWAY_S" start_manual_gateway_if_needed
|
||||
FRESH_GATEWAY_STATUS="fail"
|
||||
phase_run "fresh.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway
|
||||
FRESH_GATEWAY_STATUS="pass"
|
||||
say "fresh.gateway.ok"
|
||||
FRESH_CHANNELS_STATUS="fail"
|
||||
phase_run "fresh.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe
|
||||
FRESH_CHANNELS_STATUS="pass"
|
||||
say "fresh.channels.ok"
|
||||
FRESH_DASHBOARD_STATUS="fail"
|
||||
phase_run "fresh.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load
|
||||
FRESH_DASHBOARD_STATUS="pass"
|
||||
say "fresh.dashboard.ok"
|
||||
FRESH_AGENT_STATUS="fail"
|
||||
phase_run "fresh.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn
|
||||
FRESH_AGENT_STATUS="pass"
|
||||
say "fresh.agent.ok"
|
||||
if discord_smoke_enabled; then
|
||||
FRESH_DISCORD_STATUS="fail"
|
||||
phase_run "fresh.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke
|
||||
phase_run "fresh.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "fresh"
|
||||
FRESH_DISCORD_STATUS="pass"
|
||||
say "fresh.discord.ok"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1759,28 +1624,46 @@ run_upgrade_lane() {
|
||||
UPGRADE_PRECHECK_STATUS="skipped"
|
||||
fi
|
||||
if upgrade_uses_host_tgz; then
|
||||
UPGRADE_PERMISSION_STATUS="fail"
|
||||
phase_run "upgrade.install-main" "$(install_main_timeout)" install_main_tgz "$host_ip" "openclaw-main-upgrade.tgz"
|
||||
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-main)")"
|
||||
phase_run "upgrade.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
|
||||
phase_run "upgrade.verify-bundle-permissions" "$TIMEOUT_PERMISSION_S" verify_bundle_permissions
|
||||
UPGRADE_PERMISSION_STATUS="pass"
|
||||
say "upgrade.permissions.ok"
|
||||
else
|
||||
phase_run "upgrade.update-dev" "$TIMEOUT_UPDATE_DEV_S" run_dev_channel_update
|
||||
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.update-dev)")"
|
||||
phase_run "upgrade.verify-dev-channel" "$TIMEOUT_VERIFY_S" verify_dev_channel_update
|
||||
UPGRADE_PERMISSION_STATUS="fail"
|
||||
phase_run "upgrade.verify-bundle-permissions" "$TIMEOUT_PERMISSION_S" verify_bundle_permissions
|
||||
UPGRADE_PERMISSION_STATUS="pass"
|
||||
say "upgrade.permissions.ok"
|
||||
fi
|
||||
phase_run "upgrade.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
|
||||
phase_run "upgrade.gateway-start" "$TIMEOUT_GATEWAY_S" start_manual_gateway_if_needed
|
||||
UPGRADE_GATEWAY_STATUS="fail"
|
||||
phase_run "upgrade.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway
|
||||
UPGRADE_GATEWAY_STATUS="pass"
|
||||
say "upgrade.gateway.ok"
|
||||
UPGRADE_CHANNELS_STATUS="fail"
|
||||
phase_run "upgrade.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe
|
||||
UPGRADE_CHANNELS_STATUS="pass"
|
||||
say "upgrade.channels.ok"
|
||||
UPGRADE_DASHBOARD_STATUS="fail"
|
||||
phase_run "upgrade.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load
|
||||
UPGRADE_DASHBOARD_STATUS="pass"
|
||||
say "upgrade.dashboard.ok"
|
||||
UPGRADE_AGENT_STATUS="fail"
|
||||
phase_run "upgrade.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn
|
||||
UPGRADE_AGENT_STATUS="pass"
|
||||
say "upgrade.agent.ok"
|
||||
if discord_smoke_enabled; then
|
||||
UPGRADE_DISCORD_STATUS="fail"
|
||||
phase_run "upgrade.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke
|
||||
phase_run "upgrade.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "upgrade"
|
||||
UPGRADE_DISCORD_STATUS="pass"
|
||||
say "upgrade.discord.ok"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1858,6 +1741,8 @@ SUMMARY_JSON_PATH="$(
|
||||
SUMMARY_FRESH_MAIN_STATUS="$FRESH_MAIN_STATUS" \
|
||||
SUMMARY_FRESH_MAIN_VERSION="$FRESH_MAIN_VERSION" \
|
||||
SUMMARY_FRESH_GATEWAY_STATUS="$FRESH_GATEWAY_STATUS" \
|
||||
SUMMARY_FRESH_PERMISSION_STATUS="$FRESH_PERMISSION_STATUS" \
|
||||
SUMMARY_FRESH_CHANNELS_STATUS="$FRESH_CHANNELS_STATUS" \
|
||||
SUMMARY_FRESH_AGENT_STATUS="$FRESH_AGENT_STATUS" \
|
||||
SUMMARY_FRESH_DASHBOARD_STATUS="$FRESH_DASHBOARD_STATUS" \
|
||||
SUMMARY_FRESH_DISCORD_STATUS="$FRESH_DISCORD_STATUS" \
|
||||
@@ -1866,6 +1751,8 @@ SUMMARY_JSON_PATH="$(
|
||||
SUMMARY_LATEST_INSTALLED_VERSION="$LATEST_INSTALLED_VERSION" \
|
||||
SUMMARY_UPGRADE_MAIN_VERSION="$UPGRADE_MAIN_VERSION" \
|
||||
SUMMARY_UPGRADE_GATEWAY_STATUS="$UPGRADE_GATEWAY_STATUS" \
|
||||
SUMMARY_UPGRADE_PERMISSION_STATUS="$UPGRADE_PERMISSION_STATUS" \
|
||||
SUMMARY_UPGRADE_CHANNELS_STATUS="$UPGRADE_CHANNELS_STATUS" \
|
||||
SUMMARY_UPGRADE_AGENT_STATUS="$UPGRADE_AGENT_STATUS" \
|
||||
SUMMARY_UPGRADE_DASHBOARD_STATUS="$UPGRADE_DASHBOARD_STATUS" \
|
||||
SUMMARY_UPGRADE_DISCORD_STATUS="$UPGRADE_DISCORD_STATUS" \
|
||||
@@ -1883,9 +1770,9 @@ else
|
||||
if [[ -n "$INSTALL_VERSION" ]]; then
|
||||
printf ' baseline-install-version: %s\n' "$INSTALL_VERSION"
|
||||
fi
|
||||
printf ' fresh-main: %s (%s) discord=%s\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION" "$FRESH_DISCORD_STATUS"
|
||||
printf ' fresh-main: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION" "$FRESH_GATEWAY_STATUS" "$FRESH_PERMISSION_STATUS" "$FRESH_CHANNELS_STATUS" "$FRESH_DASHBOARD_STATUS" "$FRESH_AGENT_STATUS" "$FRESH_DISCORD_STATUS"
|
||||
printf ' latest precheck: %s (%s)\n' "$UPGRADE_PRECHECK_STATUS" "$LATEST_INSTALLED_VERSION"
|
||||
printf ' %s: %s (%s) discord=%s\n' "$(upgrade_summary_label)" "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION" "$UPGRADE_DISCORD_STATUS"
|
||||
printf ' %s: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$(upgrade_summary_label)" "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION" "$UPGRADE_GATEWAY_STATUS" "$UPGRADE_PERMISSION_STATUS" "$UPGRADE_CHANNELS_STATUS" "$UPGRADE_DASHBOARD_STATUS" "$UPGRADE_AGENT_STATUS" "$UPGRADE_DISCORD_STATUS"
|
||||
printf ' logs: %s\n' "$RUN_DIR"
|
||||
printf ' summary: %s\n' "$SUMMARY_JSON_PATH"
|
||||
fi
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/parallels-discord-common.sh"
|
||||
source "$ROOT_DIR/scripts/e2e/lib/parallels-windows-common.sh"
|
||||
|
||||
VM_NAME="Windows 11"
|
||||
SNAPSHOT_HINT="pre-openclaw-native-e2e-2026-03-12"
|
||||
MODE="both"
|
||||
@@ -20,6 +24,10 @@ UPGRADE_FROM_PACKED_MAIN=0
|
||||
JSON_OUTPUT=0
|
||||
KEEP_SERVER=0
|
||||
CHECK_LATEST_REF=1
|
||||
DISCORD_TOKEN_ENV=""
|
||||
DISCORD_TOKEN_VALUE=""
|
||||
DISCORD_GUILD_ID=""
|
||||
DISCORD_CHANNEL_ID=""
|
||||
SNAPSHOT_ID=""
|
||||
SNAPSHOT_STATE=""
|
||||
SNAPSHOT_NAME=""
|
||||
@@ -45,17 +53,27 @@ TIMEOUT_ONBOARD_S=240
|
||||
TIMEOUT_ONBOARD_PHASE_S=$((TIMEOUT_ONBOARD_S + 60))
|
||||
TIMEOUT_GATEWAY_S=120
|
||||
TIMEOUT_AGENT_S=180
|
||||
TIMEOUT_DASHBOARD_S=60
|
||||
TIMEOUT_DISCORD_S=180
|
||||
|
||||
FRESH_MAIN_STATUS="skip"
|
||||
FRESH_MAIN_VERSION="skip"
|
||||
FRESH_GATEWAY_STATUS="skip"
|
||||
FRESH_PERMISSION_STATUS="skip"
|
||||
FRESH_CHANNELS_STATUS="skip"
|
||||
FRESH_AGENT_STATUS="skip"
|
||||
FRESH_DASHBOARD_STATUS="skip"
|
||||
FRESH_DISCORD_STATUS="skip"
|
||||
UPGRADE_STATUS="skip"
|
||||
UPGRADE_PRECHECK_STATUS="skip"
|
||||
LATEST_INSTALLED_VERSION="skip"
|
||||
UPGRADE_MAIN_VERSION="skip"
|
||||
UPGRADE_GATEWAY_STATUS="skip"
|
||||
UPGRADE_PERMISSION_STATUS="skip"
|
||||
UPGRADE_CHANNELS_STATUS="skip"
|
||||
UPGRADE_AGENT_STATUS="skip"
|
||||
UPGRADE_DASHBOARD_STATUS="skip"
|
||||
UPGRADE_DISCORD_STATUS="skip"
|
||||
|
||||
say() {
|
||||
printf '==> %s\n' "$*"
|
||||
@@ -111,6 +129,9 @@ die() {
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if command -v cleanup_discord_smoke_messages >/dev/null 2>&1; then
|
||||
cleanup_discord_smoke_messages
|
||||
fi
|
||||
if [[ -n "${SERVER_PID:-}" ]]; then
|
||||
kill "$SERVER_PID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
@@ -148,6 +169,9 @@ Options:
|
||||
Example: openclaw@2026.3.13-beta.1
|
||||
Default upgrade lane without this flag: latest/site installer -> dev channel update.
|
||||
--skip-latest-ref-check Skip latest-release ref-mode precheck.
|
||||
--discord-token-env <var> Host env var name for Discord bot token.
|
||||
--discord-guild-id <id> Discord guild ID for smoke roundtrip.
|
||||
--discord-channel-id <id> Discord channel ID for smoke roundtrip.
|
||||
--keep-server Leave temp host HTTP server running.
|
||||
--json Print machine-readable JSON summary.
|
||||
-h, --help Show help.
|
||||
@@ -208,6 +232,18 @@ while [[ $# -gt 0 ]]; do
|
||||
TARGET_PACKAGE_SPEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
--discord-token-env)
|
||||
DISCORD_TOKEN_ENV="$2"
|
||||
shift 2
|
||||
;;
|
||||
--discord-guild-id)
|
||||
DISCORD_GUILD_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--discord-channel-id)
|
||||
DISCORD_CHANNEL_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-latest-ref-check)
|
||||
CHECK_LATEST_REF=0
|
||||
shift
|
||||
@@ -264,6 +300,14 @@ esac
|
||||
API_KEY_VALUE="${!API_KEY_ENV:-}"
|
||||
[[ -n "$API_KEY_VALUE" ]] || die "$API_KEY_ENV is required"
|
||||
|
||||
if [[ -n "$DISCORD_TOKEN_ENV" || -n "$DISCORD_GUILD_ID" || -n "$DISCORD_CHANNEL_ID" ]]; then
|
||||
[[ -n "$DISCORD_TOKEN_ENV" ]] || die "--discord-token-env is required when Discord smoke args are set"
|
||||
[[ -n "$DISCORD_GUILD_ID" ]] || die "--discord-guild-id is required when Discord smoke args are set"
|
||||
[[ -n "$DISCORD_CHANNEL_ID" ]] || die "--discord-channel-id is required when Discord smoke args are set"
|
||||
DISCORD_TOKEN_VALUE="${!DISCORD_TOKEN_ENV:-}"
|
||||
[[ -n "$DISCORD_TOKEN_VALUE" ]] || die "$DISCORD_TOKEN_ENV is required for Discord smoke"
|
||||
fi
|
||||
|
||||
ps_single_quote() {
|
||||
printf "%s" "$1" | sed "s/'/''/g"
|
||||
}
|
||||
@@ -285,6 +329,16 @@ ps_array_literal() {
|
||||
printf '@(%s)' "$joined"
|
||||
}
|
||||
|
||||
cleanup_discord_smoke_messages() {
|
||||
discord_smoke_enabled || return 0
|
||||
[[ -d "$RUN_DIR" ]] || return 0
|
||||
cleanup_discord_message_files \
|
||||
"$RUN_DIR/fresh.discord-sent-message-id" \
|
||||
"$RUN_DIR/fresh.discord-host-message-id" \
|
||||
"$RUN_DIR/upgrade.discord-sent-message-id" \
|
||||
"$RUN_DIR/upgrade.discord-host-message-id"
|
||||
}
|
||||
|
||||
resolve_snapshot_info() {
|
||||
local json hint
|
||||
json="$(prlctl snapshot-list "$VM_NAME" --json)"
|
||||
@@ -753,7 +807,11 @@ summary = {
|
||||
"status": os.environ["SUMMARY_FRESH_MAIN_STATUS"],
|
||||
"version": os.environ["SUMMARY_FRESH_MAIN_VERSION"],
|
||||
"gateway": os.environ["SUMMARY_FRESH_GATEWAY_STATUS"],
|
||||
"permissions": os.environ["SUMMARY_FRESH_PERMISSION_STATUS"],
|
||||
"channels": os.environ["SUMMARY_FRESH_CHANNELS_STATUS"],
|
||||
"agent": os.environ["SUMMARY_FRESH_AGENT_STATUS"],
|
||||
"dashboard": os.environ["SUMMARY_FRESH_DASHBOARD_STATUS"],
|
||||
"discord": os.environ["SUMMARY_FRESH_DISCORD_STATUS"],
|
||||
},
|
||||
"upgrade": {
|
||||
"precheck": os.environ["SUMMARY_UPGRADE_PRECHECK_STATUS"],
|
||||
@@ -761,7 +819,11 @@ summary = {
|
||||
"latestVersionInstalled": os.environ["SUMMARY_LATEST_INSTALLED_VERSION"],
|
||||
"mainVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"],
|
||||
"gateway": os.environ["SUMMARY_UPGRADE_GATEWAY_STATUS"],
|
||||
"permissions": os.environ["SUMMARY_UPGRADE_PERMISSION_STATUS"],
|
||||
"channels": os.environ["SUMMARY_UPGRADE_CHANNELS_STATUS"],
|
||||
"agent": os.environ["SUMMARY_UPGRADE_AGENT_STATUS"],
|
||||
"dashboard": os.environ["SUMMARY_UPGRADE_DASHBOARD_STATUS"],
|
||||
"discord": os.environ["SUMMARY_UPGRADE_DISCORD_STATUS"],
|
||||
},
|
||||
}
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
||||
@@ -1933,6 +1995,14 @@ verify_version_contains() {
|
||||
esac
|
||||
}
|
||||
|
||||
verify_bundle_permissions() {
|
||||
guest_powershell "$(cat <<EOF
|
||||
$(parallels_windows_permission_helpers_ps)
|
||||
Assert-NonBroadWritableInstall
|
||||
EOF
|
||||
)"
|
||||
}
|
||||
|
||||
write_onboard_runner_script() {
|
||||
WINDOWS_ONBOARD_SCRIPT_PATH="$MAIN_TGZ_DIR/openclaw-onboard-$PROVIDER.ps1"
|
||||
cat >"$WINDOWS_ONBOARD_SCRIPT_PATH" <<EOF
|
||||
@@ -2226,6 +2296,109 @@ verify_turn() {
|
||||
agent --agent main --message "Reply with exact ASCII text OK only." --json
|
||||
}
|
||||
|
||||
verify_channels_probe() {
|
||||
guest_run_openclaw "" "" channels status --probe --json
|
||||
}
|
||||
|
||||
verify_dashboard_load() {
|
||||
guest_powershell "$(cat <<'EOF'
|
||||
$dashboardUrl = 'http://127.0.0.1:18789/'
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(30)
|
||||
$dashboardReady = $false
|
||||
while ([DateTime]::UtcNow -lt $deadline) {
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $dashboardUrl -UseBasicParsing -TimeoutSec 5
|
||||
$content = [string]$response.Content
|
||||
if ($content.Contains('<title>OpenClaw Control</title>') -and $content.Contains('<openclaw-app></openclaw-app>')) {
|
||||
$dashboardReady = $true
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
if (-not $dashboardReady) {
|
||||
throw "dashboard HTML did not become ready at $dashboardUrl"
|
||||
}
|
||||
EOF
|
||||
)"
|
||||
}
|
||||
|
||||
configure_discord_smoke() {
|
||||
local guilds_json restart_rc
|
||||
guilds_json="$(build_discord_guilds_json)"
|
||||
guest_run_openclaw "" "" config set channels.discord.token "$DISCORD_TOKEN_VALUE"
|
||||
guest_run_openclaw "" "" config set channels.discord.enabled true
|
||||
guest_run_openclaw "" "" config set channels.discord.groupPolicy allowlist
|
||||
guest_run_openclaw "" "" config set channels.discord.guilds "$guilds_json" --strict-json
|
||||
set +e
|
||||
restart_gateway
|
||||
restart_rc=$?
|
||||
set -e
|
||||
if [[ $restart_rc -ne 0 ]]; then
|
||||
warn "discord config gateway restart returned rc=$restart_rc; relying on readiness probe"
|
||||
fi
|
||||
show_gateway_status_compat
|
||||
guest_run_openclaw "" "" channels status --probe --json
|
||||
}
|
||||
|
||||
wait_for_guest_discord_readback() {
|
||||
local nonce="$1"
|
||||
local response rc
|
||||
local last_response_path="$RUN_DIR/discord-last-readback.json"
|
||||
local deadline=$((SECONDS + TIMEOUT_DISCORD_S))
|
||||
while (( SECONDS < deadline )); do
|
||||
set +e
|
||||
response="$(
|
||||
guest_run_openclaw \
|
||||
"" \
|
||||
"" \
|
||||
message read \
|
||||
--channel discord \
|
||||
--target "channel:$DISCORD_CHANNEL_ID" \
|
||||
--limit 20 \
|
||||
--json
|
||||
)"
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ -n "$response" ]]; then
|
||||
printf '%s' "$response" >"$last_response_path"
|
||||
fi
|
||||
if [[ $rc -eq 0 ]] && [[ -n "$response" ]] && printf '%s' "$response" | json_contains_string "$nonce"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
run_discord_roundtrip_smoke() {
|
||||
local phase="$1"
|
||||
local nonce outbound_nonce inbound_nonce outbound_message outbound_log sent_id_file host_id_file
|
||||
nonce="$(date +%s)-$RANDOM"
|
||||
outbound_nonce="$phase-out-$nonce"
|
||||
inbound_nonce="$phase-in-$nonce"
|
||||
outbound_message="parallels-windows-smoke-outbound-$outbound_nonce"
|
||||
outbound_log="$RUN_DIR/$phase.discord-send.json"
|
||||
sent_id_file="$RUN_DIR/$phase.discord-sent-message-id"
|
||||
host_id_file="$RUN_DIR/$phase.discord-host-message-id"
|
||||
|
||||
guest_run_openclaw \
|
||||
"" \
|
||||
"" \
|
||||
message send \
|
||||
--channel discord \
|
||||
--target "channel:$DISCORD_CHANNEL_ID" \
|
||||
--message "$outbound_message" \
|
||||
--silent \
|
||||
--json >"$outbound_log"
|
||||
|
||||
discord_message_id_from_send_log "$outbound_log" >"$sent_id_file"
|
||||
wait_for_discord_host_visibility "$outbound_nonce"
|
||||
post_host_discord_message "$inbound_nonce" "$host_id_file"
|
||||
wait_for_guest_discord_readback "$inbound_nonce"
|
||||
}
|
||||
|
||||
capture_latest_ref_failure() {
|
||||
set +e
|
||||
run_ref_onboard
|
||||
@@ -2261,11 +2434,34 @@ run_fresh_main_lane() {
|
||||
fi
|
||||
FRESH_MAIN_VERSION="$(extract_last_version "$(phase_log_path "$install_log_phase")")"
|
||||
phase_run "fresh.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version || return $?
|
||||
FRESH_PERMISSION_STATUS="fail"
|
||||
phase_run "fresh.verify-bundle-permissions" "$TIMEOUT_VERIFY_S" verify_bundle_permissions || return $?
|
||||
FRESH_PERMISSION_STATUS="pass"
|
||||
say "fresh.permissions.ok"
|
||||
phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_PHASE_S" run_ref_onboard || return $?
|
||||
FRESH_GATEWAY_STATUS="fail"
|
||||
phase_run "fresh.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway_reachable || return $?
|
||||
FRESH_GATEWAY_STATUS="pass"
|
||||
say "fresh.gateway.ok"
|
||||
FRESH_CHANNELS_STATUS="fail"
|
||||
phase_run "fresh.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe || return $?
|
||||
FRESH_CHANNELS_STATUS="pass"
|
||||
say "fresh.channels.ok"
|
||||
FRESH_DASHBOARD_STATUS="fail"
|
||||
phase_run "fresh.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load || return $?
|
||||
FRESH_DASHBOARD_STATUS="pass"
|
||||
say "fresh.dashboard.ok"
|
||||
FRESH_AGENT_STATUS="fail"
|
||||
phase_run "fresh.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn || return $?
|
||||
FRESH_AGENT_STATUS="pass"
|
||||
say "fresh.agent.ok"
|
||||
if discord_smoke_enabled; then
|
||||
FRESH_DISCORD_STATUS="fail"
|
||||
phase_run "fresh.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke || return $?
|
||||
phase_run "fresh.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "fresh" || return $?
|
||||
FRESH_DISCORD_STATUS="pass"
|
||||
say "fresh.discord.ok"
|
||||
fi
|
||||
}
|
||||
|
||||
run_upgrade_lane() {
|
||||
@@ -2300,16 +2496,39 @@ run_upgrade_lane() {
|
||||
phase_run "upgrade.update-dev" "$TIMEOUT_INSTALL_S" run_dev_channel_update "$host_ip" || return $?
|
||||
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.update-dev)")"
|
||||
phase_run "upgrade.verify-dev-channel" "$TIMEOUT_VERIFY_S" verify_dev_channel_update || return $?
|
||||
UPGRADE_PERMISSION_STATUS="fail"
|
||||
phase_run "upgrade.verify-bundle-permissions" "$TIMEOUT_VERIFY_S" verify_bundle_permissions || return $?
|
||||
UPGRADE_PERMISSION_STATUS="pass"
|
||||
say "upgrade.permissions.ok"
|
||||
# Stop the old managed gateway before ref-mode onboard rewrites config and
|
||||
# gateway auth. Restarting first can leave the old token alive and make the
|
||||
# onboard health probe fail against a stale daemon.
|
||||
phase_run "upgrade.gateway-stop" "$TIMEOUT_GATEWAY_S" stop_gateway || return $?
|
||||
phase_run "upgrade.onboard-ref" "$TIMEOUT_ONBOARD_PHASE_S" run_ref_onboard || return $?
|
||||
phase_run "upgrade.gateway-restart" "$TIMEOUT_GATEWAY_S" restart_gateway || return $?
|
||||
UPGRADE_GATEWAY_STATUS="fail"
|
||||
phase_run "upgrade.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway_reachable || return $?
|
||||
UPGRADE_GATEWAY_STATUS="pass"
|
||||
say "upgrade.gateway.ok"
|
||||
UPGRADE_CHANNELS_STATUS="fail"
|
||||
phase_run "upgrade.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe || return $?
|
||||
UPGRADE_CHANNELS_STATUS="pass"
|
||||
say "upgrade.channels.ok"
|
||||
UPGRADE_DASHBOARD_STATUS="fail"
|
||||
phase_run "upgrade.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load || return $?
|
||||
UPGRADE_DASHBOARD_STATUS="pass"
|
||||
say "upgrade.dashboard.ok"
|
||||
UPGRADE_AGENT_STATUS="fail"
|
||||
phase_run "upgrade.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn || return $?
|
||||
UPGRADE_AGENT_STATUS="pass"
|
||||
say "upgrade.agent.ok"
|
||||
if discord_smoke_enabled; then
|
||||
UPGRADE_DISCORD_STATUS="fail"
|
||||
phase_run "upgrade.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke || return $?
|
||||
phase_run "upgrade.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "upgrade" || return $?
|
||||
UPGRADE_DISCORD_STATUS="pass"
|
||||
say "upgrade.discord.ok"
|
||||
fi
|
||||
}
|
||||
|
||||
IFS=$'\t' read -r SNAPSHOT_ID SNAPSHOT_STATE SNAPSHOT_NAME <<<"$(resolve_snapshot_info)"
|
||||
@@ -2325,6 +2544,11 @@ say "Resolved snapshot: $SNAPSHOT_NAME [$SNAPSHOT_STATE]"
|
||||
say "Latest npm version: $LATEST_VERSION"
|
||||
say "Current head: $(git rev-parse --short HEAD)"
|
||||
say "Run logs: $RUN_DIR"
|
||||
if discord_smoke_enabled; then
|
||||
say "Discord smoke: guild=$DISCORD_GUILD_ID channel=$DISCORD_CHANNEL_ID"
|
||||
else
|
||||
say "Discord smoke: disabled"
|
||||
fi
|
||||
|
||||
if needs_host_tgz; then
|
||||
pack_main_tgz
|
||||
@@ -2376,13 +2600,21 @@ SUMMARY_JSON_PATH="$(
|
||||
SUMMARY_FRESH_MAIN_STATUS="$FRESH_MAIN_STATUS" \
|
||||
SUMMARY_FRESH_MAIN_VERSION="$FRESH_MAIN_VERSION" \
|
||||
SUMMARY_FRESH_GATEWAY_STATUS="$FRESH_GATEWAY_STATUS" \
|
||||
SUMMARY_FRESH_PERMISSION_STATUS="$FRESH_PERMISSION_STATUS" \
|
||||
SUMMARY_FRESH_CHANNELS_STATUS="$FRESH_CHANNELS_STATUS" \
|
||||
SUMMARY_FRESH_AGENT_STATUS="$FRESH_AGENT_STATUS" \
|
||||
SUMMARY_FRESH_DASHBOARD_STATUS="$FRESH_DASHBOARD_STATUS" \
|
||||
SUMMARY_FRESH_DISCORD_STATUS="$FRESH_DISCORD_STATUS" \
|
||||
SUMMARY_UPGRADE_PRECHECK_STATUS="$UPGRADE_PRECHECK_STATUS" \
|
||||
SUMMARY_UPGRADE_STATUS="$UPGRADE_STATUS" \
|
||||
SUMMARY_LATEST_INSTALLED_VERSION="$LATEST_INSTALLED_VERSION" \
|
||||
SUMMARY_UPGRADE_MAIN_VERSION="$UPGRADE_MAIN_VERSION" \
|
||||
SUMMARY_UPGRADE_GATEWAY_STATUS="$UPGRADE_GATEWAY_STATUS" \
|
||||
SUMMARY_UPGRADE_PERMISSION_STATUS="$UPGRADE_PERMISSION_STATUS" \
|
||||
SUMMARY_UPGRADE_CHANNELS_STATUS="$UPGRADE_CHANNELS_STATUS" \
|
||||
SUMMARY_UPGRADE_AGENT_STATUS="$UPGRADE_AGENT_STATUS" \
|
||||
SUMMARY_UPGRADE_DASHBOARD_STATUS="$UPGRADE_DASHBOARD_STATUS" \
|
||||
SUMMARY_UPGRADE_DISCORD_STATUS="$UPGRADE_DISCORD_STATUS" \
|
||||
write_summary_json
|
||||
)"
|
||||
|
||||
@@ -2399,9 +2631,9 @@ else
|
||||
if [[ -n "$INSTALL_VERSION" ]]; then
|
||||
printf ' baseline-install-version: %s\n' "$INSTALL_VERSION"
|
||||
fi
|
||||
printf ' fresh-main: %s (%s)\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION"
|
||||
printf ' fresh-main: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION" "$FRESH_GATEWAY_STATUS" "$FRESH_PERMISSION_STATUS" "$FRESH_CHANNELS_STATUS" "$FRESH_DASHBOARD_STATUS" "$FRESH_AGENT_STATUS" "$FRESH_DISCORD_STATUS"
|
||||
printf ' %s precheck: %s (%s)\n' "$(upgrade_summary_label)" "$UPGRADE_PRECHECK_STATUS" "$LATEST_INSTALLED_VERSION"
|
||||
printf ' %s: %s (%s)\n' "$(upgrade_summary_label)" "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION"
|
||||
printf ' %s: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$(upgrade_summary_label)" "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION" "$UPGRADE_GATEWAY_STATUS" "$UPGRADE_PERMISSION_STATUS" "$UPGRADE_CHANNELS_STATUS" "$UPGRADE_DASHBOARD_STATUS" "$UPGRADE_AGENT_STATUS" "$UPGRADE_DISCORD_STATUS"
|
||||
printf ' logs: %s\n' "$RUN_DIR"
|
||||
printf ' summary: %s\n' "$SUMMARY_JSON_PATH"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user