Compare commits
4 Commits
release/v1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 55a05da3bf | |||
| 477b52c516 | |||
| 4364786465 | |||
| e953d87f07 |
44
.github/workflows/validate-release-pr.yml
vendored
Normal file
44
.github/workflows/validate-release-pr.yml
vendored
Normal 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
|
||||
@ -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: []
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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') }}"
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
---
|
||||
dependencies:
|
||||
- role: roles/vhosts/caddy
|
||||
- role: roles/agent_skills
|
||||
- role: roles/vhosts/gateway_openclaw
|
||||
- role: roles/vhosts/xworkmate_bridge
|
||||
|
||||
@ -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.
|
||||
#
|
||||
|
||||
6
roles/vhosts/ai-workspace/templates/Caddyfile.j2
Normal file
6
roles/vhosts/ai-workspace/templates/Caddyfile.j2
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user