diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index b4d2142..2478c4c 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -5,14 +5,14 @@ bundle: version: "0.4.0-dev" release_sequence: 0 channel: "development" - source_tree_digest: "sha256:a236bcc5c7527d3a0a3c6230e20ebe9c48796c0a5d41392b47a22da934ff49fa" - digest: "sha256:977eaefa91e2e04f211495101ffde77fcb56aaff13972acf63d9a276d25fd97d" + source_tree_digest: "sha256:19faa612aeb7afbcd594c3b3e8ef1bec59f5639e73f1ecb8e63c7756d126144c" + digest: "sha256:6e9dccd59cc1fd5e89d0c7dfab833780599b6c66adf8baf112f3ceb5c3178258" projection: - input_digest: "sha256:7b88ac05c2c898a1ac7e96d88846fe99b719ba2312751a661b1b02eaf0eadbbd" - output_digest: "sha256:498a0c7407167e63ce51f8e853b1f91b66e4ac9acba5484892c44efb2aab61fb" + input_digest: "sha256:6c591644a111349d49e3b5d25e1418a14a74b0ca98891c75a8b39b9408f0d441" + output_digest: "sha256:7f3bb302ff5b46d9ddcf68765e3e05e12938ecdafe75a82b37313b75fdb87033" files: - path: ".gds/compiled-policy.json" digest: "sha256:78d09606bb4168d74bce1f50ab62b46a7ded34652c6b23af1badfd26dd060e94" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:8f198d373f786d2d2c88e1910851c1039c6200359ee95cca11af0ab3cb5d326c" + digest: "sha256:165855583077fe66dfa6828017c7292c8dbd3fba173d259dffe1ffd59e6f6417" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index 0c05408..40140e1 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -1,8 +1,8 @@ # GENERATED FILE - DO NOT EDIT DIRECTLY # generator: gds # bundle: 0.4.0-dev -# source-tree-digest: sha256:a236bcc5c7527d3a0a3c6230e20ebe9c48796c0a5d41392b47a22da934ff49fa -# input-digest: sha256:7b88ac05c2c898a1ac7e96d88846fe99b719ba2312751a661b1b02eaf0eadbbd +# source-tree-digest: sha256:19faa612aeb7afbcd594c3b3e8ef1bec59f5639e73f1ecb8e63c7756d126144c +# input-digest: sha256:6c591644a111349d49e3b5d25e1418a14a74b0ca98891c75a8b39b9408f0d441 # output-digest: sha256:8f61a9ffd6949238099d4523a037d57199eb575f187cc8ac52baa4b5b832e79f # edit-source: # - .gds/repository.yaml diff --git a/core/app/github_ruleset_external_test.go b/core/app/github_ruleset_external_test.go index 097f6a4..0e2b9b2 100644 --- a/core/app/github_ruleset_external_test.go +++ b/core/app/github_ruleset_external_test.go @@ -108,6 +108,33 @@ func TestDesiredRulesetIsUnchangedWithoutADeclaration(t *testing.T) { } } +func TestRulesetPlannerDetectsOwnedPullRequestDrift(t *testing.T) { + desired := githubprovider.RepositoryRuleset{Enforcement: "active", Rules: []githubprovider.RulesetRule{ + { + Type: "pull_request", DismissStaleReviewsOnPush: true, + RequiredReviewThreadResolution: true, + RequireExtraApprovalForUnattributedChanges: false, + AllowedMergeMethods: []string{"merge"}, + }, + }} + observed := githubprovider.RepositoryRulesetState{Enforcement: "active", Rules: []githubprovider.RulesetRule{ + { + Type: "pull_request", DismissStaleReviewsOnPush: true, + RequiredReviewThreadResolution: true, + RequireExtraApprovalForUnattributedChanges: true, + AllowedMergeMethods: []string{"merge", "squash", "rebase"}, + }, + }} + if rulesetOwnedStateMatches(observed, desired) { + t.Fatal("planner ignored owned pull-request merge-control drift") + } + observed.Rules[0].RequireExtraApprovalForUnattributedChanges = false + observed.Rules[0].AllowedMergeMethods = []string{"merge"} + if !rulesetOwnedStateMatches(observed, desired) { + t.Fatal("planner rejected matching owned pull-request merge controls") + } +} + // A declaration that names a generated context would pin, as unowned, something // the generator governs -- so the two sources of truth would disagree with no // way to tell which won. diff --git a/core/app/github_ruleset_operations.go b/core/app/github_ruleset_operations.go index c310f65..d35d89c 100644 --- a/core/app/github_ruleset_operations.go +++ b/core/app/github_ruleset_operations.go @@ -405,6 +405,18 @@ func rulesetOwnedStateMatches( if !present { return false } + if wanted.Type == "pull_request" { + if actual.RequiredApprovingReviewCount != wanted.RequiredApprovingReviewCount || + actual.DismissStaleReviewsOnPush != wanted.DismissStaleReviewsOnPush || + actual.RequireCodeOwnerReview != wanted.RequireCodeOwnerReview || + actual.RequiredReviewThreadResolution != wanted.RequiredReviewThreadResolution || + actual.RequireLastPushApproval != wanted.RequireLastPushApproval || + actual.RequireExtraApprovalForUnattributedChanges != wanted.RequireExtraApprovalForUnattributedChanges || + !sameStringSet(actual.AllowedMergeMethods, wanted.AllowedMergeMethods) { + return false + } + continue + } if wanted.Type != "required_status_checks" { continue } @@ -421,6 +433,23 @@ func rulesetOwnedStateMatches( return true } +func sameStringSet(left, right []string) bool { + if len(left) != len(right) { + return false + } + counts := make(map[string]int, len(left)) + for _, value := range left { + counts[value]++ + } + for _, value := range right { + counts[value]-- + if counts[value] < 0 { + return false + } + } + return true +} + func loadTrackedRuleset(localRoot string) (githubprovider.RepositoryRuleset, error) { path := filepath.Join(localRoot, filepath.FromSlash(trackedRulesetRelativePath)) info, err := os.Lstat(path)