Compare commits
2 Commits
bdee5ea67f
...
7c87b86f9e
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c87b86f9e | |||
|
|
9bf2d579ea |
@ -472,10 +472,10 @@ modules:
|
|||||||
- id: toolchain-capability-pack
|
- id: toolchain-capability-pack
|
||||||
name: 项目工具链能力包
|
name: 项目工具链能力包
|
||||||
area: infra
|
area: infra
|
||||||
status: done
|
status: in_progress
|
||||||
progress: 100
|
progress: 89
|
||||||
updated: 2026-09-22
|
updated: 2026-09-22
|
||||||
note: "v1 与 T8 自洽补强均已闭环:炼境自身通过 CLI、真实 MCP 与 Gitea push/PR CI 验证 Node 24.19.0、pnpm 9.15.0 同源生效;CI 双版本来源已修复,PR #15 经安全门禁合并。不追随 Codex bundled runtime,明确不支持 WSL;通用 CI 分发模板兼容迁移另立任务。"
|
note: "v1 与 T8 自洽补强均已闭环;T9 正在将通用 CI 分发模板迁移为优先消费项目 `.nvmrc` / `packageManager`、缺失时兼容旧项目回退的同源 adapter。"
|
||||||
position: [2500, 950]
|
position: [2500, 950]
|
||||||
edges:
|
edges:
|
||||||
# 前端依赖
|
# 前端依赖
|
||||||
|
|||||||
@ -141,8 +141,16 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b
|
|||||||
- done_note: "2026-09-22:炼境自身新增 `.nvmrc` 24.19.0、packageManager pnpm@9.15.0 与最小 mise resolver。CLI 命中 mise 安装目录,typecheck 通过、Rust 151/151;真实 MCP get_toolchain_status 验收 Node/pnpm 均为 ready_managed,pnpm-lock.yaml 零改动。首次 push CI #120 精确暴露 pnpm 双版本来源,修复后 push #122 与 PR #123 均成功,PR #15 以 head 23e9c297caad897c7917776308ca35a8014941df 经安全门禁合并为 96f88a8。"
|
- done_note: "2026-09-22:炼境自身新增 `.nvmrc` 24.19.0、packageManager pnpm@9.15.0 与最小 mise resolver。CLI 命中 mise 安装目录,typecheck 通过、Rust 151/151;真实 MCP get_toolchain_status 验收 Node/pnpm 均为 ready_managed,pnpm-lock.yaml 零改动。首次 push CI #120 精确暴露 pnpm 双版本来源,修复后 push #122 与 PR #123 均成功,PR #15 以 head 23e9c297caad897c7917776308ca35a8014941df 经安全门禁合并为 96f88a8。"
|
||||||
- retrospective: "【复盘】炼境自身工具链契约闭环|任务类型: 架构调整|复杂度: M|实际轮数: 3轮(预估 1 轮)|是否返工: 是|返工原因: Gitea workflow 同时硬编码 pnpm 9 与读取 packageManager,pnpm/action-setup 拒绝双版本来源|有效策略: 本地与 MCP 后继续让真实 push/PR CI 消费契约,门禁暴露 adapter 漂移并以单一事实来源修复|遗留风险: distribute_cicd 的通用 web_ssh/tauri_oss/pr_ci 模板仍硬编码 pnpm 9 与默认 Node 20,需另立兼容旧项目的任务,不能混入“炼境自身”T8|auto_applied_rules: R02, R11, R13|rule_effectiveness: R02=有效, R11=有效, R13=有效"
|
- retrospective: "【复盘】炼境自身工具链契约闭环|任务类型: 架构调整|复杂度: M|实际轮数: 3轮(预估 1 轮)|是否返工: 是|返工原因: Gitea workflow 同时硬编码 pnpm 9 与读取 packageManager,pnpm/action-setup 拒绝双版本来源|有效策略: 本地与 MCP 后继续让真实 push/PR CI 消费契约,门禁暴露 adapter 漂移并以单一事实来源修复|遗留风险: distribute_cicd 的通用 web_ssh/tauri_oss/pr_ci 模板仍硬编码 pnpm 9 与默认 Node 20,需另立兼容旧项目的任务,不能混入“炼境自身”T8|auto_applied_rules: R02, R11, R13|rule_effectiveness: R02=有效, R11=有效, R13=有效"
|
||||||
|
|
||||||
|
### 🔵 T9. 通用 CI 分发模板消费项目工具链契约
|
||||||
|
- status: in_progress
|
||||||
|
- complexity: M
|
||||||
|
- depends: T1. Node/pnpm 工具链契约解析器, T8. 炼境自身工具链契约闭环
|
||||||
|
- files: src-tauri/src/commands/cicd.rs, src/components/dashboard/CicdModal.tsx, docs/tauri-cicd-guide.md, .blueprint/modules/toolchain-capability-pack.md, .blueprint/manifest.yaml
|
||||||
|
- acceptance: "agent_verify: [cargo test] 临时项目提供 `.nvmrc` + package.json#packageManager 时,web_ssh/tauri_oss/pr_ci 模板使用 node-version-file 且 pnpm/action-setup 不重复写 version;只有部分声明时逐工具消费契约,完全缺失时保持已保存 Node 版本或 Node 20 + pnpm 9 的旧项目回退;冲突声明拒绝生成并返回证据;`cargo test --lib` 与 `pnpm typecheck` 通过,`git diff -- pnpm-lock.yaml` 为空。human_verify: CI/CD 模态框明确标注 Node 输入仅为无项目声明时的兼容回退版本。"
|
||||||
|
- decision_note: "CI adapter 复用 parse_toolchain_contract,不复制解析规则;项目声明是第一事实源,CicdConfig 只保留旧项目回退语义。"
|
||||||
|
|
||||||
## 执行顺序
|
## 执行顺序
|
||||||
|
|
||||||
T1 → T2 → T3;T2 → T4 → T5;T3 + T4 → T6;T3 + T4 + T6 → T7 → T8。
|
T1 → T2 → T3;T2 → T4 → T5;T3 + T4 → T6;T3 + T4 + T6 → T7 → T8 → T9。
|
||||||
|
|
||||||
核心能力先只读、后受控写;炼境实现者验证完成后,必须由 Enterprise System 以消费者角色完成最终闭环。
|
核心能力先只读、后受控写;炼境实现者验证完成后,必须由 Enterprise System 以消费者角色完成最终闭环。
|
||||||
|
|||||||
@ -192,14 +192,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# package.json#packageManager 是 pnpm 版本事实源;不要在此重复写 version
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
|
||||||
version: 9
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
|
|||||||
@ -1,11 +1,123 @@
|
|||||||
// CI/CD 配置管理与 Workflow 生成(Gitea Actions,语法兼容 GitHub Actions)
|
// CI/CD 配置管理与 Workflow 生成(Gitea Actions,语法兼容 GitHub Actions)
|
||||||
// 历史部署教程与踩坑指南见 docs/tauri-cicd-guide.md(原 GitHub Actions 方案)
|
// 历史部署教程与踩坑指南见 docs/tauri-cicd-guide.md(原 GitHub Actions 方案)
|
||||||
|
use crate::commands::toolchain::parse_toolchain_contract;
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::models::{CicdConfig, WorkflowResult};
|
use crate::models::{CicdConfig, WorkflowResult};
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use uuid::Uuid;
|
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]
|
#[tauri::command]
|
||||||
pub fn get_cicd_config(project_id: String) -> Result<Option<CicdConfig>, String> {
|
pub fn get_cicd_config(project_id: String) -> Result<Option<CicdConfig>, String> {
|
||||||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||||||
@ -83,14 +195,24 @@ pub fn generate_workflow(
|
|||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let project_root = Path::new(&project_path);
|
||||||
let (content, filename, secrets) = match config.config_type.as_str() {
|
let (content, filename, secrets) = match config.config_type.as_str() {
|
||||||
"tauri_oss" => build_tauri_oss_workflow(&config),
|
"tauri_oss" => {
|
||||||
"pr_ci" => build_pr_ci_workflow(&config),
|
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)
|
||||||
|
}
|
||||||
"deepseek_review" => build_deepseek_review_workflow(&config),
|
"deepseek_review" => build_deepseek_review_workflow(&config),
|
||||||
_ => build_web_ssh_workflow(&config),
|
_ => {
|
||||||
|
let toolchain = WorkflowToolchain::from_project(project_root, &config)?;
|
||||||
|
build_web_ssh_workflow(&config, &toolchain)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let workflows_dir = Path::new(&project_path).join(".gitea").join("workflows");
|
let workflows_dir = project_root.join(".gitea").join("workflows");
|
||||||
std::fs::create_dir_all(&workflows_dir).map_err(|e| e.to_string())?;
|
std::fs::create_dir_all(&workflows_dir).map_err(|e| e.to_string())?;
|
||||||
let file_path = workflows_dir.join(&filename);
|
let file_path = workflows_dir.join(&filename);
|
||||||
std::fs::write(&file_path, &content).map_err(|e| e.to_string())?;
|
std::fs::write(&file_path, &content).map_err(|e| e.to_string())?;
|
||||||
@ -150,7 +272,11 @@ pub fn distribute_workflow_for_project(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (content, filename, secrets) = match workflow_type {
|
let (content, filename, secrets) = match workflow_type {
|
||||||
"pr_ci" => build_pr_ci_workflow(&minimal_cfg),
|
"pr_ci" => {
|
||||||
|
let toolchain =
|
||||||
|
WorkflowToolchain::from_project(Path::new(&project_path), &minimal_cfg)?;
|
||||||
|
build_pr_ci_workflow(&minimal_cfg, &toolchain)
|
||||||
|
}
|
||||||
"deepseek_review" => build_deepseek_review_workflow(&minimal_cfg),
|
"deepseek_review" => build_deepseek_review_workflow(&minimal_cfg),
|
||||||
other => {
|
other => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@ -177,12 +303,15 @@ pub fn distribute_workflow_for_project(
|
|||||||
|
|
||||||
// ── workflow 模板生成 ──────────────────────────────────────────────────────────
|
// ── workflow 模板生成 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn build_web_ssh_workflow(cfg: &CicdConfig) -> (String, String, Vec<String>) {
|
fn build_web_ssh_workflow(
|
||||||
|
cfg: &CicdConfig,
|
||||||
|
toolchain: &WorkflowToolchain,
|
||||||
|
) -> (String, String, Vec<String>) {
|
||||||
let branch = &cfg.trigger_branch;
|
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 build = cfg.build_command.as_deref().unwrap_or("pnpm build");
|
||||||
let path = cfg.deploy_path.as_deref().unwrap_or("/app/project");
|
let path = cfg.deploy_path.as_deref().unwrap_or("/app/project");
|
||||||
let start = cfg.start_command.as_deref().unwrap_or("pm2 restart app");
|
let start = cfg.start_command.as_deref().unwrap_or("pm2 restart app");
|
||||||
|
let setup_steps = toolchain.setup_steps();
|
||||||
|
|
||||||
let content = format!(
|
let content = format!(
|
||||||
r#"# Gitea Actions(语法兼容 GitHub Actions,action 由 runner 从镜像源拉取)
|
r#"# Gitea Actions(语法兼容 GitHub Actions,action 由 runner 从镜像源拉取)
|
||||||
@ -198,15 +327,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
{setup_steps}
|
||||||
with:
|
|
||||||
version: 9
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '{node}'
|
|
||||||
cache: 'pnpm'
|
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: {build}
|
- run: {build}
|
||||||
@ -234,12 +355,15 @@ jobs:
|
|||||||
(content, "deploy.yml".to_string(), secrets)
|
(content, "deploy.yml".to_string(), secrets)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_tauri_oss_workflow(cfg: &CicdConfig) -> (String, String, Vec<String>) {
|
fn build_tauri_oss_workflow(
|
||||||
let node = cfg.node_version.as_deref().unwrap_or("20");
|
cfg: &CicdConfig,
|
||||||
|
toolchain: &WorkflowToolchain,
|
||||||
|
) -> (String, String, Vec<String>) {
|
||||||
let endpoint = cfg
|
let endpoint = cfg
|
||||||
.oss_endpoint
|
.oss_endpoint
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or("oss-cn-hangzhou.aliyuncs.com");
|
.unwrap_or("oss-cn-hangzhou.aliyuncs.com");
|
||||||
|
let setup_steps = toolchain.setup_steps();
|
||||||
|
|
||||||
let content = format!(
|
let content = format!(
|
||||||
r#"# Tauri v2 Windows 自动构建 + 签名 + 阿里云 OSS 发布(Gitea Actions)
|
r#"# Tauri v2 Windows 自动构建 + 签名 + 阿里云 OSS 发布(Gitea Actions)
|
||||||
@ -258,15 +382,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
{setup_steps}
|
||||||
with:
|
|
||||||
version: 9
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '{node}'
|
|
||||||
cache: 'pnpm'
|
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@ -373,10 +489,13 @@ jobs:
|
|||||||
(content, "release.yml".to_string(), secrets)
|
(content, "release.yml".to_string(), secrets)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_pr_ci_workflow(cfg: &CicdConfig) -> (String, String, Vec<String>) {
|
fn build_pr_ci_workflow(
|
||||||
|
cfg: &CicdConfig,
|
||||||
|
toolchain: &WorkflowToolchain,
|
||||||
|
) -> (String, String, Vec<String>) {
|
||||||
let branch = &cfg.trigger_branch;
|
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 build_cmd = cfg.build_command.as_deref().unwrap_or("pnpm build");
|
||||||
|
let setup_steps = toolchain.setup_steps();
|
||||||
|
|
||||||
let content = format!(
|
let content = format!(
|
||||||
r#"# PR 代码质量检查(Gitea Actions)
|
r#"# PR 代码质量检查(Gitea Actions)
|
||||||
@ -393,15 +512,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
{setup_steps}
|
||||||
with:
|
|
||||||
version: 9
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '{node}'
|
|
||||||
cache: 'pnpm'
|
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: {build_cmd}
|
- run: {build_cmd}
|
||||||
@ -515,6 +626,7 @@ diff:
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn cfg(config_type: &str) -> CicdConfig {
|
fn cfg(config_type: &str) -> CicdConfig {
|
||||||
CicdConfig {
|
CicdConfig {
|
||||||
@ -533,9 +645,30 @@ 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]
|
#[test]
|
||||||
fn web_ssh_template_has_no_github_residue() {
|
fn web_ssh_template_has_no_github_residue() {
|
||||||
let (content, filename, secrets) = build_web_ssh_workflow(&cfg("web_ssh"));
|
let (content, filename, secrets) =
|
||||||
|
build_web_ssh_workflow(&cfg("web_ssh"), &legacy_toolchain());
|
||||||
assert!(!content.contains("github.com"), "模板残留 github.com 引用");
|
assert!(!content.contains("github.com"), "模板残留 github.com 引用");
|
||||||
assert!(!content.contains("GITHUB_TOKEN"), "模板残留 GITHUB_TOKEN");
|
assert!(!content.contains("GITHUB_TOKEN"), "模板残留 GITHUB_TOKEN");
|
||||||
assert_eq!(filename, "deploy.yml");
|
assert_eq!(filename, "deploy.yml");
|
||||||
@ -544,7 +677,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tauri_oss_template_has_no_github_residue() {
|
fn tauri_oss_template_has_no_github_residue() {
|
||||||
let (content, filename, secrets) = build_tauri_oss_workflow(&cfg("tauri_oss"));
|
let (content, filename, secrets) =
|
||||||
|
build_tauri_oss_workflow(&cfg("tauri_oss"), &legacy_toolchain());
|
||||||
assert!(!content.contains("github.com"), "模板残留 github.com 引用");
|
assert!(!content.contains("github.com"), "模板残留 github.com 引用");
|
||||||
assert!(!content.contains("GITHUB_TOKEN"), "模板残留 GITHUB_TOKEN");
|
assert!(!content.contains("GITHUB_TOKEN"), "模板残留 GITHUB_TOKEN");
|
||||||
assert!(
|
assert!(
|
||||||
@ -563,7 +697,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pr_ci_template_is_correct() {
|
fn pr_ci_template_is_correct() {
|
||||||
let (content, filename, secrets) = build_pr_ci_workflow(&cfg("pr_ci"));
|
let (content, filename, secrets) = build_pr_ci_workflow(&cfg("pr_ci"), &legacy_toolchain());
|
||||||
assert!(content.contains("pull_request"), "触发器应为 pull_request");
|
assert!(content.contains("pull_request"), "触发器应为 pull_request");
|
||||||
assert!(content.contains("pnpm build"), "应含构建步骤");
|
assert!(content.contains("pnpm build"), "应含构建步骤");
|
||||||
// --if-present 只对 pnpm run 子命令有效,直接 `pnpm lint --if-present` 会把
|
// --if-present 只对 pnpm run 子命令有效,直接 `pnpm lint --if-present` 会把
|
||||||
@ -596,4 +730,75 @@ mod tests {
|
|||||||
assert!(secrets.contains(&"LJ_TOKEN".to_string()));
|
assert!(secrets.contains(&"LJ_TOKEN".to_string()));
|
||||||
assert!(secrets.contains(&"LJ_URL".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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -166,10 +166,11 @@ export function CicdModal({
|
|||||||
placeholder="main"
|
placeholder="main"
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
label="Node.js 版本"
|
label="Node.js 兼容回退版本"
|
||||||
value={form.node_version ?? ""}
|
value={form.node_version ?? ""}
|
||||||
onChange={(v) => setForm((f) => ({ ...f, node_version: v }))}
|
onChange={(v) => setForm((f) => ({ ...f, node_version: v }))}
|
||||||
placeholder="20"
|
placeholder="20"
|
||||||
|
hint="项目有 .nvmrc / .node-version 时优先使用项目声明"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Field
|
<Field
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user