@vivswan/github-settings-as-code 2.0.1-main.0.g3fad2b2 → 2.0.1-main.1072.gfded5f5
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.
Potentially problematic release.
This version of @vivswan/github-settings-as-code might be problematic. Click here for more details.
- package/lib/pkg/cli.js +1 -1
- package/lib/pkg/index.d.ts +432 -0
- package/lib/pkg/index.js +1 -1
- package/lib/pkg/{src-DSbLta5Z.js → src-BFMt0V_E.js} +365 -162
- package/package.json +1 -1
package/lib/pkg/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as SNAPSHOT_ONLY_INPUTS, E as parseSnapshotFileConfig, F as concludeMerge, I as concludeRun, Kt as redactRanges, L as failRun, Rt as GithubApi, S as SNAPSHOT_INPUTS, T as parseConfig, U as PRIVATE_REPORT_CHANNELS, Wt as maskRegistry, _ as INPUT_DECLS, d as runSnapshot, f as runSingle, j as readSettingsFile, k as runMulti, o as snapshotRepository, pt as SECTIONS, s as validateSettings, t as runMerge, tt as describeProblem, u as concludeSnapshot, v as MERGE_INPUTS, y as MERGE_ONLY_INPUTS, yt as sectionGrant } from "./src-
|
|
2
|
+
import { C as SNAPSHOT_ONLY_INPUTS, E as parseSnapshotFileConfig, F as concludeMerge, I as concludeRun, Kt as redactRanges, L as failRun, Rt as GithubApi, S as SNAPSHOT_INPUTS, T as parseConfig, U as PRIVATE_REPORT_CHANNELS, Wt as maskRegistry, _ as INPUT_DECLS, d as runSnapshot, f as runSingle, j as readSettingsFile, k as runMulti, o as snapshotRepository, pt as SECTIONS, s as validateSettings, t as runMerge, tt as describeProblem, u as concludeSnapshot, v as MERGE_INPUTS, y as MERGE_ONLY_INPUTS, yt as sectionGrant } from "./src-BFMt0V_E.js";
|
|
3
3
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { ResultAsync, err, ok } from "neverthrow";
|
package/lib/pkg/index.d.ts
CHANGED
|
@@ -1925,6 +1925,22 @@ interface SectionPlan<Op extends PlannedOpBase = ErasedPlannedOp> {
|
|
|
1925
1925
|
drift: string[];
|
|
1926
1926
|
}
|
|
1927
1927
|
//#endregion
|
|
1928
|
+
//#region src/sections/repository/schema.d.ts
|
|
1929
|
+
declare const RepositoryConfig: z.ZodObject<{
|
|
1930
|
+
topics: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
1931
|
+
enable_vulnerability_alerts: z.ZodOptional<z.ZodBoolean>;
|
|
1932
|
+
enable_automated_security_fixes: z.ZodOptional<z.ZodBoolean>;
|
|
1933
|
+
enable_private_vulnerability_reporting: z.ZodOptional<z.ZodBoolean>;
|
|
1934
|
+
enable_git_lfs: z.ZodOptional<z.ZodBoolean>;
|
|
1935
|
+
enable_immutable_releases: z.ZodOptional<z.ZodBoolean>;
|
|
1936
|
+
enable_sponsorships: z.ZodOptional<z.ZodBoolean>;
|
|
1937
|
+
issue_creation_policy: z.ZodOptional<z.ZodEnum<{
|
|
1938
|
+
all: "all";
|
|
1939
|
+
collaborators_only: "collaborators_only";
|
|
1940
|
+
}>>;
|
|
1941
|
+
}, z.core.$catchall<z.ZodUnknown>>;
|
|
1942
|
+
type RepositoryConfig = z.infer<typeof RepositoryConfig>;
|
|
1943
|
+
//#endregion
|
|
1928
1944
|
//#region src/sections/shared/list-section.d.ts
|
|
1929
1945
|
/** A list section enumerates its live resources, so it is exactly a section with an undeclared policy. */
|
|
1930
1946
|
type ListSectionKey = UndeclaredPolicySection;
|
|
@@ -2470,6 +2486,49 @@ declare const GRAPHQL: {
|
|
|
2470
2486
|
};
|
|
2471
2487
|
type BranchesPlan = SectionPlan<PlannedOp<typeof ENDPOINTS, typeof GRAPHQL>>;
|
|
2472
2488
|
//#endregion
|
|
2489
|
+
//#region src/sections/actions/schema.d.ts
|
|
2490
|
+
declare const ActionsConfig: z.ZodObject<{
|
|
2491
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
2492
|
+
allowed_actions: z.ZodOptional<z.ZodEnum<{
|
|
2493
|
+
all: "all";
|
|
2494
|
+
local_only: "local_only";
|
|
2495
|
+
selected: "selected";
|
|
2496
|
+
}>>;
|
|
2497
|
+
selected_actions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
2498
|
+
default_workflow_permissions: z.ZodOptional<z.ZodEnum<{
|
|
2499
|
+
read: "read";
|
|
2500
|
+
write: "write";
|
|
2501
|
+
}>>;
|
|
2502
|
+
can_approve_pull_request_reviews: z.ZodOptional<z.ZodBoolean>;
|
|
2503
|
+
access_level: z.ZodOptional<z.ZodEnum<{
|
|
2504
|
+
none: "none";
|
|
2505
|
+
organization: "organization";
|
|
2506
|
+
user: "user";
|
|
2507
|
+
}>>;
|
|
2508
|
+
artifact_and_log_retention: z.ZodOptional<z.ZodObject<{
|
|
2509
|
+
days: z.ZodNumber;
|
|
2510
|
+
}, z.core.$strip>>;
|
|
2511
|
+
cache: z.ZodOptional<z.ZodObject<{
|
|
2512
|
+
max_cache_retention_days: z.ZodOptional<z.ZodNumber>;
|
|
2513
|
+
max_cache_size_gb: z.ZodOptional<z.ZodNumber>;
|
|
2514
|
+
}, z.core.$strict>>;
|
|
2515
|
+
oidc_customization_sub: z.ZodOptional<z.ZodObject<{
|
|
2516
|
+
use_default: z.ZodBoolean;
|
|
2517
|
+
include_claim_keys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
2518
|
+
use_immutable_subject: z.ZodOptional<z.ZodBoolean>;
|
|
2519
|
+
}, z.core.$strip>>;
|
|
2520
|
+
fork_pr_contributor_approval: z.ZodOptional<z.ZodObject<{
|
|
2521
|
+
approval_policy: z.ZodString;
|
|
2522
|
+
}, z.core.$strip>>;
|
|
2523
|
+
fork_pr_workflows_private_repos: z.ZodOptional<z.ZodObject<{
|
|
2524
|
+
run_workflows_from_fork_pull_requests: z.ZodBoolean;
|
|
2525
|
+
send_write_tokens_to_workflows: z.ZodBoolean;
|
|
2526
|
+
send_secrets_and_variables: z.ZodBoolean;
|
|
2527
|
+
require_approval_for_fork_pr_workflows: z.ZodBoolean;
|
|
2528
|
+
}, z.core.$strip>>;
|
|
2529
|
+
}, z.core.$strip>;
|
|
2530
|
+
type ActionsConfig = z.infer<typeof ActionsConfig>;
|
|
2531
|
+
//#endregion
|
|
2473
2532
|
//#region src/sections/shared/secrets-engine.d.ts
|
|
2474
2533
|
interface SecretEntry {
|
|
2475
2534
|
name: string;
|
|
@@ -3237,6 +3296,160 @@ declare const byKey: {
|
|
|
3237
3296
|
}> | undefined;
|
|
3238
3297
|
readonly variables?: never;
|
|
3239
3298
|
})>>;
|
|
3299
|
+
snapshot(ctx: PlanContext<{
|
|
3300
|
+
readonly get: {
|
|
3301
|
+
readonly route: "GET /repos/{owner}/{repo}";
|
|
3302
|
+
readonly statuses: {
|
|
3303
|
+
readonly 200: "the repository";
|
|
3304
|
+
};
|
|
3305
|
+
readonly primaryRead: {
|
|
3306
|
+
readonly notFound: "denied";
|
|
3307
|
+
};
|
|
3308
|
+
};
|
|
3309
|
+
readonly update: {
|
|
3310
|
+
readonly route: "PATCH /repos/{owner}/{repo}";
|
|
3311
|
+
readonly statuses: {
|
|
3312
|
+
readonly 200: "repository fields patched";
|
|
3313
|
+
};
|
|
3314
|
+
};
|
|
3315
|
+
readonly topics: {
|
|
3316
|
+
readonly route: "PUT /repos/{owner}/{repo}/topics";
|
|
3317
|
+
readonly statuses: {
|
|
3318
|
+
readonly 200: "topics replaced";
|
|
3319
|
+
};
|
|
3320
|
+
};
|
|
3321
|
+
readonly vulnerabilityAlertsGet: {
|
|
3322
|
+
readonly route: "GET /repos/{owner}/{repo}/vulnerability-alerts";
|
|
3323
|
+
readonly statuses: {
|
|
3324
|
+
readonly 204: "vulnerability alerts are enabled";
|
|
3325
|
+
readonly 404: "vulnerability alerts are disabled";
|
|
3326
|
+
};
|
|
3327
|
+
};
|
|
3328
|
+
readonly vulnerabilityAlertsPut: {
|
|
3329
|
+
readonly route: "PUT /repos/{owner}/{repo}/vulnerability-alerts";
|
|
3330
|
+
readonly statuses: {
|
|
3331
|
+
readonly 204: "vulnerability alerts enabled";
|
|
3332
|
+
};
|
|
3333
|
+
};
|
|
3334
|
+
readonly vulnerabilityAlertsRemove: {
|
|
3335
|
+
readonly route: "DELETE /repos/{owner}/{repo}/vulnerability-alerts";
|
|
3336
|
+
readonly statuses: {
|
|
3337
|
+
readonly 204: "vulnerability alerts disabled";
|
|
3338
|
+
};
|
|
3339
|
+
};
|
|
3340
|
+
readonly automatedSecurityFixesGet: {
|
|
3341
|
+
readonly route: "GET /repos/{owner}/{repo}/automated-security-fixes";
|
|
3342
|
+
readonly statuses: {
|
|
3343
|
+
readonly 200: "the automated security fixes state";
|
|
3344
|
+
readonly 404: "the feature is not enabled";
|
|
3345
|
+
};
|
|
3346
|
+
};
|
|
3347
|
+
readonly automatedSecurityFixesPut: {
|
|
3348
|
+
readonly route: "PUT /repos/{owner}/{repo}/automated-security-fixes";
|
|
3349
|
+
readonly statuses: {
|
|
3350
|
+
readonly 204: "automated security fixes enabled";
|
|
3351
|
+
};
|
|
3352
|
+
};
|
|
3353
|
+
readonly automatedSecurityFixesRemove: {
|
|
3354
|
+
readonly route: "DELETE /repos/{owner}/{repo}/automated-security-fixes";
|
|
3355
|
+
readonly statuses: {
|
|
3356
|
+
readonly 204: "automated security fixes disabled";
|
|
3357
|
+
};
|
|
3358
|
+
};
|
|
3359
|
+
readonly privateVulnerabilityReportingGet: {
|
|
3360
|
+
readonly route: "GET /repos/{owner}/{repo}/private-vulnerability-reporting";
|
|
3361
|
+
readonly statuses: {
|
|
3362
|
+
readonly 200: "the private vulnerability reporting state readable from the body";
|
|
3363
|
+
readonly 404: "the feature is not applicable on this repository (observed: private repos); read as not enabled";
|
|
3364
|
+
readonly 422: "the same condition as 404, alternate answer";
|
|
3365
|
+
};
|
|
3366
|
+
};
|
|
3367
|
+
readonly privateVulnerabilityReportingPut: {
|
|
3368
|
+
readonly route: "PUT /repos/{owner}/{repo}/private-vulnerability-reporting";
|
|
3369
|
+
readonly statuses: {
|
|
3370
|
+
readonly 204: "private vulnerability reporting enabled";
|
|
3371
|
+
};
|
|
3372
|
+
};
|
|
3373
|
+
readonly privateVulnerabilityReportingRemove: {
|
|
3374
|
+
readonly route: "DELETE /repos/{owner}/{repo}/private-vulnerability-reporting";
|
|
3375
|
+
readonly statuses: {
|
|
3376
|
+
readonly 204: "private vulnerability reporting disabled";
|
|
3377
|
+
readonly 404: "the feature is not applicable, so it is already off";
|
|
3378
|
+
readonly 422: "the feature is not applicable, so it is already off";
|
|
3379
|
+
};
|
|
3380
|
+
};
|
|
3381
|
+
readonly immutableReleasesGet: {
|
|
3382
|
+
readonly route: "GET /repos/{owner}/{repo}/immutable-releases";
|
|
3383
|
+
readonly statuses: {
|
|
3384
|
+
readonly 200: "the immutable releases state readable from the body";
|
|
3385
|
+
readonly 404: "immutable releases are not enabled";
|
|
3386
|
+
};
|
|
3387
|
+
};
|
|
3388
|
+
readonly immutableReleasesPut: {
|
|
3389
|
+
readonly route: "PUT /repos/{owner}/{repo}/immutable-releases";
|
|
3390
|
+
readonly statuses: {
|
|
3391
|
+
readonly 204: "immutable releases enabled";
|
|
3392
|
+
readonly 409: "the repository owner enforces immutable releases";
|
|
3393
|
+
};
|
|
3394
|
+
};
|
|
3395
|
+
readonly immutableReleasesRemove: {
|
|
3396
|
+
readonly route: "DELETE /repos/{owner}/{repo}/immutable-releases";
|
|
3397
|
+
readonly statuses: {
|
|
3398
|
+
readonly 204: "immutable releases disabled";
|
|
3399
|
+
readonly 409: "the repository owner enforces immutable releases";
|
|
3400
|
+
};
|
|
3401
|
+
};
|
|
3402
|
+
readonly lfsPut: {
|
|
3403
|
+
readonly route: "PUT /repos/{owner}/{repo}/lfs";
|
|
3404
|
+
readonly statuses: {
|
|
3405
|
+
readonly 202: "Git LFS enabled (GitHub processes the change asynchronously)";
|
|
3406
|
+
};
|
|
3407
|
+
readonly denialHint: string;
|
|
3408
|
+
readonly alwaysRewrite: true;
|
|
3409
|
+
};
|
|
3410
|
+
readonly lfsRemove: {
|
|
3411
|
+
readonly route: "DELETE /repos/{owner}/{repo}/lfs";
|
|
3412
|
+
readonly statuses: {
|
|
3413
|
+
readonly 204: "Git LFS disabled";
|
|
3414
|
+
};
|
|
3415
|
+
readonly denialHint: string;
|
|
3416
|
+
readonly alwaysRewrite: true;
|
|
3417
|
+
};
|
|
3418
|
+
}, {
|
|
3419
|
+
readonly featuresQuery: {
|
|
3420
|
+
readonly name: "RepositoryFeatures";
|
|
3421
|
+
readonly kind: "read";
|
|
3422
|
+
readonly query: "query RepositoryFeatures($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { id hasSponsorshipsEnabled issueCreationPolicy } }";
|
|
3423
|
+
readonly outcomes: {
|
|
3424
|
+
readonly ok: "the sponsor-button and issue-creation-policy state, plus the node id the mutation addresses";
|
|
3425
|
+
};
|
|
3426
|
+
} & {
|
|
3427
|
+
readonly _variables?: {
|
|
3428
|
+
owner: string;
|
|
3429
|
+
repo: string;
|
|
3430
|
+
} | undefined;
|
|
3431
|
+
};
|
|
3432
|
+
readonly updateFeatures: {
|
|
3433
|
+
readonly name: "UpdateRepositoryFeatures";
|
|
3434
|
+
readonly kind: "write";
|
|
3435
|
+
readonly query: "mutation UpdateRepositoryFeatures(\n $repositoryId: ID!\n $hasSponsorshipsEnabled: Boolean\n $issueCreationPolicy: IssueCreationPolicy\n ) {\n updateRepository(\n input: {\n repositoryId: $repositoryId\n hasSponsorshipsEnabled: $hasSponsorshipsEnabled\n issueCreationPolicy: $issueCreationPolicy\n }\n ) {\n repository { hasSponsorshipsEnabled issueCreationPolicy }\n }\n }";
|
|
3436
|
+
readonly outcomes: {
|
|
3437
|
+
readonly ok: "the carried values set; the echoed state verifies each one took";
|
|
3438
|
+
};
|
|
3439
|
+
} & {
|
|
3440
|
+
readonly _variables?: {
|
|
3441
|
+
repositoryId: string;
|
|
3442
|
+
hasSponsorshipsEnabled?: boolean;
|
|
3443
|
+
issueCreationPolicy?: ({
|
|
3444
|
+
readonly all: "ALL";
|
|
3445
|
+
readonly collaborators_only: "COLLABORATORS_ONLY";
|
|
3446
|
+
})["all" | "collaborators_only"];
|
|
3447
|
+
} | undefined;
|
|
3448
|
+
};
|
|
3449
|
+
}>): Promise<{
|
|
3450
|
+
value: RepositoryConfig;
|
|
3451
|
+
notes: string[];
|
|
3452
|
+
}>;
|
|
3240
3453
|
};
|
|
3241
3454
|
labels: ListSectionModule<"labels", {
|
|
3242
3455
|
readonly list: {
|
|
@@ -3450,6 +3663,68 @@ declare const byKey: {
|
|
|
3450
3663
|
}> | undefined;
|
|
3451
3664
|
readonly variables?: never;
|
|
3452
3665
|
})>>;
|
|
3666
|
+
snapshot(ctx: PlanContext<{
|
|
3667
|
+
readonly list: {
|
|
3668
|
+
readonly route: "GET /repos/{owner}/{repo}/rulesets";
|
|
3669
|
+
readonly statuses: {
|
|
3670
|
+
readonly 200: "the repository ruleset list";
|
|
3671
|
+
};
|
|
3672
|
+
readonly primaryRead: {
|
|
3673
|
+
readonly notFound: "denied";
|
|
3674
|
+
};
|
|
3675
|
+
};
|
|
3676
|
+
readonly create: {
|
|
3677
|
+
readonly route: "POST /repos/{owner}/{repo}/rulesets";
|
|
3678
|
+
readonly statuses: {
|
|
3679
|
+
readonly 201: "ruleset created";
|
|
3680
|
+
};
|
|
3681
|
+
readonly hints: {
|
|
3682
|
+
readonly 422: "Usually this means a rules[].type GitHub does not recognize, or \"parameters\" that do not fit that rule type (rules pass through verbatim, so a typo reaches GitHub unchanged)";
|
|
3683
|
+
};
|
|
3684
|
+
};
|
|
3685
|
+
readonly get: {
|
|
3686
|
+
readonly route: "GET /repos/{owner}/{repo}/rulesets/{ruleset_id}";
|
|
3687
|
+
readonly statuses: {
|
|
3688
|
+
readonly 200: "the ruleset";
|
|
3689
|
+
};
|
|
3690
|
+
};
|
|
3691
|
+
readonly update: {
|
|
3692
|
+
readonly route: "PUT /repos/{owner}/{repo}/rulesets/{ruleset_id}";
|
|
3693
|
+
readonly statuses: {
|
|
3694
|
+
readonly 200: "ruleset updated";
|
|
3695
|
+
};
|
|
3696
|
+
readonly hints: {
|
|
3697
|
+
readonly 422: "Usually this means a rules[].type GitHub does not recognize, or \"parameters\" that do not fit that rule type (rules pass through verbatim, so a typo reaches GitHub unchanged)";
|
|
3698
|
+
};
|
|
3699
|
+
};
|
|
3700
|
+
readonly remove: {
|
|
3701
|
+
readonly route: "DELETE /repos/{owner}/{repo}/rulesets/{ruleset_id}";
|
|
3702
|
+
readonly statuses: {
|
|
3703
|
+
readonly 204: "ruleset deleted";
|
|
3704
|
+
};
|
|
3705
|
+
};
|
|
3706
|
+
}, Readonly<Record<string, GraphqlOpDecl>>>): Promise<{
|
|
3707
|
+
value: undefined;
|
|
3708
|
+
notes: string[];
|
|
3709
|
+
} | {
|
|
3710
|
+
value: UndeclaredPolicyList<{
|
|
3711
|
+
name: string;
|
|
3712
|
+
target?: "branch" | "push" | "tag" | undefined;
|
|
3713
|
+
enforcement?: string | undefined;
|
|
3714
|
+
conditions?: {
|
|
3715
|
+
ref_name?: {
|
|
3716
|
+
include?: string[] | undefined;
|
|
3717
|
+
exclude?: string[] | undefined;
|
|
3718
|
+
} | undefined;
|
|
3719
|
+
} | undefined;
|
|
3720
|
+
rules?: {
|
|
3721
|
+
type: string;
|
|
3722
|
+
parameters?: Record<string, unknown> | undefined;
|
|
3723
|
+
}[] | undefined;
|
|
3724
|
+
bypass_actors?: Record<string, unknown>[] | undefined;
|
|
3725
|
+
}>;
|
|
3726
|
+
notes: string[];
|
|
3727
|
+
}>;
|
|
3453
3728
|
};
|
|
3454
3729
|
environments: {
|
|
3455
3730
|
key: "environments";
|
|
@@ -5242,6 +5517,163 @@ declare const byKey: {
|
|
|
5242
5517
|
}> | undefined;
|
|
5243
5518
|
readonly variables?: never;
|
|
5244
5519
|
})>>;
|
|
5520
|
+
snapshot(ctx: PlanContext<{
|
|
5521
|
+
readonly getPermissions: {
|
|
5522
|
+
readonly route: "GET /repos/{owner}/{repo}/actions/permissions";
|
|
5523
|
+
readonly statuses: {
|
|
5524
|
+
readonly 200: "the Actions permissions policy";
|
|
5525
|
+
};
|
|
5526
|
+
readonly primaryRead: {
|
|
5527
|
+
readonly notFound: "denied";
|
|
5528
|
+
};
|
|
5529
|
+
};
|
|
5530
|
+
readonly putPermissions: {
|
|
5531
|
+
readonly route: "PUT /repos/{owner}/{repo}/actions/permissions";
|
|
5532
|
+
readonly statuses: {
|
|
5533
|
+
readonly 204: "Actions permissions policy applied";
|
|
5534
|
+
};
|
|
5535
|
+
};
|
|
5536
|
+
readonly getSelected: {
|
|
5537
|
+
readonly route: "GET /repos/{owner}/{repo}/actions/permissions/selected-actions";
|
|
5538
|
+
readonly statuses: {
|
|
5539
|
+
readonly 200: "the selected-actions allowlist";
|
|
5540
|
+
readonly 404: "no allowlist because the policy is not selected";
|
|
5541
|
+
readonly 409: "the allowed_actions policy is not selected, so the allowlist does not apply";
|
|
5542
|
+
};
|
|
5543
|
+
};
|
|
5544
|
+
readonly putSelected: {
|
|
5545
|
+
readonly route: "PUT /repos/{owner}/{repo}/actions/permissions/selected-actions";
|
|
5546
|
+
readonly statuses: {
|
|
5547
|
+
readonly 204: "selected-actions allowlist applied";
|
|
5548
|
+
};
|
|
5549
|
+
};
|
|
5550
|
+
readonly getWorkflow: {
|
|
5551
|
+
readonly route: "GET /repos/{owner}/{repo}/actions/permissions/workflow";
|
|
5552
|
+
readonly statuses: {
|
|
5553
|
+
readonly 200: "the workflow token permissions";
|
|
5554
|
+
};
|
|
5555
|
+
};
|
|
5556
|
+
readonly putWorkflow: {
|
|
5557
|
+
readonly route: "PUT /repos/{owner}/{repo}/actions/permissions/workflow";
|
|
5558
|
+
readonly statuses: {
|
|
5559
|
+
readonly 204: "workflow token permissions applied";
|
|
5560
|
+
};
|
|
5561
|
+
};
|
|
5562
|
+
readonly getAccess: {
|
|
5563
|
+
readonly route: "GET /repos/{owner}/{repo}/actions/permissions/access";
|
|
5564
|
+
readonly statuses: {
|
|
5565
|
+
readonly 200: "the workflows access level";
|
|
5566
|
+
};
|
|
5567
|
+
};
|
|
5568
|
+
readonly putAccess: {
|
|
5569
|
+
readonly route: "PUT /repos/{owner}/{repo}/actions/permissions/access";
|
|
5570
|
+
readonly statuses: {
|
|
5571
|
+
readonly 204: "workflows access level applied";
|
|
5572
|
+
};
|
|
5573
|
+
};
|
|
5574
|
+
readonly getRetention: {
|
|
5575
|
+
readonly route: "GET /repos/{owner}/{repo}/actions/permissions/artifact-and-log-retention";
|
|
5576
|
+
readonly statuses: {
|
|
5577
|
+
readonly 200: "the artifact and log retention window";
|
|
5578
|
+
};
|
|
5579
|
+
};
|
|
5580
|
+
readonly putRetention: {
|
|
5581
|
+
readonly route: "PUT /repos/{owner}/{repo}/actions/permissions/artifact-and-log-retention";
|
|
5582
|
+
readonly statuses: {
|
|
5583
|
+
readonly 204: "artifact and log retention applied";
|
|
5584
|
+
};
|
|
5585
|
+
readonly hints: {
|
|
5586
|
+
readonly 422: "the retention window must be a whole number of days within the plan's maximum; see the artifact-and-log-retention endpoint documentation";
|
|
5587
|
+
};
|
|
5588
|
+
};
|
|
5589
|
+
readonly getCacheRetention: {
|
|
5590
|
+
readonly route: "GET /repos/{owner}/{repo}/actions/cache/retention-limit";
|
|
5591
|
+
readonly statuses: {
|
|
5592
|
+
readonly 200: "the cache retention limit";
|
|
5593
|
+
};
|
|
5594
|
+
};
|
|
5595
|
+
readonly putCacheRetention: {
|
|
5596
|
+
readonly route: "PUT /repos/{owner}/{repo}/actions/cache/retention-limit";
|
|
5597
|
+
readonly statuses: {
|
|
5598
|
+
readonly 204: "cache retention limit applied";
|
|
5599
|
+
};
|
|
5600
|
+
readonly hints: {
|
|
5601
|
+
readonly 400: "the retention limit must be a whole number of days within the allowed range; see the cache retention-limit endpoint documentation";
|
|
5602
|
+
};
|
|
5603
|
+
};
|
|
5604
|
+
readonly getCacheStorage: {
|
|
5605
|
+
readonly route: "GET /repos/{owner}/{repo}/actions/cache/storage-limit";
|
|
5606
|
+
readonly statuses: {
|
|
5607
|
+
readonly 200: "the cache storage limit";
|
|
5608
|
+
};
|
|
5609
|
+
};
|
|
5610
|
+
readonly putCacheStorage: {
|
|
5611
|
+
readonly route: "PUT /repos/{owner}/{repo}/actions/cache/storage-limit";
|
|
5612
|
+
readonly statuses: {
|
|
5613
|
+
readonly 204: "cache storage limit applied";
|
|
5614
|
+
};
|
|
5615
|
+
readonly hints: {
|
|
5616
|
+
readonly 400: "the storage limit must be a whole number of gigabytes within the allowed range; see the cache storage-limit endpoint documentation";
|
|
5617
|
+
};
|
|
5618
|
+
};
|
|
5619
|
+
readonly getOidcSub: {
|
|
5620
|
+
readonly route: "GET /repos/{owner}/{repo}/actions/oidc/customization/sub";
|
|
5621
|
+
readonly statuses: {
|
|
5622
|
+
readonly 200: "the OIDC subject claim template";
|
|
5623
|
+
};
|
|
5624
|
+
readonly permission: {
|
|
5625
|
+
readonly repo: readonly ["actions"];
|
|
5626
|
+
};
|
|
5627
|
+
};
|
|
5628
|
+
readonly putOidcSub: {
|
|
5629
|
+
readonly route: "PUT /repos/{owner}/{repo}/actions/oidc/customization/sub";
|
|
5630
|
+
readonly statuses: {
|
|
5631
|
+
readonly 201: "OIDC subject claim template applied";
|
|
5632
|
+
};
|
|
5633
|
+
readonly permission: {
|
|
5634
|
+
readonly repo: readonly ["actions"];
|
|
5635
|
+
};
|
|
5636
|
+
readonly hints: {
|
|
5637
|
+
readonly 400: "include_claim_keys entries must be unique claim keys of the OIDC token (alphanumeric and underscores only); see the OIDC subject claim customization endpoint documentation";
|
|
5638
|
+
readonly 422: "include_claim_keys entries must be unique claim keys of the OIDC token (alphanumeric and underscores only); see the OIDC subject claim customization endpoint documentation";
|
|
5639
|
+
};
|
|
5640
|
+
};
|
|
5641
|
+
readonly getForkPrApproval: {
|
|
5642
|
+
readonly route: "GET /repos/{owner}/{repo}/actions/permissions/fork-pr-contributor-approval";
|
|
5643
|
+
readonly statuses: {
|
|
5644
|
+
readonly 200: "the fork PR contributor approval policy";
|
|
5645
|
+
};
|
|
5646
|
+
};
|
|
5647
|
+
readonly putForkPrApproval: {
|
|
5648
|
+
readonly route: "PUT /repos/{owner}/{repo}/actions/permissions/fork-pr-contributor-approval";
|
|
5649
|
+
readonly statuses: {
|
|
5650
|
+
readonly 204: "fork PR contributor approval policy applied";
|
|
5651
|
+
};
|
|
5652
|
+
readonly hints: {
|
|
5653
|
+
readonly 422: "approval_policy must be one of the contributor approval policies GitHub accepts; see the fork-pr-contributor-approval endpoint documentation";
|
|
5654
|
+
};
|
|
5655
|
+
};
|
|
5656
|
+
readonly getForkPrPrivate: {
|
|
5657
|
+
readonly route: "GET /repos/{owner}/{repo}/actions/permissions/fork-pr-workflows-private-repos";
|
|
5658
|
+
readonly statuses: {
|
|
5659
|
+
readonly 200: "the private-repo fork PR workflow settings";
|
|
5660
|
+
};
|
|
5661
|
+
readonly denialHint: "the fork PR workflow settings are documented for private repositories, so a denial here can also mean the repository is public";
|
|
5662
|
+
};
|
|
5663
|
+
readonly putForkPrPrivate: {
|
|
5664
|
+
readonly route: "PUT /repos/{owner}/{repo}/actions/permissions/fork-pr-workflows-private-repos";
|
|
5665
|
+
readonly statuses: {
|
|
5666
|
+
readonly 204: "private-repo fork PR workflow settings applied";
|
|
5667
|
+
};
|
|
5668
|
+
readonly denialHint: "the fork PR workflow settings are documented for private repositories, so a denial here can also mean the repository is public";
|
|
5669
|
+
readonly hints: {
|
|
5670
|
+
readonly 422: "the settings object must carry run_workflows_from_fork_pull_requests with boolean toggles only; see the fork-pr-workflows-private-repos endpoint documentation";
|
|
5671
|
+
};
|
|
5672
|
+
};
|
|
5673
|
+
}, Readonly<Record<string, GraphqlOpDecl>>>): Promise<{
|
|
5674
|
+
value: ActionsConfig;
|
|
5675
|
+
notes: string[];
|
|
5676
|
+
}>;
|
|
5245
5677
|
};
|
|
5246
5678
|
actions_secrets: RepoSecretsSectionModule<"actions_secrets">;
|
|
5247
5679
|
dependabot_secrets: RepoSecretsSectionModule<"dependabot_secrets">;
|
package/lib/pkg/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as renderSnapshotYaml, A as parseSettingsDoc, At as parseReposInput, B as planRedaction, Bt as SECRET_TRANSPORT_WITHHELD, C as SNAPSHOT_ONLY_INPUTS, Ct as endpointMethod, D as DEFAULT_SETTINGS_FILE, Dt as SECTION_KEYS, E as parseSnapshotFileConfig, Et as PROBOT_PARITY_KEYS, F as concludeMerge, Ft as VISIBILITY_FILTERS, G as openReportChannel, Gt as prefixedIo, H as toPublicView, Ht as isRateLimitError, I as concludeRun, It as discoverRepos, J as MARKER_LABEL_CONFIG, Jt as resolveCentralTargets, K as ISSUE_TITLE, Kt as redactRanges, L as failRun, Lt as DEFAULT_API_VERSION, M as createVisibilityResolver, Mt as ARCHIVED_FILTERS, N as getRepoFile, Nt as DEFAULT_DISCOVERY_FILTERS, O as resolveTargets, Ot as SettingsFile, P as MERGE_RESULT, Pt as FORKS_FILTERS, Q as parseRecipient, R as PRIVATE_REPOS_POLICIES, Rt as GithubApi, S as SNAPSHOT_INPUTS, St as grantFor, T as parseConfig, Tt as DOCUMENT_DIRECTIVE_KEYS, U as PRIVATE_REPORT_CHANNELS, Ut as collectingIo, V as publicDetail, Vt as isPermissionError, W as applyMarkerInjection, Wt as maskRegistry, X as deliverArtifactReport, Xt as parseRepoSlug, Y as composeReport, Yt as dedupeTargets, Z as encryptReport, _ as INPUT_DECLS, _t as denialPosture, a as snapshotRepositories, at as preflightProbe, b as MERGE_REJECTED_INPUTS, bt as sectionOperations, c as SNAPSHOT_RESULTS, ct as validateSettingsDoc, d as runSnapshot, dt as mergeLayers, et as RERUN_ADVICE, f as runSingle, ft as stripNulls, g as FILTER_INPUTS, gt as sectionModule, h as DEFAULT_PRIVATE_REPOS, ht as allGraphqlOps, i as renderMergedYaml, it as REPO_RESULTS, j as readSettingsFile, jt as AFFILIATIONS, k as runMulti, kt as UNDECLARED_POLICY_SECTIONS, l as SNAPSHOT_SCHEMA_URL, lt as worstOf, m as readLayerFiles, mt as allEndpoints, n as applyRepository, nt as quoteList, o as snapshotRepository, ot as runForRepo, p as foldLayers, pt as SECTIONS, q as MARKER_LABEL, qt as silentIo, r as checkRepository, rt as SectionSelection, s as validateSettings, st as skippedSectionKeys, t as runMerge, tt as describeProblem, u as concludeSnapshot, ut as describeOptOut, v as MERGE_INPUTS, vt as readGating, w as SNAPSHOT_REJECTED_INPUTS, wt as endpointPath, x as MODES, xt as writeGatedReads, y as MERGE_ONLY_INPUTS, yt as sectionGrant, z as capturingIo, zt as SECRET_RESPONSE_WITHHELD } from "./src-
|
|
1
|
+
import { $ as renderSnapshotYaml, A as parseSettingsDoc, At as parseReposInput, B as planRedaction, Bt as SECRET_TRANSPORT_WITHHELD, C as SNAPSHOT_ONLY_INPUTS, Ct as endpointMethod, D as DEFAULT_SETTINGS_FILE, Dt as SECTION_KEYS, E as parseSnapshotFileConfig, Et as PROBOT_PARITY_KEYS, F as concludeMerge, Ft as VISIBILITY_FILTERS, G as openReportChannel, Gt as prefixedIo, H as toPublicView, Ht as isRateLimitError, I as concludeRun, It as discoverRepos, J as MARKER_LABEL_CONFIG, Jt as resolveCentralTargets, K as ISSUE_TITLE, Kt as redactRanges, L as failRun, Lt as DEFAULT_API_VERSION, M as createVisibilityResolver, Mt as ARCHIVED_FILTERS, N as getRepoFile, Nt as DEFAULT_DISCOVERY_FILTERS, O as resolveTargets, Ot as SettingsFile, P as MERGE_RESULT, Pt as FORKS_FILTERS, Q as parseRecipient, R as PRIVATE_REPOS_POLICIES, Rt as GithubApi, S as SNAPSHOT_INPUTS, St as grantFor, T as parseConfig, Tt as DOCUMENT_DIRECTIVE_KEYS, U as PRIVATE_REPORT_CHANNELS, Ut as collectingIo, V as publicDetail, Vt as isPermissionError, W as applyMarkerInjection, Wt as maskRegistry, X as deliverArtifactReport, Xt as parseRepoSlug, Y as composeReport, Yt as dedupeTargets, Z as encryptReport, _ as INPUT_DECLS, _t as denialPosture, a as snapshotRepositories, at as preflightProbe, b as MERGE_REJECTED_INPUTS, bt as sectionOperations, c as SNAPSHOT_RESULTS, ct as validateSettingsDoc, d as runSnapshot, dt as mergeLayers, et as RERUN_ADVICE, f as runSingle, ft as stripNulls, g as FILTER_INPUTS, gt as sectionModule, h as DEFAULT_PRIVATE_REPOS, ht as allGraphqlOps, i as renderMergedYaml, it as REPO_RESULTS, j as readSettingsFile, jt as AFFILIATIONS, k as runMulti, kt as UNDECLARED_POLICY_SECTIONS, l as SNAPSHOT_SCHEMA_URL, lt as worstOf, m as readLayerFiles, mt as allEndpoints, n as applyRepository, nt as quoteList, o as snapshotRepository, ot as runForRepo, p as foldLayers, pt as SECTIONS, q as MARKER_LABEL, qt as silentIo, r as checkRepository, rt as SectionSelection, s as validateSettings, st as skippedSectionKeys, t as runMerge, tt as describeProblem, u as concludeSnapshot, ut as describeOptOut, v as MERGE_INPUTS, vt as readGating, w as SNAPSHOT_REJECTED_INPUTS, wt as endpointPath, x as MODES, xt as writeGatedReads, y as MERGE_ONLY_INPUTS, yt as sectionGrant, z as capturingIo, zt as SECRET_RESPONSE_WITHHELD } from "./src-BFMt0V_E.js";
|
|
2
2
|
export { AFFILIATIONS, ARCHIVED_FILTERS, DEFAULT_API_VERSION, DEFAULT_DISCOVERY_FILTERS, DEFAULT_PRIVATE_REPOS, DEFAULT_SETTINGS_FILE, DOCUMENT_DIRECTIVE_KEYS, FILTER_INPUTS, FORKS_FILTERS, GithubApi, INPUT_DECLS, ISSUE_TITLE, MARKER_LABEL, MARKER_LABEL_CONFIG, MERGE_INPUTS, MERGE_ONLY_INPUTS, MERGE_REJECTED_INPUTS, MERGE_RESULT, MODES, PRIVATE_REPORT_CHANNELS, PRIVATE_REPOS_POLICIES, PROBOT_PARITY_KEYS, REPO_RESULTS, RERUN_ADVICE, SECRET_RESPONSE_WITHHELD, SECRET_TRANSPORT_WITHHELD, SECTIONS, SECTION_KEYS, SNAPSHOT_INPUTS, SNAPSHOT_ONLY_INPUTS, SNAPSHOT_REJECTED_INPUTS, SNAPSHOT_RESULTS, SNAPSHOT_SCHEMA_URL, SectionSelection, SettingsFile, UNDECLARED_POLICY_SECTIONS, VISIBILITY_FILTERS, allEndpoints, allGraphqlOps, applyMarkerInjection, applyRepository, capturingIo, checkRepository, collectingIo, composeReport, concludeMerge, concludeRun, concludeSnapshot, createVisibilityResolver, dedupeTargets, deliverArtifactReport, denialPosture, describeOptOut, describeProblem, discoverRepos, encryptReport, endpointMethod, endpointPath, failRun, foldLayers, getRepoFile, grantFor, isPermissionError, isRateLimitError, maskRegistry, mergeLayers, openReportChannel, parseConfig, parseRecipient, parseRepoSlug, parseReposInput, parseSettingsDoc, parseSnapshotFileConfig, planRedaction, prefixedIo, preflightProbe, publicDetail, quoteList, readGating, readLayerFiles, readSettingsFile, redactRanges, renderMergedYaml, renderSnapshotYaml, resolveCentralTargets, resolveTargets, runForRepo, runMerge, runMulti, runSingle, runSnapshot, sectionGrant, sectionModule, sectionOperations, silentIo, skippedSectionKeys, snapshotRepositories, snapshotRepository, stripNulls, toPublicView, validateSettings, validateSettingsDoc, worstOf, writeGatedReads };
|
|
@@ -2699,6 +2699,100 @@ function planContext(meta, api, repo) {
|
|
|
2699
2699
|
};
|
|
2700
2700
|
}
|
|
2701
2701
|
//#endregion
|
|
2702
|
+
//#region src/sections/shared/snapshot-helpers.ts
|
|
2703
|
+
function defOf(schema) {
|
|
2704
|
+
return schema._zod.def;
|
|
2705
|
+
}
|
|
2706
|
+
/** The schema types the projection treats as leaves: the live value passes through verbatim. */
|
|
2707
|
+
const LEAF_TYPES = /* @__PURE__ */ new Set([
|
|
2708
|
+
"string",
|
|
2709
|
+
"number",
|
|
2710
|
+
"int",
|
|
2711
|
+
"boolean",
|
|
2712
|
+
"enum",
|
|
2713
|
+
"literal",
|
|
2714
|
+
"unknown",
|
|
2715
|
+
"null"
|
|
2716
|
+
]);
|
|
2717
|
+
function isPlainObject(value) {
|
|
2718
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* A live value projected onto a schema slice, so server-assigned fields fall away without a hand
|
|
2722
|
+
* list per section. A nested `null` the slice cannot hold is GitHub's "no value" (a not-configured
|
|
2723
|
+
* setup's runner_type) and its key is omitted; a value the slice rejects at the root stays, so the
|
|
2724
|
+
* engine's validation names a body outside the shape instead of the section vanishing. A
|
|
2725
|
+
* passthrough slice (a catchall other than never) keeps every live key by design, so a section on
|
|
2726
|
+
* one names the keys it reads back itself. The casts are the boundary the engine validates behind.
|
|
2727
|
+
*/
|
|
2728
|
+
function projectOntoSchema(schema, live) {
|
|
2729
|
+
if (live === null && !schema.safeParse(null).success) return live;
|
|
2730
|
+
return project(schema, live);
|
|
2731
|
+
}
|
|
2732
|
+
function project(schema, live) {
|
|
2733
|
+
if (live === void 0) return;
|
|
2734
|
+
if (live === null) return schema.safeParse(null).success ? null : void 0;
|
|
2735
|
+
const def = defOf(schema);
|
|
2736
|
+
switch (def.type) {
|
|
2737
|
+
case "optional":
|
|
2738
|
+
case "nullable": return project(def.innerType, live);
|
|
2739
|
+
case "object": {
|
|
2740
|
+
if (!isPlainObject(live)) return live;
|
|
2741
|
+
const shape = def.shape ?? {};
|
|
2742
|
+
const out = {};
|
|
2743
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
2744
|
+
const projected = project(child, live[key]);
|
|
2745
|
+
if (projected !== void 0) out[key] = projected;
|
|
2746
|
+
}
|
|
2747
|
+
if (def.catchall !== void 0 && defOf(def.catchall).type !== "never") {
|
|
2748
|
+
for (const [key, value] of Object.entries(live)) if (!(key in shape) && value !== void 0) out[key] = value;
|
|
2749
|
+
}
|
|
2750
|
+
return out;
|
|
2751
|
+
}
|
|
2752
|
+
case "array": return Array.isArray(live) ? live.map((item) => project(def.element, item)) : live;
|
|
2753
|
+
case "record": return isPlainObject(live) ? Object.fromEntries(Object.entries(live).map(([key, value]) => [key, project(def.valueType, value)])) : live;
|
|
2754
|
+
case "union": {
|
|
2755
|
+
const option = (def.options ?? []).find((candidate) => candidate.safeParse(live).success);
|
|
2756
|
+
return option === void 0 ? live : project(option, live);
|
|
2757
|
+
}
|
|
2758
|
+
default:
|
|
2759
|
+
if (!LEAF_TYPES.has(def.type)) throw new Error(`projectOntoSchema(): unhandled schema type "${def.type}" - teach the projection its walk before authoring it in a section slice`);
|
|
2760
|
+
return live;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Refuse a live list holding two resources under one identity (GitHub allows repeated deploy-key
|
|
2765
|
+
* titles and hook urls): the planner manages one resource per identity and would refuse the
|
|
2766
|
+
* snapshot's own file, so the snapshot fails here, naming the pairs, instead of emitting it.
|
|
2767
|
+
*/
|
|
2768
|
+
function rejectLiveDuplicates(section, noun, items, keyOf, describe) {
|
|
2769
|
+
const collisions = collidingPairs(items, keyOf, describe);
|
|
2770
|
+
if (collisions.length > 0) throw new Error(`${section.key}: GitHub holds ${noun}s that resolve to one identity: ${collisions.join("; ")}. This section manages one ${noun} per identity, so the snapshot cannot declare them; delete all but one of each on GitHub, then snapshot again`);
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* One read of a snapshot that a denial may take out without failing the section: a
|
|
2774
|
+
* PermissionDenied becomes a note naming the key left out and the grant advice, anything else
|
|
2775
|
+
* propagates. For a section whose keys sit behind different grants (repository, actions).
|
|
2776
|
+
*/
|
|
2777
|
+
async function readOrNote(notes, label, read) {
|
|
2778
|
+
try {
|
|
2779
|
+
return { value: await read() };
|
|
2780
|
+
} catch (error) {
|
|
2781
|
+
if (error instanceof PermissionDenied) {
|
|
2782
|
+
notes.push(`${label}: left out of the snapshot - ${error.detail}`);
|
|
2783
|
+
return { denied: true };
|
|
2784
|
+
}
|
|
2785
|
+
throw error;
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
/** A knobbed section's snapshot value: its entries under the section's own default policy, spelled out. */
|
|
2789
|
+
function knobbedSnapshot(section, entries) {
|
|
2790
|
+
return {
|
|
2791
|
+
_undeclared: defaultUndeclaredPolicy(section),
|
|
2792
|
+
entries
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
//#endregion
|
|
2702
2796
|
//#region src/sections/actions/index.ts
|
|
2703
2797
|
const permission$14 = { repo: ["administration"] };
|
|
2704
2798
|
const OIDC_TEMPLATE_HINT = "include_claim_keys entries must be unique claim keys of the OIDC token (alphanumeric and underscores only); see the OIDC subject claim customization endpoint documentation";
|
|
@@ -2830,32 +2924,46 @@ const LiveOidcSub = z.looseObject({ include_claim_keys: z.array(z.string()).null
|
|
|
2830
2924
|
/** `N` is inferred from the GET alone, so a PUT of another name does not compile. */
|
|
2831
2925
|
function endpointRouted(wiring) {
|
|
2832
2926
|
const body = wiring.body ?? ((declared) => declared);
|
|
2833
|
-
return {
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2927
|
+
return {
|
|
2928
|
+
plan: async (ctx, _section, declared, plan) => {
|
|
2929
|
+
const live = await ctx.read[wiring.get].call();
|
|
2930
|
+
const payload = body(declared);
|
|
2931
|
+
const drift = subsetDiff(payload, live, wiring.label);
|
|
2932
|
+
if (hasDrift(drift)) plan.ops.push({
|
|
2933
|
+
role: wiring.put,
|
|
2934
|
+
payload: plainData(payload),
|
|
2935
|
+
describe: wiring.describe,
|
|
2936
|
+
drift,
|
|
2937
|
+
change: wiring.applied
|
|
2938
|
+
});
|
|
2939
|
+
},
|
|
2940
|
+
snapshot: async (ctx) => wiring.read(await ctx.read[wiring.get].call())
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
function sliceOf(key) {
|
|
2944
|
+
const slice = ActionsConfig.shape[key];
|
|
2945
|
+
return (live) => projectOntoSchema(slice, live);
|
|
2845
2946
|
}
|
|
2846
2947
|
const KEY_DESTINATION = {
|
|
2847
2948
|
enabled: "base",
|
|
2848
2949
|
allowed_actions: "base",
|
|
2849
|
-
selected_actions: {
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2950
|
+
selected_actions: {
|
|
2951
|
+
plan: async (ctx, _section, declared, plan) => {
|
|
2952
|
+
const probe = await ctx.read.getSelected.probeAbsent();
|
|
2953
|
+
const drift = "missing" in probe ? ["actions.selected: no selected-actions allowlist is readable (the live allowed_actions policy is not \"selected\", or no allowlist has been set); apply will set the declared allowlist"] : subsetDiff(declared, probe.data, "actions.selected");
|
|
2954
|
+
if (hasDrift(drift)) plan.ops.push({
|
|
2955
|
+
role: "putSelected",
|
|
2956
|
+
payload: plainData(declared),
|
|
2957
|
+
drift,
|
|
2958
|
+
change: "applied selected-actions policy"
|
|
2959
|
+
});
|
|
2960
|
+
},
|
|
2961
|
+
snapshot: async (ctx, _section, base) => {
|
|
2962
|
+
if (base.allowed_actions !== "selected") return;
|
|
2963
|
+
const probe = await ctx.read.getSelected.probeAbsent();
|
|
2964
|
+
return "missing" in probe ? void 0 : sliceOf("selected_actions")(probe.data);
|
|
2965
|
+
}
|
|
2966
|
+
},
|
|
2859
2967
|
default_workflow_permissions: "workflow",
|
|
2860
2968
|
can_approve_pull_request_reviews: "workflow",
|
|
2861
2969
|
access_level: endpointRouted({
|
|
@@ -2863,60 +2971,77 @@ const KEY_DESTINATION = {
|
|
|
2863
2971
|
put: "putAccess",
|
|
2864
2972
|
label: "actions.access",
|
|
2865
2973
|
applied: "applied workflows access level",
|
|
2866
|
-
body: (value) => ({ access_level: value })
|
|
2974
|
+
body: (value) => ({ access_level: value }),
|
|
2975
|
+
read: (live) => sliceOf("access_level")(live.access_level)
|
|
2867
2976
|
}),
|
|
2868
2977
|
artifact_and_log_retention: endpointRouted({
|
|
2869
2978
|
get: "getRetention",
|
|
2870
2979
|
put: "putRetention",
|
|
2871
2980
|
label: "actions.artifact_and_log_retention",
|
|
2872
2981
|
applied: "applied artifact and log retention",
|
|
2873
|
-
describe: "setting the artifact and log retention window"
|
|
2982
|
+
describe: "setting the artifact and log retention window",
|
|
2983
|
+
read: sliceOf("artifact_and_log_retention")
|
|
2874
2984
|
}),
|
|
2875
|
-
cache: {
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2985
|
+
cache: {
|
|
2986
|
+
plan: async (ctx, _section, declared, plan) => {
|
|
2987
|
+
const cache = declared;
|
|
2988
|
+
for (const [key, wiring] of Object.entries(CACHE_ENDPOINT_BY_KEY)) {
|
|
2989
|
+
if (!(key in cache)) continue;
|
|
2990
|
+
const live = await ctx.read[wiring.get].call();
|
|
2991
|
+
const body = { [key]: cache[key] };
|
|
2992
|
+
const drift = subsetDiff(body, live, "actions.cache");
|
|
2993
|
+
if (hasDrift(drift)) plan.ops.push({
|
|
2994
|
+
role: wiring.put,
|
|
2995
|
+
payload: plainData(body),
|
|
2996
|
+
describe: `setting the cache ${wiring.label} limit`,
|
|
2997
|
+
drift,
|
|
2998
|
+
change: `applied cache ${wiring.label} limit`
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
},
|
|
3002
|
+
snapshot: async (ctx, _section, _base, notes) => {
|
|
3003
|
+
const limits = {};
|
|
3004
|
+
for (const [key, wiring] of Object.entries(CACHE_ENDPOINT_BY_KEY)) {
|
|
3005
|
+
const read = await readOrNote(notes, `actions.cache.${key}`, () => ctx.read[wiring.get].call());
|
|
3006
|
+
if (!("denied" in read)) Object.assign(limits, read.value);
|
|
3007
|
+
}
|
|
3008
|
+
return Object.keys(limits).length === 0 ? void 0 : sliceOf("cache")(limits);
|
|
3009
|
+
}
|
|
3010
|
+
},
|
|
3011
|
+
oidc_customization_sub: {
|
|
3012
|
+
plan: async (ctx, section, declared, plan) => {
|
|
3013
|
+
const live = parseLive(section, ENDPOINTS$17.getOidcSub, LiveOidcSub, await ctx.read.getOidcSub.call());
|
|
3014
|
+
const { include_claim_keys, ...comparable } = declared;
|
|
3015
|
+
const drift = subsetDiff(comparable, live, "actions.oidc_customization_sub");
|
|
3016
|
+
if (declared.use_default === false && include_claim_keys !== void 0) {
|
|
3017
|
+
const liveKeys = live.include_claim_keys ?? [];
|
|
3018
|
+
if (!sameClaimKeyOrder(include_claim_keys, liveKeys)) drift.push(`actions.oidc_customization_sub.include_claim_keys: declared ${JSON.stringify(include_claim_keys)} != live ${JSON.stringify(liveKeys)} (claim-key order defines the subject format, so order counts); apply will set the declared value`);
|
|
3019
|
+
}
|
|
2882
3020
|
if (hasDrift(drift)) plan.ops.push({
|
|
2883
|
-
role:
|
|
2884
|
-
payload: plainData(
|
|
2885
|
-
describe:
|
|
3021
|
+
role: "putOidcSub",
|
|
3022
|
+
payload: plainData(declared),
|
|
3023
|
+
describe: "customizing the OIDC subject claim",
|
|
2886
3024
|
drift,
|
|
2887
|
-
change:
|
|
3025
|
+
change: "applied the OIDC subject claim template"
|
|
2888
3026
|
});
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
const live = parseLive(section, ENDPOINTS$17.getOidcSub, LiveOidcSub, await ctx.read.getOidcSub.call());
|
|
2893
|
-
const { include_claim_keys, ...comparable } = declared;
|
|
2894
|
-
const drift = subsetDiff(comparable, live, "actions.oidc_customization_sub");
|
|
2895
|
-
if (declared.use_default === false && include_claim_keys !== void 0) {
|
|
2896
|
-
const liveKeys = live.include_claim_keys ?? [];
|
|
2897
|
-
if (!sameClaimKeyOrder(include_claim_keys, liveKeys)) drift.push(`actions.oidc_customization_sub.include_claim_keys: declared ${JSON.stringify(include_claim_keys)} != live ${JSON.stringify(liveKeys)} (claim-key order defines the subject format, so order counts); apply will set the declared value`);
|
|
2898
|
-
}
|
|
2899
|
-
if (hasDrift(drift)) plan.ops.push({
|
|
2900
|
-
role: "putOidcSub",
|
|
2901
|
-
payload: plainData(declared),
|
|
2902
|
-
describe: "customizing the OIDC subject claim",
|
|
2903
|
-
drift,
|
|
2904
|
-
change: "applied the OIDC subject claim template"
|
|
2905
|
-
});
|
|
2906
|
-
} },
|
|
3027
|
+
},
|
|
3028
|
+
snapshot: async (ctx, section) => sliceOf("oidc_customization_sub")(parseLive(section, ENDPOINTS$17.getOidcSub, LiveOidcSub, await ctx.read.getOidcSub.call()))
|
|
3029
|
+
},
|
|
2907
3030
|
fork_pr_contributor_approval: endpointRouted({
|
|
2908
3031
|
get: "getForkPrApproval",
|
|
2909
3032
|
put: "putForkPrApproval",
|
|
2910
3033
|
label: "actions.fork_pr_contributor_approval",
|
|
2911
3034
|
applied: "applied the fork PR contributor approval policy",
|
|
2912
|
-
describe: "setting the fork PR contributor approval policy"
|
|
3035
|
+
describe: "setting the fork PR contributor approval policy",
|
|
3036
|
+
read: sliceOf("fork_pr_contributor_approval")
|
|
2913
3037
|
}),
|
|
2914
3038
|
fork_pr_workflows_private_repos: endpointRouted({
|
|
2915
3039
|
get: "getForkPrPrivate",
|
|
2916
3040
|
put: "putForkPrPrivate",
|
|
2917
3041
|
label: "actions.fork_pr_workflows_private_repos",
|
|
2918
3042
|
applied: "applied the private-repo fork PR workflow settings",
|
|
2919
|
-
describe: "setting the private-repo fork PR workflow settings"
|
|
3043
|
+
describe: "setting the private-repo fork PR workflow settings",
|
|
3044
|
+
read: sliceOf("fork_pr_workflows_private_repos")
|
|
2920
3045
|
})
|
|
2921
3046
|
};
|
|
2922
3047
|
const ROUTED_DESTINATIONS = KEY_DESTINATION;
|
|
@@ -2927,6 +3052,11 @@ async function planRouted(key, ctx, section, desired, plan) {
|
|
|
2927
3052
|
if (declared === void 0) return;
|
|
2928
3053
|
await ROUTED_DESTINATIONS[key].plan(ctx, section, declared, plan);
|
|
2929
3054
|
}
|
|
3055
|
+
/** Read one routed key back; generic so the handler and the value stay correlated to one key. */
|
|
3056
|
+
async function snapshotRouted(key, ctx, section, base, notes) {
|
|
3057
|
+
const read = await readOrNote(notes, `actions.${key}`, () => ROUTED_DESTINATIONS[key].snapshot(ctx, section, base, notes));
|
|
3058
|
+
return "denied" in read ? void 0 : read.value;
|
|
3059
|
+
}
|
|
2930
3060
|
function keysTo(destination) {
|
|
2931
3061
|
return new Set(Object.entries(KEY_DESTINATION).filter(([, dest]) => dest === destination).map(([key]) => key));
|
|
2932
3062
|
}
|
|
@@ -2979,6 +3109,21 @@ const actionsSection = {
|
|
|
2979
3109
|
}
|
|
2980
3110
|
for (const key of ROUTED_KEYS) await planRouted(key, ctx, this, desired, plan);
|
|
2981
3111
|
return plan;
|
|
3112
|
+
},
|
|
3113
|
+
async snapshot(ctx) {
|
|
3114
|
+
const notes = [];
|
|
3115
|
+
const base = projectOntoSchema(ActionsConfig, await ctx.read.getPermissions.call());
|
|
3116
|
+
const value = { ...base };
|
|
3117
|
+
const workflow = await readOrNote(notes, `actions.${[...WORKFLOW_KEYS].join("/")}`, () => ctx.read.getWorkflow.call());
|
|
3118
|
+
if (!("denied" in workflow)) Object.assign(value, projectOntoSchema(ActionsConfig, workflow.value));
|
|
3119
|
+
for (const key of ROUTED_KEYS) {
|
|
3120
|
+
const read = await snapshotRouted(key, ctx, this, base, notes);
|
|
3121
|
+
if (read !== void 0) value[key] = read;
|
|
3122
|
+
}
|
|
3123
|
+
return {
|
|
3124
|
+
value,
|
|
3125
|
+
notes
|
|
3126
|
+
};
|
|
2982
3127
|
}
|
|
2983
3128
|
};
|
|
2984
3129
|
//#endregion
|
|
@@ -3289,101 +3434,6 @@ async function planSecrets(section, scope, opts) {
|
|
|
3289
3434
|
return plan;
|
|
3290
3435
|
}
|
|
3291
3436
|
//#endregion
|
|
3292
|
-
//#region src/sections/shared/snapshot-helpers.ts
|
|
3293
|
-
function defOf(schema) {
|
|
3294
|
-
return schema._zod.def;
|
|
3295
|
-
}
|
|
3296
|
-
/** The schema types the projection treats as leaves: the live value passes through verbatim. */
|
|
3297
|
-
const LEAF_TYPES = /* @__PURE__ */ new Set([
|
|
3298
|
-
"string",
|
|
3299
|
-
"number",
|
|
3300
|
-
"int",
|
|
3301
|
-
"boolean",
|
|
3302
|
-
"enum",
|
|
3303
|
-
"literal",
|
|
3304
|
-
"unknown",
|
|
3305
|
-
"null"
|
|
3306
|
-
]);
|
|
3307
|
-
function isPlainObject(value) {
|
|
3308
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3309
|
-
}
|
|
3310
|
-
/**
|
|
3311
|
-
* A live value projected onto a schema slice, so server-assigned fields fall away without a hand
|
|
3312
|
-
* list per section. A nested `null` the slice cannot hold is GitHub's "no value" (a not-configured
|
|
3313
|
-
* setup's runner_type) and its key is omitted; a value the slice rejects at the root stays, so the
|
|
3314
|
-
* engine's validation names a body outside the shape instead of the section vanishing. A
|
|
3315
|
-
* passthrough slice (a catchall other than never) keeps every live key by design, so a section on
|
|
3316
|
-
* one names the keys it reads back itself. The casts are the boundary the engine validates behind.
|
|
3317
|
-
*/
|
|
3318
|
-
function projectOntoSchema(schema, live) {
|
|
3319
|
-
if (live === null && !schema.safeParse(null).success) return live;
|
|
3320
|
-
return project(schema, live);
|
|
3321
|
-
}
|
|
3322
|
-
function project(schema, live) {
|
|
3323
|
-
if (live === void 0) return;
|
|
3324
|
-
if (live === null) return schema.safeParse(null).success ? null : void 0;
|
|
3325
|
-
const def = defOf(schema);
|
|
3326
|
-
switch (def.type) {
|
|
3327
|
-
case "optional":
|
|
3328
|
-
case "nullable": return project(def.innerType, live);
|
|
3329
|
-
case "object": {
|
|
3330
|
-
if (!isPlainObject(live)) return live;
|
|
3331
|
-
const shape = def.shape ?? {};
|
|
3332
|
-
const out = {};
|
|
3333
|
-
for (const [key, child] of Object.entries(shape)) {
|
|
3334
|
-
const projected = project(child, live[key]);
|
|
3335
|
-
if (projected !== void 0) out[key] = projected;
|
|
3336
|
-
}
|
|
3337
|
-
if (def.catchall !== void 0 && defOf(def.catchall).type !== "never") {
|
|
3338
|
-
for (const [key, value] of Object.entries(live)) if (!(key in shape) && value !== void 0) out[key] = value;
|
|
3339
|
-
}
|
|
3340
|
-
return out;
|
|
3341
|
-
}
|
|
3342
|
-
case "array": return Array.isArray(live) ? live.map((item) => project(def.element, item)) : live;
|
|
3343
|
-
case "record": return isPlainObject(live) ? Object.fromEntries(Object.entries(live).map(([key, value]) => [key, project(def.valueType, value)])) : live;
|
|
3344
|
-
case "union": {
|
|
3345
|
-
const option = (def.options ?? []).find((candidate) => candidate.safeParse(live).success);
|
|
3346
|
-
return option === void 0 ? live : project(option, live);
|
|
3347
|
-
}
|
|
3348
|
-
default:
|
|
3349
|
-
if (!LEAF_TYPES.has(def.type)) throw new Error(`projectOntoSchema(): unhandled schema type "${def.type}" - teach the projection its walk before authoring it in a section slice`);
|
|
3350
|
-
return live;
|
|
3351
|
-
}
|
|
3352
|
-
}
|
|
3353
|
-
/**
|
|
3354
|
-
* Refuse a live list holding two resources under one identity (GitHub allows repeated deploy-key
|
|
3355
|
-
* titles and hook urls): the planner manages one resource per identity and would refuse the
|
|
3356
|
-
* snapshot's own file, so the snapshot fails here, naming the pairs, instead of emitting it.
|
|
3357
|
-
*/
|
|
3358
|
-
function rejectLiveDuplicates(section, noun, items, keyOf, describe) {
|
|
3359
|
-
const collisions = collidingPairs(items, keyOf, describe);
|
|
3360
|
-
if (collisions.length > 0) throw new Error(`${section.key}: GitHub holds ${noun}s that resolve to one identity: ${collisions.join("; ")}. This section manages one ${noun} per identity, so the snapshot cannot declare them; delete all but one of each on GitHub, then snapshot again`);
|
|
3361
|
-
}
|
|
3362
|
-
/**
|
|
3363
|
-
* One read of a snapshot that a denial may take out without failing the section: a
|
|
3364
|
-
* PermissionDenied becomes a note naming the key left out and the grant advice, anything else
|
|
3365
|
-
* propagates. For a section whose keys sit behind different grants (repository, actions).
|
|
3366
|
-
* @public
|
|
3367
|
-
*/
|
|
3368
|
-
async function readOrNote(notes, label, read) {
|
|
3369
|
-
try {
|
|
3370
|
-
return { value: await read() };
|
|
3371
|
-
} catch (error) {
|
|
3372
|
-
if (error instanceof PermissionDenied) {
|
|
3373
|
-
notes.push(`${label}: left out of the snapshot - ${error.detail}`);
|
|
3374
|
-
return { denied: true };
|
|
3375
|
-
}
|
|
3376
|
-
throw error;
|
|
3377
|
-
}
|
|
3378
|
-
}
|
|
3379
|
-
/** A knobbed section's snapshot value: its entries under the section's own default policy, spelled out. */
|
|
3380
|
-
function knobbedSnapshot(section, entries) {
|
|
3381
|
-
return {
|
|
3382
|
-
_undeclared: defaultUndeclaredPolicy(section),
|
|
3383
|
-
entries
|
|
3384
|
-
};
|
|
3385
|
-
}
|
|
3386
|
-
//#endregion
|
|
3387
3437
|
//#region src/sections/shared/repo-secrets.ts
|
|
3388
3438
|
/**
|
|
3389
3439
|
* The factory derives the routes from THIS map, so a key paired with another family's segment (which the
|
|
@@ -7290,6 +7340,66 @@ const ENDPOINTS$5 = {
|
|
|
7290
7340
|
}
|
|
7291
7341
|
};
|
|
7292
7342
|
const LiveRepository = z.looseObject({ topics: z.array(z.string()).nullish() });
|
|
7343
|
+
/**
|
|
7344
|
+
* The GET also reports what nobody can PATCH (ids, urls, counts, has_downloads) and the passthrough
|
|
7345
|
+
* slice cannot tell those apart, so the PATCH fields are spelled out and pinned below.
|
|
7346
|
+
* `name` stays out: a settings file reused on another repository would rename it.
|
|
7347
|
+
*/
|
|
7348
|
+
const SNAPSHOT_PATCH_FIELDS = [
|
|
7349
|
+
"description",
|
|
7350
|
+
"homepage",
|
|
7351
|
+
"private",
|
|
7352
|
+
"visibility",
|
|
7353
|
+
"security_and_analysis",
|
|
7354
|
+
"has_issues",
|
|
7355
|
+
"has_projects",
|
|
7356
|
+
"has_wiki",
|
|
7357
|
+
"has_discussions",
|
|
7358
|
+
"has_pull_requests",
|
|
7359
|
+
"pull_request_creation_policy",
|
|
7360
|
+
"is_template",
|
|
7361
|
+
"default_branch",
|
|
7362
|
+
"allow_squash_merge",
|
|
7363
|
+
"allow_merge_commit",
|
|
7364
|
+
"allow_rebase_merge",
|
|
7365
|
+
"allow_auto_merge",
|
|
7366
|
+
"delete_branch_on_merge",
|
|
7367
|
+
"allow_update_branch",
|
|
7368
|
+
"use_squash_pr_title_as_default",
|
|
7369
|
+
"squash_merge_commit_title",
|
|
7370
|
+
"squash_merge_commit_message",
|
|
7371
|
+
"merge_commit_title",
|
|
7372
|
+
"merge_commit_message",
|
|
7373
|
+
"archived",
|
|
7374
|
+
"allow_forking",
|
|
7375
|
+
"web_commit_signoff_required"
|
|
7376
|
+
];
|
|
7377
|
+
/**
|
|
7378
|
+
* The GET's dependabot_security_updates is the automated-security-fixes toggle, already emitted from
|
|
7379
|
+
* its own endpoint, and the PATCH rejects an unknown sub-key, so the nested object is pinned too.
|
|
7380
|
+
*/
|
|
7381
|
+
const SECURITY_AND_ANALYSIS_PATCH_FIELDS = [
|
|
7382
|
+
"advanced_security",
|
|
7383
|
+
"code_security",
|
|
7384
|
+
"secret_scanning",
|
|
7385
|
+
"secret_scanning_push_protection",
|
|
7386
|
+
"secret_scanning_ai_detection",
|
|
7387
|
+
"secret_scanning_non_provider_patterns",
|
|
7388
|
+
"secret_scanning_delegated_alert_dismissal",
|
|
7389
|
+
"secret_scanning_delegated_bypass",
|
|
7390
|
+
"secret_scanning_delegated_bypass_options",
|
|
7391
|
+
"secret_scanning_validity_checks"
|
|
7392
|
+
];
|
|
7393
|
+
/** The live security_and_analysis object narrowed to its PATCHable sub-keys; undefined when none. */
|
|
7394
|
+
function snapshotSecurityAndAnalysis(live) {
|
|
7395
|
+
if (typeof live !== "object" || live === null || Array.isArray(live)) return;
|
|
7396
|
+
const out = {};
|
|
7397
|
+
for (const field of SECURITY_AND_ANALYSIS_PATCH_FIELDS) {
|
|
7398
|
+
const value = live[field];
|
|
7399
|
+
if (value !== void 0 && value !== null) out[field] = value;
|
|
7400
|
+
}
|
|
7401
|
+
return Object.keys(out).length === 0 ? void 0 : out;
|
|
7402
|
+
}
|
|
7293
7403
|
const FEATURES_QUERY = graphqlOp()({
|
|
7294
7404
|
name: "RepositoryFeatures",
|
|
7295
7405
|
kind: "read",
|
|
@@ -7392,6 +7502,11 @@ const GRAPHQL_ROUTED_KEYS = [{
|
|
|
7392
7502
|
changeText: (value) => String(value),
|
|
7393
7503
|
unreadableHint: "a null policy means GitHub reported no issue creation policy for this repository; otherwise the field vocabulary may have changed"
|
|
7394
7504
|
}];
|
|
7505
|
+
/** Why a routed field's live value cannot be read into the settings vocabulary. */
|
|
7506
|
+
function unreadableRoutedValue(entry, raw, opName) {
|
|
7507
|
+
const hint = entry.unreadableHint ? `; ${entry.unreadableHint}` : "";
|
|
7508
|
+
return `GRAPHQL ${opName} returned ${entry.field} ${JSON.stringify(raw)}, which this section cannot read as a repository.${entry.key} value${hint}`;
|
|
7509
|
+
}
|
|
7395
7510
|
/**
|
|
7396
7511
|
* Strictness is scoped to the DECLARED keys: an unreadable value (the SDL-nullable policy, a future
|
|
7397
7512
|
* enum member) must fail loudly for a key the file declares and must not fail a run that never declared it.
|
|
@@ -7400,17 +7515,19 @@ function decodeRoutedFields(fields, routed, opName) {
|
|
|
7400
7515
|
const values = {};
|
|
7401
7516
|
for (const entry of routed) {
|
|
7402
7517
|
const decoded = entry.decode(fields[entry.field]);
|
|
7403
|
-
if (decoded === void 0) {
|
|
7404
|
-
const hint = entry.unreadableHint ? `; ${entry.unreadableHint}` : "";
|
|
7405
|
-
throw new Error(`repository: GRAPHQL ${opName} returned ${entry.field} ${JSON.stringify(fields[entry.field])}, which this section cannot read as a repository.${entry.key} value${hint}. Drop the key, or update the action if GitHub's vocabulary moved`);
|
|
7406
|
-
}
|
|
7518
|
+
if (decoded === void 0) throw new Error(`repository: ${unreadableRoutedValue(entry, fields[entry.field], opName)}. Drop the key, or update the action if GitHub's vocabulary moved`);
|
|
7407
7519
|
values[entry.key] = decoded;
|
|
7408
7520
|
}
|
|
7409
7521
|
return values;
|
|
7410
7522
|
}
|
|
7411
|
-
|
|
7412
|
-
|
|
7523
|
+
/** The Repository object of a features read, with the node id the mutation addresses. */
|
|
7524
|
+
function repositoryNode(data) {
|
|
7525
|
+
const repository = data.repository;
|
|
7413
7526
|
if (!repository || typeof repository.id !== "string") throw new Error(`repository: GRAPHQL ${FEATURES_QUERY.name} returned no repository object with an id, so the ${GRAPHQL_ROUTED_KEYS.map((entry) => entry.key).join("/")} state cannot be read. Check the token's repository access`);
|
|
7527
|
+
return repository;
|
|
7528
|
+
}
|
|
7529
|
+
async function fetchRoutedState(ctx, routed) {
|
|
7530
|
+
const repository = repositoryNode(await ctx.read.featuresQuery.call(repoVariables(ctx)));
|
|
7414
7531
|
return {
|
|
7415
7532
|
id: repository.id,
|
|
7416
7533
|
values: decodeRoutedFields(repository, routed, FEATURES_QUERY.name)
|
|
@@ -7518,6 +7635,57 @@ const repositorySection = {
|
|
|
7518
7635
|
}
|
|
7519
7636
|
}
|
|
7520
7637
|
return plan;
|
|
7638
|
+
},
|
|
7639
|
+
async snapshot(ctx) {
|
|
7640
|
+
const notes = [];
|
|
7641
|
+
const live = parseLive(this, ENDPOINTS$5.get, LiveRepository, await ctx.read.get.call());
|
|
7642
|
+
const value = {};
|
|
7643
|
+
for (const field of SNAPSHOT_PATCH_FIELDS) {
|
|
7644
|
+
const read = field === "security_and_analysis" ? snapshotSecurityAndAnalysis(live[field]) : live[field];
|
|
7645
|
+
if (read !== void 0 && read !== null) value[field] = read;
|
|
7646
|
+
}
|
|
7647
|
+
if (live.topics !== void 0 && live.topics !== null && live.topics.length > 0) value.topics = [...live.topics];
|
|
7648
|
+
const probes = [];
|
|
7649
|
+
for (const toggle of READABLE_TOGGLES) {
|
|
7650
|
+
const read = await readOrNote(notes, `repository.${toggle.key}`, async () => {
|
|
7651
|
+
const answer = await ctx.read[toggle.get].tryCall();
|
|
7652
|
+
if ("error" in answer) return {
|
|
7653
|
+
live: void 0,
|
|
7654
|
+
concealable: answer.error.status === 404
|
|
7655
|
+
};
|
|
7656
|
+
return {
|
|
7657
|
+
live: parseLive(this, ENDPOINTS$5[toggle.get], toggle.live, answer.data),
|
|
7658
|
+
concealable: false
|
|
7659
|
+
};
|
|
7660
|
+
});
|
|
7661
|
+
if (!("denied" in read)) probes.push({
|
|
7662
|
+
toggle,
|
|
7663
|
+
...read.value
|
|
7664
|
+
});
|
|
7665
|
+
}
|
|
7666
|
+
if (probes.length > 0 && probes.every((probe) => probe.concealable)) notes.push(`repository.${probes.map((probe) => probe.toggle.key).join("/")}: every toggle GET answered 404, which reads as off but is also how a fine-grained token missing the grant is answered, so they are left out; if the token does ${sectionGrant(this)}, they are all off and can be declared false`);
|
|
7667
|
+
else for (const { toggle, live } of probes) {
|
|
7668
|
+
const enabled = live === void 0 ? false : toggle.isEnabled(live);
|
|
7669
|
+
value[toggle.key] = enabled;
|
|
7670
|
+
if (live !== void 0 && toggle.isEnforced?.(live) === true) notes.push(`repository.${toggle.key}: ${OWNER_ENFORCED}, so it reads back as ${enabled} but cannot be changed from the repository`);
|
|
7671
|
+
}
|
|
7672
|
+
for (const toggle of WRITE_ONLY_TOGGLES) notes.push(`repository.${toggle.key}: GitHub exposes no endpoint to read ${toggle.label} back, so the snapshot leaves it out; declare it yourself to manage it`);
|
|
7673
|
+
const routed = await readOrNote(notes, GRAPHQL_ROUTED_KEYS.map((entry) => `repository.${entry.key}`).join(" and "), () => ctx.read.featuresQuery.call(repoVariables(ctx)));
|
|
7674
|
+
if (!("denied" in routed)) {
|
|
7675
|
+
const repository = repositoryNode(routed.value);
|
|
7676
|
+
for (const entry of GRAPHQL_ROUTED_KEYS) {
|
|
7677
|
+
const decoded = entry.decode(repository[entry.field]);
|
|
7678
|
+
if (decoded === void 0) {
|
|
7679
|
+
notes.push(`repository.${entry.key}: ${unreadableRoutedValue(entry, repository[entry.field], FEATURES_QUERY.name)}, so the snapshot leaves it out`);
|
|
7680
|
+
continue;
|
|
7681
|
+
}
|
|
7682
|
+
value[entry.key] = decoded;
|
|
7683
|
+
}
|
|
7684
|
+
}
|
|
7685
|
+
return {
|
|
7686
|
+
value,
|
|
7687
|
+
notes
|
|
7688
|
+
};
|
|
7521
7689
|
}
|
|
7522
7690
|
};
|
|
7523
7691
|
//#endregion
|
|
@@ -7552,12 +7720,24 @@ const LiveRulesetSummary = z.looseObject({
|
|
|
7552
7720
|
});
|
|
7553
7721
|
const permission$4 = { repo: ["administration"] };
|
|
7554
7722
|
/**
|
|
7555
|
-
*
|
|
7556
|
-
*
|
|
7557
|
-
*
|
|
7723
|
+
* A summary this section may manage: one no other source explicitly owns (source_type is optional in
|
|
7724
|
+
* the API type). Deletion is gated harder in plan(): only an explicit "Repository" is ever deleted,
|
|
7725
|
+
* since a missing field is not proof.
|
|
7726
|
+
*/
|
|
7727
|
+
function notInherited(summary) {
|
|
7728
|
+
return (summary.source_type ?? "Repository") === "Repository";
|
|
7729
|
+
}
|
|
7730
|
+
const BYPASS_HIDDEN = "bypass_actors is not visible to this token (GitHub returns it only to a token with write access to the ruleset)";
|
|
7731
|
+
/** GitHub omits the bypass_actors KEY (never `[]`) from a GET the token lacks write access for. */
|
|
7732
|
+
function bypassActorsVisible(live) {
|
|
7733
|
+
return typeof live === "object" && live !== null && Object.hasOwn(live, "bypass_actors");
|
|
7734
|
+
}
|
|
7735
|
+
/**
|
|
7736
|
+
* The declared ruleset minus a bypass_actors list the token cannot observe, so it reads as neither
|
|
7737
|
+
* drift nor a phantom key. Returns the input itself when nothing is hidden, so the caller can tell.
|
|
7558
7738
|
*/
|
|
7559
7739
|
function observableRuleset(ruleset, live) {
|
|
7560
|
-
if (!
|
|
7740
|
+
if (!Object.hasOwn(ruleset, "bypass_actors") || bypassActorsVisible(live)) return ruleset;
|
|
7561
7741
|
const { bypass_actors: _hidden, ...visible } = ruleset;
|
|
7562
7742
|
return visible;
|
|
7563
7743
|
}
|
|
@@ -7607,7 +7787,7 @@ const rulesetsSection = {
|
|
|
7607
7787
|
const { policy, entries } = undeclaredPolicy(declared, defaultUndeclaredPolicy(this));
|
|
7608
7788
|
const desired = entries.map(normalizeRuleset);
|
|
7609
7789
|
rejectDuplicates(this, desired, (r) => r.name, (r) => r.name);
|
|
7610
|
-
const repoRulesets = parseLive(this, ENDPOINTS$4.list, z.array(LiveRulesetSummary), await ctx.read.list.listAll()).filter(
|
|
7790
|
+
const repoRulesets = parseLive(this, ENDPOINTS$4.list, z.array(LiveRulesetSummary), await ctx.read.list.listAll()).filter(notInherited);
|
|
7611
7791
|
const idByName = new Map(repoRulesets.map((r) => [r.name, r.id]));
|
|
7612
7792
|
const plan = {
|
|
7613
7793
|
ops: [],
|
|
@@ -7629,7 +7809,7 @@ const rulesetsSection = {
|
|
|
7629
7809
|
}
|
|
7630
7810
|
const live = await ctx.read.get.call({ params: { ruleset_id: String(id) } });
|
|
7631
7811
|
const compared = observableRuleset(ruleset, live);
|
|
7632
|
-
if (compared !== ruleset) plan.notes.push(`rulesets[${ruleset.name}]:
|
|
7812
|
+
if (compared !== ruleset) plan.notes.push(`rulesets[${ruleset.name}]: ${BYPASS_HIDDEN}, so drift on it cannot be judged here; grant Administration write to check it`);
|
|
7633
7813
|
const drift = subsetDiff(compared, live, `rulesets[${ruleset.name}]`);
|
|
7634
7814
|
if (!hasDrift(drift)) continue;
|
|
7635
7815
|
const phantom = phantomKeys(compared, live);
|
|
@@ -7669,6 +7849,29 @@ const rulesetsSection = {
|
|
|
7669
7849
|
}));
|
|
7670
7850
|
}
|
|
7671
7851
|
return plan;
|
|
7852
|
+
},
|
|
7853
|
+
async snapshot(ctx) {
|
|
7854
|
+
const summaries = parseLive(this, ENDPOINTS$4.list, z.array(LiveRulesetSummary), await ctx.read.list.listAll());
|
|
7855
|
+
const notes = summaries.filter((summary) => !notInherited(summary)).map((summary) => `rulesets[${summary.name}]: inherited from the ${String(summary.source_type).toLowerCase()} (source_type "${summary.source_type}"), so it is not part of the repository's snapshot; manage it where it is defined`);
|
|
7856
|
+
const manageable = summaries.filter(notInherited);
|
|
7857
|
+
rejectLiveDuplicates(this, "ruleset", manageable, (summary) => summary.name, (summary) => `${summary.name} (id ${summary.id})`);
|
|
7858
|
+
const entries = [];
|
|
7859
|
+
for (const summary of manageable) {
|
|
7860
|
+
const live = await ctx.read.get.call({ params: { ruleset_id: String(summary.id) } });
|
|
7861
|
+
if (!bypassActorsVisible(live)) {
|
|
7862
|
+
notes.push(`rulesets[${summary.name}]: ${BYPASS_HIDDEN}, and an entry without it would clear the bypass list on the next update, so the ruleset is left out of the snapshot (kept undeclared); grant Administration write to read it back`);
|
|
7863
|
+
continue;
|
|
7864
|
+
}
|
|
7865
|
+
entries.push(projectOntoSchema(RulesetConfig, live));
|
|
7866
|
+
}
|
|
7867
|
+
if (manageable.length === 0) return {
|
|
7868
|
+
value: void 0,
|
|
7869
|
+
notes
|
|
7870
|
+
};
|
|
7871
|
+
return {
|
|
7872
|
+
value: knobbedSnapshot(this, entries),
|
|
7873
|
+
notes
|
|
7874
|
+
};
|
|
7672
7875
|
}
|
|
7673
7876
|
};
|
|
7674
7877
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vivswan/github-settings-as-code",
|
|
3
|
-
"version": "2.0.1-main.
|
|
3
|
+
"version": "2.0.1-main.1072.gfded5f5",
|
|
4
4
|
"description": "GitHub Action applying declarative repository settings: rulesets, labels, branch protection, and more.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"type": "module",
|