@aspruyt/xfg 3.9.6 → 3.9.9

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.
@@ -167,6 +167,7 @@ async function processRulesets(repos, config, options, processor, repoProcessor,
167
167
  }
168
168
  else {
169
169
  logger.error(i + 1, repoName, result.message);
170
+ collector.appendError(repoName, result.message);
170
171
  }
171
172
  results.push({
172
173
  repoName,
@@ -240,6 +241,7 @@ async function processRepoSettings(repos, config, options, processorFactory, res
240
241
  }
241
242
  else {
242
243
  logger.error(current, repoName, result.message);
244
+ collector.appendError(repoName, result.message);
243
245
  }
244
246
  if (!result.skipped) {
245
247
  const existing = results.find((r) => r.repoName === repoName);
@@ -78,7 +78,10 @@ export class GitHubRepoSettingsStrategy {
78
78
  const github = repoInfo;
79
79
  const endpoint = `/repos/${github.owner}/${github.repo}`;
80
80
  const result = await this.ghApi("GET", endpoint, undefined, options);
81
- const settings = JSON.parse(result);
81
+ const parsed = JSON.parse(result);
82
+ const settings = parsed;
83
+ // Extract owner type from nested API response
84
+ settings.owner_type = parsed.owner?.type;
82
85
  // Fetch security settings from separate endpoints
83
86
  settings.vulnerability_alerts = await this.getVulnerabilityAlerts(github, options);
84
87
  // Pass vulnerability_alerts state - automated security fixes requires it enabled
@@ -156,9 +159,18 @@ export class GitHubRepoSettingsStrategy {
156
159
  }
157
160
  async getPrivateVulnerabilityReporting(github, options) {
158
161
  const endpoint = `/repos/${github.owner}/${github.repo}/private-vulnerability-reporting`;
159
- const result = await this.ghApi("GET", endpoint, undefined, options);
160
- const data = JSON.parse(result);
161
- return data.enabled === true;
162
+ try {
163
+ const result = await this.ghApi("GET", endpoint, undefined, options);
164
+ const data = JSON.parse(result);
165
+ return data.enabled === true;
166
+ }
167
+ catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
169
+ if (message.includes("HTTP 404")) {
170
+ return false; // 404 = not available (e.g. private repos)
171
+ }
172
+ throw error; // Re-throw other errors
173
+ }
162
174
  }
163
175
  validateGitHub(repoInfo) {
164
176
  if (!isGitHubRepo(repoInfo)) {
@@ -28,6 +28,11 @@ export declare class RepoSettingsProcessor implements IRepoSettingsProcessor {
28
28
  constructor(strategy?: IRepoSettingsStrategy);
29
29
  process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RepoSettingsProcessorOptions): Promise<RepoSettingsProcessorResult>;
30
30
  private applyChanges;
31
+ /**
32
+ * Validates that desired security settings are compatible with the repo's
33
+ * visibility and owner type. Returns error messages for incompatible settings.
34
+ */
35
+ private validateSecuritySettings;
31
36
  /**
32
37
  * Resolves a GitHub App installation token for the given repo.
33
38
  * Returns undefined if no token manager or token resolution fails.
@@ -45,6 +45,15 @@ export class RepoSettingsProcessor {
45
45
  const strategyOptions = { token: effectiveToken, host: githubRepo.host };
46
46
  // Fetch current settings
47
47
  const currentSettings = await this.strategy.getSettings(githubRepo, strategyOptions);
48
+ // Validate security settings compatibility
49
+ const securityErrors = this.validateSecuritySettings(desiredSettings, currentSettings);
50
+ if (securityErrors.length > 0) {
51
+ return {
52
+ success: false,
53
+ repoName,
54
+ message: `Failed: ${securityErrors.join("; ")}`,
55
+ };
56
+ }
48
57
  // Compute diff
49
58
  const changes = diffRepoSettings(currentSettings, desiredSettings);
50
59
  if (!hasChanges(changes)) {
@@ -117,6 +126,35 @@ export class RepoSettingsProcessor {
117
126
  await this.strategy.setAutomatedSecurityFixes(repoInfo, automatedSecurityFixes, options);
118
127
  }
119
128
  }
129
+ /**
130
+ * Validates that desired security settings are compatible with the repo's
131
+ * visibility and owner type. Returns error messages for incompatible settings.
132
+ */
133
+ validateSecuritySettings(desiredSettings, currentSettings) {
134
+ const errors = [];
135
+ const isPublic = currentSettings.visibility === "public";
136
+ // privateVulnerabilityReporting is only available on public repos
137
+ if (desiredSettings.privateVulnerabilityReporting === true && !isPublic) {
138
+ errors.push("privateVulnerabilityReporting is only available for public repositories");
139
+ }
140
+ // secretScanning and secretScanningPushProtection:
141
+ // - Available on public repos (free)
142
+ // - Available on org private/internal repos with GHAS (security_and_analysis is populated)
143
+ // - NOT available on user private repos or org private/internal repos without GHAS
144
+ if (!isPublic) {
145
+ const isUserOwned = currentSettings.owner_type === "User";
146
+ const hasGHAS = currentSettings.security_and_analysis != null;
147
+ if (desiredSettings.secretScanning === true &&
148
+ (isUserOwned || !hasGHAS)) {
149
+ errors.push("secretScanning requires GitHub Advanced Security (not available for this repository)");
150
+ }
151
+ if (desiredSettings.secretScanningPushProtection === true &&
152
+ (isUserOwned || !hasGHAS)) {
153
+ errors.push("secretScanningPushProtection requires GitHub Advanced Security (not available for this repository)");
154
+ }
155
+ }
156
+ return errors;
157
+ }
120
158
  /**
121
159
  * Resolves a GitHub App installation token for the given repo.
122
160
  * Returns undefined if no token manager or token resolution fails.
@@ -39,6 +39,7 @@ export interface CurrentRepoSettings {
39
39
  status: string;
40
40
  };
41
41
  };
42
+ owner_type?: "User" | "Organization";
42
43
  vulnerability_alerts?: boolean;
43
44
  automated_security_fixes?: boolean;
44
45
  private_vulnerability_reporting?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspruyt/xfg",
3
- "version": "3.9.6",
3
+ "version": "3.9.9",
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",