第一章:Go部署反模式的演进与反思(2016–2022)
Go语言在早期生态中因“单二进制分发”特性被广泛用于云原生部署,但2016至2022年间,大量团队在实践中逐步暴露出系统性反模式。这些反模式并非源于语言缺陷,而是对Go运行时行为、依赖管理与操作系统交互的误读所导致。
二进制膨胀与隐式依赖陷阱
许多团队直接 go build 生成可执行文件,却忽略 CGO_ENABLED=0 的必要性。默认启用 CGO 会链接 libc,导致 Alpine 容器中运行失败。正确做法是:
# 构建真正静态链接的二进制(无 libc 依赖)
CGO_ENABLED=0 go build -a -ldflags '-s -w' -o app .
# -a 强制重新编译所有依赖;-s/-w 剥离调试信息和符号表
未加约束的构建常使二进制体积膨胀 3–5 倍,并引入不可控的动态链接风险。
环境感知缺失的配置硬编码
2018年前常见将数据库地址、超时值直接写入代码或 init() 函数,违反十二要素应用原则。典型错误示例:
func init() {
db, _ = sql.Open("postgres", "host=localhost port=5432 ...") // ❌ 环境耦合
}
应统一通过 os.Getenv() 或结构化配置加载,并配合 github.com/spf13/viper 实现环境感知解析。
容器生命周期管理失当
早期 Docker 部署常忽略 Go 程序对 SIGTERM 的响应。默认 http.Server.Shutdown() 未被调用,导致连接强制中断。正确模式需显式监听信号:
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() { http.ListenAndServe(":8080", mux) }()
// 捕获终止信号并优雅关闭
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
<-sig
srv.Shutdown(context.Background()) // ✅ 等待活跃请求完成
运维可观测性真空
2020年前多数服务缺乏健康检查端点与指标暴露。以下是最小可行可观测性补丁:
/healthz返回 HTTP 200(无业务逻辑依赖)/metrics暴露 Prometheus 格式指标(使用promhttp.Handler())- 日志统一采用
log/slog并注入slog.With("service", "api")
| 反模式类型 | 典型表现 | 修复成本 |
|---|---|---|
| 构建不可重现 | go.mod 未锁定依赖版本 |
中 |
| 信号处理缺失 | 容器重启丢失连接 | 低 |
| 配置硬编码 | 多环境需重新编译 | 高 |
这些教训推动了 go.work、GOTRACEBACK=system 和 net/http/pprof 标准化集成等后续演进。
第二章:Docker镜像分层失误——构建效率与安全性的双重崩塌
2.1 分层原理与Go二进制特性冲突的理论根源
分层架构依赖清晰的抽象边界与运行时动态协作,而Go的静态链接、CGO限制及无虚拟机设计,天然削弱层间解耦能力。
静态链接对依赖注入的阻断
// main.go —— 无法在运行时替换 storage 层实现
func main() {
db := &postgres.Storage{} // 编译期硬绑定
service := NewUserService(db) // 无反射/插件机制支持热替换
}
Go二进制将所有依赖(含cgo库)静态打包,导致层间接口无法通过动态加载实现多态切换,违背分层“可替换性”第一原则。
运行时能力对比表
| 特性 | Java (JVM) | Go (Native Binary) |
|---|---|---|
| 层间动态代理 | ✅ 支持字节码增强 | ❌ 无运行时类加载 |
| 跨层内存共享 | ❌ 堆隔离 | ✅ 共享同一地址空间 |
| 初始化时机控制 | ✅ 类加载器钩子 | ❌ 全局init顺序固定 |
冲突本质:抽象延迟 vs 执行确定性
graph TD A[分层诉求:运行时解耦] –> B[需延迟绑定/动态代理] C[Go特性:编译即终态] –> D[符号全解析+静态链接] B -.->|不可调和| D
2.2 COPY . /app vs COPY go.mod/go.sum优先级实践陷阱
Docker 构建缓存机制依赖 COPY 指令的执行顺序与文件变更感知粒度。错误顺序将导致无效缓存,延长构建时间。
为何 go.mod/go.sum 必须前置?
# ✅ 正确:先复制依赖声明,再复制源码
COPY go.mod go.sum ./
RUN go mod download
COPY . /app
go mod download仅依赖go.mod和go.sum;若后置COPY .,任何.go文件变更都会使go mod download缓存失效——即使依赖未变。
构建阶段缓存失效对比
| 步骤 | COPY 顺序 | 修改 main.go 后缓存是否复用 go mod download? |
|---|---|---|
| A | COPY . 先于 go.mod |
❌ 失效(因 COPY . 触发全量重运行) |
| B | COPY go.mod/go.sum 优先 |
✅ 复用(仅 go.mod/go.sum 变更才刷新) |
构建流程依赖关系(mermaid)
graph TD
A[go.mod/go.sum] --> B[go mod download]
B --> C[.go source files]
C --> D[go build]
2.3 多阶段构建中build cache失效的典型误配场景
缓存键冲突:COPY路径与依赖顺序错位
以下 Dockerfile 片段因 COPY . /app 早于 RUN pip install,导致每次源码变更都使整个依赖层失效:
FROM python:3.11-slim
WORKDIR /app
COPY . . # ❌ 触发缓存失效(含requirements.txt)
RUN pip install -r requirements.txt # ⚠️ 实际应先COPY requirements.txt
COPY . .
逻辑分析:Docker 构建缓存基于指令内容及上下文哈希。COPY . . 将 requirements.txt 和代码一并载入,后续 pip install 的输入不可预测;正确做法是分离依赖声明与源码拷贝,确保 requirements.txt 单独作为缓存锚点。
常见误配模式对比
| 误配类型 | 缓存影响 | 推荐修复方式 |
|---|---|---|
COPY . 过早执行 |
依赖层全量重建 | 先 COPY requirements.txt |
多阶段中未显式指定 --from= |
build stage 名称变动导致引用失效 | 使用命名 stage(如 as builder) |
| 构建参数未参与缓存键 | --build-arg VERSION=1.0 变更不触发重缓存 |
启用 --cache-from + --cache-to |
缓存依赖链断裂示意
graph TD
A[stage0: base] --> B[stage1: builder]
B --> C[stage2: runtime]
C -.-> D{COPY . /app}
D --> E[缓存失效]
subgraph 正确链
F[stage1: COPY requirements.txt] --> G[stage1: pip install]
G --> H[stage2: COPY --from=builder /app/venv /venv]
end
2.4 静态链接二进制被动态层污染的调试复现路径
静态链接二进制本应隔离运行时依赖,但容器镜像中注入的动态库(如 LD_PRELOAD 或 /usr/lib/ld.so.preload)可劫持其符号解析,导致行为异常。
复现关键步骤
- 编译静态二进制:
gcc -static -o vulnerable main.c - 构建含
glibc动态层的容器镜像(即使主程序静态链接) - 注入预加载库:
echo "/lib/libhook.so" > /etc/ld.so.preload
污染触发机制
// libhook.so 中的 __libc_start_main hook 示例
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
int __libc_start_main(int (*main)(int,char**,char**), int argc, char **argv,
int (*init)(int,char**,char**), void (*fini)(void),
void (*rtld_fini)(void), void *stack_end) {
static typeof(__libc_start_main) *orig = NULL;
if (!orig) orig = dlsym(RTLD_NEXT, "__libc_start_main");
printf("[HOOK] Static binary intercepted!\n"); // 实际污染证据
return orig(main, argc, argv, init, fini, rtld_fini, stack_end);
}
该 hook 利用 RTLD_NEXT 绕过静态链接约束,在 _start 入口后、main 执行前插入逻辑——因 ld-linux.so 仍参与加载流程,静态二进制无法规避其预加载机制。
污染检测对照表
| 检测项 | 静态预期值 | 污染后表现 |
|---|---|---|
ldd ./vulnerable |
not a dynamic executable |
error while loading shared libraries(若误配) |
/proc/<pid>/maps |
无 .so 映射 |
出现 libhook.so 地址段 |
graph TD
A[启动静态二进制] --> B[内核加载 ld-linux.so 作为解释器]
B --> C[读取 /etc/ld.so.preload]
C --> D[强制 dlopen libhook.so]
D --> E[符号重绑定覆盖 __libc_start_main]
E --> F[执行 hook 后跳转原始入口]
2.5 基于.dockerignore与BuildKit优化的渐进式修复方案
传统构建中,冗余文件拖慢镜像构建并引入安全风险。渐进式修复始于精准排除——.dockerignore 是第一道防线:
# 忽略开发期非必需文件
.git
node_modules/
*.log
Dockerfile.dev
.env.local
该配置阻止敏感文件和临时产物进入构建上下文,减少传输体积与攻击面;BuildKit 则通过并行化、缓存感知和按需加载实现第二层加速。
启用方式只需环境变量:
DOCKER_BUILDKIT=1 docker build --progress=plain .
| 特性 | 传统构建 | BuildKit |
|---|---|---|
| 缓存复用粒度 | 每层 | 文件级 |
| 构建日志结构 | 线性文本 | 树状进度 |
| 多阶段依赖解析 | 静态 | 动态裁剪 |
graph TD
A[源码目录] --> B[.dockerignore过滤]
B --> C[BuildKit构建上下文]
C --> D[按需解压+增量缓存]
D --> E[精简镜像输出]
第三章:CGO_ENABLED误关——跨平台兼容性与性能断崖的隐秘开关
3.1 CGO机制与Go runtime内存模型的底层耦合关系
CGO并非简单的函数桥接层,而是深度嵌入Go runtime内存生命周期管理的关键枢纽。
数据同步机制
当C代码访问Go分配的内存(如*C.char指向[]byte底层数组)时,runtime必须确保GC不回收该对象。Go通过runtime.cgoMmap和runtime.cgoUnmmap注册/注销内存区域,并在GC标记阶段扫描cgoCallers链表。
// 示例:C函数持有Go字符串数据的典型风险
func unsafeCUse(s string) {
cs := C.CString(s) // 分配C堆内存,但s本身仍在Go堆
defer C.free(unsafe.Pointer(cs))
// 若s无其他引用,GC可能在此后立即回收其底层数组!
}
此调用触发runtime.cgoAlloc,将Go内存页标记为“C可达”,阻止GC回收;参数cs本质是unsafe.Pointer,需开发者手动保证生命周期对齐。
内存屏障与栈映射
Go goroutine栈与C栈独立,CGO调用时runtime执行栈切换并插入写屏障:
| 事件 | Go runtime动作 |
|---|---|
C.somefunc() 调用 |
切换至系统栈,禁用GMP调度器抢占 |
| C回调Go函数 | 注册cgoCheckPointer校验指针有效性 |
graph TD
A[Go goroutine] -->|CGO call| B[Runtime栈切换]
B --> C[禁用GC扫描当前G栈]
C --> D[C函数执行]
D --> E[返回Go,恢复GMP调度]
这种耦合使CGO成为内存安全的高危接口——任何绕过runtime.cgoCheck的指针传递都将破坏GC一致性。
3.2 Alpine镜像中net/ssl依赖缺失的现场诊断链路
现象复现与快速验证
运行 curl https://api.example.com 报错:curl: (60) SSL certificate problem: unable to get local issuer certificate。Alpine 默认不包含 CA 证书包,且 OpenSSL 未正确链接信任库。
根本原因定位
Alpine 使用 musl libc + BusyBox,其 ca-certificates 包需显式安装并触发证书更新:
# 安装证书并生成符号链接
apk add --no-cache ca-certificates
update-ca-certificates
逻辑分析:
apk add ca-certificates仅下载证书 PEM 文件至/usr/share/ca-certificates/;update-ca-certificates才会扫描、合并并软链至/etc/ssl/certs/ca-certificates.crt——这是 OpenSSL 和 curl 默认信任路径。
依赖链路可视化
graph TD
A[Alpine 基础镜像] --> B[无 ca-certificates]
B --> C[OpenSSL 无法加载根证书]
C --> D[curl/wget/Python ssl 模块均失败]
D --> E[update-ca-certificates 调用 update-ca-certificates.d/ 脚本]
关键检查清单
- ✅
ls -l /etc/ssl/certs/ca-certificates.crt是否存在且非空 - ✅
openssl version -d输出路径下是否存在certs目录 - ✅
cat /etc/os-release | grep alpine确认版本(≥3.14 含默认证书机制优化)
3.3 CGO_ENABLED=0下time.Now()精度退化与syscall阻塞的真实案例
当 CGO_ENABLED=0 编译时,Go 运行时无法调用 glibc 的 clock_gettime(CLOCK_MONOTONIC),退而使用 syscall.Syscall(SYS_gettimeofday, ...) 实现 time.Now() —— 该 syscall 在 Linux 2.6+ 虽被优化为 vDSO 快路径,但仍依赖内核 gettimeofday 实现,其分辨率受限于 HZ 配置(通常 100–1000 Hz),而非高精度 TSC。
精度对比实测数据(x86_64, kernel 5.15)
| 环境 | time.Now() 最小间隔(ns) | 典型抖动(ns) |
|---|---|---|
CGO_ENABLED=1(vDSO clock_gettime) |
~20 | |
CGO_ENABLED=0(syscall gettimeofday) |
~1,000,000 | ±300,000 |
// 示例:在 CGO_ENABLED=0 下观测时间跳变
for i := 0; i < 5; i++ {
t := time.Now()
fmt.Printf("UnixNano: %d\n", t.UnixNano()) // 输出可能重复或跳跃 1ms
runtime.Gosched()
}
此代码在纯静态链接模式下反复调用
time.Now(),因gettimeofday仅更新于 tick 中断(如jiffies更新),故连续调用常返回相同纳秒值,导致逻辑误判“零耗时”。
根本原因链
graph TD
A[CGO_ENABLED=0] --> B[禁用 libc clock_gettime]
B --> C[回退至 syscall gettimeofday]
C --> D[依赖内核 jiffies tick]
D --> E[分辨率 ≈ 1000/HZ ms]
E --> F[time.Since 可能返回 0 或突增 1ms]
- 影响面:高频定时器、实时采样、分布式 Span 时间戳对齐;
- 规避方案:启用 CGO(默认)、或显式使用
runtime.nanotime()(非导出,不推荐生产)。
第四章:/tmp挂载陷阱——容器生命周期与临时文件语义的错位战争
4.1 tmpfs挂载覆盖宿主机/tmp导致Go os.TempDir()行为漂移
Go 的 os.TempDir() 会优先读取 $TMPDIR,其次尝试 /tmp;当容器以 tmpfs 挂载 /tmp(如 docker run --tmpfs /tmp:rw,size=100M),该路径虽存在且可写,但其内存特性与磁盘临时目录语义不同。
行为差异根源
- 宿主机
/tmp通常为 ext4/xfs 持久化文件系统 tmpfs是基于内存的虚拟文件系统,重启即清空、无持久性、inode 限制更严
典型故障现象
ioutil.TempFile("", "log-*.log")创建成功但进程退出后文件消失- 并发高时触发
no space left on device(实际是tmpfsinode 耗尽,非磁盘空间)
Go 运行时检测逻辑
// 源码 runtime/cgo/errno.go 中隐式依赖路径可写性与stat结果
// os.TempDir() 实际调用 syscall.Stat("/tmp") → 若返回 Success 且 Mode&0200 != 0,则采纳
该逻辑不校验挂载类型,仅验证可写性与存在性,导致
tmpfs被误认为“标准临时目录”。
| 场景 | /tmp 类型 |
os.TempDir() 返回值 |
是否符合预期 |
|---|---|---|---|
| 宿主机原生 ext4 | ext4 | /tmp |
✅ |
| Docker tmpfs 挂载 | tmpfs | /tmp |
❌(语义漂移) |
graph TD
A[os.TempDir()] --> B{stat /tmp}
B -->|success & writable| C[/tmp]
B -->|fail| D[$TMPDIR]
C --> E[不区分挂载类型]
E --> F[行为漂移]
4.2 systemd-tmpfiles.d配置与容器init进程的时序竞态分析
systemd-tmpfiles.d 在容器启动早期即被 systemd 扫描执行,而容器内 init 进程(如 tini 或 dumb-init)往往在 PID 1 启动后才完成初始化——二者存在微妙的启动窗口差。
竞态根源:执行时机错位
tmpfiles由systemd-tmpfiles-setup.service触发,依赖local-fs.target- 容器
init进程需等待exec调用完成、信号代理注册完毕,延迟可达毫秒级 - 若
tmpfiles创建/run/myapp并设chmod 0755,而应用在init尚未接管前尝试mkdir,将因权限冲突失败
典型配置示例
# /etc/tmpfiles.d/myapp.conf
d /run/myapp 0755 root root - # 创建目录,但不等待init就绪
f /run/myapp/ready 0644 root root - # 标记文件,易被提前写入
此配置未声明
After=依赖链,systemd不感知容器init生命周期。d行创建目录无条件执行;f行若被重复写入(如健康检查脚本误触),将破坏原子性语义。
竞态缓解策略对比
| 方案 | 可靠性 | 侵入性 | 适用场景 |
|---|---|---|---|
Type=oneshot + ExecStartPre |
★★★★☆ | 高(需改容器entrypoint) | 关键服务 |
tmpfiles 中 L 符号链接 + 延迟挂载 |
★★★☆☆ | 中 | 临时目录解耦 |
systemd-run --scope 动态生成 |
★★★★★ | 低(运行时注入) | CI/CD 环境 |
时序关键路径(mermaid)
graph TD
A[containerd start] --> B[systemd PID 1 init]
B --> C[systemd-tmpfiles-setup.service]
B --> D[tini exec app]
C --> E[创建/run/myapp]
D --> F[app open /run/myapp]
E -.->|竞态窗口| F
4.3 net/http/pprof与os/exec.Command临时目录泄漏的压测复现
复现环境配置
- Go 1.21+,启用
net/http/pprof默认路由 - 压测工具:
wrk -t4 -c100 -d30s http://localhost:8080/debug/pprof/heap - 关键诱因:
os/exec.Command在未显式指定Dir时,自动创建临时工作目录但未清理
泄漏核心代码片段
func handlePprofLeak(w http.ResponseWriter, r *http.Request) {
cmd := exec.Command("sh", "-c", "echo 'test'")
// ❌ 缺失 cmd.Dir = os.TempDir() + "/safe",且未 defer os.RemoveAll()
if err := cmd.Run(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
exec.Command内部调用os.MkdirTemp("", "cmd-*")创建临时目录,若进程异常退出或未主动清理,目录持续残留。pprof 接口高频调用会加速泄漏。
泄漏量级对比(30秒压测)
| 并发数 | 临时目录数 | 磁盘占用 |
|---|---|---|
| 50 | ~120 | 1.2 MB |
| 200 | ~480 | 4.7 MB |
修复路径
- ✅ 显式设置
cmd.Dir指向可控路径 - ✅ 使用
defer os.RemoveAll(cmd.Dir)配合defer清理 - ✅ 或改用
exec.CommandContext+ 超时控制阻断长生命周期
graph TD
A[pprof 请求] --> B[触发 exec.Command]
B --> C[os.MkdirTemp 创建 dir]
C --> D{是否显式清理?}
D -->|否| E[目录残留]
D -->|是| F[os.RemoveAll 执行]
4.4 基于XDG_RUNTIME_DIR迁移与自定义TMPDIR的生产级规避策略
在容器化与多用户共享宿主环境中,/tmp 的竞争与清理策略常导致临时文件丢失或权限冲突。核心解法是解耦运行时路径与传统临时目录。
为何优先选用 XDG_RUNTIME_DIR
- 由 systemd 用户实例自动创建(
/run/user/$UID),生命周期绑定会话 - 默认 700 权限,天然隔离多用户临时数据
- 不受
systemd-tmpfiles --clean定期清扫影响
动态覆盖 TMPDIR 的安全方式
# 在服务启动前注入(推荐 via ExecStartPre)
export TMPDIR="${XDG_RUNTIME_DIR:-/tmp}/app-tmp"
mkdir -p "$TMPDIR"
chmod 700 "$TMPDIR"
逻辑说明:
XDG_RUNTIME_DIR缺失时降级至/tmp;mkdir -p确保幂等;chmod 700防止跨用户访问。该变量需在进程环境生效前设置,不可在子 shell 中覆盖。
运行时路径策略对比
| 策略 | 生命周期 | 权限控制 | 清理风险 | 适用场景 |
|---|---|---|---|---|
/tmp |
全局、持久 | 弱(1777) | 高(daily cron) | 调试脚本 |
$XDG_RUNTIME_DIR/tmp |
会话级、自动销毁 | 强(700) | 无 | 生产服务 |
自定义 TMPDIR |
进程级 | 可控 | 依赖应用自觉清理 | 有状态中间件 |
graph TD
A[应用启动] --> B{XDG_RUNTIME_DIR exists?}
B -->|Yes| C[使用 $XDG_RUNTIME_DIR/app-tmp]
B -->|No| D[fallback to /tmp/app-$PID]
C --> E[set TMPDIR & mkdir -p]
D --> E
E --> F[应用写入临时文件]
第五章:反模式终结者:从防御性部署到可验证交付
防御性部署的代价:一个电商大促事故复盘
某头部电商平台在双十一大促前执行“防御性部署”:运维团队手动冻结所有非紧急变更,回滚上周灰度发布的库存校验服务,并启用备用静态页面兜底。结果大促首小时订单创建成功率骤降42%——根本原因竟是被冻结的库存服务中包含一项关键幂等修复(PR#8921),而备用页面完全绕过分布式锁逻辑。该事件暴露防御性策略本质是用确定性停滞换取虚假安全感。
可验证交付的核心契约:三类自动化门禁
| 门禁类型 | 触发阶段 | 验证目标 | 失败响应 |
|---|---|---|---|
| 合规门禁 | MR合并前 | OpenAPI规范一致性、敏感配置扫描 | 拒绝合并,附CVE编号与修复指引 |
| 行为门禁 | 构建后镜像生成 | 基于Golden Path的容器启动时长≤3.2s、内存峰值≤1.4GB | 自动触发性能回归比对报告 |
| 场景门禁 | 生产环境预发布 | 支付链路全链路压测(模拟120%峰值流量) | 熔断新版本,自动回滚至v2.3.7 |
真实世界的验证流水线:某银行核心账务系统迁移案例
flowchart LR
A[Git Commit] --> B{合规门禁}
B -->|通过| C[构建镜像]
C --> D{行为门禁}
D -->|通过| E[部署至预发布集群]
E --> F[调用生产影子流量]
F --> G{场景门禁:实时资金核验}
G -->|通过| H[自动打标promote-to-prod]
G -->|失败| I[触发告警并隔离镜像]
关键技术杠杆:声明式验证即代码
采用Conftest+OPA策略引擎将业务规则转化为可执行验证逻辑:
package delivery
deny[msg] {
input.container.resources.requests.memory < "2Gi"
msg := sprintf("内存请求不足:当前%s,最低要求2Gi", [input.container.resources.requests.memory])
}
deny[msg] {
count(input.env) < 3
msg := sprintf("环境变量缺失:仅配置%d个,需至少3个(DB_URL/REDIS_HOST/SECRET_KEY)", [count(input.env)])
}
组织协同的破局点:SRE与QA共建验证资产库
- QA团队将237个核心交易场景封装为独立验证单元(如“跨行转账超时补偿”、“余额不足拒绝支付”)
- SRE团队为每个单元定义SLI阈值(P99延迟≤850ms、错误率
- CI/CD流水线自动调用验证单元,失败时生成带火焰图的诊断包(含JVM堆栈+数据库慢查询日志)
度量驱动的演进路径
某证券交易平台实施可验证交付12个月后关键指标变化:
- 平均恢复时间(MTTR)从47分钟降至6.3分钟
- 生产环境配置漂移事件下降92%(从月均17次→1.4次)
- 开发人员提交到生产就绪平均耗时缩短至22分钟(含全部验证环节)
反模式识别清单:当出现这些信号时立即干预
- 部署窗口期超过3小时且依赖人工值守
- 回滚操作需要修改数据库schema
- 监控告警阈值随每次发布动态调整
- 发布checklist中存在“确认客服无投诉”等主观判断项
工具链集成实践:GitHub Actions + Argo Rollouts + Datadog
通过自定义Action实现验证结果自动注入Kubernetes ConfigMap:
- name: Publish verification report
uses: actions/github-script@v6
with:
script: |
const report = JSON.stringify({
timestamp: new Date().toISOString(),
passRate: context.payload.workflow_run.conclusion === 'success' ? 100 : 87.2,
criticalFailures: ${{ steps.validate.outputs.critical_count }}
})
await github.rest.core.createOrUpdateRepoSecret({
owner: context.repo.owner,
repo: context.repo.repo,
secret_name: 'VERIFICATION_REPORT',
encrypted_value: core.encodeBase64(report)
}) 