Prow Manual-Trigger 深度解析:架构、工作流程与实战

Prow Manual-Trigger 深度解析:架构、工作流程与实战

📚 前言

本文深入剖析 Kubernetes test-infra 仓库中的 prow/cmd/manual-trigger 项目,详细讲解其技术架构、工作流程和使用方法。Manual-Trigger 是一个 HTTP 服务,允许用户手动触发 Prow CI/CD 任务,而无需依赖 GitHub webhook 事件。

一、Test-Infra 仓库整体概述

1.1 Test-Infra 是什么?

test-infraKubernetes 官方的测试基础设施仓库,包含了 Kubernetes 项目的所有 CI/CD 工具和配置文件。它是一个大型的测试自动化框架,主要用于:

  • CI 任务管理: 自动化测试、构建、部署流程
  • 代码审查: PR 自动化测试和合并管理
  • 测试结果展示: 历史测试数据和失败分析
  • 资源管理: GCP 项目池、GitHub API 代理等

1.2 核心组件 - Prow

Prow 是这个仓库的核心,它是一个基于 Kubernetes 的 CI/CD 系统,专为 GitHub 设计。Prow 的主要组件包括:

组件 功能
Hook 监听 GitHub webhook 事件
Plank 调度和管理 ProwJob
Deck Web UI,展示任务状态
Tide 自动合并 PR
Crier 上报任务结果到 GitHub
Manual-Trigger 手动触发任务(本文重点)

二、Manual-Trigger 项目深度剖析

2.1 项目定位

Manual-Trigger 是一个 HTTP 服务,允许用户手动触发 Prow 任务,而无需通过 GitHub 事件(如 push、PR)。这在以下场景非常有用:

✅ 手动测试 CI 任务
✅ 对特定代码版本运行测试
✅ 运维和维护操作
✅ 使用自定义参数触发任务

2.2 技术架构

2.2.1 技术栈

1
2
3
4
5
6
7
编程语言: Go 1.21
框架: 标准库 net/http
Kubernetes: 通过 client-go K8s API 交互
依赖:
- Prow API (prowjobs/v1)
- Kubernetes Core API (corev1)
- Prow 配置管理 (config.Agent)

2.2.2 架构图

graph TB
    A[用户/客户端] -->|HTTP POST| B[Manual-Trigger 服务]
    B -->|读取配置| C[Config Agent]
    B -->|创建 ProwJob| D[ProwJob Client]
    D -->|提交 CRD| E[Kubernetes API Server]
    E -->|存储| F[ProwJob Custom Resource]
    F -->|监听| G[Prow Plank]
    G -->|创建 Pod| H[Kubernetes Pod]
    H -->|执行任务| I[CI/CD 任务]
    B -->|返回结果| A

核心组件说明:

  1. HTTP Handler: 接收和解析用户请求
  2. Config Agent: 读取 Prow 配置文件,查找任务定义
  3. ProwJob Client: Kubernetes 客户端,用于创建 ProwJob CRD
  4. Prow Plank: 监听 ProwJob 创建事件,调度 Pod 执行

三、工作流程详解

3.1 核心工作流 (Step by Step)

步骤 1: 接收 HTTP 请求

用户发送 POST 请求到 /manual-trigger 端点:

1
2
3
4
5
6
7
8
9
10
curl -X POST "http://manual-trigger.tessprow/manual-trigger" \
-H "Content-Type: application/json" \
-d '{
"org": "tess",
"repo": "tessops",
"base_ref": "master",
"prowtype": "postsubmit",
"prowjob": "sddz-e2e-k8s-1.32",
"user": "fesu"
}'

参数说明:

参数 说明 必需 示例
org GitHub 组织名 tess
repo 仓库名 tessops
base_ref 基础分支 master
prowtype 任务类型 postsubmit
prowjob 任务名称 sddz-e2e-k8s-1.32
pullrequest PR 号(presubmit 必需) 条件 123
user 自定义用户名 fesu

步骤 2: 请求解析与验证

handleManualTrigger 函数处理请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 支持两种输入格式:
// - JSON body
// - URL query 参数

// 2. 验证必需参数
if req.Org == "" || req.Repo == "" || req.BaseRef == "" ||
req.ProwType == "" || req.ProwJob == "" {
// 返回错误
}

// 3. 特殊验证: presubmit 任务必须提供 PR 号
if req.ProwType == "presubmit" && req.PullRequest <= 0 {
// 返回错误
}

步骤 3: 查找任务配置

从 Prow 配置中查找对应的任务定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取 Prow 配置
cfg := s.configAgent.Config()

