Skip to content

Commit 9afcf46

Browse files
committed
ci: use OIDC for npm publishing
1 parent f57308f commit 9afcf46

2 files changed

Lines changed: 89 additions & 66 deletions

File tree

.github/workflows/npm-publish.yml

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ubuntu-latest
1313
permissions:
1414
contents: read
15-
id-token: write # Required for npm provenance
15+
id-token: write # Required for npm Trusted Publishing (OIDC)
1616

1717
steps:
1818
- name: Checkout code
@@ -34,11 +34,14 @@ jobs:
3434
- name: Setup Node.js
3535
uses: actions/setup-node@v4
3636
with:
37-
node-version: '22'
37+
node-version: '24'
3838
registry-url: 'https://registry.npmjs.org'
3939
cache: 'pnpm'
4040
cache-dependency-path: 'ui/pnpm-lock.yaml'
4141

42+
- name: Update npm for OIDC publishing
43+
run: npm install -g npm@^11.5.1
44+
4245
- name: Extract version from tag
4346
id: version
4447
run: |
@@ -75,45 +78,13 @@ jobs:
7578
7679
- name: Publish all packages to NPM
7780
if: success()
78-
env:
79-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
8081
run: |
81-
# 调试:检查环境变量
82-
echo "=== 调试信息 ==="
83-
echo "NODE_AUTH_TOKEN 长度: ${#NODE_AUTH_TOKEN}"
84-
if [ -z "$NODE_AUTH_TOKEN" ]; then
85-
echo "错误: NODE_AUTH_TOKEN 未设置!"
86-
exit 1
87-
fi
88-
89-
# 配置 npm 认证
90-
echo "配置 npm 认证..."
91-
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
92-
93-
# 验证 .npmrc 文件已创建
94-
if [ -f ~/.npmrc ]; then
95-
echo "✓ .npmrc 文件已创建"
96-
echo ".npmrc 内容(隐藏 token):"
97-
cat ~/.npmrc | sed 's/:_authToken=.*/:_authToken=***HIDDEN***/g'
98-
else
99-
echo "✗ .npmrc 文件创建失败!"
100-
exit 1
101-
fi
102-
103-
# 测试 npm 认证
104-
echo "测试 npm 认证..."
105-
npm whoami || {
106-
echo "错误: npm whoami 失败!认证未成功。"
107-
exit 1
108-
}
109-
110-
# 使用自动发布脚本发布所有包
11182
python3 publish-all-packages.py
11283
11384
- name: Create summary
11485
if: success()
11586
run: |
116-
echo "### ✅ CodeKanban Published to NPM" >> $GITHUB_STEP_SUMMARY
87+
echo "### ✅ CodeKanban Published to NPM with OIDC" >> $GITHUB_STEP_SUMMARY
11788
echo "" >> $GITHUB_STEP_SUMMARY
11889
echo "**Version:** ${{ steps.version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
11990
echo "" >> $GITHUB_STEP_SUMMARY

publish-all-packages.py

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
from pathlib import Path
1010

1111

12+
MIN_NODE_VERSION_FOR_NPM_OIDC = (22, 14, 0)
13+
MIN_NPM_VERSION_FOR_OIDC = (11, 5, 1)
14+
15+
1216
def run_command(cmd: list[str], cwd: Path | None = None, shell: bool = False) -> int:
1317
"""执行命令并实时输出"""
1418
print(f"\n[执行] {' '.join(cmd)}")
@@ -34,6 +38,77 @@ def is_github_actions() -> bool:
3438
return os.getenv('GITHUB_ACTIONS') == 'true'
3539

3640

