docs(api): regenerate OpenAPI and note KenShell as the git data plane
Regenerate the OpenAPI documents and the generated web API typings after the GraphQL endpoint was removed, and update the auth architecture notes so they describe KenShell instead of githost.
ZhenYi committed
This change is very large, so some diffs are not shown. Clone the repository to see the full change.
Modified docs/oauth.md docs/oauth.md +3 −3 View docs/oauth.md
@@ -47,7 +47,7 @@
47473. GitData 浏览器侧改为 OIDC 授权码 + PKCE,cookie 中只放 KenOAuth 颁发的 **access token**。
48484. 删除 `lib/session` 与 `lib/session-layer`,新 `lib/jwt` 只做 EdDSA/JWKS 验签。
49495. GitData 用户主键统一为 `i64`,与 `auth.common.v1.Principal.user_id` 对齐。
506. GitData 的 `githost` 通过 gRPC 校验 Access Key / SSH 公钥。
506. KenShell(Git 数据面)通过 gRPC 校验 Access Key / SSH 公钥。
51517. 删除 GitData 自带管理后台,admin 用户与 RBAC 完全由 `C:\工程\KenAdmin` 管理。
52528. `/api/v1/internal/*` 改用服务密钥鉴权。
5353
@@ -108,8 +108,8 @@
108108 OIDC code/token │ │ gRPC + x-rpc-key
109109 │ │
110110┌───────────────────────────┴──┐ ┌───────┴───────────────────────┐
111│ apihost (:3000 / dev 4081) │ │ githost (:9418 / ssh :4022) │
112│ - OIDC RP / refresh │ │ - AccessKey / SSH Key 校验 │
111│ apihost (:3000 / dev 4081) │ │ KenShell (gRPC 50091) │
112│ - OIDC RP / refresh │ │ - smart HTTP / SSH │
113113│ - lib/jwt 验签 │ └───────────────────────────────┘
114114│ - 业务 REST / SPA │
115115│ - 本地投影读写 │
Deleted lib/ci/examples/actions_fixture.rs lib/ci/examples/actions_fixture.rs +0 −373
@@ -1,373 +0,0 @@
1use std::{collections::BTreeMap, path::PathBuf};
2
3use anyhow::{Context, ensure};
4use ci::{CiService, LoadedWorkflow, actions_environments::EnvironmentRules, runs::RunSeed};
5use models::{
6 ci::{runner as runners, workflow_run},
7 repo::repos,
8 space::spaces,
9};
10use sea_orm::{ActiveModelTrait, ColumnTrait, Database, EntityTrait, QueryFilter, Set};
11use serde_json::json;
12use time::OffsetDateTime;
13use uuid::Uuid;
14
15const WORKFLOW: &str = r#"name: Fixture
16on: [push, workflow_dispatch]
17jobs:
18 plan:
19 runs-on: self-hosted
20 outputs:
21 matrix: ${{ steps.plan.outputs.matrix }}
22 steps:
23 - id: plan
24 run: echo 'matrix={"value":["a","b"]}' >> "$GITHUB_OUTPUT"
25 verify:
26 needs: plan
27 runs-on: self-hosted
28 name: verify-${{ matrix.value }}
29 strategy:
30 max-parallel: 1
31 matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
32 outputs:
33 value: ${{ steps.result.outputs.value }}
34 steps:
35 - id: result
36 run: echo 'value=${{ matrix.value }}' >> "$GITHUB_OUTPUT"
37 cleanup:
38 needs: verify
39 if: always()
40 runs-on: self-hosted
41 steps:
42 - run: echo 'fixture completed'
43"#;
44
45#[tokio::main]
46async fn main() -> anyhow::Result<()> {
47 let directory = std::env::current_dir()?.join("target/actions-fixture");
48 tokio::fs::create_dir_all(&directory).await?;
49 let mut config = config::AppConfig::default();
50 config.database.url =
51 Some("postgres://postgres:fixture-password@127.0.0.1:45432/gitdata_ci_fixture".into());
52 config.database.max_connections = Some(20);
53 config.database.sqlx_logging = Some(false);
54 config.redis.url = Some("redis://127.0.0.1:46379/9".into());
55 config.s3.bucket = Some("ci-fixture".into());
56 config.s3.region = Some("us-east-1".into());
57 config.s3.endpoint = Some("http://127.0.0.1:49000".into());
58 config.s3.access_key_id = Some("fixture-access".into());
59 config.s3.secret_access_key = Some("fixture-secret-password".into());
60 config.s3.path_style = Some(true);
61 config.ci.enabled = true;
62 config.ci.secret_encryption_key = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=".into();
63 // Built by the git-data-actions repository now; override with ACTIONS_ENGINE_BINARY.
64 config.ci.actions_engine_binary = std::env::var("ACTIONS_ENGINE_BINARY").unwrap_or_else(|_| {
65 std::env::current_dir()
66 .expect("current dir")
67 .join("target/gitdata-actions-engine")
68 .to_string_lossy()
69 .into_owned()
70 });
71 config.ci.runtime_base_url = "http://127.0.0.1:43120".into();
72 config.ci.event_queue_topic = "gitdata:ci-fixture:events".into();
73 config.ci.lease_secs = 120;
74 config.git.repo_root = directory.join("repos").to_string_lossy().into_owned();
75 config.git.base_url =
76 std::env::var("FIXTURE_GIT_BASE_URL").unwrap_or_else(|_| "http://127.0.0.1:43121".into());
77 let database = Database::connect(config.database.url.clone().unwrap()).await?;
78 models::migrate::sync_schema(&database)
79 .await
80 .context("fixture schema sync")?;
81 let state = export::AppState::with_config(config).await?;
82 let service = CiService::new(state);
83 let (repo, space, owner, reviewer) = seed(&service).await?;
84 let http = axum::Router::new()
85 .nest(
86 "/api/v1",
87 ci::routes::runner_router().with_state(service.clone()),
88 )
89 .merge(ci::actions_runtime::router().with_state(service.clone()));
90 let listener = tokio::net::TcpListener::bind("127.0.0.1:43120").await?;
91 let server = tokio::spawn(async move { axum::serve(listener, http).await });
92 let runner_id = Uuid::now_v7();
93 let now = OffsetDateTime::now_utc();
94 runners::ActiveModel {
95 id: Set(runner_id),
96 name: Set("actions-fixture".into()),
97 labels: Set(json!(["self-hosted"])),
98 capacity: Set(4),
99 version: Set(Some("fixture".into())),
100 state: Set(runners::RunnerState::Active),
101 space_id: Set(Some(space.id)),
102 last_seen_at: Set(Some(now)),
103 created_at: Set(now),
104 updated_at: Set(now),
105 protocol_version: Set(Some(2)),
106 capabilities: Set(Some(json!(["actions-v2", "docker"]))),
107 }
108 .insert(service.db_write())
109 .await?;
110 let token_id = Uuid::now_v7();
111 let (token, hash, salt, last_eight) = ci::runners::generate_credential(token_id);
112 models::ci::runner_token::ActiveModel {
113 id: Set(token_id),
114 runner_id: Set(None),
115 name: Set("fixture-client".into()),
116 token_hash: Set(hash),
117 token_salt: Set(salt),
118 token_last_eight: Set(last_eight),
119 space_id: Set(Some(space.id)),
120 single_use: Set(true),
121 used_at: Set(None),
122 revoked_at: Set(None),
123 expires_at: Set(Some(now + time::Duration::days(1))),
124 created_at: Set(now),
125 updated_at: Set(now),
126 }
127 .insert(service.db_write())
128 .await?;
129 let runner_config = config::runner::RunnerConfig {
130 server_url: "http://127.0.0.1:43120".into(),
131 registration_token: token,
132 name: "fixture-client".into(),
133 actions_engine_binary: service.config().ci.actions_engine_binary.clone(),
134 workspace_root: directory.join("workspaces").to_string_lossy().into_owned(),
135 actions_node_runtimes: BTreeMap::from([(
136 "node24".into(),
137 std::env::current_dir()?
138 .join("target/node24/bin/node")
139 .to_string_lossy()
140 .into_owned(),
141 )]),
142 actions_cache_dir: directory
143 .join("action-cache")
144 .to_string_lossy()
145 .into_owned(),
146 ..Default::default()
147 };
148 let client = runner::client::RunnerClient::new(&runner_config)?;
149 let registration = client
150 .register(&ci_protocol::runner::RegisterRequest {
151 token: runner_config.registration_token.clone(),
152 name: runner_config.name.clone(),
153 labels: vec!["self-hosted".into()],
154 capacity: 4,
155 version: Some("fixture".into()),
156 protocol_versions: vec![2, 1],
157 capabilities: vec!["actions-v2".into(), "docker".into()],
158 })
159 .await?;
160 let client = client.with_credential(registration.credential);
161 let run_id = create(&service, &repo, &space, owner, WORKFLOW).await?;
162 let count = drive(&service, &client, &runner_config, run_id).await?;
163 let result = workflow_run::Entity::find_by_id(run_id)
164 .one(service.db_write())
165 .await?
166 .unwrap();
167 ensure!(
168 result.state == models::ci::workflow_run::WorkflowRunState::Success,
169 "matrix run failed: {:?}",
170 service.run_detail(run_id).await?
171 );
172 ensure!(count == 4, "expected 4 physical jobs, got {count}");
173 let jobs = models::ci::workflow_job::Entity::find()
174 .filter(models::ci::workflow_job::Column::RunId.eq(run_id))
175 .all(service.db_write())
176 .await?;
177 ensure!(
178 jobs.iter()
179 .find(|job| job.job_key == "verify")
180 .and_then(|job| job.outputs.as_ref())
181 .is_some_and(|value| value["value"] == "b")
182 );
183 println!(
184 "PASS: protocol v2, persisted DAG, dynamic matrix, cross-job outputs, terminal step events"
185 );
186 let rerun = service.rerun(run_id, owner).await?;
187 drive(&service, &client, &runner_config, rerun).await?;
188 println!("PASS: snapshot rerun");
189 let rules = EnvironmentRules {
190 reviewers: vec![reviewer],
191 wait_minutes: 0,
192 ..Default::default()
193 };
194 service
195 .upsert_actions_environment(space.id, repo.id, "production", &rules)
196 .await?;
197 let approval_workflow = "on: push\njobs:\n deploy:\n runs-on: self-hosted\n environment: production\n steps:\n - run: echo approved\n";
198 let approved = create(&service, &repo, &space, owner, approval_workflow).await?;
199 service.advance_jobs().await?;
200 let claim = service
201 .claim_job(registration.runner_id, &["self-hosted".into()])
202 .await?;
203 ensure!(claim.is_none(), "unapproved environment job was claimed");
204 let approvals = service.actions_approvals(repo.id, approved).await?;
205 ensure!(approvals.len() == 1);
206 service
207 .review_actions_environment(repo.id, approved, approvals[0].id, reviewer, true, None)
208 .await?;
209 drive(&service, &client, &runner_config, approved).await?;
210 println!("PASS: environment approval gate");
211 server.abort();
212 Ok(())
213}
214
215async fn create(
216 service: &CiService,
217 repo: &repos::Model,
218 space: &spaces::Model,
219 owner: i64,
220 source: &str,
221) -> anyhow::Result<Uuid> {
222 let actions = service
223 .parse_actions_workflow(".github/workflows/fixture.yml", source)
224 .await?;
225 let workflow = LoadedWorkflow {
226 file: actions.file.clone(),
227 name: actions.name().unwrap_or("fixture".into()),
228 definition: actions.projection(),
229 actions: Some(actions),
230 };
231 let seed = RunSeed {
232 event: models::ci::workflow_run::WorkflowEventKind::Push,
233 event_payload: json!({"event_id":Uuid::now_v7()}),
234 commit_sha: "a".repeat(40),
235 git_ref: "refs/heads/main".into(),
236 branch: Some("main".into()),
237 tag: None,
238 base_sha: None,
239 base_ref: None,
240 pr_number: None,
241 trigger_user_id: Some(owner),
242 };
243 Ok(service.create_run(repo, space, &seed, &workflow).await?)
244}
245
246async fn drive(
247 service: &CiService,
248 client: &runner::client::RunnerClient,
249 config: &config::runner::RunnerConfig,
250 run_id: Uuid,
251) -> anyhow::Result<usize> {
252 let mut count = 0;
253 for _ in 0..100 {
254 service.advance_jobs().await?;
255 service.enforce_concurrency().await?;
256 let result = client
257 .claim(&ci_protocol::runner::ClaimRequest { wait_secs: Some(0) })
258 .await?;
259 if let Some(payload) = result.job {
260 let finish = runner::actions::execute(client, config, &payload).await;
261 client.finish(&payload.job_id, &finish).await?;
262 count += 1;
263 }
264 service.advance_jobs().await?;
265 service.finalize_runs().await?;
266 let run = workflow_run::Entity::find_by_id(run_id)
267 .one(service.db_write())
268 .await?
269 .unwrap();
270 if matches!(
271 run.state,
272 models::ci::workflow_run::WorkflowRunState::Success
273 | models::ci::workflow_run::WorkflowRunState::Failure
274 | models::ci::workflow_run::WorkflowRunState::Cancelled
275 ) {
276 return Ok(count);
277 }
278 }
279 anyhow::bail!("fixture run did not reach a terminal state")
280}
281
282async fn seed(service: &CiService) -> anyhow::Result<(repos::Model, spaces::Model, i64, i64)> {
283 let owner = 1001i64;
284 let reviewer = 1002i64;
285 let now = OffsetDateTime::now_utc();
286 for (id, name) in [
287 (owner, format!("fixture-{owner}")),
288 (reviewer, format!("fixture-{reviewer}")),
289 ] {
290 models::users::user::ActiveModel {
291 uid: Set(id),
292 username: Set(name.clone()),
293 lower_name: Set(name.clone()),
294 display_name: Set(name),
295 avatar_url: Set(String::new()),
296 is_active: Set(true),
297 is_restricted: Set(false),
298 prohibit_login: Set(false),
299 visibility: Set(Default::default()),
300 version: Set(1),
301 created_at: Set(now),
302 updated_at: Set(now),
303 ..Default::default()
304 }
305 .insert(service.db_write())
306 .await?;
307 }
308 let space_id = Uuid::now_v7();
309 let name = format!("fixture-{}", space_id.simple());
310 let space = spaces::ActiveModel {
311 id: Set(space_id),
312 lower_name: Set(name.clone()),
313 name: Set(name),
314 owner_id: Set(owner),
315 is_active: Set(true),
316 allow_join: Set(false),
317 allow_invite: Set(false),
318 nums_members: Set(1),
319 allow_join_level: Set(Default::default()),
320 member_must_enable_2fa: Set(false),
321 created_at: Set(now),
322 updated_at: Set(now),
323 ..Default::default()
324 }
325 .insert(service.db_write())
326 .await?;
327 let dir = Uuid::now_v7();
328 models::dir::dirs::ActiveModel {
329 id: Set(dir),
330 space_id: Set(Some(space.id)),
331 name: Set("repos".into()),
332 kind: Set(Default::default()),
333 position: Set(0),
334 created_by_uid: Set(owner),
335 created_at: Set(now),
336 updated_at: Set(now),
337 ..Default::default()
338 }
339 .insert(service.db_write())
340 .await?;
341 let id = Uuid::now_v7();
342 let name = format!("fixture-{}", id.simple());
343 let repo = repos::ActiveModel {
344 id: Set(id),
345 dir_id: Set(dir),
346 lower_name: Set(name.clone()),
347 name: Set(name),
348 default_branch: Set("main".into()),
349 nums_watch: Set(0),
350 nums_star: Set(0),
351 nums_pr: Set(0),
352 nums_commits: Set(0),
353 nums_branches: Set(0),
354 nums_tags: Set(0),
355 enable_lfs: Set(false),
356 size: Set(0),
357 git_size: Set(0),
358 lfs_size: Set(0),
359 created_at: Set(now),
360 updated_at: Set(now),
361 ..Default::default()
362 }
363 .insert(service.db_write())
364 .await?;
365 let directory = PathBuf::from(&service.config().git.repo_root).join(repo.id.to_string());
366 tokio::fs::create_dir_all(&directory).await?;
367 let status = std::process::Command::new("git")
368 .args(["init", "--bare"])
369 .arg(directory)
370 .status()?;
371 ensure!(status.success());
372 Ok((repo, space, owner, reviewer))
373}
Modified web/src/shared/api/apiDefinitions.ts web/src/shared/api/apiDefinitions.ts +0 −2 View web/src/shared/api/apiDefinitions.ts
@@ -218,8 +218,6 @@
218218 'chat.mark_thread_read': ['PUT', '/chat/threads/{thread_id}/read-state'],
219219 'chat.get_presence': ['GET', '/chat/users/{user_id}/presence'],
220220 'chat.voice_config': ['GET', '/chat/voice/config'],
221 'graphql.graphql_get': ['GET', '/graphql'],
222 'graphql.graphql_post': ['POST', '/graphql'],
223221 'imports.list_imports': ['GET', '/imports/{space_name}'],
224222 'imports.create_import': ['POST', '/imports/{space_name}'],
225223 'imports.probe_import': ['POST', '/imports/{space_name}/probe'],
Modified web/src/shared/api/globals.d.ts web/src/shared/api/globals.d.ts View web/src/shared/api/globals.d.ts
Diff too large to display.
Modified web/openapi.json web/openapi.json View web/openapi.json
Diff too large to display.
Modified openapi.json openapi.json View openapi.json
Diff too large to display.