@aspruyt/xfg 3.9.14 → 3.10.0

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.
@@ -1,4 +1,4 @@
1
- export type { MergeMode, MergeStrategy, PRMergeOptions, RulesetTarget, RulesetEnforcement, BypassActorType, BypassMode, PatternOperator, MergeMethod, AlertsThreshold, SecurityAlertsThreshold, BypassActor, RefNameCondition, RulesetConditions, StatusCheckConfig, RequiredReviewer, CodeScanningTool, WorkflowConfig, PullRequestRuleParameters, RequiredStatusChecksParameters, UpdateRuleParameters, RequiredDeploymentsParameters, CodeScanningParameters, CodeQualityParameters, WorkflowsParameters, PatternRuleParameters, FilePathRestrictionParameters, FileExtensionRestrictionParameters, MaxFilePathLengthParameters, MaxFileSizeParameters, PullRequestRule, RequiredStatusChecksRule, RequiredSignaturesRule, RequiredLinearHistoryRule, NonFastForwardRule, CreationRule, UpdateRule, DeletionRule, RequiredDeploymentsRule, CodeScanningRule, CodeQualityRule, WorkflowsRule, CommitAuthorEmailPatternRule, CommitMessagePatternRule, CommitterEmailPatternRule, BranchNamePatternRule, TagNamePatternRule, FilePathRestrictionRule, FileExtensionRestrictionRule, MaxFilePathLengthRule, MaxFileSizeRule, RulesetRule, Ruleset, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, RepoVisibility, GitHubRepoSettings, Label, RepoSettings, ContentValue, RawFileConfig, RawRepoFileOverride, RawRepoSettings, RawRepoConfig, RawConfig, FileContent, RepoConfig, Config, } from "./types.js";
1
+ export type { MergeMode, MergeStrategy, PRMergeOptions, RulesetTarget, RulesetEnforcement, BypassActorType, BypassMode, PatternOperator, MergeMethod, AlertsThreshold, SecurityAlertsThreshold, BypassActor, RefNameCondition, RulesetConditions, StatusCheckConfig, RequiredReviewer, CodeScanningTool, WorkflowConfig, PullRequestRuleParameters, RequiredStatusChecksParameters, UpdateRuleParameters, RequiredDeploymentsParameters, CodeScanningParameters, CodeQualityParameters, WorkflowsParameters, PatternRuleParameters, FilePathRestrictionParameters, FileExtensionRestrictionParameters, MaxFilePathLengthParameters, MaxFileSizeParameters, PullRequestRule, RequiredStatusChecksRule, RequiredSignaturesRule, RequiredLinearHistoryRule, NonFastForwardRule, CreationRule, UpdateRule, DeletionRule, RequiredDeploymentsRule, CodeScanningRule, CodeQualityRule, WorkflowsRule, CommitAuthorEmailPatternRule, CommitMessagePatternRule, CommitterEmailPatternRule, BranchNamePatternRule, TagNamePatternRule, FilePathRestrictionRule, FileExtensionRestrictionRule, MaxFilePathLengthRule, MaxFileSizeRule, RulesetRule, Ruleset, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, RepoVisibility, GitHubRepoSettings, Label, RepoSettings, ContentValue, RawFileConfig, RawRepoFileOverride, RawRootSettings, RawRepoSettings, RawRepoConfig, RawConfig, FileContent, RepoConfig, Config, } from "./types.js";
2
2
  export { RULESET_FIELD_MAP, RULESET_COMPARABLE_FIELDS } from "./types.js";
3
3
  export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
4
4
  export { convertContentToString, detectOutputFormat, type OutputFormat, type ConvertOptions, } from "./formatter.js";
@@ -1,5 +1,5 @@
1
- import type { RawConfig, Config, RepoSettings, RawRepoSettings } from "./types.js";
2
- export declare function mergeSettings(root: RawRepoSettings | undefined, perRepo: RawRepoSettings | undefined): RepoSettings | undefined;
1
+ import type { RawConfig, Config, RepoSettings, RawRootSettings, RawRepoSettings } from "./types.js";
2
+ export declare function mergeSettings(root: RawRootSettings | undefined, perRepo: RawRepoSettings | undefined): RepoSettings | undefined;
3
3
  /**
4
4
  * Normalizes raw config into expanded, merged config.
5
5
  * Pipeline: expand git arrays -> merge content -> interpolate env vars
@@ -26,6 +26,7 @@ function mergePROptions(global, perRepo) {
26
26
  const mergeStrategy = perRepo.mergeStrategy ?? global.mergeStrategy;
27
27
  const deleteBranch = perRepo.deleteBranch ?? global.deleteBranch;
28
28
  const bypassReason = perRepo.bypassReason ?? global.bypassReason;
29
+ const labels = perRepo.labels ?? global.labels;
29
30
  if (merge !== undefined)
30
31
  result.merge = merge;
31
32
  if (mergeStrategy !== undefined)
@@ -34,6 +35,8 @@ function mergePROptions(global, perRepo) {
34
35
  result.deleteBranch = deleteBranch;
35
36
  if (bypassReason !== undefined)
36
37
  result.bypassReason = bypassReason;
38
+ if (labels !== undefined)
39
+ result.labels = labels;
37
40
  return Object.keys(result).length > 0 ? result : undefined;
38
41
  }
39
42
  /**
@@ -6,6 +6,7 @@ export interface PRMergeOptions {
6
6
  mergeStrategy?: MergeStrategy;
7
7
  deleteBranch?: boolean;
8
8
  bypassReason?: string;
9
+ labels?: string[];
9
10
  }
10
11
  /** Ruleset target type */
