Skip to content

Commit 252d8f0

Browse files
committed
merge: 集成 openspec 九变更批次——schema 自动同步、router 形态统一、ansible 卸载、工作台折叠与身份、对话增量同步、Env Vars 分区、飞书回调可观测、权限 mode 门控、多用户 RBAC
2 parents 8f4f4ce + 8a02c8f commit 252d8f0

160 files changed

Lines changed: 14033 additions & 2883 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.ansible/roles/sebas/defaults/main.yml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44
# All variables have defaults; the playbook is a no-config WebUI-only deploy
55
# out of the box. Override per-host via host_vars/<host>.yml or `--extra-vars`.
66

7+
# --- action ----------------------------------------------------------------
8+
9+
# What the role does. Exactly two legal values:
10+
# install — provision + start the sebas service (the default, so running
11+
# the playbook without this variable is always harmless)
12+
# uninstall — DESTRUCTIVE teardown: service, both binaries, the data
13+
# directory (incl. core.secret and the sessions DB), the
14+
# rendered config and the deploy user are all removed.
15+
# Only triggered by passing it explicitly, e.g.
16+
# `ansible-playbook site.yml -e sebas_action=uninstall`.
17+
# Any other value fails the play before a single task touches the target.
18+
sebas_action: install
19+
720
# --- identity & layout ---------------------------------------------------
821

