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 +760 -61
- package/README.md +11 -3
- package/bin/reposets.js +128 -112
- package/index.d.ts +194 -34
- package/index.js +1 -1
- package/package.json +3 -3
- package/tsdoc-metadata.json +1 -1
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,
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
config
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
|
1412
|
-
const
|
|
1413
|
-
if (
|
|
1414
|
-
|
|
1415
|
-
|
|
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
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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)
|
|
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,
|
|
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 };
|