@aspruyt/xfg 4.0.2 → 4.0.4

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.
Files changed (151) hide show
  1. package/dist/cli/index.d.ts +1 -1
  2. package/dist/cli/index.js +0 -6
  3. package/dist/cli/program.js +3 -2
  4. package/dist/cli/settings-report-builder.js +4 -4
  5. package/dist/cli/sync-command.js +72 -36
  6. package/dist/cli/sync-report-builder.d.ts +2 -6
  7. package/dist/cli/types.d.ts +2 -14
  8. package/dist/cli/types.js +1 -9
  9. package/dist/config/file-reference-resolver.js +13 -23
  10. package/dist/config/formatter.d.ts +0 -6
  11. package/dist/config/formatter.js +0 -9
  12. package/dist/config/index.d.ts +1 -2
  13. package/dist/config/index.js +0 -2
  14. package/dist/config/loader.d.ts +1 -1
  15. package/dist/config/loader.js +3 -3
  16. package/dist/config/normalizer.d.ts +1 -1
  17. package/dist/config/normalizer.js +44 -57
  18. package/dist/config/validator.d.ts +1 -1
  19. package/dist/config/validator.js +120 -121
  20. package/dist/config/validators/file-validator.d.ts +2 -4
  21. package/dist/config/validators/file-validator.js +3 -7
  22. package/dist/config/validators/repo-settings-validator.js +1 -1
  23. package/dist/config/validators/ruleset-validator.js +28 -12
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.js +0 -1
  26. package/dist/lifecycle/ado-migration-source.d.ts +2 -1
  27. package/dist/lifecycle/ado-migration-source.js +7 -5
  28. package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -3
  29. package/dist/lifecycle/github-lifecycle-provider.js +29 -19
  30. package/dist/lifecycle/lifecycle-formatter.js +2 -1
  31. package/dist/lifecycle/lifecycle-helpers.d.ts +5 -1
  32. package/dist/lifecycle/lifecycle-helpers.js +4 -4
  33. package/dist/lifecycle/repo-lifecycle-factory.d.ts +4 -4
  34. package/dist/lifecycle/repo-lifecycle-factory.js +12 -9
  35. package/dist/lifecycle/repo-lifecycle-manager.d.ts +4 -1
  36. package/dist/lifecycle/repo-lifecycle-manager.js +11 -7
  37. package/dist/lifecycle/types.d.ts +0 -15
  38. package/dist/output/github-summary.d.ts +6 -5
  39. package/dist/output/github-summary.js +36 -52
  40. package/dist/output/index.d.ts +2 -2
  41. package/dist/output/index.js +1 -1
  42. package/dist/output/lifecycle-report.d.ts +2 -12
  43. package/dist/output/lifecycle-report.js +18 -35
  44. package/dist/output/settings-report.d.ts +4 -4
  45. package/dist/output/settings-report.js +6 -6
  46. package/dist/output/sync-report.d.ts +4 -6
  47. package/dist/output/sync-report.js +2 -2
  48. package/dist/output/unified-summary.d.ts +1 -0
  49. package/dist/output/unified-summary.js +8 -8
  50. package/dist/settings/base-processor.d.ts +1 -1
  51. package/dist/settings/base-processor.js +1 -1
  52. package/dist/settings/index.d.ts +3 -3
  53. package/dist/settings/index.js +3 -3
  54. package/dist/settings/labels/diff.js +3 -2
  55. package/dist/settings/labels/formatter.js +3 -3
  56. package/dist/settings/labels/github-labels-strategy.d.ts +2 -23
  57. package/dist/settings/labels/github-labels-strategy.js +8 -28
  58. package/dist/settings/labels/index.d.ts +1 -0
  59. package/dist/settings/labels/index.js +2 -0
  60. package/dist/settings/labels/processor.d.ts +2 -2
  61. package/dist/settings/labels/processor.js +3 -4
  62. package/dist/settings/labels/types.d.ts +0 -3
  63. package/dist/settings/repo-settings/diff.d.ts +1 -1
  64. package/dist/settings/repo-settings/diff.js +2 -2
  65. package/dist/settings/repo-settings/formatter.d.ts +1 -1
  66. package/dist/settings/repo-settings/formatter.js +4 -4
  67. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -7
  68. package/dist/settings/repo-settings/github-repo-settings-strategy.js +9 -17
  69. package/dist/settings/repo-settings/index.d.ts +1 -0
  70. package/dist/settings/repo-settings/index.js +2 -0
  71. package/dist/settings/repo-settings/processor.d.ts +2 -2
  72. package/dist/settings/repo-settings/processor.js +5 -6
  73. package/dist/settings/repo-settings/types.d.ts +9 -13
  74. package/dist/settings/repo-settings/types.js +1 -14
  75. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -1
  76. package/dist/settings/rulesets/diff-algorithm.js +6 -8
  77. package/dist/settings/rulesets/formatter.js +15 -51
  78. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -20
  79. package/dist/settings/rulesets/github-ruleset-strategy.js +6 -30
  80. package/dist/settings/rulesets/index.d.ts +2 -1
  81. package/dist/settings/rulesets/index.js +3 -1
  82. package/dist/settings/rulesets/processor.d.ts +2 -2
  83. package/dist/settings/rulesets/processor.js +3 -4
  84. package/dist/{vcs → shared}/branch-utils.js +5 -4
  85. package/dist/shared/command-executor.d.ts +2 -1
  86. package/dist/shared/command-executor.js +9 -5
  87. package/dist/shared/env.d.ts +6 -6
  88. package/dist/shared/env.js +10 -17
  89. package/dist/shared/errors.d.ts +26 -0
  90. package/dist/shared/errors.js +34 -0
  91. package/dist/shared/gh-api-utils.d.ts +21 -14
  92. package/dist/shared/gh-api-utils.js +33 -22
  93. package/dist/shared/index.d.ts +9 -2
  94. package/dist/shared/index.js +16 -2
  95. package/dist/shared/logger.d.ts +24 -1
  96. package/dist/shared/logger.js +8 -3
  97. package/dist/shared/repo-detector.js +9 -11
  98. package/dist/shared/retry-utils.d.ts +5 -7
  99. package/dist/shared/retry-utils.js +3 -10
  100. package/dist/shared/shell-utils.d.ts +0 -3
  101. package/dist/shared/shell-utils.js +2 -4
  102. package/dist/shared/type-guards.d.ts +2 -9
  103. package/dist/shared/type-guards.js +0 -6
  104. package/dist/shared/xfg-template.d.ts +2 -2
  105. package/dist/shared/xfg-template.js +2 -1
  106. package/dist/sync/auth-options-builder.d.ts +3 -2
  107. package/dist/sync/auth-options-builder.js +14 -10
  108. package/dist/sync/branch-manager.d.ts +12 -7
  109. package/dist/sync/branch-manager.js +4 -7
  110. package/dist/sync/commit-message.d.ts +1 -1
  111. package/dist/sync/commit-push-manager.d.ts +8 -2
  112. package/dist/sync/commit-push-manager.js +6 -5
  113. package/dist/sync/file-sync-orchestrator.js +17 -21
  114. package/dist/sync/file-writer.js +3 -5
  115. package/dist/sync/index.d.ts +1 -1
  116. package/dist/sync/manifest-manager.d.ts +1 -0
  117. package/dist/sync/manifest.d.ts +4 -7
  118. package/dist/sync/manifest.js +42 -45
  119. package/dist/sync/repository-processor.d.ts +5 -2
  120. package/dist/sync/repository-processor.js +11 -17
  121. package/dist/sync/repository-session.js +2 -1
  122. package/dist/sync/sync-workflow.d.ts +2 -2
  123. package/dist/sync/sync-workflow.js +16 -23
  124. package/dist/sync/types.d.ts +20 -25
  125. package/dist/vcs/authenticated-git-ops.d.ts +3 -4
  126. package/dist/vcs/authenticated-git-ops.js +5 -1
  127. package/dist/vcs/azure-pr-strategy.d.ts +6 -1
  128. package/dist/vcs/azure-pr-strategy.js +38 -31
  129. package/dist/vcs/commit-strategy-selector.d.ts +10 -19
  130. package/dist/vcs/commit-strategy-selector.js +8 -24
  131. package/dist/vcs/git-commit-strategy.d.ts +1 -1
  132. package/dist/vcs/git-commit-strategy.js +1 -3
  133. package/dist/vcs/git-ops.d.ts +4 -8
  134. package/dist/vcs/git-ops.js +18 -22
  135. package/dist/vcs/github-app-token-manager.js +9 -8
  136. package/dist/vcs/github-pr-strategy.js +18 -11
  137. package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
  138. package/dist/vcs/gitlab-pr-strategy.js +14 -7
  139. package/dist/vcs/graphql-commit-strategy.d.ts +1 -7
  140. package/dist/vcs/graphql-commit-strategy.js +24 -32
  141. package/dist/vcs/index.d.ts +2 -1
  142. package/dist/vcs/pr-creator.d.ts +6 -9
  143. package/dist/vcs/pr-strategy-factory.d.ts +1 -1
  144. package/dist/vcs/pr-strategy-factory.js +2 -1
  145. package/dist/vcs/pr-strategy.d.ts +1 -1
  146. package/dist/vcs/pr-strategy.js +2 -3
  147. package/dist/vcs/types.d.ts +6 -10
  148. package/package.json +2 -2
  149. package/dist/config/errors.d.ts +0 -9
  150. package/dist/config/errors.js +0 -11
  151. /package/dist/{vcs → shared}/branch-utils.d.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  import type { RepoConfig } from "../config/types.js";
