@aspruyt/xfg 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,6 +44,20 @@ export declare class GraphQLCommitStrategy implements CommitStrategy {
44
44
  * Execute the createCommitOnBranch GraphQL mutation.
45
45
  */
46
46
  private executeGraphQLMutation;
47
+ /**
48
+ * Build a git command with optional token authentication override.
49
+ * When a token is provided, uses -c url.insteadOf to override the global
50
+ * git config and authenticate with the provided token instead.
51
+ *
52
+ * This is critical for GitHub App authentication where the global git config
53
+ * may have a PAT token embedded, but we need to use the GitHub App installation token.
54
+ *
55
+ * Uses a repo-specific URL pattern (including owner/repo) so it has a LONGER
56
+ * prefix match than the global config and takes precedence.
57
+ *
58
+ * Applies to all remote operations: push, fetch, ls-remote, etc.
59
+ */
60
+ private buildAuthenticatedGitCommand;
47
61
  /**
48
62
  * Ensure the branch exists on the remote and matches local HEAD.
49
63
  * createCommitOnBranch requires the branch to already exist.
@@ -72,7 +72,7 @@ export class GraphQLCommitStrategy {
72
72
  // Ensure the branch exists on remote and is up-to-date with local HEAD
73
73
  // createCommitOnBranch requires the branch to already exist
74
74
  // For PR branches (force=true), we force-update to ensure fresh start from main
75
- await this.ensureBranchExistsOnRemote(branchName, workDir, options.force);
75
+ await this.ensureBranchExistsOnRemote(branchName, workDir, options.force, token, githubInfo.host, githubInfo.owner, githubInfo.repo);
76
76
  // Retry loop for expectedHeadOid mismatch
77
77
  let lastError = null;
78
78
  for (let attempt = 0; attempt <= retries; attempt++) {
@@ -80,7 +80,7 @@ export class GraphQLCommitStrategy {
80
80
  // Fetch from remote to ensure we have the latest HEAD
81
81
  // This is critical for expectedHeadOid to match
82
82
  const safeBranch = escapeShellArg(branchName);
83
- await this.executor.exec(`git fetch origin ${safeBranch}:refs/remotes/origin/${safeBranch}`, workDir);
83
+ await this.executor.exec(this.buildAuthenticatedGitCommand(`fetch origin ${safeBranch}:refs/remotes/origin/${safeBranch}`, token, githubInfo.host, githubInfo.owner, githubInfo.repo), workDir);
84
84
  // Get the remote HEAD SHA for this branch (not local HEAD)
85
85
  const headSha = await this.executor.exec(`git rev-parse origin/${safeBranch}`, workDir);
86
86
  // Build and execute the GraphQL mutation
@@ -171,6 +171,31 @@ export class GraphQLCommitStrategy {
171
171
  pushed: true, // GraphQL commits are pushed directly
172
172
  };
173
173
  }
174
+ /**
175
+ * Build a git command with optional token authentication override.
176
+ * When a token is provided, uses -c url.insteadOf to override the global
177
+ * git config and authenticate with the provided token instead.
178
+ *
179
+ * This is critical for GitHub App authentication where the global git config
180
+ * may have a PAT token embedded, but we need to use the GitHub App installation token.
181
+ *
182
+ * Uses a repo-specific URL pattern (including owner/repo) so it has a LONGER
183
+ * prefix match than the global config and takes precedence.
184
+ *
185
+ * Applies to all remote operations: push, fetch, ls-remote, etc.
186
+ */
187
+ buildAuthenticatedGitCommand(gitArgs, token, host = "github.com", owner, repo) {
188
+ if (!token) {
189
+ return `git ${gitArgs}`;
190
+ }
191
+ // Use repo-specific URL pattern for LONGER prefix match to override global config
192
+ // Global config: url."https://x-access-token:PAT@github.com/".insteadOf = "https://github.com/"
193
+ // Our config: url."https://x-access-token:APP@github.com/owner/repo".insteadOf = "https://github.com/owner/repo"
194
+ // The longer prefix (owner/repo) takes precedence in git's URL matching
195
+ const repoPath = owner && repo ? `${owner}/${repo}` : "";
196
+ const urlOverride = `url."https://x-access-token:${token}@${host}/${repoPath}".insteadOf="https://${host}/${repoPath}"`;
197
+ return `git -c ${escapeShellArg(urlOverride)} ${gitArgs}`;
198
+ }
174
199
  /**
175
200
  * Ensure the branch exists on the remote and matches local HEAD.
176
201
  * createCommitOnBranch requires the branch to already exist.
@@ -180,23 +205,23 @@ export class GraphQLCommitStrategy {
180
205
  *
181
206
  * For direct mode (force=false): just ensure branch exists.
182
207
  */
183
- async ensureBranchExistsOnRemote(branchName, workDir, force) {
208
+ async ensureBranchExistsOnRemote(branchName, workDir, force, token, host, owner, repo) {
184
209
  // Branch name was validated in commit(), safe for shell use
185
210
  try {
186
211
  // Check if the branch exists on remote
187
- await this.executor.exec(`git ls-remote --exit-code --heads origin ${escapeShellArg(branchName)}`, workDir);
212
+ await this.executor.exec(this.buildAuthenticatedGitCommand(`ls-remote --exit-code --heads origin ${escapeShellArg(branchName)}`, token, host, owner, repo), workDir);
188
213
  // Branch exists - for PR branches, delete and recreate to ensure fresh from main
189
214
  if (force) {
190
- await this.executor.exec(`git push origin --delete ${escapeShellArg(branchName)}`, workDir);
215
+ await this.executor.exec(this.buildAuthenticatedGitCommand(`push origin --delete ${escapeShellArg(branchName)}`, token, host, owner, repo), workDir);
191
216
  // Now push fresh branch from local HEAD
192
- await this.executor.exec(`git push -u origin HEAD:${escapeShellArg(branchName)}`, workDir);
217
+ await this.executor.exec(this.buildAuthenticatedGitCommand(`push -u origin HEAD:${escapeShellArg(branchName)}`, token, host, owner, repo), workDir);
193
218
  }
194
219
  // For direct mode (force=false), leave existing branch as-is
195
220
  }
196
221
  catch {
197
222
  // Branch doesn't exist on remote, push it
198
223
  // This pushes the current local branch to create it on remote
199
- await this.executor.exec(`git push -u origin HEAD:${escapeShellArg(branchName)}`, workDir);
224
+ await this.executor.exec(this.buildAuthenticatedGitCommand(`push -u origin HEAD:${escapeShellArg(branchName)}`, token, host, owner, repo), workDir);
200
225
  }
201
226
  }
202
227
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspruyt/xfg",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "CLI tool for repository-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",