第一章:Jenkins配置Go环境:从入门到失控——新手常踩的4类PATH陷阱与rootless Agent下的权限降级方案
Jenkins中配置Go环境看似简单,却极易因PATH解析机制与用户上下文错位引发构建失败、go: command not found、模块下载超时或GOPATH静默失效等隐蔽问题。尤其在采用Docker-in-Docker或Kubernetes rootless Agent时,环境变量继承链断裂尤为典型。
PATH继承链断裂
Jenkins主节点通过envVars注入的PATH默认不自动传递给Agent进程。若Agent以非登录shell启动(如docker run --entrypoint /bin/sh),.bashrc或/etc/environment中的Go路径不会生效。验证方式:
# 在Pipeline中执行
sh 'echo "PATH in Jenkins step: $PATH"'
sh 'which go || echo "go not found"'
正确做法是在Agent容器启动脚本中显式追加:
# Dockerfile片段(Agent镜像)
ENV GOROOT=/usr/local/go
ENV GOPATH=/home/jenkins/go
ENV PATH=$PATH:$GOROOT/bin:$GOPATH/bin
Shell类型导致的PATH覆盖
sh(POSIX)与bash对/etc/profile加载策略不同。Jenkins默认使用sh执行脚本,而/etc/profile.d/go.sh可能仅被bash读取。解决方案:在Jenkinsfile中强制指定解释器:
steps {
sh '''#!/bin/bash
source /etc/profile.d/go.sh
go version
'''
}
多版本Go共存时的软链接漂移
当通过update-alternatives管理多个Go版本时,/usr/bin/go可能指向临时符号链接。Agent重启后链接可能重置为系统默认版本(如1.18),而构建要求1.21。建议固定安装路径并绕过系统软链:
# 在Agent初始化脚本中
export PATH="/opt/go-1.21.6/bin:$PATH" # 绝对路径优先于软链
rootless Agent的权限降级方案
非root用户无法写入/usr/local/go,但go install需写入$GOPATH/bin。必须确保:
jenkins用户对$GOPATH有完整读写权限;- 使用
go install -mod=readonly避免意外修改模块缓存; - 禁用
GOCACHE或将其指向用户可写目录:export GOCACHE=$HOME/.cache/go-build mkdir -p $GOCACHE
| 陷阱类型 | 典型症状 | 根治要点 |
|---|---|---|
| PATH继承断裂 | which go返回空 |
在Agent镜像中硬编码PATH |
| Shell类型差异 | .bashrc中PATH未生效 |
Pipeline中显式调用/bin/bash |
| 软链接漂移 | go version与预期不符 |
绝对路径+版本号目录隔离 |
| rootless权限不足 | go install Permission denied |
$GOPATH属主为jenkins用户 |
第二章:Go环境在Jenkins中的基础集成原理与实操验证
2.1 Go二进制分发包下载、解压与全局PATH注入机制解析
Go 官方提供预编译二进制包(.tar.gz),规避源码构建依赖,适用于快速部署。
下载与校验
# 下载 Linux x86_64 版本(含 SHA256 校验)
curl -O https://go.dev/dl/go1.22.5.linux-amd64.tar.gz
curl -O https://go.dev/dl/go1.22.5.linux-amd64.tar.gz.sha256
sha256sum -c go1.22.5.linux-amd64.tar.gz.sha256 # 验证完整性
-c 参数启用校验模式,确保压缩包未被篡改;SHA256 值由 Go 团队签名发布,是信任链起点。
解压与路径选择
| 目标路径 | 权限要求 | 是否推荐 | 说明 |
|---|---|---|---|
/usr/local/go |
root | ✅ | 系统级安装,PATH 易注入 |
$HOME/sdk/go |
用户 | ⚠️ | 无需 sudo,需手动配置 PATH |
PATH 注入机制
# 写入 shell 配置(以 bash 为例)
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
该操作将 go、gofmt 等可执行文件暴露至全局命令空间;$PATH 顺序决定命令优先级,前置路径具有更高权重。
graph TD A[下载 .tar.gz] –> B[SHA256 校验] B –> C[解压至权威路径] C –> D[追加 /bin 到 PATH] D –> E[shell 重载生效]
2.2 Jenkins全局工具配置中Go安装器的自动拉取与校验实践
Jenkins 的 Go 安装器(Go Tool Installer)支持从官方源自动下载、校验并部署指定版本的 Go SDK,大幅降低环境一致性风险。
校验机制设计
Jenkins 默认启用 SHA256 校验,通过比对 https://go.dev/dl/ 提供的 go${VERSION}.windows-amd64.zip.sha256(或对应平台文件)确保二进制完整性。
配置示例(Jenkinsfile 片段)
pipeline {
agent any
tools { go 'go-1.22.3' } // 引用全局配置中名为"go-1.22.3"的安装器
stages {
stage('Build') {
steps {
sh 'go version' // 自动注入 PATH,无需手动解压或配置
}
}
}
}
该配置触发 Jenkins 在首次使用时自动拉取 go1.22.3.linux-amd64.tar.gz 及其 SHA256 签名,校验通过后解压至 JENKINS_HOME/tools/hudson.plugins.golang.GolangInstallation/go-1.22.3/。
支持的校验方式对比
| 校验类型 | 启用条件 | 安全性 | Jenkins 版本要求 |
|---|---|---|---|
| SHA256 | 默认启用 | ★★★★★ | 2.300+ |
| GPG 签名 | 需手动导入公钥 | ★★★★☆ | 2.410+(实验性) |
graph TD
A[触发工具安装] --> B{检查本地缓存}
B -->|存在且校验通过| C[直接软链接]
B -->|缺失或校验失败| D[下载 go*.tar.gz + .sha256]
D --> E[计算本地 SHA256]
E --> F[比对远程签名]
F -->|匹配| G[解压并注册]
F -->|不匹配| H[中止并报错]
2.3 Pipeline脚本中go version与go env的动态探针式诊断方法
在CI/CD流水线中,Go环境一致性直接影响构建可靠性。传统硬编码版本易引发go mod download失败或GOOS/GOARCH误配。
探针式版本探测脚本
# 动态获取Go主版本并校验兼容性
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//; s/\..*//')
if [[ "$GO_VERSION" -lt "1" ]] || [[ "$GO_VERSION" -gt "20" ]]; then
echo "❌ Unsupported Go major version: $GO_VERSION" >&2; exit 1
fi
该脚本提取go version输出中的主版本号(如go1.22.3 → 1),避免正则过度匹配;-lt/-gt数值比较规避语义化版本解析开销。
环境变量健康快照
| 变量名 | 预期值示例 | 检查逻辑 |
|---|---|---|
GOROOT |
/usr/local/go |
非空且路径存在 |
GOPATH |
/home/user/go |
非空且可写 |
GO111MODULE |
on |
必须启用模块模式 |
诊断流程图
graph TD
A[执行 go version] --> B{解析主版本}
B -->|≥1.16| C[运行 go env]
B -->|<1.16| D[报错退出]
C --> E[校验关键变量]
E --> F[生成诊断报告]
2.4 多版本Go共存时JENKINS_HOME/tools/目录结构与软链接管理实战
Jenkins 通过 JENKINS_HOME/tools/ 统一管理工具链,Go 多版本共存需依赖精准的软链接调度。
目录结构约定
$JENKINS_HOME/tools/hudson.tools.GoInstaller/
├── go-1.21.0/ # 实际解压路径(含bin/go)
├── go-1.22.3/ # 另一版本
└── current -> go-1.22.3 # Jenkins 运行时读取的符号链接
逻辑说明:Jenkins 的
GoTool插件默认查找current软链接指向的bin/go。若手动切换版本,仅需重置该链接,无需修改任务配置。
版本切换脚本示例
# 切换至 Go 1.22.3(在 Jenkins 主机执行)
cd $JENKINS_HOME/tools/hudson.tools.GoInstaller
rm -f current
ln -s go-1.22.3 current
| 字段 | 说明 |
|---|---|
go-1.XX.Y/ |
版本隔离目录,含完整 SDK |
current |
Jenkins 实际调用的目标链接 |
工具注册流程
graph TD
A[用户在Jenkins UI添加Go 1.22.3] --> B[插件下载并解压至go-1.22.3/]
B --> C[自动创建或更新current→go-1.22.3]
C --> D[Pipeline中sh 'go version'即生效]
2.5 Dockerized Agent中Go环境继承失效的根因分析与env-inject补救方案
根因:Docker ENTRYPOINT 覆盖 shell 环境继承
Docker 默认 ENTRYPOINT ["agent"] 以 exec 模式启动,绕过 /bin/sh -c,导致 .bashrc/.profile 中的 GOROOT/GOPATH 未被加载,Go 工具链路径丢失。
env-inject 补救机制
使用 env-inject 在容器启动前动态注入 Go 环境变量:
# Dockerfile 片段
COPY --from=golang:1.22 /usr/local/go /usr/local/go
ENV GOROOT=/usr/local/go
ENV GOPATH=/workspace
RUN go install github.com/jenkins-x/env-inject@v0.12.0
ENTRYPOINT ["/bin/sh", "-c", "env-inject -- /usr/local/bin/agent"]
此处
env-inject --将当前 Shell 环境(含显式ENV声明)完整传递给agent进程,规避 exec 模式下环境剥离问题;--确保参数透传无歧义。
关键环境变量继承对照表
| 变量 | 原生 ENTRYPOINT 是否继承 | env-inject 后是否生效 |
|---|---|---|
GOROOT |
❌(未 source 配置文件) | ✅(显式 ENV + 注入) |
PATH |
⚠️(仅基础 PATH) | ✅(自动合并) |
CGO_ENABLED |
❌ | ✅(可预设) |
graph TD
A[Agent 容器启动] --> B{ENTRYPOINT 类型}
B -->|exec form| C[跳过 shell 初始化]
B -->|shell form| D[加载 .profile]
C --> E[Go 环境缺失]
E --> F[env-inject 注入显式 ENV]
F --> G[agent 正常调用 go build]
第三章:PATH陷阱的深度归因与防御性配置策略
3.1 Shell启动文件(.bashrc/.profile)与Jenkins Agent会话生命周期的冲突建模
Jenkins Agent 启动时默认以非交互式、非登录 shell 方式执行命令,跳过 .bashrc 加载,仅在特定条件下读取 .profile。这导致环境变量(如 JAVA_HOME、自定义 PATH)在 Pipeline 中不可见,而本地终端却正常。
环境加载差异对比
| 启动方式 | 加载 .bashrc |
加载 .profile |
交互式 | 登录式 |
|---|---|---|---|---|
| Jenkins Agent 执行 | ❌ | ✅(仅当 --login 显式启用) |
❌ | ❌ |
本地终端 ssh user |
✅(若 ~/.bashrc 被 .profile 显式调用) |
✅ | ✅ | ✅ |
典型冲突复现脚本
# 在 Jenkins Pipeline 中执行
sh 'echo "PATH: $PATH"; java -version 2>/dev/null || echo "Java not found"'
逻辑分析:该命令在非登录 shell 下运行,
.bashrc中export PATH="/opt/jdk/bin:$PATH"不生效;即使.profile存在,也因 Jenkins 默认未加--login参数而不触发加载。参数--login是显式启用 POSIX 登录 shell 的关键开关,缺失即导致环境隔离。
冲突建模(mermaid)
graph TD
A[Jenkins Agent 启动] --> B[spawn /bin/bash -c ...]
B --> C{是否含 --login?}
C -->|否| D[仅 source /etc/profile → 忽略 ~/.profile & ~/.bashrc]
C -->|是| E[source ~/.profile → 可能 source ~/.bashrc]
3.2 Jenkins节点环境变量覆盖链:系统级→节点级→Job级→Step级的优先级实验验证
Jenkins 中环境变量遵循明确的覆盖优先级:低优先级设置可被高优先级同名变量覆盖。
实验验证方法
- 在 Linux 节点
/etc/environment设置ENV_SCOPE=system - 在 Jenkins 节点配置页添加节点级变量
ENV_SCOPE=node - 在 Pipeline Job 中通过
environment { ENV_SCOPE = 'job' }声明 - 在
shStep 内执行export ENV_SCOPE=step
覆盖行为验证脚本
pipeline {
agent any
environment {
ENV_SCOPE = 'job'
}
stages {
stage('Check Env') {
steps {
script {
sh 'echo "Step-level: $ENV_SCOPE"' // 输出 step(因 export 临时覆盖)
}
sh 'echo "Job-level: $ENV_SCOPE"' // 输出 job(Step 内 export 不持久化到后续 sh)
}
}
}
}
sh步骤中export仅作用于当前 shell 进程,不污染后续步骤;而environment块定义对整个 Job 生效,但会被withEnv{}或sh 'export'在当前 shell 中瞬时覆盖。
优先级关系总结
| 作用域 | 设置位置 | 是否可被覆盖 | 持久范围 |
|---|---|---|---|
| 系统级 | /etc/environment 或启动参数 |
✅(最低) | 全节点所有 Job |
| 节点级 | Jenkins 节点配置 → “Node Properties” | ✅ | 该节点所有 Job |
| Job级 | environment {} 块 |
✅ | 当前 Job 所有 Steps |
| Step级 | sh 'export VAR=val' 或 withEnv{} |
❌(最高) | 仅当前 Step 的 shell 进程 |
graph TD
A[系统级] --> B[节点级]
B --> C[Job级]
C --> D[Step级]
style A fill:#e6f7ff,stroke:#1890ff
style D fill:#fff2e6,stroke:#fa541c
3.3 Go模块代理(GOPROXY)与PATH耦合导致的go get静默失败复现与修复
复现场景
当 GOPROXY 设为私有代理(如 https://goproxy.example.com),且 PATH 中存在同名但不可执行的 go 符号链接(如指向缺失二进制的 /usr/local/go/bin/go),go get 会跳过代理校验,直接尝试本地解析模块路径,最终静默返回空结果而非错误。
关键诊断步骤
- 检查代理可达性:
curl -I https://goproxy.example.com/github.com/gorilla/mux/@v/v1.8.0.info # 若返回 404 或超时,代理未就绪;若返回 200 但 go get 仍失败,则进入 PATH 干扰链路
PATH 干扰机制
# 错误示例:/usr/local/go/bin/go 是 dangling symlink
ls -l $(which go)
# 输出:lrwxr-xr-x 1 root root 28 Apr 10 09:22 /usr/local/go/bin/go -> /opt/go-1.21.0/bin/go-missing
Go 工具链在初始化时调用 exec.LookPath("go"),若失败则降级使用内置逻辑,跳过 GOPROXY 验证流程,导致模块下载路径解析为空。
修复方案对比
| 方案 | 操作 | 风险 |
|---|---|---|
✅ 清理 PATH 中无效 go 路径 |
sudo rm /usr/local/go/bin/go |
需重启 shell |
| ⚠️ 强制重置 GOPROXY | export GOPROXY=https://proxy.golang.org,direct |
可能绕过企业策略 |
graph TD
A[go get github.com/x/y] --> B{GOPROXY set?}
B -->|Yes| C[Fetch via proxy]
B -->|No| D[Local module lookup]
C --> E{Proxy returns 200?}
E -->|No| F[Silent fallback to local]
F --> G[Empty result, no error]
第四章:Rootless Agent下的安全降权实践与Go构建沙箱构建
4.1 非root用户启动JNLP Agent时$HOME/.go与GOCACHE权限拒绝的strace追踪与chown修复
当非 root 用户以 Jenkins JNLP Agent 方式启动 Go 构建任务时,go build 常因 $HOME/.go 或 GOCACHE 目录权限不足而失败。
追踪权限拒绝根源
使用 strace -e trace=mkdir,openat,chown -f -p $(pgrep -f 'java.*JNLP') 2>&1 | grep -E '(EACCES|EPERM)' 可捕获关键拒绝调用。
# 示例 strace 输出片段(截取)
openat(AT_FDCWD, "/home/jenkins/.cache/go-build", O_RDONLY|O_CLOEXEC) = -1 EACCES (Permission denied)
该调用表明 Go runtime 尝试读取缓存目录但被内核拒绝——通常因父目录(如 /home/jenkins)被 chown 误设为 root:root 且无其他用户执行权限。
修复方案:精准 chown
仅修正属主,保留原有权限位:
# 修复 ~/.go 和 GOCACHE(若自定义)
chown -R jenkins:jenkins $HOME/.go $HOME/.cache/go-build
chmod 700 $HOME/.go $HOME/.cache/go-build
✅
chown -R递归修复所有子项;
✅chmod 700确保仅属主可读写执行,符合 Go 安全策略。
| 目录 | 推荐权限 | 原因 |
|---|---|---|
$HOME/.go |
700 |
存储 GOPATH/pkg,含编译产物 |
$HOME/.cache/go-build |
700 |
Go build cache,敏感哈希索引 |
graph TD
A[Agent 启动] --> B{Go 调用 openat}
B -->|EACCES| C[strace 捕获拒绝点]
C --> D[检查目录属主/权限]
D --> E[chown + chmod 修复]
E --> F[Go 编译恢复成功]
4.2 使用buildkit+unshare实现无特权容器内Go交叉编译的Docker-in-Docker替代方案
传统 Docker-in-Docker(DinD)需 privileged 权限,存在安全风险。buildkit + unshare 提供轻量、无特权的替代路径。
核心原理
unshare --user --pid --mount创建用户命名空间隔离环境buildkitd在非 root 用户命名空间中运行,通过--oci-worker-no-process-sandbox=false启用进程级沙箱- Go 交叉编译依赖
GOOS/GOARCH环境变量,无需完整系统镜像
构建示例
# build-with-unshare.Dockerfile
FROM golang:1.22-alpine
RUN apk add --no-cache build-base linux-headers
# 启用用户命名空间支持
RUN echo 'kernel.unprivileged_userns_clone=1' > /etc/sysctl.d/99-unshare.conf
逻辑分析:该 Dockerfile 为
unshare运行准备基础环境;linux-headers支持 syscall 兼容性,sysctl配置允许非 root 用户创建 user NS(关键前提)。
性能与权限对比
| 方案 | 特权要求 | 安全边界 | Go 交叉编译支持 |
|---|---|---|---|
| DinD | --privileged |
弱(共享 host PID/Net) | ✅ |
| buildkit+unshare | --userns=keep-id |
强(完整 user/pid/mnt 隔离) | ✅ |
unshare --user --pid --mount --fork \
--map-root-user \
buildctl --addr unix:///tmp/buildkitd.sock build \
--frontend dockerfile.v0 \
--local context=. \
--local dockerfile=. \
--output type=cacheonly \
--opt build-arg:GOOS=linux --opt build-arg:GOARCH=arm64
参数说明:
--map-root-user将 UID 0 映射到当前用户,规避权限拒绝;--output type=cacheonly跳过导出,聚焦编译过程;--opt build-arg直接注入 Go 构建目标平台。
4.3 Jenkins Credentials Binding插件与GOSUMDB/GOPRIVATE私有仓库认证的非root安全传递
Jenkins 中需避免以 root 权限注入敏感凭证,同时确保 Go 构建过程能安全访问私有模块仓库。
Credentials Binding 安全注入机制
使用 withCredentials 绑定凭据为环境变量,而非写入文件或提升权限:
withCredentials([string(credentialsId: 'gosumdb-token', variable: 'GOSUMDB_TOKEN')]) {
sh '''
export GOSUMDB="sum.golang.org+insecure" # 或自建 sumdb
export GOPRIVATE="git.example.com/internal"
export GOPROXY="https://proxy.golang.org,direct"
go build -v
'''
}
GOSUMDB_TOKEN仅在 shell 会话中临时生效;+insecure表示跳过 TLS 校验(仅限内网可信 sumdb),实际生产应配 HTTPS + 有效证书。
Go 模块认证关键环境变量对照表
| 变量名 | 作用 | 推荐值示例 |
|---|---|---|
GOPRIVATE |
跳过代理/校验的私有域名前缀 | git.example.com,github.company.com |
GOSUMDB |
指定校验数据库(支持 token 认证) | sum.git.example.com@token:${GOSUMDB_TOKEN} |
凭据流转安全边界(mermaid)
graph TD
A[Jenkins Credential Store] -->|加密存储| B[withCredentials]
B -->|内存注入| C[Shell 环境变量]
C -->|Go 进程继承| D[go build / go mod download]
D -->|不落盘、无 root| E[安全完成校验与拉取]
4.4 基于seccomp+AppArmor的Go测试阶段syscall拦截策略与最小能力集定义
在Go单元测试执行阶段,需精准限制非必要系统调用,避免测试污染或越权行为。
拦截策略设计原则
- 仅允许
read,write,mmap,brk,clock_gettime,getpid,gettid,futex - 显式拒绝
openat,socket,clone,execve,chdir,setuid等高风险 syscall
seccomp BPF 规则片段(Go 测试初始化时加载)
// 使用 libseccomp-go 绑定
filter := seccomp.NewFilter(seccomp.ActErrno.SetReturnCode(1))
_ = filter.AddRule(seccomp.SYS(read), seccomp.ActAllow)
_ = filter.AddRule(seccomp.SYS(write), seccomp.ActAllow)
_ = filter.AddRule(seccomp.SYS(openat), seccomp.ActErrno.SetReturnCode(1)) // 显式拒绝
filter.Load()
此代码在
TestMain中提前加载:AddRule按 syscall 号注册动作;ActErrno返回EPERM而非崩溃,便于测试断言异常路径;Load()启用过滤器后对当前进程及 fork 子进程生效。
最小能力集对照表
| 能力类别 | 允许项 | 禁止项 |
|---|---|---|
| 文件访问 | read/write(已打开 fd) |
openat, mkdirat |
| 进程控制 | getpid, futex |
clone, execve |
| 网络与IPC | — | socket, bind |
安全边界协同模型
graph TD
A[Go test binary] --> B[seccomp filter]
A --> C[AppArmor profile]
B --> D[阻断非法 syscall]
C --> E[路径级文件/网络约束]
D & E --> F[纵深防御测试沙箱]
第五章:结语:走向可审计、可回滚、可声明式的Go CI基础设施
在某中型SaaS平台的CI演进实践中,团队将原有基于Shell脚本拼凑的Jenkins Pipeline全面重构为基于GitHub Actions + Terraform + Go构建的声明式流水线。整个基础设施即代码(IaC)层使用Go编写自定义Action Runner管理器,通过go run ./cmd/runnerctl apply --env=staging统一部署带版本标签的Runner集群,每个Runner镜像均嵌入SHA256校验指纹并写入不可变Artifact Registry。
审计追踪能力落地细节
所有CI作业执行前自动注入唯一trace_id,并与OpenTelemetry Collector对接;日志流经Fluent Bit转发至Loki,配合Grafana实现“点击失败Job → 跳转到对应trace → 下钻至Go test panic堆栈”闭环。审计日志表结构如下:
| 字段名 | 类型 | 示例值 | 说明 |
|---|---|---|---|
| job_id | string | gh_4b8d2f1a |
GitHub Job ID |
| commit_hash | string | a3f9c2e7d... |
触发提交哈希 |
| declared_version | string | v2.4.0-rc1 |
go.mod中声明的依赖版本 |
| runner_image_digest | string | sha256:8e9f... |
实际拉取的Runner镜像摘要 |
| audit_signature | string | sig_ecc_p384_... |
使用KMS托管密钥签名 |
回滚机制实战验证
当v2.5.0发布后发现gRPC客户端兼容性问题,运维团队执行以下三步回滚:
# 1. 检查历史部署快照
go run ./cmd/deploy list --since="2024-06-01" --format=json | jq '.[0].digest'
# 2. 精确回退至已验证镜像
go run ./cmd/deploy rollback --digest=sha256:5c1a... --namespace=ci-prod
# 3. 自动触发回归测试套件(含137个Go benchmark用例)
make test-bench COMPARE_BASE=sha256:5c1a... COMPARE_HEAD=sha256:8e9f...
整个过程耗时4分17秒,且所有操作记录同步写入区块链存证服务(Hyperledger Fabric链码部署于K8s内网)。
声明式配置收敛实践
团队将原本分散在.github/workflows/*.yml、Jenkinsfile、Makefile中的逻辑全部收口至ci-spec/v1/目录下的Go结构体定义:
type CIManifest struct {
Version string `yaml:"version"` // "v1"
Stages []Stage `yaml:"stages"`
Resources ResourceLimits `yaml:"resources"`
Security SecurityPolicy `yaml:"security"`
// 所有字段均参与JSON Schema校验与OpenAPI生成
}
该结构体经go generate自动生成Protobuf定义,供内部CI SDK调用;同时通过go run ./cmd/specgen openapi输出Swagger文档,供前端监控面板动态渲染Pipeline拓扑图。Mermaid流程图展示其状态机演进:
stateDiagram-v2
[*] --> Draft
Draft --> Validated: validate()
Validated --> Deploying: deploy()
Deploying --> Running: start()
Running --> Succeeded: all tests pass
Running --> Failed: any step fails
Failed --> Rollback: rollback()
Rollback --> Validated: revalidate() 