2
2
  import { RepoInfo } from "../shared/repo-detector.js";
3
- import type { ILogger } from "../shared/logger.js";
3
+ import type { DebugInfoLog } from "../shared/logger.js";
4
4
  import type { ISyncWorkflow, IWorkStrategy, IAuthOptionsBuilder, IRepositorySession, IBranchManager, ICommitPushManager, IPRMergeHandler, ProcessorOptions, ProcessorResult } from "./types.js";
5
5
  /**
6
6
  * Orchestrates the common sync workflow steps.
@@ -13,6 +13,6 @@ export declare class SyncWorkflow implements ISyncWorkflow {
13
13
  private readonly commitPushManager;
14
14
  private readonly prMergeHandler;
15
15
  private readonly log;
16
- constructor(authOptionsBuilder: IAuthOptionsBuilder, repositorySession: IRepositorySession, branchManager: IBranchManager, commitPushManager: ICommitPushManager, prMergeHandler: IPRMergeHandler, log: ILogger);
16
+ constructor(authOptionsBuilder: IAuthOptionsBuilder, repositorySession: IRepositorySession, branchManager: IBranchManager, commitPushManager: ICommitPushManager, prMergeHandler: IPRMergeHandler, log: DebugInfoLog);
17
17
  execute(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions, workStrategy: IWorkStrategy): Promise<ProcessorResult>;
18
18
  }
@@ -1,6 +1,5 @@
1
1
  import { getRepoDisplayName } from "../shared/repo-detector.js";
2
2
  import { safeCleanup } from "../shared/type-guards.js";
3
- import { defaultExecutor } from "../shared/command-executor.js";
4
3
  /**
5
4
  * Orchestrates the common sync workflow steps.
6
5
  * Used by RepositoryProcessor with different strategies for file sync vs manifest.
@@ -22,35 +21,36 @@ export class SyncWorkflow {
22
21
  }
23
22
  async execute(repoConfig, repoInfo, options, workStrategy) {
24
23
  const repoName = getRepoDisplayName(repoInfo);
25
- const { branchName, workDir } = options;
26
- const dryRun = options.dryRun ?? false;
27
- const retries = options.retries ?? 3;
28
- const executor = options.executor ?? defaultExecutor;
24
+ const { branchName } = options;
29
25
  const authResult = await this.authOptionsBuilder.resolve(repoInfo, repoName, options.token);
30
26
  if (!authResult.ok) {
31
27
  return authResult.skipResult;
32
28
  }
33
29
  const mergeMode = repoConfig.prOptions?.merge ?? "auto";
34
30
  const isDirectMode = mergeMode === "direct";
31
+ const runCtx = {
32
+ workDir: options.workDir,
33
+ dryRun: options.dryRun ?? false,
34
+ retries: options.retries ?? 3,
35
+ token: authResult.token,
36
+ executor: options.executor,
37
+ };
35
38
  let session = null;
36
39
  try {
37
40
  session = await this.repositorySession.setup(repoInfo, {
38
- workDir,
39
- dryRun,
40
- retries,
41
+ workDir: runCtx.workDir,
42
+ dryRun: runCtx.dryRun,
43
+ retries: runCtx.retries,
44
+ executor: runCtx.executor,
41
45
  authOptions: authResult.authOptions,
42
46
  });
43
47
  await this.branchManager.setupBranch({
48
+ ...runCtx,
44
49
  repoInfo,
45
50
  branchName,
46
51
  baseBranch: session.baseBranch,
47
- workDir,
48
52
  isDirectMode,
49
- dryRun,
50
- retries,
51
- token: authResult.token,
52
53
  gitOps: session.gitOps,
53
- executor,
54
54
  });
55
55
  const workResult = await workStrategy.execute(repoConfig, repoInfo, session, options);
56
56
  if (!workResult) {
@@ -63,17 +63,14 @@ export class SyncWorkflow {
63
63
  }
64
64
  const pushBranch = isDirectMode ? session.baseBranch : branchName;
65
65
  const commitResult = await this.commitPushManager.commitAndPush({
66
+ ...runCtx,
66
67
  repoInfo,
67
68
  gitOps: session.gitOps,
68
- workDir,
69
69
  fileChanges: workResult.fileChanges,
70
70
  commitMessage: workResult.commitMessage,
71
71
  pushBranch,
72
72
  isDirectMode,
73
- dryRun,
74
- retries,
75
- token: authResult.token,
76
- executor,
73
+ hasAppCredentials: options.hasAppCredentials,
77
74
  });
78
75
  if (!commitResult.success) {
79
76
  return commitResult.errorResult;
@@ -102,14 +99,10 @@ export class SyncWorkflow {
102
99
  repoInfo,
103
100
  repoConfig,
104
101
  options: {
102
+ ...runCtx,
105
103
  branchName,
106
104
  baseBranch: session.baseBranch,
107
- workDir,
108
- dryRun,
109
- retries,
110
105
  prTemplate: options.prTemplate,
111
- token: authResult.token,
112
- executor,
113
106
  },
114
107
  changedFiles: workResult.changedFiles,
115
108
  repoName,
@@ -1,12 +1,12 @@
1
1
  import type { FileContent, RepoConfig } from "../config/types.js";
2
2
  import type { RepoInfo } from "../shared/repo-detector.js";
3
- import type { ILocalGitOps, IGitOps, GitAuthOptions } from "../vcs/authenticated-git-ops.js";
3
+ import type { ILocalGitOps, IGitOps, GitAuthOptions } from "../vcs/types.js";
4
4
  import type { GitOpsOptions } from "../vcs/git-ops.js";
5
5
  import type { DiffStats } from "./diff-utils.js";
6
6
  import type { ILogger } from "../shared/logger.js";
7
7
  import type { XfgManifest } from "./manifest.js";
8
8
  import type { ICommandExecutor } from "../shared/command-executor.js";
9
- import type { FileAction } from "../vcs/pr-creator.js";
9
+ import type { FileAction } from "../vcs/types.js";
10
10
  export type GitOpsFactory = (options: GitOpsOptions, auth?: GitAuthOptions, retries?: number) => IGitOps;
11
11
  export interface FileWriteResult {
12
12
  fileName: string;
@@ -21,7 +21,7 @@ export interface FileWriteContext {
21
21
  noDelete: boolean;
22
22
  configId: string;
23
23
  /** True when using GraphQL commit strategy (GitHub App) which cannot set file modes */
