reposets 0.3.0 → 0.4.1

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.
package/500.js CHANGED
@@ -1037,6 +1037,24 @@ const GroupSchema = Schema.Struct({
1037
1037
  ]
1038
1038
  ]
1039
1039
  })),
1040
+ security: Schema.optional(Schema.Array(Schema.String).annotations({
1041
+ title: "Security groups",
1042
+ description: "Names of security groups (vulnerability alerts, automated security fixes, private vulnerability reporting) to apply to these repos",
1043
+ examples: [
1044
+ [
1045
+ "oss-defaults"
1046
+ ]
1047
+ ]
1048
+ })),
1049
+ code_scanning: Schema.optional(Schema.Array(Schema.String).annotations({
1050
+ title: "Code scanning groups",
1051
+ description: "Names of code_scanning groups (CodeQL default setup) to apply to these repos",
1052
+ examples: [
1053
+ [
1054
+ "oss-defaults"
1055
+ ]
1056
+ ]
1057
+ })),
1040
1058
  cleanup: Schema.optional(CleanupSchema)
1041
1059
  }).annotations({
1042
1060
  identifier: "Group",
@@ -1072,6 +1090,97 @@ const MergeCommitMessageSchema = Schema.Literal("PR_BODY", "PR_TITLE", "BLANK").
1072
1090
  title: "Merge commit message",
1073
1091
  description: "Default message body for merge commits: PR_BODY uses the pull request body, PR_TITLE uses the PR title, BLANK leaves it empty"
1074
1092
  });
