第十九章:NPM 包与 Docker CI/CD 自动化 交付详解,利用 Github Action 实现全自动化发布

第十九章:NPM 包与 Docker CI/CD 自动化 交付详解,利用 Github Action 实现全自动化发布

本章目标:将 Release Please 生成的版本号与 NPM 发布、Docker 构建流程打通,实现端到端的自动化交付


开始之前:认识什么是制品

在软件工程中,制品 指的是可以直接分发给用户的、可执行的交付物,常见类型包括:

制品类型示例分发渠道本章是否覆盖
NPM 包reactvuelodashnpmjs.com✅ 是
Docker 镜像nginx:latestnode:18-alpineDocker Hub、GHCR✅ 是
二进制可执行文件kubectlterraformGitHub Releases⚠️ 原理相同
移动应用.apk.ipaGoogle Play、App Store❌ 否

注意,本章节默认了你已经熟悉了 docker 的基础操作,在此之上我们引申出 Docker 的 CI/CD 流程,如果你对于 Docker 还不熟悉,请跳转至


19.1. NPM 包的无人值守发布

19.1.1. 手动发布的痛点与安全风险

传统手动发布流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 手动修改版本号
vim package.json # version: "1.2.3" → "1.2.4"

# 2. 手动更新CHANGELOG
vim CHANGELOG.md

# 3. 提交并打Tag
git add .
git commit -m "chore: release 1.2.4"
git tag v1.2.4
git push && git push --tags

# 4. 发布到NPM
npm publish
# ⚠️ 此时会提示输入OTP(One-Time Password)
# 需要打开手机上的认证器App(如Google Authenticator)
# 输入6位数字验证码

问题分析

步骤问题风险
1版本号可能冲突/遗漏覆盖已发布版本
2CHANGELOG 容易漏记用户不知道更新了什么
3忘记打 Tag无法回溯代码
4需要人工输入 OTPCI 无法自动化

19.1.2. NPM 的 2FA 认证机制

为什么 NPM 强制 2FA?

2018 年,著名的 event-stream 事件中,攻击者通过盗取维护者账号,向包中植入了窃取比特币钱包的恶意代码,影响了数百万下载量。

从那以后,NPM 强制要求:

  1. 所有发布操作必须启用 2FA
  2. 敏感操作(发布、删除包)需要输入 OTP

问题:CI 服务器没有手机,无法输入 OTP。

解决方案Automation Token(自动化令牌)


19.1.3. 生成 NPM Automation Token 的正确姿势

步骤 1:登录 NPM 官网

访问 https://www.npmjs.com/ 并登录。

步骤 2:进入 Access Tokens 页面

点击头像 → Access Tokens

或直接访问:https://www.npmjs.com/settings/YOUR_USERNAME/tokens

步骤 3:创建新 Token

点击 Generate New Token → 选择 Automation

三种 Token 类型对比

类型权限有效期是否需要 OTP(一次性密码)适用场景
Publish可发布包永久(可撤销)✅ 需要本地开发
Automation可发布包永久(可撤销)❌ 不需要CI/CD
Read-Only只读私有包永久(可撤销)❌ 不需要安装私有依赖

选择 Automation 后,会显示 Token(格式类似:npm_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

重要提醒

  • Token 只显示一次,必须立即复制
  • 建议复制到密码管理器(如 1Password)
  • 永远不要 提交到 Git 仓库

生成 Automation Token


19.1.4. 配置 GitHub Secrets

进入 GitHub 仓库 → SettingsSecrets and variablesActionsNew repository secret

添加 Secret:

  • Name: NPM_TOKEN
  • Value: 粘贴刚才复制的 Token

配置 NPM Token


19.1.5. 编写自动发布 Workflow

为什么作为 Release Please 的第二个 Job?

很多新手会写成独立的 workflow:

1
2
3
4
5
6
7
8
9
10
# ❌ 错误示范
on:
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
steps:
- run: npm publish

问题

  1. Release Please 使用 GITHUB_TOKEN 创建的 Release,可能不会触发 release 事件(GitHub 的防循环机制)
  2. 时间差问题:Release 创建和 workflow 触发之间有延迟

正确做法:将发布作为 Release Please workflow 的第二个 Job,通过 needsif 条件控制。


完整 Workflow 代码

文件路径.github/workflows/release-please.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
name: Release Please

on:
push:
branches:
- main

permissions:
contents: write
pull-requests: write

jobs:
release-please:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}

steps:
- uses: google-github-actions/release-please-action@v4
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

