refactor!: remove workshop-pipeline crate and add repology support

BREAKING CHANGE: workshop-pipeline crate removed entirely

- Remove workshop-pipeline/ (config structures, template resolution)
- Add Repology package name resolution to workshop-baker
- Add UPM system dependency collection for cross-pm resolution
- Replace AGENTS.md with concise knowledge base format
- Add crate-specific AGENTS.md files
- Fix various clippy warnings and derive Default where applicable
This commit is contained in:
Catty Steve
2026-04-04 13:24:26 +08:00
parent 2133f8b9b3
commit 418bdfb2f0
53 changed files with 609 additions and 2639 deletions
+1
View File
@@ -4,3 +4,4 @@ playground
.ruff_cache
.codeartsdoer
archived
+97 -135
View File
@@ -1,145 +1,107 @@
# Agent Guidelines for HoneyBiscuitWorkshop
# PROJECT KNOWLEDGE BASE
This repository is a monorepo of independent Rust projects using unified coding style and conventions.
**Generated:** 2026-04-04
**Commit:** 2133f8b
**Branch:** master
## Project Structure
## OVERVIEW
HoneyBiscuitWorkshop is a Rust-based CI/CD system ("蜜饼工坊") with LLM-powered pipeline auto-configuration. 9 independent Cargo crates, no workspace.
- Original projects use `workshop-{name}` naming convention (e.g., `workshop-executor`, `workshop-vault`)
- Submodules: `RustyVault` and `rust-tongsuo` are external projects
- Each project is independently built with Cargo (no workspace)
## STRUCTURE
```
./
├── workshop-baker/ # Main orchestrator (CLI + daemon)
├── workshop-agent/ # HTTP agent server
├── workshop-pipeline/ # Config library (prebake/finalize)
├── workshop-llm-detector/ # LLM repo analyzer
├── workshop-vault/ # Secret management client
├── workshop-cert/ # TLS certificate generator
├── workshop-deviceid/ # Device ID library
├── workshop-builder-native/ # Stub (unused)
├── workshop-helper-mac/ # MAC address utility
├── docs/ # mdBook documentation (zh-CN)
├── playground/ # Experimentation
└── third_party/ # Empty
```
## Build and Test Commands
## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| Baker (main) | `workshop-baker/src/{bake,prebake,engine}/` | Core pipeline logic |
| Agent server | `workshop-agent/src/` | Actix-web HTTP + TLS |
| Pipeline config | `workshop-pipeline/src/config/` | prebake/finalize YAML |
| LLM integration | `workshop-llm-detector/src/llm/` | Ollama/OpenAI clients |
| Secrets | `workshop-vault/src/` | Vault client wrapper |
## CODE MAP
| Symbol | Type | Location | Role |
|--------|------|----------|------|
| PipelineConfig | struct | workshop-pipeline/src/lib.rs:27 | Full pipeline config |
| CICDLLMHelper | struct | workshop-llm-detector/src/lib.rs:16 | LLM analysis entry |
| Engine | struct | workshop-baker/src/engine.rs | Build executor |
| VaultClient | struct | workshop-vault/src/client.rs | Secret ops |
## CONVENTIONS
### Rust
- **Edition**: 2024 (non-standard, requires Rust 1.85+)
- **Naming**: `snake_case` files/modules, `PascalCase` types, `SCREAMING_SNAKE_CASE` consts
- **Error handling**: `thiserror` + `anyhow` combo
- **Async**: `#[tokio::main]` + `tokio` with "full" features
- **Imports**: `std``external_crate``crate::module`
### Testing
- **Rust**: `#[test]` inline, `#[tokio::test]` async, `tests/*.rs` integration
- **Python**: `pytest` with `tests/integration/test_*.py` (Docker-based)
- **Benchmarks**: Criterion with `harness = false`
### Build Pipeline
- **prebake.yml**: Environment setup (docker/firecracker/baremetal)
- **bake.sh**: Build execution with `# @pipeline` decorators
- **finalize.yml**: Packaging, deployment, notifications
- **Annotations**: `@timeout()`, `@retry()`, `@parallel`, `@fallible`
## ANTI-PATTERNS (THIS PROJECT)
1. **Python in Rust project** — workshop-baker has Python tests (pytest.ini, test_prebake.py)
2. **temp/ directory** — Non-standard in Rust projects
3. **Certificates in source** — artifact workshop-cert/certs/ not excluded
4. **Deprecated projects** — workshop-executor.old, workshop-monitor.old still present
5. **Hardcoded mirrors** — Tsinghua University mirrors in configs
6. **Rust 2024 edition** — May not work with stable toolchains
## UNIQUE STYLES
- **Pipeline DSL**: Shell scripts with `# @decorator` annotations
- **Dual-mode baker**: CLI commands + daemon mode
- **LLM cookbook system**: YAML cookbooks for language-specific builds
- **Chinese docs** — Documentation primarily in zh-CN
- **User "vulcan"** — Custom build user (not root/runner)
## COMMANDS
```bash
# Build specific project
cd workshop-executor && cargo build --release
# Run all tests for a project
cargo test
# Run a single test
cargo test test_function_name
# Run tests in a specific module
cargo test module_name
# Run tests with verbose output
cargo test --verbose
# Run ignored tests
cargo test -- --ignored
# Lint with clippy
# Build
cargo build --release -p workshop-baker
# Test
cargo test -p workshop-baker
pytest tests/integration/test_prebake.py -v
# Lint
cargo clippy -- -D warnings
# Format code
cargo fmt
# Check formatting without making changes
cargo fmt --all -- --check
# Run tests with specific features (RustyVault)
cd RustyVault && cargo test --features sync_handler --verbose
# Generate coverage report
cargo install cargo-tarpaulin
cargo tarpaulin --out Lcov
cargo fmt --all
# Run baker
cargo run --bin workshop-baker -- prebake config.yml
cargo run --bin workshop-baker -- bake script.sh
# Run agent
cargo run --bin workshop-agent
```
## Code Style Guidelines
## NOTES
### Edition and Naming
- Workshop projects: Rust 2024 edition, RustyVault: Rust 2021 edition
- **Crates/Packages**: `workshop-{name}` (snake_case)
- **Modules/Files**: `snake_case` (e.g., `error.rs`, `config.rs`)
- **Structs/Enums**: `PascalCase` (e.g., `VaultError`, `CertificateBundle`)
- **Functions/Methods**: `snake_case` (e.g., `generate_device_id()`)
- **Constants**: `SCREAMING_SNAKE_CASE` (e.g., `EXIT_CODE_OK`, `VERSION`)
### Import Organization
```rust
use std::collections::HashMap;
use std::path::PathBuf;
use tokio::sync::mpsc;
use serde::{Serialize, Deserialize};
use anyhow::Result;
use log::info;
use crate::module_name::Type;
```
### Module Structure and Error Handling
```rust
mod config;
mod error;
mod handler;
pub use config::*;
pub use error::*;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum VaultError {
#[error("Vault operation failed: {0}")]
OperationFailed(String),
}
pub type Result<T> = std::result::Result<T, VaultError>;
```
### Async Patterns and Serialization
Use `tokio` with "full" features for async code:
```rust
#[tokio::main]
async fn main() {
env_logger::init();
// async code...
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artifact {
pub name: String,
#[serde(rename = "type")]
pub type_: String,
}
```
### Documentation and Testing
Add doc comments (`///`) for all public items. Place unit tests in `#[cfg(test)]` modules within source files.
Use `#[tokio::test]` for async tests, `#[ignore]` for tests requiring external resources. Use `maybe_async` crate for conditional async tests:
```rust
#[maybe_async::test(feature = "sync_handler", async(all(not(feature = "sync_handler")), tokio::test))]
async fn test_with_feature_flag() {
// test code...
}
```
### Formatting and Dependencies
Follow rustfmt settings from RustyVault:
- Max width: 120 characters
- Comment width: 120 characters
- Group imports: `StdExternalCrate`
- Imports granularity: `Crate`
- Trailing comma: `Vertical`
Common dependencies:
- `tokio` (v1.40-1.48) with "full" features
- `serde` and `serde_json` for serialization
- `thiserror` for custom errors
- `anyhow` for error propagation
- `minijinja` for templating
- `clap` for CLI parsing (derive features)
- `log` and `env_logger` for logging
### Clippy Lints
The following clippy lints are allowed in RustyVault:
- `result_large_err`, `ptr_arg`, `let_and_return`, `should_implement_trait`
- `new_without_default`, `field_reassign_with_default`, `await_holding_lock`
- `too_many_arguments`, `unnecessary_unwrap`, `collapsible_match`
- `large_enum_variant`, `unnecessary_map_or`
Use similar allowances for workshop projects when appropriate.
### Environment Variables
Use environment variable prefixes for configuration:
- `WSEXECUTOR_` for workshop-executor
- Common pattern: `env_logger::init()` at the start of `main()`
### Cursor/Copilot Rules
No Cursor or Copilot rules found in the repository.
- **Docker required** for integration tests
- **workshop-builder-native** is a stub (3-line Hello World)
- No root workspace Cargo.toml — each crate is independent
- `AGENTS.md.old` at root — legacy documentation
+1
View File
@@ -0,0 +1 @@
temp
+59
View File
@@ -0,0 +1,59 @@
# workshop-baker/src
**Baker CLI + execution engine for HoneyBiscuitWorkshop pipelines.**
## OVERVIEW
CLI tool and runtime for executing CI/CD pipelines with resource isolation, privilege dropping, and package management integration.
## STRUCTURE
```
src/
├── bake.rs # Top-level bake orchestration
├── bake/ # Script parsing and building
│ ├── parser.rs # @decorator-aware shell parser
│ ├── decorator.rs # @pipeline, @retry, @parallel, etc.
│ ├── builder.rs # Script template builder
│ └── schedule.rs # Function ordering via @after deps
├── engine/ # Execution runtime
│ ├── executor.rs # Script execution with cgroups
│ ├── cgroups.rs # Linux cgroup resource limits
│ ├── repology.rs # Package name resolution
│ ├── pm.rs # Package manager detection
│ └── upm.rs # User package manager (Nix/Guix)
├── prebake/ # Build environment setup
│ ├── config.rs # PrebakeConfig YAML schema
│ ├── stage/ # Bootstrap, DepsSystem, DepsUser, Hooks
│ └── security.rs # Privilege dropping logic
├── types/ # Shared type definitions
│ ├── repology.rs # RepologyEndpoint enum
│ ├── builderconfig.rs
│ ├── cache.rs
│ └── memsize.rs
├── monitor/ # Resource usage monitoring
│ ├── cgroups.rs # cgroup-based usage collection
│ └── jobobject.rs # Windows job objects (stub)
├── daemon.rs # Unix socket daemon (TODO: refactor)
└── lib.rs # CLI struct + module exports
```
## WHERE TO LOOK
| Task | Location |
|------|----------|
| Script parsing with @decorators | `bake/parser.rs:29` - `parse_script()` |
| Decorator types | `bake/decorator.rs` - `Decorator` enum |
| Build execution | `engine/executor.rs` - `Executor::execute_script()` |
| Resource limits | `engine/types.rs:56` - `ResourceLimits` struct |
| Prebake stages | `prebake/stage/` subdirectories |
| Package detection | `engine/pm.rs:detection()` |
| CLI entry | `lib.rs:26` - `cli::Cli` struct |
## CONVENTIONS
- **ExecutionContext**: Passed through all stages, carries task_id, username, env_vars, timeout
- **PrebakeStage**: Ordered enum controlling stage execution sequence (Bootstrap -> Ready)
- **Decorator**: Attached to shell functions via `# @name(args)` comments above function declarations
- **Engine**: Lightweight wrapper around cgroup manager for resource isolation
- **Dual-mode**: CLI commands (prebake, bake, finalize) vs daemon mode via Unix socket
+3 -3
View File
@@ -60,7 +60,7 @@ pub async fn bake(
let script = builder::build_script(
"bake".to_string(),
script_body,
&bake_base_path,
bake_base_path,
&workspace,
None,
use_template,
@@ -72,7 +72,7 @@ pub async fn bake(
let script = builder::build_script(
func.name,
func.body,
&bake_base_path,
bake_base_path,
&workspace,
None,
use_template,
@@ -104,6 +104,6 @@ fn privileged(prebake_config: &PrebakeConfig) -> bool {
return true;
}
}
let drop_after = get_drop_after(&prebake_config.security).unwrap_or(PrebakeStage::default());
let drop_after = get_drop_after(&prebake_config.security).unwrap_or_default();
PrebakeStage::Never == drop_after
}
+2 -2
View File
@@ -8,7 +8,7 @@ use tempfile::NamedTempFile;
use crate::error::BakeError;
/// Template for use_template=false, which just executes the main body without any wrapping.
const TRIVIAL_TEMPLATE: &'static str = "{{ main }}";
const TRIVIAL_TEMPLATE: &str = "{{ main }}";
pub fn build_script(
name: String,
@@ -23,7 +23,7 @@ pub fn build_script(
let target = target_dir.join(filename);
let bake_base_content = if use_template {
std::fs::read_to_string(&bake_base).map_err(|e| BakeError::ScriptGenerationFailed {
std::fs::read_to_string(bake_base).map_err(|e| BakeError::ScriptGenerationFailed {
script: bake_base.display().to_string(),
reason: e.to_string(),
})?
+2 -1
View File
@@ -6,8 +6,9 @@
pub mod cgroups;
pub mod executor;
pub mod pm;
pub mod upm;
pub mod repology;
pub mod types;
pub mod upm;
// Re-export commonly used types for convenience
pub use types::{
+1 -2
View File
@@ -32,7 +32,7 @@ impl CgroupManager {
pub fn set_cpu_weight(&self, weight: u64) -> io::Result<()> {
let cpu_weight_path = self.path.join("cpu.weight");
let weight = weight.max(1).min(10000);
let weight = weight.clamp(1, 10000);
fs::write(&cpu_weight_path, weight.to_string())?;
Ok(())
}
@@ -51,7 +51,6 @@ impl CgroupManager {
pub fn add_process(&self, pid: u32) -> io::Result<()> {
let cgroup_procs_path = self.path.join("cgroup.procs");
let mut file = fs::OpenOptions::new()
.write(true)
.append(true)
.open(&cgroup_procs_path)?;
+46
View File
@@ -0,0 +1,46 @@
pub mod local;
use crate::types::repology::RepologyEndpoint;
use std::collections::HashMap;
#[derive(Debug)]
pub enum RepologyError {
Unimplemented,
RemoteNotSupported,
}
pub async fn resolve_package_names(
packages: &[String],
mapped: bool,
endpoint: &RepologyEndpoint,
target_pm: &str,
) -> Result<HashMap<String, Vec<String>>, RepologyError> {
let mut result = HashMap::new();
if packages.is_empty() {
return Ok(result);
}
if mapped {
result.insert(target_pm.to_string(), packages.to_vec());
return Ok(result);
}
match endpoint {
RepologyEndpoint::Local | RepologyEndpoint::Default => {
let resolved = local::resolve(packages, target_pm).await;
if !resolved.is_empty() {
result.insert(target_pm.to_string(), resolved);
}
}
RepologyEndpoint::None => {
result.insert(target_pm.to_string(), packages.to_vec());
}
RepologyEndpoint::Disabled => {}
RepologyEndpoint::Remote | RepologyEndpoint::Server => {
return Err(RepologyError::Unimplemented);
}
}
Ok(result)
}
@@ -0,0 +1,99 @@
use std::collections::HashMap;
type PmName = &'static str;
type PkgName = &'static str;
fn pkg(packages: &[(PmName, PkgName)]) -> Vec<(PmName, PkgName)> {
packages.to_vec()
}
lazy_static::lazy_static! {
static ref MAPPING: HashMap<PkgName, Vec<(PmName, PkgName)>> = {
let mut m = HashMap::new();
m.insert("rustup", pkg(&[
("pacman", "rustup"),
("apt", "rustup"),
("dnf", "rustup"),
]));
m.insert("cargo", pkg(&[
("pacman", "cargo"),
("apt", "cargo"),
("dnf", "cargo"),
]));
m.insert("rustc", pkg(&[
("pacman", "rustc"),
("apt", "rustc"),
("dnf", "rustc"),
]));
m.insert("go", pkg(&[
("pacman", "go"),
("apt", "golang-go"),
("dnf", "golang"),
]));
m.insert("gofmt", pkg(&[
("pacman", "go"),
("apt", "golang-go"),
("dnf", "golang"),
]));
m.insert("nodejs", pkg(&[
("pacman", "nodejs"),
("apt", "nodejs"),
("dnf", "nodejs"),
]));
m.insert("npm", pkg(&[
("pacman", "npm"),
("apt", "npm"),
("dnf", "npm"),
]));
m.insert("yarn", pkg(&[
("pacman", "yarn"),
("apt", "yarn"),
("dnf", "yarn"),
]));
m.insert("pnpm", pkg(&[
("pacman", "pnpm"),
("apt", "pnpm"),
("dnf", "pnpm"),
]));
m.insert("python", pkg(&[
("pacman", "python"),
("apt", "python3"),
("dnf", "python3"),
]));
m.insert("pip", pkg(&[
("pacman", "python-pip"),
("apt", "python3-pip"),
("dnf", "python3-pip"),
]));
m
};
}
pub async fn resolve(
packages: &[String],
target_pm: &str,
) -> Vec<String> {
let mut result = Vec::new();
for pkg in packages {
if let Some(mappings) = MAPPING.get(pkg.as_str()) {
for (pm, name) in mappings {
if *pm == target_pm {
result.push(name.to_string());
}
}
}
}
result
}
+7 -4
View File
@@ -6,11 +6,14 @@ mod custom;
use crate::{ExecutionContext, error::DependencyError};
pub struct UPMSysDeps {
/// Packages to be installed
packages: Vec<String>,
pub packages: Vec<String>,
pub mapped: bool,
}
/// Whether package name is mapped to distro
mapped: bool,
impl UPMSysDeps {
pub fn new(packages: Vec<String>, mapped: bool) -> Self {
Self { packages, mapped }
}
}
#[async_trait]
+2 -5
View File
@@ -24,10 +24,7 @@ impl UserPackageManager for Custom {
}
fn system_dependency(&self, _config: &serde_yaml::Value) -> super::UPMSysDeps {
UPMSysDeps {
packages: vec![],
mapped: true,
}
UPMSysDeps::new(vec![], true)
}
async fn main(
@@ -52,7 +49,7 @@ impl UserPackageManager for Custom {
};
log::info!("Executing custom command #{}: {}.", index, cmd.name);
let result = engine
.execute_command_with_delta(&cmd.command, &ctx, &event_tx, &deltactx)
.execute_command_with_delta(&cmd.command, ctx, event_tx, &deltactx)
.await;
if let Err(e) = result {
log::error!("Custom command {} failed: {:?}", index, e);
+2 -2
View File
@@ -57,7 +57,7 @@ pub async fn monitor(
_timestamp: default_internal_timestamp(),
};
loop {
let usage: UsageStats = cgroups::get_usage(&name, prev_usage)?; // TODO: 多系统支持?
let usage: UsageStats = cgroups::get_usage(name, prev_usage)?; // TODO: 多系统支持?
prev_usage = usage.clone();
// dbg!(&usage);
let usage_info = ResourceUsageInfo {
@@ -83,7 +83,7 @@ async fn send_usage_info<S>(stream: &mut S, usage_json: &str) -> anyhow::Result<
where
S: tokio::io::AsyncWrite + Unpin,
{
stream.write(usage_json.as_bytes()).await?;
stream.write_all(usage_json.as_bytes()).await?;
stream.write_all(b"\n").await?;
Ok(())
}
+1 -1
View File
@@ -23,7 +23,7 @@ pub fn get_usage(
let mut process_count: u64 = 0;
if let Some(cpu) = cgroup.controller_of::<CpuController>() {
let stats = parse_cpustat(&cpu.cpu().stat.as_str())?;
let stats = parse_cpustat(cpu.cpu().stat.as_str())?;
cpu_usage_us = stats.usage_usec;
} else {
log::warn!("Failed to fetch CPU usage info!");
+69 -15
View File
@@ -25,12 +25,12 @@ pub mod env;
pub mod security;
pub mod stage;
use std::path::Path;
use crate::{
cli::Cli,
error::PrebakeError,
prebake::{security::get_drop_after, stage::PrebakeStage},
};
use std::path::Path;
pub use config::PrebakeConfig;
pub use security::prebake_drop_privilege;
@@ -115,11 +115,11 @@ pub async fn prebake(
};
// Wait for bakerd to finish Fetch-1
let stage = stage.unwrap_or(PrebakeStage::Bootstrap);
if let Some(env) = &prebake.envvars {
if let Some(prebake) = &env.prebake {
if let Some(env) = &prebake.envvars
&& let Some(prebake) = &env.prebake
{
ctx.env_vars.extend(prebake.clone());
}
}
// Bootstrap stage
@@ -145,24 +145,78 @@ pub async fn prebake(
}
if let Some(dependencies) = prebake.dependencies {
// DepsSystem stage
let endpoint = dependencies
.config
.as_ref()
.map(|c| c.repology_endpoint)
.unwrap_or(crate::types::repology::RepologyEndpoint::Default);
let osinfo = os_info::get();
let detected_pm = crate::engine::pm::detect(&osinfo).map(|pm| pm.name().to_string());
let upm_sys_deps = if let Some(user) = &dependencies.user {
if stage <= PrebakeStage::DepsSystem {
log::info!("Running PBStage: DepsSystem");
prebake_drop_privilege(PrebakeStage::DepsSystem, dropafter, target_user, &mut ctx)?;
ctx.task_id = format!("{}-{}-depssystem", cli.pipeline, cli.build_id);
if let Some(system) = dependencies.system {
stage::depssystem::depssystem(&system, &ctx, None).await?;
stage::depsuser::collect_upm_sysdeps(user, &ctx, &endpoint).await?
} else {
stage::depsuser::CollectedUPMdeps::default()
}
} else {
stage::depsuser::CollectedUPMdeps::default()
};
let mut system_config = dependencies.system.clone().unwrap_or_default();
if !upm_sys_deps.deps.is_empty() {
if let Some(pm_name) = &detected_pm {
for (upm_name, sys_deps) in &upm_sys_deps.deps {
let resolved = crate::engine::repology::resolve_package_names(
&sys_deps.packages,
sys_deps.mapped,
&endpoint,
pm_name,
)
.await
.map_err(|e| anyhow::anyhow!("Repology error: {:?}", e))?;
if !resolved.is_empty() {
log::info!(
"UPM {} resolved to system packages: {:?}",
upm_name,
resolved
);
}
for (target_pm, packages) in &resolved {
system_config
.entry(target_pm.clone())
.or_insert_with(|| config::SystemDependency {
packages: Vec::new(),
repositories: None,
mirror: None,
})
.packages
.extend(packages.iter().cloned());
}
}
} else {
log::warn!("No package manager detected, cannot resolve UPM system dependencies");
}
}
// DepsUser stage
if stage <= PrebakeStage::DepsUser {
if !system_config.is_empty() && stage <= PrebakeStage::DepsSystem {
log::info!("Running PBStage: DepsSystem");
prebake_drop_privilege(PrebakeStage::DepsSystem, dropafter, target_user, &mut ctx)?;
ctx.task_id = format!("{}-{}-depssystem", cli.pipeline, cli.build_id);
stage::depssystem::depssystem(&system_config, &ctx, None).await?;
}
if let Some(user) = dependencies.user
&& stage <= PrebakeStage::DepsUser
{
log::info!("Running PBStage: DepsUser");
prebake_drop_privilege(PrebakeStage::DepsUser, dropafter, target_user, &mut ctx)?;
ctx.task_id = format!("{}-{}-depsuser", cli.pipeline, cli.build_id);
if let Some(user) = dependencies.user {
stage::depsuser::depsuser(&user, &ctx, None).await?;
}
stage::depsuser::depsuser(&user, &ctx, None, &endpoint).await?;
}
}
+9
View File
@@ -11,6 +11,7 @@ use crate::types::builderconfig::{
use crate::types::cache::{CacheDirectory, CacheStrategy};
use crate::types::command::CustomCommand;
use crate::types::memsize::MemSize;
use crate::types::repology::RepologyEndpoint;
pub const VERSION_REQUIREMENT: &str = "^1.0";
@@ -230,6 +231,14 @@ pub struct Dependencies {
pub system: Option<HashMap<String, SystemDependency>>,
#[serde(default)]
pub user: Option<HashMap<String, Value>>,
#[serde(default)]
pub config: Option<DependenciesConfig>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DependenciesConfig {
#[serde(default)]
pub repology_endpoint: RepologyEndpoint,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
+1 -2
View File
@@ -182,14 +182,13 @@ pub fn prebake_drop_privilege(
if stage_current > stage_expected {
let current_uid = unsafe { libc::geteuid() };
let target_uid = lookup_uid_by_name(user).map_err(|e| {
let target_uid = lookup_uid_by_name(user).inspect_err(|_e| {
log::error!(
"Cannot drop privileges: target user '{}' does not exist at stage {}",
user,
stage_current
);
log::error!("Aborting pipeline.");
e
})?;
if current_uid == target_uid {
+2 -7
View File
@@ -6,11 +6,12 @@ pub mod hook;
pub mod depssystem;
pub mod depsuser;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum PrebakeStage {
Init,
Bootstrap,
EarlyHook,
#[default]
DepsSystem,
DepsUser,
LateHook,
@@ -50,9 +51,3 @@ impl FromStr for PrebakeStage {
}
}
}
impl Default for PrebakeStage {
fn default() -> Self {
PrebakeStage::DepsSystem
}
}
+12 -9
View File
@@ -9,11 +9,11 @@
//! and the target operating system, supporting multiple package managers.
use crate::ExecutionContext;
use crate::constant::BOOTSTRAP_SCRIPT_PATH;
use crate::engine::{Engine, EventSender, pm};
use crate::error::BootstrapError;
use crate::prebake::config::Bootstrap;
use crate::prebake::config::PrebakeConfig;
use crate::constant::BOOTSTRAP_SCRIPT_PATH;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
@@ -54,7 +54,7 @@ pub async fn bootstrap(
let bootstrap_config = config.bootstrap.clone().unwrap_or_default();
if bootstrap_config.custom.is_none() {
let script = generate_bootstrap(&bootstrap_config, &config, &osinfo)?;
let script = generate_bootstrap(&bootstrap_config, config, osinfo)?;
if ctx.dry_run {
log::info!("[DRY_RUN] Generated bootstrap script:\n{}", script);
return Ok(());
@@ -74,9 +74,9 @@ pub async fn bootstrap(
}
engine
.execute_script(&bootstrap_path, ctx, &event_tx)
.execute_script(&bootstrap_path, ctx, event_tx)
.await
.map_err(|e| BootstrapError::ExecutionError(e))?;
.map_err(BootstrapError::ExecutionError)?;
Ok(())
}
@@ -112,7 +112,7 @@ pub async fn bootstrap(
fn generate_bootstrap(
bootstrap_config: &Bootstrap,
config: &PrebakeConfig,
osinfo: &os_info::Info
osinfo: &os_info::Info,
) -> Result<String, BootstrapError> {
let mut script = String::new();
// Generate header
@@ -207,7 +207,10 @@ fn generate_bootstrap(
///
/// Returns `Some(Box<dyn PackageManager>)` if a suitable package manager is found,
/// or `None` if no package manager is configured or detectable.
fn get_package_manager(config: &PrebakeConfig, osinfo: &os_info::Info) -> Option<Box<dyn pm::PackageManager>> {
fn get_package_manager(
config: &PrebakeConfig,
osinfo: &os_info::Info,
) -> Option<Box<dyn pm::PackageManager>> {
config
.dependencies
.as_ref()
@@ -225,15 +228,15 @@ fn get_package_manager(config: &PrebakeConfig, osinfo: &os_info::Info) -> Option
}
}
if let Some((name, _)) = sysdeps.iter().next() {
if let Some(pm) = pm::select(name) {
if let Some((name, _)) = sysdeps.iter().next()
&& let Some(pm) = pm::select(name)
{
log::warn!(
"Using package manager: {} (first configured, not detected)",
name
);
return Some(pm);
}
}
log::warn!("No valid package manager found in configuration");
None
@@ -42,7 +42,7 @@ pub async fn depssystem(
let cmd = package_manager.change_mirror(mirror, Some(&osinfo));
log::debug!("Executing: {:?}", &cmd);
engine
.execute(cmd, &ctx, &event_tx)
.execute(cmd, ctx, &event_tx)
.await
.map_err(|e| DependencyError::StageFailed {
stage: "MirrorChange",
@@ -59,7 +59,7 @@ pub async fn depssystem(
let cmds = package_manager.add_repository(repos, Some(&osinfo));
for cmd in cmds {
log::debug!("Executing: {:?}", &cmd);
engine.execute(cmd, &ctx, &event_tx).await.map_err(|e| {
engine.execute(cmd, ctx, &event_tx).await.map_err(|e| {
DependencyError::StageFailed {
stage: "CustomRepoAdd",
source: e,
@@ -74,7 +74,7 @@ pub async fn depssystem(
let cmd = package_manager.update();
log::debug!("Executing: {:?}", &cmd);
engine
.execute(cmd, &ctx, &event_tx)
.execute(cmd, ctx, &event_tx)
.await
.map_err(|e| DependencyError::StageFailed {
stage: "UpdateIndex",
@@ -86,7 +86,7 @@ pub async fn depssystem(
if !pm_config.packages.is_empty() {
log::info!("Installing {} packages", pm_config.packages.len());
engine
.install_dependency(&pm_config.packages, &*package_manager, &ctx, &event_tx)
.install_dependency(&pm_config.packages, &*package_manager, ctx, &event_tx)
.await
.map_err(|e| DependencyError::StageFailed {
stage: "InstallPackage",
+50 -1
View File
@@ -3,12 +3,60 @@ use serde_yaml::Value;
use crate::ExecutionContext;
use crate::engine::{EventSender, upm};
use crate::error::DependencyError;
use crate::types::repology::RepologyEndpoint;
use std::collections::HashMap;
#[derive(Default)]
pub struct CollectedUPMdeps {
pub deps: Vec<(String, upm::UPMSysDeps)>,
}
impl CollectedUPMdeps {
pub fn extend(&mut self, other: Self) {
self.deps.extend(other.deps);
}
}
pub async fn collect_upm_sysdeps(
config: &HashMap<String, Value>,
_ctx: &ExecutionContext,
endpoint: &RepologyEndpoint,
) -> Result<CollectedUPMdeps, DependencyError> {
let mut collected = CollectedUPMdeps::default();
if *endpoint == RepologyEndpoint::Disabled {
return Ok(collected);
}
for (name, cfg) in config {
let package_manager = match upm::select(name) {
Some(pm) => pm,
None => {
log::warn!("Unknown user package manager: {}, skipping sysdep collection", name);
continue;
}
};
let sys_deps = package_manager.system_dependency(cfg);
if !sys_deps.packages.is_empty() {
log::info!(
"UPM {} system deps: {:?} (mapped: {})",
name,
sys_deps.packages,
sys_deps.mapped
);
collected.deps.push((name.clone(), sys_deps));
}
}
Ok(collected)
}
pub async fn depsuser(
config: &HashMap<String, Value>,
ctx: &ExecutionContext,
event_tx: EventSender,
_endpoint: &RepologyEndpoint,
) -> Result<(), DependencyError> {
for (name, cfg) in config {
let package_manager = match upm::select(name) {
@@ -18,9 +66,10 @@ pub async fn depsuser(
return Err(DependencyError::UnknownManager(name.to_string()));
}
};
log::info!("Running user package manager: {}", name);
log::info!("Running user package manager: {}", name);
package_manager.main(cfg, ctx, &event_tx).await?;
}
Ok(())
}
+1 -1
View File
@@ -75,7 +75,7 @@ pub async fn hook(
};
log::info!("Executing hook #{}: {}.", index, hook_cmd.name);
let result = engine
.execute_command_with_delta(&hook_cmd.command, &ctx, &event_tx, &deltactx)
.execute_command_with_delta(&hook_cmd.command, ctx, &event_tx, &deltactx)
.await;
if let Err(e) = result {
log::error!("Hook {} failed: {:?}", index, e);
+3 -2
View File
@@ -1,5 +1,6 @@
pub mod memsize;
pub mod architecture;
pub mod builderconfig;
pub mod command;
pub mod cache;
pub mod command;
pub mod memsize;
pub mod repology;
+2 -7
View File
@@ -1,7 +1,8 @@
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub enum Architecture {
#[default]
NoArch,
X86_64,
Amd64, // Same as X86_64, normalized when processing
@@ -50,9 +51,3 @@ impl FromStr for Architecture {
}
}
}
impl Default for Architecture {
fn default() -> Self {
Architecture::NoArch
}
}
+7 -3
View File
@@ -48,7 +48,6 @@ impl MemSize {
pub const fn from_gib(gib: u64) -> Self {
Self(gib * 1024 * 1024 * 1024)
}
}
impl<'de> Deserialize<'de> for MemSize {
@@ -129,14 +128,19 @@ pub fn parse_mem_size(s: &str) -> Result<MemSize, String> {
let (num_str, unit_raw) = split_number_and_unit(s)?;
// Parse the numeric part
let num: f64 = num_str.parse().map_err(|e: ParseFloatError| e.to_string())?;
let num: f64 = num_str
.parse()
.map_err(|e: ParseFloatError| e.to_string())?;
// Normalize unit: remove trailing 'B' if present, but preserve case for error detection
let unit = if unit_raw.ends_with('B') || unit_raw.ends_with('b') {
let without_b = &unit_raw[..unit_raw.len() - 1];
// Check if the original unit ends with lowercase 'b' (bits) before stripping
if unit_raw.ends_with('b') && !unit_raw.ends_with('B') {
return Err(format!("bit unit 'b' is not allowed, use 'B' for bytes: {}", unit_raw));
return Err(format!(
"bit unit 'b' is not allowed, use 'B' for bytes: {}",
unit_raw
));
}
without_b
} else {
+66
View File
@@ -0,0 +1,66 @@
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub enum RepologyEndpoint {
#[default]
Default,
Disabled,
None,
Local,
Remote,
Server,
}
impl<'de> serde::Deserialize<'de> for RepologyEndpoint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, Visitor};
use std::fmt;
struct RepologyEndpointVisitor;
impl<'de> Visitor<'de> for RepologyEndpointVisitor {
type Value = RepologyEndpoint;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("disabled, none, local, remote, server, or default")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match v {
"disabled" => Ok(RepologyEndpoint::Disabled),
"none" => Ok(RepologyEndpoint::None),
"local" => Ok(RepologyEndpoint::Local),
"remote" => Ok(RepologyEndpoint::Remote),
"server" => Ok(RepologyEndpoint::Server),
"default" | "" => Ok(RepologyEndpoint::Default),
_ => Err(de::Error::unknown_variant(
v,
&["disabled", "none", "local", "remote", "server", "default"],
)),
}
}
}
deserializer.deserialize_str(RepologyEndpointVisitor)
}
}
impl serde::Serialize for RepologyEndpoint {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
RepologyEndpoint::Default => serializer.serialize_str("default"),
RepologyEndpoint::Disabled => serializer.serialize_str("disabled"),
RepologyEndpoint::None => serializer.serialize_str("none"),
RepologyEndpoint::Local => serializer.serialize_str("local"),
RepologyEndpoint::Remote => serializer.serialize_str("remote"),
RepologyEndpoint::Server => serializer.serialize_str("server"),
}
}
}
+40
View File
@@ -0,0 +1,40 @@
# workshop-llm-detector
## OVERVIEW
LLM-powered repository analyzer that generates pipeline configurations by combining AI analysis with language-specific cookbook templates.
## STRUCTURE
```
src/
├── llm/ # LLM client abstraction
│ ├── client.rs # LLMClient trait (async_trait)
│ ├── ollama.rs # OllamaClient implementation
│ └── openai.rs # OpenAIClient implementation
├── cookbook/ # Language-specific build configs
│ └── mod.rs # CookbookManager + Cookbook struct
├── llm_output.rs # LLMOutput parsed response struct
└── lib.rs # CICDLLMHelper entry point
```
## WHERE TO LOOK
| Symbol | Type | Location | Role |
|--------|------|----------|------|
| CICDLLMHelper | struct | lib.rs:16 | Main entry point; analyze_repository() orchestrates LLM + cookbook merge |
| LLMClient | trait | llm/client.rs:5 | Async trait for generate(prompt) -> Result<String> |
| OllamaClient | struct | llm/ollama.rs:8 | Ollama local LLM adapter |
| OpenAIClient | struct | llm/openai.rs:8 | OpenAI API adapter |
| CookbookManager | struct | cookbook/mod.rs:26 | Loads .cookbook YAML files; get_cookbook(project_type) |
| Cookbook | struct | cookbook/mod.rs:7 | Language-specific deps, env, artifacts template |
| LLMOutput | struct | llm_output.rs:5 | Parsed YAML from LLM (project_type, dependencies, build_steps, artifacts) |
| AnalysisResult | struct | lib.rs:157 | Merged LLM + cookbook output |
## CONVENTIONS
- **Async trait**: Uses `async_trait::async_trait` macro (not native async fn in traits)
- **Cookbook files**: `*.cookbook.yaml` in cookbook directory; maps project_type -> Cookbook
- **LLM prompt**: Builds YAML requiring project_type, dependencies, build_steps, artifacts, environment fields
- **Output templates**: Writes prebake.tmpl.yaml, finalize.tmpl.yaml, build.sh via serde_yaml
- **Error handling**: thiserror for CookbookError, anyhow for runtime errors
-375
View File
@@ -1,375 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "env_filter"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "indexmap"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-static"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "syn"
version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "workshop-pipeline"
version = "0.1.0"
dependencies = [
"env_logger",
"lazy_static",
"log",
"regex",
"serde",
"serde_json",
"serde_yaml",
"thiserror",
]
-16
View File
@@ -1,16 +0,0 @@
[package]
name = "workshop-pipeline"
version = "0.1.0"
edition = "2024"
[dependencies]
lazy_static = "1.5.0"
log = "0.4.28"
regex = "1.12.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
serde_yaml = "0.9.34"
thiserror = "2.0.17"
[dev-dependencies]
env_logger = "0.11.8"
@@ -1,217 +0,0 @@
#!/bin/bash
# .workshop/bake.sh
# 构建阶段脚本 - 使用标准的bash语法
set -euo pipefail # 严格模式:错误退出、未定义变量检查、管道错误检查
# 切换到工作目录
cd "$WORKSPACE_DIR"
# 创建必要的目录
mkdir -p target
mkdir -p "$CARGO_REGISTRY"
mkdir -p "$CARGO_GIT"
# 配置Cargo使用缓存目录
export CARGO_HOME="$BUILD_CACHE_DIR/cargo"
export CARGO_TARGET_DIR="$WORKSPACE_DIR/target"
# 构建前检查
log "Starting build process"
log "Build ID: $BUILD_ID"
log "Commit: $COMMIT_SHA"
log "Branch: $BRANCH_NAME"
log "Rust version: $RUST_VERSION"
# 检查必要的工具
check_requirements() {
log "Checking build requirements..."
if ! command -v cargo &> /dev/null; then
log_error "Cargo not found. Please ensure Rust is installed."
exit 1
fi
if ! command -v git &> /dev/null; then
log_error "Git not found. Please install git."
exit 1
fi
log "All requirements satisfied"
}
# 代码质量检查
run_lints() {
log "Running code quality checks..."
# Rustfmt检查代码格式
if command -v rustfmt &> /dev/null; then
log "Checking code formatting with rustfmt..."
cargo fmt -- --check || {
log_warn "Code formatting issues found"
# 非致命错误,继续构建
}
fi
# Clippy静态分析
log "Running Clippy..."
cargo clippy -- -D warnings || {
log_warn "Clippy warnings found"
# 非致命错误,继续构建
}
# 安全检查
if command -v cargo-audit &> /dev/null; then
log "Checking for security vulnerabilities..."
cargo audit || {
log_warn "Security vulnerabilities found"
# 非致命错误,继续构建
}
fi
}
# 运行测试
run_tests() {
log "Running tests..."
# 单元测试
cargo test --lib --bins --tests || {
log_error "Unit tests failed"
exit 1
}
# 集成测试(如果存在)
if [ -d "tests" ] && [ -n "$(find tests -name '*.rs' -print -quit)" ]; then
cargo test --tests || {
log_error "Integration tests failed"
exit 1
}
fi
# 文档测试
cargo test --doc || {
log_warn "Documentation tests failed"
# 文档测试失败不阻断构建
}
}
# 构建项目
build_project() {
log "Building project..."
case "$BUILD_TYPE" in
debug)
log "Building in debug mode"
cargo build
;;
release)
log "Building in release mode"
cargo build --release
;;
*)
log_error "Unknown build type: $BUILD_TYPE"
exit 1
;;
esac
# 检查构建是否成功
if [ $? -eq 0 ]; then
log "Build completed successfully"
else
log_error "Build failed"
exit 1
fi
}
# 生成文档
generate_docs() {
log "Generating documentation..."
if [ "$BUILD_TYPE" = "release" ]; then
cargo doc --no-deps --release || {
log_warn "Documentation generation failed"
# 文档生成失败不阻断构建
}
else
cargo doc --no-deps || {
log_warn "Documentation generation failed"
# 文档生成失败不阻断构建
}
fi
}
# 创建构建产物清单
create_artifact_manifest() {
log "Creating artifact manifest..."
local manifest_file="$WORKSPACE_DIR/artifacts.json"
cat > "$manifest_file" << EOF
{
"build_id": "$BUILD_ID",
"commit_sha": "$COMMIT_SHA",
"branch": "$BRANCH_NAME",
"build_type": "$BUILD_TYPE",
"timestamp": "$(date -Iseconds)",
"artifacts": [
{
"name": "binary",
"path": "target/$BUILD_TYPE/$(basename "$WORKSPACE_DIR")",
"type": "executable"
},
{
"name": "documentation",
"path": "target/doc",
"type": "directory"
}
]
}
EOF
log "Artifact manifest created: $manifest_file"
}
# 构建后清理
cleanup() {
log "Performing build cleanup..."
# 清理临时文件
find . -name "*.tmp" -delete
find . -name "*.log" -delete
# 保留构建缓存
log "Build cache preserved in: $BUILD_CACHE_DIR"
}
# 主构建流程
main() {
log "Starting bake phase"
check_requirements
run_lints
run_tests
build_project
generate_docs
create_artifact_manifest
cleanup
log "Bake phase completed successfully"
# 输出构建摘要
echo "=========================================="
log_info "Build Summary"
echo "------------------------------------------"
log_info "Build ID: $BUILD_ID"
log_info "Status: SUCCESS"
log_info "Artifacts:"
log_info " - Binary: target/$BUILD_TYPE/$(basename "$WORKSPACE_DIR")"
log_info " - Documentation: target/doc/"
log_info " - Manifest: artifacts.json"
echo "=========================================="
}
# 信号处理
trap 'log_error "Build interrupted"; exit 130' INT TERM
# 执行主函数
main "$@"
@@ -1,29 +0,0 @@
#!/bin/sh
# Color for output
# 颜色定义用于输出
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No color, 无颜色
# logging function
# 日志辅助函数
bake_log() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"
}
bake_log_warn() {
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*${NC}"
}
bake_log_error() {
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*${NC}"
}
bake_log_info() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*${NC}"
}
{{ main }}
@@ -1,270 +0,0 @@
# finalize.yaml.tmpl
# 版本号
version: "1.0"
# 产物定义
artifacts:
# 主要构建产物
primary:
- name: "application-binary"
type: "executable"
path: "target/release/{{.project_name}}"
platform: "linux/x86_64"
permissions: "755"
description: "Main application binary"
- name: "static-assets"
type: "directory"
path: "static"
include_patterns:
- "**/*.css"
- "**/*.js"
- "**/*.png"
- "**/*.jpg"
exclude_patterns:
- "**/*.test.*"
- "**/node_modules/**"
# 辅助产物
secondary:
- name: "documentation"
type: "directory"
path: "target/doc"
compress: true
format: "tar.gz"
- name: "build-logs"
type: "file"
path: "build.log"
retention_days: 30
- name: "test-results"
type: "directory"
path: "test-results"
format: "junit" # 支持junit, tap, custom
# 打包配置
packaging:
# 主要包配置
primary_package:
format: "tar.gz" # tar.gz, zip, docker, oci
output: "{{.project_name}}-{{.version}}-{{.platform}}.{{.format}}"
include:
- "artifacts/application-binary"
- "artifacts/static-assets"
- "LICENSE"
- "README.md"
exclude:
- "**/*.log"
- "**/*.tmp"
# Docker镜像配置
docker:
enabled: true
registry: "registry.example.com"
repository: "{{.project_name}}"
tags:
- "{{.version}}"
- "latest"
- "{{.branch}}"
dockerfile: "Dockerfile.final"
build_args:
- "VERSION={{.version}}"
- "BUILD_ID={{.build_id}}"
labels:
org.label-schema.version: "{{.version}}"
org.label-schema.build-date: "{{.build_date}}"
org.label-schema.vcs-ref: "{{.commit_sha}}"
# 发布配置
distribution:
# 本地存储
local:
directory: "/artifacts/{{.project_name}}/{{.version}}"
retention_days: 90
# 对象存储
s3:
enabled: true
bucket: "my-artifacts"
prefix: "{{.project_name}}/{{.version}}"
region: "us-east-1"
acl: "private"
storage_class: "STANDARD"
# 包管理器仓库
package_registries:
crates_io:
enabled: true
token: "{{env.CRATES_IO_TOKEN}}"
npm:
enabled: false
registry: "https://registry.npmjs.org/"
token: "{{env.NPM_TOKEN}}"
docker_registry:
enabled: true
registry: "registry.example.com"
username: "{{env.DOCKER_USERNAME}}"
password: "{{env.DOCKER_PASSWORD}}"
# 质量门禁
quality_gates:
# 测试覆盖率要求
# 部署配置
deployment:
# 环境定义
environments:
staging:
type: "kubernetes"
namespace: "staging"
cluster: "staging-cluster"
manifests:
- "k8s/deployment.yaml"
- "k8s/service.yaml"
health_check:
endpoint: "/health"
timeout: 30
interval: 10
production:
type: "kubernetes"
namespace: "production"
cluster: "production-cluster"
manual_approval: true
rollout_strategy: "blue-green"
manifests:
- "k8s/deployment.yaml"
# 部署策略
strategy:
type: "rolling" # rolling, blue-green, canary
max_unavailable: "25%"
max_surge: "25%"
wait_timeout: 300
# 通知配置
notifications:
# 成功通知
on_success:
- type: "slack"
webhook: "{{env.SLACK_WEBHOOK}}"
channel: "#builds"
message: "Build {{.build_id}} succeeded for {{.project_name}}"
- type: "email"
to:
- "team@example.com"
subject: "Build Success: {{.project_name}} v{{.version}}"
message: "Build {{.build_id}} succeeded for {{.project_name}} v{{.version}}"
template: "success_email.html"
# 失败通知
on_failure:
- type: "slack"
webhook: "{{env.SLACK_WEBHOOK}}"
channel: "#build-alerts"
message: "🚨 Build {{.build_id}} failed for {{.project_name}}"
- type: "pagerduty"
service_key: "{{env.PAGERDUTY_KEY}}"
message: "Build {{.build_id}} failed for {{.project_name}} v{{.version}}"
severity: "error"
# 部署通知
on_deploy:
- type: "webhook"
url: "https://api.example.com/deployments"
method: "POST"
headers:
Authorization: "Bearer {{env.DEPLOY_WEBHOOK_TOKEN}}"
message: ""
# 清理策略
cleanup:
# 构建产物清理
artifacts:
keep_latest: 10
keep_tags:
- "latest"
- "stable"
- "v*.*.*"
older_than_days: 30
# 构建环境清理
environment:
containers: true
volumes: true
networks: true
images: true
# 缓存清理
cache:
strategy: "lru"
max_size_gb: 50
ttl_days: 7
# 钩子脚本
hooks:
# 打包前钩子
pre_package:
- name: "validate-artifacts"
command: "./scripts/validate-artifacts.sh"
timeout: 60
- name: "generate-changelog"
command: "git log --oneline {{.previous_version}}..{{.current_version}} > CHANGELOG.md"
# 打包后钩子
post_package:
- name: "verify-package"
command: "./scripts/verify-package.sh {{.package_path}}"
# 发布前钩子
pre_release:
- name: "notify-stakeholders"
command: "./scripts/notify-stakeholders.sh"
# 发布后钩子
post_release:
- name: "update-dashboard"
command: "curl -X POST https://dashboard.example.com/api/deployments"
# 部署后钩子
post_deploy:
- name: "run-smoke-tests"
command: "./scripts/smoke-tests.sh {{.deployment_url}}"
timeout: 300
# 变量替换上下文
variables:
# 内置变量(由CI/CD系统提供)
builtin:
- "project_name"
- "version"
- "build_id"
- "commit_sha"
- "branch"
- "build_date"
- "platform"
# 自定义变量
custom:
deployment_url: "https://{{.project_name}}-{{.environment}}.example.com"
release_notes: "Automated build {{.build_id}}"
# 环境变量映射
environment:
- "CRATES_IO_TOKEN"
- "DOCKER_USERNAME"
- "SLACK_WEBHOOK"
- "PAGERDUTY_KEY"
# 元数据
metadata:
description: "Finalization configuration for {{.project_name}}"
schema_version: "1.0"
maintainer: "devops@example.com"
last_updated: "2024-01-01"
@@ -1,177 +0,0 @@
# prebake.yaml.tmpl
#
# 术语说明:
# 可选:该字段可以不给出
# 默认为...:隐含“可选”
# 留空:尚未定义,留作后续
# 版本号,用于格式兼容性检查
version: "1.0"
# 构建环境定义
environment:
# 构建机类型
builder: "docker" # 或 "firecracker", "custom", "baremetal"
# 构建机配置,注意以下只会出现与上述构建机类型对应的组。
config:
# baremetal:
# 不需要
# docker:
# 镜像名
image: "rust:1.70-slim"
# Dockerfile路径,相对于项目根目录,不能与镜像一同指定,两者必须指定其一
dockerfile: "/Dockerfile"
# Docker构建参数,可选,只能与dockerfile搭配使用
build_args:
- "RUST_VERSION=1.70"
# firecracker:
# 留空,还没做
# custom:
# 自定义构建机名称
name: "my-custom-builder"
# 设置脚本,相对于项目根目录
setup_script: "setup-builder.sh"
# 清洁脚本,相对于项目根目录
cleanup_script: "cleanup-builder.sh"
# 硬件资源要求,满足最小要求的构建机可以被选中,可选
resources:
cpu: 2 # CPU核心数,默认为1
memory: "2G" # 内存大小,也可以写作2048MB,默认为"1G"
disk: "10G" # 磁盘空间,也可以写作10240MB,默认为"1G"
architecture: "x86_64" # 架构要求,只能是"noarch"或{"x86_64"/"amd64", "arm64"/"aarch64", "riscv64", "loongarch64"/"loong64"}中的一个或几个
gpu: false # 是否需要GPU,默认为false
gpu_type: "cuda" # GPU类型(如果需要),可以是"cuda", "rocm", "vulkan", "oneapi"中的一个或几个
tags: # 自定义标签,只有满足标签的构建机可以被选中
- "custom_tag"
# 网络配置,可选
network:
enabled: true # 启用网络,对baremetal不适用,默认为true
outbound: true # 允许出站连接,对baremetal不适用,默认为true
dns_servers: # 指定构建机的DNS服务器,对baremetal不适用,custom应该自行处理DNS问题,可选
- "8.8.8.8"
- "1.1.1.1"
proxies: # 代理配置,暂时留空,可选
# 依赖定义,安装依赖时按custom_pre-system-languages-custom顺序进行,可选
dependencies:
# 系统包依赖,按软件包类型给出或any通配,若存在发行版则选择之,否则选中any,也没有则报警告但继续
system:
deb:
packages: # 安装的软件包
- "git"
- "curl"
- "build-essential"
- "pkg-config"
- "libssl-dev"
repositories:
- "ppa:custom/ppa" # 自定义PPAUbuntu
mirror: "https://mirrors.tuna.tsinghua.edu.cn" # 指定软件包镜像源,可选
pacman:
packages:
- "git"
- "curl"
- "base-devel"
- "pkg-config"
- "openssl"
repositories:
- "https://mirrors.tuna.tsinghua.edu.cn/arch4edu/$arch" # 自定义仓库(Arch
mirror: "https://mirrors.tuna.tsinghua.edu.cn" # 指定软件包镜像源,可选
rpm:
# 略
# 语言特定依赖,可选,此处定义不稳定
languages:
rust:
toolchain: "stable-x86_64-unknown-linux-gnu"
# 自定义依赖步骤,可选
custom:
- name: "install-custom-tool" # 名称
command: "curl -fsSL https://example.com/install.sh | sh" # 命令
environment: # 环境变量
CUSTOM_VAR: "value"
- name: "build-native-dep"
command: "make && make install"
working_dir: "native-deps" # 工作目录,基于项目根目录
timeout: 300 # 超时,默认为300秒
# 自定义依赖步骤,但是在依赖安装时最先进行,可选
custom_pre:
- name: "install-custom-tool"
command: "curl -fsSL https://example.com/install.sh | sh"
environment:
CUSTOM_VAR: "value"
- name: "build-native-dep"
command: "make && make install"
working_dir: "native-deps"
timeout: 300
# 环境变量配置,可选
environment_variables:
# 构建环境变量,只在prebake阶段有效
prebake:
RUST_BACKTRACE: "full"
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
NODE_ENV: "production"
# 运行时环境变量(会传递给bake阶段,与该阶段获取的环境一并作为.env保存)
bake:
DATABASE_URL: "postgresql://user:pass@localhost/db"
LOG_LEVEL: "info"
__ENV_FILE: ".env" # 特殊变量,指定项目中.env的位置并合并其中的值。该值本身不传入bake阶段
# 缓存配置,可选
cache:
# 缓存目录
directories:
- path: "/usr/local/cargo"
strategy: "always" # 在"always","readonly","clean"和"never"中选择一个,可以被流水线启动时配置覆盖,默认为"always"
mode: "zstd" # 在"gzip","zstd","none"和"dir"中选择一个
- path: "/root/.npm"
strategy: "clean"
mode: "none"
- path: "/app/target"
strategy: "always"
mode: "dir"
# 缓存策略
strategy:
ttl_days: 30 # 缓存生存时间
max_size_gb: 20 # 最大缓存大小
cleanup_policy: "lru" # 清理策略:lru, fifo, size
# 安全配置
security:
# 留空
# 钩子脚本,可选
hooks:
# 依赖安装前
pre_dependencies:
- command: "echo 'Starting dependency installation'"
# 依赖安装后
post_dependencies:
- command: "cargo fetch"
working_dir: "/app"
# 环境准备完成
ready:
- command: "echo 'Environment ready for build'"
# 元数据
metadata:
description: "Rust project build environment"
maintainer: "team@example.com"
tags: ["rust", "web", "api"]
project_type: "rust-cargo"
@@ -1,27 +0,0 @@
use workshop_pipeline::*;
fn main() -> PipelineResult<()> {
env_logger::init();
// 创建变量提供者
let variable_provider = DefaultVariableProvider::new()
.with_variable("project_name", "my-awesome-project")
.with_variable("version", "1.0.0")
.with_secret("CRATES_IO_TOKEN", "super-secret-token");
// 加载流水线配置
let config = PipelineConfig::load_from_dir(
"./examples",
"user123",
"pipeline456",
&variable_provider,
)?;
// 输出完整配置(用于engine)
println!("完整配置: {:#?}", config);
// 输出遮蔽后的配置(用于日志)
let output_config = config.for_output();
println!("遮蔽配置: {:#?}", output_config);
Ok(())
}
-7
View File
@@ -1,7 +0,0 @@
pub mod prebake;
pub mod finalize;
pub mod scripts;
pub use prebake::*;
pub use finalize::*;
pub use scripts::*;
-99
View File
@@ -1,99 +0,0 @@
mod artifacts;
mod packaging;
mod distribution;
mod deployment;
mod notifications;
mod quality;
pub use artifacts::*;
pub use packaging::*;
pub use distribution::*;
pub use deployment::*;
pub use notifications::*;
pub use quality::*;
use serde::{Deserialize, Serialize};
/// Finalize配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FinalizeConfig {
pub version: String,
pub artifacts: Option<ArtifactsConfig>,
pub packaging: Option<PackagingConfig>,
pub distribution: Option<DistributionConfig>,
pub quality_gates: Option<QualityGates>,
pub deployment: Option<DeploymentConfig>,
pub notifications: Option<NotificationsConfig>,
pub cleanup: Option<CleanupConfig>,
pub hooks: Option<FinalizeHooks>,
pub variables: Option<VariableContext>,
pub metadata: Option<FinalizeMetadata>,
}
/// 清理配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CleanupConfig {
pub artifacts: ArtifactCleanup,
pub environment: EnvironmentCleanup,
pub cache: CacheCleanup,
}
/// 产物清理配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactCleanup {
pub keep_latest: u32,
pub keep_tags: Vec<String>,
pub older_than_days: u32,
}
/// 环境清理配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentCleanup {
pub containers: bool,
pub volumes: bool,
pub networks: bool,
pub images: bool,
}
/// 缓存清理配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheCleanup {
pub strategy: String,
pub max_size_gb: u32,
pub ttl_days: u32,
}
/// Finalize钩子配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FinalizeHooks {
pub pre_package: Option<Vec<FinalizeHook>>,
pub post_package: Option<Vec<FinalizeHook>>,
pub pre_release: Option<Vec<FinalizeHook>>,
pub post_release: Option<Vec<FinalizeHook>>,
pub post_deploy: Option<Vec<FinalizeHook>>,
}
/// Finalize钩子
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FinalizeHook {
pub name: String,
pub command: String,
pub timeout: Option<u32>,
}
/// 变量上下文
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariableContext {
pub builtin: Vec<String>,
pub custom: std::collections::HashMap<String, String>,
pub environment: Vec<String>,
}
/// Finalize元数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FinalizeMetadata {
pub description: String,
pub schema_version: String,
pub maintainer: String,
pub last_updated: String,
}
@@ -1,25 +0,0 @@
use serde::{Deserialize, Serialize};
/// 产物配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactsConfig {
pub primary: Option<Vec<Artifact>>,
pub secondary: Option<Vec<Artifact>>,
}
/// 产物
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artifact {
pub name: String,
#[serde(rename = "type")]
pub type_: String,
pub path: String,
pub platform: Option<String>,
pub permissions: Option<String>,
pub description: Option<String>,
pub include_patterns: Option<Vec<String>>,
pub exclude_patterns: Option<Vec<String>>,
pub compress: Option<bool>,
pub format: Option<String>,
pub retention_days: Option<u32>,
}
@@ -1,40 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// 部署配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeploymentConfig {
pub environments: HashMap<String, Environment>,
pub strategy: DeploymentStrategy,
}
/// 环境配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Environment {
#[serde(rename = "type")]
pub type_: String,
pub namespace: String,
pub cluster: String,
pub manifests: Vec<String>,
pub health_check: Option<HealthCheck>,
pub manual_approval: Option<bool>,
pub rollout_strategy: Option<String>,
}
/// 健康检查配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheck {
pub endpoint: String,
pub timeout: u32,
pub interval: u32,
}
/// 部署策略
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeploymentStrategy {
#[serde(rename = "type")]
pub type_: String,
pub max_unavailable: String,
pub max_surge: String,
pub wait_timeout: u32,
}
@@ -1,59 +0,0 @@
use serde::{Deserialize, Serialize};
/// 发布配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistributionConfig {
pub local: Option<LocalStorage>,
pub s3: Option<S3Storage>,
pub package_registries: Option<PackageRegistries>,
}
/// 本地存储配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalStorage {
pub directory: String,
pub retention_days: u32,
}
/// S3存储配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct S3Storage {
pub enabled: bool,
pub bucket: String,
pub prefix: String,
pub region: String,
pub acl: String,
pub storage_class: String,
}
/// 包注册表配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageRegistries {
pub crates_io: Option<CratesIoConfig>,
pub npm: Option<NpmConfig>,
pub docker_registry: Option<DockerRegistryConfig>,
}
/// Crates.io配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CratesIoConfig {
pub enabled: bool,
pub token: String,
}
/// NPM配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NpmConfig {
pub enabled: bool,
pub registry: String,
pub token: String,
}
/// Docker注册表配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerRegistryConfig {
pub enabled: bool,
pub registry: String,
pub username: String,
pub password: String,
}
@@ -1,28 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// 通知配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationsConfig {
pub on_success: Vec<Notification>,
pub on_failure: Vec<Notification>,
pub on_deploy: Vec<Notification>,
}
/// 通知
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Notification {
#[serde(rename = "type")]
pub type_: String,
pub webhook: Option<String>,
pub channel: Option<String>,
pub message: String,
pub to: Option<Vec<String>>,
pub subject: Option<String>,
pub template: Option<String>,
pub url: Option<String>,
pub method: Option<String>,
pub headers: Option<HashMap<String, String>>,
pub service_key: Option<String>,
pub severity: Option<String>,
}
@@ -1,30 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// 打包配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackagingConfig {
pub primary_package: Option<PrimaryPackage>,
pub docker: Option<DockerPackaging>,
}
/// 主要包配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrimaryPackage {
pub format: String,
pub output: String,
pub include: Vec<String>,
pub exclude: Vec<String>,
}
/// Docker打包配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerPackaging {
pub enabled: bool,
pub registry: String,
pub repository: String,
pub tags: Vec<String>,
pub dockerfile: String,
pub build_args: Vec<String>,
pub labels: HashMap<String, String>,
}
@@ -1,35 +0,0 @@
use serde::{Deserialize, Serialize};
/// 质量门禁
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityGates {
pub test_coverage: Option<TestCoverageGate>,
pub security_scan: Option<SecurityScanGate>,
pub code_quality: Option<CodeQualityGate>,
}
/// 测试覆盖率门禁
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestCoverageGate {
pub enabled: bool,
pub minimum: f32,
pub file: String,
}
/// 安全扫描门禁
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityScanGate {
pub enabled: bool,
pub tools: Vec<String>,
pub fail_on: String,
pub exceptions: Vec<String>,
}
/// 代码质量门禁
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeQualityGate {
pub enabled: bool,
pub maximum_duplication: f32,
pub maximum_complexity: f32,
pub minimum_maintainability: f32,
}
-107
View File
@@ -1,107 +0,0 @@
mod environment;
mod dependencies;
mod security;
mod hooks;
use std::collections::HashMap;
pub use environment::*;
pub use dependencies::*;
pub use security::*;
pub use hooks::*;
use serde::{Deserialize, Serialize};
/// Prebake配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrebakeConfig {
pub version: String,
pub environment: EnvironmentConfig,
pub dependencies: Option<DependenciesConfig>,
pub environment_variables: Option<EnvironmentVariables>,
pub cache: Option<CacheConfig>,
pub security: Option<SecurityConfig>,
pub hooks: Option<HooksConfig>,
pub metadata: Option<Metadata>,
}
/// 环境变量配置 - 调整阶段
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentVariables {
pub prebake: Option<HashMap<String, String>>,
pub bake: Option<HashMap<String, String>>,
}
/// 缓存配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
pub directories: Vec<CacheDirectory>,
pub strategy: Option<CacheStrategy>,
}
/// 缓存目录配置 - 更新字段
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheDirectory {
pub path: String,
#[serde(default = "default_cache_strategy")]
pub strategy: CacheStrategyType,
#[serde(default = "default_cache_mode")]
pub mode: CacheMode,
}
/// 缓存策略类型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CacheStrategyType {
#[serde(rename = "always")]
Always,
#[serde(rename = "readonly")]
ReadOnly,
#[serde(rename = "clean")]
Clean,
#[serde(rename = "never")]
Never,
}
/// 缓存模式
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CacheMode {
#[serde(rename = "gzip")]
Gzip,
#[serde(rename = "zstd")]
Zstd,
#[serde(rename = "none")]
None,
#[serde(rename = "dir")]
Dir,
}
/// 缓存策略
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStrategy {
pub ttl_days: u32,
pub max_size_gb: u32,
pub cleanup_policy: String,
}
/// 安全配置 - 留空
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
// 根据新模板,暂时留空
}
/// 元数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
pub description: String,
pub maintainer: String,
pub tags: Vec<String>,
pub project_type: String,
}
// 默认值函数
fn default_cache_strategy() -> CacheStrategyType {
CacheStrategyType::Always
}
fn default_cache_mode() -> CacheMode {
CacheMode::Zstd
}
@@ -1,82 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// 依赖配置 - 调整顺序
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependenciesConfig {
pub custom_pre: Option<Vec<CustomDependency>>,
pub system: Option<SystemDependencies>,
pub languages: Option<LanguageDependencies>,
pub custom: Option<Vec<CustomDependency>>,
}
/// 系统依赖 - 按发行版组织
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemDependencies {
pub ubuntu: Option<DistributionDeps>,
pub arch: Option<DistributionDeps>,
pub fedora: Option<DistributionDeps>,
// 可以添加其他发行版...
}
/// 发行版依赖
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistributionDeps {
pub packages: Vec<String>,
pub repositories: Option<Vec<String>>,
pub mirror: Option<String>,
}
/// 语言依赖 - 简化结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanguageDependencies {
pub rust: Option<RustConfig>,
// 其他语言暂时保持不变...
pub node: Option<NodeConfig>,
pub python: Option<PythonConfig>,
pub go: Option<GoConfig>,
}
/// Rust配置 - 简化
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RustConfig {
pub toolchain: String,
}
/// Node配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeConfig {
pub version: String,
pub package_manager: String,
pub registry: String,
}
/// Python配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PythonConfig {
pub version: String,
pub package_manager: String,
pub requirements_file: String,
}
/// Go配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoConfig {
pub version: String,
pub modules: bool,
}
// TODO: 更多语言配置
/// 自定义依赖
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomDependency {
pub name: String,
pub command: String,
pub environment: Option<HashMap<String, String>>,
pub working_dir: Option<String>,
#[serde(default = "default_timeout")]
pub timeout: u32,
}
fn default_timeout() -> u32 { 300 }
@@ -1,117 +0,0 @@
use serde::{Deserialize, Serialize};
/// 环境配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentConfig {
pub builder: String,
pub config: BuilderConfig,
pub resources: ResourceRequirements,
pub network: NetworkConfig,
}
/// 构建器配置 - 现在各个配置是互斥的
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuilderConfig {
pub baremetal: Option<BaremetalConfig>,
pub docker: Option<DockerConfig>,
pub firecracker: Option<FirecrackerConfig>,
pub custom: Option<CustomBuilderConfig>,
}
/// 裸机配置 - 现在不需要具体配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaremetalConfig {
// 根据新模板,baremetal不需要配置项
}
/// Docker配置 - 更新字段
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerConfig {
/// 镜像名
pub image: Option<String>,
/// Dockerfile路径,相对于项目根目录
pub dockerfile: Option<String>,
/// Docker构建参数,可选,只能与dockerfile搭配使用
pub build_args: Option<Vec<String>>,
}
/// Firecracker配置 - 留空
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FirecrackerConfig {
// 根据新模板,暂时留空
}
/// 自定义构建器配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomBuilderConfig {
pub name: String,
pub setup_script: String,
pub cleanup_script: String,
}
/// 资源需求 - 更新字段
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceRequirements {
#[serde(default = "default_cpu")]
pub cpu: u32,
#[serde(default = "default_memory")]
pub memory: String,
#[serde(default = "default_disk")]
pub disk: String,
pub architecture: Architecture,
#[serde(default)]
pub gpu: bool,
pub gpu_type: Option<Vec<GpuType>>,
pub tags: Option<Vec<String>>,
}
/// 架构要求
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Architecture {
Single(String),
Multiple(Vec<String>),
}
impl Default for Architecture {
fn default() -> Self {
Architecture::Single("x86_64".to_string())
}
}
/// GPU类型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GpuType {
#[serde(rename = "cuda")]
Cuda,
#[serde(rename = "rocm")]
Rocm,
#[serde(rename = "vulkan")]
Vulkan,
#[serde(rename = "oneapi")]
OneApi,
}
/// 网络配置 - 更新字段
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
#[serde(default = "default_network_enabled")]
pub enabled: bool,
#[serde(default = "default_outbound")]
pub outbound: bool,
pub dns_servers: Option<Vec<String>>,
pub proxies: Option<ProxiesConfig>,
}
/// 代理配置 - 暂时留空
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxiesConfig {
// 暂不定义具体字段
}
// 默认值函数
fn default_cpu() -> u32 { 1 }
fn default_memory() -> String { "1G".to_string() }
fn default_disk() -> String { "1G".to_string() }
fn default_network_enabled() -> bool { true }
fn default_outbound() -> bool { true }
@@ -1,17 +0,0 @@
use serde::{Deserialize, Serialize};
/// 钩子配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HooksConfig {
pub pre_dependencies: Option<Vec<Hook>>,
pub post_dependencies: Option<Vec<Hook>>,
pub ready: Option<Vec<Hook>>,
}
/// 钩子
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hook {
pub command: String,
pub working_dir: Option<String>,
pub timeout: Option<u32>,
}
@@ -1,19 +0,0 @@
use serde::{Deserialize, Serialize};
/// 安全配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
pub capabilities: Vec<String>,
pub seccomp: String,
pub apparmor: bool,
pub no_new_privileges: bool,
pub limits: ResourceLimits,
}
/// 资源限制
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceLimits {
pub max_processes: u32,
pub max_file_size: String,
pub max_open_files: u32,
}
-49
View File
@@ -1,49 +0,0 @@
use serde::{Deserialize, Serialize};
/// Bake脚本
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BakeScript {
pub content: String,
pub path: String,
}
impl BakeScript {
pub fn new(content: String) -> Self {
Self {
content,
path: ".workshop/bake.sh".to_string(),
}
}
}
/// 为bench.sh预留的结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchScript {
pub content: String,
pub path: String,
}
impl BenchScript {
pub fn new(content: String) -> Self {
Self {
content,
path: ".workshop/bench.sh".to_string(),
}
}
}
/// 为test.sh预留的结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestScript {
pub content: String,
pub path: String,
}
impl TestScript {
pub fn new(content: String) -> Self {
Self {
content,
path: ".workshop/test.sh".to_string(),
}
}
}
-19
View File
@@ -1,19 +0,0 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PipelineError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("YAML parse error: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("Variable error: {0}")]
Variable(#[from] crate::variable::VariableError),
#[error("Template error: {0}")]
Template(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Validation error: {0}")]
Validation(String),
}
pub type PipelineResult<T> = Result<T, PipelineError>;
-88
View File
@@ -1,88 +0,0 @@
mod error;
mod variable;
mod template;
mod config;
use std::path::Path;
use std::collections::HashMap;
use serde::Serialize;
use log::info;
pub use error::*;
pub use variable::*;
pub use template::*;
pub use config::*;
// 重新导出常用的配置类型
#[allow(unused)]
pub use config::prebake::*;
#[allow(unused)]
pub use config::finalize::*;
#[allow(unused)]
pub use config::scripts::*;
pub type PipelineResult<T> = Result<T, PipelineError>;
/// 完整的流水线配置
#[derive(Debug, Clone, Serialize)]
pub struct PipelineConfig {
pub prebake: PrebakeConfig,
pub bake: BakeScript,
pub finalize: FinalizeConfig,
pub variables: HashMap<String, VariableValue>,
}
impl PipelineConfig {
/// 从文件系统加载完整配置
pub fn load_from_dir<P: AsRef<Path>>(
dir: P,
user_id: &str,
pipeline_id: &str,
variable_provider: &dyn VariableProvider,
) -> PipelineResult<Self> {
let dir = dir.as_ref();
info!("Reading prebake.yaml...");
let prebake_path = dir.join(".workshop/prebake.yaml");
info!("Reading bake.sh...");
let bake_path = dir.join(".workshop/bake.sh");
info!("Reading finalize.yaml...");
let finalize_path = dir.join(".workshop/finalize.yaml");
let mut variable_resolver = VariableResolver::new(user_id, pipeline_id, variable_provider);
// 加载并处理prebake配置
let prebake_content = std::fs::read_to_string(&prebake_path)?;
let prebake: PrebakeConfig = serde_yaml::from_str(&prebake_content)?;
// let prebake = variable_resolver.resolve_prebake(prebake)?;
let prebake = prebake.resolve_template(&mut variable_resolver)?;
// 加载bake脚本
let bake_content = std::fs::read_to_string(&bake_path)?;
let bake = BakeScript::new(bake_content);
// 加载并处理finalize配置
let finalize_content = std::fs::read_to_string(&finalize_path)?;
let finalize: FinalizeConfig = serde_yaml::from_str(&finalize_content)?;
// let finalize = variable_resolver.resolve_finalize(finalize)?;
let finalize = finalize.resolve_template(&mut variable_resolver)?;
Ok(Self {
prebake,
bake,
finalize,
variables: variable_resolver.get_resolved_variables(),
})
}
/// 获取用于输出的配置(遮蔽secret)
pub fn for_output(&self) -> Self {
Self {
prebake: self.prebake.clone(),
bake: self.bake.clone(),
finalize: self.finalize.for_output(),
variables: self.variables.iter()
.map(|(k, v)| (k.clone(), v.for_output()))
.collect(),
}
}
}
-353
View File
@@ -1,353 +0,0 @@
use crate::{
config::finalize::FinalizeConfig,
config::prebake::{
Architecture, CacheMode, CacheStrategyType, DistributionDeps, GpuType, PrebakeConfig,
SystemDependencies,
},
error::PipelineError,
variable::VariableResolver,
};
use lazy_static::lazy_static;
use regex::Regex;
// use serde::{Deserialize, Serialize};
lazy_static! {
static ref HANDLEBARS_REGEX: Regex =
Regex::new(r"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}").unwrap();
static ref SHELL_VAR_REGEX: Regex = Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").unwrap();
}
pub trait TemplateResolvable {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError>
where
Self: Sized;
}
// 为基本类型实现模板解析
impl TemplateResolvable for String {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
let mut result = self.clone();
// 处理 {{VARIABLE}} 格式
for cap in HANDLEBARS_REGEX.captures_iter(result.clone().as_str()) {
let var_name = &cap[1];
if let Ok(value) = resolver.resolve_variable(var_name) {
result = result.replace(&cap[0], &value);
}
}
// 处理$VARIABLE格式,TODO
Ok(result)
}
}
// 为Option<String>实现
// impl TemplateResolvable for Option<String> {
// fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
// match self {
// Some(s) => Ok(Some(s.resolve_template(resolver)?)),
// None => Ok(None),
// }
// }
// }
// 为Vec<String>实现
impl TemplateResolvable for Vec<String> {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
let mut result = Vec::new();
for item in self {
result.push(item.resolve_template(resolver)?);
}
Ok(result)
}
}
// impl TemplateResolvable for Option<Vec<String>> {
// fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
// match self {
// Some(vec) => Ok(Some(vec.resolve_template(resolver)?)),
// None => Ok(None),
// }
// }
// }
// 为HashMap<String, String>实现
impl TemplateResolvable for std::collections::HashMap<String, String> {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
let mut result = std::collections::HashMap::new();
for (key, value) in self {
result.insert(
key.resolve_template(resolver)?,
value.resolve_template(resolver)?,
);
}
Ok(result)
}
}
impl<T: TemplateResolvable> TemplateResolvable for Option<T> {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError>
where
Self: Sized,
{
match self {
Some(value) => Ok(Some(value.resolve_template(resolver)?)),
None => Ok(None),
}
}
}
// 为Architecture实现
impl TemplateResolvable for Architecture {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
match self {
Architecture::Single(arch) => {
Ok(Architecture::Single(arch.resolve_template(resolver)?))
}
Architecture::Multiple(archs) => {
Ok(Architecture::Multiple(archs.resolve_template(resolver)?))
}
}
}
}
// 为SystemDependencies实现
impl TemplateResolvable for SystemDependencies {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
let mut deps = self.clone();
if let Some(ref mut ubuntu) = deps.ubuntu {
*ubuntu = ubuntu.resolve_template(resolver)?;
}
if let Some(ref mut arch) = deps.arch {
*arch = arch.resolve_template(resolver)?;
}
if let Some(ref mut fedora) = deps.fedora {
*fedora = fedora.resolve_template(resolver)?;
}
Ok(deps)
}
}
// 为DistributionDeps实现
impl TemplateResolvable for DistributionDeps {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(DistributionDeps {
packages: self.packages.resolve_template(resolver)?,
repositories: self.repositories.resolve_template(resolver)?,
mirror: self.mirror.resolve_template(resolver)?,
})
}
}
// 枚举类型不需要模板解析,直接克隆
impl TemplateResolvable for GpuType {
fn resolve_template(&self, _resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(self.clone())
}
}
impl TemplateResolvable for CacheStrategyType {
fn resolve_template(&self, _resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(self.clone())
}
}
impl TemplateResolvable for CacheMode {
fn resolve_template(&self, _resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(self.clone())
}
}
// 为PrebakeConfig实现模板解析
impl TemplateResolvable for PrebakeConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
let mut config = self.clone();
// 解析环境配置
config.environment = config.environment.resolve_template(resolver)?;
// 解析依赖配置
if let Some(ref mut deps) = config.dependencies {
*deps = deps.resolve_template(resolver)?;
}
// 解析环境变量
if let Some(ref mut env_vars) = config.environment_variables {
*env_vars = env_vars.resolve_template(resolver)?;
}
// 解析缓存配置
if let Some(ref mut cache) = config.cache {
*cache = cache.resolve_template(resolver)?;
}
Ok(config)
}
}
// 为EnvironmentConfig实现
impl TemplateResolvable for crate::config::prebake::EnvironmentConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
builder: self.builder.resolve_template(resolver)?,
config: self.config.resolve_template(resolver)?,
resources: self.resources.resolve_template(resolver)?,
network: self.network.resolve_template(resolver)?,
})
}
}
// 为BuilderConfig实现
impl TemplateResolvable for crate::config::prebake::BuilderConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
baremetal: self.baremetal.resolve_template(resolver)?,
docker: self.docker.resolve_template(resolver)?,
firecracker: self.firecracker.resolve_template(resolver)?,
custom: self.custom.resolve_template(resolver)?,
})
}
}
// 为DockerConfig实现
impl TemplateResolvable for crate::config::prebake::DockerConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
image: self.image.resolve_template(resolver)?,
dockerfile: self.dockerfile.resolve_template(resolver)?,
build_args: self.build_args.resolve_template(resolver)?,
})
}
}
// 为CustomBuilderConfig实现
impl TemplateResolvable for crate::config::prebake::CustomBuilderConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
name: self.name.resolve_template(resolver)?,
setup_script: self.setup_script.resolve_template(resolver)?,
cleanup_script: self.cleanup_script.resolve_template(resolver)?,
})
}
}
// 为ResourceRequirements实现
impl TemplateResolvable for crate::config::prebake::ResourceRequirements {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
cpu: self.cpu,
memory: self.memory.resolve_template(resolver)?,
disk: self.disk.resolve_template(resolver)?,
architecture: self.architecture.resolve_template(resolver)?,
gpu: self.gpu,
gpu_type: self.gpu_type.clone(),
tags: self.tags.resolve_template(resolver)?,
})
}
}
// 为NetworkConfig实现
impl TemplateResolvable for crate::config::prebake::NetworkConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
enabled: self.enabled,
outbound: self.outbound,
dns_servers: self.dns_servers.resolve_template(resolver)?,
proxies: self.proxies.resolve_template(resolver)?,
})
}
}
// 为DependenciesConfig实现
impl TemplateResolvable for crate::config::prebake::DependenciesConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
custom_pre: self.custom_pre.resolve_template(resolver)?,
system: self.system.resolve_template(resolver)?,
languages: self.languages.resolve_template(resolver)?,
custom: self.custom.resolve_template(resolver)?,
})
}
}
// 为LanguageDependencies实现
impl TemplateResolvable for crate::config::prebake::LanguageDependencies {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
rust: self.rust.resolve_template(resolver)?,
node: self.node.resolve_template(resolver)?,
python: self.python.resolve_template(resolver)?,
go: self.go.resolve_template(resolver)?,
})
}
}
// 为RustConfig实现
impl TemplateResolvable for crate::config::prebake::RustConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
toolchain: self.toolchain.resolve_template(resolver)?,
})
}
}
// 为CustomDependency实现
impl TemplateResolvable for crate::config::prebake::CustomDependency {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
name: self.name.resolve_template(resolver)?,
command: self.command.resolve_template(resolver)?,
environment: self.environment.resolve_template(resolver)?,
working_dir: self.working_dir.resolve_template(resolver)?,
timeout: self.timeout,
})
}
}
// 为EnvironmentVariables实现
impl TemplateResolvable for crate::config::prebake::EnvironmentVariables {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
prebake: self.prebake.resolve_template(resolver)?,
bake: self.bake.resolve_template(resolver)?,
})
}
}
// 为CacheConfig实现
impl TemplateResolvable for crate::config::prebake::CacheConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
directories: self.directories.resolve_template(resolver)?,
strategy: self.strategy.resolve_template(resolver)?,
})
}
}
// 为CacheDirectory实现
impl TemplateResolvable for crate::config::prebake::CacheDirectory {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
path: self.path.resolve_template(resolver)?,
strategy: self.strategy.resolve_template(resolver)?,
mode: self.mode.resolve_template(resolver)?,
})
}
}
// 为CacheStrategy实现
impl TemplateResolvable for crate::config::prebake::CacheStrategy {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
Ok(Self {
ttl_days: self.ttl_days,
max_size_gb: self.max_size_gb,
cleanup_policy: self.cleanup_policy.resolve_template(resolver)?,
})
}
}
// FinalizeConfig的模板解析保持不变...
-128
View File
@@ -1,128 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum VariableError {
#[error("Variable not found: {name}")]
NotFound { name: String },
#[error("Variable resolution failed: {message}")]
Resolution { message: String },
}
/// 变量值类型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VariableValue {
String(String),
Secret(String),
}
impl VariableValue {
pub fn as_str(&self) -> &str {
match self {
VariableValue::String(s) => s,
VariableValue::Secret(s) => s,
}
}
pub fn for_output(&self) -> Self {
match self {
VariableValue::String(s) => VariableValue::String(s.clone()),
VariableValue::Secret(_) => VariableValue::Secret("***".to_string()),
}
}
}
/// 变量提供者trait
pub trait VariableProvider: Send + Sync {
fn get_variable(&self, user_id: &str, pipeline_id: &str, name: &str) -> Option<VariableValue>;
fn get_secret(&self, user_id: &str, pipeline_id: &str, name: &str) -> Option<VariableValue>;
}
/// 默认的变量提供者实现(用于测试)
pub struct DefaultVariableProvider {
variables: HashMap<String, VariableValue>,
secrets: HashMap<String, VariableValue>,
}
impl DefaultVariableProvider {
pub fn new() -> Self {
Self {
variables: HashMap::new(),
secrets: HashMap::new(),
}
}
pub fn with_variable(mut self, name: &str, value: &str) -> Self {
self.variables.insert(name.to_string(), VariableValue::String(value.to_string()));
self
}
pub fn with_secret(mut self, name: &str, value: &str) -> Self {
self.secrets.insert(name.to_string(), VariableValue::Secret(value.to_string()));
self
}
}
impl VariableProvider for DefaultVariableProvider {
fn get_variable(&self, _user_id: &str, _pipeline_id: &str, name: &str) -> Option<VariableValue> {
self.variables.get(name).cloned()
}
fn get_secret(&self, _user_id: &str, _pipeline_id: &str, name: &str) -> Option<VariableValue> {
self.secrets.get(name).cloned()
}
}
impl Default for DefaultVariableProvider {
fn default() -> Self {
Self::new()
}
}
/// 变量解析器
pub struct VariableResolver<'a> {
user_id: String,
pipeline_id: String,
provider: &'a dyn VariableProvider,
resolved_variables: HashMap<String, VariableValue>,
}
impl<'a> VariableResolver<'a> {
pub fn new(
user_id: &str,
pipeline_id: &str,
provider: &'a dyn VariableProvider,
) -> Self {
Self {
user_id: user_id.to_string(),
pipeline_id: pipeline_id.to_string(),
provider,
resolved_variables: HashMap::new(),
}
}
pub fn resolve_variable(&mut self, name: &str) -> Result<String, VariableError> {
// 先尝试普通变量
if let Some(value) = self.provider.get_variable(&self.user_id, &self.pipeline_id, name) {
let value_str = value.as_str().to_string();
self.resolved_variables.insert(name.to_string(), value);
return Ok(value_str);
}
// 再尝试secret
if let Some(value) = self.provider.get_secret(&self.user_id, &self.pipeline_id, name) {
let value_str = value.as_str().to_string();
self.resolved_variables.insert(name.to_string(), value);
return Ok(value_str);
}
Err(VariableError::NotFound {
name: name.to_string(),
})
}
pub fn get_resolved_variables(&self) -> HashMap<String, VariableValue> {
self.resolved_variables.clone()
}
}