4. 使用Generic Packages管理任意制品
GitLab 的 Generic Packages 是通用包仓库能力,它不要求制品符合 npm、Maven、PyPI、NuGet 等特定生态的目录结构和元数据规范,而是把任意二进制文件或归档文件作为“包”来管理。对于编译产物、安装包、固件镜像、模型文件、离线文档、测试报告归档等无法归入标准包管理器的制品,Generic Packages 提供了一条统一的上传、下载、删除与版本化通道。它的核心价值在于:制品不再散落在流水线临时目录或对象存储的随机路径中,而是拥有仓库、包名、版本、文件名四层可寻址标识,并能通过 API 与 CI/CD 变量实现自动化流转。
理解 Generic Packages 的关键是掌握它的寻址模型。一个通用包的完整坐标由四部分组成:项目路径(或项目 ID)、包名(package_name)、包版本(package_version)和文件名(file_name)。上传时,GitLab 会按“包名 + 版本”建立逻辑包,同一版本下可以包含多个文件;下载时既可以按文件名精确获取,也可以列出某版本下的全部文件。这种模型与容器镜像的“仓库 + 标签 + 摘要”思路相似,但更轻量,适合直接承载构建产物。
上传:用 CI/CD 作业自动发布构建产物
Generic Packages 的上传通过 HTTP PUT 完成,端点格式为 /projects/:id/packages/generic/:package_name/:package_version/:file_name。在流水线中,最稳妥的做法是使用 CI_JOB_TOKEN 作为认证凭据,它由 GitLab 自动注入,无需在项目里保存长期令牌。下面是一个典型的 .gitlab-ci.yml 片段,展示如何把构建产物打包并发布为通用包:
publish:generic:
stage: publish
image: alpine:3.19
script:
- apk add --no-cache curl tar
- tar -czf app-linux-amd64.tar.gz -C build/ .
- |
curl --fail --silent --show-error \
--header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--upload-file app-linux-amd64.tar.gz \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/app-release/${PACKAGE_VERSION}/app-linux-amd64.tar.gz"
rules:
- if: $CI_COMMIT_TAG
variables:
PACKAGE_VERSION: $CI_COMMIT_TAG
- if: $CI_COMMIT_BRANCH == "main"
variables:
PACKAGE_VERSION: "snapshot-${CI_COMMIT_SHORT_SHA}"
这段配置体现了两个重要的工程习惯。第一,版本号来自流水线变量:当提交被打上标签时,PACKAGE_VERSION 取标签名(如 v1.4.0),发布正式版本;当提交进入 main 分支时,版本号取 snapshot- 加短提交 SHA,发布快照版本。这样同一套作业既能产出可追溯的正式包,也能产出用于联调的临时包,且不会互相覆盖。第二,上传前先打包成 tar.gz,把多个构建文件收敛为单一制品,减少包内文件数量,也便于下载后一次性解压。
需要特别注意的是,Generic Packages 的上传是“同名同版本同文件覆盖”还是“拒绝重复”,取决于 GitLab 版本与配置。在较新的版本中,重复上传同一路径会返回 403 或 409,因此版本号必须唯一。如果确实需要重新发布同一版本,应先删除旧包再上传,或改用带构建序号的版本号,例如 v1.4.0-build.27。
下载与校验:让消费端拿到正确版本
下载端点与上传端点对称,使用 GET 方法即可。在部署作业中,通常先下载再校验摘要,最后才部署。下面的示例展示了从通用包仓库拉取指定版本并校验 SHA256 的过程:
deploy:generic:
stage: deploy
image: alpine:3.19
script:
- apk add --no-cache curl sha256sum
- |
curl --fail --silent --show-error \
--header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--output app.tar.gz \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/app-release/${PACKAGE_VERSION}/app-linux-amd64.tar.gz"
- echo "${EXPECTED_SHA256} app.tar.gz" | sha256sum -c -
- tar -xzf app.tar.gz -C /opt/app
这里的 EXPECTED_SHA256 可以来自上游作业生成的校验文件,也可以作为流水线变量在触发部署时传入。校验通过后再解压部署,能有效避免网络中断或存储异常导致的半包问题。如果校验失败,作业应立即失败,而不是继续部署一个损坏的制品。
删除:清理过期版本与释放存储
删除通用包有两种粒度:删除某个包版本下的单个文件,或删除整个包。删除单个文件的端点是在下载端点基础上加 /:file_name 并使用 DELETE 方法;删除整个包则使用 /projects/:id/packages/:package_id。由于删除操作不可逆,建议在流水线中只对快照版本执行自动清理,正式版本保留人工确认。例如可以增加一个定时清理作业,删除 30 天前的 snapshot 包:
cleanup:snapshots:
stage: cleanup
image: alpine:3.19
script:
- apk add --no-cache curl jq
- |
curl --silent --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages?package_type=generic&per_page=100" \
| jq -r '.[] | select(.version | startswith("snapshot-")) | .id' \
| while read id; do
curl --request DELETE --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/${id}"
done
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
这段脚本先用 packages API 列出通用包,再用 jq 过滤出版本号以 snapshot- 开头的包,逐个删除。它把“清理”也纳入了流水线,而不是依赖人工在界面上点击,适合制品量较大的项目。
版本化流转的实践要点
把上传、下载、删除串起来看,Generic Packages 的版本化流转依赖三个约定。第一,版本号必须唯一且可追溯,推荐正式版本用语义化标签,快照版本用分支加短 SHA。第二,认证统一使用 CI_JOB_TOKEN,避免在脚本中硬编码个人访问令牌;如果跨项目下载,需要在目标项目配置允许访问的作业令牌范围。第三,下载后必须校验摘要,把校验作为部署的前置条件。做到这三点,通用包仓库就能成为构建产物在流水线之间稳定传递的中间层,而不是一个只用来临时存放文件的网盘。
CI_COMMIT_TAG 取值,这样镜像标签与通用包版本可以互相对应,排查问题时更容易定位到同一次构建。