reposets 0.2.1 → 0.4.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.
package/500.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Context, Data, Effect, Layer, Option, Ref, Schema } from "effect";
2
2
  import { blake2b } from "blakejs";
3
3
  import tweetnacl from "tweetnacl";
4
- import { AppDirsConfig, ConfigError, ConfigFile, FirstMatch, Jsonifiable, TomlCodec, UpwardWalk, XdgConfig, XdgConfigLive, XdgSavePath, taplo, tombi } from "xdg-effect";
4
+ import { AppDirsConfig, ConfigError, ConfigFile, ExplicitPath, FirstMatch, Jsonifiable, StaticDir, TomlCodec, UpwardWalk, XdgConfigLive, XdgConfigResolver, XdgSavePath, taplo, tombi } from "xdg-effect";
5
5
  import { existsSync, readFileSync, statSync } from "node:fs";
6
- import { dirname, isAbsolute, join, resolve } from "node:path";
6
+ import { isAbsolute, resolve } from "node:path";
7
7
  import { Octokit } from "@octokit/rest";
8
8
  class ResolveError extends Data.TaggedError("ResolveError") {
9
9
  }
@@ -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"
@@ -1367,64 +1594,109 @@ const CONFIG_FILENAME = "reposets.config.toml";
1367
1594
  const CREDENTIALS_FILENAME = "reposets.credentials.toml";
1368
1595
  const ReposetsConfigFile = ConfigFile.Tag("reposets/Config");
1369
1596
  const ReposetsCredentialsFile = ConfigFile.Tag("reposets/Credentials");
1370
- const xdgLayer = XdgConfigLive({
1371
- app: new AppDirsConfig({
1372
- namespace: "reposets"
1373
- }),
1374
- config: {
1375
- tag: ReposetsConfigFile,
1376
- schema: ConfigSchema,
1377
- codec: TomlCodec,
1378
- strategy: FirstMatch,
1379
- resolvers: [
1380
- UpwardWalk({
1381
- filename: CONFIG_FILENAME
1382
- }),
1383
- XdgConfig({
1384
- filename: CONFIG_FILENAME
1385
- })
1386
- ]
1597
+ function validateConfigRefs(config) {
1598
+ const errors = [];
1599
+ const definedSettings = new Set(Object.keys(config.settings));
1600
+ const definedSecrets = new Set(Object.keys(config.secrets));
1601
+ const definedVariables = new Set(Object.keys(config.variables));
1602
+ const definedRulesets = new Set(Object.keys(config.rulesets));
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));
1606
+ for (const [groupName, group] of Object.entries(config.groups)){
1607
+ if (group.settings) {
1608
+ for (const ref of group.settings)if (!definedSettings.has(ref)) errors.push(`group '${groupName}': unknown settings group '${ref}'`);
1609
+ }
1610
+ if (group.rulesets) {
1611
+ for (const ref of group.rulesets)if (!definedRulesets.has(ref)) errors.push(`group '${groupName}': unknown ruleset '${ref}'`);
1612
+ }
1613
+ if (group.environments) {
1614
+ for (const ref of group.environments)if (!definedEnvironments.has(ref)) errors.push(`group '${groupName}': unknown environment '${ref}'`);
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
+ }
1622
+ if (group.secrets) {
1623
+ if (group.secrets.actions) {
1624
+ for (const ref of group.secrets.actions)if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': unknown secrets group '${ref}'`);
1625
+ }
1626
+ if (group.secrets.dependabot) {
1627
+ for (const ref of group.secrets.dependabot)if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': unknown secrets group '${ref}'`);
1628
+ }
1629
+ if (group.secrets.codespaces) {
1630
+ for (const ref of group.secrets.codespaces)if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': unknown secrets group '${ref}'`);
1631
+ }
1632
+ if (group.secrets.environments) for (const [envName, secretGroups] of Object.entries(group.secrets.environments)){
1633
+ if (!definedEnvironments.has(envName)) errors.push(`group '${groupName}': unknown environment '${envName}' in secrets.environments`);
1634
+ for (const ref of secretGroups)if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': in secrets.environments.'${envName}': unknown secrets group '${ref}'`);
1635
+ }
1636
+ }
1637
+ if (group.variables) {
1638
+ if (group.variables.actions) {
1639
+ for (const ref of group.variables.actions)if (!definedVariables.has(ref)) errors.push(`group '${groupName}': unknown variables group '${ref}'`);
1640
+ }
1641
+ if (group.variables.environments) for (const [envName, varGroups] of Object.entries(group.variables.environments)){
1642
+ if (!definedEnvironments.has(envName)) errors.push(`group '${groupName}': unknown environment '${envName}' in variables.environments`);
1643
+ for (const ref of varGroups)if (!definedVariables.has(ref)) errors.push(`group '${groupName}': in variables.environments.'${envName}': unknown variables group '${ref}'`);
1644
+ }
1645
+ }
1387
1646
  }
1388
- });
1389
- const credentialsLayer = ConfigFile.Live({
1390
- tag: ReposetsCredentialsFile,
1391
- schema: CredentialsSchema,
1392
- codec: TomlCodec,
1393
- strategy: FirstMatch,
1394
- resolvers: [
1395
- UpwardWalk({
1396
- filename: CREDENTIALS_FILENAME
1397
- }),
1398
- XdgConfig({
1399
- filename: CREDENTIALS_FILENAME
1400
- })
1401
- ],
1402
- defaultPath: XdgSavePath(CREDENTIALS_FILENAME)
1403
- }).pipe(Layer.provide(xdgLayer));
1404
- const ConfigFilesLive = Layer.mergeAll(xdgLayer, credentialsLayer);
1405
- function resolveConfigFlag(configFlag) {
1406
- if (Option.isNone(configFlag)) return;
1407
- const flag = configFlag.value;
1408
- if (existsSync(flag) && statSync(flag).isDirectory()) return join(flag, CONFIG_FILENAME);
1409
- return flag;
1647
+ if (errors.length > 0) return Effect.fail(new ConfigError({
1648
+ operation: "validate",
1649
+ reason: errors.join("\n")
1650
+ }));
1651
+ return Effect.succeed(config);
1410
1652
  }