41+
def command_output(cmd: list[str], cwd: Path | None = None) -> str:
42+
"""执行命令并返回 stdout"""
43+
result = subprocess.run(cmd, cwd=cwd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
44+
if result.returncode != 0:
45+
raise RuntimeError(result.stdout.strip() or f"command failed: {' '.join(cmd)}")
46+
return result.stdout.strip()
47+
48+
49+
def parse_version(value: str) -> tuple[int, int, int]:
50+
"""解析 node/npm 版本号,忽略 v 前缀和预发布后缀"""
51+
cleaned = value.strip().lstrip('v').split('-', 1)[0]
52+
parts = cleaned.split('.')
53+
version = []
54+
for part in parts[:3]:
55+
try:
56+
version.append(int(part))
57+
except ValueError:
58+
version.append(0)
59+
while len(version) < 3:
60+
version.append(0)
61+
return tuple(version)
62+
63+
64+
def check_github_actions_oidc(root_dir: Path) -> int:
65+
"""校验 GitHub Actions OIDC 发布所需环境"""
66+
print("\n[认证] GitHub Actions 环境:使用 npm Trusted Publishing (OIDC)")
67+
68+
request_token = os.getenv('ACTIONS_ID_TOKEN_REQUEST_TOKEN')
69+
request_url = os.getenv('ACTIONS_ID_TOKEN_REQUEST_URL')
70+
if not request_token or not request_url:
71+
print("[错误] 未检测到 GitHub Actions OIDC 请求环境变量")
72+
print("[提示] 请确认 workflow job 配置了 permissions.id-token: write,并使用 GitHub-hosted runner")
73+
return 1
74+
75+
try:
76+
node_version_text = command_output(["node", "--version"], cwd=root_dir)
77+
npm_version_text = command_output(["npm", "--version"], cwd=root_dir)
78+
except RuntimeError as exc:
79+
print(f"[错误] 检查 node/npm 版本失败: {exc}")
80+
return 1
81+
82+
node_version = parse_version(node_version_text)
83+
npm_version = parse_version(npm_version_text)
84+
print(f"[认证] Node.js: {node_version_text}")
85+
print(f"[认证] npm: {npm_version_text}")
86+
87+
if node_version < MIN_NODE_VERSION_FOR_NPM_OIDC:
88+
required = ".".join(str(part) for part in MIN_NODE_VERSION_FOR_NPM_OIDC)
89+
print(f"[错误] npm OIDC 发布需要 Node.js >= {required}")
90+
return 1
91+
92+
if npm_version < MIN_NPM_VERSION_FOR_OIDC:
93+
required = ".".join(str(part) for part in MIN_NPM_VERSION_FOR_OIDC)
94+
print(f"[错误] npm OIDC 发布需要 npm >= {required}")
95+
return 1
96+
97+
print("[认证] OIDC 环境检查通过,npm publish 将在发布时换取短期令牌")
98+
return 0
99+
100+
101+
def check_local_npm_auth(root_dir: Path) -> int:
102+
"""本地发布仍使用当前 npm 登录状态"""
103+
print("\n[认证] 本地环境:检查 npm 登录状态...")
104+
auth_result = run_command(["npm", "whoami"], cwd=root_dir)
105+
if auth_result != 0:
106+
print("\n[错误] npm whoami 失败,请先运行 npm login 或改用 GitHub Actions OIDC 发布")
107+
return auth_result
108+
print("[认证] npm 登录状态正常")
109+
return 0
110+
111+
37112
def main():
38113
root_dir = Path(__file__).parent.absolute()
39114
npm_packages_dir = root_dir / "npm-packages"
@@ -66,42 +141,19 @@ def main():
66141
"linux-arm64"
67142
]
68143

69-
# 调试信息:检查 npm 认证
70-
print("\n[调试] 检查 npm 认证状态...")
71-
check_auth_cmd = ["npm", "whoami"]
72-
auth_result = run_command(check_auth_cmd, cwd=root_dir)
73-
74-
if auth_result != 0:
75-
print("\n[警告] npm whoami 失败,可能未正确认证")
76-
print("[调试] 检查 .npmrc 配置...")
77-
npmrc_path = Path.home() / ".npmrc"
78-
if npmrc_path.exists():
79-
print(f"[调试] 找到 .npmrc: {npmrc_path}")
80-
# 不打印内容,避免泄露 token
81-
else:
82-
print(f"[警告] 未找到 .npmrc: {npmrc_path}")
83-
84-
if is_github_actions():
85-
print("[调试] 在 GitHub Actions 中,检查 NODE_AUTH_TOKEN 环境变量...")
86-
if os.getenv('NODE_AUTH_TOKEN'):
87-
print("[调试] NODE_AUTH_TOKEN 已设置 (长度: {})".format(len(os.getenv('NODE_AUTH_TOKEN', ''))))
88-
else:
89-
print("[错误] NODE_AUTH_TOKEN 未设置!")
90-
return 1
144+
if is_github_actions():
145+
auth_result = check_github_actions_oidc(root_dir)
91146
else:
92-
print("[调试] npm 认证成功")
147+
auth_result = check_local_npm_auth(root_dir)
148+
if auth_result != 0:
149+
return auth_result
93150

94151
# 构建发布命令
95152
publish_cmd = ["npm", "publish", "--access", "public"]
96-
97-
# Granular access tokens 在某些情况下与 --provenance 不兼容
98-
# 暂时禁用以测试基本发布功能
99-
use_provenance = False
100-
if is_github_actions() and use_provenance:
101-
publish_cmd.append("--provenance")
102-
print("\n[信息] 检测到 GitHub Actions 环境,将使用 --provenance 发布")
153+
if is_github_actions():
154+
print("\n[信息] 使用 npm Trusted Publishing (OIDC) 发布")
103155
else:
104-
print("\n[信息] 使用标准发布模式(不使用 provenance)")
156+
print("\n[信息] 使用本地 npm 登录凭据发布")
105157

106158
# 发布所有平台包
107159
print("\n[步骤 1/2] 发布平台包")

0 commit comments

Comments
 (0)