第一章:Go程序交付物命名的核心概念与CNCF合规性概览
Go程序交付物的命名不仅是工程可维护性的基础,更是云原生生态中可发现性、可审计性与自动化治理的前提。在CNCF(Cloud Native Computing Foundation)技术栈中,交付物(如二进制文件、容器镜像、Helm Chart 或 OCI Artifact)的命名需遵循语义化、唯一性、可解析性三项核心原则:语义化要求名称体现功能意图(如 prometheus-alertmanager 而非 app-v2),唯一性确保跨环境无歧义(结合组织域、项目名与版本标识),可解析性则支持工具链自动提取元数据(例如从 ghcr.io/acme/otel-collector:v0.104.0 中识别注册中心、租户、组件与语义版本)。
命名约束与CNCF推荐实践
CNCF SIG Architecture 明确建议:
- 二进制文件名应小写、连字符分隔、无扩展名(
cli-tool, ✅;CLI_Tool.exe, ❌) - 容器镜像名须符合 OCI Distribution Spec,包含权威域名、路径和可选标签(
quay.io/coreos/etcd:v3.5.19) - 所有交付物应避免硬编码环境后缀(如
-prod,-staging),改用配置驱动或标签区分
Go构建产物命名实操示例
使用 go build 时,通过 -o 参数显式指定输出路径与名称,强制统一风格:
# 构建跨平台二进制,名称含组织前缀与语义版本
GOOS=linux GOARCH=amd64 go build -o ./dist/acme-authz-server-v1.8.3 ./cmd/authz-server
# 验证文件名是否符合CNCF命名规范(小写、连字符、无空格/下划线/点号前缀)
ls ./dist/ | grep -E '^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$' # 匹配有效命名模式
| 组件类型 | 合规命名示例 | 违规示例 | 风险说明 |
|---|---|---|---|
| CLI 工具 | fluxctl, kubebuilder |
KubeBuilder.exe |
大小写混用、平台耦合 |
| 容器镜像 | ghcr.io/kyverno/kyverno:v1.12.2 |
localhost:5000/kyverno:latest |
缺失权威域名、不可追溯 |
| Helm Chart 包 | kyverno-3.1.2.tgz |
chart_v3.1.2.zip |
扩展名非标准、命名冗余 |
交付物命名不是开发收尾的装饰步骤,而是服务生命周期管理的起点——它直接影响CI/CD流水线的制品归档策略、SBOM生成准确性及漏洞扫描覆盖率。
第二章:Go程序交付物命名的七步标准化流程解析
2.1 基于Go模块路径(module path)推导语义化主名称:理论定义与go.mod实战校验
Go 模块路径(如 github.com/org/repo/v2)不仅是依赖定位标识,更是语义化主名称(semantic main name)的唯一来源——它隐式声明了包的权威命名空间与版本边界。
模块路径结构解析
- 协议前缀(
https://)被忽略 - 主机名(
github.com)定义注册中心 - 路径段
org/repo构成组织级命名空间 - 尾缀
/v2显式标识主版本,决定go list -m输出的main标识
go.mod 实战校验
// go.mod
module github.com/example/cli/v3
go 1.21
此模块路径直接推导出语义化主名称为
github.com/example/cli,v3仅影响导入兼容性,不改变主名称本质。go list -m -json将返回"Path": "github.com/example/cli/v3",而"Main"字段恒为true,印证其作为根模块的命名权威性。
| 模块路径示例 | 推导主名称 | 是否含主版本后缀 |
|---|---|---|
golang.org/x/net |
golang.org/x/net |
否 |
github.com/gorilla/mux/v2 |
github.com/gorilla/mux |
是 |
graph TD
A[go.mod module directive] --> B[提取主机+路径前缀]
B --> C[剥离/vN后缀]
C --> D[语义化主名称]
2.2 版本标识规范化:Go build -ldflags -X 与 Helm chart version双轨对齐实践
在持续交付中,二进制版本与 Helm Chart 版本需严格一致,否则将引发环境漂移与回滚失效。
编译时注入 Go 应用版本
go build -ldflags "-X 'main.Version=${CI_COMMIT_TAG:-dev}' \
-X 'main.Commit=${CI_COMMIT_SHORT_SHA}'" \
-o bin/app ./cmd/app
-X 通过符号链接修改未导出包变量;${CI_COMMIT_TAG} 来自 CI 环境,确保 main.Version 在运行时可读,避免硬编码。
Helm Chart 版本同步机制
| 源头 | Go 二进制 | Chart.yaml version |
|---|---|---|
| 发布触发 | v1.2.0 tag |
v1.2.0 |
| 预发布构建 | v1.2.0-rc.1 |
v1.2.0-rc.1 |
双轨校验流程
graph TD
A[Git Tag 推送] --> B{CI 触发}
B --> C[注入 -X main.Version]
B --> D[更新 Chart.yaml version]
C --> E[生成带版本的二进制]
D --> F[打包带同版本的 chart]
E & F --> G[归档至同一制品库路径]
2.3 构建目标平台标识(GOOS/GOARCH)编码规则:交叉编译产物命名一致性验证
Go 的交叉编译依赖 GOOS 和 GOARCH 环境变量组合生成唯一目标标识,如 linux/amd64、windows/arm64。命名一致性是自动化构建与制品仓库管理的前提。
标准化命名格式
- 必须小写、无空格、仅含 ASCII 字母/数字和
/ GOOS值限定为:aix,darwin,dragonfly,freebsd,linux,netbsd,openbsd,plan9,solaris,windows,zosGOARCH值需匹配官方支持列表(见go tool dist list)
验证逻辑示例
# 检查当前环境是否生成合法标识
GOOS=linux GOARCH=arm64 go build -o "app-linux-arm64" main.go
此命令将生成严格遵循
<binary>-<GOOS>-<GOARCH>模式的二进制文件。go build内部会校验GOOS/GOARCH组合有效性,非法值(如GOOS=win32)将直接报错unsupported GOOS/GOARCH pair。
合法组合速查表
| GOOS | GOARCH | 示例文件名 |
|---|---|---|
| linux | amd64 | app-linux-amd64 |
| windows | arm64 | app-windows-arm64 |
| darwin | arm64 | app-darwin-arm64 |
自动化校验流程
graph TD
A[读取GOOS/GOARCH] --> B{是否在白名单中?}
B -->|否| C[拒绝构建并报错]
B -->|是| D[生成标准化文件名]
D --> E[写入制品元数据]
2.4 CNCF Artifact Naming Policy映射:从OCI镜像tag到Go二进制文件名的语义映射
CNCF Artifact Naming Policy 要求跨生态制品具备可预测、可解析、语义一致的命名结构。OCI镜像 tag(如 v1.2.3-rc.1+linux-amd64)需无损映射为 Go 构建产物的二进制文件名(如 myapp-v1.2.3-rc.1-linux-amd64)。
映射核心规则
- 去除 OCI tag 中的
@sha256:后缀(仅用于校验,非语义) - 将
/和:替换为-(避免路径歧义与 shell 解析问题) - 强制小写化以保障跨文件系统兼容性
示例转换逻辑
# 输入:registry.example.com/app/myapp:v1.2.3-rc.1@sha256:abc123
echo "registry.example.com/app/myapp:v1.2.3-rc.1@sha256:abc123" | \
sed -E 's|.*\/([^\/]+):([^@]+)(@.*)?|\1-\2|' | \
tr '[:upper:]' '[:lower:]'
# 输出:myapp-v1.2.3-rc.1
该命令提取镜像名与 tag 主体,剥离 registry 和 digest;tr 确保大小写归一,适配 Go 的 GOOS/GOARCH 命名惯例。
语义字段对齐表
| OCI Tag 片段 | Go 二进制名字段 | 说明 |
|---|---|---|
myapp |
前缀 | 镜像仓库名(不含 registry) |
v1.2.3-rc.1 |
版本标识 | 保留 SemVer + 预发布修饰符 |
linux-amd64 |
平台后缀 | 由 GOOS+GOARCH 注入 |
graph TD
A[OCI Image Ref] --> B{解析 registry/name:tag@digest}
B --> C[提取 name & tag]
C --> D[标准化分隔符与大小写]
D --> E[拼接 GOOS-GOARCH 后缀]
E --> F[最终二进制名]
2.5 Helm Chart元数据联动机制:Chart.yaml中appVersion/name字段与Go程序binary name自动同步方案
数据同步机制
Helm Chart 的 Chart.yaml 中 name 和 appVersion 字段常需与 Go 二进制文件名(如 myapp-v1.2.3-linux-amd64)保持一致,避免部署时版本错位。
实现方案核心
采用 Makefile + go build -ldflags + yq 构建流水线:
# Makefile 片段:自动提取并注入
BINARY_NAME := $(shell basename $(shell ls bin/myapp-*.tar.gz | head -n1) | cut -d'-' -f1)
APP_VERSION := $(shell basename $(shell ls bin/myapp-*.tar.gz | head -n1) | cut -d'-' -f2)
sync-chart-meta:
yq e -i '.name = "$(BINARY_NAME)" | .appVersion = "$(APP_VERSION)"' Chart.yaml
逻辑分析:
basename提取归档名,cut -d'-' -f1/2分别解析 binary 前缀与语义化版本;yq e -i原地更新 YAML,确保 Chart 元数据与构建产物严格对齐。
关键约束对比
| 字段 | 来源 | 是否可手动覆盖 | 同步触发时机 |
|---|---|---|---|
name |
二进制文件名前缀 | 否(CI 强制校验) | make sync-chart-meta |
appVersion |
归档名中第二段版本 | 否 | 同上 |
graph TD
A[Go build 生成 bin/myapp-v1.2.3-linux-amd64] --> B[Extract name/version via shell]
B --> C[yq 更新 Chart.yaml]
C --> D[Helm package 验证通过]
第三章:Kubernetes原生兼容性验证体系构建
3.1 Helm Chart包结构与Go交付物绑定关系:templates/_helpers.tpl中name函数的Go程序名注入实践
Helm Chart 的 templates/_helpers.tpl 是自定义模板函数的核心载体,其中 name 函数常被用于统一生成资源名称,但其默认实现仅基于 Chart 名称。若需将 Go 程序名(如 main.go 中的 app.Name 或构建时注入的 -ldflags="-X main.version=..." 变量)动态绑定,需在 values.yaml 中预留字段并透传。
name 函数增强实践
# values.yaml
app:
name: "user-service" # Go 二进制实际部署名,非 Chart 名
// _helpers.tpl 中重写 name 函数
{{- define "mychart.name" -}}
{{- default .Values.app.name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end }}
此处
default .Values.app.name .Chart.Name优先取 Go 交付物指定的应用名,确保 Kubernetes 资源标签(如app.kubernetes.io/name)与二进制语义一致;trunc 63满足 DNS-1123 限制,trimSuffix "-"避免末尾连字符非法。
| 绑定层级 | 来源 | 示例值 |
|---|---|---|
| Chart 层 | .Chart.Name |
mychart |
| Go 交付层 | .Values.app.name |
authd |
| 运行时层 | .Release.Name |
prod-auth |
graph TD
A[Go 编译时注入 app.Name] --> B[values.yaml 显式声明]
B --> C[_helpers.tpl 中 name 函数读取]
C --> D[K8s 资源 metadata.name 与 label 同步]
3.2 K8s Deployment/StatefulSet资源中container.image与Go二进制名称的语义一致性审计
容器镜像标签(container.image)与 Go 二进制可执行文件名应体现同一语义版本,否则将导致运行时行为歧义。
为什么需要语义对齐?
- 镜像
ghcr.io/org/app:v1.2.0应打包名为app的二进制(非myapp-linux-amd64) - 启动命令
command: ["/app"]依赖镜像内二进制命名一致性
典型不一致场景
- ❌
image: app:v1.2.0+ENTRYPOINT ["./app-linux-arm64"] - ✅
image: app:v1.2.0+ENTRYPOINT ["./app"]
自动化校验脚本片段
# Dockerfile 片段:标准化二进制安装路径
COPY app /app
RUN chmod +x /app
ENTRYPOINT ["/app"]
此处
/app是硬编码入口点,要求构建产物必须命名为app;若 Go 构建使用-o ./bin/myapp,则需同步修正COPY路径与ENTRYPOINT,否则 Pod 启动失败。
| 检查项 | 期望值 | 违例示例 |
|---|---|---|
container.image 标签 |
org/app:v1.2.0 |
org/app:latest |
| 容器内主二进制路径 | /app |
/usr/local/bin/myapp-v1 |
graph TD
A[CI 构建阶段] --> B[提取 go build -o 输出名]
B --> C[比对 image tag 中的组件名]
C --> D{一致?}
D -->|否| E[拒绝推送镜像]
D -->|是| F[打标并推送到 registry]
3.3 Operator SDK集成场景下Go程序名作为CRD spec字段默认值的声明式验证
在 Operator SDK v1.28+ 中,可通过 controller-gen 的 +kubebuilder:default 标签结合 Go 运行时信息实现动态默认值注入。
默认值注入机制
Operator 启动时通过 os.Args[0] 提取二进制名,并在 Reconcile 前由 Webhook 或 DefaultingWebhook 自动填充:
// +kubebuilder:default:=os.Args[0]
// +kubebuilder:validation:Optional
ProgramName string `json:"programName,omitempty"`
该注解不被 controller-gen 直接解析为 os.Args[0],而是作为标记供自定义 DefaultingWebhook 使用;实际默认值需在
Default()方法中调用filepath.Base(os.Args[0])计算并赋值。
验证流程关键节点
- ✅ CR 创建时未设
spec.programName→ DefaultingWebhook 注入进程名(如my-operator) - ✅ CR 更新时显式设为空字符串 → 不覆盖(因
omitempty+Optional约束) - ❌ 若
spec.programName非空但含非法字符 →ValidatingWebhook拒绝(正则校验^[a-z0-9]([-a-z0-9]*[a-z0-9])?$)
| 验证阶段 | 触发时机 | 是否可跳过 |
|---|---|---|
| Defaulting | CREATE/UPDATE 请求预处理 | 否 |
| Schema Validation | OpenAPI v3 生成时 | 是(需禁用) |
graph TD
A[CR YAML 提交] --> B{DefaultingWebhook}
B -->|未设置 programName| C[注入 os.Args[0] 基名]
B -->|已设置| D[透传原值]
C & D --> E[ValidatingWebhook 校验格式]
E -->|通过| F[持久化至 etcd]
第四章:自动化工具链与CI/CD流水线集成
4.1 go-release-action与helm package协同:基于Git Tag触发的双模命名自动生成流水线
当 Git 仓库打上语义化标签(如 v1.2.0)时,CI 流水线需同步发布 Go 二进制与 Helm Chart,且二者版本需严格对齐并支持双模命名:Go 产物用 app-v1.2.0-linux-amd64,Helm Chart 用 app-1.2.0.tgz(无前缀 v)。
双模版本提取逻辑
- name: Extract version
id: version
run: |
# 从 TAG_NAME 剥离 'v' 前缀,供 Helm 使用;保留原值供 Go 发布
echo "semver=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
echo "helm_version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
该步骤利用 Bash 参数扩展实现零依赖版本切分:$GITHUB_REF#refs/tags/ 去除路径前缀,$GITHUB_REF#refs/tags/v 进一步移除 v,生成 Helm 兼容的 1.2.0。
流水线协同编排
graph TD
A[Git Tag Push] --> B[Trigger Workflow]
B --> C[go-release-action]
B --> D[helm package]
C --> E[Binary: app-v1.2.0-OS-ARCH]
D --> F[Chart: app-1.2.0.tgz]
| 组件 | 版本来源 | 命名规则 |
|---|---|---|
go-release-action |
env.SEMVER |
v{semver}-{os}-{arch} |
helm package |
env.HELM_VERSION |
{name}-{helm_version}.tgz |
4.2 Makefile驱动的命名合规性检查:go list -m -f ‘{{.Path}}’ + helm lint双重校验脚本
在模块化 Helm Chart 开发中,Go 模块路径与 Chart 名称必须严格一致,否则 CI/CD 流程易因语义混淆失败。
核心校验逻辑
# Makefile 片段
CHART_NAME := $(shell helm show chart charts/myapp | grep '^name:' | cut -d' ' -f2)
MODULE_PATH := $(shell go list -m -f '{{.Path}}' 2>/dev/null)
check-naming:
@test "$$(echo $(MODULE_PATH) | awk -F'/' '{print $$NF}')" = "$(CHART_NAME)" \
|| (echo "❌ Module basename '$$(echo $(MODULE_PATH) | awk -F'/' '{print $$NF}')" \
"≠ Chart name '$(CHART_NAME)'"; exit 1)
@helm lint charts/myapp --strict
该规则先提取 go.mod 中模块路径末段作为预期 Chart 名,再比对 Chart.yaml 中实际 name 字段;随后执行 helm lint --strict 进行 YAML 结构与 Helm 最佳实践校验。
双重校验价值对比
| 维度 | go list -m 检查 |
helm lint 检查 |
|---|---|---|
| 关注焦点 | Go 模块与 Chart 命名一致性 | Chart 结构、values schema、模板语法 |
| 失败时机 | 构建早期(编译前) | 打包前(Helm 渲染前) |
graph TD
A[make check-naming] --> B[提取 MODULE_PATH]
A --> C[读取 CHART_NAME]
B & C --> D{basename 匹配?}
D -->|否| E[报错退出]
D -->|是| F[helm lint --strict]
F --> G[通过则允许发布]
4.3 Argo CD Application manifest中Go程序name字段的静态分析与动态注入策略
Argo CD 的 Application manifest 中,name 字段既是资源标识符,也是 Go 程序运行时解析的元数据入口点。其值需满足 Kubernetes DNS-1123 标准(小写字母、数字、连字符,且不以连字符开头/结尾),但实际构建中常混入环境变量或 Git 分支信息。
静态约束校验
# application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-go-app-prod # ✅ 合法:全小写+连字符+非首尾连字符
spec:
source:
repoURL: https://git.example.com/my-go-app.git
targetRevision: main
path: manifests/prod
此处
name被硬编码为静态字符串,利于 CI 流水线可重现性,但牺牲多环境灵活性;Argo CD Controller 在Apply()阶段会调用ValidateName()方法执行 RFC 1123 检查,失败则拒收。
动态注入机制
支持通过 kustomize 或 helm 插值实现运行时生成:
- Kustomize:
namePrefix+commonLabels - Helm:
{{ .Values.app.name }}模板注入 - Argo CD 自定义插件:通过
argocd-utilCLI 解析--name参数并 patch manifest
| 注入方式 | 是否支持 GitOps 原子性 | 运行时依赖 |
|---|---|---|
| 静态 YAML | ✅ 是 | 无 |
| Kustomize patch | ✅ 是 | kustomize v4.5+ |
| Helm template | ⚠️ 依赖 values.yaml 可控性 | helm v3.8+ |
// name_validator.go
func ValidateName(name string) error {
if len(name) == 0 {
return errors.New("name cannot be empty")
}
if !regexp.MustCompile(`^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$`).MatchString(name) {
return fmt.Errorf("invalid name %q: must match regex ^[a-z0-9]([a-z0-9\\-]*[a-z0-9])?$", name)
}
return nil
}
ValidateName函数在 Argo CD 控制器初始化 Application 对象时被调用,对metadata.name执行双阶段校验:长度非空 + 正则匹配;若用于 CI 工具链,建议在pre-commit钩子中复用该逻辑提前拦截非法命名。
graph TD A[CI Pipeline] –> B{name 来源} B –>|静态文件| C[直接校验 YAML] B –>|Kustomize| D[build 后校验 rendered YAML] B –>|Helm| E[template + yq 校验输出]
4.4 Prometheus指标命名规范联动:Go程序exporter binary name与metric_prefix的自动对齐机制
Prometheus 生态中,指标前缀(metric_prefix)应与二进制名(binary_name)语义一致,避免 node_exporter 暴露 redis_up 等歧义指标。
自动对齐实现原理
Go 启动时通过 os.Args[0] 提取二进制 basename,并标准化为 snake_case:
import "strings"
func deriveMetricPrefix() string {
bin := filepath.Base(os.Args[0]) // e.g., "mydb-exporter"
return strings.TrimSuffix(
strings.ReplaceAll(bin, "-", "_"),
"-exporter", // → "mydb"
)
}
逻辑说明:
filepath.Base剥离路径;ReplaceAll("-", "_")统一分隔符;TrimSuffix("-exporter")移除约定后缀。最终生成mydb_作为metric_prefix。
对齐验证规则
| 场景 | binary_name | 推导 metric_prefix | 是否合规 |
|---|---|---|---|
| 标准 exporter | postgres-exporter |
postgres_ |
✅ |
| 自定义服务 | iot-gateway |
iot_gateway_ |
✅ |
| 错误命名 | exporter-redis |
exporter_redis_ |
❌(语义倒置) |
数据同步机制
graph TD
A[os.Args[0]] --> B[basename → snake_case]
B --> C[apply suffix rules]
C --> D[set global prefix]
D --> E[metric.MustNewConstMetric<br/>with prefixed name]
第五章:演进趋势与社区最佳实践展望
云原生可观测性栈的融合演进
近年来,Prometheus、OpenTelemetry(OTel)与eBPF技术正加速深度集成。以CNCF毕业项目SigNoz为例,其v1.8版本已支持通过eBPF自动注入HTTP/gRPC指标,无需修改应用代码;同时将OTel Collector配置嵌入Kubernetes CRD,实现声明式可观测性策略下发。某跨境电商平台在2023年Q4完成迁移后,告警平均响应时间从83秒降至11秒,错误根因定位耗时下降67%。
GitOps驱动的SRE实践标准化
Git作为唯一事实源(Single Source of Truth)已成为主流。如下表所示,三类典型团队在GitOps落地路径上的差异:
| 团队类型 | 配置管理工具 | 自动化触发方式 | 平均发布失败率 |
|---|---|---|---|
| 初级SRE团队 | Helm + Kustomize | 手动合并PR后触发Argo CD Sync | 12.4% |
| 成熟云原生团队 | Jsonnet + Tanka | GitHub Actions监听tag推送 | 2.1% |
| 金融级合规团队 | Crossplane + OPA | 策略引擎校验+人工审批门禁 | 0.3% |
某国有银行核心支付系统采用Crossplane管理AWS RDS与Aurora资源,所有数据库变更必须经OPA策略引擎校验(如“禁止非加密存储”“备份保留≥35天”),策略违规请求被自动拦截并生成审计日志。
混沌工程从实验走向常态化
Netflix的Chaos Mesh已不再是“季度演练”,而是嵌入CI/CD流水线的必经关卡。某物流调度平台在Jenkinsfile中定义如下阶段:
stage('Chaos Validation') {
steps {
script {
sh 'kubectl apply -f chaos/network-delay.yaml'
timeout(time: 300, unit: 'SECONDS') {
waitUntil { sh(script: 'curl -s http://scheduler/api/health | grep "status":"up"', returnStatus: true) == 0 }
}
sh 'kubectl delete -f chaos/network-delay.yaml'
}
}
}
该机制使服务在遭遇网络抖动时的自动重试成功率提升至99.98%,且故障恢复SLA达标率稳定在99.95%以上。
开源社区协同治理新模式
Linux基金会主导的OpenSSF Scorecard项目正在重塑供应链安全实践。截至2024年6月,GitHub上Star数超5k的Go语言项目中,启用自动化Scorecard扫描并公开结果的占比已达41%。其中,Terraform Provider for Alibaba Cloud通过持续修复Scorecard检测出的token-permission和binary-artifacts问题,使其安全评分从5.2升至9.7,直接推动阿里云客户采纳率提升23个百分点。
AI辅助运维的生产化落地
Datadog推出的Watchdog Copilot并非概念验证,而是已在真实场景中接管部分L1事件处置。某在线教育平台将其接入PagerDuty,当检测到“API延迟P95 > 2s且错误率突增”时,Copilot自动执行三步操作:① 查询最近部署记录匹配变更;② 调取对应服务的火焰图快照;③ 向值班工程师推送含调用链截图与回滚命令的一键处置卡片——该流程已覆盖73%的常规性能告警。
可观测性即代码(Observability-as-Code)范式普及
随着OpenTelemetry Protocol(OTLP)成为W3C推荐标准,监控规则本身正被纳入IaC生命周期。HashiCorp Terraform Registry中observability类模块下载量在2024上半年同比增长217%,其中grafana-cloud-alert-rule模块支持将告警逻辑以HCL声明,与应用部署同步生效,避免传统手工配置导致的“监控漂移”。
flowchart LR
A[Git Commit] --> B{CI Pipeline}
B --> C[Build & Test]
B --> D[OTel Config Validation]
D --> E[Alert Rule Linting]
E --> F[Deploy to Grafana Cloud API]
F --> G[Rule Versioned in Git] 