Compare commits

...

4 Commits

Author SHA1 Message Date
55a05da3bf
feat: add XWorkmate install redirect (#23)
Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
2026-06-29 15:47:04 +08:00
477b52c516
fix(acp_server_opencode): detect opencode CLI at deploy time (portable across Debian/Ubuntu/macOS) (#22)
Stop assuming a fixed opencode path. Probe the real binary with 'command -v'
using the role PATH, then feed the resolved path to both the systemd unit and
the launchd plist (plist now also passes -opencode-bin). Falls back to the
OS-aware default when opencode is not yet installed.

Also remove the dead acp-bridge.service.j2 template: it was not deployed by any
task and referenced two undefined vars (acp_opencode_bridge_disabled_binary_path,
acp_opencode_bridge_opencode_binary_path) — a hardcoding landmine.

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 15:31:54 +08:00
4364786465
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 <manbuzhe2009@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 15:25:32 +08:00
e953d87f07
ci: add release/* branch source validation workflow (#19)
release/* 仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。
详见 iac_modules/docs/tldr-github-branch-model.md

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 12:12:33 +08:00
11 changed files with 179 additions and 50 deletions

View File

@ -0,0 +1,44 @@
name: Validate Release PR
# release/* 分支的发布策略门禁:仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。
# 详见 iac_modules/docs/tldr-github-branch-model.md
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
pull-requests: read
jobs:
validate-release-source:
runs-on: ubuntu-latest
if: startsWith(github.base_ref, 'release/')
steps:
- name: Check PR source branch
run: |
SRC="${{ github.head_ref }}"
TGT="${{ github.base_ref }}"
LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}"
echo "🔍 Validating PR into release branch"
echo " source: $SRC"
echo " target: $TGT"
echo " labels: $LABELS"
if [[ "$SRC" =~ ^hotfix/ ]]; then
echo "✅ Allowed: hotfix/* branch"
exit 0
fi
if [[ "$LABELS" =~ (^|,)(cherry-pick|backport)(,|$) ]]; then
echo "✅ Allowed: cherry-pick/backport labeled PR"
exit 0
fi
echo "❌ Rejected."
echo "release/* 仅接受:"
echo " - 来自 hotfix/* 的 PR"
echo " - 带 cherry-pick 或 backport 标签的 PR已验证 feature 的 backport/cherry-pick"
echo "禁止从 main / develop / feature/* 直接合并到 release/*。"
exit 1

View File

@ -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 }}/<cli>). 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: []

View File

@ -79,6 +79,29 @@
- "{{ acp_opencode_home }}/.local"
- "{{ acp_opencode_workdir }}"
# Resolve the OpenCode CLI at deploy time instead of assuming a fixed path.
# npm installs it wherever the active node prefix points (NodeSource -> /usr/bin
# on Debian/Ubuntu, ~/.local/bin or Homebrew on macOS), so probe the real path
# with the role PATH and fall back to the OS-aware default when not yet present.
- name: Resolve OpenCode CLI binary path
ansible.builtin.shell: |
set -eu
export PATH="{{ acp_opencode_path }}:${PATH}"
command -v opencode || true
args:
executable: /bin/bash
register: acp_opencode_resolved_bin
changed_when: false
- name: Use resolved OpenCode CLI binary path when present
ansible.builtin.set_fact:
acp_opencode_binary_path: "{{ acp_opencode_resolved_bin.stdout_lines[0] | trim }}"
when: acp_opencode_resolved_bin.stdout | default('') | trim | length > 0
- name: Report effective OpenCode CLI binary path
ansible.builtin.debug:
msg: "OpenCode CLI binary resolved to: {{ acp_opencode_binary_path }}"
- name: Deploy Caddy main file
ansible.builtin.template:
src: Caddyfile.j2

View File

@ -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') }}"

View File

@ -1,27 +0,0 @@
[Unit]
Description=XWorkmate OpenCode ACP bridge server
After=network-online.target {{ acp_opencode_service_name }}.service
Wants=network-online.target
[Service]
Type=simple
User={{ acp_opencode_service_user }}
Group={{ acp_opencode_service_group }}
WorkingDirectory={{ acp_opencode_workdir }}
Environment=HOME={{ acp_opencode_home }}
Environment=TERM=xterm-256color
Environment=ACP_LISTEN_ADDR={{ acp_opencode_bridge_listen_host }}:{{ acp_opencode_bridge_listen_port }}
Environment=ACP_ALLOWED_ORIGINS={{ acp_opencode_bridge_allowed_origins | join(',') }}
{% if acp_opencode_auth_token | trim | length > 0 %}
Environment=ACP_AUTH_TOKEN={{ acp_opencode_auth_token }}
{% endif %}
Environment=ACP_CODEX_BIN={{ acp_opencode_bridge_disabled_binary_path }}
Environment=ACP_CLAUDE_BIN={{ acp_opencode_bridge_disabled_binary_path }}
Environment=ACP_GEMINI_BIN={{ acp_opencode_bridge_disabled_binary_path }}
Environment=ACP_OPENCODE_BIN={{ acp_opencode_bridge_opencode_binary_path }}
ExecStart={{ acp_opencode_bridge_binary_path }} serve --listen {{ acp_opencode_bridge_listen_host }}:{{ acp_opencode_bridge_listen_port }}
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target

View File

@ -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

View File

@ -17,6 +17,7 @@
exec "{{ 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 }}"
</string>
</array>

View File

@ -6,6 +6,16 @@ migrate_litellm_db: "litellm"
migrate_litellm_db_user: "litellm"
migrate_litellm_db_host: "127.0.0.1"
# Public bootstrap redirects
ai_workspace_caddy_base_dir: "{{ caddy_config_dir | default('/etc/caddy') }}"
ai_workspace_caddy_conf_dir: "{{ ai_workspace_caddy_base_dir }}/conf.d"
ai_workspace_caddyfile_path: "{{ ai_workspace_caddy_base_dir }}/Caddyfile"
ai_workspace_caddy_fragment_path: "{{ ai_workspace_caddy_conf_dir }}/install.svc.plus.caddy"
ai_workspace_public_domain: "install.svc.plus"
ai_workspace_install_script_url: "https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh"
ai_workspace_xworkmate_install_script_url: "https://raw.githubusercontent.com/ai-workspace-lab/xworkmate-app/main/scripts/install-xworkmate-app.sh"
ai_workspace_manage_caddy: true
# Migration paths
openclaw_data_dir: "~/.openclaw"
xworkspace_state_dir: "~/.local/state/xworkspace"

View File

@ -1,5 +1,6 @@
---
dependencies:
- role: roles/vhosts/caddy
- role: roles/agent_skills
- role: roles/vhosts/gateway_openclaw
- role: roles/vhosts/xworkmate_bridge

View File

@ -1,4 +1,35 @@
---
- name: Ensure AI Workspace Caddy fragment directory exists
ansible.builtin.file:
path: "{{ ai_workspace_caddy_conf_dir }}"
state: directory
mode: "0755"
when: ai_workspace_manage_caddy | bool
- name: Render install.svc.plus redirect fragment
ansible.builtin.template:
src: Caddyfile.j2
dest: "{{ ai_workspace_caddy_fragment_path }}"
mode: "0644"
register: ai_workspace_caddy_fragment
when: ai_workspace_manage_caddy | bool
- name: Validate Caddy configuration
ansible.builtin.command: >-
caddy validate --config {{ ai_workspace_caddyfile_path }}
changed_when: false
when:
- ai_workspace_manage_caddy | bool
- ai_workspace_caddy_fragment.changed
- name: Reload Caddy after updating install redirects
ansible.builtin.service:
name: caddy
state: reloaded
when:
- ai_workspace_manage_caddy | bool
- ai_workspace_caddy_fragment.changed
# =============================================================================
# Final deployment of the prebuilt XWorkspace Console runtime.
#

View File

@ -0,0 +1,6 @@
{{ ai_workspace_public_domain }} {
redir /ai-workspace {{ ai_workspace_install_script_url }} 302
redir /ai-workspace/latest {{ ai_workspace_install_script_url }} 302
redir /xworkmate-app {{ ai_workspace_xworkmate_install_script_url }} 302
redir /xworkmate-app/latest {{ ai_workspace_xworkmate_install_script_url }} 302
}