软件项目开发全流程 · 第 24 课 · 构建与打包:制品与版本号
1、"我这儿明明是好的"
开发说"我在本机跑通了",运维部署到服务器却报错。排查半天——本地装的依赖是 3.2 版,服务器上是 3.0 版。
问题的根源是:代码 ≠ 制品。你的源码不是要部署的东西,构建出来的"制品(Artifact)"才是。本课讲清楚:怎么把一个项目变成一个可复现、可追溯、可部署的制品。
2、构建:从源码到制品
构建(Build) 是把源码 + 依赖 + 配置,编译打包成可部署的东西:
源码 ──构建──▶ 制品
│ ├── 后端:jar / war / 二进制 / 容器镜像
│ ├── 前端:静态资源包(dist/)
│ └── 移动端:apk / ipa"一次构建,多地部署" 是铁律:
┌── 测试环境部署(用这个制品)
构建 ───┼── 预发环境部署(用同一个制品)
└── 生产环境部署(还是这个制品)为什么? 如果你在测试环境用 A 制品、生产环境重新构建一遍,那构建过程本身就可能引入差异(时间戳、随机数、依赖解析),"测过的"和"上线的"就不是同一个东西了。构建一次,处处部署,才能保证"测试通过的东西 = 上线的东西"。
3、构建的可复现性
可复现构建(Reproducible Build):相同的源码 + 相同的环境,每次构建产出完全相同的制品。
破坏可复现的常见因素:
- 依赖版本浮动(
^1.2.0可能解析成不同小版本)→ 锁定版本(package-lock.json/poetry.lock/Cargo.lock)。 - 构建时间戳打进制品 → 用固定时间戳(
SOURCE_DATE_EPOCH)。 - 构建依赖全局环境 → 容器化构建(Docker),环境也是"代码"。
- 构建产物顺序不定 → 排序后打包、确定性工具。
价值:可复现 + 锁依赖 = 你真的知道线上跑的是什么。出了事故能准确回溯、重建。
4、语义化版本(SemVer):让版本号有意义
版本号不是随便递增的数字,它是一种契约:
1 . 4 . 2
│ │ └── PATCH:向后兼容的 bug 修复
│ └──────── MINOR:向后兼容的新功能
└────────────── MAJOR:不兼容的破坏性变更"向后兼容"是什么意思? 用你库的下游代码,不用改就能升级。
1.3.0 → 1.3.1 ✅ 安全升级(只有 bug 修复)
1.3.0 → 1.4.0 ✅ 安全升级(加了功能,旧接口还在)
1.3.0 → 2.0.0 ⚠️ 谨慎!旧代码可能编译不过/行为变了预发布版本:1.4.0-beta.1、2.0.0-rc.2(rc = release candidate,发布候选)。
常见配套:
- 构建号(Build Number):CI 每次构建递增(如
#1234),用于精确追溯。 - 提交哈希:制品打上 git commit SHA,出问题一秒定位源码。
- 版本嵌入:把版本号写进制品(
app --version能打出来)。
5、依赖管理:别让"依赖地狱"找上门
App ──依赖──▶ 库 A ──依赖──▶ 库 C@1.0
└──依赖──▶ 库 B ──依赖──▶ 库 C@2.0 ← 冲突!应对原则:
| 做法 | 说明 |
|---|---|
| 锁定文件 | 提交 lock 文件,保证团队/CI/H 环境装同一套版本 |
| 依赖尽量少 | 每多一个依赖,多一份安全与维护负担 |
| 定期升级 | 别攒到"三年不升、一升全崩" |
| 扫描漏洞 | 依赖扫描进 CI(第 23 课) |
| 区分直接/传递依赖 | 传递依赖的坑更难查 |
6、制品管理:制品仓库
制品不该只在某人电脑里,要放进制品仓库(Artifact Repository):
- 通用:Nexus、Artifactory、GitHub Packages
- 容器镜像:Docker Hub、Harbor、云厂商镜像仓库
- 语言专属:npm registry、PyPI、Maven Central
制品仓库要点:
- 不可变(Immutable):已发布版本永不覆盖。要改就发新版本号。否则"1.4.2"今天和明天可能是两份东西,追溯就废了。
- 保留策略:旧版本保留期、清理规则。
- 权限与审计:谁能推、谁能拉,留痕。
7、一个可复现的构建脚本示例
# Makefile
VERSION := $(shell git describe --tags --always)
COMMIT := $(shell git rev-parse --short HEAD)
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
build:
docker build \
--build-arg VERSION=$(VERSION) \
--build-arg COMMIT=$(COMMIT) \
--build-arg DATE=$(DATE) \
-t myapp:$(VERSION) .
@echo "制品: myapp:$(VERSION) commit=$(COMMIT)"
# 构建完打上版本标签并推入仓库
publish: build
docker push registry.example.com/myapp:$(VERSION)# Dockerfile(多阶段构建,产物小、可复现)
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python -m build
FROM python:3.12-slim
ARG VERSION
ARG COMMIT
ENV APP_VERSION=$VERSION APP_COMMIT=$COMMIT
COPY --from=build /app/dist/*.whl /tmp/
RUN pip install --no-cache-dir /tmp/*.whl
CMD ["myapp"]注意--no-cache-dir、锁定依赖、固定基础镜像标签(别用:latest),都是为了可复现。
8、常见误区
| 误区 | 问题 | 正解 |
|---|---|---|
| 部署时现场构建 | 各环境制品不一致 | 构建一次,处处部署 |
| 不锁依赖 | 环境间版本漂移 | 提交 lock 文件 |
| 覆盖同版本制品 | 追溯失效 | 制品不可变,改就升版本 |
| 版本号随手编 | 下游无法判断兼容性 | 遵守 SemVer |
用 :latest 基础镜像 | 构建结果随时间变 | 固定具体版本标签 |
| 不记录 commit | 事故无法定位 | 制品嵌入版本 + commit |
9、动手实验 🎯
任务:为贯穿项目「团队任务清单」建立可复现的构建与版本管理。
- 写 Dockerfile:用多阶段构建,产出精简镜像,
docker run myapp --version能打出注入的版本。 - 注入元信息:把
VERSION、COMMIT、BUILD_DATE通过构建参数写进制品。 - 锁依赖:生成并提交 lock 文件,验证两次构建依赖完全一致。
- 验证可复现:连构两次,比较镜像摘要(
docker inspect)。一致吗?不一致,找出原因。 - 打版本标签:用
git tag v0.1.0,构建时用git describe自动读版本号。 - 推制品仓库:把镜像推到(本地或云端)仓库,尝试覆盖同 tag,看是否被拒绝(或配置成拒绝)。
- 反思:如果生产事故发生时,你能精确找回"当时跑的那份制品"吗?
下一课:持续交付与部署。
10、今天你带走的三句话
- 部署的是制品,不是源码;构建一次,处处部署。
- 可复现构建 = 锁依赖 + 容器化 + 去随机性;制品不可变。
- SemVer 是契约:MAJOR 破坏 / MINOR 新增 / PATCH 修复。
下一课:持续交付与部署。