1093
+ const SecurityAndAnalysisStatusSchema = Schema.Literal("enabled", "disabled").annotations({
1094
+ identifier: "SecurityAndAnalysisStatus",
1095
+ title: "Security feature status",
1096
+ description: 'Whether the security feature is "enabled" or "disabled"'
1097
+ });
1098
+ const DelegatedBypassReviewerModeSchema = Schema.Literal("ALWAYS", "EXEMPT").annotations({
1099
+ identifier: "DelegatedBypassReviewerMode",
1100
+ title: "Delegated bypass reviewer mode",
1101
+ description: "ALWAYS: reviewer is always required to approve bypass; EXEMPT: reviewer can bypass without review"
1102
+ });
1103
+ const DelegatedBypassReviewerSchema = Schema.Union(Schema.Struct({
1104
+ team: Schema.String.annotations({
1105
+ title: "Team slug",
1106
+ description: 'GitHub team slug (e.g., "security-team"); resolved to numeric reviewer_id at sync time',
1107
+ examples: [
1108
+ "security-team"
1109
+ ]
1110
+ }),
1111
+ mode: Schema.optional(DelegatedBypassReviewerModeSchema)
1112
+ }), Schema.Struct({
1113
+ role: Schema.String.annotations({
1114
+ title: "Organization role name",
1115
+ description: 'Organization role name as defined in `GET /orgs/{org}/organization-roles` (e.g., "all_repo_admin", "security_manager"). Resolved to the numeric role ID at sync time.',
1116
+ examples: [
1117
+ "all_repo_admin",
1118
+ "all_repo_maintain",
1119
+ "security_manager"
1120
+ ]
1121
+ }),
1122
+ mode: Schema.optional(DelegatedBypassReviewerModeSchema)
1123
+ })).annotations({
1124
+ identifier: "DelegatedBypassReviewer",
1125
+ title: "Delegated bypass reviewer",
1126
+ description: "A reviewer who can approve secret-scanning push-protection bypass requests. Must specify exactly one of team or role."
1127
+ });
1128
+ const SecurityAndAnalysisSchema = Schema.Struct({
1129
+ advanced_security: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
1130
+ title: "GitHub Advanced Security",
1131
+ description: "(GHAS-licensed) Master toggle for GitHub Advanced Security features. Free on public repos; requires a GHAS license on private repos."
1132
+ })),
1133
+ code_security: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
1134
+ title: "GitHub Code Security",
1135
+ description: "(GHAS-licensed) Toggle GitHub Code Security functionality."
1136
+ })),
1137
+ secret_scanning: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
1138
+ title: "Secret scanning",
1139
+ description: "Detect exposed credentials and sensitive data committed to the repository."
1140
+ })),
1141
+ secret_scanning_push_protection: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
1142
+ title: "Secret scanning push protection",
1143
+ description: "Block git pushes that contain detected secrets."
1144
+ })),
1145
+ secret_scanning_ai_detection: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
1146
+ title: "Secret scanning AI detection",
1147
+ description: "(GHAS-licensed) AI-powered detection of generic secrets beyond standard provider patterns."
1148
+ })),
1149
+ secret_scanning_non_provider_patterns: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
1150
+ title: "Secret scanning non-provider patterns",
1151
+ description: "(GHAS-licensed) Detect custom secret patterns beyond the standard provider list."
1152
+ })),
1153
+ secret_scanning_delegated_alert_dismissal: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
1154
+ title: "Delegated alert dismissal",
1155
+ description: "(org-only) Allow delegated dismissal of secret scanning alerts."
1156
+ })),
1157
+ secret_scanning_delegated_bypass: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
1158
+ title: "Delegated push protection bypass",
1159
+ description: "(org-only) Allow delegated approval of secret scanning push protection bypass requests."
1160
+ })),
1161
+ delegated_bypass_reviewers: Schema.optional(Schema.Array(DelegatedBypassReviewerSchema).annotations({
1162
+ title: "Delegated bypass reviewers",
1163
+ description: "(org-only) Reviewers authorized to approve push protection bypass requests. Each entry must specify a team slug or role name."
1164
+ })),
1165
+ dependabot_security_updates: Schema.optional(SecurityAndAnalysisStatusSchema.annotations({
1166
+ title: "Dependabot security updates",
1167
+ description: "Automatically open pull requests to patch known dependency vulnerabilities."
1168
+ }))
1169
+ }).annotations({
1170
+ identifier: "SecurityAndAnalysis",
1171
+ title: "Security and analysis",
1172
+ description: "GitHub repository security_and_analysis fields applied via the same PATCH /repos call as other settings. (GHAS-licensed) fields require a GHAS license on private repos; (org-only) fields are silently skipped on personal repos.",
1173
+ jsonSchema: {
1174
+ ...tombi({
1175
+ tableKeysOrder: "schema"
1176
+ }),
1177
+ ...taplo({
1178
+ links: {
1179
+ key: "https://github.com/spencerbeggs/reposets/blob/main/docs/configuration.md"
1180
+ }
1181
+ })
1182
+ }
1183
+ });
1075
1184
  const SettingsGroupSchema = Schema.Struct({
1076
1185
  is_template: Schema.optional(Schema.Boolean.annotations({
1077
1186
  title: "Template repository",
@@ -1136,7 +1245,8 @@ const SettingsGroupSchema = Schema.Struct({
1136
1245
  web_commit_signoff_required: Schema.optional(Schema.Boolean.annotations({
1137
1246
  title: "Require commit signoff",
1138
1247
  description: "Require contributors to sign off on web-based commits"
1139
- }))
1248
+ })),
1249
+ security_and_analysis: Schema.optional(SecurityAndAnalysisSchema)
1140
1250
  }, {
1141
1251
  key: Schema.String,
1142
1252
  value: Jsonifiable
@@ -1155,6 +1265,99 @@ const SettingsGroupSchema = Schema.Struct({
1155
1265
  })
1156
1266
  }
1157
1267
  });
1268
+ const SecurityGroupSchema = Schema.Struct({
1269
+ vulnerability_alerts: Schema.optional(Schema.Boolean.annotations({
1270
+ title: "Vulnerability alerts",
1271
+ description: "Enable Dependabot vulnerability alerts (PUT/DELETE /repos/{o}/{r}/vulnerability-alerts)."
1272
+ })),
1273
+ automated_security_fixes: Schema.optional(Schema.Boolean.annotations({
1274
+ title: "Automated security fixes",
1275
+ description: "Enable Dependabot security pull requests (PUT/DELETE /repos/{o}/{r}/automated-security-fixes). Requires vulnerability_alerts to also be enabled."
1276
+ })),
1277
+ private_vulnerability_reporting: Schema.optional(Schema.Boolean.annotations({
1278
+ title: "Private vulnerability reporting",
1279
+ description: "Enable the private vulnerability reporting inbox (PUT/DELETE /repos/{o}/{r}/private-vulnerability-reporting)."
1280
+ }))
1281
+ }).pipe(Schema.filter((group)=>!(true === group.automated_security_fixes && false === group.vulnerability_alerts), {
1282
+ identifier: "SecurityGroup",
1283
+ message: ()=>"automated_security_fixes = true requires vulnerability_alerts to be enabled (or omitted to leave the existing setting in place)"
1284
+ })).annotations({
1285
+ identifier: "SecurityGroup",
1286
+ title: "Security group",
1287
+ description: "Toggles for repository-level security features that have dedicated PUT/DELETE endpoints (vulnerability alerts, automated security fixes, private vulnerability reporting). Omitted keys are left untouched.",
1288
+ jsonSchema: {
1289
+ ...tombi({
1290
+ tableKeysOrder: "schema"
1291
+ }),
1292
+ ...taplo({
1293
+ links: {
1294
+ key: "https://github.com/spencerbeggs/reposets/blob/main/docs/configuration.md"
1295
+ }
1296
+ })
1297
+ }
1298
+ });
1299
+ const CodeScanningLanguageSchema = Schema.Literal("actions", "c-cpp", "csharp", "go", "java-kotlin", "javascript-typescript", "python", "ruby", "swift").annotations({
1300
+ identifier: "CodeScanningLanguage",
1301
+ title: "CodeQL default-setup language",
1302
+ description: "Languages supported by GitHub code scanning default setup. Note: this is narrower than the CodeQL analyzer (Rust is supported by CodeQL but not by default setup)."
1303
+ });
1304
+ const CodeScanningStateSchema = Schema.Literal("configured", "not-configured").annotations({
1305
+ identifier: "CodeScanningState",
1306
+ title: "Default setup state",
1307
+ description: '"configured" enables CodeQL default setup; "not-configured" disables it.'
1308
+ });
1309
+ const CodeScanningQuerySuiteSchema = Schema.Literal("default", "extended").annotations({
1310
+ identifier: "CodeScanningQuerySuite",
1311
+ title: "Query suite",
1312
+ description: '"default" runs the standard query set; "extended" includes additional security queries.'
1313
+ });
1314
+ const CodeScanningThreatModelSchema = Schema.Literal("remote", "remote_and_local").annotations({
1315
+ identifier: "CodeScanningThreatModel",
1316
+ title: "Threat model",
1317
+ description: '"remote" analyzes network sources only; "remote_and_local" also includes filesystem and environment access.'
1318
+ });
1319
+ const CodeScanningRunnerTypeSchema = Schema.Literal("standard", "labeled").annotations({
1320
+ identifier: "CodeScanningRunnerType",
1321
+ title: "Runner type",
1322
+ description: '"standard" uses GitHub-hosted runners; "labeled" uses runners matching runner_label.'
1323
+ });
1324
+ const CodeScanningGroupSchema = Schema.Struct({
1325
+ state: Schema.optional(CodeScanningStateSchema),
1326
+ languages: Schema.optional(Schema.Array(CodeScanningLanguageSchema).annotations({
1327
+ title: "Languages",
1328
+ description: "CodeQL languages to analyze. Languages not detected in the repository are skipped with a warning at sync time.",
1329
+ examples: [
1330
+ [
1331
+ "javascript-typescript",
1332
+ "python"
1333
+ ]
1334
+ ]
1335
+ })),
1336
+ query_suite: Schema.optional(CodeScanningQuerySuiteSchema),
1337
+ threat_model: Schema.optional(CodeScanningThreatModelSchema),
1338
+ runner_type: Schema.optional(CodeScanningRunnerTypeSchema),
1339
+ runner_label: Schema.optional(Schema.String.annotations({
1340
+ title: "Runner label",
1341
+ description: 'Self-hosted runner label. Required when runner_type = "labeled".'
1342
+ }))
1343
+ }).pipe(Schema.filter((group)=>"labeled" !== group.runner_type || void 0 !== group.runner_label, {
1344
+ identifier: "CodeScanningGroup",
1345
+ message: ()=>'runner_label is required when runner_type = "labeled"'
1346
+ })).annotations({
1347
+ identifier: "CodeScanningGroup",
1348
+ title: "Code scanning group",
1349
+ description: "CodeQL default setup configuration applied via PATCH /repos/{o}/{r}/code-scanning/default-setup. The endpoint returns 202 Accepted and configures asynchronously; reposets sends the request and does not poll for completion.",
1350
+ jsonSchema: {
1351
+ ...tombi({
1352
+ tableKeysOrder: "schema"
1353
+ }),
1354
+ ...taplo({
1355
+ links: {
1356
+ key: "https://github.com/spencerbeggs/reposets/blob/main/docs/configuration.md"
1357
+ }
1358
+ })
1359
+ }
1360
+ });
1158
1361
  const LogLevelSchema = Schema.Literal("silent", "info", "verbose", "debug").annotations({
1159
1362
  identifier: "LogLevel",
1160
1363
  title: "Log level",
@@ -1235,12 +1438,36 @@ const ConfigSchema = Schema.Struct({
1235
1438
  }), {
1236
1439
  default: ()=>({})
1237
1440
  }),
1441
+ security: Schema.optionalWith(Schema.Record({
1442
+ key: Schema.String,
1443
+ value: SecurityGroupSchema
1444
+ }).annotations({
1445
+ title: "Security groups",
1446
+ description: "Named security groups for vulnerability alerts, automated security fixes, and private vulnerability reporting",
1447
+ jsonSchema: tombi({
1448
+ additionalKeyLabel: "security_group"
1449
+ })
1450
+ }), {
1451
+ default: ()=>({})
1452
+ }),
1453
+ code_scanning: Schema.optionalWith(Schema.Record({
1454
+ key: Schema.String,
1455
+ value: CodeScanningGroupSchema
1456
+ }).annotations({
1457
+ title: "Code scanning groups",
1458
+ description: "Named code scanning groups for CodeQL default setup configuration",
1459
+ jsonSchema: tombi({
1460
+ additionalKeyLabel: "code_scanning_group"
1461
+ })
1462
+ }), {
1463
+ default: ()=>({})
1464
+ }),
1238
1465
  groups: Schema.Record({
1239
1466
  key: Schema.String,
1240
1467
  value: GroupSchema
1241
1468
  }).annotations({
1242
1469
  title: "Groups",
1243
- description: "Named groups of repositories with their settings, secrets, variables, rulesets, and environment assignments",
1470
+ description: "Named groups of repositories with their settings, secrets, variables, rulesets, environments, security, and code scanning assignments",
1244
1471
  jsonSchema: tombi({
1245
1472
  additionalKeyLabel: "group_name"
1246
1473
  })
@@ -1248,7 +1475,7 @@ const ConfigSchema = Schema.Struct({
1248
1475
  }).annotations({
1249
1476
  identifier: "Config",
1250
1477
  title: "reposets Configuration",
1251
- description: "Configuration for syncing GitHub repository settings, secrets, variables, rulesets, and deployment environments",
1478
+ description: "Configuration for syncing GitHub repository settings, secrets, variables, rulesets, deployment environments, advanced security toggles, and CodeQL default setup",
1252
1479
  jsonSchema: {
1253
1480
  ...tombi({
1254
1481
  tableKeysOrder: "schema"
@@ -1374,6 +1601,8 @@ function validateConfigRefs(config) {
1374
1601
  const definedVariables = new Set(Object.keys(config.variables));
1375
1602
  const definedRulesets = new Set(Object.keys(config.rulesets));
1376
1603
  const definedEnvironments = new Set(Object.keys(config.environments));
1604
+ const definedSecurity = new Set(Object.keys(config.security));
1605
+ const definedCodeScanning = new Set(Object.keys(config.code_scanning));
1377
1606
  for (const [groupName, group] of Object.entries(config.groups)){
1378
1607
  if (group.settings) {
1379
1608
  for (const ref of group.settings)if (!definedSettings.has(ref)) errors.push(`group '${groupName}': unknown settings group '${ref}'`);
@@ -1384,6 +1613,12 @@ function validateConfigRefs(config) {
1384
1613
  if (group.environments) {
1385
1614
  for (const ref of group.environments)if (!definedEnvironments.has(ref)) errors.push(`group '${groupName}': unknown environment '${ref}'`);
1386
1615
  }
1616
+ if (group.security) {
1617
+ for (const ref of group.security)if (!definedSecurity.has(ref)) errors.push(`group '${groupName}': unknown security group '${ref}'`);
1618
+ }
1619
+ if (group.code_scanning) {
1620
+ for (const ref of group.code_scanning)if (!definedCodeScanning.has(ref)) errors.push(`group '${groupName}': unknown code_scanning group '${ref}'`);
1621
+ }
1387
1622
  if (group.secrets) {
1388
1623
  if (group.secrets.actions) {
1389
1624
  for (const ref of group.secrets.actions)if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': unknown secrets group '${ref}'`);
@@ -1537,6 +1772,31 @@ class GitHubClient extends Context.Tag("GitHubClient")() {
1537
1772
  const ORG_ONLY_SETTINGS = new Set([
1538
1773
  "allow_forking"
1539
1774
  ]);
1775
+ const SAA_STATUS_FIELDS = new Set([
1776
+ "advanced_security",
1777
+ "code_security",
1778
+ "secret_scanning",
1779
+ "secret_scanning_push_protection",
1780
+ "secret_scanning_ai_detection",
1781
+ "secret_scanning_non_provider_patterns",
1782
+ "secret_scanning_delegated_alert_dismissal",
1783
+ "secret_scanning_delegated_bypass",
1784
+ "dependabot_security_updates"
1785
+ ]);
1786
+ function transformSecurityAndAnalysis(value) {
1787
+ if (null === value || "object" != typeof value) return;
1788
+ const input = value;
1789
+ const out = {};
1790
+ for (const [key, raw] of Object.entries(input))if (void 0 !== raw) {
1791
+ if (SAA_STATUS_FIELDS.has(key) && ("enabled" === raw || "disabled" === raw)) out[key] = {
1792
+ status: raw
1793
+ };
1794
+ else if ("delegated_bypass_reviewers" === key && Array.isArray(raw) && raw.length > 0) out.secret_scanning_delegated_bypass_options = {
1795
+ reviewers: raw
1796
+ };
1797
+ }
1798
+ return Object.keys(out).length > 0 ? out : void 0;
1799
+ }
1540
1800
  const GRAPHQL_SETTINGS = {
1541
1801
  has_sponsorships: "hasSponsorshipsEnabled",
1542
1802
  has_pull_requests: "hasPullRequestsEnabled"
@@ -1563,6 +1823,8 @@ function GitHubClientLive(token) {
1563
1823
  });
1564
1824
  }
1565
1825
  const ownerTypeCache = new Map();
1826
+ const teamIdCache = new Map();
1827
+ const roleIdCache = new Map();
1566
1828
  return {
1567
1829
  getOwnerType (owner) {
1568
1830
  return Effect.tryPromise({
@@ -1656,6 +1918,11 @@ function GitHubClientLive(token) {
1656
1918
  const restSettings = {};
1657
1919
  const graphqlInput = {};
1658
1920
  for (const [key, value] of Object.entries(settings)){
1921
+ if ("security_and_analysis" === key) {
1922
+ const saa = transformSecurityAndAnalysis(value);
1923
+ if (void 0 !== saa) restSettings.security_and_analysis = saa;
1924
+ continue;
1925
+ }
1659
1926
  const graphqlField = GRAPHQL_SETTINGS[key];
1660
1927
  if (void 0 !== graphqlField) graphqlInput[graphqlField] = value;
1661
1928
  else restSettings[key] = value;
@@ -1982,6 +2249,160 @@ function GitHubClientLive(token) {
1982
2249
  },
1983
2250
  catch: wrapError
1984
2251
  });
2252
+ },
2253
+ getVulnerabilityAlerts (owner, repo) {
2254
+ return Effect.tryPromise({
2255
+ try: async ()=>{
2256
+ try {
2257
+ await octokit.request("GET /repos/{owner}/{repo}/vulnerability-alerts", {
2258
+ owner,
2259
+ repo
2260
+ });
2261
+ return true;
2262
+ } catch (error) {
2263
+ const status = error.status;
2264
+ if (404 === status) return false;
2265
+ throw error;
2266
+ }
2267
+ },
2268
+ catch: wrapError
2269
+ });
2270
+ },
2271
+ setVulnerabilityAlerts (owner, repo, enabled) {
2272
+ return Effect.tryPromise({
2273
+ try: async ()=>{
2274
+ if (enabled) await octokit.request("PUT /repos/{owner}/{repo}/vulnerability-alerts", {
2275
+ owner,
2276
+ repo
2277
+ });
2278
+ else await octokit.request("DELETE /repos/{owner}/{repo}/vulnerability-alerts", {
2279
+ owner,
2280
+ repo
2281
+ });
2282
+ },
2283
+ catch: wrapError
2284
+ });
2285
+ },
2286
+ getAutomatedSecurityFixes (owner, repo) {
2287
+ return Effect.tryPromise({
2288
+ try: async ()=>{
2289
+ const { data } = await octokit.request("GET /repos/{owner}/{repo}/automated-security-fixes", {
2290
+ owner,
2291
+ repo
2292
+ });
2293
+ return Boolean(data.enabled);
2294
+ },
2295
+ catch: wrapError
2296
+ });
2297
+ },
2298
+ setAutomatedSecurityFixes (owner, repo, enabled) {
2299
+ return Effect.tryPromise({
2300
+ try: async ()=>{
2301
+ if (enabled) await octokit.request("PUT /repos/{owner}/{repo}/automated-security-fixes", {
2302
+ owner,
2303
+ repo
2304
+ });
2305
+ else await octokit.request("DELETE /repos/{owner}/{repo}/automated-security-fixes", {
2306
+ owner,
2307
+ repo
2308
+ });
2309
+ },
2310
+ catch: wrapError
2311
+ });
2312
+ },
2313
+ getPrivateVulnerabilityReporting (owner, repo) {
2314
+ return Effect.tryPromise({
2315
+ try: async ()=>{
2316
+ const { data } = await octokit.request("GET /repos/{owner}/{repo}/private-vulnerability-reporting", {
2317
+ owner,
2318
+ repo
2319
+ });
2320
+ return Boolean(data.enabled);
2321
+ },
2322
+ catch: wrapError
2323
+ });
2324
+ },
2325
+ setPrivateVulnerabilityReporting (owner, repo, enabled) {
2326
+ return Effect.tryPromise({
2327
+ try: async ()=>{
2328
+ if (enabled) await octokit.request("PUT /repos/{owner}/{repo}/private-vulnerability-reporting", {
2329
+ owner,
2330
+ repo
2331
+ });
2332
+ else await octokit.request("DELETE /repos/{owner}/{repo}/private-vulnerability-reporting", {
2333
+ owner,
2334
+ repo
2335
+ });
2336
+ },
2337
+ catch: wrapError
2338
+ });
2339
+ },
2340
+ updateCodeScanningDefaultSetup (owner, repo, config) {
2341
+ return Effect.tryPromise({
2342
+ try: async ()=>{
2343
+ const body = {};
2344
+ if (void 0 !== config.state) body.state = config.state;
2345
+ if (void 0 !== config.languages) body.languages = [
2346
+ ...config.languages
2347
+ ];
2348
+ if (void 0 !== config.query_suite) body.query_suite = config.query_suite;
2349
+ if (void 0 !== config.threat_model) body.threat_model = config.threat_model;
2350
+ if (void 0 !== config.runner_type) body.runner_type = config.runner_type;
2351
+ if (void 0 !== config.runner_label) body.runner_label = config.runner_label;
2352
+ await octokit.request("PATCH /repos/{owner}/{repo}/code-scanning/default-setup", {
2353
+ owner,
2354
+ repo,
2355
+ ...body
2356
+ });
2357
+ },
2358
+ catch: wrapError
2359
+ });
2360
+ },
2361
+ listRepoLanguages (owner, repo) {
2362
+ return Effect.tryPromise({
2363
+ try: async ()=>{
2364
+ const { data } = await octokit.repos.listLanguages({
2365
+ owner,
2366
+ repo
2367
+ });
2368
+ return Object.keys(data);
2369
+ },
2370
+ catch: wrapError
2371
+ });
2372
+ },
2373
+ resolveTeamId (org, slug) {
2374
+ return Effect.tryPromise({
2375
+ try: async ()=>{
2376
+ const cacheKey = `${org}:${slug}`;
2377
+ const cached = teamIdCache.get(cacheKey);
2378
+ if (void 0 !== cached) return cached;
2379
+ const { data } = await octokit.teams.getByName({
2380
+ org,
2381
+ team_slug: slug
2382
+ });
2383
+ teamIdCache.set(cacheKey, data.id);
2384
+ return data.id;
2385
+ },
2386
+ catch: wrapError
2387
+ });
2388
+ },
2389
+ resolveRoleId (org, name) {
2390
+ return Effect.tryPromise({
2391
+ try: async ()=>{
2392
+ const cacheKey = `${org}:${name}`;
2393
+ const cached = roleIdCache.get(cacheKey);
2394
+ if (void 0 !== cached) return cached;
2395
+ const { data } = await octokit.request("GET /orgs/{org}/organization-roles", {
2396
+ org
2397
+ });
2398
+ const roles = data.roles ?? [];
2399
+ const role = roles.find((r)=>r.name === name);
2400
+ if (!role) throw new Error(`organization role '${name}' not found in '${org}' (available: ${roles.map((r)=>r.name).join(", ") || "none"})`);
2401
+ roleIdCache.set(cacheKey, role.id);
2402
+ return role.id;
2403
+ },
2404
+ catch: wrapError
2405
+ });
1985
2406
  }
1986
2407
  };
1987
2408
  })());
@@ -2158,6 +2579,82 @@ function GitHubClientTest() {
2158
2579
  }
2159
2580
  });
2160
2581
  return Effect["void"];
2582
+ },
2583
+ getVulnerabilityAlerts (_owner, _repo) {
2584
+ return Effect.succeed(false);
2585
+ },
2586
+ setVulnerabilityAlerts (owner, repo, enabled) {
2587
+ recorded.push({
2588
+ method: "setVulnerabilityAlerts",
2589
+ args: {
2590
+ owner,
2591
+ repo,
2592
+ enabled
2593
+ }
2594
+ });
2595
+ return Effect["void"];
2596
+ },
2597
+ getAutomatedSecurityFixes (_owner, _repo) {
2598
+ return Effect.succeed(false);
2599
+ },
2600
+ setAutomatedSecurityFixes (owner, repo, enabled) {
2601
+ recorded.push({
2602
+ method: "setAutomatedSecurityFixes",
2603
+ args: {
2604
+ owner,
2605
+ repo,
2606
+ enabled
2607
+ }
2608
+ });
2609
+ return Effect["void"];
2610
+ },
2611
+ getPrivateVulnerabilityReporting (_owner, _repo) {
2612
+ return Effect.succeed(false);
2613
+ },
2614
+ setPrivateVulnerabilityReporting (owner, repo, enabled) {
2615
+ recorded.push({
2616
+ method: "setPrivateVulnerabilityReporting",
2617
+ args: {
2618
+ owner,
2619
+ repo,
2620
+ enabled
2621
+ }
2622
+ });
2623
+ return Effect["void"];
2624
+ },
2625
+ updateCodeScanningDefaultSetup (owner, repo, config) {
2626
+ recorded.push({
2627
+ method: "updateCodeScanningDefaultSetup",
2628
+ args: {
2629
+ owner,
2630
+ repo,
2631
+ config
2632
+ }
2633
+ });
2634
+ return Effect["void"];
2635
+ },
2636
+ listRepoLanguages (_owner, _repo) {
2637
+ return Effect.succeed([]);
2638
+ },
2639
+ resolveTeamId (org, slug) {
2640
+ recorded.push({
2641
+ method: "resolveTeamId",
2642
+ args: {
2643
+ org,
2644
+ slug
2645
+ }
2646
+ });
2647
+ return Effect.succeed(0);
2648
+ },
2649
+ resolveRoleId (org, name) {
2650
+ recorded.push({
2651
+ method: "resolveRoleId",
2652
+ args: {
2653
+ org,
2654
+ name
2655
+ }
2656
+ });
2657
+ return Effect.succeed(0);
2161
2658
  }
2162
2659
  });
2163
2660
  return {
@@ -2172,6 +2669,8 @@ class SyncLogger extends Context.Tag("SyncLogger")() {
2172
2669
  function pluralize(resource, count) {
2173
2670
  if (1 === count) return resource;
2174
2671
  if ("ruleset" === resource) return "rulesets";
2672
+ if ("security feature" === resource) return "security features";
2673
+ if ("code scanning" === resource) return "code scanning";
2175
2674
  return `${resource}s`;
2176
2675
  }
2177
2676
  function SyncLoggerLive(config) {
@@ -2278,6 +2777,24 @@ function SyncLoggerLive(config) {
2278
2777
  };
2279
2778
  }));
2280
2779
  }
2780
+ const ORG_ONLY_SAA_FIELDS = new Set([
2781
+ "secret_scanning_delegated_alert_dismissal",
2782
+ "secret_scanning_delegated_bypass",
2783
+ "delegated_bypass_reviewers"
2784
+ ]);
2785
+ const REPO_LANG_TO_CODEQL = {
2786
+ JavaScript: "javascript-typescript",
2787
+ TypeScript: "javascript-typescript",
2788
+ C: "c-cpp",
2789
+ "C++": "c-cpp",
2790
+ "C#": "csharp",
2791
+ Go: "go",
2792
+ Java: "java-kotlin",
2793
+ Kotlin: "java-kotlin",
2794
+ Python: "python",
2795
+ Ruby: "ruby",
2796
+ Swift: "swift"
2797
+ };
2281
2798
  class SyncEngine extends Context.Tag("SyncEngine")() {
2282
2799
  }
2283
2800
  function isCleanupActive(scope) {
@@ -2335,6 +2852,29 @@ function groupEntryNames(group) {
2335
2852
  if ("resolved" in group) return Object.keys(group.resolved);
2336
2853
  return [];
2337
2854
  }
2855
+ function mergeSecurityAndAnalysis(blocks) {
2856
+ const merged = {};
2857
+ let hasAny = false;
2858
+ for (const block of blocks)if (block) {
2859
+ hasAny = true;
2860
+ for (const [key, value] of Object.entries(block))if (void 0 !== value) merged[key] = value;
2861
+ }
2862
+ return hasAny ? merged : void 0;
2863
+ }
2864
+ function mergeSecurityGroups(groups) {
2865
+ const merged = {};
2866
+ for (const group of groups)if (group) {
2867
+ for (const [key, value] of Object.entries(group))if ("boolean" == typeof value) merged[key] = value;
2868
+ }
2869
+ return merged;
2870
+ }
2871
+ function mergeCodeScanningGroups(groups) {
2872
+ const merged = {};
2873
+ for (const group of groups)if (group) {
2874
+ for (const [key, value] of Object.entries(group))if (void 0 !== value) merged[key] = value;
2875
+ }
2876
+ return merged;
2877
+ }
2338
2878
  const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2339
2879
  const github = yield* GitHubClient;
2340
2880
  const credResolver = yield* CredentialResolver;
@@ -2459,9 +2999,14 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2459
2999
  const settingGroupRefs = group.settings ?? [];
2460
3000
  const mergedSettings = {};
2461
3001
  const skippedSettings = [];
3002
+ const saaBlocks = [];
2462
3003
  for (const ref of settingGroupRefs){
2463
3004
  const settingGroup = config.settings[ref];
2464
- if (settingGroup) Object.assign(mergedSettings, settingGroup);
3005
+ if (settingGroup) {
3006
+ const { security_and_analysis, ...rest } = settingGroup;
3007
+ Object.assign(mergedSettings, rest);
3008
+ saaBlocks.push(security_and_analysis);
3009
+ }
2465
3010
  }
2466
3011
  if ("User" === ownerType) {
2467
3012
  for (const key of ORG_ONLY_SETTINGS)if (key in mergedSettings) {
@@ -2469,6 +3014,58 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2469
3014
  skippedSettings.push(key);
2470
3015
  }
2471
3016
  }
3017
+ const mergedSAA = mergeSecurityAndAnalysis(saaBlocks);
3018
+ const skippedSAA = [];
3019
+ if (mergedSAA) {
3020
+ const saaOut = {
3021
+ ...mergedSAA
3022
+ };
3023
+ if ("User" === ownerType) {
3024
+ for (const key of ORG_ONLY_SAA_FIELDS)if (key in saaOut) {
3025
+ delete saaOut[key];
3026
+ skippedSAA.push(key);
3027
+ }
3028
+ } else {
3029
+ const reviewers = saaOut.delegated_bypass_reviewers;
3030
+ if (Array.isArray(reviewers)) {
3031
+ const resolved = [];
3032
+ for (const reviewer of reviewers)if ("string" == typeof reviewer.team) {
3033
+ const teamId = yield* github.resolveTeamId(owner, reviewer.team).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
3034
+ yield* logger.syncError(`resolve team '${reviewer.team}'`, err.message);
3035
+ })));
3036
+ if (void 0 !== teamId) {
3037
+ const entry = {
3038
+ reviewer_id: teamId,
3039
+ reviewer_type: "TEAM"
3040
+ };
3041
+ if (void 0 !== reviewer.mode) entry.mode = reviewer.mode;
3042
+ resolved.push(entry);
3043
+ }
3044
+ } else if ("string" == typeof reviewer.role) {
3045
+ const roleId = yield* github.resolveRoleId(owner, reviewer.role).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
3046
+ yield* logger.syncError(`resolve role '${reviewer.role}'`, err.message);
3047
+ })));
3048
+ if (void 0 !== roleId) {
3049
+ const entry = {
3050
+ reviewer_id: roleId,
3051
+ reviewer_type: "ROLE"
3052
+ };
3053
+ if (void 0 !== reviewer.mode) entry.mode = reviewer.mode;
3054
+ resolved.push(entry);
3055
+ }
3056
+ }
3057
+ saaOut.delegated_bypass_reviewers = resolved;
3058
+ }
3059
+ }
3060
+ if (Object.keys(saaOut).length > 0) mergedSettings.security_and_analysis = saaOut;
3061
+ }
3062
+ const securityGroupRefs = group.security ?? [];
3063
+ const mergedSecurity = mergeSecurityGroups(securityGroupRefs.map((ref)=>config.security[ref]));
3064
+ const codeScanningRefs = group.code_scanning ?? [];
3065
+ const mergedCodeScanning = mergeCodeScanningGroups(codeScanningRefs.map((ref)=>config.code_scanning[ref]));
3066
+ const hasSecurity = Object.keys(mergedSecurity).length > 0;
3067
+ const securityContradiction = true === mergedSecurity.automated_security_fixes && false === mergedSecurity.vulnerability_alerts;
3068
+ const hasCodeScanning = Object.keys(mergedCodeScanning).length > 0;
2472
3069
  const hasSecrets = secretScopes.some((s)=>(resolvedSecrets.get(s)?.size ?? 0) > 0);
2473
3070
  const hasVariables = resolvedVariables.size > 0;
2474
3071
  const hasRulesets = rulesetMap.size > 0;
@@ -2478,7 +3075,7 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2478
3075
  const hasEnvVariables = resolvedEnvVariables.size > 0;
2479
3076
  const hasCleanup = !noCleanup && (isCleanupActive(effectiveCleanup.secrets.actions) || isCleanupActive(effectiveCleanup.secrets.dependabot) || isCleanupActive(effectiveCleanup.secrets.codespaces) || isCleanupActive(effectiveCleanup.secrets.environments) || isCleanupActive(effectiveCleanup.variables.actions) || isCleanupActive(effectiveCleanup.variables.environments) || isCleanupActive(effectiveCleanup.rulesets) || isCleanupActive(effectiveCleanup.environments));
2480
3077
  for (const repoName of group.repos)if (!repoFilter || repoName === repoFilter) {
2481
- if (!hasSecrets && !hasVariables && !hasRulesets && !hasSettings && !hasEnvironments && !hasEnvSecrets && !hasEnvVariables && !hasCleanup) {
3078
+ if (!hasSecrets && !hasVariables && !hasRulesets && !hasSettings && !hasEnvironments && !hasEnvSecrets && !hasEnvVariables && !hasSecurity && !hasCodeScanning && !hasCleanup) {
2482
3079
  yield* logger.repoSkip(owner, repoName, "no changes configured");
2483
3080
  continue;
2484
3081
  }
@@ -2489,6 +3086,66 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2489
3086
  yield* github.syncSettings(owner, repoName, mergedSettings).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError("settings", err.message)));
2490
3087
  }