// 优先在 presubmits 中查找
if presubmits, ok := cfg.PresubmitsStatic[req.Org+"/"+req.Repo]; ok {
for _, p := range presubmits {
if p.Name == req.ProwJob {
prowJob, err = s.createProwJobFromPresubmit(p, req)
break
}
}
}

// 如果没找到,在 postsubmits 中查找
if prowJob == nil {
if postsubmits, ok := cfg.PostsubmitsStatic[req.Org+"/"+req.Repo]; ok {
// ...类似逻辑
}
}

步骤 4: 创建 ProwJob 规范

根据任务类型创建 ProwJob 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func (s *server) createProwJobFromPresubmit(presubmit config.Presubmit, req triggerRequest) (*prowapi.ProwJob, error) {
// 1. 构建 Refs (代码引用)
refs := prowapi.Refs{
Org: req.Org,
Repo: req.Repo,
BaseRef: req.BaseRef,
RepoLink: fmt.Sprintf("https://github.corp.ebay.com/%s/%s", req.Org, req.Repo),
}

// 2. 如果是 presubmit,添加 PR 信息
if req.ProwType == "presubmit" && req.PullRequest > 0 {
refs.Pulls = []prowapi.Pull{
{
Number: req.PullRequest,
Link: fmt.Sprintf("https://github.corp.ebay.com/%s/%s/pull/%d",
req.Org, req.Repo, req.PullRequest),
},
}
}

// 3. 创建 ProwJob Spec
spec := pjutil.PresubmitSpec(presubmit, refs)

// 4. 如果指定为 postsubmit 类型,覆盖类型
if req.ProwType == "postsubmit" {
spec.Type = prowapi.PostsubmitJob
}

// 5. 添加标签和注解
pj := pjutil.NewProwJob(spec, labels, annotations)

// 6. 如果提供了 user 参数,注入 AUTHOR 环境变量
if req.User != "" {
s.addAuthorEnvToProwJob(&pj, req.User)
}

return &pj, nil
}

步骤 5: 提交到 Kubernetes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 创建 ProwJob CRD 资源
if !s.dryRun {
ctx := context.Background()
_, err = s.prowJobClient.Create(ctx, prowJob, metav1.CreateOptions{})
if err != nil {
// 处理错误
}

// 等待 BuildID 生成 (最多 15 秒)
// BuildID 用于生成日志链接
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
// 超时
goto buildResponse
case <-ticker.C:
latestProwJob, _ := s.prowJobClient.Get(context.Background(), prowJob.Name, metav1.GetOptions{})
if latestProwJob.Status.BuildID != "" {
prowJob = latestProwJob
goto buildResponse
}
}
}
}

步骤 6: 返回响应

1
2
3
4
5
6
7
8
9
10
11
12
// 生成状态链接和日志链接
statusLink := fmt.Sprintf("https://%s/prowjob?prowjob=%s", baseURL, prowJob.Name)
logLink := fmt.Sprintf("https://%s/log?job=%s&id=%s", baseURL, req.ProwJob, prowJob.Status.BuildID)

// 返回 JSON 响应
{
"success": true,
"message": "ProwJob created successfully",
"job_name": "715c1a56-f815-11f0-b860-8acb6d93d6ad",
"status_link": "https://prow.example.com/prowjob?prowjob=...",
"log_link": "https://prow.example.com/log?job=...&id=..."
}

步骤 7: Prow 执行任务

一旦 ProwJob CRD 被创建:

  1. Plank 组件监听到新的 ProwJob
  2. Plank 根据 spec.PodSpec 创建 Kubernetes Pod
  3. Pod 执行测试/构建任务
  4. Plank 更新 ProwJob 的 status 字段
  5. Deck 在 Web UI 上展示任务状态
  6. Crier 可以将结果上报到外部系统

3.2 核心数据结构

ProwJob CRD 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type ProwJob struct {
metav1.TypeMeta
metav1.ObjectMeta

Spec ProwJobSpec // 任务定义
Status ProwJobStatus // 运行时状态
}

type ProwJobSpec struct {
Type ProwJobType // presubmit, postsubmit, periodic, batch
Agent ProwJobAgent // kubernetes, jenkins, tekton-pipeline
Cluster string // 运行集群
Job string // 任务名称
Refs *Refs // Git 引用信息
PodSpec *corev1.PodSpec // Pod 定义
// ...更多字段
}

type Refs struct {
Org string // GitHub 组织
Repo string // 仓库名
BaseRef string // 基础分支
BaseSHA string // 基础 commit SHA
Pulls []Pull // PR 列表
}

