From 9c7739c2c4d17f3bd4049a8899802ec5d6f1119f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niclas=20H=C3=BClsmann?= Date: Wed, 15 Jul 2026 12:44:03 +0200 Subject: [PATCH 1/2] fix(e2e): unblock onboarding agent's permissions, fix macOS timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second manual run (after the bash-3.2/XDG fix) got further - opencode resolved config/model correctly this time and started reading the README - but hit two more real bugs: - opencode's non-interactive `run` mode auto-REJECTS any permission request it can't ask about interactively, including reading paths outside the workdir (e.g. /etc/os-release) or running package managers. That's everything this test needs the agent to do. Add --auto (opencode's equivalent of claude-code's --dangerously-skip-permissions) so it can actually act. - macOS has no `timeout` binary (GNU coreutils only) and its BSD sleep rejects duration suffixes like "20m" - the macOS leg failed immediately with "timeout: command not found". Replace it with a small portable run_with_timeout() (background + sleep-then-kill watchdog + wait), and switch the timeout value to plain seconds so `sleep` works on both GNU and BSD. Verified against both a modern bash and the real bash:3.2 image, including the case where the wrapped command exits well before the timeout - an earlier draft of this wrapper toggled `set -e`/`set +e` globally inside the function, which leaked out and re-armed errexit before the caller could read $?, aborting the script early; fixed by capturing the status via `wait "$pid" || status=$?` instead. Signed-off-by: Niclas Hülsmann --- .github/workflows/e2e-readme-onboarding.yml | 8 ++-- scripts/e2e-readme-onboarding.sh | 46 ++++++++++++++++++--- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/.github/workflows/e2e-readme-onboarding.yml b/.github/workflows/e2e-readme-onboarding.yml index 139d52d2..2417fc5f 100644 --- a/.github/workflows/e2e-readme-onboarding.yml +++ b/.github/workflows/e2e-readme-onboarding.yml @@ -19,10 +19,10 @@ on: description: 'Pinned opencode package spec for the driving agent. Overrides scripts/e2e-readme-onboarding.sh''s default for THIS RUN ONLY' type: string default: 'opencode-ai@1.17.12' - timeout: - description: 'Wall-clock timeout for the onboarding agent session (e.g. 20m, 30m)' + timeout_secs: + description: 'Wall-clock timeout for the onboarding agent session, in seconds' type: string - default: '20m' + default: '1200' permissions: contents: read @@ -42,7 +42,7 @@ jobs: SKAINET_TOKEN: ${{ secrets.SKAINET_TOKEN }} SKAINET_INTERNAL: ${{ secrets.SKAINET_EXTERNAL }} E2E_VERSION_OPENCODE: ${{ github.event.inputs.opencode_version }} - E2E_ONBOARDING_TIMEOUT: ${{ github.event.inputs.timeout }} + E2E_ONBOARDING_TIMEOUT_SECS: ${{ github.event.inputs.timeout_secs }} steps: - name: Checkout uses: actions/checkout@v4 diff --git a/scripts/e2e-readme-onboarding.sh b/scripts/e2e-readme-onboarding.sh index 40326201..2ef271e0 100755 --- a/scripts/e2e-readme-onboarding.sh +++ b/scripts/e2e-readme-onboarding.sh @@ -31,7 +31,7 @@ # E2E_ONBOARDING_MODEL override the model id (default: zai-org/GLM-5.2) # E2E_LOG_DIR artifact output dir (default: /tmp/e2e-readme-logs) # E2E_README_REF git ref to extract README.md from (default: HEAD) -# E2E_ONBOARDING_TIMEOUT agent wall-clock timeout, e.g. "20m" (default: 20m) +# E2E_ONBOARDING_TIMEOUT_SECS agent wall-clock timeout in seconds (default: 1200) # # Exit code 0 = README was sufficient (doctor healthy + report written). # Exit code 1 = gaps blocked a working setup, or the agent never produced @@ -44,14 +44,40 @@ set -euo pipefail # (duplicated there too, per existing e2e.yml convention). DEFAULT_OPENCODE_VERSION="opencode-ai@1.17.12" DEFAULT_MODEL="zai-org/GLM-5.2" -DEFAULT_TIMEOUT="20m" +DEFAULT_TIMEOUT_SECS="1200" REPO="$(cd "$(dirname "$0")/.." && pwd)" LOG_DIR="${E2E_LOG_DIR:-/tmp/e2e-readme-logs}" README_REF="${E2E_README_REF:-HEAD}" OPENCODE_VERSION="${E2E_VERSION_OPENCODE:-$DEFAULT_OPENCODE_VERSION}" MODEL="${E2E_ONBOARDING_MODEL:-$DEFAULT_MODEL}" -TIMEOUT="${E2E_ONBOARDING_TIMEOUT:-$DEFAULT_TIMEOUT}" +# Seconds, not a duration string ("20m") — macOS ships only BSD sleep, +# which (unlike GNU sleep) rejects unit suffixes, and has no `timeout` +# binary at all. See run_with_timeout below. +TIMEOUT_SECS="${E2E_ONBOARDING_TIMEOUT_SECS:-$DEFAULT_TIMEOUT_SECS}" + +# Portable stand-in for GNU coreutils `timeout` (absent on macOS by +# default — only Linux runners have it). Backgrounds the command, races +# a sleep+kill watchdog against it, and returns the command's exit +# status (or 143/SIGTERM if the watchdog fired first). +run_with_timeout() { + local secs="$1"; shift + "$@" & + local pid=$! + ( sleep "$secs"; kill -TERM "$pid" 2>/dev/null ) & + local watchdog=$! + # `|| status=$?` (not set +e/-e) — set -e/+e are global, not scoped to + # this function, so toggling them here would leak into the caller and + # (if the caller had errexit off to inspect $?, as the one call site + # below does) silently re-enable it before this function returns, + # aborting the script on a non-zero exit instead of letting the caller + # read agent_status. + local status=0 + wait "$pid" || status=$? + kill "$watchdog" 2>/dev/null || true + wait "$watchdog" 2>/dev/null || true + return "$status" +} : "${SKAINET_TOKEN:?SKAINET_TOKEN not set}" : "${SKAINET_INTERNAL:?SKAINET_INTERNAL not set}" @@ -174,7 +200,7 @@ EOF # read sidesteps the bug entirely, regardless of body content. PROMPT="$(cat "$PROMPT_FILE")" -echo "== Running onboarding agent (timeout $TIMEOUT) ==" +echo "== Running onboarding agent (timeout ${TIMEOUT_SECS}s) ==" cd "$ONBOARD_DIR" set +e # GH Actions runners preset XDG_CONFIG_HOME/XDG_DATA_HOME (pointing at the @@ -183,14 +209,22 @@ set +e # override all three to match where auth.json/opencode.json were # written above, or opencode silently reads the real machine's config # instead of ours. Same fix as withHome() in internal/e2e/harnesses.go. +# +# --auto: opencode's non-interactive `run` mode auto-REJECTS any +# permission request it can't ask about interactively — including +# reading paths outside the workdir (e.g. /etc/os-release) or running +# package managers. Without it the agent can't do anything this test +# needs (installing prerequisites, omac, a harness). Equivalent to +# claude-code's --dangerously-skip-permissions; same rationale as that +# flag in claudeCodeConfig() (internal/e2e/harnesses.go). HOME="$DRIVER_HOME" \ XDG_CONFIG_HOME="$DRIVER_HOME/.config" \ XDG_DATA_HOME="$DRIVER_HOME/.local/share" \ XDG_STATE_HOME="$DRIVER_HOME/.local/state" \ SKAINET_TOKEN="$SKAINET_TOKEN" \ PATH="$PATH" \ -timeout --signal=TERM "$TIMEOUT" \ - opencode run --print-logs -m "model/$MODEL" "$PROMPT" \ +run_with_timeout "$TIMEOUT_SECS" \ + opencode run --print-logs --auto -m "model/$MODEL" "$PROMPT" \ > "$TRANSCRIPT_FILE" 2>&1 agent_status=$? set -e From 737d5771268019ceec6cc372ba51c9b6b328d91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niclas=20H=C3=BClsmann?= Date: Wed, 15 Jul 2026 12:53:09 +0200 Subject: [PATCH 2/2] fix(e2e): make cleanup trap writable-first so go install cache doesn't flip the exit code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both OS legs of the third manual run actually succeeded end-to-end this time (agent installed omac, got doctor healthy, launched omac start, and wrote a real report) - but ubuntu-latest still reported red. Root cause: the agent's own (documented, README-driven) `go install` attempt populates the Go module cache under $DRIVER_HOME/go/pkg/mod, which Go deliberately makes read-only. The script's `trap 'rm -rf "$WORK"' EXIT` then fails with a wall of "Permission denied", and since that's the EXIT trap's own exit status, it silently overrides whatever exit code the script had already decided on - turning a real pass into a false failure. Reproduced and fixed locally: chmod everything writable before rm -rf. Signed-off-by: Niclas Hülsmann --- scripts/e2e-readme-onboarding.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/e2e-readme-onboarding.sh b/scripts/e2e-readme-onboarding.sh index 2ef271e0..67e7c828 100755 --- a/scripts/e2e-readme-onboarding.sh +++ b/scripts/e2e-readme-onboarding.sh @@ -85,7 +85,14 @@ run_with_timeout() { mkdir -p "$LOG_DIR" WORK="$(mktemp -d)" -trap 'rm -rf "$WORK"' EXIT +# The agent may `go install` something (the README's "from source" path), +# which populates the Go module cache read-only by design (go's own +# tamper-protection) — a plain `rm -rf` on it fails with "Permission +# denied", and since this is the script's own EXIT trap, that failure +# status silently overrides whatever exit code the script explicitly +# set, turning a real success into a false failure. chmod everything +# writable first. +trap 'chmod -R u+w "$WORK" 2>/dev/null; rm -rf "$WORK"' EXIT # Fresh HOME for the driving opencode CLI — a real onboarding session # starts with no prior omac/opencode state, and this must never see the