922
# OS account the systemd unit runs as. `sebas service --install` rejects root
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
---
2+
# sebas role — tasks.
3+
#
4+
# Order matters: user → dirs → binary → config → install → flush → health.
5+
# Idempotency is enforced per-step; the install step is gated by stat +
6+
# checksum so a redeploy with the same binary converges to no-op.
7+
#
8+
# Binary provisioning (release / file / preinstalled) funnels into a single
9+
# extract + copy step. The release tarball layout
10+
# (leading-dir: `sebas-<tag>-<target>/sebas`) is the canonical shape; a
11+
# self-built tarball in `file` mode is expected to match it (see README).
12+
13+
# ---------------------------------------------------------------- pre: derive
14+
15+
- name: Split sebas_webui_listen into host and port facts
16+
ansible.builtin.set_fact:
17+
sebas_webui_host: "{{ sebas_webui_listen | split(':') | first }}"
18+
sebas_webui_port: "{{ sebas_webui_listen | split(':') | last | int }}"
19+
20+
- name: Resolve latest release tag from GitHub
21+
ansible.builtin.uri:
22+
url: "https://api.github.com/repos/{{ sebas_github_repo }}/releases/latest"
23+
return_content: true
24+
register: sebas_github_latest
25+
when:
26+
- sebas_artifact_source == 'release'
27+
- sebas_version == 'latest'
28+
# Fail loudly on rate limit so the user knows to switch to sebas_artifact_source=file.
29+
failed_when: >-
30+
sebas_github_latest.status != 200 or
31+
(sebas_github_latest.status | string) == '403'
32+
33+
- name: Pin resolved tag as a fact
34+
ansible.builtin.set_fact:
35+
sebas_version_resolved: >-
36+
{{ sebas_version if sebas_version != 'latest'
37+
else (sebas_github_latest.json.tag_name | regex_replace('^v', '')) }}
38+
when: sebas_artifact_source == 'release'
39+
40+
# Layout assumed: <basename>/sebas (matches taiki-e/upload-rust-binary-action
41+
# default `leading-dir: true`, which release.yml does not override).
42+
- name: Pin tarball basename as a fact
43+
ansible.builtin.set_fact:
44+
sebas_artifact_basename: "{{ release_basename if sebas_artifact_source == 'release' else file_basename }}"
45+
vars:
46+
release_basename: "sebas-{{ sebas_version_resolved }}-x86_64-unknown-linux-gnu"
47+
file_basename: "{{ sebas_artifact_file | basename | regex_replace('\\.tar\\.gz$', '') }}"
48+
when: sebas_artifact_source in ['release', 'file']
49+
50+
- name: Fail when sebas_artifact_source=file but no path is given
51+
ansible.builtin.fail:
52+
msg: "sebas_artifact_source=file requires sebas_artifact_file (path to a prebuilt tarball or binary)"
53+
when:
54+
- sebas_artifact_source == 'file'
55+
- sebas_artifact_file | length == 0
56+
57+
# ------------------------------------------------------------ user & layout
58+
59+
- name: Ensure deploy user exists
60+
ansible.builtin.user:
61+
name: "{{ sebas_deploy_user }}"
62+
home: "{{ sebas_deploy_home }}"
63+
shell: /usr/sbin/nologin
64+
create_home: true
65+
system: false
66+
tags: [sebas_user]
67+
68+
- name: Ensure sebas data directory layout exists with correct ownership
69+
ansible.builtin.file:
70+
path: "{{ item }}"
71+
state: directory
72+
owner: "{{ sebas_deploy_user }}"
73+
group: "{{ sebas_deploy_user }}"
74+
mode: "0750"
75+
loop:
76+
- "{{ sebas_data_dir }}"
77+
- "{{ sebas_data_dir }}/bin"
78+
- "{{ sebas_config_path | dirname }}"
79+
80+
# ---------------------------------------------- binary: resolve tarball path
81+
82+
# Final tarball location on the controller, regardless of source. Each branch
83+
# below sets sebas_tarball_path; the extract step downstream consumes it.
84+
# `release` downloads from GitHub; `file` uses a user-supplied tarball that
85+
# is expected to match release layout (see README); `preinstalled` skips
86+
# provisioning entirely.
87+
88+
- name: (release) Download sebas tarball to controller
89+
ansible.builtin.get_url:
90+
url: "https://github.com/{{ sebas_github_repo }}/releases/download/v{{ sebas_version_resolved }}/{{ sebas_artifact_basename }}.tar.gz"
91+
dest: "/tmp/{{ sebas_artifact_basename }}.tar.gz"
92+
mode: "0644"
93+
delegate_to: localhost
94+
become: false
95+
when: sebas_artifact_source == 'release'
96+
97+
- name: Pin sebas_tarball_path to the downloaded release tarball
98+
ansible.builtin.set_fact:
99+
sebas_tarball_path: "/tmp/{{ sebas_artifact_basename }}.tar.gz"
100+
when: sebas_artifact_source == 'release'
101+
102+
- name: Pin sebas_tarball_path to the user-supplied tarball
103+
ansible.builtin.set_fact:
104+
sebas_tarball_path: "{{ sebas_artifact_file }}"
105+
when: sebas_artifact_source == 'file'
106+
107+
# ---------------------------------------- binary: extract + install copy
108+
109+
# `unarchive` puts files under <basename>/sebas. We then copy just the binary
110+
# to the managed path. Done as a 2-step so the checksum gate downstream sees
111+
# only the file that's actually deployed.
112+
113+
- name: Extract sebas tarball on the controller (release / file modes)
114+
ansible.builtin.unarchive:
115+
src: "{{ sebas_tarball_path }}"
116+
dest: /tmp
117+
remote_src: true
118+
creates: "/tmp/{{ sebas_artifact_basename }}/sebas"
119+
delegate_to: localhost
120+
become: false
121+
when: sebas_artifact_source in ['release', 'file']
122+
123+
- name: Copy extracted binary to target
124+
ansible.builtin.copy:
125+
src: "/tmp/{{ sebas_artifact_basename }}/sebas"
126+
dest: "{{ sebas_bin_path }}"
127+
owner: root
128+
group: root
129+
mode: "0755"
130+
when: sebas_artifact_source in ['release', 'file']
131+
132+
# (preinstalled) mode — no tarball, just verify what's already on the target.
133+
134+
- name: (preinstalled) Verify a sebas binary already exists at the managed path
135+
ansible.builtin.stat:
136+
path: "{{ sebas_bin_path }}"
137+
register: sebas_bin_stat
138+
when: sebas_artifact_source == 'preinstalled'
139+
140+
- name: (preinstalled) Fail when the binary is missing
141+
ansible.builtin.fail:
142+
msg: "sebas_artifact_source=preinstalled but no binary at {{ sebas_bin_path }}"
143+
when:
144+
- sebas_artifact_source == 'preinstalled'
145+
- not sebas_bin_stat.stat.exists
146+
147+
# ---------------------------------------------------------- checksum gate
148+
149+
# `sebas service --install` is NOT idempotent: with --force absent and the
150+
# unit already present, it exits 3. We only force when the seeded stable
151+
# binary doesn't match the supplied one, so a redeploy with the same
152+
# artifact converges to no-op.
153+
- name: Compute checksum of the provisioned binary
154+
ansible.builtin.stat:
155+
path: "{{ sebas_bin_path }}"
156+
checksum: sha256
157+
register: sebas_bin_stat
158+
159+
- name: Compute checksum of the previously seeded stable binary
160+
ansible.builtin.stat:
161+
path: "{{ sebas_data_dir }}/bin/sebas"
162+
checksum: sha256
163+
register: sebas_seeded_stat
164+
165+
- name: Decide whether the install step needs --force
166+
ansible.builtin.set_fact:
167+
sebas_install_force: >-
168+
{{ (not sebas_seeded_stat.stat.exists)
169+
or (sebas_bin_stat.stat.checksum != sebas_seeded_stat.stat.checksum) }}
170+
171+
# --------------------------------------------------------------- config
172+
173+
- name: Render config.toml onto the target
174+
ansible.builtin.template:
175+
src: config.toml.j2
176+
dest: "{{ sebas_config_path }}"
177+
owner: "{{ sebas_deploy_user }}"
178+
group: "{{ sebas_deploy_user }}"
179+
mode: "0640"
180+
notify: restart sebas
181+
182+
# --------------------------------------------------------------- install
183+
184+
- name: Install sebas as a systemd service
185+
ansible.builtin.command:
186+
cmd: >-
187+
{{ sebas_bin_path }} service --install
188+
--user {{ sebas_deploy_user }}
189+
--config {{ sebas_config_path }}
190+
--force
191+
--auto-start
192+
register: sebas_install
193+
changed_when: sebas_install_force | bool
194+
# `--skip-tags sebas_install` runs only the prep work (user, dirs, binary).
195+
tags: [sebas_install]
196+
197+
# Make sure any `restart sebas` notification from the config render runs
198+
# before we probe /health. Without this, the health check races the restart
199+
# (notified handlers flush at end-of-play by default).
200+
- name: Flush pending handlers
201+
ansible.builtin.meta: flush_handlers
202+
203+
# --------------------------------------------------------------- health
204+
205+
- name: Wait for the sebas service to become active
206+
ansible.builtin.systemd:
207+
name: sebas
208+
state: started
209+
register: sebas_service_state
210+
until: sebas_service_state.status.ActiveState == 'active'
211+
retries: "{{ sebas_health_retries }}"
212+
delay: "{{ sebas_health_delay }}"
213+
214+
- name: Probe /health over HTTP
215+
ansible.builtin.uri:
216+
url: "http://{{ sebas_webui_host }}:{{ sebas_webui_port }}/health"
217+
status_code: 200
218+
return_content: true
219+
register: sebas_health
220+
until: sebas_health.status == 200
221+
retries: "{{ sebas_health_retries }}"
222+
delay: "{{ sebas_health_delay }}"
223+
224+
- name: Report deploy outcome
225+
ansible.builtin.debug:
226+
msg:
227+
- "sebas deployed."
228+
- "service: {{ sebas_service_state.status.ActiveState }}"
229+
- "webui: http://{{ sebas_webui_listen }}"
230+
- "config: {{ sebas_config_path }}"

0 commit comments

Comments
 (0)