24
- isGraphQLCommitMode?: boolean;
24
+ hasAppCredentials?: boolean;
25
25
  }
26
26
  export interface FileWriterDeps {
27
27
  gitOps: ILocalGitOps;
@@ -52,18 +52,21 @@ export interface IManifestManager {
52
52
  deleteOrphans(filesToDelete: string[], options: OrphanDeleteOptions, deps: OrphanDeleteDeps): Promise<void>;
53
53
  saveUpdatedManifest(workDir: string, manifest: XfgManifest, existingManifest: XfgManifest | null, dryRun: boolean, fileChanges: Map<string, FileWriteResult>): void;
54
54
  }
55
- export interface BranchSetupOptions {
56
- repoInfo: RepoInfo;
57
- branchName: string;
58
- baseBranch: string;
55
+ /** Common runtime context shared across workflow step options bags. */
56
+ export interface RunContext {
59
57
  workDir: string;
60
- isDirectMode: boolean;
61
58
  dryRun: boolean;
62
59
  retries: number;
63
60
  token?: string;
64
- gitOps: IGitOps;
65
61
  executor: ICommandExecutor;
66
62
  }
63
+ export interface BranchSetupOptions extends RunContext {
64
+ repoInfo: RepoInfo;
65
+ branchName: string;
66
+ baseBranch: string;
67
+ isDirectMode: boolean;
68
+ gitOps: IGitOps;
69
+ }
67
70
  export interface IBranchManager {
68
71
  setupBranch(options: BranchSetupOptions): Promise<void>;
69
72
  }
@@ -76,12 +79,13 @@ export type AuthResult = {
76
79
  skipResult: ProcessorResult;
77
80
  };
78
81
  export interface IAuthOptionsBuilder {
79
- resolve(repoInfo: RepoInfo, repoName: string, preResolvedToken?: string): Promise<AuthResult>;
82
+ resolve(repoInfo: RepoInfo, repoName: string, token?: string): Promise<AuthResult>;
80
83
  }
81
84
  export interface SessionOptions {
82
85
  workDir: string;
83
86
  dryRun: boolean;
84
87
  retries: number;
88
+ executor: ICommandExecutor;
85
89
  authOptions?: GitAuthOptions;
86
90
  }
87
91
  export interface SessionContext {
@@ -92,18 +96,14 @@ export interface SessionContext {
92
96
  export interface IRepositorySession {
93
97
  setup(repoInfo: RepoInfo, options: SessionOptions): Promise<SessionContext>;
94
98
  }
95
- export interface CommitPushOptions {
99
+ export interface CommitPushOptions extends RunContext {
96
100
  repoInfo: RepoInfo;
97
101
  gitOps: IGitOps;
98
- workDir: string;
99
102
  fileChanges: Map<string, FileWriteResult>;
100
103
  commitMessage: string;
101
104
  pushBranch: string;
102
105
  isDirectMode: boolean;
103
- dryRun: boolean;
104
- retries: number;
105
- token?: string;
106
- executor: ICommandExecutor;
106
+ hasAppCredentials?: boolean;
107
107
  }
108
108
  export type CommitPushResult = {
109
109
  success: true;
@@ -124,13 +124,13 @@ export interface ProcessorOptions {
124
124
  configId: string;
125
125
  dryRun?: boolean;
126
126
  retries?: number;
127
- executor?: ICommandExecutor;
127
+ executor: ICommandExecutor;
128
128
  prTemplate?: string;
129
129
  noDelete?: boolean;
130
- /** Pre-resolved GitHub token avoids duplicate resolution in AuthOptionsBuilder */
130
+ /** GitHub token for authentication (resolved by caller) */
131
131
  token?: string;
132
132
  /** True when using GraphQL commit strategy (GitHub App) which cannot set file modes */
133
- isGraphQLCommitMode?: boolean;
133
+ hasAppCredentials?: boolean;
134
134
  }
135
135
  export interface FileChangeDetail {
136
136
  path: string;
@@ -162,15 +162,10 @@ export interface FileSyncResult {
162
162
  export interface IFileSyncOrchestrator {
163
163
  sync(repoConfig: RepoConfig, repoInfo: RepoInfo, session: SessionContext, options: ProcessorOptions): Promise<FileSyncResult>;
164
164
  }
165
- export interface PRHandlerOptions {
165
+ export interface PRHandlerOptions extends RunContext {
166
166
  branchName: string;
167
167
  baseBranch: string;
168
- workDir: string;
169
- dryRun: boolean;
170
- retries: number;
171
168
  prTemplate?: string;
172
- token?: string;
173
- executor: ICommandExecutor;
174
169
  }
175
170
  export interface CreateAndMergeInput {
176
171
  repoInfo: RepoInfo;
@@ -1,6 +1,6 @@
1
1
  import type { ICommandExecutor } from "../shared/command-executor.js";
2
+ import type { DebugLog } from "../shared/logger.js";
2
3
  import type { GitAuthOptions, ILocalGitOps, IGitOps } from "./types.js";
3
- export type { GitAuthOptions, ILocalGitOps, INetworkGitOps, IGitOps, } from "./types.js";
4
4
  /**
5
5
  * Adds authentication to network git operations and delegates local ops.
6
6
  *
@@ -15,9 +15,7 @@ export declare class AuthenticatedGitOps implements IGitOps {
15
15
  private readonly retries;
16
16
  private readonly auth?;
17
17
  private readonly log?;
18
- constructor(localOps: ILocalGitOps, executor: ICommandExecutor, workDir: string, retries: number, auth?: GitAuthOptions | undefined, log?: {
19
- debug(msg: string): void;
20
- } | undefined);
18
+ constructor(localOps: ILocalGitOps, executor: ICommandExecutor, workDir: string, retries: number, auth?: GitAuthOptions | undefined, log?: DebugLog | undefined);
21
19
  private execWithRetry;
22
20
  /**
23
21
  * Build the authenticated remote URL.
@@ -31,6 +29,7 @@ export declare class AuthenticatedGitOps implements IGitOps {
31
29
  wouldChange(fileName: string, content: string): boolean;
32
30
  hasChanges(): Promise<boolean>;
33
31
  getChangedFiles(): Promise<string[]>;
32
+ stageAll(): Promise<void>;
34
33
  hasStagedChanges(): Promise<boolean>;
35
34
  fileExistsOnBranch(fileName: string, branch: string): Promise<boolean>;
36
35
  fileExists(fileName: string): boolean;
@@ -1,6 +1,7 @@
1
1
  import { escapeShellArg } from "../shared/shell-utils.js";
2
2
  import { withRetry } from "../shared/retry-utils.js";
3
3
  import { toErrorMessage } from "../shared/type-guards.js";
4
+ import { SyncError } from "../shared/errors.js";
4
5
  /**
5
6
  * Adds authentication to network git operations and delegates local ops.
6
7
  *
@@ -33,7 +34,7 @@ export class AuthenticatedGitOps {
33
34
  */
34
35
  getAuthenticatedUrl() {
35
36
  if (!this.auth) {
36
- throw new Error("getAuthenticatedUrl() called without auth options");
37
+ throw new SyncError("getAuthenticatedUrl() called without auth options");
37
38
  }
38
39
  const { token, host, owner, repo } = this.auth;
39
40
  return `https://x-access-token:${token}@${host}/${owner}/${repo}`;
@@ -63,6 +64,9 @@ export class AuthenticatedGitOps {
63
64
  getChangedFiles() {
64
65
  return this.localOps.getChangedFiles();
65
66
  }
67
+ stageAll() {
68
+ return this.localOps.stageAll();
69
+ }
66
70
  hasStagedChanges() {
67
71
  return this.localOps.hasStagedChanges();
68
72
  }
@@ -4,9 +4,14 @@ import type { IPRStrategyLogger } from "./pr-strategy.js";
4
4
  import type { PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./types.js";
5
5
  import { ICommandExecutor } from "../shared/command-executor.js";
6
6
  export declare class AzurePRStrategy extends BasePRStrategy {
7
- constructor(executor?: ICommandExecutor, log?: IPRStrategyLogger);
7
+ constructor(executor: ICommandExecutor, log?: IPRStrategyLogger);
8
8
  private getOrgUrl;
9
9
  private buildPRUrl;
10
+ /**
11
+ * Query Azure DevOps for an existing PR ID matching source/target branches.
12
+ * Returns the raw PR ID string, or null if none found.
13
+ */
14
+ private findExistingPRId;
10
15
  checkExistingPR(options: CloseExistingPROptions): Promise<string | null>;
11
16
  closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
12
17
  create(options: PRStrategyOptions): Promise<PRResult>;
@@ -2,9 +2,11 @@ import { existsSync, writeFileSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { escapeShellArg } from "../shared/shell-utils.js";
4
4
  import { assertAzureDevOpsRepo, } from "../shared/repo-detector.js";
5
+ import { SyncError } from "../shared/errors.js";
5
6
  import { BasePRStrategy } from "./pr-strategy.js";
6
- import { withRetry } from "../shared/retry-utils.js";
7
+ import { withRetry, isPermanentError } from "../shared/retry-utils.js";
7
8
  import { toErrorMessage, safeCleanup } from "../shared/type-guards.js";
9
+ import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
8
10
  import { sanitizeCredentials } from "../shared/sanitize-utils.js";
9
11
  import { getStderr } from "../shared/command-executor.js";
10
12
  export class AzurePRStrategy extends BasePRStrategy {
@@ -18,17 +20,21 @@ export class AzurePRStrategy extends BasePRStrategy {
18
20
  buildPRUrl(repoInfo, prId) {
19
21
  return `https://dev.azure.com/${encodeURIComponent(repoInfo.organization)}/${encodeURIComponent(repoInfo.project)}/_git/${encodeURIComponent(repoInfo.repo)}/pullrequest/${prId.trim()}`;
20
22
  }
21
- async checkExistingPR(options) {
22
- const { repoInfo, branchName, baseBranch, workDir, retries = 3 } = options;
23
- assertAzureDevOpsRepo(repoInfo, "Azure PR strategy");
24
- const azureRepoInfo = repoInfo;
23
+ /**
24
+ * Query Azure DevOps for an existing PR ID matching source/target branches.
25
+ * Returns the raw PR ID string, or null if none found.
26
+ */
27
+ async findExistingPRId(azureRepoInfo, branchName, baseBranch, workDir, retries) {
25
28
  const orgUrl = this.getOrgUrl(azureRepoInfo);
26
29
  const command = `az repos pr list --repository ${escapeShellArg(azureRepoInfo.repo)} --source-branch ${escapeShellArg(branchName)} --target-branch ${escapeShellArg(baseBranch)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(azureRepoInfo.project)} --query "[0].pullRequestId" -o tsv`;
27
30
  try {
28
- const existingPRId = await withRetry(() => this.executor.exec(command, workDir), { retries });
29
- return existingPRId ? this.buildPRUrl(azureRepoInfo, existingPRId) : null;
31
+ const existingPRId = await withRetry(() => this.executor.exec(command, workDir), { retries, log: this.log });
32
+ return existingPRId ? existingPRId.trim() : null;
30
33
  }
31
34
  catch (error) {
35
+ if (isPermanentError(error)) {
36
+ throw error;
37
+ }
32
38
  const stderr = getStderr(error);
33
39
  if (stderr && !stderr.includes("does not exist")) {
34
40
  this.log?.debug(`Azure PR check failed - ${sanitizeCredentials(stderr).trim()}`);
@@ -36,46 +42,41 @@ export class AzurePRStrategy extends BasePRStrategy {
36
42
  return null;
37
43
  }
38
44
  }
45
+ async checkExistingPR(options) {
46
+ const { repoInfo, branchName, baseBranch, workDir, retries = 3 } = options;
47
+ assertAzureDevOpsRepo(repoInfo, "Azure PR strategy");
48
+ const azureRepoInfo = repoInfo;
49
+ const prId = await this.findExistingPRId(azureRepoInfo, branchName, baseBranch, workDir, retries);
50
+ return prId ? this.buildPRUrl(azureRepoInfo, prId) : null;
51
+ }
39
52
  async closeExistingPR(options) {
40
53
  const { repoInfo, branchName, baseBranch, workDir, retries = 3 } = options;
41
54
  assertAzureDevOpsRepo(repoInfo, "Azure PR strategy");
42
55
  const azureRepoInfo = repoInfo;
43
56
  const orgUrl = this.getOrgUrl(azureRepoInfo);
44
- // First check if there's an existing PR
45
- const existingUrl = await this.checkExistingPR({
46
- repoInfo,
47
- branchName,
48
- baseBranch,
49
- workDir,
50
- retries,
51
- });
52
- if (!existingUrl) {
53
- return false;
54
- }
55
- // Extract PR ID from URL
56
- const prInfo = this.parsePRUrl(existingUrl);
57
- if (!prInfo) {
58
- this.log?.warn(`Could not parse PR URL: ${existingUrl}`);
57
+ const prId = await this.findExistingPRId(azureRepoInfo, branchName, baseBranch, workDir, retries);
58
+ if (!prId) {
59
59
  return false;
60
60
  }
61
61
  // Abandon the PR (Azure DevOps equivalent of closing)
62
- const abandonCommand = `az repos pr update --id ${escapeShellArg(prInfo.prId)} --status abandoned --org ${escapeShellArg(orgUrl)}`;
62
+ const abandonCommand = `az repos pr update --id ${escapeShellArg(prId)} --status abandoned --org ${escapeShellArg(orgUrl)}`;
63
63
  try {
64
64
  await withRetry(() => this.executor.exec(abandonCommand, workDir), {
65
65
  retries,
66
+ log: this.log,
66
67
  });
67
68
  }
68
69
  catch (error) {
69
70
  const message = toErrorMessage(error);
70
- this.log?.warn(`Failed to abandon PR #${prInfo.prId}: ${message}`);
71
+ this.log?.warn(`Failed to abandon PR #${prId}: ${message}`);
71
72
  return false;
72
73
  }
73
74
  try {
74
75
  const getRefCommand = `az repos ref list --repository ${escapeShellArg(azureRepoInfo.repo)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(azureRepoInfo.project)} --filter heads/${escapeShellArg(branchName)} --query "[0].objectId" -o tsv`;
75
- const objectId = await withRetry(() => this.executor.exec(getRefCommand, workDir), { retries });
76
+ const objectId = await withRetry(() => this.executor.exec(getRefCommand, workDir), { retries, log: this.log });
76
77
  if (objectId) {
77
78
  const deleteBranchCommand = `az repos ref delete --name refs/heads/${escapeShellArg(branchName)} --repository ${escapeShellArg(azureRepoInfo.repo)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(azureRepoInfo.project)} --object-id ${escapeShellArg(objectId)}`;
78
- await withRetry(() => this.executor.exec(deleteBranchCommand, workDir), { retries });
79
+ await withRetry(() => this.executor.exec(deleteBranchCommand, workDir), { retries, log: this.log });
79
80
  }
80
81
  }
81
82
  catch (error) {
@@ -91,12 +92,18 @@ export class AzurePRStrategy extends BasePRStrategy {
91
92
  const azureRepoInfo = repoInfo;
92
93
  const orgUrl = this.getOrgUrl(azureRepoInfo);
93
94
  const descFile = join(workDir, this.bodyFilePath);
94
- writeFileSync(descFile, body, "utf-8");
95
+ try {
96
+ writeFileSync(descFile, body, "utf-8");
97
+ }
98
+ catch (err) {
99
+ throw new SyncError(`Failed to write PR description to ${descFile}: ${toErrorMessage(err)}`);
100
+ }
95
101
  // Azure CLI @file syntax: escape the full @path to handle special chars in workDir
96
102
  const command = `az repos pr create --repository ${escapeShellArg(azureRepoInfo.repo)} --source-branch ${escapeShellArg(branchName)} --target-branch ${escapeShellArg(baseBranch)} --title ${escapeShellArg(title)} --description ${escapeShellArg("@" + descFile)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(azureRepoInfo.project)} --query "pullRequestId" -o tsv`;
97
103
  try {
98
104
  const prId = await withRetry(() => this.executor.exec(command, workDir), {
99
105
  retries,
106
+ log: this.log,
100
107
  });
101
108
  return {
102
109
  url: this.buildPRUrl(azureRepoInfo, prId),
@@ -108,7 +115,7 @@ export class AzurePRStrategy extends BasePRStrategy {
108
115
  safeCleanup(() => {
109
116
  if (existsSync(descFile))
110
117
  unlinkSync(descFile);
111
- }, `failed to remove ${descFile}`, this.log ?? { debug() { } });
118
+ }, `failed to remove ${descFile}`, this.log ?? NO_OP_DEBUG_LOG);
112
119
  }
113
120
  }
114
121
  /**
@@ -128,7 +135,6 @@ export class AzurePRStrategy extends BasePRStrategy {
128
135
  }
129
136
  async merge(options) {
130
137
  const { prUrl, config, workDir, retries = 3 } = options;
131
- // Manual mode: do nothing
132
138
  if (config.mode === "manual") {
133
139
  return {
134
140
  success: true,
@@ -136,7 +142,6 @@ export class AzurePRStrategy extends BasePRStrategy {
136
142
  merged: false,
137
143
  };
138
144
  }
139
- // Parse PR URL to extract details
140
145
  const prInfo = this.parsePRUrl(prUrl);
141
146
  if (!prInfo) {
142
147
  return {
@@ -161,6 +166,7 @@ export class AzurePRStrategy extends BasePRStrategy {
161
166
  }
162
167
  if (config.mode === "force") {
163
168
  const bypassReason = config.bypassReason ?? "Automated config sync via xfg";
169
+ this.log?.warn(`Bypassing policies for PR ${prInfo.prId} (reason: ${bypassReason})`);
164
170
  const forceCommand = `az repos pr update --id ${escapeShellArg(prInfo.prId)} --bypass-policy true --bypass-policy-reason ${escapeShellArg(bypassReason)} --status completed ${squashFlag} ${deleteBranchFlag} --org ${escapeShellArg(orgUrl)}`.trim();
165
171
  return this.executeMergeCommand(() => this.executor.exec(forceCommand, workDir), retries, {
166
172
  success: true,
@@ -168,9 +174,10 @@ export class AzurePRStrategy extends BasePRStrategy {
168
174
  merged: true,
169
175
  }, "Failed to bypass policies and complete PR");
170
176
  }
177
+ const _exhaustive = config.mode;
171
178
  return {
172
179
  success: false,
173
- message: `Unknown merge mode: ${config.mode}`,
180
+ message: `Merge not applicable for mode: ${_exhaustive}`,
174
181
  merged: false,
175
182
  };
176
183
  }
@@ -2,26 +2,17 @@ import { RepoInfo } from "../shared/repo-detector.js";
2
2
  import type { ICommitStrategy } from "./types.js";
3
3
  import { GitHubAppTokenManager } from "./github-app-token-manager.js";
4
4
  import { ICommandExecutor } from "../shared/command-executor.js";
5
+ interface GitHubAppCredentials {
6
+ appId: string;
7
+ privateKey: string;
8
+ }
5
9
  /**
6
- * Checks if GitHub App credentials are configured via environment variables.
7
- * Both XFG_GITHUB_APP_ID and XFG_GITHUB_APP_PRIVATE_KEY must be set.
10
+ * Creates a GitHubAppTokenManager from credentials, or null if not provided.
8
11
  */
9
- export declare function hasGitHubAppCredentials(): boolean;
12
+ export declare function createTokenManager(credentials?: GitHubAppCredentials): GitHubAppTokenManager | null;
10
13
  /**
11
- * Creates a GitHubAppTokenManager if credentials are configured, otherwise null.
14
+ * Returns GraphQLCommitStrategy for GitHub repos with App credentials (verified commits),
15
+ * or GitCommitStrategy for all other cases.
12
16
  */
13
- export declare function createTokenManager(): GitHubAppTokenManager | null;
14
- /**
15
- * Factory function to get the appropriate commit strategy for a repository.
16
- *
17
- * For GitHub repositories with GitHub App credentials (XFG_GITHUB_APP_ID and
18
- * XFG_GITHUB_APP_PRIVATE_KEY), returns GraphQLCommitStrategy which creates
19
- * verified commits via the GitHub GraphQL API.
20
- *
21
- * For all other cases (GitHub with PAT, Azure DevOps, GitLab), returns GitCommitStrategy
22
- * which uses standard git commands.
23
- *
24
- * @param repoInfo - Repository information
25
- * @param executor - Optional command executor for shell commands
26
- */
27
- export declare function getCommitStrategy(repoInfo: RepoInfo, executor?: ICommandExecutor): ICommitStrategy;
17
+ export declare function getCommitStrategy(repoInfo: RepoInfo, executor: ICommandExecutor, hasAppCredentials?: boolean): ICommitStrategy;
18
+ export {};
@@ -3,36 +3,20 @@ import { GitCommitStrategy } from "./git-commit-strategy.js";
3
3
  import { GraphQLCommitStrategy } from "./graphql-commit-strategy.js";
4
4
  import { GitHubAppTokenManager } from "./github-app-token-manager.js";
5
5
  /**
6
- * Checks if GitHub App credentials are configured via environment variables.
7
- * Both XFG_GITHUB_APP_ID and XFG_GITHUB_APP_PRIVATE_KEY must be set.
6
+ * Creates a GitHubAppTokenManager from credentials, or null if not provided.
8
7
  */
9
- export function hasGitHubAppCredentials() {
10
- return !!(process.env.XFG_GITHUB_APP_ID && process.env.XFG_GITHUB_APP_PRIVATE_KEY);
11
- }
12
- /**
13
- * Creates a GitHubAppTokenManager if credentials are configured, otherwise null.
14
- */
15
- export function createTokenManager() {
16
- if (!hasGitHubAppCredentials()) {
8
+ export function createTokenManager(credentials) {
9
+ if (!credentials) {
17
10
  return null;
18
11
  }
19
- return new GitHubAppTokenManager(process.env.XFG_GITHUB_APP_ID, process.env.XFG_GITHUB_APP_PRIVATE_KEY);
12
+ return new GitHubAppTokenManager(credentials.appId, credentials.privateKey);
20
13
  }
21
14
  /**
22
- * Factory function to get the appropriate commit strategy for a repository.
23
- *
24
- * For GitHub repositories with GitHub App credentials (XFG_GITHUB_APP_ID and
25
- * XFG_GITHUB_APP_PRIVATE_KEY), returns GraphQLCommitStrategy which creates
26
- * verified commits via the GitHub GraphQL API.
27
- *
28
- * For all other cases (GitHub with PAT, Azure DevOps, GitLab), returns GitCommitStrategy
29
- * which uses standard git commands.
30
- *
31
- * @param repoInfo - Repository information
32
- * @param executor - Optional command executor for shell commands
15
+ * Returns GraphQLCommitStrategy for GitHub repos with App credentials (verified commits),
16
+ * or GitCommitStrategy for all other cases.
33
17
  */
34
- export function getCommitStrategy(repoInfo, executor) {
35
- if (isGitHubRepo(repoInfo) && hasGitHubAppCredentials()) {
18
+ export function getCommitStrategy(repoInfo, executor, hasAppCredentials) {
19
+ if (isGitHubRepo(repoInfo) && hasAppCredentials) {
36
20
  return new GraphQLCommitStrategy(executor);
37
21
  }
38
22
  return new GitCommitStrategy(executor);
@@ -7,7 +7,7 @@ import { ICommandExecutor } from "../shared/command-executor.js";
7
7
  */
8
8
  export declare class GitCommitStrategy implements ICommitStrategy {
9
9
  private executor;
10
- constructor(executor?: ICommandExecutor);
10
+ constructor(executor: ICommandExecutor);
11
11
  /**
12
12
  * Create a commit with the given file changes and push to remote.
13
13
  * Runs: git add -A, git commit, git push (with optional --force-with-lease)
@@ -1,4 +1,3 @@
1
- import { defaultExecutor, } from "../shared/command-executor.js";
2
1
  import { withRetry } from "../shared/retry-utils.js";
3
2
  import { escapeShellArg } from "../shared/shell-utils.js";
4
3
  /**
@@ -9,7 +8,7 @@ import { escapeShellArg } from "../shared/shell-utils.js";
9
8
  export class GitCommitStrategy {
10
9
  executor;
11
10
  constructor(executor) {
12
- this.executor = executor ?? defaultExecutor;
11
+ this.executor = executor;
13
12
  }
14
13
  /**
15
14
  * Create a commit with the given file changes and push to remote.
@@ -34,7 +33,6 @@ export class GitCommitStrategy {
34
33
  retries,
35
34
  });
36
35
  }
37
- // Get the commit SHA
38
36
  const sha = await this.executor.exec("git rev-parse HEAD", workDir);
39
37
  return {
40
38
  sha: sha.trim(),
@@ -1,13 +1,12 @@
1
1
  import { ICommandExecutor } from "../shared/command-executor.js";
2
+ import type { DebugLog } from "../shared/logger.js";
2
3
  import type { ILocalGitOps } from "./types.js";
3
4
  export interface GitOpsOptions {
4
5
  workDir: string;
5
6
  dryRun?: boolean;
6
- executor?: ICommandExecutor;
7
+ executor: ICommandExecutor;
7
8
  /** Optional logger for debug messages */
8
- log?: {
9
- debug(msg: string): void;
10
- };
9
+ log?: DebugLog;
11
10
  }
12
11
  export declare class GitOps implements ILocalGitOps {
13
12
  private readonly _workDir;
@@ -54,10 +53,7 @@ export declare class GitOps implements ILocalGitOps {
54
53
  * Uses the same this.exec() pattern as other methods in this class.
55
54
  */
56
55
  getChangedFiles(): Promise<string[]>;
57
- /**
58
- * Check if there are staged changes ready to commit.
59
- * Uses `git diff --cached --quiet` which exits with 1 if there are staged changes.
60
- */
56
+ stageAll(): Promise<void>;
61
57
  hasStagedChanges(): Promise<boolean>;
62
58
  /**
63
59
  * Check if a file exists on a specific branch.