本节摘要:自动化构建将源码经编译、依赖解析、打包变为可部署制品(JAR、Docker 镜像等)。本节以 Maven
-B非交互构建、npm ci 锁定依赖、Docker 多阶段构建为主线,并说明 Nexus 制品入库策略。
阅读完本节,你应当能够:
npm ci 与 npm install 在 CI 中的差异Java 团队最简 Jenkins stage:
stage('Build') { steps { sh 'mvn -B clean package -DskipTests' } }
-B(batch mode)禁止 Maven 等待人工输入——CI 无人值守必加。完整 verify 含测试:
mvn -B clean verify -Dmaven.test.failure.ignore=false
构建产物默认在 target/*.jar。Archive 到 Jenkins 或 push Nexus:
stage('Publish') { steps { sh ''' mvn deploy -DskipTests \ -DaltDeploymentRepository=nexus::default::https://nexus.corp/repository/maven-releases/ ''' } }

- uses: actions/setup-node@v4 with: { node-version: '20', cache: 'npm' } - run: npm ci - run: npm run build
| 命令 | CI 适用 | 说明 |
|---|---|---|
| npm ci | ✅ 推荐 | 严格按 package-lock.json,先删 node_modules |
| npm install | ❌ | 可能更新 lock,不可复现 |
| pnpm i --frozen-lockfile | ✅ | monorepo 友好 |
poetry install --no-interaction --no-root poetry build
wheel 输出在 dist/,推 PyPI 私有库或作为 Docker COPY 源。
FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html
CI 中 build & push:
- run: docker build -t registry.corp/myapp:${{ github.sha }} . - run: docker push registry.corp/myapp:${{ github.sha }}
tag 用 commit SHA,不用 latest——回滚与审计需要 immutable 标识。
| 语言 | 工具 | 制品 |
|---|---|---|
| Java | Maven, Gradle | JAR/WAR |
| .NET | dotnet CLI | DLL/nupkg |
| Node | npm, webpack | dist/ |
| Python | poetry, setuptools | wheel |
| Go | go build | 静态二进制 |
| 通用 | Docker | 镜像 |
⚠️ 常见坑:CI 用
SNAPSHOT覆盖同版本 JAR——无法追溯哪次 commit 上了生产。
💡 关键直觉:构建脚本本身进 Git(Maven pom、Dockerfile)——「在我机器上能编」= CI 也能编,环境差异交给容器。
- uses: gradle/actions/setup-gradle@v3 - run: ./gradlew build --no-daemon
GitHub Actions 的 Gradle build action 缓存 ~/.gradle/caches,大型项目可省 5–10 分钟。
| 仓库类型 | 存放 | 保留策略 |
|---|---|---|
| maven-snapshots | 开发分支构建 | 30 天 |
| maven-releases | tag 发布 | 永久 |
| docker-hosted | 镜像 | 按 digest |
// build.gradle.kts plugins { java } tasks.test { useJUnitPlatform() } tasks.withType<Test> { maxParallelForks = Runtime.runtime.availableProcessors() / 2 }
Gradle build cache 跨 CI job 复用编译输出——企业 Nexus 托管 remote build cache。
相同源码 + 相同 lock 文件 → 相同制品 hash。Go 1.20+ 默认 reproducible;Docker 用 --build-arg SOURCE_DATE_EPOCH 固定时间戳。供应链安全 SBOM(Syft、CycloneDX)在 build 后生成,随制品入库。
Maven settings.xml 在 CI 用 secret 注入,不进 Git:
<server> <id>nexus</id> <username>${env.NEXUS_USER}</username> <password>${env.NEXUS_PASS}</password> </server>
npm 用 .npmrc 指向私有 registry,token 来自 NPM_TOKEN 环境变量。
| 现象 | 原因 | 修复 |
|---|---|---|
| 依赖下载 403 | IP 未加 Nexus 白名单 | 加 runner egress |
| 编译 OOM | 默认 heap 太小 | MAVEN_OPTS=-Xmx2g |
| 时区测试失败 | runner UTC vs 本地 CST | 测试用固定 ZoneId |
| 并发 test 竞态 | 共享 /tmp | 隔离 temp dir |
下一节 2.3 在构建之后设测试与静态分析门禁。
构建阶段的目标是「可复现、可追踪、可加速」,三个目标有时互相冲突。可复现依赖 lock 文件与干净环境,可追踪依赖制品 tag 与构建日志,可加速依赖缓存与并行——缓存往往会引入「脏状态」,需要与可复现做权衡。推荐的折中是:CI runner 的缓存只用于依赖下载(Maven Central、npm registry 的 HTTP 缓存),不缓存编译产物;制品库(Nexus/Harbor)负责跨构建共享,用 immutable tag 保证可追踪。
构建时还要注意环境隔离:同一份源码在开发机、CI runner、Docker 容器里构建的结果应当一致。把构建工具链版本(JDK、Node、Python)写进版本控制,用 mise/asdf 或容器镜像固定版本,能消除「在我机器上能编」的经典问题。对多语言 monorepo,建议给每个服务独立构建 job,避免一个语言的构建失败拖慢全仓。
可追溯性是制品管理的底线:每次构建要能回答「这份制品对应哪个 commit、由哪次构建产出」。实现方式是给制品打上不可变标识——Maven 用版本号加 build number,容器镜像用 commit SHA,npm 包用 version 加 pre-release 标签。制品库的保留策略要区分快照与正式发布:快照可过期清理,正式发布永久保留,否则磁盘很快被占满且无法审计。