feat: add workshop-pipeline

Signed-off-by: Catty Steve <4795515+Catty2014@user.noreply.gitee.com>
Workshop-pipeline is a crate for parsing prebake/bake/finalize config.
Not finished yet, further testing required.
This commit is contained in:
Catty Steve
2025-11-14 11:05:05 +08:00
parent f66a36d0f4
commit f7ea4dfb3e
24 changed files with 2235 additions and 0 deletions
+375
View File
@@ -0,0 +1,375 @@
# 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
@@ -0,0 +1,16 @@
[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"
@@ -0,0 +1,254 @@
#!/bin/bash
# .workshop/bake
# 构建阶段脚本 - 使用标准的bash语法
set -euo pipefail # 严格模式:错误退出、未定义变量检查、管道错误检查
# 颜色定义用于输出
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
# 日志函数
log() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"
}
log_warn() {
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*${NC}"
}
log_error() {
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*${NC}"
}
log_info() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*${NC}"
}
# 环境变量(由CI/CD系统注入)
readonly WORKSPACE_DIR="${WORKSPACE_DIR:-/workspace}"
readonly BUILD_ID="${BUILD_ID:-unknown}"
readonly COMMIT_SHA="${COMMIT_SHA:-}"
readonly BRANCH_NAME="${BRANCH_NAME:-main}"
readonly BUILD_CACHE_DIR="${BUILD_CACHE_DIR:-/cache}"
# 构建配置(从prebake和环境变量读取)
readonly RUST_VERSION="${RUST_VERSION:-1.70}"
readonly BUILD_TYPE="${BUILD_TYPE:-release}"
readonly CARGO_REGISTRY="${CARGO_REGISTRY:-$BUILD_CACHE_DIR/cargo/registry}"
readonly CARGO_GIT="${CARGO_GIT:-$BUILD_CACHE_DIR/cargo/git}"
# 切换到工作目录
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 "$@"
@@ -0,0 +1,286 @@
# .workshop/finalize.yaml
# 版本号
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:
# 测试覆盖率要求
test_coverage:
enabled: true
minimum: 80.0
file: "coverage/lcov.info"
# 安全扫描
security_scan:
enabled: true
tools:
- "cargo-audit"
- "trivy"
fail_on: "high" # low, medium, high, critical
exceptions:
- "CVE-2021-12345" # 已知例外
# 代码质量
code_quality:
enabled: true
maximum_duplication: 5.0
maximum_complexity: 25.0
minimum_maintainability: 50.0
# 部署配置
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"
# 部署策略
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}}"
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}}"
severity: "error"
# 部署通知
on_deploy:
- type: "webhook"
url: "https://api.example.com/deployments"
method: "POST"
headers:
Authorization: "Bearer {{env.DEPLOY_WEBHOOK_TOKEN}}"
# 清理策略
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"
@@ -0,0 +1,180 @@
# .workshop/prebake.yaml
# 版本号,用于格式兼容性检查
version: "1.0"
# 构建环境定义
environment:
# 构建机类型
builder: "docker" # 或 "firecracker", "custom", "baremetal"
# 构建机配置
config:
baremetal:
memory_mb: 2048
cpus: 2
arch: x86_64
# Docker 构建机配置
docker:
base_image: "rust:1.70-slim"
dockerfile: |
FROM rust:1.70-slim
WORKDIR /app
RUN apt-get update && apt-get install -y pkg-config libssl-dev
build_args:
- "RUST_VERSION=1.70"
# Firecracker 构建机配置
firecracker:
kernel_image: "file:///opt/firecracker/vmlinux"
rootfs_image: "file:///opt/firecracker/rootfs.ext4"
memory_mb: 1024
vcpus: 2
boot_args: "console=ttyS0 reboot=k panic=1 pci=off"
# 自定义构建机配置
custom:
name: "my-custom-builder"
setup_script: "setup-builder.sh"
cleanup_script: "cleanup-builder.sh"
# 硬件资源要求
resources:
cpu: 2 # CPU核心数
memory: "2G" # 内存大小
disk: "10G" # 磁盘空间
architecture: "x86_64" # 架构要求
gpu: false # 是否需要GPU
gpu_type: "cuda" # GPU类型(如果需要)
# 网络配置
network:
enabled: true
outbound: true # 允许出站连接
dns_servers:
- "8.8.8.8"
- "1.1.1.1"
# 依赖定义
dependencies:
# 系统包依赖
system:
packages:
- "git"
- "curl"
- "build-essential"
- "pkg-config"
- "libssl-dev"
repositories:
- "ppa:custom/ppa" # 自定义PPAUbuntu
# 语言特定依赖
languages:
rust:
version: "1.70"
toolchain: "stable"
components:
- "rustfmt"
- "clippy"
targets:
- "x86_64-unknown-linux-gnu"
node:
version: "18"
package_manager: "npm" # 或 "yarn", "pnpm"
registry: "https://registry.npmjs.org/"
python:
version: "3.11"
package_manager: "pip" # 或 "poetry", "conda"
requirements_file: "requirements.txt"
go:
version: "1.19"
modules: true
# 自定义依赖步骤
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
# 环境变量配置
environment_variables:
# 构建环境变量
build:
RUST_BACKTRACE: "full"
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
NODE_ENV: "production"
# 运行时环境变量(会传递给bake阶段)
runtime:
DATABASE_URL: "postgresql://user:pass@localhost/db"
LOG_LEVEL: "info"
# 缓存配置
cache:
# 缓存目录
directories:
- path: "/usr/local/cargo"
strategy: "persistent" # persistent, build, session
compression: "zstd"
- path: "/root/.npm"
strategy: "build"
compression: "none"
- path: "/app/target"
strategy: "persistent"
compression: "zstd"
# 缓存策略
strategy:
ttl_days: 30 # 缓存生存时间
max_size_gb: 20 # 最大缓存大小
cleanup_policy: "lru" # 清理策略:lru, fifo, size
# 安全配置
security:
# 权限限制
capabilities:
- "NET_BIND_SERVICE"
# 安全策略
seccomp: "strict" # strict, filter, disabled
apparmor: true
no_new_privileges: true
# 资源限制
limits:
max_processes: 512
max_file_size: "1G"
max_open_files: 1024
# 钩子脚本
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"
+27
View File
@@ -0,0 +1,27 @@
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(())
}
+79
View File
@@ -0,0 +1,79 @@
pub mod prebake;
pub mod finalize;
pub mod scripts;
pub use prebake::*;
pub use finalize::*;
pub use scripts::*;
// use log::info;
// use crate::TemplateResolvable;
// 完整的流水线配置
// #[derive(Debug, Clone, serde::Serialize)]
// pub struct PipelineConfig {
// pub prebake: PrebakeConfig,
// pub bake: BakeScript,
// pub finalize: FinalizeConfig,
// pub variables: std::collections::HashMap<String, crate::variable::VariableValue>,
// }
// impl PipelineConfig {
// /// 从文件系统加载完整配置
// pub fn load_from_dir<P: AsRef<std::path::Path>>(
// dir: P,
// user_id: &str,
// pipeline_id: &str,
// variable_provider: &dyn crate::variable::VariableProvider,
// ) -> crate::error::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 = crate::variable::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(),
// }
// }
// }
+99
View File
@@ -0,0 +1,99 @@
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,
}
@@ -0,0 +1,25 @@
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>,
}
@@ -0,0 +1,40 @@
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,
}
@@ -0,0 +1,59 @@
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,
}
@@ -0,0 +1,28 @@
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>,
}
@@ -0,0 +1,30 @@
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>,
}
@@ -0,0 +1,35 @@
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,
}
+63
View File
@@ -0,0 +1,63 @@
mod environment;
mod dependencies;
mod security;
mod hooks;
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 build: Option<std::collections::HashMap<String, String>>,
pub runtime: Option<std::collections::HashMap<String, String>>,
}
/// 缓存配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
pub directories: Vec<CacheDirectory>,
pub strategy: CacheStrategy,
}
/// 缓存目录配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheDirectory {
pub path: String,
pub strategy: String,
pub compression: String,
}
/// 缓存策略
#[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 Metadata {
pub description: String,
pub maintainer: String,
pub tags: Vec<String>,
pub project_type: String,
}
@@ -0,0 +1,67 @@
use serde::{Deserialize, Serialize};
/// 依赖配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependenciesConfig {
pub system: Option<SystemDependencies>,
pub languages: Option<LanguageDependencies>,
pub custom: Option<Vec<CustomDependency>>,
}
/// 系统依赖
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemDependencies {
pub packages: Vec<String>,
pub repositories: Vec<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 version: String,
pub toolchain: String,
pub components: Vec<String>,
pub targets: Vec<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,
}
/// 自定义依赖
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomDependency {
pub name: String,
pub command: String,
pub environment: Option<std::collections::HashMap<String, String>>,
pub working_dir: Option<String>,
pub timeout: Option<u32>,
}
@@ -0,0 +1,72 @@
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 {
pub memory_mb: u32,
pub cpus: u32,
pub arch: String,
}
/// Docker配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerConfig {
pub base_image: String,
pub dockerfile: String,
pub build_args: Vec<String>,
}
/// Firecracker配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FirecrackerConfig {
pub kernel_image: String,
pub rootfs_image: String,
pub memory_mb: u32,
pub vcpus: u32,
pub boot_args: String,
}
/// 自定义构建器配置
#[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 {
pub cpu: u32,
pub memory: String,
pub disk: String,
pub architecture: String,
pub gpu: bool,
pub gpu_type: Option<String>,
}
/// 网络配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
pub enabled: bool,
pub outbound: bool,
pub dns_servers: Vec<String>,
}
@@ -0,0 +1,17 @@
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>,
}
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,49 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,88 @@
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(),
}
}
}
+180
View File
@@ -0,0 +1,180 @@
use crate::{
config::prebake::PrebakeConfig,
config::finalize::FinalizeConfig,
variable::VariableResolver,
error::PipelineError,
};
use regex::Regex;
use lazy_static::lazy_static;
// 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 格式
// for cap in SHELL_VAR_REGEX.captures_iter(&result) {
// let var_name = &cap[1];
// if let Ok(value) = resolver.resolve_variable(var_name) {
// result = result.replace(&cap[0], &value);
// }
// }
Ok(result)
}
}
impl TemplateResolvable for PrebakeConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
let mut config = self.clone();
// 解析环境变量
if let Some(ref mut env_vars) = config.environment_variables {
if let Some(ref mut build) = env_vars.build {
for (_key, value) in build.iter_mut() {
*value = value.resolve_template(resolver)?;
}
}
if let Some(ref mut runtime) = env_vars.runtime {
for (_key, value) in runtime.iter_mut() {
*value = value.resolve_template(resolver)?;
}
}
}
// 解析自定义依赖
if let Some(ref mut deps) = config.dependencies {
if let Some(ref mut custom) = deps.custom {
for dep in custom.iter_mut() {
dep.command = dep.command.resolve_template(resolver)?;
if let Some(ref mut env) = dep.environment {
for (_key, value) in env.iter_mut() {
*value = value.resolve_template(resolver)?;
}
}
}
}
}
Ok(config)
}
}
impl TemplateResolvable for FinalizeConfig {
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
let mut config = self.clone();
// 解析产物路径
if let Some(ref mut artifacts) = config.artifacts {
if let Some(ref mut primary) = artifacts.primary {
for artifact in primary.iter_mut() {
artifact.path = artifact.path.resolve_template(resolver)?;
}
}
}
// 解析打包配置
if let Some(ref mut packaging) = config.packaging {
if let Some(ref mut primary) = packaging.primary_package {
primary.output = primary.output.resolve_template(resolver)?;
}
if let Some(ref mut docker) = packaging.docker {
docker.repository = docker.repository.resolve_template(resolver)?;
for tag in docker.tags.iter_mut() {
*tag = tag.resolve_template(resolver)?;
}
for build_arg in docker.build_args.iter_mut() {
*build_arg = build_arg.resolve_template(resolver)?;
}
for (_key, value) in docker.labels.iter_mut() {
*value = value.resolve_template(resolver)?;
}
}
}
// 解析发布配置
if let Some(ref mut distribution) = config.distribution {
if let Some(ref mut local) = distribution.local {
local.directory = local.directory.resolve_template(resolver)?;
}
if let Some(ref mut s3) = distribution.s3 {
s3.prefix = s3.prefix.resolve_template(resolver)?;
}
if let Some(ref mut registries) = distribution.package_registries {
if let Some(ref mut crates_io) = registries.crates_io {
crates_io.token = crates_io.token.resolve_template(resolver)?;
}
if let Some(ref mut npm) = registries.npm {
npm.token = npm.token.resolve_template(resolver)?;
}
if let Some(ref mut docker_registry) = registries.docker_registry {
docker_registry.username = docker_registry.username.resolve_template(resolver)?;
docker_registry.password = docker_registry.password.resolve_template(resolver)?;
}
}
}
// 解析通知配置
if let Some(ref mut notifications) = config.notifications {
for notification in notifications.on_success.iter_mut() {
notification.message = notification.message.resolve_template(resolver)?;
if let Some(ref mut webhook) = notification.webhook {
*webhook = webhook.resolve_template(resolver)?;
}
}
for notification in notifications.on_failure.iter_mut() {
notification.message = notification.message.resolve_template(resolver)?;
}
}
Ok(config)
}
}
impl FinalizeConfig {
pub fn for_output(&self) -> Self {
let mut config = self.clone();
// 遮蔽secret类型的变量
if let Some(ref mut distribution) = config.distribution {
if let Some(ref mut registries) = distribution.package_registries {
if let Some(ref mut crates_io) = registries.crates_io {
crates_io.token = "***".to_string();
}
if let Some(ref mut npm) = registries.npm {
npm.token = "***".to_string();
}
if let Some(ref mut docker_registry) = registries.docker_registry {
docker_registry.username = "***".to_string();
docker_registry.password = "***".to_string();
}
}
}
config
}
}
+128
View File
@@ -0,0 +1,128 @@
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()
}
}