From 4364786465411d10b2255403ed2d0de0da34a1e2 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 15:25:32 +0800 Subject: [PATCH] fix(acp_server_opencode): service PATH + bin var + surface adapter crash in validate (#21) ACP readiness probe returned 000 for the full retry window on xworkmate-bridge-ubuntu-26 (nothing listening = adapter crash-loop), but the play aborted at the probe so the real cause never reached the CI log. - systemd unit: add Environment=PATH ({{ acp_opencode_path }}, parity with the launchd plist) so the lazily-spawned opencode/node CLI resolves; replace the hardcoded --opencode-bin /usr/bin/opencode with {{ acp_opencode_binary_path }} ({{ npm_global_bin }}/opencode), matching the gemini/codex roles and macOS. - validate.yml: wrap the readiness probe in block/rescue that dumps systemctl status + journalctl on failure, so the adapter crash reason is visible. - fix latent undefined var in the summary (acp_opencode_adapter_http -> acp_opencode_adapter_probe), which would have errored once the endpoint came up. Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 --- .../acp_server_opencode/defaults/main.yml | 4 + .../acp_server_opencode/tasks/validate.yml | 79 +++++++++++++------ .../templates/opencode-acp.service.j2 | 3 +- 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/roles/vhosts/acp_server_opencode/defaults/main.yml b/roles/vhosts/acp_server_opencode/defaults/main.yml index 3545352..85eb5d7 100644 --- a/roles/vhosts/acp_server_opencode/defaults/main.yml +++ b/roles/vhosts/acp_server_opencode/defaults/main.yml @@ -11,6 +11,10 @@ acp_opencode_workdir: "{{ ansible_env.HOME | default('/home/' + acp_opencode_ser # user-level npm global bin lives under ~/.local/bin; include Homebrew + system. acp_opencode_npm_global_bin: "{{ acp_opencode_home + '/.local/bin' if ansible_os_family == 'Darwin' else '/usr/bin' }}" acp_opencode_path: "{{ acp_opencode_npm_global_bin }}:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin" +# OpenCode CLI binary the adapter spawns lazily (mirrors gemini/codex which use +# {{ acp_X_npm_global_bin }}/). Was hardcoded to /usr/bin/opencode in the +# unit template; use the resolved npm global bin so macOS (~/.local/bin) works too. +acp_opencode_binary_path: "{{ acp_opencode_npm_global_bin }}/opencode" acp_opencode_listen_host: 127.0.0.1 acp_opencode_listen_port: 38992 acp_opencode_packages: [] diff --git a/roles/vhosts/acp_server_opencode/tasks/validate.yml b/roles/vhosts/acp_server_opencode/tasks/validate.yml index 1199b90..8909094 100644 --- a/roles/vhosts/acp_server_opencode/tasks/validate.yml +++ b/roles/vhosts/acp_server_opencode/tasks/validate.yml @@ -20,33 +20,68 @@ # 用 curl 重试循环替代 uri:服务刚 (重)启时 adapter 会先 accept TCP 但短时间内不 # 应答(读挂起),而 uri 默认 30s 超时 + retries/until 在连接超时上不可靠循环(实测 # 仅试一次即失败)。每次 5s 上限、真重试,给冷启动足够时间(adapter 就绪后 ~4ms 回 200)。 -- name: Validate OpenCode local ACP endpoint (readiness retry) - ansible.builtin.shell: | - set -eu - url="http://{{ acp_opencode_listen_host }}:{{ acp_opencode_listen_port }}/acp/rpc" - body='{"jsonrpc":"2.0","id":1,"method":"acp.capabilities","params":{}}' - code="" - for i in $(seq 1 30); do - code="$(curl -s -m 5 -o /dev/null -w '%{http_code}' -X POST "$url" \ - -H 'Content-Type: application/json' -d "$body" 2>/dev/null || true)" - if [ "$code" = "200" ]; then - echo "OpenCode ACP endpoint ready after ${i} attempt(s)" - exit 0 - fi - sleep 2 - done - echo "OpenCode ACP endpoint ${url} not ready after retries (last code: ${code:-none})" >&2 - exit 1 - args: - executable: /bin/bash - changed_when: false - register: acp_opencode_adapter_probe +# 包在 block/rescue:探针失败时把 systemctl status + journalctl 打到 CI 日志, +# 否则 play 在此中止,看不到 adapter 进程真正的崩溃原因(如 last code 000)。 +- name: Validate OpenCode local ACP endpoint + block: + - name: Validate OpenCode local ACP endpoint (readiness retry) + ansible.builtin.shell: | + set -eu + url="http://{{ acp_opencode_listen_host }}:{{ acp_opencode_listen_port }}/acp/rpc" + body='{"jsonrpc":"2.0","id":1,"method":"acp.capabilities","params":{}}' + code="" + for i in $(seq 1 30); do + code="$(curl -s -m 5 -o /dev/null -w '%{http_code}' -X POST "$url" \ + -H 'Content-Type: application/json' -d "$body" 2>/dev/null || true)" + if [ "$code" = "200" ]; then + echo "OpenCode ACP endpoint ready after ${i} attempt(s)" + exit 0 + fi + sleep 2 + done + echo "OpenCode ACP endpoint ${url} not ready after retries (last code: ${code:-none})" >&2 + exit 1 + args: + executable: /bin/bash + changed_when: false + register: acp_opencode_adapter_probe + rescue: + - name: Capture OpenCode ACP service status on failure + ansible.builtin.command: systemctl status "{{ acp_opencode_service_name }}" --no-pager --full + register: acp_opencode_status_fail + changed_when: false + failed_when: false + when: ansible_os_family != 'Darwin' + + - name: Capture recent OpenCode ACP service logs on failure + ansible.builtin.command: journalctl -u "{{ acp_opencode_service_name }}" -n 80 --no-pager + register: acp_opencode_journal_fail + changed_when: false + failed_when: false + when: ansible_os_family != 'Darwin' + + - name: Show OpenCode ACP failure diagnostics + ansible.builtin.debug: + msg: + - "Probe stderr: {{ acp_opencode_adapter_probe.stderr | default('N/A') }}" + - "Listeners: {{ acp_opencode_ss.stdout | default('N/A') }}" + - "Service status: {{ acp_opencode_status_fail.stdout | default('N/A') }}" + - "Recent logs: {{ acp_opencode_journal_fail.stdout | default('N/A') }}" + + - name: Fail after emitting OpenCode ACP diagnostics + ansible.builtin.fail: + msg: >- + OpenCode ACP endpoint + {{ acp_opencode_listen_host }}:{{ acp_opencode_listen_port }} did not + become ready. See the diagnostics above (service status + journal) for + the adapter crash cause. - name: Show OpenCode ACP status ansible.builtin.command: systemctl status "{{ acp_opencode_service_name }}" --no-pager register: acp_opencode_status changed_when: false failed_when: false + when: ansible_os_family != 'Darwin' - name: Show OpenCode ACP validation summary ansible.builtin.debug: @@ -55,6 +90,6 @@ - "Preferred WebSocket endpoint: {{ acp_opencode_public_base_url }}/acp" - "Compatibility HTTP RPC endpoint: {{ acp_opencode_public_base_url }}/acp/rpc" - "OpenCode ACP adapter listener: {{ acp_opencode_listen_host }}:{{ acp_opencode_listen_port }}" + - "Readiness probe: {{ acp_opencode_adapter_probe.stdout | default('N/A') }}" - "Service: {{ acp_opencode_status.stdout | default('N/A') }}" - "Socket: {{ acp_opencode_ss.stdout | default('N/A') }}" - - "Adapter capabilities HTTP: {{ acp_opencode_adapter_http.content | default('N/A') }}" diff --git a/roles/vhosts/acp_server_opencode/templates/opencode-acp.service.j2 b/roles/vhosts/acp_server_opencode/templates/opencode-acp.service.j2 index 56e4eb7..804a8cc 100644 --- a/roles/vhosts/acp_server_opencode/templates/opencode-acp.service.j2 +++ b/roles/vhosts/acp_server_opencode/templates/opencode-acp.service.j2 @@ -9,9 +9,10 @@ User={{ acp_opencode_service_user }} Group={{ acp_opencode_service_group }} WorkingDirectory={{ acp_opencode_workdir }} Environment=HOME={{ acp_opencode_home }} +Environment=PATH={{ acp_opencode_path }} Environment=TERM=xterm-256color Environment=OPENCODE_ADAPTER_ALLOWED_ORIGINS={{ acp_opencode_bridge_allowed_origins | join(',') }} -ExecStart={{ acp_opencode_bridge_binary_path }} adapter opencode --listen {{ acp_opencode_listen_host }}:{{ acp_opencode_listen_port }} --opencode-bin /usr/bin/opencode --cwd {{ acp_opencode_workdir }} +ExecStart={{ acp_opencode_bridge_binary_path }} adapter opencode --listen {{ acp_opencode_listen_host }}:{{ acp_opencode_listen_port }} --opencode-bin {{ acp_opencode_binary_path }} --cwd {{ acp_opencode_workdir }} Restart=always RestartSec=2