2491
3088
  for (const key of skippedSettings)yield* logger.syncOperation("skip", "setting", key, "(org-only, owner is a personal account)");
3089
+ for (const key of skippedSAA)yield* logger.syncOperation("skip", "security_and_analysis", key, "(org-only, owner is a personal account)");
3090
+ if (hasSecurity && securityContradiction) yield* logger.syncError("security merge", "automated_security_fixes = true requires vulnerability_alerts to be enabled (or omitted); skipping security sync");
3091
+ else if (hasSecurity) {
3092
+ if (void 0 !== mergedSecurity.vulnerability_alerts) {
3093
+ const desired = mergedSecurity.vulnerability_alerts;
3094
+ const current = yield* github.getVulnerabilityAlerts(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
3095
+ yield* logger.syncError("get vulnerability_alerts", err.message);
3096
+ return desired;
3097
+ })));
3098
+ if (current !== desired) {
3099
+ yield* logger.syncOperation("sync", "vulnerability_alerts", desired ? "enable" : "disable");
3100
+ yield* github.setVulnerabilityAlerts(owner, repoName, desired).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError("vulnerability_alerts", err.message)));
3101
+ }
3102
+ }
3103
+ if (void 0 !== mergedSecurity.automated_security_fixes) {
3104
+ const desired = mergedSecurity.automated_security_fixes;
3105
+ const current = yield* github.getAutomatedSecurityFixes(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
3106
+ yield* logger.syncError("get automated_security_fixes", err.message);
3107
+ return desired;
3108
+ })));
3109
+ if (current !== desired) {
3110
+ yield* logger.syncOperation("sync", "automated_security_fixes", desired ? "enable" : "disable");
3111
+ yield* github.setAutomatedSecurityFixes(owner, repoName, desired).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError("automated_security_fixes", err.message)));
3112
+ }
3113
+ }
3114
+ if (void 0 !== mergedSecurity.private_vulnerability_reporting) {
3115
+ const desired = mergedSecurity.private_vulnerability_reporting;
3116
+ const current = yield* github.getPrivateVulnerabilityReporting(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
3117
+ yield* logger.syncError("get private_vulnerability_reporting", err.message);
3118
+ return desired;
3119
+ })));
3120
+ if (current !== desired) {
3121
+ yield* logger.syncOperation("sync", "private_vulnerability_reporting", desired ? "enable" : "disable");
3122
+ yield* github.setPrivateVulnerabilityReporting(owner, repoName, desired).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError("private_vulnerability_reporting", err.message)));
3123
+ }
3124
+ }
3125
+ }
3126
+ if (hasCodeScanning) {
3127
+ let desiredConfig = mergedCodeScanning;
3128
+ if (void 0 !== mergedCodeScanning.languages) {
3129
+ const detected = yield* github.listRepoLanguages(owner, repoName).pipe(Effect.catchTag("GitHubApiError", (err)=>Effect.gen(function*() {
3130
+ yield* logger.syncError("list repo languages", err.message);
3131
+ return [];
3132
+ })));
3133
+ const detectedCodeQL = new Set();
3134
+ for (const lang of detected){
3135
+ const mapped = REPO_LANG_TO_CODEQL[lang];
3136
+ if (mapped) detectedCodeQL.add(mapped);
3137
+ }
3138
+ const filtered = [];
3139
+ for (const lang of mergedCodeScanning.languages)if ("actions" === lang || detectedCodeQL.has(lang)) filtered.push(lang);
3140
+ else yield* logger.syncOperation("skip", "code_scanning language", lang, "(not detected in repository)");
3141
+ desiredConfig = {
3142
+ ...mergedCodeScanning,
3143
+ languages: filtered
3144
+ };
3145
+ }
3146
+ yield* logger.syncOperation("sync", "code_scanning", desiredConfig.state ?? "default-setup");
3147
+ yield* github.updateCodeScanningDefaultSetup(owner, repoName, desiredConfig).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError("code_scanning default setup", err.message)));
3148
+ }
2492
3149
  for (const envName of envRefs){
2493
3150
  const envConfig = config.environments[envName];
2494
3151
  if (envConfig) {
@@ -2520,6 +3177,11 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2520
3177
  yield* github.syncRuleset(owner, repoName, ruleset.name, ruleset).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`ruleset ${ruleset.name}`, err.message)));
