chore: remove unused crates and add documentation

- Delete workshop-agent (HTTP server stub)
- Delete workshop-builder-native (empty stub)
- Delete workshop-helper-mac (unused)
- Delete workshop-llm-detector (moved to separate repo)
- Add AGENTS.md to bake/, engine/, finalize/, prebake/, cert/, vault/
- Fix finalize plugin validation and logging improvements
This commit is contained in:
Catty Steve
2026-04-22 12:47:48 +08:00
parent 7b3b71a4b3
commit 40f307b471
48 changed files with 420 additions and 5779 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# PROJECT KNOWLEDGE BASE
**Generated:** 2026-04-04
**Commit:** 2133f8b
**Generated:** 2026-04-22
**Commit:** 7b3b71a
**Branch:** master
## OVERVIEW
-2588
View File
File diff suppressed because it is too large Load Diff
-20
View File
@@ -1,20 +0,0 @@
[package]
name = "workshop-agent"
version = "0.1.0"
edition = "2024"
[dependencies]
actix-web = { version = "4.11.0", features = ["rustls-0_23"] }
actix-web-httpauth = "0.8.2"
anyhow = "1.0.100"
config = "0.15.18"
env_logger = "0.11.8"
log = "0.4.28"
querystring = "1.1.0"
rustls = "0.23.35"
rustls-pemfile = "2.2.0"
serde = { version = "1.0.228", features = ["derive"] }
shellflip = "2.1.2"
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
uuid = { version = "1.18.1", features = ["v4"] }
-3
View File
@@ -1,3 +0,0 @@
host = "127.0.0.1"
port = 8080
-36
View File
@@ -1,36 +0,0 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub tls: Option<TLSConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TLSConfig {
pub cert: PathBuf,
pub key: PathBuf,
}
impl ServerConfig {
pub fn init() -> anyhow::Result<Self> {
let mut settings = config::Config::builder();
settings = settings
.add_source(config::Environment::with_prefix("HBWAGENT"))
.add_source(config::File::with_name("agent.toml").required(false));
let config: ServerConfig = settings.build()?.try_deserialize()?;
Ok(config)
}
pub fn is_tls_enabled(&self) -> bool {
self.tls.is_some()
}
pub fn bind_address(&self) -> (String, u16) {
// format!("{}:{}", self.host, self.port)
(self.host.clone(), self.port)
}
}
-19
View File
@@ -1,19 +0,0 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AgentError {
#[error("Failed when loading TLS certificate: {0}")]
LoadTLSCertError(#[from] rustls::Error),
#[error("I/O error: {0}")]
IOError(#[from] std::io::Error),
#[error("General error: {0}")]
GeneralError(String),
#[error("Internal server error: {0}")]
InternalServerError(String),
}
pub type Result<T> = std::result::Result<T, AgentError>;
-79
View File
@@ -1,79 +0,0 @@
use serde::{Serialize, Deserialize};
use crate::state;
use actix_web::{get, post, HttpResponse, Responder, HttpRequest, web};
#[derive(Debug, Serialize, Deserialize)]
struct ResponseData {
message: String,
code: i32,
}
/// Default endpoint
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Ok().json(ResponseData {
message: "HBW agent is running.".to_string(),
code: 0,
})
}
/// Endpoint for initializing.
/// Initialize is divided into %d stages.
#[post("/init")]
async fn init(req: HttpRequest, info: web::Json<crate::init::InitData>, data: web::Data<state::AppState>) -> impl Responder {
// 检查是否启用TLS阻止访问
if data.use_tls {
return HttpResponse::Forbidden().body("Access forbidden: This agent is already configured. If you'd like to reconfigure, remove TLS section in agent.toml");
}
let mut token_verified = false;
// 检查Bearer Token
if let Some(auth_header) = req.headers().get("authorization") {
if let Ok(auth_str) = auth_header.to_str() {
if auth_str.starts_with("Bearer ") {
let token = &auth_str[7..]; // 去掉 "Bearer " 前缀
if token == data.token.as_ref().unwrap() {
// Token验证成功
token_verified = true;
}
}
}
}
if token_verified {
// Token验证成功,执行初始化操作
//
let query = querystring::querify(req.query_string());
let stage = query.get(0).unwrap_or(&("0","0"));
match stage.0 {
"1" => {
// 初始化阶段1
// todo!("实现初始化阶段1");
crate::init::init_stage1(info);
}
"2" => {
// 初始化阶段2
todo!("实现初始化阶段2");
}
_ => {
// 未知的初始化阶段
// HttpResponse::BadRequest().body("Unknown initialization stage")
return HttpResponse::BadRequest().body("Unknown initialization stage");
}
}
todo!("实现初始化操作");
HttpResponse::Ok().body("Initialization successful")
} else {
// Token验证失败
HttpResponse::Unauthorized().body("Invalid or missing Bearer token")
}
}
/// Endpoint for pinging the server.
#[get("/ping")]
async fn ping() -> impl Responder {
HttpResponse::Ok().json(ResponseData {
message: "pong".to_string(),
code: 0,
})
}
-19
View File
@@ -1,19 +0,0 @@
use serde::{Deserialize, Serialize};
use actix_web::{web, HttpResponse};
use uuid::Uuid;
pub fn generate_token() -> String {
// 生成UUIDv4 token
Uuid::new_v4().to_string()
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InitData {
server_id: String,
timestamp: u64,
}
pub fn init_stage1(data: web::Json<InitData>) -> () {
}
-45
View File
@@ -1,45 +0,0 @@
use log::info;
use actix_web::{web, App, HttpServer};
mod config;
mod tls;
mod error;
mod handler;
mod init;
mod state;
#[actix_web::main]
async fn main() -> Result<(), ()> {
env_logger::init();
info!("Starting workshop-agent...");
let config = config::ServerConfig::init().unwrap();
let token = if config.is_tls_enabled() {
None
} else {
let _token = init::generate_token();
println!("Generated token: {}", _token);
Some(_token)
};
let appstate = web::Data::new(state::AppState {
use_tls: config.is_tls_enabled(),
token,
});
let mut server = HttpServer::new(move || App::new()
.app_data(appstate.clone())
.service(handler::index)
.service(handler::ping)
.service(handler::init));
if config.is_tls_enabled() {
let tls_certs_file = config.tls.clone().unwrap().cert;
let tls_key_file = config.tls.clone().unwrap().key;
let tls_config = tls::load_rustls_config(&tls_certs_file, &tls_key_file).unwrap();
info!("Configuring TLS...");
server = server.bind_rustls_0_23(config.bind_address(), tls_config).unwrap();
}
else {
server = server.bind(config.bind_address()).unwrap();
}
let _ = server.run().await;
Ok(())
}
-5
View File
@@ -1,5 +0,0 @@
#[derive(Clone)]
pub struct AppState {
pub token: Option<String>,
pub use_tls: bool,
}
-33
View File
@@ -1,33 +0,0 @@
use crate::error::AgentError;
use anyhow::Result;
use rustls::ServerConfig;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
pub fn load_rustls_config(
cert_path: &Path,
key_path: &Path,
) -> Result<rustls::ServerConfig, AgentError> {
// 初始化 TLS 配置
let config = ServerConfig::builder()
.with_no_client_auth();
// 加载 TLS 证书
let mut cert_file = &mut BufReader::new(File::open(cert_path)?);
let cert_chain = rustls_pemfile::certs(&mut cert_file)
.collect::<Result<Vec<_>, _>>()?;
// 加载私钥
let mut key_file = &mut BufReader::new(File::open(key_path)?);
let key = rustls_pemfile::pkcs8_private_keys(&mut key_file)
.next()
.unwrap();
if let Ok(key) = key {
Ok(config.with_single_cert(cert_chain, rustls::pki_types::PrivateKeyDer::Pkcs8(key))?)
} else {
// TODO: How to implement better?
Err(AgentError::GeneralError("Failed to load TLS key.".to_string()))
}
}
+4 -1
View File
@@ -1,6 +1,9 @@
# workshop-baker/src
**Baker CLI + execution engine for HoneyBiscuitWorkshop pipelines.**
**Generated:** 2026-04-22
**Commit:** 7b3b71a
Baker CLI + execution engine for HoneyBiscuitWorkshop pipelines.
## OVERVIEW
+46
View File
@@ -0,0 +1,46 @@
# workshop-baker/src/bake
**Generated:** 2026-04-22
**Commit:** 7b3b71a
Script parsing and build orchestration with decorator DSL.
## STRUCTURE
```
bake/
├── parser.rs # @decorator-aware shell script parser
├── decorator.rs # Decorator enum + 100+ unit tests
├── builder.rs # Script template assembly
└── schedule.rs # Function ordering via @after dependencies
```
## WHERE TO LOOK
| Task | Location |
|------|----------|
| Parse script | `parser.rs:29` - `parse_script()` |
| Decorator types | `decorator.rs` - `Decorator` enum variants |
| Build script | `builder.rs` - template builder |
| Schedule deps | `schedule.rs` - @after resolution |
## DECORATORS
| Decorator | Purpose |
|-----------|---------|
| `@pipeline` | Marks main build function |
| `@timeout(N)` | Execution timeout seconds |
| `@retry(N, M)` | Retry N times, M sec delay |
| `@parallel` | Run concurrently |
| `@fallible` | Failure doesn't fail pipeline |
| `@after(func)` | Dependency ordering |
| `@export` | Export vars to env |
| `@loop(N)` | Repeat N times |
| `@if(COND)` | Conditional execution |
## CONVENTIONS
- **Decorator Syntax**: `# @name(args)` comment above function
- **Parsing**: Shell grammar with decorator annotation extraction
- **Scheduling**: DAG resolution for @after dependencies
- **Tests**: `decorator.rs:175-829` has 100+ inline unit tests
+46
View File
@@ -0,0 +1,46 @@
# workshop-baker/src/engine
**Generated:** 2026-04-22
**Commit:** 7b3b71a
Execution runtime with resource isolation and package management.
## STRUCTURE
```
engine/
├── executor.rs # Script execution with cgroups, pipes, exit codes
├── cgroups.rs # Linux cgroup resource limits (memory, CPU, IO)
├── types.rs # ResourceLimits, ExecutionResult, ProcessConfig
├── pm.rs # Package manager detection (apt, pacman, etc.)
├── pm/ # PM implementations
│ ├── apt.rs # Debian/Ubuntu (WIP - todo!() stubs)
│ └── pacman.rs # Arch Linux
├── upm.rs # User package managers (Nix/Guix)
├── upm/custom.rs # Custom PM support (unimplemented)
└── repology.rs # Repology API for package name resolution
└── local.rs # Python-based local repology (non-standard)
```
## WHERE TO LOOK
| Task | Location |
|------|----------|
| Execute script with limits | `executor.rs:Executor::execute_script()` |
| Resource limits | `types.rs:56` - `ResourceLimits` struct |
| Cgroup management | `cgroups.rs` - CgroupManager v1/v2 |
| Package detection | `pm.rs:detection()` |
| Repology lookup | `repology.rs` - RepologyEndpoint |
## CONVENTIONS
- **Executor**: Lightweight wrapper around CgroupManager for isolation
- **ResourceLimits**: Memory, CPU, IO bounds passed to cgroups
- **Package Manager Detection**: Auto-detects from `/etc/os-release`
- **WIP**: apt.rs has `todo!()` stubs (lines 22, 45, 50)
## ANTI-PATTERNS
1. **Python in Rust**`repology/local.rs` embeds Python repology client
2. **Unsafe libc** — Multiple `unsafe { libc::geteuid() }` in security.rs (parent)
3. **Unwrap heavy** — cgroups.rs has many `.unwrap()` calls
+56
View File
@@ -0,0 +1,56 @@
# workshop-baker/src/finalize
**Generated:** 2026-04-22
**Commit:** 7b3b71a
Artifact packaging, distribution, deployment, and notifications.
## STRUCTURE
```
finalize/
├── config.rs # FinalizeConfig YAML (artifacts, deploy, notify)
├── stage.rs # FinalizeStage enum
├── stage/hook.rs # Post-build hooks
├── event.rs # Event propagation
├── template.rs # Variable substitution {{VAR}}
├── plugin.rs # Plugin trait + registry
├── plugin/
│ ├── metadata.rs # Plugin manifest parsing
│ ├── dylib.rs # Dynamic library plugins
│ ├── rhai.rs # Rhai scripting (todo!())
│ ├── shell.rs # Shell command plugins
│ └── internal/ # Built-in plugins
│ ├── mail/ # Email notifications (SMTP)
│ ├── webhook.rs # HTTP webhooks
│ ├── satori.rs # Satori integration
│ └── insitenotify.rs # In-site notifications
│ └── fetch/ # Artifact fetching
│ ├── git.rs # Git repository fetch
│ ├── http.rs # HTTP download
│ ├── extract.rs # Archive extraction
│ └── checksum.rs# SHA256 verification (SHA1 deprecated)
```
## WHERE TO LOOK
| Task | Location |
|------|----------|
| Config loading | `config.rs` - FinalizeConfig |
| Email notify | `plugin/internal/mail/` - SMTP templates |
| Webhooks | `plugin/internal/webhook.rs` |
| Artifact fetch | `plugin/fetch/` - git/http/extract |
| Checksums | `plugin/fetch/checksum.rs:13` - SHA1 deprecated |
## CONVENTIONS
- **Plugin System**: Dynamic + internal plugins via Plugin trait
- **Template Syntax**: `{{VARIABLE}}` handlebars-style
- **Stages**: Hook → Fetch → Build → Publish → Notify → Cleanup
- **Fetch**: Supports git, http, with extraction and checksum verify
## ANTI-PATTERNS
1. **SHA1 deprecated**`checksum.rs:13` warns SHA1 is deprecated
2. **Unimplemented**`rhai.rs:17` - "todo!()" for Rhai scripting
3. **TODO markers** — Multiple TODOs in config.rs (lines 305-306)
+68
View File
@@ -290,6 +290,18 @@ impl FinalizeConfig {
Err(e) => errors.push(format!("Invalid version format: {}", e)),
}
// Validate plugin definitions: each plugin must have exactly one URL
for (plugin_name, urls) in &self.plugin {
match urls.len() {
0 => errors.push(format!("Plugin '{}' has no URL defined", plugin_name)),
1 => {}
n => errors.push(format!(
"Plugin '{}' has {} URL definitions, expected exactly 1",
plugin_name, n
)),
}
}
// TODO: Schema-based validation for plugin configs
// TODO: Validate that publish targets reference existing plugins
@@ -371,4 +383,60 @@ hooks:
let early = hooks.early.unwrap();
assert_eq!(early[0].timeout_ms, 30000);
}
#[test]
fn test_validate_plugin_zero_urls() {
let config: FinalizeConfig = serde_yaml::from_str(
r#"
version: "0.0.1"
plugin:
show_file_list: {}
"#,
)
.unwrap();
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors
.iter()
.any(|e| e.contains("show_file_list") && e.contains("no URL")));
}
#[test]
fn test_validate_plugin_one_url_ok() {
let config: FinalizeConfig = serde_yaml::from_str(
r#"
version: "0.0.1"
plugin:
show_file_list:
"https://example.com/plugin.tar.gz":
fallible: false
"#,
)
.unwrap();
let result = config.validate();
assert!(result.is_ok());
}
#[test]
fn test_validate_plugin_multiple_urls() {
let config: FinalizeConfig = serde_yaml::from_str(
r#"
version: "0.0.1"
plugin:
show_file_list:
"https://example.com/plugin1.tar.gz":
fallible: false
"https://example.com/plugin2.tar.gz":
fallible: false
"#,
)
.unwrap();
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors
.iter()
.any(|e| e.contains("show_file_list") && e.contains("2 URL definitions")));
}
}
+2 -2
View File
@@ -205,13 +205,13 @@ pub fn register(external_plugins: RawPluginMap) -> Result<PluginMap, PluginError
// Register internal plugin
for i in internal::get_internal_plugin() {
log::info!("Registering internal plugin {}", i.name);
log::debug!("Registering internal plugin {}", i.name);
plugins.insert(i.name.to_string(), FinalizePlugin::new(i.entry));
}
// Register external plugin
for (name, data) in external_plugins.into_iter() {
log::info!("Registering external plugin {}", name);
log::debug!("Registering external plugin {}", name);
let (_source, config) = data.into_iter().next().unwrap_or_else(|| {
panic!(
"Internal Error: Plugin {} has no source configured, this should have been caught by validation!",
@@ -49,15 +49,18 @@ pub async fn fetch_plugin(
// NOTE: as a practical compromise, we use git command for git clone
// gix will be supported later, with ONLY https scheme
// TODO: Implement gix
log::info!("Fetching git plugin: {}", url);
git::fetch_git_plugin(&path, name, basedir, argument.shallow).await
}
URLScheme::Http | URLScheme::Https => {
log::info!("Fetching http plugin: {}", url);
let checksum = argument.get_checksum();
http::fetch_http_plugin(url, name, basedir, checksum).await
}
URLScheme::File => {
// TODO: Support client mode
// NOTE: In standalone mode, we just assume path is well-defined and utilize
log::info!("Fetching file plugin: {}", url);
return Ok(());
}
URLScheme::Unknown => {
@@ -92,6 +92,7 @@ pub async fn fetch_git_plugin(
})
}
GitVersion::Single(version) => {
log::debug!("Fetching git plugin: {}@{}", name, version);
let shallow_flag = shallow.unwrap_or(false);
git_clone_and_checkout(
&baseurl,
@@ -103,6 +104,7 @@ pub async fn fetch_git_plugin(
.await
}
GitVersion::Mixed(tag, commit) => {
log::debug!("Fetching git plugin: {}@{}/{}", name, tag, commit);
let shallow_flag = shallow.unwrap_or(false);
let result = git_clone_and_checkout(
&baseurl,
@@ -48,6 +48,21 @@ pub async fn download_to_file(
Ok(())
}
fn extract_archive_extension(url: &str) -> &str {
let path_part = url.split('?').next().unwrap_or(url).split('#').next().unwrap_or(url);
let file_part = path_part.rsplit('/').next().unwrap_or("");
let lower = file_part.to_lowercase();
if lower.ends_with(".tar.gz") || lower.ends_with(".tgz") {
".tar.gz"
} else if lower.ends_with(".tar.zst") || lower.ends_with(".tar.zstd") || lower.ends_with(".tzst") {
".tar.zst"
} else if lower.ends_with(".tar") {
".tar"
} else {
".tar"
}
}
pub async fn fetch_http_plugin(
url: &str,
name: &str,
@@ -56,7 +71,8 @@ pub async fn fetch_http_plugin(
) -> Result<(), PluginError> {
let temp_dir = std::env::temp_dir().join("workshop-plugins");
tokio::fs::create_dir_all(&temp_dir).await.ok();
let temp_file = temp_dir.join(format!("{}-{}.tar.tmp", name, uuid::Uuid::new_v4()));
let ext = extract_archive_extension(url);
let temp_file = temp_dir.join(format!("{}-{}{}", name, uuid::Uuid::new_v4(), ext));
download_to_file(url, &temp_file, name).await?;
@@ -52,11 +52,11 @@ impl AsyncPluginFn for Mail {
};
let subject = render_template(&subject_template, &template_data)
.map_err(|e| PluginError::GeneralError(format!("Template render error: {}", e)))?;
.map_err(|e| PluginError::GeneralError(format!("Failed to render subject: {}", e)))?;
let body_html = render_template(&body_template_html, &template_data)
.map_err(|e| PluginError::GeneralError(format!("Template render error: {}", e)))?;
.map_err(|e| PluginError::GeneralError(format!("Failed to render body_html: {}", e)))?;
let body_text = render_template(&body_template_text, &template_data)
.map_err(|e| PluginError::GeneralError(format!("Template render error: {}", e)))?;
.map_err(|e| PluginError::GeneralError(format!("Failed to render body_text(?): {}", e)))?;
let email = build_email(&config, subject, body_html, body_text)?;
@@ -142,14 +142,17 @@ async fn send_email(
let mut transport_builder = match config.smtp.encryption {
Encryption::Tls => {
log::debug!("Using TLS encryption");
AsyncSmtpTransport::<Tokio1Executor>::relay(&config.smtp.host)
.map_err(|e| PluginError::GeneralError(format!("Invalid SMTP host: {}", e)))?
}
Encryption::Starttls => {
log::debug!("Using StartTLS encryption");
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&config.smtp.host)
.map_err(|e| PluginError::GeneralError(format!("Invalid SMTP host: {}", e)))?
}
Encryption::None => {
log::debug!("Using NO encryption");
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&config.smtp.host)
}
};
@@ -158,12 +161,15 @@ async fn send_email(
match config.smtp.auth.auth_type {
AuthType::Password => {
log::debug!("Using password auth");
transport_builder = transport_builder.credentials((
config.smtp.auth.username.clone(),
config.smtp.auth.password.clone()
).into());
}
AuthType::None => {}
AuthType::None => {
log::debug!("Using no auth");
}
}
let transport = transport_builder.build();
@@ -165,7 +165,6 @@ impl DefaultTemplates {
<tr><td><strong>Build ID:</strong></td><td>{{ build.id }}</td></tr>
<tr><td><strong>Status:</strong></td><td>{{ build.status }}</td></tr>
<tr><td><strong>Duration:</strong></td><td>{{ build.duration }}</td></tr>
<tr><td><strong>Commit:</strong></td><td>{{ commit.hash | slice(0, 7) }}</td></tr>
<tr><td><strong>Author:</strong></td><td>{{ commit.author }}</td></tr>
<tr><td><strong>Message:</strong></td><td>{{ commit.message }}</td></tr>
</table>
+21 -23
View File
@@ -91,7 +91,7 @@ fn convert_argument(prefix: &str, argument: serde_yaml::Value, in_sequence: bool
envvars.insert(prefix.to_string(), value.to_string());
}
serde_yaml::Value::String(value) => {
envvars.insert(prefix.to_string(), format!("\"{}\"", value));
envvars.insert(prefix.to_string(), format!("{}", value));
}
serde_yaml::Value::Number(value) => {
envvars.insert(prefix.to_string(), value.to_string());
@@ -199,8 +199,8 @@ mod tests {
let result = convert_argument("PLUGIN", yaml, false);
// Keys are uppercased in actual call() function
assert_eq!(result.get("PLUGIN_HOST"), Some(&"\"docker.io\"".to_string()));
assert_eq!(result.get("PLUGIN_USER"), Some(&"\"admin\"".to_string()));
assert_eq!(result.get("PLUGIN_HOST"), Some(&"docker.io".to_string()));
assert_eq!(result.get("PLUGIN_USER"), Some(&"admin".to_string()));
}
#[test]
@@ -213,7 +213,7 @@ from:
let result = convert_argument("CFG", yaml, false);
// Nested keys use uppercase format: CFG_FROM_HOST
assert_eq!(result.get("CFG_FROM_HOST"), Some(&"\"smtp.example.com\"".to_string()));
assert_eq!(result.get("CFG_FROM_HOST"), Some(&"smtp.example.com".to_string()));
assert_eq!(result.get("CFG_FROM_PORT"), Some(&"587".to_string()));
}
@@ -231,7 +231,7 @@ empty: ~
assert_eq!(result.get("VAR_ENABLED"), Some(&"true".to_string()));
assert_eq!(result.get("VAR_COUNT"), Some(&"42".to_string()));
assert_eq!(result.get("VAR_RATIO"), Some(&"3.14".to_string()));
assert_eq!(result.get("VAR_NAME"), Some(&"\"test\"".to_string()));
assert_eq!(result.get("VAR_NAME"), Some(&"test".to_string()));
assert!(!result.contains_key("VAR_EMPTY"));
}
@@ -240,8 +240,7 @@ empty: ~
let yaml = serde_yaml::from_str("tags:\n - latest\n - v1.0\n - stable").unwrap();
let result = convert_argument("CFG", yaml, false);
// Strings in arrays are quoted by convert_argument
assert_eq!(result.get("CFG_TAGS"), Some(&"(\"latest\" \"v1.0\" \"stable\")".to_string()));
assert_eq!(result.get("CFG_TAGS"), Some(&"(latest v1.0 stable)".to_string()));
}
#[test]
@@ -255,10 +254,10 @@ env:
").unwrap();
let result = convert_argument("CFG", yaml, false);
assert_eq!(result.get("CFG_ENV_0_NAME"), Some(&"\"KEY1\"".to_string()));
assert_eq!(result.get("CFG_ENV_0_VALUE"), Some(&"\"val1\"".to_string()));
assert_eq!(result.get("CFG_ENV_1_NAME"), Some(&"\"KEY2\"".to_string()));
assert_eq!(result.get("CFG_ENV_1_VALUE"), Some(&"\"val2\"".to_string()));
assert_eq!(result.get("CFG_ENV_0_NAME"), Some(&"KEY1".to_string()));
assert_eq!(result.get("CFG_ENV_0_VALUE"), Some(&"val1".to_string()));
assert_eq!(result.get("CFG_ENV_1_NAME"), Some(&"KEY2".to_string()));
assert_eq!(result.get("CFG_ENV_1_VALUE"), Some(&"val2".to_string()));
assert!(!result.contains_key("CFG_ENV"));
}
@@ -305,7 +304,7 @@ token: ~
let result = convert_argument("CFG", yaml, false);
// Keys are uppercased: CFG_HOST
assert_eq!(result.get("CFG_HOST"), Some(&"\"docker.io\"".to_string()));
assert_eq!(result.get("CFG_HOST"), Some(&"docker.io".to_string()));
// Null value is skipped (not set)
assert!(!result.contains_key("CFG_TOKEN"));
}
@@ -319,8 +318,8 @@ token: ~
let result = convert_argument("CFG", yaml, false);
// Number keys are preserved but prefixed: CFG_0, CFG_1
assert_eq!(result.get("CFG_0"), Some(&"\"first\"".to_string()));
assert_eq!(result.get("CFG_1"), Some(&"\"second\"".to_string()));
assert_eq!(result.get("CFG_0"), Some(&"first".to_string()));
assert_eq!(result.get("CFG_1"), Some(&"second".to_string()));
}
#[test]
@@ -340,12 +339,11 @@ plugin:
let result = convert_argument("CFG", yaml, false);
// All keys uppercased for shell environment variables
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_HOST"), Some(&"\"docker.io\"".to_string()));
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_IMAGE"), Some(&"\"user/app:tag\"".to_string()));
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_HOST"), Some(&"docker.io".to_string()));
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_IMAGE"), Some(&"user/app:tag".to_string()));
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_OPTIONS_FORCE"), Some(&"true".to_string()));
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_OPTIONS_RETRIES"), Some(&"3".to_string()));
// Strings in sequences are quoted
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_TAGS"), Some(&"(\"latest\" \"v1.0\")".to_string()));
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_TAGS"), Some(&"(latest v1.0)".to_string()));
}
#[test]
@@ -358,9 +356,9 @@ token: 'Bearer abc123'
let result = convert_argument("CFG", yaml, false);
// Keys uppercased: CFG_URL, CFG_PATH, CFG_TOKEN
assert_eq!(result.get("CFG_URL"), Some(&"\"https://example.com/path?query=value\"".to_string()));
assert_eq!(result.get("CFG_PATH"), Some(&"\"/workspace/output.tar.gz\"".to_string()));
assert_eq!(result.get("CFG_TOKEN"), Some(&"\"Bearer abc123\"".to_string()));
assert_eq!(result.get("CFG_URL"), Some(&"https://example.com/path?query=value".to_string()));
assert_eq!(result.get("CFG_PATH"), Some(&"/workspace/output.tar.gz".to_string()));
assert_eq!(result.get("CFG_TOKEN"), Some(&"Bearer abc123".to_string()));
}
#[test]
@@ -372,7 +370,7 @@ cmd: \"echo 'test'\"
let result = convert_argument("CFG", yaml, false);
// Keys uppercased: CFG_MSG, CFG_CMD
assert_eq!(result.get("CFG_MSG"), Some(&"\"Hello World\"".to_string()));
assert_eq!(result.get("CFG_CMD"), Some(&"\"echo 'test'\"".to_string()));
assert_eq!(result.get("CFG_MSG"), Some(&"Hello World".to_string()));
assert_eq!(result.get("CFG_CMD"), Some(&"echo 'test'".to_string()));
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ pub async fn hook(
log::error!("Plugin hook {} failed: {:?}", index, e);
ExecutionError::ExecutionFailed(e.to_string())
})?;
return Ok(());
continue;
}
let deltactx = DeltaExecutionContext {
working_dir: hook_cmd.working_dir.as_ref().map(PathBuf::from),
+51
View File
@@ -0,0 +1,51 @@
# workshop-baker/src/prebake
**Generated:** 2026-04-22
**Commit:** 7b3b71a
Build environment setup: bootstrap, dependencies, security, hooks.
## STRUCTURE
```
prebake/
├── config.rs # PrebakeConfig YAML schema (environment, deps, cache)
├── stage.rs # PrebakeStage enum (Bootstrap→Ready ordering)
├── stage/
│ ├── bootstrap.rs # User creation (vulcan), sudo/doas setup
│ ├── environment.rs # Env var injection
│ ├── depssystem.rs # System package installation
│ ├── depsuser.rs # User package installation (cargo, npm, etc.)
│ └── hook.rs # Hook execution (pre/post stage)
├── env/ # Environment providers
│ ├── mod.rs # EnvProvider trait
│ ├── container.rs # Docker/container support
│ ├── firecracker.rs # Firecracker microVM
│ ├── baremetal.rs # Direct host execution
│ └── custom.rs # Custom env (todo!())
├── security.rs # Privilege dropping (unsafe libc calls)
└── event.rs # Event logging
```
## WHERE TO LOOK
| Task | Location |
|------|----------|
| Config loading | `config.rs:PrebakeConfig::from_yaml()` |
| Stage ordering | `stage.rs:PrebakeStage` enum |
| User setup | `stage/bootstrap.rs` - vulcan user creation |
| Security | `security.rs:72` - privilege dropping |
| Hooks | `stage/hook.rs` - pre/post stage hooks |
## CONVENTIONS
- **PrebakeStage**: Ordered enum (Bootstrap→DepsSystem→DepsUser→Environment→Hooks→Ready)
- **ExecutionContext**: Carries task_id, username, env_vars through all stages
- **Target User**: "vulcan" (hardcoded in bootstrap.rs:6-24)
## ANTI-PATTERNS
1. **Hardcoded user** — "vulcan" username hardcoded throughout
2. **Unsafe blocks**`security.rs` has 5+ unsafe libc calls
3. **Incomplete** — custom.rs and apt.rs have `todo!()` stubs
4. **doas.conf** — bootstrap.rs:183: "We do not know how to write doas.conf lol"
-7
View File
@@ -1,7 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "workshop-builder-native"
version = "0.1.0"
-6
View File
@@ -1,6 +0,0 @@
[package]
name = "workshop-builder-native"
version = "0.1.0"
edition = "2024"
[dependencies]
-36
View File
@@ -1,36 +0,0 @@
// src/executors/error.rs
use std::io;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ExecutionError {
#[error("I/O error: {0}")]
IoError(String),
#[error("YAML parse error: {0}")]
YamlParseError(String),
#[error("Command execution error: {0}")]
CommandExecutionError(String),
#[error("Unsupported platform: {0}")]
UnsupportedPlatform(String),
#[error("Template error: {0}")]
TemplateError(#[from] crate::services::template_engine::TemplateError),
#[error("Environment error: {0}")]
EnvironmentError(#[from] crate::services::environment_manager::EnvironmentError),
}
impl From<io::Error> for ExecutionError {
fn from(error: io::Error) -> Self {
ExecutionError::IoError(error.to_string())
}
}
impl From<serde_yaml::Error> for ExecutionError {
fn from(error: serde_yaml::Error) -> Self {
ExecutionError::YamlParseError(error.to_string())
}
}
-3
View File
@@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}
+49
View File
@@ -0,0 +1,49 @@
# workshop-cert/src
**Generated:** 2026-04-22
**Commit:** 7b3b71a
TLS certificate generator (CA, server, client certificates).
## STRUCTURE
```
src/
├── main.rs # CLI entry (gen-ca, gen-server, gen-client, gen-all)
├── lib.rs # Library exports
├── generator.rs # Certificate generation logic
└── config.rs # Certificate config from cert.toml
```
## WHERE TO LOOK
| Task | Location |
|------|----------|
| CLI commands | `main.rs:144` - 4 subcommands |
| Cert generation | `generator.rs` - rcgen-based |
| Config loading | `config.rs` - cert.toml parser |
## BINARY
```bash
workshop_certgen gen-ca # Generate CA cert/key
workshop_certgen gen-server # Generate server cert
workshop_certgen gen-client # Generate client cert
workshop_certgen gen-all # Generate all
```
## ANTI-PATTERNS
**CRITICAL**: `certs/` directory contains private keys in source:
- `ca-key.pem` - CA private key
- `server-key.pem` - Server private key
These should be gitignored and generated at deploy time.
## CONFIG
`cert.toml` defines:
- CA validity: 10 years
- Server/client validity: 1 year
- Subject fields (CN, O, OU)
- Key algorithm (Ed25519)
-169
View File
@@ -1,169 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "if-addrs"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf39cc0423ee66021dc5eccface85580e4a001e0c5288bae8bea7ecb69225e90"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "macaddr"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8"
[[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 = "route-rs"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66fa49dc79c685a59fa33339e126a794a67cf4ed865c2c9785268e23ec51409"
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
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 = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "workshop-helper-mac"
version = "0.1.0"
dependencies = [
"if-addrs",
"macaddr",
"route-rs",
"thiserror",
]
-10
View File
@@ -1,10 +0,0 @@
[package]
name = "workshop-helper-mac"
version = "0.1.0"
edition = "2024"
[dependencies]
if-addrs = "0.14.0"
macaddr = "1.0.1"
route-rs = "0.1.0"
thiserror = "2.0.17"
-69
View File
@@ -1,69 +0,0 @@
use if_addrs::IfAddr;
use macaddr::MacAddr;
use std::net::IpAddr;
use thiserror::Error;
// 定义一个自定义的错误类型,让错误处理更优雅
#[derive(Error, Debug)]
enum MainMacError {
#[error("无法获取网络接口列表: {0}")]
IfAddrsError(#[from] if_addrs::Error),
#[error("无法获取路由信息: {0}")]
RouteError(#[from] route_rs::Error),
#[error("未找到默认路由")]
NoDefaultRoute,
#[error("默认路由对应的接口 '{0}' 未在接口列表中找到")]
InterfaceNotFound(String),
#[error("接口 '{0}' 没有 MAC 地址")]
NoMacAddress(String),
#[error("找到的主网卡 '{0}' 是虚拟接口,已跳过")]
VirtualInterface(String),
}
/// 检查接口名称是否暗示它是一个虚拟接口
fn is_virtual_interface(name: &str) -> bool {
let virtual_patterns = ["tun", "tap", "virbr", "veth", "lo"];
virtual_patterns.iter().any(|&pat| name.contains(pat))
}
/// 寻找并返回主网卡的 MAC 地址
fn find_main_mac_address() -> Result<MacAddr, MainMacError> {
// 步骤 1: 获取默认路由,以确定主网卡的名称
let default_route = route_rs::get_default_route()?;
let iface_name = default_route.interface_name().ok_or(MainMacError::NoDefaultRoute)?;
println!("默认路由所在的接口名称: '{}'", iface_name);
// 步骤 2: 检查是否为虚拟接口
if is_virtual_interface(iface_name) {
return Err(MainMacError::VirtualInterface(iface_name.to_string()));
}
// 步骤 3: 获取所有网络接口的详细信息
let if_addrs = if_addrs::get_if_addrs()?;
// 步骤 4: 遍历接口列表,找到与默认路由接口名匹配的接口
for if_addr in if_addrs {
if if_addr.name == iface_name {
// 找到了匹配的接口,提取其 MAC 地址
return if_addr.mac_addr.ok_or_else(|| MainMacError::NoMacAddress(iface_name.to_string()));
}
}
// 如果循环结束都没有找到,说明出了问题
Err(MainMacError::InterfaceNotFound(iface_name.to_string()))
}
fn main() {
println!("正在寻找主网卡的 MAC 地址...");
match find_main_mac_address() {
Ok(mac) => println!("成功找到主网卡 MAC 地址: {}", mac),
Err(e) => eprintln!("失败: {}", e),
}
}
-1
View File
@@ -1 +0,0 @@
target
-2045
View File
File diff suppressed because it is too large Load Diff
-25
View File
@@ -1,25 +0,0 @@
[package]
name = "workshop-llm-detector"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "workshop-llm-detector"
path = "src/main.rs"
[lib]
name = "workshop_llm_detector"
path = "src/lib.rs"
[dependencies]
anyhow = "1.0.100"
async-trait = "0.1.89"
clap = { version = "4.5.51", features = ["derive"] }
log = "0.4.28"
ollama-rs = "0.3.2"
openai_api_rust = "0.1.9"
reqwest = { version = "0.12.24", features = ["json"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_yaml = "0.9.34"
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
@@ -1,29 +0,0 @@
name: "Rust"
project_types:
- "rust"
- "cargo"
install_path: "/install"
artifacts:
- "/install/bin/*"
- "/install/lib/*.so"
- "/install/lib/*.a"
- "/target/release/*"
dependencies:
deb:
- "build-essential"
- "curl"
- "pkg-config"
- "libssl-dev"
rpm:
- "gcc"
- "gcc-c++"
- "curl"
- "openssl-devel"
pacman:
- "base-devel"
- "curl"
- "openssl"
environment:
RUSTUP_DIST_SERVER: "https://static.rust-lang.org"
CARGO_HOME: "/usr/local/cargo"
RUSTUP_HOME: "/usr/local/rustup"
@@ -1,24 +0,0 @@
name: "make"
project_types:
- "make"
- "makefile"
install_path: "/install"
artifacts:
- "/install/*"
dependencies:
deb:
- "build-essential"
- "curl"
- "pkg-config"
- "libssl-dev"
rpm:
- "gcc"
- "gcc-c++"
- "curl"
- "openssl-devel"
pacman:
- "base-devel"
- "curl"
- "openssl"
environment:
NONE: "none"
-40
View File
@@ -1,40 +0,0 @@
# 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
-62
View File
@@ -1,62 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cookbook {
pub name: String,
pub project_types: Vec<String>,
pub install_path: String,
pub artifacts: Vec<String>,
pub dependencies: HashMap<String, Vec<String>>, // key: package_type (deb, rpm, etc.)
pub environment: HashMap<String, String>,
}
#[derive(Debug, thiserror::Error)]
pub enum CookbookError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("YAML error: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("Cookbook not found for project type: {0}")]
NotFound(String),
}
pub struct CookbookManager {
cookbooks: HashMap<String, Cookbook>,
}
impl CookbookManager {
pub fn load_from_dir<P: AsRef<Path>>(dir: P) -> Result<Self, CookbookError> {
let mut cookbooks = HashMap::new();
let entries = fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "yaml" || ext == "yml") {
if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) {
if file_name.ends_with(".cookbook") {
let content = fs::read_to_string(&path)?;
let cookbook: Cookbook = serde_yaml::from_str(&content)?;
// Register all project types for this cookbook
for project_type in &cookbook.project_types {
cookbooks.insert(project_type.clone(), cookbook.clone());
}
}
}
}
}
Ok(CookbookManager { cookbooks })
}
pub fn get_cookbook(&self, project_type: &str) -> Result<&Cookbook, CookbookError> {
self.cookbooks
.get(project_type)
.ok_or_else(|| CookbookError::NotFound(project_type.to_string()))
}
}
-175
View File
@@ -1,175 +0,0 @@
mod cookbook;
mod llm;
mod llm_output;
pub use cookbook::{Cookbook, CookbookManager, CookbookError};
pub use llm::{LLMClient, ollama::OllamaClient, openai::OpenAIClient};
pub use llm_output::LLMOutput;
use serde::Serialize;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use anyhow::Result;
use serde_yaml;
pub struct CICDLLMHelper {
cookbook_manager: CookbookManager,
}
impl CICDLLMHelper {
pub fn new(cookbook_dir: &str) -> Result<Self, CookbookError> {
let cookbook_manager = CookbookManager::load_from_dir(cookbook_dir)?;
Ok(Self { cookbook_manager })
}
pub async fn analyze_repository(
&self,
repository_path: &str,
additional_files: &[String],
llm_client: &dyn LLMClient,
package_type: &str,
) -> Result<AnalysisResult> {
// Read README.md
let readme_path = Path::new(repository_path).join("README.md");
let readme_content = fs::read_to_string(&readme_path)
.unwrap_or_else(|_| "No README.md found".to_string());
// Read additional files
let mut file_contents = Vec::new();
for file in additional_files {
let file_path = Path::new(repository_path).join(file);
if let Ok(content) = fs::read_to_string(&file_path) {
file_contents.push(format!("File: {}\n{}\n", file, content));
}
}
// Prepare prompt for LLM
let prompt = self.build_prompt(&readme_content, &file_contents, package_type);
// Get LLM response
let llm_response = llm_client.generate(&prompt).await?;
println!("LLM Response: {}", llm_response);
// Parse LLM output
let llm_output: LLMOutput = serde_yaml::from_str(&llm_response)
.map_err(|e| anyhow::anyhow!("Failed to parse LLM output: {}", e))?;
// Get cookbook for project type
let cookbook = self.cookbook_manager.get_cookbook(&llm_output.project_type)?;
// Merge cookbook and LLM output
let result = self.merge_outputs(&llm_output, cookbook, package_type);
Ok(result)
}
fn build_prompt(&self, readme_content: &str, file_contents: &[String], package_type: &str) -> String {
format!(
r#"Analyze the following repository files and determine the build configuration.
README.md:
{}
Additional files:
{}
Please provide the analysis in YAML format with the following structure:
project_type: <string>
dependencies: <list of package names for {package_type} packages>
build_steps: <list of shell commands>
artifacts: <list of artifact paths, use /install as base directory>
environment: <key-value pairs for environment variables>
other: <any other relevant information>
Focus on identifying:
1. The project type (e.g., rust, python, nodejs, cmake, make, etc.)
2. Dependencies needed for building (as {package_type} packages)
3. Build steps as shell commands
4. Artifact paths (output files, typically in /install)
5. Environment variables needed for building
Output only valid YAML, no additional text and remove '```' in output(output as plain text)."#,
readme_content,
file_contents.join("\n"),
package_type = package_type
)
}
fn merge_outputs(&self, llm_output: &LLMOutput, cookbook: &Cookbook, package_type: &str) -> AnalysisResult {
// Merge dependencies
let cookbook_deps = cookbook.dependencies.get(package_type)
.cloned()
.unwrap_or_default();
let mut all_dependencies = cookbook_deps;
all_dependencies.extend_from_slice(&llm_output.dependencies);
all_dependencies.sort();
all_dependencies.dedup();
// Merge environment variables
let mut all_environment = cookbook.environment.clone();
for (key, value) in &llm_output.environment {
all_environment.insert(key.clone(), value.clone());
}
// Merge artifacts
let mut all_artifacts = cookbook.artifacts.clone();
all_artifacts.extend_from_slice(&llm_output.artifacts);
all_artifacts.sort();
all_artifacts.dedup();
AnalysisResult {
dependencies: all_dependencies,
build_steps: llm_output.build_steps.clone(),
artifacts: all_artifacts,
environment: all_environment,
project_type: llm_output.project_type.clone(),
other: llm_output.other.clone(),
}
}
pub fn write_output_files(&self, result: &AnalysisResult, output_dir: &str) -> Result<()> {
let output_path = Path::new(output_dir);
// Write prebake.tmpl.yaml
let prebake_content = serde_yaml::to_string(&PrebakeTemplate {
dependencies: &result.dependencies,
environment: &result.environment,
})?;
fs::write(output_path.join("prebake.tmpl.yaml"), prebake_content)?;
// Write finalize.tmpl.yaml
let finalize_content = serde_yaml::to_string(&FinalizeTemplate {
artifacts: &result.artifacts,
})?;
fs::write(output_path.join("finalize.tmpl.yaml"), finalize_content)?;
// Write build.sh
let build_script = result.build_steps.join("\n");
fs::write(output_path.join("build.sh"), build_script)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct AnalysisResult {
pub dependencies: Vec<String>,
pub build_steps: Vec<String>,
pub artifacts: Vec<String>,
pub environment: HashMap<String, String>,
pub project_type: String,
pub other: HashMap<String, serde_yaml::Value>,
}
#[derive(Serialize)]
struct PrebakeTemplate<'a> {
dependencies: &'a Vec<String>,
environment: &'a HashMap<String, String>,
}
#[derive(Serialize)]
struct FinalizeTemplate<'a> {
artifacts: &'a Vec<String>,
}
-7
View File
@@ -1,7 +0,0 @@
use async_trait::async_trait;
use anyhow::Result;
#[async_trait]
pub trait LLMClient {
async fn generate(&self, prompt: &str) -> Result<String>;
}
-6
View File
@@ -1,6 +0,0 @@
mod client;
pub use client::LLMClient;
pub mod ollama;
pub mod openai;
-27
View File
@@ -1,27 +0,0 @@
use async_trait::async_trait;
use anyhow::Result;
use ollama_rs::generation::completion::request::GenerationRequest;
use ollama_rs::Ollama;
use super::LLMClient;
pub struct OllamaClient {
client: Ollama,
model: String,
}
impl OllamaClient {
pub fn new(url: String, model: String) -> Self {
let client = Ollama::new(url, 11434);
Self { client, model }
}
}
#[async_trait]
impl LLMClient for OllamaClient {
async fn generate(&self, prompt: &str) -> Result<String> {
let request = GenerationRequest::new(self.model.clone(), prompt.to_string());
let response = self.client.generate(request).await?;
Ok(response.response)
}
}
-56
View File
@@ -1,56 +0,0 @@
use async_trait::async_trait;
use anyhow::Result;
use openai_api_rust::*;
use openai_api_rust::chat::*;
use super::LLMClient;
pub struct OpenAIClient {
client: OpenAI,
model: String,
}
impl OpenAIClient {
pub fn new(api_key: String, base_url: Option<String>, model: String) -> Self {
let auth = Auth::new(api_key.as_str());
let client = if let Some(url) = base_url {
OpenAI::new(auth, url.as_str())
} else {
OpenAI::new(auth, "https://api.openai.com/v1/")
};
Self { client, model }
}
}
#[async_trait]
impl LLMClient for OpenAIClient {
async fn generate(&self, prompt: &str) -> Result<String> {
let message = Message {
role: Role::User,
content: prompt.to_string(),
};
let body = ChatBody {
model: self.model.clone(),
messages: vec![message],
max_tokens: Some(2048),
temperature: Some(0.1),
top_p: Some(0.1),
n: Some(1),
stream: Some(false),
stop: None,
presence_penalty: None,
frequency_penalty: None,
logit_bias: None,
user: None,
};
let response = self.client.chat_completion_create(&body).unwrap();
if let Some(choice) = response.choices.first() {
Ok(choice.message.as_ref().unwrap().content.clone())
} else {
anyhow::bail!("No response from OpenAI")
}
}
}
-13
View File
@@ -1,13 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LLMOutput {
pub project_type: String,
pub dependencies: Vec<String>,
pub build_steps: Vec<String>,
pub artifacts: Vec<String>,
pub environment: HashMap<String, String>,
#[serde(flatten)]
pub other: HashMap<String, serde_yaml::Value>,
}
-87
View File
@@ -1,87 +0,0 @@
use clap::Parser;
use log::error;
use workshop_llm_detector::{CICDLLMHelper, LLMClient, OllamaClient, OpenAIClient};
// use std::path::Path;
use anyhow::Result;
#[derive(Parser)]
#[command(name = "workshop-llm-detector")]
#[command(about = "LLM helper for analyzing repositories")]
struct Cli {
/// Repository path to analyze
repository_path: String,
/// Additional files to read (comma separated)
#[arg(short, long, value_delimiter = ',')]
files: Vec<String>,
/// LLM Provider
#[arg(long, default_value = "openai")]
provider: String,
/// Model to use
#[arg(long)]
model: String,
/// Model API URL
#[arg(long)]
model_url: Option<String>,
/// Package type for dependencies
#[arg(long, default_value = "deb")]
package_type: String,
/// Cookbook directory
#[arg(long, default_value = "cookbook")]
cookbook_dir: String,
/// Output directory
#[arg(short, long, default_value = ".")]
output_dir: String,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Cli::parse();
// Initialize cookbook manager
let helper = CICDLLMHelper::new(&args.cookbook_dir)?;
// Initialize LLM client
let llm_client: Box<dyn LLMClient> = match args.provider.as_str() {
"openai" => {
let api_key = std::env::var("OPENAI_API_KEY")
.expect("OPENAI_API_KEY environment variable must be set");
let base_url = args.model_url.clone();
Box::new(OpenAIClient::new(api_key, base_url, args.model))
}
"ollama" => {
let url = args.model_url.unwrap_or_else(|| "http://localhost:11434".to_string());
Box::new(OllamaClient::new(url, args.model))
}
_ => {
error!("Unsupported provider: {}", args.provider);
std::process::exit(1);
}
};
// Analyze repository
let result = helper.analyze_repository(
&args.repository_path,
&args.files,
&*llm_client,
&args.package_type,
).await?;
println!("Analysis completed for project type: {}", result.project_type);
println!("Found {} dependencies", result.dependencies.len());
println!("Generated {} build steps", result.build_steps.len());
println!("Identified {} artifacts", result.artifacts.len());
// Write output files
helper.write_output_files(&result, &args.output_dir)?;
println!("Output files written to {}", args.output_dir);
Ok(())
}
+42
View File
@@ -0,0 +1,42 @@
# workshop-vault/src
**Generated:** 2026-04-22
**Commit:** 7b3b71a
Secret management client for HashiCorp Vault.
## STRUCTURE
```
src/
├── lib.rs # Library entry, re-exports
├── client.rs # WorkshopSecretManager client
├── rusty_vault_client.rs # RustyVault adapter
├── models.rs # Secret, SecretEngine structs
├── error.rs # VaultError enum
└── examples/usage.rs # Usage examples
```
## WHERE TO LOOK
| Symbol | Type | Location | Purpose |
|--------|------|----------|---------|
| create_vault_client | fn | lib.rs:45 | Create client from config |
| create_vault_client_from_env | fn | lib.rs:60 | Create from env vars |
| WorkshopSecretManager | struct | client.rs:16 | Main client |
| Secret | struct | models.rs:5 | Secret data |
| VaultError | enum | error.rs:8 | Error types |
## CONVENTIONS
- **Library only**: No main.rs, only lib.rs
- **Adapter pattern**: rusty_vault_client.rs wraps rusty_vault crate
- **Error handling**: thiserror for VaultError
- **Async**: All operations async via rusty_vault
## DEPENDENCIES
- `rusty_vault` - Vault client library
- `async-trait` - Async trait support
- `tokio` - Async runtime
- `serde` - Serialization