banner
约 4,200 字
14 分钟

从源码到 Release:使用 GitHub Actions 自动构建 RPM 与 DEB

摘要

一套与编程语言和具体打包工具无关的 Linux 软件发布方法:区分编译产物、系统软件包、Actions Artifact 与 Release Asset,并处理版本、架构、兼容性、验证和供应链安全。

从源码到 Release:使用 GitHub Actions 自动构建 RPM 与 DEB

为 Linux 项目提供 RPM 和 DEB 下载,看起来只是“在 CI 里多执行两条打包命令”,实际上涉及版本管理、目录布局、依赖声明、架构命名、目标系统兼容性、安装验证和发布权限。GitHub Actions 可以把这些步骤串成可重复执行的流水线,但它不会自动保证软件包符合所有发行版的规范,也不会让同一个 RPM 或 DEB 天然兼容整个 Linux 生态。

本文不绑定某种编程语言,也不要求使用特定打包工具。示例假设仓库已经提供可从命令行调用的构建脚本,重点讨论如何组织一条通用而可靠的自动发布流程。

先区分四种容易混淆的产物

一条发布流水线通常会接触四种不同对象。

编译产物是编译器或构建系统生成的程序,例如可执行文件、动态库、静态资源和配置模板。它们可能还不能直接安装。

系统软件包是 RPM 或 DEB 文件。除程序文件外,它还包含软件名称、版本、架构、依赖、安装路径、维护脚本和卸载信息。

Actions Artifact是某次工作流运行保存的文件。它适合在任务之间传递构建结果,或者让维护者下载测试。Artifact 有保留期限,删除工作流运行时,对应 Artifact 也会被删除。

Release Asset是附加在 GitHub Release 上的正式下载文件。Release 基于 Git 标签,适合向最终用户分发软件包、压缩包、校验和与发布说明。

因此,合理的流程不是简单地“编译后上传”,而是:

纯文本
源码
  → 编译或整理程序文件
  → 构建 RPM 与 DEB
  → 检查软件包元数据
  → 在干净环境中安装和冒烟测试
  → 上传 Actions Artifact
  → 标签构建成功后创建 GitHub Release
  → 上传正式软件包与校验和

RPM 和 DEB 不是两种压缩格式

DEB 主要用于 Debian、Ubuntu 及其衍生发行版;RPM 主要用于 Fedora、RHEL、Rocky Linux、AlmaLinux、openSUSE 等发行版。但“使用相同包格式”不等于“能够跨发行版直接运行”。

一个软件包能否使用,还取决于目标系统的 ABI、glibc、libstdc++、OpenSSL、图形栈、音频栈以及其他动态库版本。不同发行版对依赖名、默认路径和打包政策也可能有差异。同一个 RPM 不一定适用于所有 RPM 系发行版,同一个 DEB 也不一定适用于所有 Debian 系发行版。

所以,自动化打包解决的是重复构建和发布问题;发行版兼容性仍然需要明确的支持范围与实际测试。

把打包逻辑放在仓库脚本里

不要把所有业务逻辑都堆进工作流 YAML。GitHub Actions 更适合负责准备环境、调用脚本、传递产物和发布 Release。具体的打包过程应尽量保存在仓库中,例如:

纯文本
scripts/
├── build.sh
├── package-deb.sh
├── package-rpm.sh
└── verify-packages.sh

这样可以在本地复现 CI 行为,也便于以后迁移到其他 CI 平台。工作流只需要约定统一接口:

bash
./scripts/package-deb.sh "$PROJECT_VERSION" "$OUTPUT_DIR"
./scripts/package-rpm.sh "$PROJECT_VERSION" "$OUTPUT_DIR"
./scripts/verify-packages.sh "$OUTPUT_DIR"

脚本应在失败时返回非零状态,并避免把缺少输出文件当作成功。Shell 脚本通常可以从以下设置开始:

bash
set -euo pipefail

不过它不是完整的错误处理方案。脚本仍应明确检查参数、目录、命令和最终文件是否存在。

使用两类触发方式

手动触发适合测试打包流程,标签触发适合正式发布:

YAML
on:
  workflow_dispatch:
  push:
    tags:
      - "v*"

