diff --git a/ansible.cfg b/ansible.cfg index 17c8f0c..3395587 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -8,6 +8,9 @@ forks = 10 poll_interval = 10 transport = smart gathering = smart +fact_caching = jsonfile +fact_caching_connection = /tmp/ansible_facts +fact_caching_timeout = 3600 # 输出配置:使用 ansible-core 内置 callback,避免在轻量 CI 环境里缺少额外插件 stdout_callback = default @@ -25,3 +28,6 @@ deprecation_warnings = False cache = True cache_plugin = jsonfile cache_timeout = 3600 + +[ssh_connection] +pipelining = True diff --git a/docs/ai-workspace-runtime-delivery-plan.md b/docs/ai-workspace-runtime-delivery-plan.md new file mode 100644 index 0000000..818065f --- /dev/null +++ b/docs/ai-workspace-runtime-delivery-plan.md @@ -0,0 +1,163 @@ +# AI Workspace Runtime 交付计划 + +## 1. 目标与边界 + +本计划定义 AI Workspace 核心运行时从源码仓库构建、发布、离线聚合到目标机部署的完整交付链路。 + +核心原则: + +- LiteLLM、xworkspace-console、xworkmate-bridge、QMD 分别在各自源码仓库的 GitHub Actions build job 中构建。 +- 每个组件独立发布 `runtime-*` GitHub Release 及其 SHA256 清单。 +- offline package 只下载已发布产物,逐文件完成 SHA256 校验后再聚合。 +- 目标机只允许校验、解包、安装、配置、启动和健康检查,禁止源码编译、依赖构建及镜像构建。 +- 所有未经过 CI 或目标机矩阵实测的能力均保持 `TODO`,不得仅依据设计或局部实现标记完成。 + +## 2. 目标架构 + +```text +LiteLLM repository ---------- build job --> runtime-litellm-* ----------\ +xworkspace-console repository build job --> runtime-xworkspace-console-* --\ +xworkmate-bridge repository -- build job --> runtime-xworkmate-bridge-* -----+--> offline package job +QMD repository -------------- build job --> runtime-qmd-* ------------------/ | + | download + | SHA256 verify + | manifest aggregate + v + offline-package-* + | + v + target host: verify/install only +``` + +### 2.1 组件 Release + +四个组件必须由各自仓库负责构建,聚合仓库不得从源码代建组件。 + +| 组件 | 构建责任 | Release 命名 | 必需产物 | +| --- | --- | --- | --- | +| LiteLLM | LiteLLM 仓库 GitHub Actions build job | `runtime-litellm-*` | 固定版本 Python runtime/依赖包、启动入口、组件 manifest、SHA256 清单 | +| xworkspace-console | xworkspace-console 仓库 GitHub Actions build job | `runtime-xworkspace-console-*` | dashboard 静态产物、API 二进制、运行配置模板、组件 manifest、SHA256 清单 | +| xworkmate-bridge | xworkmate-bridge 仓库 GitHub Actions build job | `runtime-xworkmate-bridge-*` | bridge 二进制、systemd/运行配置模板、组件 manifest、SHA256 清单 | +| QMD | QMD 仓库 GitHub Actions build job | `runtime-qmd-*` | 已安装依赖和已构建 CLI/runtime、组件 manifest、SHA256 清单 | + +每个组件 manifest 至少记录:组件名、源码 commit、版本、构建时间、目标 OS、目标架构、入口文件、文件列表及每个文件的 SHA256。 + +### 2.2 offline package 聚合 + +offline package job 必须: + +1. 从四个组件的 `runtime-*` Release 下载与目标平台匹配的产物和 SHA256 清单。 +2. 在聚合前执行 SHA256 校验;缺少清单、文件缺失或摘要不一致时立即失败。 +3. 生成聚合 manifest,固定四个组件的 Release tag、源码 commit、资产 URL、资产大小和 SHA256。 +4. 将已校验组件产物、部署 playbook 所需依赖及聚合 manifest 打包为 `offline-package-*`。 +5. 对最终 offline package 再生成 SHA256,并在 CI 中执行一次解包与结构校验。 + +禁止以 `latest` 作为不可追溯的部署输入;重新聚合必须基于明确 tag 或不可变 commit。 + +### 2.3 目标机部署 + +目标机部署必须开启 prebuilt-only 约束。缺少任一预构建产物时直接失败,不得回退到以下行为: + +- `git clone` 或源码 checkout; +- `npm install`、`npm run build`、`go build`、`go run`; +- `pip install` 从公网或源码解析构建依赖; +- `docker build`、`podman build` 或其他本地镜像构建; +- 任何需要编译器、SDK 或前端构建工具链的安装步骤。 + +部署仅执行:offline package SHA256 校验、manifest 校验、解包、文件安装、权限设置、配置渲染、服务启动、健康检查和结果汇总。 + +## 3. 资源与性能约束 + +### 3.1 并发控制 + +- 全局并发硬上限必须满足 `并发数 <= 2 * 在线 CPU 数`,在线 CPU 数以执行时实际可用 CPU 为准。 +- 初始并发取任务上限、配置上限和 `2 * 在线 CPU 数` 三者最小值。 +- 调度器必须随 load 动态收缩:负载超过阈值时停止发放新任务并逐级降低并发;负载恢复且持续稳定后再缓慢扩容。 +- 动态收缩不得中断正在执行的不可重入安装步骤;只限制后续任务进入。 +- 日志和最终摘要必须记录 CPU 数、load 采样、每次并发调整的时间、原因及调整前后值。 + +### 3.2 部署耗时分布 + +每次部署必须记录总耗时及至少以下阶段耗时: + +- offline package 下载; +- SHA256 与 manifest 校验; +- 解包; +- 各组件安装; +- 配置渲染; +- 服务启动; +- 健康检查。 + +CI/验收报告按 OS、架构、冷启动/缓存命中、首次执行/幂等重跑分组,统计样本数、最小值、最大值、平均值以及 P50、P90、P95、P99。样本不足时保留原始数据并明确标注,不以单次耗时代替分布结论。 + +## 4. 支持矩阵与验收 + +目标支持以下全部组合: + +| 发行版 | 版本 | 架构 | +| --- | --- | --- | +| Debian | 11、12、13 | amd64、arm64 | +| Ubuntu | 22.04、24.04、26.04 | amd64、arm64 | + +每个矩阵项必须验证: + +1. offline package 下载和 SHA256 校验成功。 +2. 目标机在无源码、无构建工具链、组件外网访问受限的条件下部署成功。 +3. 四个组件版本与聚合 manifest 完全一致。 +4. 服务启动、健康检查和关键 smoke test 成功。 +5. 同一主机使用同一输入至少连续执行两次;第二次成功且无非预期变更、无重复资源、无凭据轮换、无构建行为。 +6. 首次部署和幂等重跑均产出阶段耗时及完整摘要。 + +Ubuntu 26.04 在实际可用 runner/镜像和依赖生态完成验证前,只能保持计划支持状态,不得标记已验证。 + +## 5. 当前事实 + +以下状态只记录当前仓库或相邻交付文档能够证明的事实,不把目标设计视为完成: + +- [x] 聚合入口已拆分为 preflight 与 runtime playbook;preflight 已校验 `docker`、`k3s`、`systemd` 运行模式组合。 +- [x] xworkspace-console 与 QMD 的部署代码已出现预构建 archive 输入及 prebuilt-only 缺包失败入口。 +- [x] 相邻一键部署文档已记录:xworkspace-console 离线包 `publish-release` 链路和 Release 产物上传曾核对完成。 +- [x] 相邻一键部署文档已记录:一键安装脚本优先使用离线安装包。 +- [ ] xworkspace-console 与 QMD 当前仍存在目标机源码 checkout/依赖安装/构建回退,尚未满足“目标机禁止构建”。 +- [ ] LiteLLM 当前可覆盖 package spec,但未证明其独立 `runtime-litellm-*` Release 和完全离线、免构建安装链路。 +- [ ] xworkmate-bridge 独立 `runtime-xworkmate-bridge-*` Release 和预构建消费链路尚未在本计划范围内验证。 +- [ ] 四组件 Release 的一致命名、manifest 和 SHA256 契约尚未完成验证。 +- [ ] offline package 的逐文件下载、SHA256 校验、聚合 manifest 和最终包校验尚未完成验证。 +- [ ] 并发硬上限、基于 load 的动态收缩及调整日志尚未完成验证。 +- [ ] 部署耗时分布统计尚未完成验证。 +- [ ] 连续重复执行的幂等性验收尚未完成。 +- [ ] Debian 11/12/13、Ubuntu 22.04/24.04/26.04 的 amd64/arm64 全矩阵尚未完成验证。 + +## 6. TODO + +### P0:构建与发布闭环 + +- [ ] TODO:在 LiteLLM 仓库建立 build job,发布 `runtime-litellm-*` Release、组件 manifest 和 SHA256 清单。 +- [ ] TODO:在 xworkspace-console 仓库固化 build job,确认每次发布 `runtime-xworkspace-console-*` Release、组件 manifest 和 SHA256 清单。 +- [ ] TODO:在 xworkmate-bridge 仓库建立 build job,发布 `runtime-xworkmate-bridge-*` Release、组件 manifest 和 SHA256 清单。 +- [ ] TODO:在 QMD 仓库建立 build job,发布 `runtime-qmd-*` Release、组件 manifest 和 SHA256 清单。 +- [ ] TODO:为 amd64、arm64 分别产出可安装资产;若资产与发行版相关,则按支持矩阵拆分并在 manifest 中明确兼容范围。 +- [ ] TODO:增加 Release 契约测试,拒绝缺失入口、manifest、SHA256 或架构资产的发布。 + +### P0:离线聚合与目标机免构建 + +- [ ] TODO:实现 offline package job,按固定 tag 下载四组件 Release,并在聚合前逐文件执行 SHA256 校验。 +- [ ] TODO:生成可追溯聚合 manifest,并为最终 `offline-package-*` 生成和发布 SHA256。 +- [ ] TODO:在目标机部署入口强制 prebuilt-only,删除或禁用四组件所有源码构建回退。 +- [ ] TODO:增加“目标机禁止构建”守卫,检测到编译器调用、包构建命令、源码 checkout 或镜像构建即失败。 +- [ ] TODO:在断网或仅允许访问 offline package 源的目标机上完成端到端部署验证。 + +### P1:并发、性能与可观测性 + +- [ ] TODO:实现在线 CPU 探测和 `<= 2 * 在线 CPU` 的全局并发硬限制。 +- [ ] TODO:定义 load 采样窗口、收缩/恢复阈值、迟滞策略和最低并发,完成动态收缩测试。 +- [ ] TODO:记录阶段级耗时、组件级耗时、并发变化和环境标签,产出结构化 JSON 及人类可读摘要。 +- [ ] TODO:汇总部署耗时分布,至少输出 count/min/max/avg/P50/P90/P95/P99,并区分首次执行与幂等重跑。 + +### P1:幂等与平台矩阵 + +- [ ] TODO:为每个支持矩阵项连续执行至少两次,验证第二次无非预期 changed、服务中断、重复资源或凭据变化。 +- [ ] TODO:覆盖 Debian 11/12/13 amd64/arm64。 +- [ ] TODO:覆盖 Ubuntu 22.04/24.04/26.04 amd64/arm64。 +- [ ] TODO:保存每个矩阵项的 Release tag、offline package SHA256、部署日志、耗时数据和验收结论。 +- [ ] TODO:全部矩阵通过后,再把“计划支持”更新为“已验证支持”;部分通过时逐项记录,不做整体完成声明。 diff --git a/roles/vhosts/litellm/defaults/main.yml b/roles/vhosts/litellm/defaults/main.yml index d632c2e..23444e9 100644 --- a/roles/vhosts/litellm/defaults/main.yml +++ b/roles/vhosts/litellm/defaults/main.yml @@ -8,9 +8,12 @@ litellm_version: "3ad385a8a46988b6a81fe6c0bc22ef58685baa58" litellm_debian_11_compat_version: "1.74.9" litellm_package_spec: >- {{ + lookup('ansible.builtin.env', 'LITELLM_PACKAGE_SPEC') + | default( 'litellm[proxy]==' ~ litellm_debian_11_compat_version if ansible_facts.distribution == 'Debian' and ansible_facts.distribution_major_version == '11' - else 'litellm[proxy] @ git+' ~ litellm_source_repo ~ '@' ~ litellm_version + else 'litellm[proxy] @ git+' ~ litellm_source_repo ~ '@' ~ litellm_version, + true) }} litellm_python_executable: "{{ lookup('ansible.builtin.env', 'LITELLM_PYTHON_EXECUTABLE') | default('python3', true) }}" litellm_pip_executable: "{{ lookup('ansible.builtin.env', 'LITELLM_PIP_EXECUTABLE') | default('', true) }}" diff --git a/roles/vhosts/qmd/defaults/main.yml b/roles/vhosts/qmd/defaults/main.yml index 441bca6..621e36d 100644 --- a/roles/vhosts/qmd/defaults/main.yml +++ b/roles/vhosts/qmd/defaults/main.yml @@ -5,6 +5,9 @@ qmd_home: "/home/{{ qmd_user }}" qmd_source_repo: "https://github.com/ai-workspace-services/qmd.git" qmd_version: "6021ea34ac27ac9b5c9a7d655500544917c801dd" qmd_source_dir: "{{ qmd_home }}/.local/src/qmd" +qmd_runtime_archive: "{{ lookup('ansible.builtin.env', 'QMD_RUNTIME_ARCHIVE') | default('', true) }}" +qmd_runtime_marker: "{{ qmd_source_dir }}/.runtime-archive-sha256" +ai_workspace_prebuilt_components_required: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_PREBUILT_COMPONENTS_REQUIRED') | default('false', true) | bool }}" qmd_binary_path: "{{ qmd_home }}/.bun/bin/qmd" qmd_config_dir: "{{ qmd_home }}/.config/qmd" qmd_cache_dir: "{{ qmd_home }}/.cache/qmd" diff --git a/roles/vhosts/qmd/tasks/main.yml b/roles/vhosts/qmd/tasks/main.yml index 3a0c655..b1bfcd4 100644 --- a/roles/vhosts/qmd/tasks/main.yml +++ b/roles/vhosts/qmd/tasks/main.yml @@ -62,6 +62,53 @@ group: "{{ qmd_group }}" mode: "0755" +- name: Validate packaged QMD runtime + ansible.builtin.stat: + path: "{{ qmd_runtime_archive }}" + register: qmd_runtime_archive_stat + when: qmd_runtime_archive | length > 0 + +- name: Require packaged QMD runtime + ansible.builtin.assert: + that: + - qmd_runtime_archive | length > 0 + - qmd_runtime_archive_stat.stat.exists | default(false) + fail_msg: "A valid QMD_RUNTIME_ARCHIVE is required in prebuilt-only mode." + when: ai_workspace_prebuilt_components_required + +- name: Inspect installed QMD runtime marker + ansible.builtin.slurp: + path: "{{ qmd_runtime_marker }}" + register: qmd_runtime_marker_content + failed_when: false + when: qmd_runtime_archive | length > 0 + +- name: Install packaged QMD runtime + ansible.builtin.unarchive: + src: "{{ qmd_runtime_archive }}" + dest: "{{ qmd_source_dir | dirname }}" + remote_src: true + owner: "{{ qmd_user }}" + group: "{{ qmd_group }}" + when: + - qmd_runtime_archive | length > 0 + - qmd_runtime_archive_stat.stat.exists | default(false) + - >- + (qmd_runtime_marker_content.content | default('') | b64decode | trim) + != (qmd_runtime_archive_stat.stat.checksum | default('')) + or not ((qmd_source_dir ~ '/bin/qmd') is file) + +- name: Record installed QMD runtime checksum + ansible.builtin.copy: + dest: "{{ qmd_runtime_marker }}" + owner: "{{ qmd_user }}" + group: "{{ qmd_group }}" + mode: "0644" + content: "{{ qmd_runtime_archive_stat.stat.checksum }}\n" + when: + - qmd_runtime_archive | length > 0 + - qmd_runtime_archive_stat.stat.exists | default(false) + - name: Checkout pinned QMD source ansible.builtin.git: repo: "{{ qmd_source_repo }}" @@ -71,6 +118,7 @@ become: true become_user: "{{ qmd_user }}" register: qmd_source_checkout + when: qmd_runtime_archive | length == 0 - name: Install QMD source dependencies ansible.builtin.command: @@ -79,6 +127,7 @@ become: true become_user: "{{ qmd_user }}" when: + - qmd_runtime_archive | length == 0 - qmd_source_checkout.changed | default(false) or not (qmd_binary.stat.exists | default(false)) - name: Build QMD from pinned source @@ -88,6 +137,7 @@ become: true become_user: "{{ qmd_user }}" when: + - qmd_runtime_archive | length == 0 - qmd_source_checkout.changed | default(false) or not (qmd_binary.stat.exists | default(false)) - name: Link pinned QMD binary @@ -99,7 +149,7 @@ group: "{{ qmd_group }}" force: true when: - - qmd_source_checkout.changed | default(false) or not (qmd_binary.stat.exists | default(false)) + - qmd_runtime_archive | length > 0 or qmd_source_checkout.changed | default(false) or not (qmd_binary.stat.exists | default(false)) - name: Reinspect QMD binary after source install ansible.builtin.stat: @@ -147,7 +197,9 @@ print("QMD fallback shim") if __name__ == "__main__": main() - when: not (qmd_binary_after_source.stat.exists | default(false)) + when: + - not ai_workspace_prebuilt_components_required + - not (qmd_binary_after_source.stat.exists | default(false)) - name: Reinspect QMD binary ansible.builtin.stat: diff --git a/setup-ai-workspace-all-in-one.yml b/setup-ai-workspace-all-in-one.yml index 0d4ac36..601a2a0 100644 --- a/setup-ai-workspace-all-in-one.yml +++ b/setup-ai-workspace-all-in-one.yml @@ -27,59 +27,7 @@ # # ============================================================================== -- name: Validate AI Workspace runtime modes - hosts: all - gather_facts: false - vars: - ai_workspace_runtime_modes: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_RUNTIME_MODES') | default('docker,systemd', true) }}" - tasks: - - name: Normalize runtime mode list - ansible.builtin.set_fact: - ai_workspace_runtime_mode_list: >- - {{ - ai_workspace_runtime_modes.split(',') - | map('trim') - | reject('equalto', '') - | list - if ai_workspace_runtime_modes is string - else ai_workspace_runtime_modes - }} +- import_playbook: setup-ai-workspace-preflight.yml - - name: Validate runtime mode combination - ansible.builtin.assert: - that: - - ai_workspace_runtime_mode_list | length > 0 - - not ('docker' in ai_workspace_runtime_mode_list and 'k3s' in ai_workspace_runtime_mode_list) - - ai_workspace_runtime_mode_list | difference(['docker', 'k3s', 'systemd']) | length == 0 - fail_msg: "docker 与 k3s 互斥;请选择 docker/k3s/systemd 的合法组合。" - -# 基础工作区与控制台 - import_playbook: setup-nodejs.yml -- import_playbook: setup-xworkspace-console.yaml -- import_playbook: setup-ai-agent-skills.yml - -# 网关运行时先启动,供 xworkmate-bridge 健康检查聚合 -- import_playbook: deploy_gateway_openclaw.yml - -# 核心网关与桥接 -- import_playbook: deploy_xworkmate_bridge_vhosts.yml - -# 基础数据与密钥设施 -- import_playbook: setup-vault.yaml -- import_playbook: setup-postgres-standalone.yaml -- import_playbook: setup-litellm.yaml - -# 大模型与 AI Agents -- import_playbook: deploy_QMD.yml - -# 可选服务 -- import_playbook: setup-xfce-xrdp.yaml - -# 最后的部署校验 -- name: 最终部署状态检查 - hosts: all - become: true - gather_facts: false - roles: - - role: roles/vhosts/validation - +- import_playbook: setup-ai-workspace-runtime.yml diff --git a/setup-ai-workspace-preflight.yml b/setup-ai-workspace-preflight.yml new file mode 100644 index 0000000..8d6322d --- /dev/null +++ b/setup-ai-workspace-preflight.yml @@ -0,0 +1,26 @@ +--- +- name: Validate AI Workspace runtime modes + hosts: all + gather_facts: false + vars: + ai_workspace_runtime_modes: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_RUNTIME_MODES') | default('docker,systemd', true) }}" + tasks: + - name: Normalize runtime mode list + ansible.builtin.set_fact: + ai_workspace_runtime_mode_list: >- + {{ + ai_workspace_runtime_modes.split(',') + | map('trim') + | reject('equalto', '') + | list + if ai_workspace_runtime_modes is string + else ai_workspace_runtime_modes + }} + + - name: Validate runtime mode combination + ansible.builtin.assert: + that: + - ai_workspace_runtime_mode_list | length > 0 + - not ('docker' in ai_workspace_runtime_mode_list and 'k3s' in ai_workspace_runtime_mode_list) + - ai_workspace_runtime_mode_list | difference(['docker', 'k3s', 'systemd']) | length == 0 + fail_msg: "docker 与 k3s 互斥;请选择 docker/k3s/systemd 的合法组合。" diff --git a/setup-ai-workspace-runtime.yml b/setup-ai-workspace-runtime.yml new file mode 100644 index 0000000..c198516 --- /dev/null +++ b/setup-ai-workspace-runtime.yml @@ -0,0 +1,29 @@ +--- +# 基础工作区与控制台 +- import_playbook: setup-xworkspace-console.yaml +- import_playbook: setup-ai-agent-skills.yml + +# 网关运行时先启动,供 xworkmate-bridge 健康检查聚合 +- import_playbook: deploy_gateway_openclaw.yml + +# 核心网关与桥接 +- import_playbook: deploy_xworkmate_bridge_vhosts.yml + +# 基础数据与密钥设施 +- import_playbook: setup-vault.yaml +- import_playbook: setup-postgres-standalone.yaml +- import_playbook: setup-litellm.yaml + +# 大模型与 AI Agents +- import_playbook: deploy_QMD.yml + +# 可选服务 +- import_playbook: setup-xfce-xrdp.yaml + +# 最后的部署校验 +- name: 最终部署状态检查 + hosts: all + become: true + gather_facts: false + roles: + - role: roles/vhosts/validation diff --git a/setup-xworkspace-console.yaml b/setup-xworkspace-console.yaml index 48023d7..47f23f8 100644 --- a/setup-xworkspace-console.yaml +++ b/setup-xworkspace-console.yaml @@ -15,8 +15,16 @@ xworkspace_console_repo_dir: /home/ubuntu/xworkspace-console xworkspace_console_source_repo: "https://github.com/ai-workspace-lab/xworkspace-console.git" xworkspace_console_source_version: "main" + xworkspace_console_runtime_archive: "{{ lookup('ansible.builtin.env', 'XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE') | default('', true) }}" + ai_workspace_prebuilt_components_required: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_PREBUILT_COMPONENTS_REQUIRED') | default('false', true) | bool }}" xworkspace_console_dashboard_dir: /home/ubuntu/xworkspace-console/dashboard xworkspace_console_api_dir: /home/ubuntu/xworkspace-console/api + xworkspace_console_api_binary: /home/ubuntu/xworkspace-console/bin/xworkspace-api + xworkspace_console_runtime_marker: /home/ubuntu/xworkspace-console/.runtime-archive-sha256 + xworkspace_console_api_working_dir: >- + {{ xworkspace_console_repo_dir if xworkspace_console_runtime_archive | length > 0 else xworkspace_console_api_dir }} + xworkspace_console_api_exec: >- + {{ xworkspace_console_api_binary if xworkspace_console_runtime_archive | length > 0 else '/usr/bin/env go run .' }} xworkspace_console_scripts_dir: /home/ubuntu/xworkspace/scripts xworkspace_console_config_dir: /home/ubuntu/.config/xworkspace xworkspace_console_url: http://127.0.0.1:17000 @@ -454,6 +462,54 @@ --disable-sync \ --new-window + - name: Validate packaged XWorkspace Console runtime + ansible.builtin.stat: + path: "{{ xworkspace_console_runtime_archive }}" + register: xworkspace_console_runtime_archive_stat + when: xworkspace_console_runtime_archive | length > 0 + + - name: Require packaged XWorkspace Console runtime + ansible.builtin.assert: + that: + - xworkspace_console_runtime_archive | length > 0 + - xworkspace_console_runtime_archive_stat.stat.exists | default(false) + fail_msg: "A valid XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE is required in prebuilt-only mode." + when: ai_workspace_prebuilt_components_required + + - name: Inspect installed XWorkspace Console runtime marker + ansible.builtin.slurp: + path: "{{ xworkspace_console_runtime_marker }}" + register: xworkspace_console_runtime_marker_content + failed_when: false + when: xworkspace_console_runtime_archive | length > 0 + + - name: Install packaged XWorkspace Console runtime + ansible.builtin.unarchive: + src: "{{ xworkspace_console_runtime_archive }}" + dest: "{{ xworkspace_console_repo_dir | dirname }}" + remote_src: true + owner: "{{ xworkspace_console_user }}" + group: "{{ xworkspace_console_user }}" + when: + - xworkspace_console_runtime_archive | length > 0 + - xworkspace_console_runtime_archive_stat.stat.exists | default(false) + - >- + (xworkspace_console_runtime_marker_content.content | default('') | b64decode | trim) + != (xworkspace_console_runtime_archive_stat.stat.checksum | default('')) + or not (xworkspace_console_api_binary is file) + or not ((xworkspace_console_dashboard_dir ~ '/dist/index.html') is file) + + - name: Record installed XWorkspace Console runtime checksum + ansible.builtin.copy: + dest: "{{ xworkspace_console_runtime_marker }}" + owner: "{{ xworkspace_console_user }}" + group: "{{ xworkspace_console_user }}" + mode: "0644" + content: "{{ xworkspace_console_runtime_archive_stat.stat.checksum }}\n" + when: + - xworkspace_console_runtime_archive | length > 0 + - xworkspace_console_runtime_archive_stat.stat.exists | default(false) + - name: Clone xworkspace-console repository ansible.builtin.git: repo: "{{ xworkspace_console_source_repo }}" @@ -462,12 +518,28 @@ depth: 1 force: true become_user: "{{ xworkspace_console_user }}" + register: xworkspace_console_source_checkout + when: xworkspace_console_runtime_archive | length == 0 - name: Build dashboard assets on target ansible.builtin.shell: | + set -euo pipefail cd "{{ xworkspace_console_dashboard_dir }}" + source_commit="$(git -C "{{ xworkspace_console_repo_dir }}" rev-parse HEAD)" + marker=".ai-workspace-build-commit" + if [ -f "dist/index.html" ] && [ "$(cat "$marker" 2>/dev/null || true)" = "$source_commit" ]; then + echo "build=unchanged" + exit 0 + fi npm install && npm run build + printf '%s\n' "$source_commit" > "$marker" + echo "build=changed" + args: + executable: /bin/bash become_user: "{{ xworkspace_console_user }}" + register: xworkspace_console_dashboard_build + changed_when: "'build=changed' in (xworkspace_console_dashboard_build.stdout | default(''))" + when: xworkspace_console_runtime_archive | length == 0 - name: Deploy AI Workspace portal service configuration ansible.builtin.copy: @@ -547,9 +619,9 @@ [Service] Type=simple - WorkingDirectory={{ xworkspace_console_api_dir }} + WorkingDirectory={{ xworkspace_console_api_working_dir }} EnvironmentFile={{ xworkspace_console_config_dir }}/portal.env - ExecStart=/usr/bin/env go run . + ExecStart={{ xworkspace_console_api_exec }} Restart=always RestartSec=2