type ProwJobStatus struct {
State ProwJobState // triggered, pending, success, failure, aborted, error
BuildID string // 构建 ID
StartTime metav1.Time
CompletionTime *metav1.Time
URL string // 任务详情 URL
}

ProwJob 生命周期状态:

stateDiagram-v2
    [*] --> Triggered: ProwJob 创建
    Triggered --> Pending: Pod 开始运行
    Pending --> Success: 任务成功 (exit 0)
    Pending --> Failure: 任务失败 (exit non-zero)
    Pending --> Aborted: 被提前终止
    Pending --> Error: 调度失败
    Success --> [*]
    Failure --> [*]
    Aborted --> [*]
    Error --> [*]

四、关键特性详解

4.1 灵活的类型转换

Manual-Trigger 支持将 presubmit 任务作为 postsubmit 运行

1
2
3
4
5
6
// 即使任务在配置中定义为 presubmit
// 也可以通过设置 prowtype=postsubmit 将其作为 postsubmit 运行
if req.ProwType == "postsubmit" {
spec := pjutil.PresubmitSpec(presubmit, refs)
spec.Type = prowapi.PostsubmitJob // 覆盖类型
}

使用场景: 想在某个分支上运行测试,但不想创建 PR

4.2 自定义环境变量注入

通过 user 参数可以注入 AUTHOR 环境变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (s *server) addAuthorEnvToProwJob(pj *prowapi.ProwJob, user string) {
if pj.Spec.PodSpec != nil && pj.Spec.PodSpec.Containers != nil {
for i := range pj.Spec.PodSpec.Containers {
authorEnv := corev1.EnvVar{
Name: "AUTHOR",
Value: user,
}
// 检查是否已存在,存在则更新,否则追加
found := false
for j := range pj.Spec.PodSpec.Containers[i].Env {
if pj.Spec.PodSpec.Containers[i].Env[j].Name == "AUTHOR" {
pj.Spec.PodSpec.Containers[i].Env[j].Value = user
found = true
break
}
}
if !found {
pj.Spec.PodSpec.Containers[i].Env = append(pj.Spec.PodSpec.Containers[i].Env, authorEnv)
}
}
}
}

使用场景: 任务脚本可以根据 AUTHOR 环境变量执行不同逻辑

4.3 BuildID 等待机制

为了提供完整的日志链接,服务会等待 BuildID 生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 轮询最多 15 秒
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
// 超时,BuildID 仍未生成
goto buildResponse
case <-ticker.C:
// 每 500ms 查询一次
latestProwJob, err := s.prowJobClient.Get(context.Background(), prowJob.Name, metav1.GetOptions{})
if latestProwJob.Status.BuildID != "" {
// BuildID 已生成
goto buildResponse
}
}
}

五、部署架构

5.1 Kubernetes 部署清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# ServiceAccount: 服务账户
apiVersion: v1
kind: ServiceAccount
metadata:
name: manual-trigger
namespace: tessprow

---
# Role: 定义权限 (最小权限原则)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: manual-trigger
namespace: tessprow
rules:
- apiGroups:
- prow.k8s.io
resources:
- prowjobs
verbs:
- create # 创建 ProwJob
- get # 获取 ProwJob 状态
- list # 列出 ProwJob

---
# RoleBinding: 绑定权限
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: manual-trigger
namespace: tessprow
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: manual-trigger
subjects:
- kind: ServiceAccount
name: manual-trigger
namespace: tessprow

---
# Deployment: 运行服务
apiVersion: apps/v1
kind: Deployment
metadata:
name: manual-trigger
namespace: tessprow
labels:
app: manual-trigger
spec:
replicas: 1
selector:
matchLabels:
app: manual-trigger
template:
metadata:
labels:
app: manual-trigger
spec:
serviceAccountName: manual-trigger
containers:
- name: manual-trigger
image: hub.tess.io/prowimages/manual-trigger:latest
imagePullPolicy: Always
args:
- --config-path=/etc/config/config.yaml # Prow 配置文件
- --dry-run=false
- --port=8080
- --namespace=tessprow
ports:
- name: http
containerPort: 8080
- name: metrics
containerPort: 9090 # Prometheus metrics
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: config
mountPath: /etc/config
readOnly: true
volumes:
- name: config
configMap:
name: config # Prow 配置的 ConfigMap

---
# Service: 暴露服务
apiVersion: v1
kind: Service
metadata:
name: manual-trigger
namespace: tessprow
labels:
app: manual-trigger
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: 8080
protocol: TCP
- name: metrics
port: 9090
targetPort: 9090
protocol: TCP
selector:
app: manual-trigger

