【免费下载链接】tailcat
like netcat, but over Tailscale's data plane, without Tailscale's control plane
本文是 tailcat(基于 Tailscale 数据面、无需控制面的 netcat 风格工具)的版本发布(Release)操作手册。核心内容以仓库根目录的 RELEASING.md 为骨架,并结合仓库中的 tag.sh、.goreleaser.yaml、Dockerfile.goreleaser 与 .github/workflows/release.yml 等真实配置展开。读完本文,你将掌握:如何用./tag.sh打一个 SSH 签名的带注释标签、推标签后 GitHub Actions 如何触发 GoReleaser 构建产物、发布包含哪些平台工件与容器镜像、tailcat version的版本号从哪里来,以及如何在本地用goreleaser release --snapshot做不发布的全量试构建。
一、整体发布模型:推 Tag 即发布
tailcat 的发布模型非常简洁:发布 = 推送一个版本标签。不需要手工登录任何发布平台,也不需要本地机器执行构建——构建与发布全部由托管在 GitHub Actions 上的 Release 工作流(.github/workflows/release.yml)完成。
工作流的触发条件是推送任意v*格式的标签(见 .github/workflows/release.yml 中的on: push: tags: ["v*"])。触发后,工作流会按以下步骤执行:
- 检出代码(
fetch-depth: 0,即完整克隆历史,用于生成 changelog); - 安装与
go.mod匹配的 Go 工具链; - 用
docker/setup-buildx-action准备 buildx 构建器(多架构容器镜像需要docker-container驱动); - 登录
ghcr.io容器仓库; - 运行 GoReleaser(版本
~> v2),执行goreleaser release --clean,使用的配置是仓库根目录的.goreleaser.yaml。
GoReleaser 在 CI 中完成全部工作:编译各平台二进制、打包归档、生成.deb/.rpm、构建并推送容器镜像、生成校验和文件,最终创建一个 GitHub Release 草稿(draft),changelog 由提交日志自动生成。草稿对关注者(watchers)不可见,直到发布者手动编辑并发布。
这一设计的关键点是 .goreleaser.yaml 中的release.draft: true:发布动作(publish)才是通知关注者的那一步,因此发布通知里携带的是人工整理过的发布说明,而不是机器生成的提交日志。
工作流权限
contents: write:允许创建草稿 Release;packages: write:允许向ghcr.io推送容器镜像。
二、打标签:运行 ./tag.sh
发布的第一步是在本地打一个带注释的、SSH 签名的版本标签。这一步由仓库根目录的 tag.sh 脚本完成,它会替你把安全校验都做掉。
使用方式
./tag.sh v0.1.0脚本接受0.1.0和v0.1.0两种写法,并统一规范化为v0.1.0形式。执行后脚本不会推送,而是打印出推送命令让你手动执行:
git push origin v0.1.0tag.sh 内部做了哪些检查
对照 tag.sh 源码,可以拆解出四个关键环节:
版本号格式校验(tag.sh):版本号必须匹配
vX.Y.Z形式,可选后缀-pre这类预发布标识(正则^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$),否则脚本直接报错退出。SSH 签名密钥检查(tag.sh):脚本要求 git 配置了
user.signingkey且指向你的 SSH 公钥。若未配置,会提示你执行:git config --global user.signingkey ~/.ssh/id_ed25519.pub也就是说,tailcat 使用SSH 签名(gpg.format=ssh)而不是传统的 OpenPGP 来签署发布标签。
以 origin 为唯一事实来源检查标签是否已存在(tag.sh):脚本通过
git ls-remote --tags origin "refs/tags/$tag"直接查询远端,而不是信任本地可能过期的引用。只要远端已存在同名标签,脚本立即失败,防止覆盖已发布的版本。本地未推送标签可安全替换(tag.sh):如果标签只存在于本地(从未推送过),脚本会先删除重建,然后执行签名:
git -c gpg.format=ssh tag -s -f -m "tailcat $tag" "$tag"-s表示签名,-f表示强制替换,-m指定提交信息(如tailcat v0.1.0)。
脚本全程set -euo pipefail,任何一步失败都会中断,保证不会留下半成品状态。
发布操作清单
- 确认
main分支的 Test 工作流(.github/workflows/test.yml)是绿色通过状态; - 运行
./tag.sh vX.Y.Z(前提:git 已配置 SSHuser.signingkey); - 按脚本提示执行
git push origin vX.Y.Z; - 到 Actions 页观察 Release 工作流执行;
- 工作流结束后,Releases 页面会出现包含全部工件的草稿;
- 编辑草稿:用人工撰写的发布说明替换或置于自动 changelog 之上,然后发布。发布(Publish)才是通知关注者的动作。
三、发布包含哪些工件(Artifacts)
二进制与安装包
每个 Release 都包含(由 .goreleaser.yaml 的 builds 与 nfpms、archives 段决定):
| 工件类型 | 平台/架构 | 格式 |
|---|---|---|
| Linux 静态二进制 | amd64、arm64、armv7 | tar.gz |
| Debian 包 | 上述三种架构 | .deb |
| RPM 包 | 上述三种架构 | .rpm |
| Windows 二进制 | amd64、arm64 | zip |
| 校验和文件 | 以上全部 | checksums.txt(SHA-256) |
编译细节值得展开:
- 静态编译:构建环境设置
CGO_ENABLED=0(.goreleaser.yaml),产出纯静态二进制,这也是容器镜像可以放心使用 distroless 基镜像的前提; - 裁剪符号:
-s -w剥离符号表与调试信息,减小体积; - 版本注入:
-X main.version=v{{ .Version }}(.goreleaser.yaml),把 GoReleaser 的版本号直接写进二进制; - 构建标签裁剪:编译时传入一长串
ts_omit_*构建标签(如ts_omit_dns、ts_omit_serve、ts_omit_health等,见 .goreleaser.yaml),用于裁剪掉 tailscale 库中 tailcat 用不到的功能模块,缩小二进制体积。配置文件注释明确要求该标签列表与build-tags.txt和internal/buildtags.ReleaseTags保持同步,并用go run ./internal/buildtags/printtags重新生成,且由测试强制校验(见 internal/buildtags/buildtags_test.go); - 平台矩阵:Linux/Windows × amd64/arm64,Linux 额外支持 arm(armv7),但忽略 Windows/arm 组合(.goreleaser.yaml);
- 归档命名:
tailcat_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }},Linux 用 tar.gz、Windows 用 zip,并附带 LICENSE 与 README.md(.goreleaser.yaml)。
.deb/.rpm包的元信息在 nfpms 段定义(.goreleaser.yaml):包名tailcat、维护者 Tailscale Inc.、BSD-3-Clause 许可证,安装目录/usr/bin。
容器镜像
每次发布还会向ghcr.io/tailscale/tailcat推送amd64 与 arm64 双架构镜像,同时打vX.Y.Z和latest两个标签(.goreleaser.yaml)。
镜像的构建方式很有特点——它不是从源码构建,而是复用 GoReleaser 已经编译好的二进制。看 Dockerfile.goreleaser:
FROM gcr.io/distroless/static-debian12:nonroot ARG TARGETPLATFORM COPY $TARGETPLATFORM/tailcat /usr/local/bin/tailcat ENTRYPOINT ["/usr/local/bin/tailcat"]几点值得注意:
- 基镜像是
gcr.io/distroless/static-debian12:nonroot。distroless 静态基镜像自带CA 证书(连接 DERP 中继做 TLS 握手需要)和nonroot 用户可写的家目录(缓存 DERP 地图、存放生成的密钥); COPY $TARGETPLATFORM/tailcat说明它依赖构建上下文中按平台分目录预置的二进制,所以直接从仓库 checkout 去docker build是行不通的——Dockerfile 头部的注释明确说明了这一点;- 容器内的状态全部位于 nonroot 用户的家目录:身份密钥在
~/.config/tailcat,缓存的 DERP 地图在~/.cache/tailcat。要跨运行持久化,可以挂载卷:docker run -v tailcat-state:/home/nonroot ghcr.io/tailscale/tailcat新建的命名卷会自动继承 nonroot 所有权。另一种方式是用
--key直接指定容器内任意位置挂载进来的密钥文件路径。
镜像还带有完整的 OCI 标签(org.opencontainers.image.title/source/version/revision/licenses),其中 revision 取自{{ .FullCommit }},方便追溯镜像对应的源码提交。
四、版本号从哪里来:tailcat version 的双通道逻辑
发布版二进制中嵌入的版本号来自 GoReleaser 的-ldflags -X main.version=...注入(.goreleaser.yaml)。在源码侧,对应的实现是 cmd/tailcat/tailcat.go:
// version is set via -ldflags by GoReleaser at release time. // It is empty for go-install and plain go-build builds. var version string // versionString returns the version set at release build time, // falling back to the module version from the Go build info. func versionString() string { if version != "" { return version } if bi, ok := debug.ReadBuildInfo(); ok && bi.Main.Version != "" { return bi.Main.Version } return "unknown" }逻辑分两层:
- 发布版:GoReleaser 注入的
main.version非空,tailcat version直接打印它; - 自编译版:如果用
go install github.com/tailscale/tailcat/cmd/tailcat@vX.Y.Z安装,-ldflags注入为空,此时回退到debug.ReadBuildInfo()读取Go build info 中的模块版本(即@后面的版本);本地go build且不带模块版本时,最终回退为"unknown"。
另外值得注意:tailcat version子命令是官方入口(cmd/tailcat/tailcat.go),但同时保留了tailcat --version这个未广告的别名——--version并不是注册过的 flag,而是 main 在命令行解析失败时做的特判(cmd/tailcat/tailcat.go),目的是兼容 nixpkgs 的versionCheckHook(见 cmd/tailcat/e2e_test.go 的说明与测试)。
五、本地试构建:goreleaser release --snapshot
发布前想在本地完整验证一遍构建、不打标签也不推送任何东西?安装 GoReleaser 后执行:
goreleaser release --snapshot --clean--snapshot:快照模式,版本号带-SNAPSHOT-后缀,不推送任何远程产物(不创建 Release、不推容器镜像);--clean:开始前清空dist/目录。
工件全部落在dist/(该目录在.gitignore中,不会污染仓库)。
快照模式与真实发布的差异
快照模式下有两个与容器镜像相关的行为差异,需要提前知晓(见 RELEASING.md):
- 不打多架构 manifest:容器镜像以每个平台一个独立 tag的方式构建进本地 Docker daemon(如
tailcat:linux_amd64、tailcat:linux_arm64),而不是合并成一个 multi-arch manifest,也不会推送到 ghcr.io; - 需要 buildx 的 docker-container 驱动:构建镜像依赖该驱动,先执行
docker buildx create --use创建即可。
关于 .gitignore 与构建一致性
- .gitignore 将
dist/排除在版本控制之外,本地试构建不会污染提交; - 由于快照模式版本号带
-SNAPSHOT-后缀,恰好可以用来验证-X main.version=v{{ .Version }}的注入链路是否工作:dist/里生成的二进制执行tailcat version应能看到快照版本。
六、端到端流程回顾与排错要点
把整条链路串起来看:
./tag.sh v0.1.0 └─ 校验版本格式 → 校验 SSH signingkey → 查询 origin 防重复 → 打 SSH 签名带注释标签 git push origin v0.1.0 └─ 触发 .github/workflows/release.yml(tags: v*) └─ setup-go(版本取自 go.mod)→ setup-buildx → 登录 ghcr.io └─ goreleaser release --clean(配置 .goreleaser.yaml) ├─ 编译 linux/windows × amd64/arm64(/armv7) 静态二进制 ├─ 打包 tar.gz/zip、deb/rpm ├─ 构建并推送 amd64+arm64 容器镜像(vX.Y.Z + latest) ├─ 生成 checksums.txt └─ 创建草稿 Release(draft: true) 发布者编辑草稿 → Publish(通知关注者)常见坑位对照:
| 现象 | 原因 | 处理 |
|---|---|---|
tag.sh报user.signingkey is not set | git 未配置 SSH 签名密钥 | git config --global user.signingkey ~/.ssh/id_ed25519.pub |
tag.sh报 tag 已存在于 origin | 版本重复发布 | 换新版本号;不要覆盖已发布的 tag |
| 本地 tag 可被替换 | 该 tag 从未推送过 | 脚本自动-f重建,无需干预 |
| 本地快照构建容器镜像失败 | 缺少 buildx docker-container 驱动 | docker buildx create --use |
tailcat version打印 unknown | 本地go build无任何版本信息 | 用go install ...@vX.Y.Z或走 GoReleaser 构建 |
tailcat --version也能用 | nixpkgs 兼容别名 | 官方入口是tailcat version子命令 |
七、相关文件索引
发布链路涉及的关键仓库文件(均以仓库根目录为起点):
- RELEASING.md:本文的骨架文档,官方发布流程说明;
- tag.sh:本地打 SSH 签名标签的脚本;
- .goreleaser.yaml:GoReleaser v2 配置,定义构建矩阵、归档、deb/rpm、容器镜像、校验和与草稿 Release;
- Dockerfile.goreleaser:容器镜像 Dockerfile(基于 distroless,复用预编译二进制);
- .github/workflows/release.yml:Release 工作流(
tags: v*触发); - .github/workflows/test.yml:发布前需保证绿色的 Test 工作流;
- cmd/tailcat/tailcat.go:
main.version变量与versionString()双通道回退逻辑; - internal/buildtags/buildtags_test.go:强制构建标签列表与 GoReleaser 配置同步的测试;
- cmd/tailcat/e2e_test.go:
--version兼容别名的端到端测试。
需要说明的是,本文介绍的流程以当前仓库实际内容为准:标签触发式发布、SSH 签名、GoReleaser v2 配置、draft 发布模型以及 distroless 容器镜像,均可在上述仓库文件中逐一核对。实际操作时请以仓库当前版本的 RELEASING.md 为准,注意版本号、tag 列表与配置文件的同步性。
【免费下载链接】tailcat
like netcat, but over Tailscale's data plane, without Tailscale's control plane
相关推荐
chezmoi 发布流程全解析:从 GoReleaser 测试构建到 cosign 签名的自动化发布管线
chezmoi 发布流程全解析:从 GoReleaser 测试构建到 cosign 签名的自动化发布管线 本文以 chezmoi 官方开发者文档 release
开发工具CLI配置管理Bitcoin 仓库 libsecp256k1 发布流程完全指南:从 Sanity Checks 到 tag 与归档签名发布
Bitcoin 仓库 libsecp256k1 发布流程完全指南:从 Sanity Checks 到 tag 与归档签名发布 本指南以 Bitcoin Core
区块链金融科技网络密码学fiftyone-db 发布全流程指南:从 GitHub Release 标签到 PyPI 自动构建发布
fiftyone db 发布全流程指南:从 GitHub Release 标签到 PyPI 自动构建发布 fiftyone db 是 FiftyOne http
人工智能计算机视觉数据集数据可视化数据标注模型评测
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考