1411
- function loadConfigWithDir(configFile, configFlag) {
1412
- const resolved = resolveConfigFlag(configFlag);
1413
- if (resolved) return Effect.map(configFile.loadFrom(resolved), (config)=>({
1414
- config,
1415
- configDir: dirname(resolved)
1653
+ function makeConfigFilesLive(configFlag) {
1654
+ const configResolvers = [];
1655
+ if (Option.isSome(configFlag)) {
1656
+ const flag = configFlag.value;
1657
+ if (existsSync(flag) && statSync(flag).isDirectory()) configResolvers.push(StaticDir({
1658
+ dir: flag,
1659
+ filename: CONFIG_FILENAME
1416
1660
  }));
1417
- return Effect.flatMap(configFile.discover, (sources)=>{
1418
- if (0 === sources.length) return Effect.fail(new ConfigError({
1419
- operation: "discover",
1420
- reason: "No config file found"
1421
- }));
1422
- return Effect.succeed({
1423
- config: sources[0].value,
1424
- configDir: dirname(sources[0].path)
1425
- });
1661
+ else configResolvers.push(ExplicitPath(flag));
1662
+ }
1663
+ configResolvers.push(UpwardWalk({
1664
+ filename: CONFIG_FILENAME
1665
+ }), XdgConfigResolver({
1666
+ filename: CONFIG_FILENAME
1667
+ }));
1668
+ return XdgConfigLive.multi({
1669
+ app: new AppDirsConfig({
1670
+ namespace: "reposets"
1671
+ }),
1672
+ configs: [
1673
+ {
1674
+ tag: ReposetsConfigFile,
1675
+ schema: ConfigSchema,
1676
+ codec: TomlCodec,
1677
+ strategy: FirstMatch,
1678
+ resolvers: configResolvers,
1679
+ validate: validateConfigRefs
1680
+ },
1681
+ {
1682
+ tag: ReposetsCredentialsFile,
1683
+ schema: CredentialsSchema,
1684
+ codec: TomlCodec,
1685
+ strategy: FirstMatch,
1686
+ resolvers: [
1687
+ UpwardWalk({
1688
+ filename: CREDENTIALS_FILENAME
1689
+ }),
1690
+ XdgConfigResolver({
1691
+ filename: CREDENTIALS_FILENAME
1692
+ })
1693
+ ],
1694
+ defaultPath: XdgSavePath(CREDENTIALS_FILENAME)
1695
+ }
1696
+ ]
1426
1697
  });
1427
1698
  }
1699
+ const ConfigFilesLive = makeConfigFilesLive(Option.none());
1428
1700
  class OnePasswordClient extends Context.Tag("OnePasswordClient")() {
1429
1701
  }
1430
1702
  const OnePasswordClientLive = Layer.succeed(OnePasswordClient, {
@@ -1500,6 +1772,31 @@ class GitHubClient extends Context.Tag("GitHubClient")() {
1500
1772
  const ORG_ONLY_SETTINGS = new Set([
1501
1773
  "allow_forking"
1502
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
+ }
1503
1800
  const GRAPHQL_SETTINGS = {
1504
1801
  has_sponsorships: "hasSponsorshipsEnabled",
1505
1802
  has_pull_requests: "hasPullRequestsEnabled"
@@ -1526,6 +1823,8 @@ function GitHubClientLive(token) {
1526
1823
  });
1527
1824
  }
1528
1825
  const ownerTypeCache = new Map();
1826
+ const teamIdCache = new Map();
1827
+ const roleIdCache = new Map();
1529
1828
  return {
1530
1829
  getOwnerType (owner) {
1531
1830
  return Effect.tryPromise({
@@ -1619,6 +1918,11 @@ function GitHubClientLive(token) {
1619
1918
  const restSettings = {};
1620
1919
  const graphqlInput = {};
1621
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
+ }
1622
1926
  const graphqlField = GRAPHQL_SETTINGS[key];
1623
1927
  if (void 0 !== graphqlField) graphqlInput[graphqlField] = value;
1624
1928
  else restSettings[key] = value;
@@ -1945,6 +2249,160 @@ function GitHubClientLive(token) {
1945
2249
  },
1946
2250
  catch: wrapError
1947
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
+ });
1948
2406
  }
1949
2407
  };
1950
2408
  })());
@@ -2121,6 +2579,82 @@ function GitHubClientTest() {
2121
2579
  }
2122
2580
  });
2123
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);
2124
2658
  }
2125
2659
  });