5.2 容器镜像构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 第一阶段: 编译
FROM golang:1.21 AS builder
WORKDIR /workspace
COPY . .
WORKDIR /workspace/prow/cmd/manual-trigger
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manual-trigger .

# 第二阶段: 运行
FROM hub.tess.io/tess/ubuntu-22.04:hardened
COPY --from=builder /workspace/prow/cmd/manual-trigger/manual-trigger /usr/local/bin/manual-trigger

# 创建非 root 用户 (安全最佳实践)
RUN useradd -r -u 1000 -g root prow && \
chmod +x /usr/local/bin/manual-trigger

USER 1000:0
ENTRYPOINT ["/usr/local/bin/manual-trigger"]

构建命令:

1
2
docker build -t hub.tess.io/fesu/manual-trigger:latest \
-f prow/cmd/manual-trigger/Dockerfile .

六、使用示例

6.1 基础使用

1
2
3
4
5
6
7
8
9
10
11
# 触发 postsubmit 任务
curl -X POST "http://manual-trigger.tessprow/manual-trigger" \
-H "Content-Type: application/json" \
-d '{
"org": "tess",
"repo": "tessops",
"base_ref": "master",
"prowtype": "postsubmit",
"prowjob": "sddz-e2e-k8s-1.32",
"user": "admin"
}'

响应示例:

1
2
3
4
5
6
7
{
"success": true,
"message": "ProwJob created successfully",
"job_name": "715c1a56-f815-11f0-b860-8acb6d93d6ad",
"status_link": "https://prow.tessprow/prowjob?prowjob=715c1a56-f815-11f0-b860-8acb6d93d6ad",
"log_link": "https://prow.tessprow/log?job=sddz-e2e-k8s-1.32&id=20260328-100000"
}

6.2 触发 presubmit 任务

1
2
3
4
5
6
7
8
9
10
11
# 必须提供 pullrequest 参数
curl -X POST "http://manual-trigger.tessprow/manual-trigger" \
-H "Content-Type: application/json" \
-d '{
"org": "tess",
"repo": "tessops",
"base_ref": "master",
"prowtype": "presubmit",
"prowjob": "sddz-e2e-k8s-1.32",
"pullrequest": 123
}'

6.3 将 presubmit 任务作为 postsubmit 运行

1
2
3
4
5
6
7
8
9
# 即使任务定义为 presubmit,也可以在分支上运行
curl -X POST "http://manual-trigger.tessprow/manual-trigger" \
-d '{
"org": "tess",
"repo": "tessops",
"base_ref": "feature-branch",
"prowtype": "postsubmit",
"prowjob": "sddz-e2e-k8s-1.32"
}'

6.4 Shell 脚本封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash

MANUAL_TRIGGER_URL="http://manual-trigger.tessprow/manual-trigger"

# 触发任务函数
trigger_job() {
local org=$1
local repo=$2
local branch=$3
local job=$4
local type=${5:-postsubmit}
local pr=$6
local user=${7:-""}

local data="{\"org\":\"$org\",\"repo\":\"$repo\",\"base_ref\":\"$branch\",\"prowtype\":\"$type\",\"prowjob\":\"$job\"}"

if [ ! -z "$pr" ]; then
data=$(echo $data | sed "s/}$/,\"pullrequest\":$pr}/")
fi

if [ ! -z "$user" ]; then
data=$(echo $data | sed "s/}$/,\"user\":\"$user\"}/")
fi

echo "📤 触发任务: $data"
response=$(curl -s -X POST $MANUAL_TRIGGER_URL \
-H "Content-Type: application/json" \
-d "$data")

echo "$response" | jq .

# 提取日志链接
log_link=$(echo "$response" | jq -r '.log_link')
if [ "$log_link" != "null" ] && [ ! -z "$log_link" ]; then
echo "📊 日志链接: $log_link"
fi
}

# 使用示例
trigger_job "tess" "tessops" "master" "sddz-e2e-k8s-1.32" "postsubmit" "" "admin"

七、与 Prow 生态的集成

7.1 Prow 配置文件

Manual-Trigger 依赖 Prow 配置文件来查找任务定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# config/jobs/tess/tessops/tessops-presubmits.yaml
presubmits:
tess/tessops:
- name: sddz-e2e-k8s-1.32
always_run: true
decorate: true
spec:
containers:
- image: gcr.io/k8s-testimages/kubekins-e2e:latest
command:
- runner.sh
args:
- make
- test
env:
- name: AUTHOR
value: "" # 会被 manual-trigger 覆盖

