Skip to content

Commit 89098f5

Browse files
committed
ci(release): fix app token git auth recovery
- Use a Basic x-access-token git auth header for App token git pushes. - Allow manual recovery of the current npm latest version after partial release. - Document the recovery path and cover the workflow/version policy with tests. - Verified with targeted tests, full tests, lint, build, format, and actionlint.
1 parent 4d66aea commit 89098f5

5 files changed

Lines changed: 52 additions & 7 deletions

File tree

.github/workflows/release.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,8 @@ jobs:
493493
env:
494494
GH_TOKEN: ${{ steps.write-app-token.outputs.token }}
495495
run: |
496-
git config --local http.https://github.com/.extraheader "AUTHORIZATION: bearer ${GH_TOKEN}"
496+
GIT_AUTH_HEADER=$(printf "x-access-token:%s" "$GH_TOKEN" | base64 | tr -d '\n')
497+
git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${GIT_AUTH_HEADER}"
497498
498499
- name: Resolve existing release PR
499500
id: release_pr
@@ -778,7 +779,8 @@ jobs:
778779
MERGE_COMMIT=${{ steps.wait_for_merge.outputs.merge_commit }}
779780
780781
# PR 已合并,检出该 release PR 的 merge commit,避免 main 后续推进影响 tag 目标。
781-
git config --local http.https://github.com/.extraheader "AUTHORIZATION: bearer ${GH_TOKEN}"
782+
GIT_AUTH_HEADER=$(printf "x-access-token:%s" "$GH_TOKEN" | base64 | tr -d '\n')
783+
git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${GIT_AUTH_HEADER}"
782784
git fetch origin main
783785
git checkout -B main "$MERGE_COMMIT"
784786
git config user.name "github-actions[bot]"

docs/RELEASE_PR_GUARDS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,16 @@ GitHub App installation token 有固定有效期。release job 等待 PR 合并
235235
同理,lint、build、test 和 npm publish 完成后,创建 release 分支、PR 和 auto-merge 前也
236236
需要重新生成 write token,并刷新 Git 的 auth header,缩短 token 实际使用窗口。
237237

238+
GitHub App token 同时会被 `gh``git` 使用,但两者的认证形态不同:
239+
240+
- `gh` 命令通过 `GH_TOKEN` 使用 token。
241+
- `git push` / `git fetch` 使用 HTTPS Git 凭据,需要配置成 Basic auth header,
242+
`x-access-token:<token>` 的 base64 形式。
243+
244+
不要把 Git 的 `http.https://github.com/.extraheader` 配置成 `AUTHORIZATION: bearer ...`
245+
这种写法可能让 `gh` API 正常工作,但 HTTPS Git push 会在认证阶段失败,表现为
246+
`could not read Username for 'https://github.com'`
247+
238248
### 6.5 增强 release job 的可恢复性
239249

240250
release job 应允许在超时或中断后重跑:
@@ -249,6 +259,9 @@ release job 应允许在超时或中断后重跑:
249259
- 如果 release PR 仍是 OPEN 且已经启用 auto-merge,重跑时直接复用该状态,不重复调用
250260
`gh pr merge --auto` 导致失败。
251261
- 如果 npm 版本已经存在,校验并重置 `latest` dist-tag,不重复发布。
262+
- 如果 npm 已发布但 release 分支、release PR、tag 或 GitHub Release 还没有完成,维护者
263+
可以手动重跑主包 release workflow,并显式输入当前 npm latest 版本继续恢复。此时 workflow
264+
不应自动递增到下一个 patch,也不应因为 npm 上已有该版本而提前失败。
252265
- 如果 tag 或 GitHub Release 已经存在,校验或补充 assets,不重复创建。
253266

254267
这样 release PR 检查较慢时,不需要新建第二个 workflow。维护者可以在同一个 release PR

scripts/resolve-main-release-version.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ function resolveManualVersion(currentVersion) {
113113
const version = readEnv('MAIN_MANUAL_VERSION').trim();
114114
assertStableVersion(version);
115115

116-
if (compareStableVersions(version, currentVersion) <= 0) {
116+
if (compareStableVersions(version, currentVersion) < 0) {
117117
throw new Error(
118-
`Manual target version ${version} must be greater than current online latest ${currentVersion}.`
118+
`Manual target version ${version} must be greater than or equal to current online latest ${currentVersion}.`
119119
);
120120
}
121121

@@ -142,7 +142,7 @@ function main() {
142142
? resolveManualVersion(currentVersion)
143143
: resolveAutoVersion(currentVersion, publishedVersions);
144144

145-
if (npmVersionExists(version)) {
145+
if (!manualVersion && npmVersionExists(version)) {
146146
throw new Error(`${PACKAGE_NAME}@${version} already exists on npm.`);
147147
}
148148

src/__tests__/mainReleaseVersionPolicy.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,34 @@ describe('main package release version policy', () => {
106106
expect(result.stdout).toContain('Major/minor changed: true');
107107
});
108108

109+
it('allows manual recovery of the current npm latest version', () => {
110+
const fakeBin = createFakeNpm('1.24.11');
111+
const result = runResolver({
112+
PATH: fakeBin,
113+
GITHUB_REF_NAME: 'main',
114+
MAIN_MANUAL_VERSION: '1.24.11',
115+
});
116+
117+
expect(result.status).toBe(0);
118+
expect(result.stdout).toContain('Resolved @taptap/instant-games-open-mcp version: 1.24.11');
119+
expect(result.stdout).toContain('Version mode: manual');
120+
expect(result.stdout).toContain('Major/minor changed: false');
121+
});
122+
123+
it('rejects manual versions below the current npm latest version', () => {
124+
const fakeBin = createFakeNpm('1.24.11');
125+
const result = runResolver({
126+
PATH: fakeBin,
127+
GITHUB_REF_NAME: 'main',
128+
MAIN_MANUAL_VERSION: '1.24.10',
129+
});
130+
131+
expect(result.status).toBe(1);
132+
expect(result.stderr).toContain(
133+
'Manual target version 1.24.10 must be greater than or equal to current online latest 1.24.11.'
134+
);
135+
});
136+
109137
it('rejects latest publishing outside main', () => {
110138
const fakeBin = createFakeNpm('1.24.5');
111139
const result = runResolver({

src/__tests__/releaseWorkflowGuards.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ describe('release PR required workflow guards', () => {
130130
"if: success() && steps.wait_for_merge.outputs.state == 'MERGED'"
131131
);
132132
expect(releaseStep).toContain('GH_TOKEN: ${{ steps.final-app-token.outputs.token }}');
133-
expect(releaseStep).toContain('AUTHORIZATION: bearer ${GH_TOKEN}');
133+
expect(releaseStep).toContain('AUTHORIZATION: basic ${GIT_AUTH_HEADER}');
134134
expect(releaseStep).not.toContain('for i in $(seq 1 360)');
135135
});
136136

@@ -148,7 +148,9 @@ describe('release PR required workflow guards', () => {
148148

149149
const configureStep = getStepBody(workflow, 'Configure release write app token');
150150
expect(configureStep).toContain('GH_TOKEN: ${{ steps.write-app-token.outputs.token }}');
151-
expect(configureStep).toContain('AUTHORIZATION: bearer ${GH_TOKEN}');
151+
expect(configureStep).toContain('x-access-token:%s');
152+
expect(configureStep).toContain('AUTHORIZATION: basic ${GIT_AUTH_HEADER}');
153+
expect(configureStep).not.toContain('AUTHORIZATION: bearer');
152154

153155
for (const stepName of [
154156
'Resolve existing release PR',

0 commit comments

Comments
 (0)