2521
3178
  }
2522
3179
  }
3180
+ if (hasSecurity) {
3181
+ const securityCount = Object.values(mergedSecurity).filter((v)=>void 0 !== v).length;
3182
+ yield* logger.syncSummary("security feature", securityCount, "");
3183
+ }
3184
+ if (hasCodeScanning) yield* logger.syncSummary("code scanning", 1, mergedCodeScanning.state ?? "applied");
2523
3185
  if (hasEnvironments) yield* logger.syncSummary("environment", envRefs.length, "");
2524
3186
  if (hasSecrets) {
2525
3187
  const scopeCounts = [];
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue.svg)](https://www.typescriptlang.org/)
6
6
 
7
- Declarative GitHub repository management. Define your repo settings, secrets, variables, rulesets, and deployment environments in a TOML config file, then apply them across all your repositories with a single command.
7
+ Declarative GitHub repository management. Define your repo settings, secrets, variables, rulesets, deployment environments, advanced security toggles, and CodeQL default setup in a TOML config file, then apply them across all your repositories with a single command.
8
8
 
9
9
  ## Why reposets
10
10
 
@@ -17,7 +17,8 @@ Managing repository settings by hand doesn't scale. When you have dozens of repo
17
17
  - **Multi-scope secret and variable management** — Assign the same secret group to Actions, Dependabot, Codespaces, and deployment environments with scoped targeting.
18
18
  - **Ruleset shorthand syntax** — Define branch and tag rulesets with compact inline syntax for pull request rules, status checks, and boolean flags instead of verbose API payloads.
19
19
  - **Deployment environment management** — Configure wait timers, reviewers, and branch policies for deployment environments alongside your other settings.
20
- - **Group-based targeting** — Organize repos into groups that share settings, secrets, variables, rulesets, and environments. Change the group config, sync once, and every repo updates.
20
+ - **Advanced security and CodeQL** — Toggle secret scanning, push protection, vulnerability alerts, automated security fixes, private vulnerability reporting, and CodeQL default setup. License- and ownership-aware: GHAS-licensed fields warn instead of failing on private repos without a license, and org-only fields are silently skipped on personal accounts.
21
+ - **Group-based targeting** — Organize repos into groups that share settings, secrets, variables, rulesets, environments, security toggles, and code scanning configuration. Change the group config, sync once, and every repo updates.
21
22
  - **Cleanup policies** — Automatically remove undeclared resources per scope with optional preserve lists, so your repos converge to the declared state.
22
23
  - **Dry-run and validation** — Preview changes before applying, validate config locally without touching the GitHub API, and catch typos with built-in diagnostics.
23
24
 
@@ -93,7 +94,7 @@ All commands accept `--log-level silent|info|verbose|debug`.
93
94
 
94
95
  reposets uses two TOML files:
95
96
 
96
- - `reposets.config.toml` — defines settings, secrets, variables, rulesets, environments, and groups
97
+ - `reposets.config.toml` — defines settings, secrets, variables, rulesets, environments, security, code_scanning, and groups
97
98
  - `reposets.credentials.toml` — stores GitHub tokens and optional resolve sections for named values
98
99
 
99
100
  Config lookup order (first match wins):
@@ -112,8 +113,14 @@ reposets requires a fine-grained personal access token with:
112
113
  - Repository > Secrets (Read and write)
113
114
  - Repository > Variables (Read and write)
114
115
  - Repository > Environments (Read and write)
116
+ - Repository > Code scanning alerts (Read and write) — for `[code_scanning.*]`
117
+ - Repository > Dependabot alerts (Read and write) — for `[security.*]`
118
+ - Repository > Secret scanning alerts (Read and write) — for `security_and_analysis`
119
+ - Organization > Members (Read) — for `delegated_bypass_reviewers` team slugs on org-owned repos
115
120
  - Account > GPG keys (Read and write)
116
121
 
122
+ The four security-related scopes are only required if you use the corresponding config sections.
123
+
117
124
  ## Documentation
118
125
 
119
126
  Full reference guides are available in the [`docs/`](https://github.com/spencerbeggs/reposets/tree/main/docs) folder:
@@ -124,6 +131,7 @@ Full reference guides are available in the [`docs/`](https://github.com/spencerb
124
131
  - [Secrets and Variables](https://github.com/spencerbeggs/reposets/blob/main/docs/secrets-and-variables.md) - resource groups, three kinds (file/value/resolved), and scoping
125
132
  - [Rulesets](https://github.com/spencerbeggs/reposets/blob/main/docs/rulesets.md) - branch and tag ruleset configuration
126
133
  - [Environments](https://github.com/spencerbeggs/reposets/blob/main/docs/environments.md) - deployment environment setup
134
+ - [Advanced Security](https://github.com/spencerbeggs/reposets/blob/main/docs/configuration.md#security-and-analysis-nested-block) - secret scanning, vulnerability alerts, automated security fixes, private vulnerability reporting, and CodeQL default setup
127
135
  - [Cleanup](https://github.com/spencerbeggs/reposets/blob/main/docs/cleanup.md) - automatic cleanup of undeclared resources
128
136
  - [Token Permissions](https://github.com/spencerbeggs/reposets/blob/main/docs/token-permissions.md) - GitHub PAT setup guide
129
137
 
package/bin/reposets.js CHANGED
@@ -81,6 +81,8 @@ const KNOWN_CONFIG_KEYS = new Set([
81
81
  "variables",
82
82
  "rulesets",
83
83
  "environments",
84
+ "security",
85
+ "code_scanning",
84
86
  "groups"
85
87
  ]);
86
88
  const KNOWN_GROUP_KEYS = new Set([
@@ -92,6 +94,8 @@ const KNOWN_GROUP_KEYS = new Set([
92
94
  "variables",
93
95
  "rulesets",
94
96
  "environments",
97
+ "security",
98
+ "code_scanning",
95
99
  "cleanup"
96
100
  ]);
97
101
  const KNOWN_CLEANUP_KEYS = new Set([
@@ -206,7 +210,11 @@ const doctorCommand = Command.make("doctor", {
206
210
  yield* Effect.log(" Repository permissions > Secrets (Read and write) -- Actions secrets");
207
211
  yield* Effect.log(" Repository permissions > Variables (Read and write) -- Actions variables");
208
212
  yield* Effect.log(" Repository permissions > Environments (Read and write) -- environment sync");
213
+ yield* Effect.log(" Repository permissions > Code scanning alerts (Read and write) -- code_scanning sync");
214
+ yield* Effect.log(" Repository permissions > Dependabot alerts (Read and write) -- security feature sync");
215
+ yield* Effect.log(" Repository permissions > Secret scanning alerts (Read and write) -- secret scanning delegation");
209
216
  yield* Effect.log(" Account permissions > GPG keys (Read and write) -- secrets encryption key");
217
+ yield* Effect.log(" Organization permissions > Members (Read) -- resolve team slugs (org-level only)");
210
218
  if (0 === warnings) yield* Effect.log("\nNo unknown keys detected.");
211
219
  else yield* Effect.log(`\n${warnings} warning(s) found.`);
212
220
  }).pipe(Effect.provide(makeConfigFilesLive(config)))).pipe(Command.withDescription("Deep config diagnostics with typo detection"));
@@ -256,6 +264,35 @@ const CONFIG_TEMPLATE = `# reposets configuration
256
264
  # [[rulesets.default-branch.rules]]
257
265
  # type = "deletion"
258
266
 
267
+ # --- Advanced security ---
268
+ # Nested inside a settings group (folded into the same PATCH /repos call).
269
+ # Some fields are GHAS-licensed and only work on public repos or
270
+ # private repos with a GHAS subscription. Org-only fields are silently
271
+ # skipped on personal accounts.
272
+ #
273
+ # [settings.defaults.security_and_analysis]
274
+ # secret_scanning = "enabled"
275
+ # secret_scanning_push_protection = "enabled"
276
+ # dependabot_security_updates = "enabled"
277
+
278
+ # --- Security feature toggles ---
279
+ # Dedicated PUT/DELETE endpoints; omit a key to leave it untouched.
280
+ #
281
+ # [security.oss-defaults]
282
+ # vulnerability_alerts = true
283
+ # automated_security_fixes = true
284
+ # private_vulnerability_reporting = true
285
+
286
+ # --- CodeQL default setup ---
287
+ # Applies via PATCH /repos/{o}/{r}/code-scanning/default-setup.
288
+ # Languages not detected in the repo are skipped with a warning.
289
+ #
290
+ # [code_scanning.oss-defaults]
291
+ # state = "configured"
292
+ # languages = ["javascript-typescript", "python"]
293
+ # query_suite = "extended"
294
+ # threat_model = "remote"
295
+
259
296
  # --- Cleanup defaults ---
260
297
  # [cleanup]
261
298
  # secrets = false
@@ -269,6 +306,8 @@ const CONFIG_TEMPLATE = `# reposets configuration
269
306
  # secrets = { actions = ["from-files", "from-creds"] }
270
307
  # variables = { actions = ["turbo", "bot"] }
271
308
  # rulesets = ["default-branch"]
309
+ # security = ["oss-defaults"]
310
+ # code_scanning = ["oss-defaults"]
272
311
  `;
273
312
  const CREDENTIALS_TEMPLATE = `# reposets credentials (keep this file private)
274
313
  # See: https://github.com/spencerbeggs/reposets
@@ -356,6 +395,8 @@ const listCommand = Command.make("list", {
356
395
  if (parts.length) yield* Effect.log(` variables: ${parts.join(", ")}`);
357
396
  }
358
397
  if (group.rulesets?.length) yield* Effect.log(` rulesets: ${group.rulesets.join(", ")}`);
398
+ if (group.security?.length) yield* Effect.log(` security: ${group.security.join(", ")}`);
399
+ if (group.code_scanning?.length) yield* Effect.log(` code_scanning: ${group.code_scanning.join(", ")}`);
359
400
  if (group.credentials) yield* Effect.log(` credentials: ${group.credentials}`);
360
401
  yield* Effect.log("");
361
402
  }
package/index.d.ts CHANGED
@@ -111,6 +111,24 @@ export declare const CleanupScopeSchema: Schema.Union<[typeof Schema.Boolean, Sc
111
111
  preserve: Schema.Array$<typeof Schema.String>;
112
112
  }>]>;
113
113
 
114
+ declare type CodeScanningGroup = typeof CodeScanningGroupSchema.Type;
115
+
116
+ declare const CodeScanningGroupSchema: Schema.refine<{
117
+ readonly state?: "configured" | "not-configured" | undefined;
118
+ readonly languages?: readonly ("actions" | "c-cpp" | "csharp" | "go" | "java-kotlin" | "javascript-typescript" | "python" | "ruby" | "swift")[] | undefined;
119
+ readonly query_suite?: "default" | "extended" | undefined;
120
+ readonly threat_model?: "remote" | "remote_and_local" | undefined;
121
+ readonly runner_type?: "labeled" | "standard" | undefined;
122
+ readonly runner_label?: string | undefined;
123
+ }, Schema.Struct<{
124
+ state: Schema.optional<Schema.Literal<["configured", "not-configured"]>>;
125
+ languages: Schema.optional<Schema.Array$<Schema.Literal<["actions", "c-cpp", "csharp", "go", "java-kotlin", "javascript-typescript", "python", "ruby", "swift"]>>>;
126
+ query_suite: Schema.optional<Schema.Literal<["default", "extended"]>>;
127
+ threat_model: Schema.optional<Schema.Literal<["remote", "remote_and_local"]>>;
128
+ runner_type: Schema.optional<Schema.Literal<["standard", "labeled"]>>;
129
+ runner_label: Schema.optional<Schema.SchemaClass<string, string, never>>;
130
+ }>>;
131
+
114
132
  export declare type Config = typeof ConfigSchema.Type;
115
133
 
116
134
  export declare const CONFIG_FILENAME = "reposets.config.toml";
@@ -148,6 +166,24 @@ export declare const ConfigSchema: Schema.Struct<{
148
166
  merge_commit_message: Schema.optional<Schema.Literal<["PR_BODY", "PR_TITLE", "BLANK"]>>;
149
167
  delete_branch_on_merge: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
150
168
  web_commit_signoff_required: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
169
+ security_and_analysis: Schema.optional<Schema.Struct<{
170
+ advanced_security: Schema.optional<Schema.Literal<["enabled", "disabled"]>>;
171
+ code_security: Schema.optional<Schema.Literal<["enabled", "disabled"]>>;
172
+ secret_scanning: Schema.optional<Schema.Literal<["enabled", "disabled"]>>;
173
+ secret_scanning_push_protection: Schema.optional<Schema.Literal<["enabled", "disabled"]>>;
174
+ secret_scanning_ai_detection: Schema.optional<Schema.Literal<["enabled", "disabled"]>>;
175
+ secret_scanning_non_provider_patterns: Schema.optional<Schema.Literal<["enabled", "disabled"]>>;
176
+ secret_scanning_delegated_alert_dismissal: Schema.optional<Schema.Literal<["enabled", "disabled"]>>;
177
+ secret_scanning_delegated_bypass: Schema.optional<Schema.Literal<["enabled", "disabled"]>>;
178
+ delegated_bypass_reviewers: Schema.optional<Schema.Array$<Schema.Union<[Schema.Struct<{
179
+ team: Schema.SchemaClass<string, string, never>;
180
+ mode: Schema.optional<Schema.Literal<["ALWAYS", "EXEMPT"]>>;
181
+ }>, Schema.Struct<{
182
+ role: Schema.SchemaClass<string, string, never>;
183
+ mode: Schema.optional<Schema.Literal<["ALWAYS", "EXEMPT"]>>;
184
+ }>]>>>;
185
+ dependabot_security_updates: Schema.optional<Schema.Literal<["enabled", "disabled"]>>;
186
+ }>>;
151
187
  }, readonly [{
152
188
  readonly key: typeof Schema.String;
153
189
  readonly value: Schema.SchemaClass<unknown, unknown, never>;
@@ -390,6 +426,34 @@ export declare const ConfigSchema: Schema.Struct<{
390
426
  }>>, {
391
427
  default: () => {};
392
428
  }>;
429
+ security: Schema.optionalWith<Schema.Record$<typeof Schema.String, Schema.refine<{
430
+ readonly vulnerability_alerts?: boolean | undefined;
431
+ readonly automated_security_fixes?: boolean | undefined;
432
+ readonly private_vulnerability_reporting?: boolean | undefined;
433
+ }, Schema.Struct<{
434
+ vulnerability_alerts: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
435
+ automated_security_fixes: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
436
+ private_vulnerability_reporting: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
437
+ }>>>, {
438
+ default: () => {};
439
+ }>;
440
+ code_scanning: Schema.optionalWith<Schema.Record$<typeof Schema.String, Schema.refine<{
441
+ readonly state?: "configured" | "not-configured" | undefined;
442
+ readonly languages?: readonly ("actions" | "c-cpp" | "csharp" | "go" | "java-kotlin" | "javascript-typescript" | "python" | "ruby" | "swift")[] | undefined;
443
+ readonly query_suite?: "default" | "extended" | undefined;
444
+ readonly threat_model?: "remote" | "remote_and_local" | undefined;
445
+ readonly runner_type?: "labeled" | "standard" | undefined;
446
+ readonly runner_label?: string | undefined;
447
+ }, Schema.Struct<{
448
+ state: Schema.optional<Schema.Literal<["configured", "not-configured"]>>;
449
+ languages: Schema.optional<Schema.Array$<Schema.Literal<["actions", "c-cpp", "csharp", "go", "java-kotlin", "javascript-typescript", "python", "ruby", "swift"]>>>;
450
+ query_suite: Schema.optional<Schema.Literal<["default", "extended"]>>;
451
+ threat_model: Schema.optional<Schema.Literal<["remote", "remote_and_local"]>>;
452
+ runner_type: Schema.optional<Schema.Literal<["standard", "labeled"]>>;
453
+ runner_label: Schema.optional<Schema.SchemaClass<string, string, never>>;
454
+ }>>>, {
455
+ default: () => {};
456
+ }>;
393
457
  groups: Schema.Record$<typeof Schema.String, Schema.Struct<{
394
458
  owner: Schema.optional<Schema.SchemaClass<string, string, never>>;
395
459
  repos: Schema.Array$<typeof Schema.String>;
@@ -407,6 +471,8 @@ export declare const ConfigSchema: Schema.Struct<{
407
471
  environments: Schema.optional<Schema.Record$<typeof Schema.String, Schema.Array$<typeof Schema.String>>>;
408
472
  }>>;
409
473
  rulesets: Schema.optional<Schema.Array$<typeof Schema.String>>;
474
+ security: Schema.optional<Schema.Array$<typeof Schema.String>>;
475
+ code_scanning: Schema.optional<Schema.Array$<typeof Schema.String>>;
410
476
  cleanup: Schema.optional<Schema.Struct<{
411
477
  secrets: Schema.optionalWith<Schema.Struct<{
412
478
  actions: Schema.optionalWith<Schema.Union<[typeof Schema.Boolean, Schema.Struct<{
@@ -559,6 +625,16 @@ declare interface GitHubClientService {
559
625
  readonly deleteEnvironment: (owner: string, repo: string, name: string) => Effect.Effect<void, GitHubApiError>;
560
626
  readonly deleteEnvironmentSecret: (owner: string, repo: string, envName: string, name: string) => Effect.Effect<void, GitHubApiError>;
561
627
  readonly deleteEnvironmentVariable: (owner: string, repo: string, envName: string, name: string) => Effect.Effect<void, GitHubApiError>;
628
+ readonly getVulnerabilityAlerts: (owner: string, repo: string) => Effect.Effect<boolean, GitHubApiError>;
629
+ readonly setVulnerabilityAlerts: (owner: string, repo: string, enabled: boolean) => Effect.Effect<void, GitHubApiError>;
630
+ readonly getAutomatedSecurityFixes: (owner: string, repo: string) => Effect.Effect<boolean, GitHubApiError>;
631
+ readonly setAutomatedSecurityFixes: (owner: string, repo: string, enabled: boolean) => Effect.Effect<void, GitHubApiError>;
632
+ readonly getPrivateVulnerabilityReporting: (owner: string, repo: string) => Effect.Effect<boolean, GitHubApiError>;
633
+ readonly setPrivateVulnerabilityReporting: (owner: string, repo: string, enabled: boolean) => Effect.Effect<void, GitHubApiError>;
634
+ readonly updateCodeScanningDefaultSetup: (owner: string, repo: string, config: CodeScanningGroup) => Effect.Effect<void, GitHubApiError>;
635
+ readonly listRepoLanguages: (owner: string, repo: string) => Effect.Effect<ReadonlyArray<string>, GitHubApiError>;
636
+ readonly resolveTeamId: (org: string, slug: string) => Effect.Effect<number, GitHubApiError>;
637
+ readonly resolveRoleId: (org: string, name: string) => Effect.Effect<number, GitHubApiError>;
562
638
  }
563
639
 
564
640
  export declare function GitHubClientTest(): {
@@ -585,6 +661,8 @@ export declare const GroupSchema: Schema.Struct<{
585
661
  environments: Schema.optional<Schema.Record$<typeof Schema.String, Schema.Array$<typeof Schema.String>>>;
586
662
  }>>;
587
663
  rulesets: Schema.optional<Schema.Array$<typeof Schema.String>>;
664
+ security: Schema.optional<Schema.Array$<typeof Schema.String>>;
665
+ code_scanning: Schema.optional<Schema.Array$<typeof Schema.String>>;
588
666
  cleanup: Schema.optional<Schema.Struct<{
589
667
  secrets: Schema.optionalWith<Schema.Struct<{
590
668
  actions: Schema.optionalWith<Schema.Union<[typeof Schema.Boolean, Schema.Struct<{
@@ -712,6 +790,24 @@ readonly merge_commit_title?: "MERGE_MESSAGE" | "PR_TITLE" | undefined;
712
790
  readonly merge_commit_message?: "BLANK" | "PR_BODY" | "PR_TITLE" | undefined;
713
791
  readonly delete_branch_on_merge?: boolean | undefined;
714
792
  readonly web_commit_signoff_required?: boolean | undefined;
793
+ readonly security_and_analysis?: {
794
+ readonly advanced_security?: "disabled" | "enabled" | undefined;
795
+ readonly code_security?: "disabled" | "enabled" | undefined;
796
+ readonly secret_scanning?: "disabled" | "enabled" | undefined;
797
+ readonly secret_scanning_push_protection?: "disabled" | "enabled" | undefined;
798
+ readonly secret_scanning_ai_detection?: "disabled" | "enabled" | undefined;
799
+ readonly secret_scanning_non_provider_patterns?: "disabled" | "enabled" | undefined;
800
+ readonly secret_scanning_delegated_alert_dismissal?: "disabled" | "enabled" | undefined;
801
+ readonly secret_scanning_delegated_bypass?: "disabled" | "enabled" | undefined;
802
+ readonly delegated_bypass_reviewers?: readonly ({
803
+ readonly team: string;
804
+ readonly mode?: "ALWAYS" | "EXEMPT" | undefined;
805
+ } | {
806
+ readonly role: string;
807
+ readonly mode?: "ALWAYS" | "EXEMPT" | undefined;
808
+ })[] | undefined;
809
+ readonly dependabot_security_updates?: "disabled" | "enabled" | undefined;
810
+ } | undefined;
715
811
  };
716
812
  };
717
813
  readonly secrets: {
@@ -942,6 +1038,23 @@ readonly type: "branch" | "tag";
942
1038
  }[] | undefined;
943
1039
  };
944
1040
  };
1041
+ readonly security: {
1042
+ readonly [x: string]: {
1043
+ readonly vulnerability_alerts?: boolean | undefined;
1044
+ readonly automated_security_fixes?: boolean | undefined;
1045
+ readonly private_vulnerability_reporting?: boolean | undefined;
1046
+ };
1047
+ };
1048
+ readonly code_scanning: {
1049
+ readonly [x: string]: {
1050
+ readonly state?: "configured" | "not-configured" | undefined;
1051
+ readonly languages?: readonly ("actions" | "c-cpp" | "csharp" | "go" | "java-kotlin" | "javascript-typescript" | "python" | "ruby" | "swift")[] | undefined;
1052
+ readonly query_suite?: "default" | "extended" | undefined;
1053
+ readonly threat_model?: "remote" | "remote_and_local" | undefined;
1054
+ readonly runner_type?: "labeled" | "standard" | undefined;
1055
+ readonly runner_label?: string | undefined;
1056
+ };
1057
+ };
945
1058
  readonly groups: {
946
1059
  readonly [x: string]: {
947
1060
  readonly owner?: string | undefined;
@@ -964,6 +1077,8 @@ readonly [x: string]: readonly string[];
964
1077
  } | undefined;
965
1078
  } | undefined;
966
1079
  readonly rulesets?: readonly string[] | undefined;
1080
+ readonly security?: readonly string[] | undefined;
1081
+ readonly code_scanning?: readonly string[] | undefined;
967
1082
  readonly cleanup?: {
968
1083
  readonly secrets: {
969
1084
  readonly actions: boolean | {
@@ -1021,6 +1136,24 @@ readonly merge_commit_title?: "MERGE_MESSAGE" | "PR_TITLE" | undefined;
1021
1136
  readonly merge_commit_message?: "BLANK" | "PR_BODY" | "PR_TITLE" | undefined;
1022
1137
  readonly delete_branch_on_merge?: boolean | undefined;
1023
1138
  readonly web_commit_signoff_required?: boolean | undefined;
1139
+ readonly security_and_analysis?: {
1140
+ readonly advanced_security?: "disabled" | "enabled" | undefined;
1141
+ readonly code_security?: "disabled" | "enabled" | undefined;
1142
+ readonly secret_scanning?: "disabled" | "enabled" | undefined;
1143
+ readonly secret_scanning_push_protection?: "disabled" | "enabled" | undefined;
1144
+ readonly secret_scanning_ai_detection?: "disabled" | "enabled" | undefined;
1145
+ readonly secret_scanning_non_provider_patterns?: "disabled" | "enabled" | undefined;
1146
+ readonly secret_scanning_delegated_alert_dismissal?: "disabled" | "enabled" | undefined;
1147
+ readonly secret_scanning_delegated_bypass?: "disabled" | "enabled" | undefined;
1148
+ readonly delegated_bypass_reviewers?: readonly ({
1149
+ readonly team: string;
1150
+ readonly mode?: "ALWAYS" | "EXEMPT" | undefined;
1151
+ } | {
1152
+ readonly role: string;
1153
+ readonly mode?: "ALWAYS" | "EXEMPT" | undefined;
1154
+ })[] | undefined;
1155
+ readonly dependabot_security_updates?: "disabled" | "enabled" | undefined;
1156
+ } | undefined;
1024
1157
  };
1025
1158
  };
1026
1159
  readonly secrets: {
@@ -1251,6 +1384,23 @@ readonly type: "branch" | "tag";
1251
1384
  }[] | undefined;
1252
1385
  };
1253
1386
  };
1387
+ readonly security: {
1388
+ readonly [x: string]: {
1389
+ readonly vulnerability_alerts?: boolean | undefined;
1390
+ readonly automated_security_fixes?: boolean | undefined;
1391
+ readonly private_vulnerability_reporting?: boolean | undefined;
1392
+ };
1393
+ };
1394
+ readonly code_scanning: {
1395
+ readonly [x: string]: {
1396
+ readonly state?: "configured" | "not-configured" | undefined;
1397
+ readonly languages?: readonly ("actions" | "c-cpp" | "csharp" | "go" | "java-kotlin" | "javascript-typescript" | "python" | "ruby" | "swift")[] | undefined;
1398
+ readonly query_suite?: "default" | "extended" | undefined;
1399
+ readonly threat_model?: "remote" | "remote_and_local" | undefined;
1400
+ readonly runner_type?: "labeled" | "standard" | undefined;
1401
+ readonly runner_label?: string | undefined;
1402
+ };
1403
+ };
1254
1404
  readonly groups: {
1255
1405
  readonly [x: string]: {
1256
1406
  readonly owner?: string | undefined;
@@ -1273,6 +1423,8 @@ readonly [x: string]: readonly string[];
1273
1423
  } | undefined;
1274
1424
  } | undefined;
1275
1425
  readonly rulesets?: readonly string[] | undefined;
1426
+ readonly security?: readonly string[] | undefined;
1427
+ readonly code_scanning?: readonly string[] | undefined;
1276
1428
  readonly cleanup?: {
1277
1429
  readonly secrets: {
1278
1430
  readonly actions: boolean | {
@@ -1644,7 +1796,7 @@ declare interface SyncLoggerService {
1644
1796
  readonly groupStart: (name: string, repoCount: number) => Effect.Effect<void>;
1645
1797
  readonly repoStart: (owner: string, repo: string) => Effect.Effect<void>;
1646
1798
  readonly repoSkip: (owner: string, repo: string, reason: string) => Effect.Effect<void>;
1647
- readonly syncSummary: (resource: "secret" | "variable" | "ruleset" | "environment", count: number, detail: string) => Effect.Effect<void>;
1799
+ readonly syncSummary: (resource: "secret" | "variable" | "ruleset" | "environment" | "security feature" | "code scanning", count: number, detail: string) => Effect.Effect<void>;
1648
1800
  readonly settingsApplied: () => Effect.Effect<void>;
1649
1801
  readonly cleanupSummary: (resource: string, count: number, names: string[]) => Effect.Effect<void>;
1650
1802
  readonly syncOperation: (verb: "sync" | "apply" | "delete" | "skip", resource: string, name: string, detail?: string, source?: string) => Effect.Effect<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reposets",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "description": "CLI tool to sync GitHub repo settings, secrets and rulesets across personal repositories",
6
6
  "keywords": [
@@ -42,19 +42,19 @@
42
42
  }
43
43
  },
44
44
  "bin": {
45
- "reposets": "./bin/reposets.js"
45
+ "reposets": "bin/reposets.js"
46
46
  },
47
47
  "dependencies": {
48
48
  "@1password/sdk": "^0.4.0",
49
49
  "@effect/cli": "^0.75.1",
50
- "@effect/platform": "^0.96.0",
50
+ "@effect/platform": "^0.96.1",
51
51
  "@effect/platform-node": "^0.106.0",
52
52
  "@octokit/rest": "^22.0.1",
53
53
  "blakejs": "^1.2.1",
54
- "effect": "^3.21.1",
55
- "smol-toml": ">=1.6.1",
54
+ "effect": "^3.21.2",
55
+ "smol-toml": "^1.6.1",
56
56
  "tweetnacl": "^1.0.3",
57
- "xdg-effect": "^1.0.0"
57
+ "xdg-effect": "^1.0.1"
58
58
  },
59
59
  "engines": {
60
60
  "node": ">=20.0.0"