# ========== NPM发布Job ==========
publish-npm:
name: 📦 Publish to NPM
needs: release-please
# ⭐ 关键:只有Release真正创建时才运行
if: needs.release-please.outputs.release_created == 'true'
runs-on: ubuntu-latest

permissions:
contents: read
id-token: write # 如果使用Provenance需要

steps:
- name: 📥 Checkout代码
uses: actions/checkout@v4
with:
# ⭐ 关键:检出对应的Tag,而不是main分支
ref: ${{ needs.release-please.outputs.tag_name }}

- name: 📦 Setup pnpm(如果项目使用pnpm)
uses: pnpm/action-setup@v4
with:
version: latest

- name: 🟢 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
# ⭐ 自动配置.npmrc文件
cache: 'pnpm' # 或 'npm'

- name: 📥 安装依赖
run: pnpm install --frozen-lockfile
# 如果使用npm:npm ci

- name: 🏗️ 构建(如果需要)
run: pnpm run build
# 有些包需要编译(如TypeScript),有些不需要

- name: 🚀 发布到NPM
env:
# ⭐ 关键:将Secret注入为环境变量
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: pnpm publish --no-git-checks --access public
# 参数说明:
# --no-git-checks: 跳过Git检查(在CI中必需)
# --access public: 公开包(如果是私有包则改为 restricted)

19.1.6. 常见问题排查手册

问题 1:“This package has been marked as private”

错误信息

1
2
npm ERR! This package has been marked as private
npm ERR! Remove the 'private' field from package.json to publish it.

原因
package.json 中有 "private": true 字段。

解决方法:打开 package.json,删除或修改:

1
2
3
4
5
{
"name": "my-package",
"version": "1.0.0",
- "private": true
}

⚠️ 注意:如果这个包确实不应该发布到公开 NPM,请检查是否配置错误。


问题 2:“You must be logged in to publish packages”

错误信息

1
2
npm ERR! code ENEEDAUTH
npm ERR! need auth This command requires you to be logged in.

原因

  1. NODE_AUTH_TOKEN 环境变量没有正确设置
  2. Token 已过期或被撤销
  3. actions/setup-noderegistry-url 配置错误

解决方法

  1. 确认 secrets.NPM_TOKEN 在 GitHub Secrets 中存在
  2. 确认 workflow 中有 registry-url: 'https://registry.npmjs.org'
  3. 检查 Token 是否过期(在 NPM 官网查看)

19.2. Docker 镜像的分级发布与标签管理

19.2.1. 生产环境 vs 测试环境:矛盾的需求

在容器化交付中,不同环境对镜像的要求是截然相反的。如果不做分级管理,就会导致“测试环境无法验证最新代码”或“生产环境随意更新导致事故”。

维度测试/开发环境 (Dev/Test)生产环境 (Prod)
核心诉求时效性:代码合并后,必须立即能测稳定性:版本必须锁定,严禁变动
镜像标签latest (滚动更新)v1.2.3 (不可变)
更新频率高频 (每次 Merge Request 合并)低频 (仅在正式发版时)
拉取策略imagePullPolicy: AlwaysimagePullPolicy: IfNotPresent

设计目标

  1. 日常开发:只要代码合并到 main 分支,流水线应自动构建 latest 标签,供测试环境拉取。
  2. 正式发布:当发布新版本时,流水线除更新 latest 外,必须 额外 打上 v1.2.3 等精确版本标签,供生产环境使用。

19.2.2. 企业级镜像标签 (Tag) 策略

为了满足从开发调试、自动升级到严格回滚的全场景需求,一个成熟的 Docker 镜像仓库应包含以下五个层级的标签。

标签类型示例触发时机适用场景关键特性
Git SHAsha-abc1234每次 Push调试/定位:精准对应某次代码提交,用于排查 Bug。唯一性
Latestlatest每次 Push测试环境:永远指向 main 分支的最新代码。易变性
Patchv1.2.3发版时生产部署:锁定特定版本,生产环境 YAML 必须使用此标签。不可变
Minorv1.2发版时安全更新:指向 v1.2.x 系列的最新版,用于自动获取 Bug 修复。滑动指针
Majorv1发版时长期维护:指向 v1.x.x 系列的最新版。滑动指针

最佳实践图解

1
2
3
4
5
6
7
Commit A (Fix Bug) ---> Build ---> my-app:latest, my-app:sha-AAA
|
Commit B (Release) ---> Build ---> my-app:latest, my-app:v1.2.3, my-app:sha-BBB
| (生产环境使用此 tag 部署)
|
Commit C (Feature) ---> Build ---> my-app:latest, my-app:sha-CCC
(测试环境自动拉取此 tag)

