开源项目自动化发布流水线:从手动打包到CI/CD集成
1. 项目概述:一个为开源项目量身定制的发布流水线
如果你维护过一个稍微有点规模的开源项目,尤其是那种需要跨平台编译、打包、发布到不同包管理器的项目,那你一定对发布日感到头疼。我说的不是写代码,而是代码写好之后,那一系列繁琐、重复且容易出错的操作:更新版本号、编译不同平台的二进制文件、生成校验和、打上 Git Tag、推送到 GitHub Releases、同步到 Homebrew 或 AUR…… 这些步骤,手动操作一次两次还行,但每次发布都来一遍,不仅效率低下,还极易因为手滑而出错。
pathintegral-institute/mcpm.sh这个项目,就是为了解决这个痛点而生的。它不是一个独立的工具,而是一个高度可定制、开箱即用的Shell 脚本集合,或者说,是一个“发布流水线”的参考实现。它的核心目标,是帮助开源项目维护者,特别是那些用 Go、Rust 等语言编写的、需要分发跨平台 CLI 工具的项目,实现发布流程的自动化。
这个脚本库的名字mcpm.sh很有意思,它暗示了其多平台包管理器(Multi-Platform Package Manager)的集成能力。虽然它本身不直接管理包,但它能帮你把编译好的产物,自动化地推送到各个包管理器对应的仓库或发布渠道。你可以把它理解为一个“胶水脚本”集合,它把goreleaser、git、gh(GitHub CLI)等工具串联起来,形成一条完整的自动化流水线。
它适合谁?如果你是个人开发者或小团队,正在维护一个需要分发的命令行工具,并且厌倦了每次发布都要执行一串命令、检查一堆文件,那么mcpm.sh提供的思路和脚本,能为你节省大量时间,并显著提升发布流程的可靠性和一致性。接下来,我将带你深入拆解这个项目的设计思路、核心实现,并分享如何将其适配到你自己的项目中。
2. 核心设计思路与架构解析
2.1 从手动到自动:发布流程的抽象与建模
在深入代码之前,我们先理解一个理想的自动化发布流程应该包含哪些环节。mcpm.sh的设计正是基于对这个流程的抽象:
- 前置检查:确保工作目录干净、工具链(如 Go, goreleaser, gh)已安装、拥有足够的权限(如 GitHub token)。
- 版本管理:确定本次发布的版本号(遵循 SemVer),并更新项目中的相关文件(如
version.go、Cargo.toml、package.json)。 - 构建与打包:调用构建工具(如
goreleaser),为所有目标平台(linux/amd64, darwin/arm64 等)编译二进制文件,并打包成压缩包(.tar.gz, .zip)。 - 产物校验:为所有构建产物生成 SHA256 或 SHA512 校验和文件,供用户下载后验证完整性。
- 代码快照与标签:将当前的代码状态提交为一个发布提交(如
chore: release v1.2.3),并打上对应的 Git Tag(v1.2.3)。 - 发布到 GitHub:将构建产物、校验和文件以及变更日志(CHANGELOG)上传到 GitHub Releases,并发布。
- 同步到包管理器:根据项目配置,将新版本信息推送到其他包管理器仓库。例如:
- Homebrew:更新 Formula 文件中的 URL 和 SHA256 校验和,提交到 Homebrew tap 仓库。
- AUR (Arch Linux):更新 PKGBUILD 文件,提交到 AUR。
- Scoop (Windows):更新 Manifest 文件,提交到 Scoop bucket 仓库。
- 后置清理与通知:可选地清理临时构建文件,或发送通知到 Slack/Discord 等协作工具。
mcpm.sh并没有用一个巨大的脚本来完成所有事情,而是采用了模块化的设计。它将上述流程分解为多个独立的、职责单一的 Shell 脚本(或函数),然后由一个主脚本(比如release.sh)来按顺序调用它们。这种设计的好处非常明显:可维护性和可定制性极强。你可以轻松替换其中任何一个环节,比如把goreleaser换成cargo build,或者为你的项目添加一个发布到 PyPI 的步骤。
2.2 工具链选型与依赖分析
这个脚本集合强依赖几个外部工具,它的价值在于“编排”而非“创造”。理解这些依赖是使用和定制它的前提:
- Git:基础中的基础,用于代码版本管理和打 Tag。
- GitHub CLI (
gh):这是与 GitHub 交互的核心。gh命令使得从命令行创建 Releases、上传资产变得极其简单和安全(通过 PAT 认证)。mcpm.sh中大量使用了gh release create和gh release upload等命令。 - GoReleaser (
goreleaser):对于 Go 项目而言,这是构建跨平台二进制文件的“瑞士军刀”。它不仅能处理编译,还能自动生成校验和、归档文件,并支持与 Homebrew、Scoop 等集成。mcpm.sh通常将goreleaser作为构建引擎来调用。 - curl / jq:用于与其他 REST API(如 Homebrew tap 仓库的 API)进行交互,
jq用于解析 JSON 响应。 - sed / awk:Shell 脚本的文本处理利器,用于在配置文件中替换版本号、校验和等字符串。
注意:虽然
mcpm.sh的示例可能围绕 Go 项目,但其架构是语言无关的。你可以将goreleaser替换为任何其他构建系统(如make、cargo、npm run build),只要它能产出预期的产物文件即可。脚本的核心逻辑在于“流程控制”和“GitHub集成”。
2.3 环境配置与安全考量
自动化发布涉及敏感操作(推送代码、创建 Releases),因此安全配置至关重要。mcpm.sh通常会假设以下环境变量已设置:
GITHUB_TOKEN:一个具有repo权限(或更细粒度权限)的 Personal Access Token。这是gh命令行工具和脚本与你的 GitHub 仓库交互的凭证。绝对不要将此 Token 硬编码在脚本中!应通过 GitHub Actions 的 Secrets、本地环境变量或密码管理器来注入。HOMEBREW_GITHUB_API_TOKEN(可选):如果你要自动提交到 Homebrew tap,可能需要一个对 tap 仓库有写入权限的 Token。
脚本的开头应该包含严格的错误检查:
#!/usr/bin/env bash set -euo pipefail # 启用严格模式:错误退出、未定义变量报错、管道错误检测 # 检查必要命令是否存在 for cmd in git gh goreleaser; do if ! command -v $cmd &> /dev/null; then echo "错误: 未找到命令 '$cmd',请先安装。" exit 1 fi done # 检查 GITHUB_TOKEN if [[ -z "${GITHUB_TOKEN:-}" ]]; then echo "错误: 环境变量 GITHUB_TOKEN 未设置。" exit 1 fi这种“失败早,失败快”的策略,能避免在脚本执行到一半时因环境问题而卡住,造成混乱。
3. 核心脚本模块拆解与实操要点
让我们以一个典型的release.sh主脚本为例,拆解其内部可能包含的模块。请注意,以下代码是基于mcpm.sh思想的重构和解释,并非其原始代码的直接拷贝。
3.1 版本号管理与一致性
版本号是发布的基石,必须在所有地方保持一致。一个常见的做法是在项目根目录维护一个VERSION文件,或者通过git describe动态获取。脚本需要处理版本号的读取、验证和注入。
### 3.1.1 读取与验证版本号 read_version() { # 方法1: 从 VERSION 文件读取 if [[ -f VERSION ]]; then VERSION=$(cat VERSION | tr -d '[:space:]') # 方法2: 从命令行参数读取 elif [[ $# -gt 0 ]]; then VERSION=$1 else echo "用法: $0 <version> 或确保 VERSION 文件存在" exit 1 fi # 验证版本号格式 (简易 SemVer 检查) if [[ ! $VERSION =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?(\+[a-zA-Z0-9\.]+)?$ ]]; then echo "错误: 版本号 '$VERSION' 不符合 SemVer 格式。" exit 1 fi # 确保有 'v' 前缀,这是 Git Tag 的常见格式 if [[ ! $VERSION =~ ^v ]]; then VERSION="v$VERSION" fi echo "当前发布版本: $VERSION" }实操要点:
- 单一事实来源:确保整个发布流程只从一个地方(如
VERSION文件)读取版本号,避免在多个文件(go.mod,Cargo.toml,package.json)中手动修改导致不一致。 - 预发布版本处理:如果你的版本号包含
-beta.1或-rc.2这样的预发布标识,需要确保你的构建工具和包管理器支持。goreleaser对此有良好支持,但 Homebrew 的稳定版 Formula 通常只接受正式版。
3.2 构建与打包:以 GoReleaser 为核心
这是产生最终分发产物的阶段。goreleaser通过一个.goreleaser.yaml配置文件来定义所有构建细节。
### 3.2.1 执行构建 run_build() { echo "开始构建多平台二进制文件..." # 使用 --clean 确保每次构建从干净的环境开始 # 使用 --snapshot 可以在不打 Tag 的情况下测试构建流程 if [[ "${DRY_RUN:-false}" == "true" ]]; then echo "[干跑模式] 将执行: goreleaser build --clean" goreleaser build --clean --snapshot else goreleaser build --clean fi # 构建产物通常位于 `dist/` 目录下 }.goreleaser.yaml关键配置解析:
# .goreleaser.yaml 示例片段 builds: - env: - CGO_ENABLED=0 # 静态链接,避免运行时依赖问题 goos: - linux - darwin - windows goarch: - amd64 - arm64 ignore: # 忽略一些不常见的组合 - goos: darwin goarch: 386 archives: - format: tar.gz name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" checksum: name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"注意事项:
- 静态编译:对于 Go 项目,设置
CGO_ENABLED=0进行静态编译,可以最大程度避免目标系统缺少动态库的问题,真正做到“开箱即用”。这是分发 CLI 工具的最佳实践。 - 产物命名模板:清晰的命名模板(如
myapp_v1.2.3_linux_amd64.tar.gz)能让用户一眼看出文件的平台和架构,也便于后续脚本处理。 dist/目录:goreleaser默认将产出放在dist/目录。在脚本中,应将该目录视为只读的构建产物区,任何后续步骤(如计算校验和)都基于此目录的文件。
3.3 创建 GitHub Release 并上传资产
这是将打包好的软件交付给用户的核心步骤。GitHub Releases 提供了一个稳定的文件托管和版本说明页面。
### 3.3.1 创建 Release 与上传 create_github_release() { local version=$1 local release_notes="CHANGELOG.md" # 假设变更日志在此文件 echo "创建 GitHub Release: $version" # 1. 创建 Release (草案状态,便于最后检查) gh release create "$version" \ --title "$version" \ --notes-file "$release_notes" \ --draft \ --target "$(git rev-parse --abbrev-ref HEAD)" # 2. 上传所有构建产物 echo "上传构建产物..." for file in dist/*.tar.gz dist/*.zip dist/*_checksums.txt; do if [[ -f "$file" ]]; then gh release upload "$version" "$file" --clobber fi done # 3. 发布前再次确认(如果是自动化流程,可跳过) if [[ -z "${SKIP_CONFIRM:-}" ]]; then read -p "检查 Draft Release 无误后,按回车发布,或 Ctrl+C 取消..." gh release edit "$version" --draft=false echo "Release $version 已正式发布!" else gh release edit "$version" --draft=false fi }实操心得:
- 使用
--draft参数:先创建为草稿,上传完所有资产并检查无误后,再改为发布状态。这给了你一个宝贵的“缓冲检查期”,避免有问题的 Release 直接公开。 --clobber参数:如果同一文件重复上传,--clobber会覆盖之前的,这在调试时很有用。- 变更日志自动化:理想情况下,
CHANGELOG.md应该通过git log或类似git-chglog的工具自动生成,确保每次发布的内容都准确反映代码变更。手动维护变更日志在长期项目中极易出错。
3.4 包管理器集成:以 Homebrew 为例
对于 macOS 用户,通过 Homebrew 安装是首选方式。自动化更新 Homebrew Formula 是提升用户体验的关键一步。
### 3.4.1 更新 Homebrew Formula update_homebrew_formula() { local version=$1 local tap_repo="user/homebrew-tap" # 你的 Homebrew tap 仓库 local formula="myapp.rb" # Formula 文件名 local archive_url="https://github.com/yourname/yourrepo/releases/download/$version/yourproject_${version}_darwin_all.tar.gz" local checksum=$(shasum -a 256 "dist/yourproject_${version}_darwin_all.tar.gz" | awk '{print $1}') echo "更新 Homebrew Formula 在仓库: $tap_repo" # 克隆 tap 仓库到临时目录 local tmp_dir=$(mktemp -d) git clone "https://github.com/$tap_repo.git" "$tmp_dir" pushd "$tmp_dir" > /dev/null # 更新 Formula 文件中的 url 和 sha256 字段 sed -i.bak \ -e "s|url \".*\"|url \"$archive_url\"|" \ -e "s|sha256 \".*\"|sha256 \"$checksum\"|" \ "Formula/$formula" # 提交并推送更改 git add "Formula/$formula" git commit -m "Update $formula to $version" git push origin main popd > /dev/null rm -rf "$tmp_dir" echo "Homebrew Formula 更新完成。用户可通过 'brew update && brew upgrade myapp' 更新。" }关键细节与避坑指南:
- SHA256 校验和:Homebrew 严格要求提供源码或二进制包的 SHA256 校验和。必须使用
shasum -a 256计算,且确保计算的文件与url字段指向的文件完全一致。任何不一致都会导致brew install失败。 - Tap 仓库权限:运行此脚本的机器(或 GitHub Actions Runner)必须拥有对你个人或组织的 Homebrew tap 仓库的写入权限(通常通过 SSH 密钥或 Token 实现)。
- Formula 结构:如果你的应用是纯二进制分发,Formula 会相对简单,主要包含
url、sha256和install阶段(将二进制文件复制到bin目录)。如果涉及复杂构建,则需要重写install方法。 - 测试安装:在推送 Formula 更新后,最好能在另一台干净的机器上(或 Docker 容器中)运行
brew install user/tap/myapp进行测试,确保安装流程顺畅。
4. 完整自动化流水线搭建实战
理解了各个模块后,我们将它们串联起来,形成一个健壮的、可重用的发布脚本。同时,我们将探讨如何将其集成到 CI/CD(如 GitHub Actions)中,实现“一键发布”或“标签触发发布”。
4.1 主脚本release.sh的完整编排
#!/usr/bin/env bash # 项目发布自动化脚本 set -euo pipefail # 引入配置(如果有) source .release-config 2>/dev/null || true # 定义颜色输出(可选) RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' # No Color error() { echo -e "${RED}[错误]${NC} $*" >&2; exit 1; } info() { echo -e "${GREEN}[信息]${NC} $*"; } # --- 模块函数定义 (此处省略,见上文) --- # read_version() # run_build() # create_github_release() # update_homebrew_formula() # 可能还有 update_aur()、update_scoop() 等 # --- 主流程 --- main() { # 解析命令行参数 local dry_run=false local skip_confirm=false while [[ $# -gt 0 ]]; do case $1 in --dry-run) dry_run=true ;; --skip-confirm) skip_confirm=true ;; *) VERSION_ARG="$1" ;; esac shift done export DRY_RUN="$dry_run" export SKIP_CONFIRM="$skip_confirm" # 步骤 1: 版本准备 read_version "${VERSION_ARG:-}" info "使用版本号: $VERSION" # 步骤 2: 运行测试(可选但强烈推荐) info "运行单元测试..." go test ./... || error "单元测试失败,中止发布。" # 步骤 3: 构建 run_build # 步骤 4: 创建 GitHub Release if [[ "$dry_run" != "true" ]]; then create_github_release "$VERSION" else info "[干跑模式] 跳过创建 GitHub Release。" fi # 步骤 5: 更新包管理器 if [[ "$dry_run" != "true" ]]; then # 检查是否配置了 Homebrew tap if [[ -n "${HOMEBREW_TAP_REPO:-}" ]]; then update_homebrew_formula "$VERSION" else info "未配置 HOMEBREW_TAP_REPO,跳过 Homebrew 更新。" fi # 类似地,可以调用 update_aur, update_scoop 等 fi info "发布流程执行完毕!" if [[ "$dry_run" == "true" ]]; then info "此为干跑模式,未执行任何实际写入操作。" fi } # 执行主函数,并传递所有参数 main "$@"这个主脚本的特点:
- 参数化:支持
--dry-run(干跑)模式,用于测试整个流程而不做任何实际修改(如推送代码、创建 Release)。这是自动化脚本的黄金法则,务必提供。 - 模块化调用:每个核心步骤都是独立的函数,逻辑清晰,易于调试和替换。
- 错误处理:通过
set -euo pipefail和自定义的error函数,确保任何一步失败都会停止整个流程,避免产生不一致的中间状态。 - 配置外部化:通过
source .release-config读取配置(如仓库地址、Token 别名),避免将敏感信息硬编码在脚本中。
4.2 集成到 GitHub Actions 实现 CI/CD
本地脚本已经很强大了,但真正的“自动化”是将它交给 CI/CD 服务器。GitHub Actions 是托管在 GitHub 上的天然选择。
# .github/workflows/release.yml name: Release on: push: tags: - 'v*' # 当推送 v 开头的 tag 时触发 jobs: release: runs-on: ubuntu-latest permissions: contents: write # 需要写入权限来创建 Release steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 # 获取所有历史,用于生成 changelog - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: 'go.mod' - name: Install dependencies run: | # 安装 goreleaser go install github.com/goreleaser/goreleaser@latest # 安装 GitHub CLI (gh) type -p gh >/dev/null || sudo apt-get install gh -y - name: Run tests run: go test ./... - name: Release with custom script env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GitHub 自动提供 HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_TOKEN }} # 自定义 Token,用于推送 Homebrew tap run: | # 给 gh 命令行工具授权 echo "$GITHUB_TOKEN" | gh auth login --with-token # 执行我们的自动化发布脚本,传递 tag 名作为版本号 # 这里 TAG_NAME 是 GitHub Actions 提供的环境变量,如 v1.2.3 bash ./scripts/release.sh --skip-confirm "${TAG_NAME}"GitHub Actions 集成的精髓:
- 标签触发:
on: push: tags: - 'v*'是最常见的发布触发方式。你只需要在本地git tag v1.2.3 && git push --tags,剩下的全部交给 Actions。 - 权限管理:注意
permissions: contents: write,这是创建 Release 所必需的。对于更新 Homebrew tap 等需要推送到其他仓库的操作,需要配置额外的 Token(如HOMEBREW_TOKEN)并存储在仓库的 Secrets 中。 - 安全注入 Token:
GITHUB_TOKEN是 Actions 运行时自动生成的,权限仅限于当前仓库。对于跨仓库操作(如推送到 Homebrew tap),必须使用你自己创建的 Personal Access Token,并将其添加到仓库 Secrets。 --skip-confirm参数:在 CI 环境中,没有交互式终端,所以我们需要传递--skip-confirm来让脚本自动发布 Release,而不是停在确认步骤。
5. 常见问题排查与进阶技巧
即使脚本再完善,在实际操作中也会遇到各种问题。这里记录了一些典型场景和解决思路。
5.1 构建阶段问题
问题1:goreleaser构建失败,提示go: cannot find main module。
- 原因:通常是因为在错误的目录下运行,或者
go.mod文件不存在。 - 解决:确保在项目根目录(包含
go.mod的目录)下运行脚本。在脚本中使用pushd/popd或cd明确切换目录。
问题2:为 macOS arm64 构建的二进制文件在旧版 Intel Mac 上报错。
- 原因:Go 默认构建的 Darwin ARM64 二进制文件可能使用了新的指令集或链接库,与 Intel 不兼容。如果你需要通用二进制(Universal Binary),情况更复杂。
- 解决:对于纯 Go 项目,静态编译(
CGO_ENABLED=0)的二进制文件通常跨 Intel/Apple Silicon 兼容。如果问题依旧,检查是否使用了-buildmode或其他特殊编译标志。对于通用二进制,goreleaser可以通过goos: darwin和goarch: amd64,arm64分别构建,然后使用lip工具合并,但这需要额外的配置和脚本。
5.2 GitHub Release 阶段问题
问题3:gh release upload失败,提示HTTP 422 Validation Failed。
- 原因:可能尝试上传同名文件,且未使用
--clobber;或者 Release 的草稿已存在但状态不对;更常见的是,上传的资产大小超过 GitHub 的限制(单个文件通常 2GB,但大文件建议用gh release upload的--clobber并检查网络)。 - 解决:
- 确保使用
--clobber。 - 检查是否已存在同名的草稿 Release,可以先删除旧的:
gh release delete <tag> --cleanup-tag -y(谨慎操作!)。 - 对于大文件,考虑使用分块上传或 GitHub 的发行版资产 API 的高级特性。
- 确保使用
问题4:自动生成的变更日志(CHANGELOG)内容混乱,包含了合并提交(Merge Commit)或琐碎的提交信息。
- 原因:默认的
git log会输出所有提交。 - 解决:使用专业的变更日志生成工具,如:
- git-chglog:通过
.chglog目录下的配置文件,可以精细控制提交信息的格式、分类和过滤规则,能很好地忽略合并提交和chore:类型的提交。 - 语义化发布(Semantic Release):这是一套更完整的理念和工具集,强制要求提交信息符合 Conventional Commits 规范,并自动决定版本号、生成变更日志和发布。可以与现有脚本结合,用其生成变更日志,再用我们的脚本处理构建和分发。
- git-chglog:通过
5.3 包管理器集成问题
问题5:Homebrew 安装失败,提示SHA256 mismatch。
- 原因:Formula 中记录的
sha256值与实际下载的压缩包计算出的值不一致。这是最常见的问题。 - 排查步骤:
- 手动下载 Formula 中
url字段指向的文件。 - 在本地计算其 SHA256:
shasum -a 256 下载的文件.tar.gz。 - 与 Formula 文件中的
sha256行对比。 - 如果不一致,说明自动化脚本中计算校验和的文件,与最终上传到 GitHub Release 的文件不是同一个。检查构建流程是否确定,是否有后处理步骤修改了文件。
- 手动下载 Formula 中
- 解决:确保在脚本中,计算校验和的对象就是最终要上传的那个文件,并且在上传后、更新 Formula 前,文件没有再被改动。
问题6:自动提交到 Homebrew tap 仓库时,推送被拒绝(权限不足)。
- 原因:使用的 Token 没有对目标 tap 仓库的写入权限,或者使用的是 HTTPS 地址但未正确配置认证。
- 解决:
- 确认 Token 具有
repo或public_repo(对于公开仓库)权限。 - 在脚本中,可以考虑使用 SSH 方式来克隆和推送,这需要将 SSH 私钥配置到 CI 的 Secrets 中,并设置好
git remote地址(git@github.com:user/repo.git)。
- 确认 Token 具有
5.4 进阶技巧与优化建议
- 增量构建与缓存:在 GitHub Actions 中,可以使用
actions/cache来缓存 Go 模块和构建缓存,显著加快构建速度。缓存~/go/pkg/mod和~/.cache/goreleaser目录是不错的选择。 - 多架构 Docker 镜像:如果你的项目也提供 Docker 镜像,可以在发布流水线中加入构建和推送多平台(linux/amd64, linux/arm64)Docker 镜像的步骤。可以使用
docker buildx来实现。 - 版本号策略自动化:与其手动修改
VERSION文件,不如考虑使用git tag作为唯一版本来源。脚本可以自动检测最新的 Tag,或者根据 Conventional Commits 自动推导下一个版本号(这属于 Semantic Release 的范畴)。 - 回滚机制:自动化虽好,但也要有备无患。在脚本中,可以考虑在关键步骤(如打 Tag、创建 Release)之前创建“检查点”,一旦后续步骤失败,能自动或手动执行回滚操作(如删除刚创建的 Tag 和 Draft Release)。
- 通知与审计:发布完成后,可以添加一个步骤,将发布结果(成功或失败,包含版本号和链接)发送到团队的 Slack 或 Discord 频道。同时,在 GitHub Releases 的正文中,可以自动关联本次发布对应的 Git Commit 范围,方便追溯。
通过以上拆解,我们可以看到,pathintegral-institute/mcpm.sh所代表的不仅仅是一组脚本,更是一种提升开源项目维护效率和专业度的工程化思维。它将重复、易错的手动操作固化为可靠、可重复的自动化流程。你可以直接借鉴它的架构,也可以根据自己项目的具体需求(比如是 Rust、Python 还是 Node.js 项目)进行裁剪和改造。核心在于理解每个环节的目的和实现方式,然后组装成适合你自己的“发布流水线”。当你下次再执行./scripts/release.sh v2.0.0并看着一切自动完成时,你会感谢自己当初投入时间搭建了这套系统。