7.2 与其他 Prow 组件交互

graph LR
    A[Manual-Trigger] -->|创建 ProwJob| B[Kubernetes API]
    B --> C[Plank 调度器]
    B --> D[Deck Web UI]
    B --> E[Crier 通知器]
    C -->|创建 Pod| F[执行任务]
    D -->|展示状态| G[用户界面]
    E -->|上报结果| H[GitHub/Slack]

八、安全与监控

8.1 RBAC 权限

最小权限原则:

权限 说明
create 只能创建 ProwJob
get 只能查询 ProwJob 状态
list 只能列出 ProwJob
delete 不能删除 ProwJob
update 不能更新 ProwJob

8.2 监控指标

服务暴露 Prometheus metrics (端口 9090):

1
2
// 在 main() 中启用
metrics.ExposeMetrics("manual-trigger", configAgent.Config().PushGateway, o.instrumentationOptions.MetricsPort)

可监控的指标:

  • HTTP 请求计数
  • 请求延迟
  • 错误率
  • ProwJob 创建成功/失败次数

Prometheus 查询示例:

1
2
3
4
5
6
7
8
9
# 每秒请求数
rate(http_requests_total{job="manual-trigger"}[5m])

# 错误率
rate(http_requests_total{job="manual-trigger",code=~"4.."}[5m])
/ rate(http_requests_total{job="manual-trigger"}[5m])

# P95 延迟
histogram_quantile(0.95, http_request_duration_seconds_bucket{job="manual-trigger"})

8.3 运行安全

  • 非 root 用户: UID 1000
  • 只读配置: ConfigMap 挂载为 readOnly
  • 健康检查: liveness 和 readiness probe
  • 资源限制: CPU 和内存限制

九、故障排查

9.1 常见错误

错误 1: Job not found in config

1
2
3
4
{
"success": false,
"message": "Job sddz-e2e-k8s-1.32 not found in config for tess/tessops"
}

原因:

  • 任务名称拼写错误
  • 任务未在 Prow 配置中定义
  • org/repo 组合错误

解决方法:

1
2
3
4
5
# 检查 Prow 配置
kubectl get configmap config -n tessprow -o yaml | grep "sddz-e2e-k8s-1.32"

# 或查看配置文件
cat config/jobs/tess/tessops/tessops-presubmits.yaml | grep "name:"

错误 2: Pull request number is required

1
2
3
4
{
"success": false,
"message": "Pull request number is required for presubmit jobs. Please provide 'pullrequest' parameter."
}

原因: presubmit 任务必须提供 PR 号

解决方法: 添加 pullrequest 参数或改用 prowtype=postsubmit

9.2 调试技巧

查看 ProwJob 状态

1
2
3
4
5
6
7
8
# 列出最近的 ProwJobs
kubectl get prowjobs -n tessprow --sort-by=.metadata.creationTimestamp | tail -10

# 查看特定 ProwJob 详情
kubectl get prowjob <job_name> -n tessprow -o yaml

# 查看 Pod 日志
kubectl logs -n tessprow <pod_name>

查看服务日志

1
2
# 查看 manual-trigger 日志
kubectl logs -n tessprow -l app=manual-trigger --tail=100 -f

十、总结

10.1 Manual-Trigger 的核心价值

价值 说明
灵活性 绕过 GitHub webhook,手动控制 CI 任务
可测试性 在不创建 PR 的情况下测试任务
可运维性 用于维护、热修复、紧急部署
可扩展性 支持自定义参数和环境变量

10.2 技术亮点

简洁的架构: 单一 HTTP 服务,职责清晰
Kubernetes 原生: 使用 CRD,与 Prow 生态无缝集成
类型安全: Go 强类型,减少运行时错误
优雅的错误处理: 详细的错误信息,易于调试
等待机制: BuildID 轮询,提供完整的用户体验

10.3 适用场景

场景 说明
手动测试 开发新 CI 任务时,手动触发测试
紧急修复 需要立即在特定分支运行测试
批量触发 脚本批量触发多个任务
定制化执行 使用自定义环境变量运行任务
无 PR 测试 在分支上运行 presubmit 类型的任务

10.4 扩展阅读


参考资料

  1. Kubernetes test-infra 仓库
  2. Prow 官方文档
  3. Manual-Trigger 源码
  4. ProwJob API 定义

作者: Tashen
日期: 2026-03-28
标签: #Kubernetes #Prow #CI/CD #Go #DevOps


💡 提示: 如果你在使用 Manual-Trigger 过程中遇到问题,欢迎在评论区讨论!