19.2.3. 动态标签编排 (Dynamic Tagging)

为了在一条流水线中同时满足上述需求,我们利用 docker/metadata-action 的条件逻辑,而不是编写两个重复的 Job。

逻辑流向

  1. 监听:流水线监听 main 分支的所有推送。
  2. 判断release-please 判断当前提交是“普通合并”还是“正式发版”。
  3. 分支处理
    • 如果是 普通合并:仅构建 latestsha-xxxx
    • 如果是 正式发版:构建 latestsha-xxxx 以及 v1.2.3

19.2.4. 完整工作流实现

此配置文件实现了上述的“动静分离”策略。它确保了测试环境随时可用,且生产环境版本严格受控。

文件路径.github/workflows/release-publish.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
name: Release & Publish

on:
push:
branches:
- main

permissions:
contents: write
pull-requests: write

jobs:
# ====================================================
# Job 1: 版本控制中心 (Release Please)
# ====================================================
release-please:
runs-on: ubuntu-latest
outputs:
# 输出布尔值:true 代表这是一次正式发版,false 代表只是普通代码合并
release_created: ${{ steps.release.outputs.release_created }}
# 输出版本号:例如 v1.2.3
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- uses: google-github-actions/release-please-action@v4
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

# ====================================================
# Job 2: 全场景 Docker 构建
# ====================================================
publish-docker:
needs: release-please
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: 📥 Checkout 代码
uses: actions/checkout@v4
# 默认检出 main 分支最新代码,这符合 latest 的定义

- name: 🏷️ 配置动态标签策略
id: meta
uses: docker/metadata-action@v5
with:
images: prorise123/git-hooks-demo
# 核心配置:使用 enable 属性控制特定标签是否生成
tags: |
# 1. 基础层:Git SHA (调试用) - 每次必打
type=sha,format=long

# 2. 基础层:Latest (测试环境用) - 每次必打
type=raw,value=latest

# 3. 生产层:精确版本 (生产环境用) - 仅在 release_created=true 时启用
type=raw,value=${{ needs.release-please.outputs.tag_name }},enable=${{ needs.release-please.outputs.release_created == 'true' }}

# 4. (可选) 语义化次版本 v1.2 - 仅在 release_created=true 时启用
# 如果 tag_name 是 v1.2.3,此规则会自动提取 v1.2
type=semver,pattern={{major}}.{{minor}},value=${{ needs.release-please.outputs.tag_name }},enable=${{ needs.release-please.outputs.release_created == 'true' }}
- name: 🛠️ Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: 🔐 登录 Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: 🚀 构建并推送
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
# 自动应用上述 step 生成的所有标签
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
cache-from: type=gha
cache-to: type=gha,mode=max

19.2.5. 结果验证

运行上述流水线后,Docker Hub 的仓库中会出现以下情况,完美匹配企业需求:

  1. 日常开发提交时

    • 更新 latest 标签(测试环境自动获取新功能)。
    • 新增 sha-7b3f1a... 标签(保留历史构建记录)。
    • 不会 生成 v1.2.3(保护生产版本号不被滥用)。
  2. Release Please 自动发版时

    • 更新 latest 标签。
    • 新增 sha-8c4d2e... 标签。
    • 新增 v1.2.3 标签(生产环境 YAML 修改为此版本进行上线)。
    • 新增 v1.2 标签(可选,供依赖次版本的下游服务使用)。

19.3. 完整 CD 流程集成:Release → NPM → Docker

19.3.1. 最终架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
┌──────────────────────────────────────────────────────┐
│ 完整CD Pipeline架构 │
└──────────────────────────────────────────────────────┘

[开发者推送代码到main分支]

┌─────────────────────┐
│ Job 1: CI (单元测试) │ ← 继承自第16章
└─────────────────────┘
↓ (成功后)
┌──────────────────────────┐
│ Job 2: Release Please │
│ • 扫描提交信息 │
│ • 创建/更新Release PR │
└──────────────────────────┘
↓ (PR合并后)
┌──────────────────────────┐
│ Job 2: Release Please │
│ • 检测到PR已合并 │
│ • 创建Git Tag & Release │
│ • 设置outputs: │
│ - release_created=true │
│ - tag_name=v1.2.3 │
└──────────────────────────┘

├──────────────────────┐
│ ↓
┌───────────────────┐ ┌─────────────────────┐
│ Job 3: NPM Publish│ │ Job 4: Docker Build │
│ if: release_created│ │ if: release_created │
│ • 检出Tag代码 │ │ • 构建多标签镜像 │
│ • npm publish │ │ • 推送到Docker Hub │
└───────────────────┘ └─────────────────────┘