11
12
  export type RulesetTarget = "branch" | "tag";
@@ -304,6 +305,12 @@ export interface RawRepoFileOverride {
304
305
  vars?: Record<string, string>;
305
306
  deleteOrphaned?: boolean;
306
307
  }
308
+ export interface RawRootSettings {
309
+ rulesets?: Record<string, Ruleset | false>;
310
+ repo?: GitHubRepoSettings | false;
311
+ labels?: Record<string, Label | false>;
312
+ deleteOrphaned?: boolean;
313
+ }
307
314
  export interface RawRepoSettings {
308
315
  rulesets?: Record<string, Ruleset | false> & {
309
316
  inherit?: boolean;
@@ -334,7 +341,7 @@ export interface RawConfig {
334
341
  prTemplate?: string;
335
342
  githubHosts?: string[];
336
343
  deleteOrphaned?: boolean;
337
- settings?: RawRepoSettings;
344
+ settings?: RawRootSettings;
338
345
  }
339
346
  export interface FileContent {
340
347
  fileName: string;
@@ -1,4 +1,4 @@
1
- import type { RawConfig, RawRepoSettings } from "./types.js";
1
+ import type { RawConfig, RawRepoSettings, RawRootSettings } from "./types.js";
2
2
  /**
3
3
  * Validates settings object containing rulesets, labels, and repo settings.
4
4
  */
@@ -16,7 +16,7 @@ export declare function validateForSync(config: RawConfig): void;
16
16
  /**
17
17
  * Checks if settings contain actionable configuration.
18
18
  */
19
- export declare function hasActionableSettings(settings: RawRepoSettings | undefined): boolean;
19
+ export declare function hasActionableSettings(settings: RawRootSettings | RawRepoSettings | undefined): boolean;
20
20
  /**
21
21
  * Validates that config is suitable for the settings command.
22
22
  * @throws Error if no settings are defined or no actionable settings exist
@@ -274,6 +274,17 @@ export function validateRawConfig(config) {
274
274
  }
275
275
  }
276
276
  }
277
+ // Validate prOptions.labels if present
278
+ if (config.prOptions?.labels !== undefined) {
279
+ if (!Array.isArray(config.prOptions.labels)) {
280
+ throw new Error("prOptions.labels must be an array of strings");
281
+ }
282
+ for (const label of config.prOptions.labels) {
283
+ if (typeof label !== "string" || label.length === 0) {
284
+ throw new Error("prOptions.labels entries must be non-empty strings");
285
+ }
286
+ }
287
+ }
277
288
  // Validate each repo
278
289
  for (let i = 0; i < config.repos.length; i++) {
279
290
  const repo = config.repos[i];
@@ -1,6 +1,6 @@
1
1
  import { escapeShellArg } from "../shared/shell-utils.js";
2
2
  import { defaultExecutor, } from "../shared/command-executor.js";
3
- import { withRetry } from "../shared/retry-utils.js";
3
+ import { withRetry, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "../shared/retry-utils.js";
4
4
  import { isGitHubRepo, } from "../shared/repo-detector.js";
5
5
  import { logger } from "../shared/logger.js";
6
6
  /**
@@ -304,11 +304,20 @@ export class GitHubLifecycleProvider {
304
304
  const hostnameFlag = getHostnameFlag(repoInfo);
305
305
  const hostnamePart = hostnameFlag ? `${hostnameFlag} ` : "";
306
306
  const apiPath = `repos/${escapeShellArg(repoInfo.owner)}/${escapeShellArg(repoInfo.repo)}`;
307
+ // After repo creation, GitHub may return 404 due to eventual consistency.
308
+ // Exclude 404/not-found from permanent errors so withRetry retries them.
309
+ const postCreatePermanentPatterns = DEFAULT_PERMANENT_ERROR_PATTERNS.filter((p) => !p.test("404 Not Found"));
307
310
  // Get the SHA of the README.md created by --add-readme
308
- const fileInfo = await withRetry(() => this.executor.exec(`${tokenPrefix}gh api ${hostnamePart}${apiPath}/contents/README.md --jq '.sha'`, this.cwd), { retries: this.retries });
311
+ const fileInfo = await withRetry(() => this.executor.exec(`${tokenPrefix}gh api ${hostnamePart}${apiPath}/contents/README.md --jq '.sha'`, this.cwd), {
312
+ retries: this.retries,
313
+ permanentErrorPatterns: postCreatePermanentPatterns,
314
+ });
309
315
  const sha = fileInfo.trim();
310
316
  // Delete the README.md to leave the repo clean
311
317
  await withRetry(() => this.executor.exec(`${tokenPrefix}gh api ${hostnamePart}${apiPath}/contents/README.md ` +
312
- `--method DELETE -f message='Remove initialization file' -f sha=${escapeShellArg(sha)}`, this.cwd), { retries: this.retries });
318
+ `--method DELETE -f message='Remove initialization file' -f sha=${escapeShellArg(sha)}`, this.cwd), {
319
+ retries: this.retries,
320
+ permanentErrorPatterns: postCreatePermanentPatterns,
321
+ });
313
322
  }
314
323
  }
@@ -17,6 +17,7 @@ export class PRMergeHandler {
17
17
  prTemplate: options.prTemplate,
18
18
  executor: options.executor,
19
19
  token: options.token,
20
+ labels: repoConfig.prOptions?.labels,
20
21
  });
21
22
  const mergeMode = repoConfig.prOptions?.merge ?? "auto";
22
23
  let mergeResult;
@@ -114,7 +114,7 @@ export class GitHubPRStrategy extends BasePRStrategy {
114
114
  }
115
115
  }
116
116
  async create(options) {
117
- const { repoInfo, title, body, branchName, baseBranch, workDir, retries = 3, token, } = options;
117
+ const { repoInfo, title, body, branchName, baseBranch, workDir, retries = 3, token, labels, } = options;
118
118
  if (!isGitHubRepo(repoInfo)) {
119
119
  throw new Error("Expected GitHub repository");
120
120
  }
@@ -123,7 +123,13 @@ export class GitHubPRStrategy extends BasePRStrategy {
123
123
  writeFileSync(bodyFile, body, "utf-8");
124
124
  // Token is passed via env var to avoid shell injection
125
125
  const tokenEnv = buildTokenEnv(token);
126
- const command = `gh pr create --title ${escapeShellArg(title)} --body-file ${escapeShellArg(bodyFile)} --base ${escapeShellArg(baseBranch)} --head ${escapeShellArg(branchName)}`;
126
+ let command = `gh pr create --title ${escapeShellArg(title)} --body-file ${escapeShellArg(bodyFile)} --base ${escapeShellArg(baseBranch)} --head ${escapeShellArg(branchName)}`;
127
+ // Append label flags
128
+ if (labels && labels.length > 0) {
129
+ for (const label of labels) {
130
+ command += ` --label ${escapeShellArg(label)}`;
131
+ }
132
+ }
127
133
  try {
128
134
  const result = await withRetry(() => this.executor.exec(command, workDir, { env: tokenEnv }), { retries });
129
135
  // Extract URL from output - use strict regex for valid PR URLs only
@@ -21,6 +21,8 @@ export interface PROptions {
21
21
  executor?: ICommandExecutor;
22
22
  /** GitHub App installation token for authentication */
23
23
  token?: string;
24
+ /** Labels to apply to the created PR */
25
+ labels?: string[];
24
26
  }
25
27
  export interface PRResult {
26
28
  url?: string;
@@ -97,7 +97,7 @@ export function formatPRTitle(files) {
97
97
  return `chore: sync ${changedFiles.length} config files`;
98
98
  }
99
99
  export async function createPR(options) {
100
- const { repoInfo, branchName, baseBranch, files, workDir, dryRun, retries, prTemplate, executor, token, } = options;
100
+ const { repoInfo, branchName, baseBranch, files, workDir, dryRun, retries, prTemplate, executor, token, labels, } = options;
101
101
  const title = formatPRTitle(files);
102
102
  const body = formatPRBody(files, repoInfo, prTemplate);
103
103
  if (dryRun) {
@@ -117,6 +117,7 @@ export async function createPR(options) {
117
117
  workDir,
118
118
  retries,
119
119
  token,
120
+ labels,
120
121
  });
121
122
  }
122
123
  export async function mergePR(options) {
@@ -25,6 +25,8 @@ export interface PRStrategyOptions {
25
25
  retries?: number;
26
26
  /** GitHub App installation token for authentication */
27
27
  token?: string;
28
+ /** Labels to apply to the created PR */
29
+ labels?: string[];
28
30
  }
29
31
  export interface MergeOptions {
30
32
  prUrl: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspruyt/xfg",
3
- "version": "3.9.14",
3
+ "version": "3.10.0",
4
4
  "description": "Manage files, settings, and repositories across GitHub, Azure DevOps, and GitLab — declaratively, from a single YAML config",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",