2126
2660
  return {
@@ -2135,6 +2669,8 @@ class SyncLogger extends Context.Tag("SyncLogger")() {
2135
2669
  function pluralize(resource, count) {
2136
2670
  if (1 === count) return resource;
2137
2671
  if ("ruleset" === resource) return "rulesets";
2672
+ if ("security feature" === resource) return "security features";
2673
+ if ("code scanning" === resource) return "code scanning";
2138
2674
  return `${resource}s`;
2139
2675
  }
2140
2676
  function SyncLoggerLive(config) {
@@ -2241,6 +2777,24 @@ function SyncLoggerLive(config) {
2241
2777
  };
2242
2778
  }));
2243
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
+ };
2244
2798
  class SyncEngine extends Context.Tag("SyncEngine")() {
2245
2799
  }
2246
2800
  function isCleanupActive(scope) {
@@ -2298,6 +2852,29 @@ function groupEntryNames(group) {
2298
2852
  if ("resolved" in group) return Object.keys(group.resolved);
2299
2853
  return [];
2300
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
+ }
2301
2878
  const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2302
2879
  const github = yield* GitHubClient;
2303
2880
  const credResolver = yield* CredentialResolver;
@@ -2422,9 +2999,14 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2422
2999
  const settingGroupRefs = group.settings ?? [];
2423
3000
  const mergedSettings = {};
2424
3001
  const skippedSettings = [];
3002
+ const saaBlocks = [];
2425
3003
  for (const ref of settingGroupRefs){
2426
3004
  const settingGroup = config.settings[ref];
2427
- 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
+ }
2428
3010
  }
