Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .bumpy/fix-git-push-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@varlock/bumpy': patch
---

Fix git push auth in CI by using remote URL token embedding instead of extraheader approach, which doesn't work with actions/checkout@v6
27 changes: 15 additions & 12 deletions packages/bumpy/src/core/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,9 @@ export function pushWithTags(opts?: { cwd?: string }): void {
* Temporarily configure git credentials using BUMPY_GH_TOKEN (or GH_TOKEN),
* execute a callback, then restore the original config.
*
* Uses the http.extraheader approach (same as actions/checkout) rather than
* embedding tokens in the remote URL, because extraheader takes priority over
* any credential manager that may be installed on the runner.
*
* Also clears any existing credential config set by actions/checkout (extraheader
* or includeIf entries) so our token is used instead of the default GITHUB_TOKEN.
* Embeds the token in the remote URL and clears any existing credential config
* set by actions/checkout (extraheader or includeIf entries) so our token is
* used instead of the default GITHUB_TOKEN.
*/
export function withGitToken(cwd: string | undefined, fn: () => void): void {
const token = process.env.BUMPY_GH_TOKEN || process.env.GH_TOKEN;
Expand All @@ -37,8 +34,6 @@ export function withGitToken(cwd: string | undefined, fn: () => void): void {
}

const extraHeaderKey = `http.${server}/.extraheader`;
// Authorization: bearer works for both GitHub PATs and GITHUB_TOKEN
const authHeader = `Authorization: bearer ${token}`;

// Save and clear any existing credential config set by actions/checkout:
// 1. Direct http.<server>/.extraheader in local config
Expand All @@ -56,15 +51,21 @@ export function withGitToken(cwd: string | undefined, fn: () => void): void {
}
}

// Rewrite the remote URL to embed the token — this is the most reliable
// auth method as it bypasses all credential helpers and extraheader issues
const originalUrl = tryRunArgs(['git', 'remote', 'get-url', 'origin'], { cwd });
const authedUrl = originalUrl ? originalUrl.replace(/^https:\/\//, `https://x-access-token:${token}@`) : null;

try {
if (savedHeader) {
runArgs(['git', 'config', '--local', '--unset-all', extraHeaderKey], { cwd });
}
for (const entry of savedIncludeIfs) {
tryRunArgs(['git', 'config', '--local', '--unset', entry.key], { cwd });
}
// Set our token as the Authorization header — this takes priority over credential managers
runArgs(['git', 'config', '--local', extraHeaderKey, authHeader], { cwd });
if (authedUrl) {
runArgs(['git', 'remote', 'set-url', 'origin', authedUrl], { cwd });
}
try {
fn();
} catch (err) {
Expand All @@ -73,8 +74,10 @@ export function withGitToken(cwd: string | undefined, fn: () => void): void {
throw new Error(msg.replaceAll(token, '***'));
}
} finally {
// Remove our injected header
tryRunArgs(['git', 'config', '--local', '--unset-all', extraHeaderKey], { cwd });
// Restore original remote URL
if (originalUrl) {
runArgs(['git', 'remote', 'set-url', 'origin', originalUrl], { cwd });
}
// Restore previous credential config
if (savedHeader) {
runArgs(['git', 'config', '--local', extraHeaderKey, savedHeader], { cwd });
Expand Down