@ -1,123 +1,11 @@
// CI/CD 配置管理与 Workflow 生成( Gitea Actions, 语法兼容 GitHub Actions)
// 历史部署教程与踩坑指南见 docs/tauri-cicd-guide.md( 原 GitHub Actions 方案)
use crate ::commands ::toolchain ::parse_toolchain_contract ;
use crate ::db ;
use crate ::models ::{ CicdConfig , WorkflowResult } ;
use rusqlite ::params ;
use std ::path ::Path ;
use uuid ::Uuid ;
#[ derive(Debug, Clone, PartialEq, Eq) ]
struct WorkflowToolchain {
node_version : String ,
node_version_file : Option < String > ,
pnpm_version : Option < String > ,
node_source : String ,
pnpm_source : String ,
}
impl WorkflowToolchain {
fn from_project ( root : & Path , cfg : & CicdConfig ) -> Result < Self , String > {
let contract = parse_toolchain_contract ( root ) ? ;
if ! contract . conflicts . is_empty ( ) {
let evidence = [ & contract . node , & contract . pnpm ]
. into_iter ( )
. filter ( | declaration | declaration . state = = " conflict " )
. map ( | declaration | {
let candidates = declaration
. candidates
. iter ( )
. filter ( | candidate | candidate . exact )
. map ( | candidate | format! ( " {} = {} " , candidate . source , candidate . version ) )
. collect ::< Vec < _ > > ( )
. join ( " , " ) ;
format! ( " {} [ {} ] " , declaration . tool , candidates )
} )
. collect ::< Vec < _ > > ( )
. join ( " ; " ) ;
return Err ( format! (
" 项目工具链声明冲突,拒绝生成 CI workflow: {evidence} "
) ) ;
}
let fallback_node = cfg
. node_version
. as_deref ( )
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
. unwrap_or ( " 20 " ) ;
let node_version = contract
. node
. version
. clone ( )
. unwrap_or_else ( | | fallback_node . to_string ( ) ) ;
let node_version_file = contract
. node
. source
. as_deref ( )
. filter ( | source | matches! ( * source , " .nvmrc " | " .node-version " ) )
. map ( str ::to_string ) ;
let node_source = contract
. node
. source
. clone ( )
. unwrap_or_else ( | | " CicdConfig fallback " . to_string ( ) ) ;
let package_manager_is_source =
contract . pnpm . source . as_deref ( ) = = Some ( " package.json#packageManager " ) ;
let pnpm_version = if package_manager_is_source {
None
} else {
Some (
contract
. pnpm
. version
. clone ( )
. unwrap_or_else ( | | " 9 " . to_string ( ) ) ,
)
} ;
let pnpm_source = contract
. pnpm
. source
. clone ( )
. unwrap_or_else ( | | " legacy pnpm 9 fallback " . to_string ( ) ) ;
Ok ( Self {
node_version ,
node_version_file ,
pnpm_version ,
node_source ,
pnpm_source ,
} )
}
fn setup_steps ( & self ) -> String {
let pnpm_version = self
. pnpm_version
. as_ref ( )
. map ( | version | format! ( " \n with: \n version: ' {version} ' " ) )
. unwrap_or_default ( ) ;
let node_selector = self
. node_version_file
. as_ref ( )
. map ( | path | format! ( " node-version-file: ' {path} ' " ) )
. unwrap_or_else ( | | format! ( " node-version: ' {} ' " , self . node_version ) ) ;
format! (
r #" # 工具链来源: Node={node_source}; pnpm={pnpm_source}
- uses : pnpm / action - setup @ v4 { pnpm_version }
- name : Setup Node . js
uses : actions / setup - node @ v4
with :
{ node_selector }
cache : ' pnpm ' " #,
node_source = self . node_source ,
pnpm_source = self . pnpm_source ,
)
}
}
#[ tauri::command ]
pub fn get_cicd_config ( project_id : String ) -> Result < Option < CicdConfig > , String > {
let conn = db ::pool ( ) . get ( ) . map_err ( | e | e . to_string ( ) ) ? ;
@ -195,24 +83,14 @@ pub fn generate_workflow(
)
. map_err ( | e | e . to_string ( ) ) ? ;
let project_root = Path ::new ( & project_path ) ;
let ( content , filename , secrets ) = match config . config_type . as_str ( ) {
" tauri_oss " = > {
let toolchain = WorkflowToolchain ::from_project ( project_root , & config ) ? ;
build_tauri_oss_workflow ( & config , & toolchain )
}
" pr_ci " = > {
let toolchain = WorkflowToolchain ::from_project ( project_root , & config ) ? ;
build_pr_ci_workflow ( & config , & toolchain )
}
" tauri_oss " = > build_tauri_oss_workflow ( & config ) ,
" pr_ci " = > build_pr_ci_workflow ( & config ) ,
" deepseek_review " = > build_deepseek_review_workflow ( & config ) ,
_ = > {
let toolchain = WorkflowToolchain ::from_project ( project_root , & config ) ? ;
build_web_ssh_workflow ( & config , & toolchain )
}
_ = > build_web_ssh_workflow ( & config ) ,
} ;
let workflows_dir = project_root . join ( " .gitea " ) . join ( " workflows " ) ;
let workflows_dir = Path ::new ( & project_path ) . join ( " .gitea " ) . join ( " workflows " ) ;
std ::fs ::create_dir_all ( & workflows_dir ) . map_err ( | e | e . to_string ( ) ) ? ;
let file_path = workflows_dir . join ( & filename ) ;
std ::fs ::write ( & file_path , & content ) . map_err ( | e | e . to_string ( ) ) ? ;
@ -272,11 +150,7 @@ pub fn distribute_workflow_for_project(
} ;
let ( content , filename , secrets ) = match workflow_type {
" pr_ci " = > {
let toolchain =
WorkflowToolchain ::from_project ( Path ::new ( & project_path ) , & minimal_cfg ) ? ;
build_pr_ci_workflow ( & minimal_cfg , & toolchain )
}
" pr_ci " = > build_pr_ci_workflow ( & minimal_cfg ) ,
" deepseek_review " = > build_deepseek_review_workflow ( & minimal_cfg ) ,
other = > {
return Err ( format! (
@ -303,15 +177,12 @@ pub fn distribute_workflow_for_project(
// ── workflow 模板生成 ──────────────────────────────────────────────────────────
fn build_web_ssh_workflow (
cfg : & CicdConfig ,
toolchain : & WorkflowToolchain ,
) -> ( String , String , Vec < String > ) {
fn build_web_ssh_workflow ( cfg : & CicdConfig ) -> ( String , String , Vec < String > ) {
let branch = & cfg . trigger_branch ;
let node = cfg . node_version . as_deref ( ) . unwrap_or ( " 20 " ) ;
let build = cfg . build_command . as_deref ( ) . unwrap_or ( " pnpm build " ) ;
let path = cfg . deploy_path . as_deref ( ) . unwrap_or ( " /app/project " ) ;
let start = cfg . start_command . as_deref ( ) . unwrap_or ( " pm2 restart app " ) ;
let setup_steps = toolchain . setup_steps ( ) ;
let content = format! (
r #" # Gitea Actions( 语法兼容 GitHub Actions, action 由 runner 从镜像源拉取)
@ -327,7 +198,15 @@ jobs:
steps :
- uses : actions / checkout @ v4
{ setup_steps }
- uses : pnpm / action - setup @ v4
with :
version : 9
- name : Setup Node . js
uses : actions / setup - node @ v4
with :
node - version : ' { node } '
cache : ' pnpm '
- run : pnpm install - - frozen - lockfile
- run : { build }
@ -355,15 +234,12 @@ jobs:
( content , " deploy.yml " . to_string ( ) , secrets )
}
fn build_tauri_oss_workflow (
cfg : & CicdConfig ,
toolchain : & WorkflowToolchain ,
) -> ( String , String , Vec < String > ) {
fn build_tauri_oss_workflow ( cfg : & CicdConfig ) -> ( String , String , Vec < String > ) {
let node = cfg . node_version . as_deref ( ) . unwrap_or ( " 20 " ) ;
let endpoint = cfg
. oss_endpoint
. as_deref ( )
. unwrap_or ( " oss-cn-hangzhou.aliyuncs.com " ) ;
let setup_steps = toolchain . setup_steps ( ) ;
let content = format! (
r #" # Tauri v2 Windows 自动构建 + 签名 + 阿里云 OSS 发布( Gitea Actions)
@ -382,7 +258,15 @@ jobs:
steps :
- uses : actions / checkout @ v4
{ setup_steps }
- uses : pnpm / action - setup @ v4
with :
version : 9
- name : Setup Node . js
uses : actions / setup - node @ v4
with :
node - version : ' { node } '
cache : ' pnpm '
- name : Install Rust
uses : dtolnay / rust - toolchain @ stable
@ -489,13 +373,10 @@ jobs:
( content , " release.yml " . to_string ( ) , secrets )
}
fn build_pr_ci_workflow (
cfg : & CicdConfig ,
toolchain : & WorkflowToolchain ,
) -> ( String , String , Vec < String > ) {
fn build_pr_ci_workflow ( cfg : & CicdConfig ) -> ( String , String , Vec < String > ) {
let branch = & cfg . trigger_branch ;
let node = cfg . node_version . as_deref ( ) . unwrap_or ( " 20 " ) ;
let build_cmd = cfg . build_command . as_deref ( ) . unwrap_or ( " pnpm build " ) ;
let setup_steps = toolchain . setup_steps ( ) ;
let content = format! (
r #" # PR 代码质量检查( Gitea Actions)
@ -512,7 +393,15 @@ jobs:
steps :
- uses : actions / checkout @ v4
{ setup_steps }
- uses : pnpm / action - setup @ v4
with :
version : 9
- name : Setup Node . js
uses : actions / setup - node @ v4
with :
node - version : ' { node } '
cache : ' pnpm '
- run : pnpm install - - frozen - lockfile
- run : { build_cmd }
@ -626,7 +515,6 @@ diff:
#[ cfg(test) ]
mod tests {
use super ::* ;
use std ::path ::PathBuf ;
fn cfg ( config_type : & str ) -> CicdConfig {
CicdConfig {
@ -645,30 +533,9 @@ mod tests {
}
}
fn legacy_toolchain ( ) -> WorkflowToolchain {
WorkflowToolchain {
node_version : " 20 " . into ( ) ,
node_version_file : None ,
pnpm_version : Some ( " 9 " . into ( ) ) ,
node_source : " CicdConfig fallback " . into ( ) ,
pnpm_source : " legacy pnpm 9 fallback " . into ( ) ,
}
}
fn temp_project ( name : & str ) -> PathBuf {
let root = std ::env ::temp_dir ( ) . join ( format! (
" lian-jing-cicd-{name}-{}-{} " ,
std ::process ::id ( ) ,
Uuid ::new_v4 ( )
) ) ;
std ::fs ::create_dir_all ( & root ) . unwrap ( ) ;
root
}
#[ test ]
fn web_ssh_template_has_no_github_residue ( ) {
let ( content , filename , secrets ) =
build_web_ssh_workflow ( & cfg ( " web_ssh " ) , & legacy_toolchain ( ) ) ;
let ( content , filename , secrets ) = build_web_ssh_workflow ( & cfg ( " web_ssh " ) ) ;
assert! ( ! content . contains ( " github.com " ) , " 模板残留 github.com 引用 " ) ;
assert! ( ! content . contains ( " GITHUB_TOKEN " ) , " 模板残留 GITHUB_TOKEN " ) ;
assert_eq! ( filename , " deploy.yml " ) ;
@ -677,8 +544,7 @@ mod tests {
#[ test ]
fn tauri_oss_template_has_no_github_residue ( ) {
let ( content , filename , secrets ) =
build_tauri_oss_workflow ( & cfg ( " tauri_oss " ) , & legacy_toolchain ( ) ) ;
let ( content , filename , secrets ) = build_tauri_oss_workflow ( & cfg ( " tauri_oss " ) ) ;
assert! ( ! content . contains ( " github.com " ) , " 模板残留 github.com 引用 " ) ;
assert! ( ! content . contains ( " GITHUB_TOKEN " ) , " 模板残留 GITHUB_TOKEN " ) ;
assert! (
@ -697,7 +563,7 @@ mod tests {
#[ test ]
fn pr_ci_template_is_correct ( ) {
let ( content , filename , secrets ) = build_pr_ci_workflow ( & cfg ( " pr_ci " ) , & legacy_toolchain ( ) );
let ( content , filename , secrets ) = build_pr_ci_workflow ( & cfg ( " pr_ci " ) );
assert! ( content . contains ( " pull_request " ) , " 触发器应为 pull_request " ) ;
assert! ( content . contains ( " pnpm build " ) , " 应含构建步骤 " ) ;
// --if-present 只对 pnpm run 子命令有效,直接 `pnpm lint --if-present` 会把
@ -730,75 +596,4 @@ mod tests {
assert! ( secrets . contains ( & " LJ_TOKEN " . to_string ( ) ) ) ;
assert! ( secrets . contains ( & " LJ_URL " . to_string ( ) ) ) ;
}
#[ test ]
fn templates_consume_standard_project_toolchain_contract ( ) {
let root = temp_project ( " standard-contract " ) ;
std ::fs ::write ( root . join ( " .nvmrc " ) , " 24.19.0 \n " ) . unwrap ( ) ;
std ::fs ::write (
root . join ( " package.json " ) ,
r # "{"packageManager":"pnpm@9.15.0"}"# ,
)
. unwrap ( ) ;
let config = cfg ( " pr_ci " ) ;
let toolchain = WorkflowToolchain ::from_project ( & root , & config ) . unwrap ( ) ;
let contents = [
build_web_ssh_workflow ( & config , & toolchain ) . 0 ,
build_tauri_oss_workflow ( & config , & toolchain ) . 0 ,
build_pr_ci_workflow ( & config , & toolchain ) . 0 ,
] ;
for content in contents {
assert! ( content . contains ( " node-version-file: '.nvmrc' " ) ) ;
assert! ( content . contains ( " pnpm=package.json#packageManager " ) ) ;
assert! ( ! content . contains ( " version: '9' " ) ) ;
assert! ( ! content . contains ( " node-version: '20' " ) ) ;
}
std ::fs ::remove_dir_all ( root ) . unwrap ( ) ;
}
#[ test ]
fn toolchain_contract_falls_back_per_missing_tool ( ) {
let root = temp_project ( " partial-contract " ) ;
std ::fs ::write ( root . join ( " .nvmrc " ) , " 22.14.0 \n " ) . unwrap ( ) ;
let config = cfg ( " pr_ci " ) ;
let toolchain = WorkflowToolchain ::from_project ( & root , & config ) . unwrap ( ) ;
let content = build_pr_ci_workflow ( & config , & toolchain ) . 0 ;
assert! ( content . contains ( " node-version-file: '.nvmrc' " ) ) ;
assert! ( content . contains ( " version: '9' " ) ) ;
assert! ( content . contains ( " pnpm=legacy pnpm 9 fallback " ) ) ;
std ::fs ::remove_dir_all ( root ) . unwrap ( ) ;
}
#[ test ]
fn legacy_project_keeps_configured_node_and_pnpm_fallback ( ) {
let root = temp_project ( " legacy-contract " ) ;
let mut config = cfg ( " pr_ci " ) ;
config . node_version = Some ( " 18.20.8 " . into ( ) ) ;
let toolchain = WorkflowToolchain ::from_project ( & root , & config ) . unwrap ( ) ;
let content = build_pr_ci_workflow ( & config , & toolchain ) . 0 ;
assert! ( content . contains ( " node-version: '18.20.8' " ) ) ;
assert! ( content . contains ( " version: '9' " ) ) ;
assert! ( content . contains ( " Node=CicdConfig fallback " ) ) ;
std ::fs ::remove_dir_all ( root ) . unwrap ( ) ;
}
#[ test ]
fn conflicting_project_contract_blocks_workflow_generation ( ) {
let root = temp_project ( " conflicting-contract " ) ;
std ::fs ::write ( root . join ( " .nvmrc " ) , " 24.19.0 \n " ) . unwrap ( ) ;
std ::fs ::write ( root . join ( " .node-version " ) , " 22.14.0 \n " ) . unwrap ( ) ;
let error = WorkflowToolchain ::from_project ( & root , & cfg ( " pr_ci " ) ) . unwrap_err ( ) ;
assert! ( error . contains ( " 拒绝生成 CI workflow " ) ) ;
assert! ( error . contains ( " .nvmrc=24.19.0 " ) ) ;
assert! ( error . contains ( " .node-version=22.14.0 " ) ) ;
std ::fs ::remove_dir_all ( root ) . unwrap ( ) ;
}
}