2429
3011
  if ("User" === ownerType) {
2430
3012
  for (const key of ORG_ONLY_SETTINGS)if (key in mergedSettings) {
@@ -2432,6 +3014,58 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2432
3014
  skippedSettings.push(key);
2433
3015
  }
2434
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;
2435
3069
  const hasSecrets = secretScopes.some((s)=>(resolvedSecrets.get(s)?.size ?? 0) > 0);
2436
3070
  const hasVariables = resolvedVariables.size > 0;
2437
3071
  const hasRulesets = rulesetMap.size > 0;
@@ -2441,7 +3075,7 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2441
3075
  const hasEnvVariables = resolvedEnvVariables.size > 0;
2442
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));
2443
3077
  for (const repoName of group.repos)if (!repoFilter || repoName === repoFilter) {
2444
- if (!hasSecrets && !hasVariables && !hasRulesets && !hasSettings && !hasEnvironments && !hasEnvSecrets && !hasEnvVariables && !hasCleanup) {
3078
+ if (!hasSecrets && !hasVariables && !hasRulesets && !hasSettings && !hasEnvironments && !hasEnvSecrets && !hasEnvVariables && !hasSecurity && !hasCodeScanning && !hasCleanup) {
2445
3079
  yield* logger.repoSkip(owner, repoName, "no changes configured");
2446
3080
  continue;
2447
3081
  }
@@ -2452,6 +3086,66 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2452
3086
  yield* github.syncSettings(owner, repoName, mergedSettings).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError("settings", err.message)));
2453
3087
  }
2454
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
+ }
2455
3149
  for (const envName of envRefs){
2456
3150
  const envConfig = config.environments[envName];
2457
3151
  if (envConfig) {
@@ -2483,6 +3177,11 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2483
3177
  yield* github.syncRuleset(owner, repoName, ruleset.name, ruleset).pipe(Effect.catchTag("GitHubApiError", (err)=>logger.syncError(`ruleset ${ruleset.name}`, err.message)));
2484
3178
  }
2485
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");
2486
3185
  if (hasEnvironments) yield* logger.syncSummary("environment", envRefs.length, "");
2487
3186
  if (hasSecrets) {
2488
3187
  const scopeCounts = [];
@@ -2652,4 +3351,4 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2652
3351
  }
2653
3352
  };
2654
3353
  }));
2655
- export { BypassActorSchema, CleanupSchema, CleanupScopeSchema, ConfigFilesLive, ConfigSchema, CredentialProfileSchema, CredentialResolver, CredentialResolverLive, CredentialsSchema, GitHubApiError, GitHubClient, GitHubClientLive, GitHubClientTest, GroupSchema, LogLevelSchema, OnePasswordClient, OnePasswordClientLive, OnePasswordClientTest, OnePasswordError, ReposetsConfigFile, ReposetsCredentialsFile, ResolveError, ResolveSectionSchema, ResolvedRefSchema, RulesetSchema, SecretGroupSchema, SyncEngine, SyncEngineLive, SyncError, SyncLogger, SyncLoggerLive, VariableGroupSchema, buildRulesetPayload, encryptSecret, loadConfigWithDir, resolveConfigFlag };
3354
+ export { BypassActorSchema, CONFIG_FILENAME, CREDENTIALS_FILENAME, CleanupSchema, CleanupScopeSchema, ConfigFilesLive, ConfigSchema, CredentialProfileSchema, CredentialResolver, CredentialResolverLive, CredentialsSchema, GitHubApiError, GitHubClient, GitHubClientLive, GitHubClientTest, GroupSchema, LogLevelSchema, OnePasswordClient, OnePasswordClientLive, OnePasswordClientTest, OnePasswordError, ReposetsConfigFile, ReposetsCredentialsFile, ResolveError, ResolveSectionSchema, ResolvedRefSchema, RulesetSchema, SecretGroupSchema, SyncEngine, SyncEngineLive, SyncError, SyncLogger, SyncLoggerLive, VariableGroupSchema, buildRulesetPayload, encryptSecret, makeConfigFilesLive, validateConfigRefs };