v1.2.3 只是一种常见约定,不是 GitHub 的强制规则。项目可以使用其他标签格式,但必须明确标签如何映射到项目版本、Debian 版本和 RPM 版本。

版本号经常同时存在于源码、构建配置、Git 标签、RPM 元数据、DEB 元数据、文件名和 Release 标题中。正式发布前应检查它们是否一致。还要注意,Debian 与 RPM 的版本比较规则并不完全相同,因此复杂版本字符串最好经过包格式专用的规范化,而不是机械地复制同一个字符串。

一份通用的工作流骨架

下面的工作流展示一种常见结构:DEB 和 RPM 分开构建,各自上传 Artifact;只有标签触发且两个构建任务都成功后,才创建 Release。

YAML
name: Package Linux

on:
  workflow_dispatch:
  push:
    tags:
      - "v*"

permissions:
  contents: read

jobs:
  deb:
    name: Build DEB
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Resolve version
        shell: bash
        run: |
          if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
            version="${GITHUB_REF_NAME#v}"
          else
            version="0.0.0.git.${GITHUB_SHA:0:7}"
          fi
          printf 'PROJECT_VERSION=%s\n' "$version" >> "$GITHUB_ENV"

      - name: Install build dependencies
        run: ./scripts/install-deb-build-deps.sh

      - name: Build package
        run: ./scripts/package-deb.sh "$PROJECT_VERSION" "$RUNNER_TEMP/deb-out"

      - name: Verify package
        run: ./scripts/verify-deb.sh "$RUNNER_TEMP/deb-out"

      - name: Upload DEB artifact
        uses: actions/upload-artifact@v4
        with:
          name: packages-deb
          path: ${{ runner.temp }}/deb-out/*
          if-no-files-found: error
          retention-days: 7

  rpm:
    name: Build RPM
    runs-on: ubuntu-24.04
    container:
      image: fedora:42
    steps:
      - name: Install workflow tools
        run: dnf install -y git

      - name: Checkout
        uses: actions/checkout@v6

      - name: Resolve version
        shell: bash
        run: |
          if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
            version="${GITHUB_REF_NAME#v}"
          else
            version="0.0.0.git.${GITHUB_SHA:0:7}"
          fi
          printf 'PROJECT_VERSION=%s\n' "$version" >> "$GITHUB_ENV"

      - name: Install build dependencies
        run: ./scripts/install-rpm-build-deps.sh

      - name: Build package
        run: ./scripts/package-rpm.sh "$PROJECT_VERSION" "$RUNNER_TEMP/rpm-out"

      - name: Verify package
        run: ./scripts/verify-rpm.sh "$RUNNER_TEMP/rpm-out"

      - name: Upload RPM artifact
        uses: actions/upload-artifact@v4
        with:
          name: packages-rpm
          path: ${{ runner.temp }}/rpm-out/*
          if-no-files-found: error
          retention-days: 7

  release:
    name: Publish Release
    if: github.ref_type == 'tag'
    needs: [deb, rpm]
    runs-on: ubuntu-24.04
    permissions:
      contents: write
    steps:
      - name: Download packages
        uses: actions/download-artifact@v5
        with:
          pattern: packages-*
          path: dist
          merge-multiple: true

      - name: Check release files
        shell: bash
        run: |
          set -euo pipefail
          find dist -maxdepth 1 -type f -print
          compgen -G 'dist/*.deb' >/dev/null
          compgen -G 'dist/*.rpm' >/dev/null

      - name: Generate checksums
        shell: bash
        run: |
          cd dist
          sha256sum ./*.deb ./*.rpm > SHA256SUMS

      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: |
          gh release create "$GITHUB_REF_NAME" \
            dist/* \
            --verify-tag \
            --generate-notes \
            --title "$GITHUB_REF_NAME"

这份配置是结构模板,不是复制后即可适用于所有项目的成品。install-*-build-deps.shpackage-*.sh 和验证脚本必须由项目按照实际构建系统实现。容器镜像和 Runner 版本也应根据项目支持周期固定并定期更新。

在受控发布流程中,固定环境版本通常比使用 latest 更容易复现。不过固定版本并不意味着永久不更新;它意味着升级由维护者主动完成,并经过测试。

DEB 应怎样构建

对于具有标准 Debian 打包目录的源码,dpkg-buildpackage 是核心构建工具。一个常见命令是:

bash
dpkg-buildpackage -us -uc -b

其中 -us -uc 表示本次构建不签名源码和 .changes 文件,-b 表示构建二进制包。正式进入 Debian 仓库的流程比“为 GitHub Release 生成一个 DEB”严格得多,需要遵循 Debian Policy,并维护完整的源包信息。

也可以使用 debuildsbuildpbuilder、语言生态工具或通用打包工具。选择取决于目标:内部工具和第三方下载包可以采用较轻量的方案;准备进入发行版官方仓库的包,则应使用对应发行版认可的标准流程。

无论采用哪种工具,都应准确填写包名、版本、架构、维护者、描述、运行依赖、冲突关系和安装文件列表。

RPM 应怎样构建

标准 RPM 通常由 Spec 文件描述,并使用 rpmbuild 构建。例如:

bash
rpmbuild -ba packaging/example.spec

RPM 同样包含二进制包和源包两类。Spec 文件负责描述源码、构建步骤、安装阶段、文件清单、依赖、脚本和变更记录。

语言生态工具或通用打包器可以简化生成 RPM 的过程,但不应因此跳过元数据检查和安装测试。如果目标是进入 Fedora、EPEL 或其他发行版仓库,还需要遵守相应项目的打包规范,而不是仅以“能生成 .rpm 文件”为完成标准。

不要忽略架构映射

同一硬件架构在不同生态中可能使用不同名称。最常见的例子是 64 位 x86:

纯文本
Debian:amd64
RPM:x86_64
常见工具或系统输出:x86_64

64 位 ARM 也经常表现为:

纯文本
Debian:arm64
RPM:aarch64

因此,不要把 uname -m、Runner 架构或矩阵变量原样写入所有软件包。应建立明确的映射函数,并让无法识别的架构直接失败:

bash
case "$machine" in
  x86_64)
    deb_arch=amd64
    rpm_arch=x86_64
    ;;
  aarch64|arm64)
    deb_arch=arm64
    rpm_arch=aarch64
    ;;
  *)
    echo "unsupported architecture: $machine" >&2
    exit 1
    ;;
esac

跨架构构建还涉及交叉编译器、目标系统库和安装测试,不能只靠改文件名完成。

软件包生成后必须验证

文件存在不代表软件包正确。至少应检查元数据和文件列表。

DEB 可以使用:

bash
dpkg-deb --info package.deb
dpkg-deb --contents package.deb

RPM 可以使用:

bash
rpm -qip package.rpm
rpm -qlp package.rpm

还可以引入 lintianrpmlint。但检查器的每一条警告不一定都应该阻止发布,项目需要制定清晰的质量门槛。真正关键的是,编译失败、软件包生成失败、核心测试失败、安装失败和 Release 上传失败不应被忽略。

比静态检查更可靠的是在干净的容器或虚拟机中执行安装测试:

纯文本
安装软件包
→ 验证文件位置和权限
→ 执行 example --version 或其他冒烟测试
→ 检查服务、桌面文件或资源
→ 卸载软件包
→ 确认卸载行为符合预期

如果程序依赖 systemd、图形桌面、内核模块或真实硬件,仅使用普通容器可能不够,需要虚拟机或专门的集成测试环境。

构建环境决定兼容性下限

在较新的构建系统中生成的软件,可能引用较新的 glibc、libstdc++ 或其他共享库符号,从而无法在较旧系统运行。把二进制文件放进 DEB 或 RPM 并不会消除这种限制。

提高兼容性的常见做法包括:在项目支持的较旧基础环境中构建;为不同发行版或版本分别构建;使用与目标系统一致的容器;准确声明运行依赖;检查动态链接关系;在支持列表中的系统上安装测试。

静态链接有时可以减少运行依赖,但并不适用于所有语言、许可证、系统库和桌面程序。它应被视为一种工程选择,而不是通用解法。

Artifact 与 Release 应各司其职

所有分支或手动运行都可以上传 Artifact,方便维护者检查打包结果;正式 Release 则应只在满足明确条件时创建,例如推送受保护的版本标签且全部构建和测试成功。

GitHub 当前使用 actions/upload-artifact@v4 上传 Artifact;该版本中的 Artifact 是不可变的,同一工作流中应使用唯一名称。下载多个构建任务的产物时,可以使用 actions/download-artifact@v5,再由发布任务集中生成校验和。

Release 创建需要写入仓库内容,因此只在发布 Job 中授予:

YAML
permissions:
  contents: write

其他构建 Job 保持 contents: read,符合最小权限原则。GITHUB_TOKEN 由 GitHub 为每个 Job 自动生成,其权限仅限当前仓库,并会在任务结束后失效。

校验和不是签名

为 Release 生成 SHA-256 校验和很有价值:

bash
sha256sum ./*.deb ./*.rpm > SHA256SUMS

它可以帮助用户确认文件没有在下载过程中损坏,也能比较文件是否发生变化。但如果攻击者能够同时替换软件包和校验文件,单独的校验和不能证明发布者身份。

要建立发布者身份与构建来源的信任,还可以考虑 Git 标签签名、RPM 或 Debian 软件包签名、GitHub Artifact Attestations、SBOM 和受保护发布环境。签名流程涉及私钥存储、轮换、撤销和信任分发,值得单独设计,不能只把长期私钥随意写入仓库 Secret 后就视为安全。

第三方 Action 也是供应链依赖

每个 uses: 都会执行外部代码。正式发布流程应优先使用维护清晰的 Action,授予最小权限,并评估是否固定到不可变的提交 SHA。固定大版本标签便于接收兼容更新,固定提交 SHA 则能减少上游标签被移动带来的风险;两者之间需要根据维护成本和威胁模型作出选择。

还应避免把不可信输入直接拼进 Shell。分支名、标签名、Issue 内容、PR 标题和工作流输入都可能包含特殊字符。应通过环境变量传递,并在脚本中引用变量,而不是直接把表达式插入命令文本。

不要用 continue-on-error 掩盖发布失败

continue-on-error: true 可以用于实验性分析或尚未纳入质量门槛的检查,但不应该用于以下步骤:

纯文本
程序编译
核心测试
软件包生成
安装验证
校验和生成
Release 创建与文件上传

所谓“宽松检查”应表示某些非核心警告暂时不阻塞流水线,而不是让错误的软件包被当作成功结果发布。

常见问题

本地能编译,Actions 中失败

通常是 CI 环境缺少开发包、编译器、系统头文件、pkg-config 元数据或外部工具。运行时库和开发包往往不是同一个软件包。

软件包可以安装,但程序不能启动

可能是运行依赖没有声明、构建环境过新、动态库名称不一致,或者程序在构建时自动关闭了某项功能。应在干净目标环境中执行真实启动测试。

同一个包不能覆盖多个发行版

包格式相同不代表依赖和 ABI 相同。可以按发行版系列分别构建,并在文件名和 Release 说明中明确支持范围。

Release 返回 403

通常需要检查发布 Job 是否具有 contents: write,仓库或组织策略是否限制了 Actions 权限,以及触发事件是否来自不可信的 Fork。

重跑标签工作流时 Release 已存在

发布脚本需要明确选择失败、覆盖 Asset、更新草稿,还是生成新的版本。不要在没有策略的情况下静默覆盖正式发布文件。启用不可变 Release 时,GitHub 建议先创建草稿、附加全部文件,再发布。

结语

GitHub Actions 能够自动化 RPM 和 DEB 的构建与发布,但可靠交付的关键不在于 YAML 有多长,而在于边界是否清楚:支持哪些发行版和架构,版本如何映射,文件安装到哪里,依赖如何声明,构建环境如何固定,软件包怎样验证,发布任务拥有什么权限,以及用户如何验证下载内容。

一条成熟的流水线应当做到可重复、可审查、可验证,并在任何核心步骤失败时停止发布。只有这样,“自动生成两个安装包”才真正升级为一套可信的软件发布流程。

参考资料

END