feat(merge): require passing checks to merge, add mr:force-merge and ignore_checks, drop failing_checks_block
- commit
- cfc292c45fe829739b29bc34ab81a4feb4f3f8b2
- author
- brice · · 2026-10-21 13:16 UTC
- parent
- d5e0505
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
42 files · +168 -187
M gen/ermite/storage/merge_request.pb.go +4 -13 generated file: collapsed
M go.mod +2 -0
| @@ -172,3 +172,5 @@ require ( | |||
| 172 | 172 | mvdan.cc/xurls/v2 v2.6.0 // indirect | |
| 173 | 173 | pluginrpc.com/pluginrpc v0.5.0 // indirect | |
| 174 | 174 | ) | |
| 175 | + | ||
| 176 | + | replace bnema.dev/ermite-api => ../../../ermite-api/.worktrees/mr-ready-merge-checks | |
| @@ -172,3 +172,5 @@ require ( | |||||
| 172 | mvdan.cc/xurls/v2 v2.6.0 // indirect | 172 | mvdan.cc/xurls/v2 v2.6.0 // indirect | ||
| 173 | pluginrpc.com/pluginrpc v0.5.0 // indirect | 173 | pluginrpc.com/pluginrpc v0.5.0 // indirect | ||
| 174 | ) | 174 | ) | ||
| 175 | + | ||||
| 176 | + | replace bnema.dev/ermite-api => ../../../ermite-api/.worktrees/mr-ready-merge-checks | |||
M go.sum +0 -2
| @@ -1,7 +1,5 @@ | |||
| 1 | 1 | bnema.dev/ermite-actions v0.2.0 h1:238UBhSZcm8RxqqjZvX3RLGKQNyKtxoXKnKIkYt4P/M= | |
| 2 | 2 | bnema.dev/ermite-actions v0.2.0/go.mod h1:qoIB/Iz4xKdLUG4R6wuBDk6cemdugR+UEo5bpSJJYh4= | |
| 3 | - | bnema.dev/ermite-api v0.14.0 h1:tN4uirlBla/jxkrcJsLhJz3zuswY3KABRNGw1KLgG5Y= | |
| 4 | - | bnema.dev/ermite-api v0.14.0/go.mod h1:L5smjt+IAigyAo3Ny2EnFB9IwvIGOPT1GoByZNgbDa8= | |
| 5 | 3 | bnema.dev/ermite-git v0.1.0 h1:8GNhQDj0MxyvrI4kHewZ9k0rT86e9348T3BJ49WHfmI= | |
| 6 | 4 | bnema.dev/ermite-git v0.1.0/go.mod h1:m9d4VYHm28mrY6VxVqWQMRIsadGq80FeLub/nrZX6Cg= | |
| 7 | 5 | bnema.dev/ermite-kv v0.1.0 h1:NbD76jgqMq08AVTe/l0EEqDTHjywaYNoqC5c/WgYRcE= | |
| @@ -1,7 +1,5 @@ | |||||
| 1 | bnema.dev/ermite-actions v0.2.0 h1:238UBhSZcm8RxqqjZvX3RLGKQNyKtxoXKnKIkYt4P/M= | 1 | bnema.dev/ermite-actions v0.2.0 h1:238UBhSZcm8RxqqjZvX3RLGKQNyKtxoXKnKIkYt4P/M= | ||
| 2 | bnema.dev/ermite-actions v0.2.0/go.mod h1:qoIB/Iz4xKdLUG4R6wuBDk6cemdugR+UEo5bpSJJYh4= | 2 | bnema.dev/ermite-actions v0.2.0/go.mod h1:qoIB/Iz4xKdLUG4R6wuBDk6cemdugR+UEo5bpSJJYh4= | ||
| 3 | - | bnema.dev/ermite-api v0.14.0 h1:tN4uirlBla/jxkrcJsLhJz3zuswY3KABRNGw1KLgG5Y= | |||
| 4 | - | bnema.dev/ermite-api v0.14.0/go.mod h1:L5smjt+IAigyAo3Ny2EnFB9IwvIGOPT1GoByZNgbDa8= | |||
| 5 | bnema.dev/ermite-git v0.1.0 h1:8GNhQDj0MxyvrI4kHewZ9k0rT86e9348T3BJ49WHfmI= | 3 | bnema.dev/ermite-git v0.1.0 h1:8GNhQDj0MxyvrI4kHewZ9k0rT86e9348T3BJ49WHfmI= | ||
| 6 | bnema.dev/ermite-git v0.1.0/go.mod h1:m9d4VYHm28mrY6VxVqWQMRIsadGq80FeLub/nrZX6Cg= | 4 | bnema.dev/ermite-git v0.1.0/go.mod h1:m9d4VYHm28mrY6VxVqWQMRIsadGq80FeLub/nrZX6Cg= | ||
| 7 | bnema.dev/ermite-kv v0.1.0 h1:NbD76jgqMq08AVTe/l0EEqDTHjywaYNoqC5c/WgYRcE= | 5 | bnema.dev/ermite-kv v0.1.0 h1:NbD76jgqMq08AVTe/l0EEqDTHjywaYNoqC5c/WgYRcE= | ||
M internal/adapters/client/features.go +2 -0
| @@ -8,6 +8,8 @@ import "slices" | |||
| 8 | 8 | // ServerService.GetVersion. A server without one still works for everything | |
| 9 | 9 | // else: the CLI warns instead of failing. | |
| 10 | 10 | const ( | |
| 11 | + | // FeatureIgnoreChecks permits merging with checks bypassed. | |
| 12 | + | FeatureIgnoreChecks = "merge_requests.ignore_checks" | |
| 11 | 13 | // FeatureRepoUpdate is RepoService.UpdateRepo (`ermite repo edit`). | |
| 12 | 14 | FeatureRepoUpdate = "repo.update" | |
| 13 | 15 | // FeatureEventsResume is EventService.Watch with resume_after, which | |
| @@ -8,6 +8,8 @@ import "slices" | |||||
| 8 | // ServerService.GetVersion. A server without one still works for everything | 8 | // ServerService.GetVersion. A server without one still works for everything | ||
| 9 | // else: the CLI warns instead of failing. | 9 | // else: the CLI warns instead of failing. | ||
| 10 | const ( | 10 | const ( | ||
| 11 | + | // FeatureIgnoreChecks permits merging with checks bypassed. | |||
| 12 | + | FeatureIgnoreChecks = "merge_requests.ignore_checks" | |||
| 11 | // FeatureRepoUpdate is RepoService.UpdateRepo (`ermite repo edit`). | 13 | // FeatureRepoUpdate is RepoService.UpdateRepo (`ermite repo edit`). | ||
| 12 | FeatureRepoUpdate = "repo.update" | 14 | FeatureRepoUpdate = "repo.update" | ||
| 13 | // FeatureEventsResume is EventService.Watch with resume_after, which | 15 | // FeatureEventsResume is EventService.Watch with resume_after, which | ||
M internal/adapters/client/scopes.go +3 -0
| @@ -28,6 +28,9 @@ var PresetNames = []string{presetCLI, presetAgent, presetCI, presetRunner} | |||
| 28 | 28 | // ScopeName is the name of a scope as users type it and as the docs write | |
| 29 | 29 | // it: SCOPE_REPO_READ is "repo:read". | |
| 30 | 30 | func ScopeName(s ermite.Scope) string { | |
| 31 | + | if s == ermite.Scope_SCOPE_MR_FORCE_MERGE { | |
| 32 | + | return "mr:force-merge" | |
| 33 | + | } | |
| 31 | 34 | name, ok := ermite.Scope_name[int32(s)] | |
| 32 | 35 | if !ok { | |
| 33 | 36 | return strconv.Itoa(int(s)) | |
| @@ -28,6 +28,9 @@ var PresetNames = []string{presetCLI, presetAgent, presetCI, presetRunner} | |||||
| 28 | // ScopeName is the name of a scope as users type it and as the docs write | 28 | // ScopeName is the name of a scope as users type it and as the docs write | ||
| 29 | // it: SCOPE_REPO_READ is "repo:read". | 29 | // it: SCOPE_REPO_READ is "repo:read". | ||
| 30 | func ScopeName(s ermite.Scope) string { | 30 | func ScopeName(s ermite.Scope) string { | ||
| 31 | + | if s == ermite.Scope_SCOPE_MR_FORCE_MERGE { | |||
| 32 | + | return "mr:force-merge" | |||
| 33 | + | } | |||
| 31 | name, ok := ermite.Scope_name[int32(s)] | 34 | name, ok := ermite.Scope_name[int32(s)] | ||
| 32 | if !ok { | 35 | if !ok { | ||
| 33 | return strconv.Itoa(int(s)) | 36 | return strconv.Itoa(int(s)) | ||
M internal/adapters/client/scopes_test.go +1 -0
| @@ -16,6 +16,7 @@ import ( | |||
| 16 | 16 | ||
| 17 | 17 | func TestScopeWireNames(t *testing.T) { | |
| 18 | 18 | want := map[ermite.Scope]string{ | |
| 19 | + | ermite.Scope_SCOPE_MR_FORCE_MERGE: "mr:force-merge", | |
| 19 | 20 | ermite.Scope_SCOPE_MR_REVIEW: "mr:review", | |
| 20 | 21 | ermite.Scope_SCOPE_REPO_READ: "repo:read", | |
| 21 | 22 | ermite.Scope_SCOPE_REPO_WRITE: "repo:write", | |
| @@ -16,6 +16,7 @@ import ( | |||||
| 16 | 16 | ||||
| 17 | func TestScopeWireNames(t *testing.T) { | 17 | func TestScopeWireNames(t *testing.T) { | ||
| 18 | want := map[ermite.Scope]string{ | 18 | want := map[ermite.Scope]string{ | ||
| 19 | + | ermite.Scope_SCOPE_MR_FORCE_MERGE: "mr:force-merge", | |||
| 19 | ermite.Scope_SCOPE_MR_REVIEW: "mr:review", | 20 | ermite.Scope_SCOPE_MR_REVIEW: "mr:review", | ||
| 20 | ermite.Scope_SCOPE_REPO_READ: "repo:read", | 21 | ermite.Scope_SCOPE_REPO_READ: "repo:read", | ||
| 21 | ermite.Scope_SCOPE_REPO_WRITE: "repo:write", | 22 | ermite.Scope_SCOPE_REPO_WRITE: "repo:write", | ||
M internal/adapters/kvstore/issues_test.go +3 -3
| @@ -314,11 +314,11 @@ func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) { | |||
| 314 | 314 | assert.Len(t, full10.Scopes, 11) | |
| 315 | 315 | assert.Equal(t, 1, countScope(full10.Scopes, domain.ScopeMRReview)) | |
| 316 | 316 | assert.Contains(t, full10.Scopes, domain.ScopeIssueWrite) | |
| 317 | - | assert.True(t, domain.TokenPrincipal(full10).HasOwnerRights()) | |
| 317 | + | assert.False(t, domain.TokenPrincipal(full10).HasOwnerRights(), "historical migration does not grant mr:force-merge") | |
| 318 | 318 | assert.Len(t, get("reposmr").Scopes, 10) | |
| 319 | 319 | assert.NotContains(t, get("reposmr").Scopes, domain.ScopeIssueWrite) | |
| 320 | - | assert.True(t, domain.TokenPrincipal(full).HasOwnerRights()) | |
| 321 | - | assert.True(t, domain.TokenPrincipal(get("otherfull")).HasOwnerRights(), "the order of the scopes does not matter") | |
| 320 | + | assert.False(t, domain.TokenPrincipal(full).HasOwnerRights(), "historical migration does not grant mr:force-merge") | |
| 321 | + | assert.False(t, domain.TokenPrincipal(get("otherfull")).HasOwnerRights(), "historical migration does not grant mr:force-merge") | |
| 322 | 322 | ||
| 323 | 323 | assert.NotContains(t, get("missing").Scopes, domain.ScopeIssueWrite) | |
| 324 | 324 | assert.NotContains(t, get("missing").Scopes, domain.ScopeMRReview) | |
| @@ -314,11 +314,11 @@ func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) { | |||||
| 314 | assert.Len(t, full10.Scopes, 11) | 314 | assert.Len(t, full10.Scopes, 11) | ||
| 315 | assert.Equal(t, 1, countScope(full10.Scopes, domain.ScopeMRReview)) | 315 | assert.Equal(t, 1, countScope(full10.Scopes, domain.ScopeMRReview)) | ||
| 316 | assert.Contains(t, full10.Scopes, domain.ScopeIssueWrite) | 316 | assert.Contains(t, full10.Scopes, domain.ScopeIssueWrite) | ||
| 317 | - | assert.True(t, domain.TokenPrincipal(full10).HasOwnerRights()) | 317 | + | assert.False(t, domain.TokenPrincipal(full10).HasOwnerRights(), "historical migration does not grant mr:force-merge") |
| 318 | assert.Len(t, get("reposmr").Scopes, 10) | 318 | assert.Len(t, get("reposmr").Scopes, 10) | ||
| 319 | assert.NotContains(t, get("reposmr").Scopes, domain.ScopeIssueWrite) | 319 | assert.NotContains(t, get("reposmr").Scopes, domain.ScopeIssueWrite) | ||
| 320 | - | assert.True(t, domain.TokenPrincipal(full).HasOwnerRights()) | 320 | + | assert.False(t, domain.TokenPrincipal(full).HasOwnerRights(), "historical migration does not grant mr:force-merge") |
| 321 | - | assert.True(t, domain.TokenPrincipal(get("otherfull")).HasOwnerRights(), "the order of the scopes does not matter") | 321 | + | assert.False(t, domain.TokenPrincipal(get("otherfull")).HasOwnerRights(), "historical migration does not grant mr:force-merge") |
| 322 | 322 | ||||
| 323 | assert.NotContains(t, get("missing").Scopes, domain.ScopeIssueWrite) | 323 | assert.NotContains(t, get("missing").Scopes, domain.ScopeIssueWrite) | ||
| 324 | assert.NotContains(t, get("missing").Scopes, domain.ScopeMRReview) | 324 | assert.NotContains(t, get("missing").Scopes, domain.ScopeMRReview) | ||
M internal/adapters/kvstore/mergerequests.go +2 -2
| @@ -939,7 +939,7 @@ func decodeSide(s storagepb.DiffSide) domain.DiffSide { | |||
| 939 | 939 | } | |
| 940 | 940 | ||
| 941 | 941 | func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) { | |
| 942 | - | pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, MergeModesV2: true, AllowAutoMerge: s.AllowAutoMerge} | |
| 942 | + | pb := &storagepb.RepoSettings{MergeModesV2: true, AllowAutoMerge: s.AllowAutoMerge} | |
| 943 | 943 | for _, m := range s.AllowedMergeModes { | |
| 944 | 944 | mode, err := encodeMode(m) | |
| 945 | 945 | if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED { | |
| @@ -979,5 +979,5 @@ func decodeSettings(pb *storagepb.RepoSettings) domain.RepoSettings { | |||
| 979 | 979 | if !pb.GetMergeModesV2() && slices.Equal(normalized, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}) { | |
| 980 | 980 | normalized = domain.MergeModes() | |
| 981 | 981 | } | |
| 982 | - | return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock(), Issues: decodeIssuesMode(pb.GetIssues()), AllowAutoMerge: pb.GetAllowAutoMerge()} | |
| 982 | + | return domain.RepoSettings{AllowedMergeModes: normalized, Issues: decodeIssuesMode(pb.GetIssues()), AllowAutoMerge: pb.GetAllowAutoMerge()} | |
| 983 | 983 | } | |
| @@ -939,7 +939,7 @@ func decodeSide(s storagepb.DiffSide) domain.DiffSide { | |||||
| 939 | } | 939 | } | ||
| 940 | 940 | ||||
| 941 | func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) { | 941 | func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) { | ||
| 942 | - | pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, MergeModesV2: true, AllowAutoMerge: s.AllowAutoMerge} | 942 | + | pb := &storagepb.RepoSettings{MergeModesV2: true, AllowAutoMerge: s.AllowAutoMerge} |
| 943 | for _, m := range s.AllowedMergeModes { | 943 | for _, m := range s.AllowedMergeModes { | ||
| 944 | mode, err := encodeMode(m) | 944 | mode, err := encodeMode(m) | ||
| 945 | if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED { | 945 | if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED { | ||
| @@ -979,5 +979,5 @@ func decodeSettings(pb *storagepb.RepoSettings) domain.RepoSettings { | |||||
| 979 | if !pb.GetMergeModesV2() && slices.Equal(normalized, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}) { | 979 | if !pb.GetMergeModesV2() && slices.Equal(normalized, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}) { | ||
| 980 | normalized = domain.MergeModes() | 980 | normalized = domain.MergeModes() | ||
| 981 | } | 981 | } | ||
| 982 | - | return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock(), Issues: decodeIssuesMode(pb.GetIssues()), AllowAutoMerge: pb.GetAllowAutoMerge()} | 982 | + | return domain.RepoSettings{AllowedMergeModes: normalized, Issues: decodeIssuesMode(pb.GetIssues()), AllowAutoMerge: pb.GetAllowAutoMerge()} |
| 983 | } | 983 | } | ||
M internal/adapters/kvstore/mergerequests_test.go +2 -2
| @@ -470,7 +470,7 @@ func TestRepoSettings(t *testing.T) { | |||
| 470 | 470 | require.NoError(t, err) | |
| 471 | 471 | assert.Equal(t, domain.DefaultRepoSettings(), got.Settings) | |
| 472 | 472 | ||
| 473 | - | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, FailingChecksBlock: false, Issues: domain.IssuesOwner} | |
| 473 | + | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, Issues: domain.IssuesOwner} | |
| 474 | 474 | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = got.Settings; return true, nil }) | |
| 475 | 475 | require.NoError(t, err) | |
| 476 | 476 | again, err := s.Get(ctx, n) | |
| @@ -634,7 +634,7 @@ func TestStoredOldDefaultModesGainMerge(t *testing.T) { | |||
| 634 | 634 | ||
| 635 | 635 | put := func(db *kv.DB, repo string, modes ...storagepb.MergeMode) { | |
| 636 | 636 | val, err := proto.Marshal(&storagepb.Repo{Name: repo, Visibility: storagepb.Visibility_VISIBILITY_PUBLIC, | |
| 637 | - | Settings: &storagepb.RepoSettings{AllowedMergeModes: modes, FailingChecksBlock: true}}) | |
| 637 | + | Settings: &storagepb.RepoSettings{AllowedMergeModes: modes}}) | |
| 638 | 638 | require.NoError(t, err) | |
| 639 | 639 | var b kv.Batch | |
| 640 | 640 | b.Put([]byte("repo/"+repo), val) | |
| @@ -470,7 +470,7 @@ func TestRepoSettings(t *testing.T) { | |||||
| 470 | require.NoError(t, err) | 470 | require.NoError(t, err) | ||
| 471 | assert.Equal(t, domain.DefaultRepoSettings(), got.Settings) | 471 | assert.Equal(t, domain.DefaultRepoSettings(), got.Settings) | ||
| 472 | 472 | ||||
| 473 | - | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, FailingChecksBlock: false, Issues: domain.IssuesOwner} | 473 | + | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, Issues: domain.IssuesOwner} |
| 474 | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = got.Settings; return true, nil }) | 474 | _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = got.Settings; return true, nil }) | ||
| 475 | require.NoError(t, err) | 475 | require.NoError(t, err) | ||
| 476 | again, err := s.Get(ctx, n) | 476 | again, err := s.Get(ctx, n) | ||
| @@ -634,7 +634,7 @@ func TestStoredOldDefaultModesGainMerge(t *testing.T) { | |||||
| 634 | 634 | ||||
| 635 | put := func(db *kv.DB, repo string, modes ...storagepb.MergeMode) { | 635 | put := func(db *kv.DB, repo string, modes ...storagepb.MergeMode) { | ||
| 636 | val, err := proto.Marshal(&storagepb.Repo{Name: repo, Visibility: storagepb.Visibility_VISIBILITY_PUBLIC, | 636 | val, err := proto.Marshal(&storagepb.Repo{Name: repo, Visibility: storagepb.Visibility_VISIBILITY_PUBLIC, | ||
| 637 | - | Settings: &storagepb.RepoSettings{AllowedMergeModes: modes, FailingChecksBlock: true}}) | 637 | + | Settings: &storagepb.RepoSettings{AllowedMergeModes: modes}}) |
| 638 | require.NoError(t, err) | 638 | require.NoError(t, err) | ||
| 639 | var b kv.Batch | 639 | var b kv.Batch | ||
| 640 | b.Put([]byte("repo/"+repo), val) | 640 | b.Put([]byte("repo/"+repo), val) | ||
M internal/adapters/rpc/api.go +1 -1
| @@ -35,7 +35,7 @@ const RunnerMaxRequestBytes = MaxRequestBytes + 64<<10 | |||
| 35 | 35 | // always implements, as GetVersion lists them. FeaturePasskeys comes on top | |
| 36 | 36 | // when Config.Passkeys is set, and FeatureCISecrets when the settings services | |
| 37 | 37 | // are, and FeatureCommitSignatures when Config.TrustedKeys is set. | |
| 38 | - | var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows", "merge_requests.auto_merge"} | |
| 38 | + | var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows", "merge_requests.auto_merge", "merge_requests.ignore_checks"} | |
| 39 | 39 | ||
| 40 | 40 | // FeaturePasskeys is listed by GetVersion when passkeys are available. | |
| 41 | 41 | // FeatureCIArtifacts (artifact.go) when the artifact services are served. | |
| @@ -35,7 +35,7 @@ const RunnerMaxRequestBytes = MaxRequestBytes + 64<<10 | |||||
| 35 | // always implements, as GetVersion lists them. FeaturePasskeys comes on top | 35 | // always implements, as GetVersion lists them. FeaturePasskeys comes on top | ||
| 36 | // when Config.Passkeys is set, and FeatureCISecrets when the settings services | 36 | // when Config.Passkeys is set, and FeatureCISecrets when the settings services | ||
| 37 | // are, and FeatureCommitSignatures when Config.TrustedKeys is set. | 37 | // are, and FeatureCommitSignatures when Config.TrustedKeys is set. | ||
| 38 | - | var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows", "merge_requests.auto_merge"} | 38 | + | var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows", "merge_requests.auto_merge", "merge_requests.ignore_checks"} |
| 39 | 39 | ||||
| 40 | // FeaturePasskeys is listed by GetVersion when passkeys are available. | 40 | // FeaturePasskeys is listed by GetVersion when passkeys are available. | ||
| 41 | // FeatureCIArtifacts (artifact.go) when the artifact services are served. | 41 | // FeatureCIArtifacts (artifact.go) when the artifact services are served. | ||
M internal/adapters/rpc/convert.go +2 -1
| @@ -72,7 +72,7 @@ func repoToPB(v ports.RepoView) *pb.Repo { | |||
| 72 | 72 | } | |
| 73 | 73 | ||
| 74 | 74 | func settingsToPB(s domain.RepoSettings) *pb.RepoSettings { | |
| 75 | - | out := &pb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, AllowAutoMerge: s.AllowAutoMerge, Issues: issuesModeToPB(s.Issues)} | |
| 75 | + | out := &pb.RepoSettings{AllowAutoMerge: s.AllowAutoMerge, Issues: issuesModeToPB(s.Issues)} | |
| 76 | 76 | for _, m := range s.AllowedMergeModes { | |
| 77 | 77 | out.AllowedMergeModes = append(out.AllowedMergeModes, mergeModeToPB(m)) | |
| 78 | 78 | } | |
| @@ -131,6 +131,7 @@ var scopeToPB = map[domain.Scope]pb.Scope{ | |||
| 131 | 131 | domain.ScopeRepoAdmin: pb.Scope_SCOPE_REPO_ADMIN, | |
| 132 | 132 | domain.ScopeMRWrite: pb.Scope_SCOPE_MR_WRITE, | |
| 133 | 133 | domain.ScopeMRMerge: pb.Scope_SCOPE_MR_MERGE, | |
| 134 | + | domain.ScopeMRForceMerge: pb.Scope_SCOPE_MR_FORCE_MERGE, | |
| 134 | 135 | domain.ScopeCIWrite: pb.Scope_SCOPE_CI_WRITE, | |
| 135 | 136 | domain.ScopeSecretsWrite: pb.Scope_SCOPE_SECRETS_WRITE, | |
| 136 | 137 | domain.ScopeRunnerRegister: pb.Scope_SCOPE_RUNNER_REGISTER, | |
| @@ -72,7 +72,7 @@ func repoToPB(v ports.RepoView) *pb.Repo { | |||||
| 72 | } | 72 | } | ||
| 73 | 73 | ||||
| 74 | func settingsToPB(s domain.RepoSettings) *pb.RepoSettings { | 74 | func settingsToPB(s domain.RepoSettings) *pb.RepoSettings { | ||
| 75 | - | out := &pb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, AllowAutoMerge: s.AllowAutoMerge, Issues: issuesModeToPB(s.Issues)} | 75 | + | out := &pb.RepoSettings{AllowAutoMerge: s.AllowAutoMerge, Issues: issuesModeToPB(s.Issues)} |
| 76 | for _, m := range s.AllowedMergeModes { | 76 | for _, m := range s.AllowedMergeModes { | ||
| 77 | out.AllowedMergeModes = append(out.AllowedMergeModes, mergeModeToPB(m)) | 77 | out.AllowedMergeModes = append(out.AllowedMergeModes, mergeModeToPB(m)) | ||
| 78 | } | 78 | } | ||
| @@ -131,6 +131,7 @@ var scopeToPB = map[domain.Scope]pb.Scope{ | |||||
| 131 | domain.ScopeRepoAdmin: pb.Scope_SCOPE_REPO_ADMIN, | 131 | domain.ScopeRepoAdmin: pb.Scope_SCOPE_REPO_ADMIN, | ||
| 132 | domain.ScopeMRWrite: pb.Scope_SCOPE_MR_WRITE, | 132 | domain.ScopeMRWrite: pb.Scope_SCOPE_MR_WRITE, | ||
| 133 | domain.ScopeMRMerge: pb.Scope_SCOPE_MR_MERGE, | 133 | domain.ScopeMRMerge: pb.Scope_SCOPE_MR_MERGE, | ||
| 134 | + | domain.ScopeMRForceMerge: pb.Scope_SCOPE_MR_FORCE_MERGE, | |||
| 134 | domain.ScopeCIWrite: pb.Scope_SCOPE_CI_WRITE, | 135 | domain.ScopeCIWrite: pb.Scope_SCOPE_CI_WRITE, | ||
| 135 | domain.ScopeSecretsWrite: pb.Scope_SCOPE_SECRETS_WRITE, | 136 | domain.ScopeSecretsWrite: pb.Scope_SCOPE_SECRETS_WRITE, | ||
| 136 | domain.ScopeRunnerRegister: pb.Scope_SCOPE_RUNNER_REGISTER, | 137 | domain.ScopeRunnerRegister: pb.Scope_SCOPE_RUNNER_REGISTER, | ||
M internal/adapters/rpc/convert_test.go +9 -9
| @@ -214,12 +214,12 @@ func TestRepoToPBAlwaysHasSettings(t *testing.T) { | |||
| 214 | 214 | // A repository value without settings (never stored with them) reads as the defaults. | |
| 215 | 215 | r := repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustName(t, "x")}}) | |
| 216 | 216 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_MERGE_COMMIT}, r.GetSettings().GetAllowedMergeModes()) | |
| 217 | - | assert.True(t, r.GetSettings().GetFailingChecksBlock()) | |
| 217 | + | assert.False(t, r.GetSettings().GetAllowAutoMerge()) | |
| 218 | 218 | ||
| 219 | 219 | custom := domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}} | |
| 220 | 220 | r = repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustName(t, "x"), Settings: custom}}) | |
| 221 | 221 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, r.GetSettings().GetAllowedMergeModes()) | |
| 222 | - | assert.False(t, r.GetSettings().GetFailingChecksBlock()) | |
| 222 | + | assert.False(t, r.GetSettings().GetAllowAutoMerge()) | |
| 223 | 223 | } | |
| 224 | 224 | ||
| 225 | 225 | func mustName(t *testing.T, s string) domain.RepoName { | |
| @@ -235,8 +235,8 @@ func TestUpdateParamsSettingsPaths(t *testing.T) { | |||
| 235 | 235 | return &pb.UpdateRepoRequest{ | |
| 236 | 236 | UpdateMask: &fieldmaskpb.FieldMask{Paths: paths}, | |
| 237 | 237 | Settings: &pb.RepoSettings{ | |
| 238 | - | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, | |
| 239 | - | FailingChecksBlock: true, | |
| 238 | + | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, | |
| 239 | + | AllowAutoMerge: true, | |
| 240 | 240 | }, | |
| 241 | 241 | } | |
| 242 | 242 | } | |
| @@ -244,18 +244,18 @@ func TestUpdateParamsSettingsPaths(t *testing.T) { | |||
| 244 | 244 | require.NoError(t, err) | |
| 245 | 245 | require.NotNil(t, p.AllowedMergeModes) | |
| 246 | 246 | assert.Equal(t, []domain.MergeMode{domain.MergeModeSquash}, *p.AllowedMergeModes) | |
| 247 | - | require.NotNil(t, p.FailingChecksBlock) | |
| 248 | - | assert.True(t, *p.FailingChecksBlock) | |
| 247 | + | require.NotNil(t, p.AllowAutoMerge) | |
| 248 | + | assert.True(t, *p.AllowAutoMerge) | |
| 249 | 249 | ||
| 250 | 250 | p, err = updateParams(msg("settings.allowed_merge_modes")) | |
| 251 | 251 | require.NoError(t, err) | |
| 252 | 252 | assert.NotNil(t, p.AllowedMergeModes) | |
| 253 | - | assert.Nil(t, p.FailingChecksBlock, "not named: left alone") | |
| 253 | + | assert.Nil(t, p.AllowAutoMerge, "not named: left alone") | |
| 254 | 254 | ||
| 255 | - | p, err = updateParams(msg("settings.failing_checks_block")) | |
| 255 | + | p, err = updateParams(msg("settings.allow_auto_merge")) | |
| 256 | 256 | require.NoError(t, err) | |
| 257 | 257 | assert.Nil(t, p.AllowedMergeModes) | |
| 258 | - | assert.NotNil(t, p.FailingChecksBlock) | |
| 258 | + | assert.NotNil(t, p.AllowAutoMerge) | |
| 259 | 259 | ||
| 260 | 260 | // Modes named with nothing to take them from are an empty list, which the use case refuses. | |
| 261 | 261 | p, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.allowed_merge_modes"}}}) | |
| @@ -214,12 +214,12 @@ func TestRepoToPBAlwaysHasSettings(t *testing.T) { | |||||
| 214 | // A repository value without settings (never stored with them) reads as the defaults. | 214 | // A repository value without settings (never stored with them) reads as the defaults. | ||
| 215 | r := repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustName(t, "x")}}) | 215 | r := repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustName(t, "x")}}) | ||
| 216 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_MERGE_COMMIT}, r.GetSettings().GetAllowedMergeModes()) | 216 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_MERGE_COMMIT}, r.GetSettings().GetAllowedMergeModes()) | ||
| 217 | - | assert.True(t, r.GetSettings().GetFailingChecksBlock()) | 217 | + | assert.False(t, r.GetSettings().GetAllowAutoMerge()) |
| 218 | 218 | ||||
| 219 | custom := domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}} | 219 | custom := domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}} | ||
| 220 | r = repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustName(t, "x"), Settings: custom}}) | 220 | r = repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustName(t, "x"), Settings: custom}}) | ||
| 221 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, r.GetSettings().GetAllowedMergeModes()) | 221 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, r.GetSettings().GetAllowedMergeModes()) | ||
| 222 | - | assert.False(t, r.GetSettings().GetFailingChecksBlock()) | 222 | + | assert.False(t, r.GetSettings().GetAllowAutoMerge()) |
| 223 | } | 223 | } | ||
| 224 | 224 | ||||
| 225 | func mustName(t *testing.T, s string) domain.RepoName { | 225 | func mustName(t *testing.T, s string) domain.RepoName { | ||
| @@ -235,8 +235,8 @@ func TestUpdateParamsSettingsPaths(t *testing.T) { | |||||
| 235 | return &pb.UpdateRepoRequest{ | 235 | return &pb.UpdateRepoRequest{ | ||
| 236 | UpdateMask: &fieldmaskpb.FieldMask{Paths: paths}, | 236 | UpdateMask: &fieldmaskpb.FieldMask{Paths: paths}, | ||
| 237 | Settings: &pb.RepoSettings{ | 237 | Settings: &pb.RepoSettings{ | ||
| 238 | - | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, | 238 | + | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, |
| 239 | - | FailingChecksBlock: true, | 239 | + | AllowAutoMerge: true, |
| 240 | }, | 240 | }, | ||
| 241 | } | 241 | } | ||
| 242 | } | 242 | } | ||
| @@ -244,18 +244,18 @@ func TestUpdateParamsSettingsPaths(t *testing.T) { | |||||
| 244 | require.NoError(t, err) | 244 | require.NoError(t, err) | ||
| 245 | require.NotNil(t, p.AllowedMergeModes) | 245 | require.NotNil(t, p.AllowedMergeModes) | ||
| 246 | assert.Equal(t, []domain.MergeMode{domain.MergeModeSquash}, *p.AllowedMergeModes) | 246 | assert.Equal(t, []domain.MergeMode{domain.MergeModeSquash}, *p.AllowedMergeModes) | ||
| 247 | - | require.NotNil(t, p.FailingChecksBlock) | 247 | + | require.NotNil(t, p.AllowAutoMerge) |
| 248 | - | assert.True(t, *p.FailingChecksBlock) | 248 | + | assert.True(t, *p.AllowAutoMerge) |
| 249 | 249 | ||||
| 250 | p, err = updateParams(msg("settings.allowed_merge_modes")) | 250 | p, err = updateParams(msg("settings.allowed_merge_modes")) | ||
| 251 | require.NoError(t, err) | 251 | require.NoError(t, err) | ||
| 252 | assert.NotNil(t, p.AllowedMergeModes) | 252 | assert.NotNil(t, p.AllowedMergeModes) | ||
| 253 | - | assert.Nil(t, p.FailingChecksBlock, "not named: left alone") | 253 | + | assert.Nil(t, p.AllowAutoMerge, "not named: left alone") |
| 254 | 254 | ||||
| 255 | - | p, err = updateParams(msg("settings.failing_checks_block")) | 255 | + | p, err = updateParams(msg("settings.allow_auto_merge")) |
| 256 | require.NoError(t, err) | 256 | require.NoError(t, err) | ||
| 257 | assert.Nil(t, p.AllowedMergeModes) | 257 | assert.Nil(t, p.AllowedMergeModes) | ||
| 258 | - | assert.NotNil(t, p.FailingChecksBlock) | 258 | + | assert.NotNil(t, p.AllowAutoMerge) |
| 259 | 259 | ||||
| 260 | // Modes named with nothing to take them from are an empty list, which the use case refuses. | 260 | // Modes named with nothing to take them from are an empty list, which the use case refuses. | ||
| 261 | p, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.allowed_merge_modes"}}}) | 261 | p, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.allowed_merge_modes"}}}) | ||
M internal/adapters/rpc/mergerequest.go +6 -1
| @@ -399,7 +399,7 @@ func (s *mergeRequestService) MergeMergeRequest(ctx context.Context, req *connec | |||
| 399 | 399 | return nil, connectError(ctx, err) | |
| 400 | 400 | } | |
| 401 | 401 | res, err := s.mrs.Merge(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.MergeParams{ | |
| 402 | - | Mode: mode, HeadID: head, DeleteSource: req.Msg.GetDeleteSourceBranch(), | |
| 402 | + | Mode: mode, HeadID: head, DeleteSource: req.Msg.GetDeleteSourceBranch(), IgnoreChecks: req.Msg.GetIgnoreChecks(), | |
| 403 | 403 | }) | |
| 404 | 404 | if err != nil { | |
| 405 | 405 | return nil, connectError(ctx, err) | |
| @@ -423,3 +423,8 @@ func (s *mergeRequestService) MarkRead(ctx context.Context, req *connect.Request | |||
| 423 | 423 | } | |
| 424 | 424 | return connect.NewResponse(&pb.MarkReadResponse{}), nil | |
| 425 | 425 | } | |
| 426 | + | ||
| 427 | + | // MarkReady is implemented in the next server slice. | |
| 428 | + | func (s *mergeRequestService) MarkReady(context.Context, *connect.Request[pb.MarkReadyRequest]) (*connect.Response[pb.MarkReadyResponse], error) { | |
| 429 | + | return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("MarkReady is not implemented")) | |
| 430 | + | } | |
| @@ -399,7 +399,7 @@ func (s *mergeRequestService) MergeMergeRequest(ctx context.Context, req *connec | |||||
| 399 | return nil, connectError(ctx, err) | 399 | return nil, connectError(ctx, err) | ||
| 400 | } | 400 | } | ||
| 401 | res, err := s.mrs.Merge(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.MergeParams{ | 401 | res, err := s.mrs.Merge(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.MergeParams{ | ||
| 402 | - | Mode: mode, HeadID: head, DeleteSource: req.Msg.GetDeleteSourceBranch(), | 402 | + | Mode: mode, HeadID: head, DeleteSource: req.Msg.GetDeleteSourceBranch(), IgnoreChecks: req.Msg.GetIgnoreChecks(), |
| 403 | }) | 403 | }) | ||
| 404 | if err != nil { | 404 | if err != nil { | ||
| 405 | return nil, connectError(ctx, err) | 405 | return nil, connectError(ctx, err) | ||
| @@ -423,3 +423,8 @@ func (s *mergeRequestService) MarkRead(ctx context.Context, req *connect.Request | |||||
| 423 | } | 423 | } | ||
| 424 | return connect.NewResponse(&pb.MarkReadResponse{}), nil | 424 | return connect.NewResponse(&pb.MarkReadResponse{}), nil | ||
| 425 | } | 425 | } | ||
| 426 | + | ||||
| 427 | + | // MarkReady is implemented in the next server slice. | |||
| 428 | + | func (s *mergeRequestService) MarkReady(context.Context, *connect.Request[pb.MarkReadyRequest]) (*connect.Response[pb.MarkReadyResponse], error) { | |||
| 429 | + | return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("MarkReady is not implemented")) | |||
| 430 | + | } | |||
M internal/adapters/rpc/mergerequest_test.go +2 -1
| @@ -140,6 +140,7 @@ func TestMergeRequestServiceMerge(t *testing.T) { | |||
| 140 | 140 | wantCall bool | |
| 141 | 141 | }{ | |
| 142 | 142 | {name: "ok", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head, DeleteSourceBranch: true}, wantCall: true}, | |
| 143 | + | {name: "ignore checks", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head, IgnoreChecks: true}, wantCall: true}, | |
| 143 | 144 | {name: "head is not a full id", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: "abc"}, wantCode: connect.CodeInvalidArgument}, | |
| 144 | 145 | {name: "invalid repo", req: &pb.MergeMergeRequestRequest{Repo: "../x", Number: 1}, wantCode: connect.CodeInvalidArgument}, | |
| 145 | 146 | {name: "head moved", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head}, usecase: domain.ErrConflict, wantCode: connect.CodeAborted, wantCall: true}, | |
| @@ -152,7 +153,7 @@ func TestMergeRequestServiceMerge(t *testing.T) { | |||
| 152 | 153 | mrs := mocks.NewMergeRequestService(t) | |
| 153 | 154 | if tt.wantCall { | |
| 154 | 155 | mrs.EXPECT().Merge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), mock.MatchedBy(func(p ports.MergeParams) bool { | |
| 155 | - | return p.DeleteSource == tt.req.GetDeleteSourceBranch() && (tt.req.GetHeadId() == "") == p.HeadID.IsZero() | |
| 156 | + | return p.IgnoreChecks == tt.req.GetIgnoreChecks() && p.DeleteSource == tt.req.GetDeleteSourceBranch() && (tt.req.GetHeadId() == "") == p.HeadID.IsZero() | |
| 156 | 157 | })).Return(ports.MergeResult{MergeRequest: domain.MergeRequest{Number: 1}, SourceDeleted: true}, tt.usecase).Once() | |
| 157 | 158 | } | |
| 158 | 159 | resp, err := (&mergeRequestService{mrs: mrs}).MergeMergeRequest(ownerCtx(), connect.NewRequest(tt.req)) | |
| @@ -140,6 +140,7 @@ func TestMergeRequestServiceMerge(t *testing.T) { | |||||
| 140 | wantCall bool | 140 | wantCall bool | ||
| 141 | }{ | 141 | }{ | ||
| 142 | {name: "ok", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head, DeleteSourceBranch: true}, wantCall: true}, | 142 | {name: "ok", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head, DeleteSourceBranch: true}, wantCall: true}, | ||
| 143 | + | {name: "ignore checks", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head, IgnoreChecks: true}, wantCall: true}, | |||
| 143 | {name: "head is not a full id", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: "abc"}, wantCode: connect.CodeInvalidArgument}, | 144 | {name: "head is not a full id", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: "abc"}, wantCode: connect.CodeInvalidArgument}, | ||
| 144 | {name: "invalid repo", req: &pb.MergeMergeRequestRequest{Repo: "../x", Number: 1}, wantCode: connect.CodeInvalidArgument}, | 145 | {name: "invalid repo", req: &pb.MergeMergeRequestRequest{Repo: "../x", Number: 1}, wantCode: connect.CodeInvalidArgument}, | ||
| 145 | {name: "head moved", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head}, usecase: domain.ErrConflict, wantCode: connect.CodeAborted, wantCall: true}, | 146 | {name: "head moved", req: &pb.MergeMergeRequestRequest{Repo: "demo", Number: 1, HeadId: head}, usecase: domain.ErrConflict, wantCode: connect.CodeAborted, wantCall: true}, | ||
| @@ -152,7 +153,7 @@ func TestMergeRequestServiceMerge(t *testing.T) { | |||||
| 152 | mrs := mocks.NewMergeRequestService(t) | 153 | mrs := mocks.NewMergeRequestService(t) | ||
| 153 | if tt.wantCall { | 154 | if tt.wantCall { | ||
| 154 | mrs.EXPECT().Merge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), mock.MatchedBy(func(p ports.MergeParams) bool { | 155 | mrs.EXPECT().Merge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), mock.MatchedBy(func(p ports.MergeParams) bool { | ||
| 155 | - | return p.DeleteSource == tt.req.GetDeleteSourceBranch() && (tt.req.GetHeadId() == "") == p.HeadID.IsZero() | 156 | + | return p.IgnoreChecks == tt.req.GetIgnoreChecks() && p.DeleteSource == tt.req.GetDeleteSourceBranch() && (tt.req.GetHeadId() == "") == p.HeadID.IsZero() |
| 156 | })).Return(ports.MergeResult{MergeRequest: domain.MergeRequest{Number: 1}, SourceDeleted: true}, tt.usecase).Once() | 157 | })).Return(ports.MergeResult{MergeRequest: domain.MergeRequest{Number: 1}, SourceDeleted: true}, tt.usecase).Once() | ||
| 157 | } | 158 | } | ||
| 158 | resp, err := (&mergeRequestService{mrs: mrs}).MergeMergeRequest(ownerCtx(), connect.NewRequest(tt.req)) | 159 | resp, err := (&mergeRequestService{mrs: mrs}).MergeMergeRequest(ownerCtx(), connect.NewRequest(tt.req)) | ||
M internal/adapters/rpc/repo.go +2 -5
| @@ -80,7 +80,7 @@ func (s *repoService) UpdateRepo(ctx context.Context, req *connect.Request[pb.Up | |||
| 80 | 80 | ||
| 81 | 81 | // updateParams maps the field mask of an update. The mask must name at least | |
| 82 | 82 | // one of "description", "topics", "visibility", "default_branch", "settings", | |
| 83 | - | // "settings.allowed_merge_modes", "settings.failing_checks_block" and | |
| 83 | + | // "settings.allowed_merge_modes" and | |
| 84 | 84 | // "settings.allow_auto_merge", and nothing else; the optional issues field stands outside the mask, and alone | |
| 85 | 85 | // it is a valid update. | |
| 86 | 86 | func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) { | |
| @@ -113,7 +113,7 @@ func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) { | |||
| 113 | 113 | params.Visibility = &vis | |
| 114 | 114 | case "default_branch": | |
| 115 | 115 | params.DefaultBranch = new(msg.GetDefaultBranch()) | |
| 116 | - | case "settings", "settings.allowed_merge_modes", "settings.failing_checks_block", "settings.allow_auto_merge": | |
| 116 | + | case "settings", "settings.allowed_merge_modes", "settings.allow_auto_merge": | |
| 117 | 117 | all := path == "settings" | |
| 118 | 118 | if all || path == "settings.allowed_merge_modes" { | |
| 119 | 119 | modes, err := mergeModesFromPB(msg.GetSettings().GetAllowedMergeModes()) | |
| @@ -122,9 +122,6 @@ func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) { | |||
| 122 | 122 | } | |
| 123 | 123 | params.AllowedMergeModes = &modes | |
| 124 | 124 | } | |
| 125 | - | if all || path == "settings.failing_checks_block" { | |
| 126 | - | params.FailingChecksBlock = new(msg.GetSettings().GetFailingChecksBlock()) | |
| 127 | - | } | |
| 128 | 125 | if all || path == "settings.allow_auto_merge" { | |
| 129 | 126 | params.AllowAutoMerge = new(msg.GetSettings().GetAllowAutoMerge()) | |
| 130 | 127 | } | |
| @@ -80,7 +80,7 @@ func (s *repoService) UpdateRepo(ctx context.Context, req *connect.Request[pb.Up | |||||
| 80 | 80 | ||||
| 81 | // updateParams maps the field mask of an update. The mask must name at least | 81 | // updateParams maps the field mask of an update. The mask must name at least | ||
| 82 | // one of "description", "topics", "visibility", "default_branch", "settings", | 82 | // one of "description", "topics", "visibility", "default_branch", "settings", | ||
| 83 | - | // "settings.allowed_merge_modes", "settings.failing_checks_block" and | 83 | + | // "settings.allowed_merge_modes" and |
| 84 | // "settings.allow_auto_merge", and nothing else; the optional issues field stands outside the mask, and alone | 84 | // "settings.allow_auto_merge", and nothing else; the optional issues field stands outside the mask, and alone | ||
| 85 | // it is a valid update. | 85 | // it is a valid update. | ||
| 86 | func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) { | 86 | func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) { | ||
| @@ -113,7 +113,7 @@ func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) { | |||||
| 113 | params.Visibility = &vis | 113 | params.Visibility = &vis | ||
| 114 | case "default_branch": | 114 | case "default_branch": | ||
| 115 | params.DefaultBranch = new(msg.GetDefaultBranch()) | 115 | params.DefaultBranch = new(msg.GetDefaultBranch()) | ||
| 116 | - | case "settings", "settings.allowed_merge_modes", "settings.failing_checks_block", "settings.allow_auto_merge": | 116 | + | case "settings", "settings.allowed_merge_modes", "settings.allow_auto_merge": |
| 117 | all := path == "settings" | 117 | all := path == "settings" | ||
| 118 | if all || path == "settings.allowed_merge_modes" { | 118 | if all || path == "settings.allowed_merge_modes" { | ||
| 119 | modes, err := mergeModesFromPB(msg.GetSettings().GetAllowedMergeModes()) | 119 | modes, err := mergeModesFromPB(msg.GetSettings().GetAllowedMergeModes()) | ||
| @@ -122,9 +122,6 @@ func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) { | |||||
| 122 | } | 122 | } | ||
| 123 | params.AllowedMergeModes = &modes | 123 | params.AllowedMergeModes = &modes | ||
| 124 | } | 124 | } | ||
| 125 | - | if all || path == "settings.failing_checks_block" { | |||
| 126 | - | params.FailingChecksBlock = new(msg.GetSettings().GetFailingChecksBlock()) | |||
| 127 | - | } | |||
| 128 | if all || path == "settings.allow_auto_merge" { | 125 | if all || path == "settings.allow_auto_merge" { | ||
| 129 | params.AllowAutoMerge = new(msg.GetSettings().GetAllowAutoMerge()) | 126 | params.AllowAutoMerge = new(msg.GetSettings().GetAllowAutoMerge()) | ||
| 130 | } | 127 | } | ||
M internal/adapters/rpc/repo_test.go +1 -7
| @@ -284,24 +284,18 @@ func TestRepoToPBTopics(t *testing.T) { | |||
| 284 | 284 | func TestUpdateParamsAllowAutoMerge(t *testing.T) { | |
| 285 | 285 | t.Parallel() | |
| 286 | 286 | mask := func(paths ...string) *fieldmaskpb.FieldMask { return &fieldmaskpb.FieldMask{Paths: paths} } | |
| 287 | - | settings := &pb.RepoSettings{AllowAutoMerge: true, FailingChecksBlock: true, AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}} | |
| 287 | + | settings := &pb.RepoSettings{AllowAutoMerge: true, AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}} | |
| 288 | 288 | ||
| 289 | 289 | params, err := updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.allow_auto_merge"), Settings: settings}) | |
| 290 | 290 | require.NoError(t, err) | |
| 291 | 291 | require.NotNil(t, params.AllowAutoMerge) | |
| 292 | 292 | assert.True(t, *params.AllowAutoMerge) | |
| 293 | - | assert.Nil(t, params.FailingChecksBlock, "only the masked setting is set") | |
| 294 | 293 | assert.Nil(t, params.AllowedMergeModes) | |
| 295 | 294 | ||
| 296 | 295 | params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings"), Settings: settings}) | |
| 297 | 296 | require.NoError(t, err) | |
| 298 | 297 | require.NotNil(t, params.AllowAutoMerge, "settings covers allow_auto_merge") | |
| 299 | 298 | assert.True(t, *params.AllowAutoMerge) | |
| 300 | - | require.NotNil(t, params.FailingChecksBlock) | |
| 301 | 299 | require.NotNil(t, params.AllowedMergeModes) | |
| 302 | 300 | ||
| 303 | - | params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.failing_checks_block"), Settings: settings}) | |
| 304 | - | require.NoError(t, err) | |
| 305 | - | assert.Nil(t, params.AllowAutoMerge) | |
| 306 | - | require.NotNil(t, params.FailingChecksBlock) | |
| 307 | 301 | } | |
| @@ -284,24 +284,18 @@ func TestRepoToPBTopics(t *testing.T) { | |||||
| 284 | func TestUpdateParamsAllowAutoMerge(t *testing.T) { | 284 | func TestUpdateParamsAllowAutoMerge(t *testing.T) { | ||
| 285 | t.Parallel() | 285 | t.Parallel() | ||
| 286 | mask := func(paths ...string) *fieldmaskpb.FieldMask { return &fieldmaskpb.FieldMask{Paths: paths} } | 286 | mask := func(paths ...string) *fieldmaskpb.FieldMask { return &fieldmaskpb.FieldMask{Paths: paths} } | ||
| 287 | - | settings := &pb.RepoSettings{AllowAutoMerge: true, FailingChecksBlock: true, AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}} | 287 | + | settings := &pb.RepoSettings{AllowAutoMerge: true, AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}} |
| 288 | 288 | ||||
| 289 | params, err := updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.allow_auto_merge"), Settings: settings}) | 289 | params, err := updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.allow_auto_merge"), Settings: settings}) | ||
| 290 | require.NoError(t, err) | 290 | require.NoError(t, err) | ||
| 291 | require.NotNil(t, params.AllowAutoMerge) | 291 | require.NotNil(t, params.AllowAutoMerge) | ||
| 292 | assert.True(t, *params.AllowAutoMerge) | 292 | assert.True(t, *params.AllowAutoMerge) | ||
| 293 | - | assert.Nil(t, params.FailingChecksBlock, "only the masked setting is set") | |||
| 294 | assert.Nil(t, params.AllowedMergeModes) | 293 | assert.Nil(t, params.AllowedMergeModes) | ||
| 295 | 294 | ||||
| 296 | params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings"), Settings: settings}) | 295 | params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings"), Settings: settings}) | ||
| 297 | require.NoError(t, err) | 296 | require.NoError(t, err) | ||
| 298 | require.NotNil(t, params.AllowAutoMerge, "settings covers allow_auto_merge") | 297 | require.NotNil(t, params.AllowAutoMerge, "settings covers allow_auto_merge") | ||
| 299 | assert.True(t, *params.AllowAutoMerge) | 298 | assert.True(t, *params.AllowAutoMerge) | ||
| 300 | - | require.NotNil(t, params.FailingChecksBlock) | |||
| 301 | require.NotNil(t, params.AllowedMergeModes) | 299 | require.NotNil(t, params.AllowedMergeModes) | ||
| 302 | 300 | ||||
| 303 | - | params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.failing_checks_block"), Settings: settings}) | |||
| 304 | - | require.NoError(t, err) | |||
| 305 | - | assert.Nil(t, params.AllowAutoMerge) | |||
| 306 | - | require.NotNil(t, params.FailingChecksBlock) | |||
| 307 | } | 301 | } | ||
M internal/adapters/web/checks.go +1 -3
| @@ -111,10 +111,8 @@ func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | |||
| 111 | 111 | switch { | |
| 112 | 112 | case len(checks) == 0: | |
| 113 | 113 | sec.Note = "no check on this commit: nothing blocks the merge." | |
| 114 | - | case repo.Settings.FailingChecksBlock: | |
| 115 | - | sec.Note = "a failing or unfinished check blocks the merge." | |
| 116 | 114 | default: | |
| 117 | - | sec.Note = "checks do not block the merge in this repository." | |
| 115 | + | sec.Note = "a failing or unfinished check blocks the merge." | |
| 118 | 116 | } | |
| 119 | 117 | return sec | |
| 120 | 118 | } | |
| @@ -111,10 +111,8 @@ func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | |||||
| 111 | switch { | 111 | switch { | ||
| 112 | case len(checks) == 0: | 112 | case len(checks) == 0: | ||
| 113 | sec.Note = "no check on this commit: nothing blocks the merge." | 113 | sec.Note = "no check on this commit: nothing blocks the merge." | ||
| 114 | - | case repo.Settings.FailingChecksBlock: | |||
| 115 | - | sec.Note = "a failing or unfinished check blocks the merge." | |||
| 116 | default: | 114 | default: | ||
| 117 | - | sec.Note = "checks do not block the merge in this repository." | 115 | + | sec.Note = "a failing or unfinished check blocks the merge." |
| 118 | } | 116 | } | ||
| 119 | return sec | 117 | return sec | ||
| 120 | } | 118 | } | ||
M internal/adapters/web/mr.go +1 -1
| @@ -740,7 +740,7 @@ func (s *Server) mrFacts(ctx context.Context, r *http.Request, d *repoData, p *M | |||
| 740 | 740 | p.Modes = append(p.Modes, name) | |
| 741 | 741 | } | |
| 742 | 742 | } | |
| 743 | - | p.ChecksBlock = d.repo.Settings.FailingChecksBlock | |
| 743 | + | p.ChecksBlock = true | |
| 744 | 744 | if head != "" { | |
| 745 | 745 | checks := s.checksFor(ctx, d.repo.Name, head) | |
| 746 | 746 | p.Checks = checksSection(d.repo.Repo, checks) | |
| @@ -740,7 +740,7 @@ func (s *Server) mrFacts(ctx context.Context, r *http.Request, d *repoData, p *M | |||||
| 740 | p.Modes = append(p.Modes, name) | 740 | p.Modes = append(p.Modes, name) | ||
| 741 | } | 741 | } | ||
| 742 | } | 742 | } | ||
| 743 | - | p.ChecksBlock = d.repo.Settings.FailingChecksBlock | 743 | + | p.ChecksBlock = true |
| 744 | if head != "" { | 744 | if head != "" { | ||
| 745 | checks := s.checksFor(ctx, d.repo.Name, head) | 745 | checks := s.checksFor(ctx, d.repo.Name, head) | ||
| 746 | p.Checks = checksSection(d.repo.Repo, checks) | 746 | p.Checks = checksSection(d.repo.Repo, checks) | ||
M internal/app/api_test.go +1 -1
| @@ -196,7 +196,7 @@ func TestAC2ReadAccess(t *testing.T) { | |||
| 196 | 196 | for _, who := range allPrincipals { | |
| 197 | 197 | resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{})) | |
| 198 | 198 | require.NoError(t, err, who) | |
| 199 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge"}, resp.Msg.GetFeatures(), who) | |
| 199 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks"}, resp.Msg.GetFeatures(), who) | |
| 200 | 200 | assert.NotEmpty(t, resp.Msg.GetVersion()) | |
| 201 | 201 | } | |
| 202 | 202 | }) | |
| @@ -196,7 +196,7 @@ func TestAC2ReadAccess(t *testing.T) { | |||||
| 196 | for _, who := range allPrincipals { | 196 | for _, who := range allPrincipals { | ||
| 197 | resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{})) | 197 | resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{})) | ||
| 198 | require.NoError(t, err, who) | 198 | require.NoError(t, err, who) | ||
| 199 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge"}, resp.Msg.GetFeatures(), who) | 199 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks"}, resp.Msg.GetFeatures(), who) |
| 200 | assert.NotEmpty(t, resp.Msg.GetVersion()) | 200 | assert.NotEmpty(t, resp.Msg.GetVersion()) | ||
| 201 | } | 201 | } | ||
| 202 | }) | 202 | }) | ||
M internal/app/ci_e2e_test.go +3 -9
| @@ -13,7 +13,6 @@ import ( | |||
| 13 | 13 | "connectrpc.com/connect" | |
| 14 | 14 | "github.com/stretchr/testify/assert" | |
| 15 | 15 | "github.com/stretchr/testify/require" | |
| 16 | - | "google.golang.org/protobuf/types/known/fieldmaskpb" | |
| 17 | 16 | "google.golang.org/protobuf/types/known/timestamppb" | |
| 18 | 17 | ||
| 19 | 18 | pb "bnema.dev/ermite-api/gen/ermite" | |
| @@ -271,7 +270,7 @@ func TestCIPushRunsAWorkflowOnARunner(t *testing.T) { | |||
| 271 | 270 | } | |
| 272 | 271 | ||
| 273 | 272 | // AC5: merge refused while checks are pending or failing, allowed when they | |
| 274 | - | // pass, and when failing_checks_block is off. | |
| 273 | + | // pass, or when explicitly bypassed by an authorized caller. | |
| 275 | 274 | func TestCIMergeIsBlockedByChecks(t *testing.T) { | |
| 276 | 275 | c := newCIHarness(t, nil) | |
| 277 | 276 | runner := c.registerRunner("alpha", "ubuntu-latest") | |
| @@ -302,14 +301,9 @@ func TestCIMergeIsBlockedByChecks(t *testing.T) { | |||
| 302 | 301 | assert.Equal(t, connect.CodeFailedPrecondition, ciErrCode(err)) | |
| 303 | 302 | assert.Contains(t, err.Error(), "failing checks block the merge") | |
| 304 | 303 | ||
| 305 | - | // Off: the merge goes through. | |
| 306 | - | _, err = c.root.repo.UpdateRepo(c.ctx, connect.NewRequest(&pb.UpdateRepoRequest{ | |
| 307 | - | Repo: "proj", | |
| 308 | - | Settings: &pb.RepoSettings{FailingChecksBlock: false}, | |
| 309 | - | UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.failing_checks_block"}}, | |
| 310 | - | })) | |
| 304 | + | // An explicit override by root allows the merge despite failure. | |
| 305 | + | _, err = c.root.mr.MergeMergeRequest(c.ctx, connect.NewRequest(&pb.MergeMergeRequestRequest{Repo: "proj", Number: mr.Msg.GetMergeRequest().GetNumber(), HeadId: feat, IgnoreChecks: true})) | |
| 311 | 306 | require.NoError(t, err) | |
| 312 | - | require.NoError(t, merge()) | |
| 313 | 307 | } | |
| 314 | 308 | ||
| 315 | 309 | // AC2: a job token works for its job only, and not once the job has ended. | |
| @@ -13,7 +13,6 @@ import ( | |||||
| 13 | "connectrpc.com/connect" | 13 | "connectrpc.com/connect" | ||
| 14 | "github.com/stretchr/testify/assert" | 14 | "github.com/stretchr/testify/assert" | ||
| 15 | "github.com/stretchr/testify/require" | 15 | "github.com/stretchr/testify/require" | ||
| 16 | - | "google.golang.org/protobuf/types/known/fieldmaskpb" | |||
| 17 | "google.golang.org/protobuf/types/known/timestamppb" | 16 | "google.golang.org/protobuf/types/known/timestamppb" | ||
| 18 | 17 | ||||
| 19 | pb "bnema.dev/ermite-api/gen/ermite" | 18 | pb "bnema.dev/ermite-api/gen/ermite" | ||
| @@ -271,7 +270,7 @@ func TestCIPushRunsAWorkflowOnARunner(t *testing.T) { | |||||
| 271 | } | 270 | } | ||
| 272 | 271 | ||||
| 273 | // AC5: merge refused while checks are pending or failing, allowed when they | 272 | // AC5: merge refused while checks are pending or failing, allowed when they | ||
| 274 | - | // pass, and when failing_checks_block is off. | 273 | + | // pass, or when explicitly bypassed by an authorized caller. |
| 275 | func TestCIMergeIsBlockedByChecks(t *testing.T) { | 274 | func TestCIMergeIsBlockedByChecks(t *testing.T) { | ||
| 276 | c := newCIHarness(t, nil) | 275 | c := newCIHarness(t, nil) | ||
| 277 | runner := c.registerRunner("alpha", "ubuntu-latest") | 276 | runner := c.registerRunner("alpha", "ubuntu-latest") | ||
| @@ -302,14 +301,9 @@ func TestCIMergeIsBlockedByChecks(t *testing.T) { | |||||
| 302 | assert.Equal(t, connect.CodeFailedPrecondition, ciErrCode(err)) | 301 | assert.Equal(t, connect.CodeFailedPrecondition, ciErrCode(err)) | ||
| 303 | assert.Contains(t, err.Error(), "failing checks block the merge") | 302 | assert.Contains(t, err.Error(), "failing checks block the merge") | ||
| 304 | 303 | ||||
| 305 | - | // Off: the merge goes through. | 304 | + | // An explicit override by root allows the merge despite failure. |
| 306 | - | _, err = c.root.repo.UpdateRepo(c.ctx, connect.NewRequest(&pb.UpdateRepoRequest{ | 305 | + | _, err = c.root.mr.MergeMergeRequest(c.ctx, connect.NewRequest(&pb.MergeMergeRequestRequest{Repo: "proj", Number: mr.Msg.GetMergeRequest().GetNumber(), HeadId: feat, IgnoreChecks: true})) |
| 307 | - | Repo: "proj", | |||
| 308 | - | Settings: &pb.RepoSettings{FailingChecksBlock: false}, | |||
| 309 | - | UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.failing_checks_block"}}, | |||
| 310 | - | })) | |||
| 311 | require.NoError(t, err) | 306 | require.NoError(t, err) | ||
| 312 | - | require.NoError(t, merge()) | |||
| 313 | } | 307 | } | ||
| 314 | 308 | ||||
| 315 | // AC2: a job token works for its job only, and not once the job has ended. | 309 | // AC2: a job token works for its job only, and not once the job has ended. | ||
M internal/app/cli_e2e_test.go +2 -2
| @@ -203,7 +203,7 @@ type eventOut struct { | |||
| 203 | 203 | MergeRequest *mrOut `json:"merge_request"` | |
| 204 | 204 | } | |
| 205 | 205 | ||
| 206 | - | var repoKeys = []string{"allow_auto_merge", "created_at", "default_branch", "description", "failing_checks_block", "issues", "merge_modes", "name", "pushed_at", "topics", "visibility"} | |
| 206 | + | var repoKeys = []string{"allow_auto_merge", "created_at", "default_branch", "description", "issues", "merge_modes", "name", "pushed_at", "topics", "visibility"} | |
| 207 | 207 | ||
| 208 | 208 | // AC6: the CLI end to end against a real forge. | |
| 209 | 209 | func TestAC6CLIEndToEnd(t *testing.T) { | |
| @@ -350,7 +350,7 @@ func TestAC6CLIEndToEnd(t *testing.T) { | |||
| 350 | 350 | ver := decode[versionOut](t, res.stdout) | |
| 351 | 351 | assert.Equal(t, c.url, ver.Server.URL) | |
| 352 | 352 | assert.NotEmpty(t, ver.Server.Version) | |
| 353 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge"}, ver.Server.Features) | |
| 353 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks"}, ver.Server.Features) | |
| 354 | 354 | assert.Equal(t, []string{"passkeys"}, ver.Server.MissingFeatures) | |
| 355 | 355 | stdout, stderr, code = c.bin(t, secret, "version") | |
| 356 | 356 | require.Equal(t, 0, code, stderr) | |
| @@ -203,7 +203,7 @@ type eventOut struct { | |||||
| 203 | MergeRequest *mrOut `json:"merge_request"` | 203 | MergeRequest *mrOut `json:"merge_request"` | ||
| 204 | } | 204 | } | ||
| 205 | 205 | ||||
| 206 | - | var repoKeys = []string{"allow_auto_merge", "created_at", "default_branch", "description", "failing_checks_block", "issues", "merge_modes", "name", "pushed_at", "topics", "visibility"} | 206 | + | var repoKeys = []string{"allow_auto_merge", "created_at", "default_branch", "description", "issues", "merge_modes", "name", "pushed_at", "topics", "visibility"} |
| 207 | 207 | ||||
| 208 | // AC6: the CLI end to end against a real forge. | 208 | // AC6: the CLI end to end against a real forge. | ||
| 209 | func TestAC6CLIEndToEnd(t *testing.T) { | 209 | func TestAC6CLIEndToEnd(t *testing.T) { | ||
| @@ -350,7 +350,7 @@ func TestAC6CLIEndToEnd(t *testing.T) { | |||||
| 350 | ver := decode[versionOut](t, res.stdout) | 350 | ver := decode[versionOut](t, res.stdout) | ||
| 351 | assert.Equal(t, c.url, ver.Server.URL) | 351 | assert.Equal(t, c.url, ver.Server.URL) | ||
| 352 | assert.NotEmpty(t, ver.Server.Version) | 352 | assert.NotEmpty(t, ver.Server.Version) | ||
| 353 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge"}, ver.Server.Features) | 353 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks"}, ver.Server.Features) |
| 354 | assert.Equal(t, []string{"passkeys"}, ver.Server.MissingFeatures) | 354 | assert.Equal(t, []string{"passkeys"}, ver.Server.MissingFeatures) | ||
| 355 | stdout, stderr, code = c.bin(t, secret, "version") | 355 | stdout, stderr, code = c.bin(t, secret, "version") | ||
| 356 | require.Equal(t, 0, code, stderr) | 356 | require.Equal(t, 0, code, stderr) | ||
M internal/app/cli_issue_test.go +1 -1
| @@ -202,7 +202,7 @@ func TestRepoShowIssues(t *testing.T) { | |||
| 202 | 202 | }} | |
| 203 | 203 | url := f.serve(t) | |
| 204 | 204 | res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t) | |
| 205 | - | assert.Contains(t, res.stdout, "issues owner\n") | |
| 205 | + | assert.Contains(t, res.stdout, "issues owner\n") | |
| 206 | 206 | res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t) | |
| 207 | 207 | assert.Contains(t, res.stdout, `"issues": "owner"`) | |
| 208 | 208 | } | |
| @@ -202,7 +202,7 @@ func TestRepoShowIssues(t *testing.T) { | |||||
| 202 | }} | 202 | }} | ||
| 203 | url := f.serve(t) | 203 | url := f.serve(t) | ||
| 204 | res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t) | 204 | res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t) | ||
| 205 | - | assert.Contains(t, res.stdout, "issues owner\n") | 205 | + | assert.Contains(t, res.stdout, "issues owner\n") |
| 206 | res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t) | 206 | res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t) | ||
| 207 | assert.Contains(t, res.stdout, `"issues": "owner"`) | 207 | assert.Contains(t, res.stdout, `"issues": "owner"`) | ||
| 208 | } | 208 | } | ||
M internal/app/cli_mr_e2e_test.go +2 -14
| @@ -382,29 +382,17 @@ func TestRepoEditMergeSettingsCLI(t *testing.T) { | |||
| 382 | 382 | c.seed("proj", domain.VisibilityPublic) | |
| 383 | 383 | res := c.cli(t, adminToken, "repo", "show", "proj", "--json").ok(t) | |
| 384 | 384 | repo := decode[struct { | |
| 385 | - | MergeModes []string `json:"merge_modes"` | |
| 386 | - | FailingChecksBlock bool `json:"failing_checks_block"` | |
| 385 | + | MergeModes []string `json:"merge_modes"` | |
| 387 | 386 | }](t, res.stdout) | |
| 388 | 387 | assert.Equal(t, []string{"fast-forward", "squash", "merge"}, repo.MergeModes) | |
| 389 | - | assert.True(t, repo.FailingChecksBlock) | |
| 390 | 388 | ||
| 391 | 389 | res = c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "squash", "--json").ok(t) | |
| 392 | 390 | repo = decode[struct { | |
| 393 | - | MergeModes []string `json:"merge_modes"` | |
| 394 | - | FailingChecksBlock bool `json:"failing_checks_block"` | |
| 391 | + | MergeModes []string `json:"merge_modes"` | |
| 395 | 392 | }](t, res.stdout) | |
| 396 | 393 | assert.Equal(t, []string{"squash"}, repo.MergeModes) | |
| 397 | - | assert.True(t, repo.FailingChecksBlock, "untouched") | |
| 398 | - | res = c.cli(t, adminToken, "repo", "edit", "proj", "--failing-checks-block=false", "--json").ok(t) | |
| 399 | - | repo = decode[struct { | |
| 400 | - | MergeModes []string `json:"merge_modes"` | |
| 401 | - | FailingChecksBlock bool `json:"failing_checks_block"` | |
| 402 | - | }](t, res.stdout) | |
| 403 | - | assert.Equal(t, []string{"squash"}, repo.MergeModes, "untouched") | |
| 404 | - | assert.False(t, repo.FailingChecksBlock) | |
| 405 | 394 | res = c.cli(t, adminToken, "repo", "show", "proj").ok(t) | |
| 406 | 395 | assert.Regexp(t, `(?m)^merge modes\s+squash$`, res.stdout) | |
| 407 | - | assert.Regexp(t, `(?m)^failing checks block\s+false$`, res.stdout) | |
| 408 | 396 | ||
| 409 | 397 | require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "rebase").err) | |
| 410 | 398 | require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "").err) | |
| @@ -382,29 +382,17 @@ func TestRepoEditMergeSettingsCLI(t *testing.T) { | |||||
| 382 | c.seed("proj", domain.VisibilityPublic) | 382 | c.seed("proj", domain.VisibilityPublic) | ||
| 383 | res := c.cli(t, adminToken, "repo", "show", "proj", "--json").ok(t) | 383 | res := c.cli(t, adminToken, "repo", "show", "proj", "--json").ok(t) | ||
| 384 | repo := decode[struct { | 384 | repo := decode[struct { | ||
| 385 | - | MergeModes []string `json:"merge_modes"` | 385 | + | MergeModes []string `json:"merge_modes"` |
| 386 | - | FailingChecksBlock bool `json:"failing_checks_block"` | |||
| 387 | }](t, res.stdout) | 386 | }](t, res.stdout) | ||
| 388 | assert.Equal(t, []string{"fast-forward", "squash", "merge"}, repo.MergeModes) | 387 | assert.Equal(t, []string{"fast-forward", "squash", "merge"}, repo.MergeModes) | ||
| 389 | - | assert.True(t, repo.FailingChecksBlock) | |||
| 390 | 388 | ||||
| 391 | res = c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "squash", "--json").ok(t) | 389 | res = c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "squash", "--json").ok(t) | ||
| 392 | repo = decode[struct { | 390 | repo = decode[struct { | ||
| 393 | - | MergeModes []string `json:"merge_modes"` | 391 | + | MergeModes []string `json:"merge_modes"` |
| 394 | - | FailingChecksBlock bool `json:"failing_checks_block"` | |||
| 395 | }](t, res.stdout) | 392 | }](t, res.stdout) | ||
| 396 | assert.Equal(t, []string{"squash"}, repo.MergeModes) | 393 | assert.Equal(t, []string{"squash"}, repo.MergeModes) | ||
| 397 | - | assert.True(t, repo.FailingChecksBlock, "untouched") | |||
| 398 | - | res = c.cli(t, adminToken, "repo", "edit", "proj", "--failing-checks-block=false", "--json").ok(t) | |||
| 399 | - | repo = decode[struct { | |||
| 400 | - | MergeModes []string `json:"merge_modes"` | |||
| 401 | - | FailingChecksBlock bool `json:"failing_checks_block"` | |||
| 402 | - | }](t, res.stdout) | |||
| 403 | - | assert.Equal(t, []string{"squash"}, repo.MergeModes, "untouched") | |||
| 404 | - | assert.False(t, repo.FailingChecksBlock) | |||
| 405 | res = c.cli(t, adminToken, "repo", "show", "proj").ok(t) | 394 | res = c.cli(t, adminToken, "repo", "show", "proj").ok(t) | ||
| 406 | assert.Regexp(t, `(?m)^merge modes\s+squash$`, res.stdout) | 395 | assert.Regexp(t, `(?m)^merge modes\s+squash$`, res.stdout) | ||
| 407 | - | assert.Regexp(t, `(?m)^failing checks block\s+false$`, res.stdout) | |||
| 408 | 396 | ||||
| 409 | require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "rebase").err) | 397 | require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "rebase").err) | ||
| 410 | require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "").err) | 398 | require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "").err) | ||
M internal/app/cli_repo.go +17 -29
| @@ -34,9 +34,8 @@ type jsonRepo struct { | |||
| 34 | 34 | CreatedAt *string `json:"created_at"` | |
| 35 | 35 | PushedAt *string `json:"pushed_at"` | |
| 36 | 36 | // MergeModes are the allowed merge modes ("fast-forward", "squash", "merge"). | |
| 37 | - | MergeModes []string `json:"merge_modes"` | |
| 38 | - | FailingChecksBlock bool `json:"failing_checks_block"` | |
| 39 | - | AllowAutoMerge bool `json:"allow_auto_merge"` | |
| 37 | + | MergeModes []string `json:"merge_modes"` | |
| 38 | + | AllowAutoMerge bool `json:"allow_auto_merge"` | |
| 40 | 39 | // Issues is the issue mode ("off", "owner" or "public"). | |
| 41 | 40 | Issues string `json:"issues"` | |
| 42 | 41 | } | |
| @@ -47,17 +46,16 @@ func toJSONRepo(r *ermite.Repo) jsonRepo { | |||
| 47 | 46 | modes = append(modes, modeName(m)) | |
| 48 | 47 | } | |
| 49 | 48 | return jsonRepo{ | |
| 50 | - | MergeModes: modes, | |
| 51 | - | FailingChecksBlock: r.GetSettings().GetFailingChecksBlock(), | |
| 52 | - | AllowAutoMerge: r.GetSettings().GetAllowAutoMerge(), | |
| 53 | - | Issues: issuesModeName(r.GetSettings().GetIssues()), | |
| 54 | - | Name: r.GetName(), | |
| 55 | - | Description: r.GetDescription(), | |
| 56 | - | Topics: append([]string{}, r.GetTopics()...), | |
| 57 | - | Visibility: visibilityName(r.GetVisibility()), | |
| 58 | - | DefaultBranch: r.GetDefaultBranch(), | |
| 59 | - | CreatedAt: jsonTime(r.GetCreatedAt()), | |
| 60 | - | PushedAt: jsonTime(r.GetPushedAt()), | |
| 49 | + | MergeModes: modes, | |
| 50 | + | AllowAutoMerge: r.GetSettings().GetAllowAutoMerge(), | |
| 51 | + | Issues: issuesModeName(r.GetSettings().GetIssues()), | |
| 52 | + | Name: r.GetName(), | |
| 53 | + | Description: r.GetDescription(), | |
| 54 | + | Topics: append([]string{}, r.GetTopics()...), | |
| 55 | + | Visibility: visibilityName(r.GetVisibility()), | |
| 56 | + | DefaultBranch: r.GetDefaultBranch(), | |
| 57 | + | CreatedAt: jsonTime(r.GetCreatedAt()), | |
| 58 | + | PushedAt: jsonTime(r.GetPushedAt()), | |
| 61 | 59 | } | |
| 62 | 60 | } | |
| 63 | 61 | ||
| @@ -117,7 +115,7 @@ const jsonRepoHelp = ` | |||
| 117 | 115 | JSON fields of a repository (--json): name, description, visibility ("public" or | |
| 118 | 116 | "private"), default_branch, created_at and pushed_at (RFC 3339 UTC; pushed_at is | |
| 119 | 117 | null until the first push), merge_modes (array of the allowed merge modes, "fast-forward", "squash" and/or | |
| 120 | - | "merge"), failing_checks_block (whether a failing check prevents a merge), allow_auto_merge (whether | |
| 118 | + | "merge"), allow_auto_merge (whether | |
| 121 | 119 | auto-merge can be enabled) and issues (the issue mode: "off", "owner" or "public").` | |
| 122 | 120 | ||
| 123 | 121 | func newRepoCmd(env *cliEnv) *cobra.Command { | |
| @@ -230,7 +228,6 @@ func printRepo(w io.Writer, r *ermite.Repo) error { | |||
| 230 | 228 | {"created", shortTime(r.GetCreatedAt(), "-")}, | |
| 231 | 229 | {"last push", shortTime(r.GetPushedAt(), "never")}, | |
| 232 | 230 | {"merge modes", orDash(strings.Join(modes, ", "))}, | |
| 233 | - | {"failing checks block", strconv.FormatBool(r.GetSettings().GetFailingChecksBlock())}, | |
| 234 | 231 | {"allow auto-merge", strconv.FormatBool(r.GetSettings().GetAllowAutoMerge())}, | |
| 235 | 232 | {"issues", issuesModeName(r.GetSettings().GetIssues())}, | |
| 236 | 233 | }) | |
| @@ -311,7 +308,7 @@ func buildCreateRepo(name, description string, topics []string, visibility strin | |||
| 311 | 308 | func newRepoEditCmd(env *cliEnv) *cobra.Command { | |
| 312 | 309 | var description, visibility, defaultBranch string | |
| 313 | 310 | var topics, mergeModes []string | |
| 314 | - | var failingChecksBlock, allowAutoMerge bool | |
| 311 | + | var allowAutoMerge bool | |
| 315 | 312 | var issues string | |
| 316 | 313 | cmd := &cobra.Command{ | |
| 317 | 314 | Use: "edit <name>", | |
| @@ -321,7 +318,6 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command { | |||
| 321 | 318 | "may be merged with (fast-forward, squash, merge; every mode is allowed by default, so to disable one,\n" + | |
| 322 | 319 | "list the others: --merge-modes fast-forward,squash disables the merge commit; squash and merge\n" + | |
| 323 | 320 | "commits are signed with the server's key, see `ermite signing-key show`), and\n" + | |
| 324 | - | "--failing-checks-block says whether a failing check prevents a merge (a failed or unfinished CI check of the head; see README, CI).\n" + | |
| 325 | 321 | "--issues sets who may use issues: off (default), owner (the owner and tokens with issue:write) or\n" + | |
| 326 | 322 | "public (visitors of a public repository may also propose issues through a moderated form).\nNeeds a server with the issues feature.\n" + | |
| 327 | 323 | "Needs the repo:admin scope." + jsonRepoHelp, | |
| @@ -333,7 +329,6 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command { | |||
| 333 | 329 | cmd.Flags().StringVar(&visibility, "visibility", "", "new visibility: public or private") | |
| 334 | 330 | cmd.Flags().StringVar(&defaultBranch, "default-branch", "", "short name of the new default branch") | |
| 335 | 331 | cmd.Flags().StringSliceVar(&mergeModes, "merge-modes", nil, "allowed merge modes, comma-separated: fast-forward, squash, merge (all by default; list the ones to keep)") | |
| 336 | - | cmd.Flags().BoolVar(&failingChecksBlock, "failing-checks-block", true, "a failing check prevents a merge (--failing-checks-block=false to allow it)") | |
| 337 | 332 | cmd.Flags().BoolVar(&allowAutoMerge, "allow-auto-merge", false, "allow auto-merge on merge requests (--allow-auto-merge=false to forbid it)") | |
| 338 | 333 | cmd.Flags().StringVar(&issues, "issues", "", "issue mode: off, owner or public") | |
| 339 | 334 | _ = cmd.RegisterFlagCompletionFunc("issues", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { | |
| @@ -345,7 +340,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command { | |||
| 345 | 340 | }) | |
| 346 | 341 | asJSON := addJSONFlag(cmd) | |
| 347 | 342 | cmd.RunE = func(cmd *cobra.Command, args []string) error { | |
| 348 | - | req, err := buildUpdateRepo(cmd, args[0], description, topics, visibility, defaultBranch, mergeModes, failingChecksBlock, allowAutoMerge, issues) | |
| 343 | + | req, err := buildUpdateRepo(cmd, args[0], description, topics, visibility, defaultBranch, mergeModes, allowAutoMerge, issues) | |
| 349 | 344 | if err != nil { | |
| 350 | 345 | return err | |
| 351 | 346 | } | |
| @@ -369,7 +364,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command { | |||
| 369 | 364 | ||
| 370 | 365 | // buildUpdateRepo maps the flags of `repo edit` to the request: only the flags | |
| 371 | 366 | // the user passed go into the update mask. | |
| 372 | - | func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []string, visibility, defaultBranch string, mergeModes []string, failingChecksBlock, allowAutoMerge bool, issues string) (*ermite.UpdateRepoRequest, error) { | |
| 367 | + | func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []string, visibility, defaultBranch string, mergeModes []string, allowAutoMerge bool, issues string) (*ermite.UpdateRepoRequest, error) { | |
| 373 | 368 | req := &ermite.UpdateRepoRequest{Repo: name} | |
| 374 | 369 | var paths []string | |
| 375 | 370 | if cmd.Flags().Changed("description") { | |
| @@ -411,13 +406,6 @@ func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []stri | |||
| 411 | 406 | req.Settings = &ermite.RepoSettings{AllowedMergeModes: modes} | |
| 412 | 407 | paths = append(paths, "settings.allowed_merge_modes") | |
| 413 | 408 | } | |
| 414 | - | if cmd.Flags().Changed("failing-checks-block") { | |
| 415 | - | if req.Settings == nil { | |
| 416 | - | req.Settings = &ermite.RepoSettings{} | |
| 417 | - | } | |
| 418 | - | req.Settings.FailingChecksBlock = failingChecksBlock | |
| 419 | - | paths = append(paths, "settings.failing_checks_block") | |
| 420 | - | } | |
| 421 | 409 | if cmd.Flags().Changed("allow-auto-merge") { | |
| 422 | 410 | if req.Settings == nil { | |
| 423 | 411 | req.Settings = &ermite.RepoSettings{} | |
| @@ -434,7 +422,7 @@ func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []stri | |||
| 434 | 422 | req.Issues = &mode | |
| 435 | 423 | } | |
| 436 | 424 | if len(paths) == 0 && req.Issues == nil { | |
| 437 | - | return nil, errors.New("nothing to change: give at least one of --description, --topics, --visibility, --default-branch, --merge-modes, --failing-checks-block, --allow-auto-merge, --issues") | |
| 425 | + | return nil, errors.New("nothing to change: give at least one of --description, --topics, --visibility, --default-branch, --merge-modes, --allow-auto-merge, --issues") | |
| 438 | 426 | } | |
| 439 | 427 | req.UpdateMask = &fieldmaskpb.FieldMask{Paths: paths} | |
| 440 | 428 | return req, nil | |
| @@ -34,9 +34,8 @@ type jsonRepo struct { | |||||
| 34 | CreatedAt *string `json:"created_at"` | 34 | CreatedAt *string `json:"created_at"` | ||
| 35 | PushedAt *string `json:"pushed_at"` | 35 | PushedAt *string `json:"pushed_at"` | ||
| 36 | // MergeModes are the allowed merge modes ("fast-forward", "squash", "merge"). | 36 | // MergeModes are the allowed merge modes ("fast-forward", "squash", "merge"). | ||
| 37 | - | MergeModes []string `json:"merge_modes"` | 37 | + | MergeModes []string `json:"merge_modes"` |
| 38 | - | FailingChecksBlock bool `json:"failing_checks_block"` | 38 | + | AllowAutoMerge bool `json:"allow_auto_merge"` |
| 39 | - | AllowAutoMerge bool `json:"allow_auto_merge"` | |||
| 40 | // Issues is the issue mode ("off", "owner" or "public"). | 39 | // Issues is the issue mode ("off", "owner" or "public"). | ||
| 41 | Issues string `json:"issues"` | 40 | Issues string `json:"issues"` | ||
| 42 | } | 41 | } | ||
| @@ -47,17 +46,16 @@ func toJSONRepo(r *ermite.Repo) jsonRepo { | |||||
| 47 | modes = append(modes, modeName(m)) | 46 | modes = append(modes, modeName(m)) | ||
| 48 | } | 47 | } | ||
| 49 | return jsonRepo{ | 48 | return jsonRepo{ | ||
| 50 | - | MergeModes: modes, | 49 | + | MergeModes: modes, |
| 51 | - | FailingChecksBlock: r.GetSettings().GetFailingChecksBlock(), | 50 | + | AllowAutoMerge: r.GetSettings().GetAllowAutoMerge(), |
| 52 | - | AllowAutoMerge: r.GetSettings().GetAllowAutoMerge(), | 51 | + | Issues: issuesModeName(r.GetSettings().GetIssues()), |
| 53 | - | Issues: issuesModeName(r.GetSettings().GetIssues()), | 52 | + | Name: r.GetName(), |
| 54 | - | Name: r.GetName(), | 53 | + | Description: r.GetDescription(), |
| 55 | - | Description: r.GetDescription(), | 54 | + | Topics: append([]string{}, r.GetTopics()...), |
| 56 | - | Topics: append([]string{}, r.GetTopics()...), | 55 | + | Visibility: visibilityName(r.GetVisibility()), |
| 57 | - | Visibility: visibilityName(r.GetVisibility()), | 56 | + | DefaultBranch: r.GetDefaultBranch(), |
| 58 | - | DefaultBranch: r.GetDefaultBranch(), | 57 | + | CreatedAt: jsonTime(r.GetCreatedAt()), |
| 59 | - | CreatedAt: jsonTime(r.GetCreatedAt()), | 58 | + | PushedAt: jsonTime(r.GetPushedAt()), |
| 60 | - | PushedAt: jsonTime(r.GetPushedAt()), | |||
| 61 | } | 59 | } | ||
| 62 | } | 60 | } | ||
| 63 | 61 | ||||
| @@ -117,7 +115,7 @@ const jsonRepoHelp = ` | |||||
| 117 | JSON fields of a repository (--json): name, description, visibility ("public" or | 115 | JSON fields of a repository (--json): name, description, visibility ("public" or | ||
| 118 | "private"), default_branch, created_at and pushed_at (RFC 3339 UTC; pushed_at is | 116 | "private"), default_branch, created_at and pushed_at (RFC 3339 UTC; pushed_at is | ||
| 119 | null until the first push), merge_modes (array of the allowed merge modes, "fast-forward", "squash" and/or | 117 | null until the first push), merge_modes (array of the allowed merge modes, "fast-forward", "squash" and/or | ||
| 120 | - | "merge"), failing_checks_block (whether a failing check prevents a merge), allow_auto_merge (whether | 118 | + | "merge"), allow_auto_merge (whether |
| 121 | auto-merge can be enabled) and issues (the issue mode: "off", "owner" or "public").` | 119 | auto-merge can be enabled) and issues (the issue mode: "off", "owner" or "public").` | ||
| 122 | 120 | ||||
| 123 | func newRepoCmd(env *cliEnv) *cobra.Command { | 121 | func newRepoCmd(env *cliEnv) *cobra.Command { | ||
| @@ -230,7 +228,6 @@ func printRepo(w io.Writer, r *ermite.Repo) error { | |||||
| 230 | {"created", shortTime(r.GetCreatedAt(), "-")}, | 228 | {"created", shortTime(r.GetCreatedAt(), "-")}, | ||
| 231 | {"last push", shortTime(r.GetPushedAt(), "never")}, | 229 | {"last push", shortTime(r.GetPushedAt(), "never")}, | ||
| 232 | {"merge modes", orDash(strings.Join(modes, ", "))}, | 230 | {"merge modes", orDash(strings.Join(modes, ", "))}, | ||
| 233 | - | {"failing checks block", strconv.FormatBool(r.GetSettings().GetFailingChecksBlock())}, | |||
| 234 | {"allow auto-merge", strconv.FormatBool(r.GetSettings().GetAllowAutoMerge())}, | 231 | {"allow auto-merge", strconv.FormatBool(r.GetSettings().GetAllowAutoMerge())}, | ||
| 235 | {"issues", issuesModeName(r.GetSettings().GetIssues())}, | 232 | {"issues", issuesModeName(r.GetSettings().GetIssues())}, | ||
| 236 | }) | 233 | }) | ||
| @@ -311,7 +308,7 @@ func buildCreateRepo(name, description string, topics []string, visibility strin | |||||
| 311 | func newRepoEditCmd(env *cliEnv) *cobra.Command { | 308 | func newRepoEditCmd(env *cliEnv) *cobra.Command { | ||
| 312 | var description, visibility, defaultBranch string | 309 | var description, visibility, defaultBranch string | ||
| 313 | var topics, mergeModes []string | 310 | var topics, mergeModes []string | ||
| 314 | - | var failingChecksBlock, allowAutoMerge bool | 311 | + | var allowAutoMerge bool |
| 315 | var issues string | 312 | var issues string | ||
| 316 | cmd := &cobra.Command{ | 313 | cmd := &cobra.Command{ | ||
| 317 | Use: "edit <name>", | 314 | Use: "edit <name>", | ||
| @@ -321,7 +318,6 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command { | |||||
| 321 | "may be merged with (fast-forward, squash, merge; every mode is allowed by default, so to disable one,\n" + | 318 | "may be merged with (fast-forward, squash, merge; every mode is allowed by default, so to disable one,\n" + | ||
| 322 | "list the others: --merge-modes fast-forward,squash disables the merge commit; squash and merge\n" + | 319 | "list the others: --merge-modes fast-forward,squash disables the merge commit; squash and merge\n" + | ||
| 323 | "commits are signed with the server's key, see `ermite signing-key show`), and\n" + | 320 | "commits are signed with the server's key, see `ermite signing-key show`), and\n" + | ||
| 324 | - | "--failing-checks-block says whether a failing check prevents a merge (a failed or unfinished CI check of the head; see README, CI).\n" + | |||
| 325 | "--issues sets who may use issues: off (default), owner (the owner and tokens with issue:write) or\n" + | 321 | "--issues sets who may use issues: off (default), owner (the owner and tokens with issue:write) or\n" + | ||
| 326 | "public (visitors of a public repository may also propose issues through a moderated form).\nNeeds a server with the issues feature.\n" + | 322 | "public (visitors of a public repository may also propose issues through a moderated form).\nNeeds a server with the issues feature.\n" + | ||
| 327 | "Needs the repo:admin scope." + jsonRepoHelp, | 323 | "Needs the repo:admin scope." + jsonRepoHelp, | ||
| @@ -333,7 +329,6 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command { | |||||
| 333 | cmd.Flags().StringVar(&visibility, "visibility", "", "new visibility: public or private") | 329 | cmd.Flags().StringVar(&visibility, "visibility", "", "new visibility: public or private") | ||
| 334 | cmd.Flags().StringVar(&defaultBranch, "default-branch", "", "short name of the new default branch") | 330 | cmd.Flags().StringVar(&defaultBranch, "default-branch", "", "short name of the new default branch") | ||
| 335 | cmd.Flags().StringSliceVar(&mergeModes, "merge-modes", nil, "allowed merge modes, comma-separated: fast-forward, squash, merge (all by default; list the ones to keep)") | 331 | cmd.Flags().StringSliceVar(&mergeModes, "merge-modes", nil, "allowed merge modes, comma-separated: fast-forward, squash, merge (all by default; list the ones to keep)") | ||
| 336 | - | cmd.Flags().BoolVar(&failingChecksBlock, "failing-checks-block", true, "a failing check prevents a merge (--failing-checks-block=false to allow it)") | |||
| 337 | cmd.Flags().BoolVar(&allowAutoMerge, "allow-auto-merge", false, "allow auto-merge on merge requests (--allow-auto-merge=false to forbid it)") | 332 | cmd.Flags().BoolVar(&allowAutoMerge, "allow-auto-merge", false, "allow auto-merge on merge requests (--allow-auto-merge=false to forbid it)") | ||
| 338 | cmd.Flags().StringVar(&issues, "issues", "", "issue mode: off, owner or public") | 333 | cmd.Flags().StringVar(&issues, "issues", "", "issue mode: off, owner or public") | ||
| 339 | _ = cmd.RegisterFlagCompletionFunc("issues", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { | 334 | _ = cmd.RegisterFlagCompletionFunc("issues", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { | ||
| @@ -345,7 +340,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command { | |||||
| 345 | }) | 340 | }) | ||
| 346 | asJSON := addJSONFlag(cmd) | 341 | asJSON := addJSONFlag(cmd) | ||
| 347 | cmd.RunE = func(cmd *cobra.Command, args []string) error { | 342 | cmd.RunE = func(cmd *cobra.Command, args []string) error { | ||
| 348 | - | req, err := buildUpdateRepo(cmd, args[0], description, topics, visibility, defaultBranch, mergeModes, failingChecksBlock, allowAutoMerge, issues) | 343 | + | req, err := buildUpdateRepo(cmd, args[0], description, topics, visibility, defaultBranch, mergeModes, allowAutoMerge, issues) |
| 349 | if err != nil { | 344 | if err != nil { | ||
| 350 | return err | 345 | return err | ||
| 351 | } | 346 | } | ||
| @@ -369,7 +364,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command { | |||||
| 369 | 364 | ||||
| 370 | // buildUpdateRepo maps the flags of `repo edit` to the request: only the flags | 365 | // buildUpdateRepo maps the flags of `repo edit` to the request: only the flags | ||
| 371 | // the user passed go into the update mask. | 366 | // the user passed go into the update mask. | ||
| 372 | - | func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []string, visibility, defaultBranch string, mergeModes []string, failingChecksBlock, allowAutoMerge bool, issues string) (*ermite.UpdateRepoRequest, error) { | 367 | + | func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []string, visibility, defaultBranch string, mergeModes []string, allowAutoMerge bool, issues string) (*ermite.UpdateRepoRequest, error) { |
| 373 | req := &ermite.UpdateRepoRequest{Repo: name} | 368 | req := &ermite.UpdateRepoRequest{Repo: name} | ||
| 374 | var paths []string | 369 | var paths []string | ||
| 375 | if cmd.Flags().Changed("description") { | 370 | if cmd.Flags().Changed("description") { | ||
| @@ -411,13 +406,6 @@ func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []stri | |||||
| 411 | req.Settings = &ermite.RepoSettings{AllowedMergeModes: modes} | 406 | req.Settings = &ermite.RepoSettings{AllowedMergeModes: modes} | ||
| 412 | paths = append(paths, "settings.allowed_merge_modes") | 407 | paths = append(paths, "settings.allowed_merge_modes") | ||
| 413 | } | 408 | } | ||
| 414 | - | if cmd.Flags().Changed("failing-checks-block") { | |||
| 415 | - | if req.Settings == nil { | |||
| 416 | - | req.Settings = &ermite.RepoSettings{} | |||
| 417 | - | } | |||
| 418 | - | req.Settings.FailingChecksBlock = failingChecksBlock | |||
| 419 | - | paths = append(paths, "settings.failing_checks_block") | |||
| 420 | - | } | |||
| 421 | if cmd.Flags().Changed("allow-auto-merge") { | 409 | if cmd.Flags().Changed("allow-auto-merge") { | ||
| 422 | if req.Settings == nil { | 410 | if req.Settings == nil { | ||
| 423 | req.Settings = &ermite.RepoSettings{} | 411 | req.Settings = &ermite.RepoSettings{} | ||
| @@ -434,7 +422,7 @@ func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []stri | |||||
| 434 | req.Issues = &mode | 422 | req.Issues = &mode | ||
| 435 | } | 423 | } | ||
| 436 | if len(paths) == 0 && req.Issues == nil { | 424 | if len(paths) == 0 && req.Issues == nil { | ||
| 437 | - | return nil, errors.New("nothing to change: give at least one of --description, --topics, --visibility, --default-branch, --merge-modes, --failing-checks-block, --allow-auto-merge, --issues") | 425 | + | return nil, errors.New("nothing to change: give at least one of --description, --topics, --visibility, --default-branch, --merge-modes, --allow-auto-merge, --issues") |
| 438 | } | 426 | } | ||
| 439 | req.UpdateMask = &fieldmaskpb.FieldMask{Paths: paths} | 427 | req.UpdateMask = &fieldmaskpb.FieldMask{Paths: paths} | ||
| 440 | return req, nil | 428 | return req, nil | ||
M internal/app/cli_repo_test.go +17 -27
| @@ -19,8 +19,7 @@ var ( | |||
| 19 | 19 | Name: "alpha", Description: "The first one", Topics: []string{"cli", "go"}, Visibility: ermite.Visibility_VISIBILITY_PUBLIC, | |
| 20 | 20 | DefaultBranch: "main", CreatedAt: ts("2026-01-02T03:04:05Z"), PushedAt: ts("2026-03-04T05:06:07Z"), | |
| 21 | 21 | Settings: &ermite.RepoSettings{ | |
| 22 | - | AllowedMergeModes: []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_FAST_FORWARD, ermite.MergeMode_MERGE_MODE_SQUASH}, | |
| 23 | - | FailingChecksBlock: true, | |
| 22 | + | AllowedMergeModes: []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_FAST_FORWARD, ermite.MergeMode_MERGE_MODE_SQUASH}, | |
| 24 | 23 | }, | |
| 25 | 24 | } | |
| 26 | 25 | repoBeta = &ermite.Repo{ | |
| @@ -54,8 +53,8 @@ func TestRepoList(t *testing.T) { | |||
| 54 | 53 | { | |
| 55 | 54 | name: "json", repos: both, args: []string{"repo", "list", "--json"}, wantJSON: true, | |
| 56 | 55 | wantStdout: `[ | |
| 57 | - | {"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"failing_checks_block":true,"allow_auto_merge":false,"issues":"off"}, | |
| 58 | - | {"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"} | |
| 56 | + | {"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"allow_auto_merge":false,"issues":"off"}, | |
| 57 | + | {"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"} | |
| 59 | 58 | ]`, | |
| 60 | 59 | }, | |
| 61 | 60 | { | |
| @@ -107,22 +106,21 @@ func TestRepoShow(t *testing.T) { | |||
| 107 | 106 | url := f.serve(t) | |
| 108 | 107 | res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t) | |
| 109 | 108 | assert.Equal(t, ""+ | |
| 110 | - | "name alpha\n"+ | |
| 111 | - | "visibility public\n"+ | |
| 112 | - | "default branch main\n"+ | |
| 113 | - | "description The first one\n"+ | |
| 114 | - | "topics cli, go\n"+ | |
| 115 | - | "created 2026-01-02 03:04\n"+ | |
| 116 | - | "last push 2026-03-04 05:06\n"+ | |
| 117 | - | "merge modes fast-forward, squash\n"+ | |
| 118 | - | "failing checks block true\n"+ | |
| 119 | - | "allow auto-merge false\n"+ | |
| 120 | - | "issues off\n", res.stdout) | |
| 109 | + | "name alpha\n"+ | |
| 110 | + | "visibility public\n"+ | |
| 111 | + | "default branch main\n"+ | |
| 112 | + | "description The first one\n"+ | |
| 113 | + | "topics cli, go\n"+ | |
| 114 | + | "created 2026-01-02 03:04\n"+ | |
| 115 | + | "last push 2026-03-04 05:06\n"+ | |
| 116 | + | "merge modes fast-forward, squash\n"+ | |
| 117 | + | "allow auto-merge false\n"+ | |
| 118 | + | "issues off\n", res.stdout) | |
| 121 | 119 | _, reqs := f.snapshot() | |
| 122 | 120 | assert.Equal(t, "alpha", reqs[0].(*ermite.GetRepoRequest).GetRepo()) | |
| 123 | 121 | ||
| 124 | 122 | res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t) | |
| 125 | - | assert.JSONEq(t, `{"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"failing_checks_block":true,"allow_auto_merge":false,"issues":"off"}`, res.stdout) | |
| 123 | + | assert.JSONEq(t, `{"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"allow_auto_merge":false,"issues":"off"}`, res.stdout) | |
| 126 | 124 | } | |
| 127 | 125 | ||
| 128 | 126 | func TestRepoShowNeedsOneArg(t *testing.T) { | |
| @@ -178,7 +176,7 @@ func TestRepoCreateJSON(t *testing.T) { | |||
| 178 | 176 | return &ermite.CreateRepoResponse{Repo: repoBeta}, nil | |
| 179 | 177 | }} | |
| 180 | 178 | res := runCLI(t, envFor(f.serve(t)), "", "repo", "create", "beta", "--json").ok(t) | |
| 181 | - | assert.JSONEq(t, `{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"}`, res.stdout) | |
| 179 | + | assert.JSONEq(t, `{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"}`, res.stdout) | |
| 182 | 180 | } | |
| 183 | 181 | ||
| 184 | 182 | func TestRepoEditMask(t *testing.T) { | |
| @@ -209,16 +207,8 @@ func TestRepoEditMask(t *testing.T) { | |||
| 209 | 207 | {"merge modes", []string{"--merge-modes", "squash,ff"}, []string{"settings.allowed_merge_modes"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | |
| 210 | 208 | assert.Equal(t, []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_SQUASH, ermite.MergeMode_MERGE_MODE_FAST_FORWARD}, r.GetSettings().GetAllowedMergeModes()) | |
| 211 | 209 | }}, | |
| 212 | - | {"failing checks do not block", []string{"--failing-checks-block=false"}, []string{"settings.failing_checks_block"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | |
| 213 | - | assert.False(t, r.GetSettings().GetFailingChecksBlock()) | |
| 214 | - | assert.Empty(t, r.GetSettings().GetAllowedMergeModes()) | |
| 215 | - | }}, | |
| 216 | - | {"failing checks block", []string{"--failing-checks-block"}, []string{"settings.failing_checks_block"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | |
| 217 | - | assert.True(t, r.GetSettings().GetFailingChecksBlock()) | |
| 218 | - | }}, | |
| 219 | 210 | {"allow auto-merge", []string{"--allow-auto-merge"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | |
| 220 | 211 | assert.True(t, r.GetSettings().GetAllowAutoMerge()) | |
| 221 | - | assert.False(t, r.GetSettings().GetFailingChecksBlock()) | |
| 222 | 212 | }}, | |
| 223 | 213 | {"forbid auto-merge", []string{"--allow-auto-merge=false"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | |
| 224 | 214 | assert.False(t, r.GetSettings().GetAllowAutoMerge()) | |
| @@ -460,9 +450,9 @@ func TestRepoEditAllowAutoMerge(t *testing.T) { | |||
| 460 | 450 | assert.Equal(t, []string{"settings.allow_auto_merge"}, got.GetUpdateMask().GetPaths()) | |
| 461 | 451 | assert.True(t, got.GetSettings().GetAllowAutoMerge()) | |
| 462 | 452 | ||
| 463 | - | res = runCLI(t, envFor(url), "", "repo", "edit", "alpha", "--allow-auto-merge", "--failing-checks-block=false", "--json").ok(t) | |
| 453 | + | res = runCLI(t, envFor(url), "", "repo", "edit", "alpha", "--allow-auto-merge", "--json").ok(t) | |
| 464 | 454 | assert.Contains(t, res.stdout, `"allow_auto_merge": true`) | |
| 465 | 455 | _, reqs = f.snapshot() | |
| 466 | 456 | got = reqs[1].(*ermite.UpdateRepoRequest) | |
| 467 | - | assert.ElementsMatch(t, []string{"settings.allow_auto_merge", "settings.failing_checks_block"}, got.GetUpdateMask().GetPaths()) | |
| 457 | + | assert.ElementsMatch(t, []string{"settings.allow_auto_merge"}, got.GetUpdateMask().GetPaths()) | |
| 468 | 458 | } | |
| @@ -19,8 +19,7 @@ var ( | |||||
| 19 | Name: "alpha", Description: "The first one", Topics: []string{"cli", "go"}, Visibility: ermite.Visibility_VISIBILITY_PUBLIC, | 19 | Name: "alpha", Description: "The first one", Topics: []string{"cli", "go"}, Visibility: ermite.Visibility_VISIBILITY_PUBLIC, | ||
| 20 | DefaultBranch: "main", CreatedAt: ts("2026-01-02T03:04:05Z"), PushedAt: ts("2026-03-04T05:06:07Z"), | 20 | DefaultBranch: "main", CreatedAt: ts("2026-01-02T03:04:05Z"), PushedAt: ts("2026-03-04T05:06:07Z"), | ||
| 21 | Settings: &ermite.RepoSettings{ | 21 | Settings: &ermite.RepoSettings{ | ||
| 22 | - | AllowedMergeModes: []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_FAST_FORWARD, ermite.MergeMode_MERGE_MODE_SQUASH}, | 22 | + | AllowedMergeModes: []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_FAST_FORWARD, ermite.MergeMode_MERGE_MODE_SQUASH}, |
| 23 | - | FailingChecksBlock: true, | |||
| 24 | }, | 23 | }, | ||
| 25 | } | 24 | } | ||
| 26 | repoBeta = &ermite.Repo{ | 25 | repoBeta = &ermite.Repo{ | ||
| @@ -54,8 +53,8 @@ func TestRepoList(t *testing.T) { | |||||
| 54 | { | 53 | { | ||
| 55 | name: "json", repos: both, args: []string{"repo", "list", "--json"}, wantJSON: true, | 54 | name: "json", repos: both, args: []string{"repo", "list", "--json"}, wantJSON: true, | ||
| 56 | wantStdout: `[ | 55 | wantStdout: `[ | ||
| 57 | - | {"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"failing_checks_block":true,"allow_auto_merge":false,"issues":"off"}, | 56 | + | {"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"allow_auto_merge":false,"issues":"off"}, |
| 58 | - | {"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"} | 57 | + | {"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"} |
| 59 | ]`, | 58 | ]`, | ||
| 60 | }, | 59 | }, | ||
| 61 | { | 60 | { | ||
| @@ -107,22 +106,21 @@ func TestRepoShow(t *testing.T) { | |||||
| 107 | url := f.serve(t) | 106 | url := f.serve(t) | ||
| 108 | res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t) | 107 | res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t) | ||
| 109 | assert.Equal(t, ""+ | 108 | assert.Equal(t, ""+ | ||
| 110 | - | "name alpha\n"+ | 109 | + | "name alpha\n"+ |
| 111 | - | "visibility public\n"+ | 110 | + | "visibility public\n"+ |
| 112 | - | "default branch main\n"+ | 111 | + | "default branch main\n"+ |
| 113 | - | "description The first one\n"+ | 112 | + | "description The first one\n"+ |
| 114 | - | "topics cli, go\n"+ | 113 | + | "topics cli, go\n"+ |
| 115 | - | "created 2026-01-02 03:04\n"+ | 114 | + | "created 2026-01-02 03:04\n"+ |
| 116 | - | "last push 2026-03-04 05:06\n"+ | 115 | + | "last push 2026-03-04 05:06\n"+ |
| 117 | - | "merge modes fast-forward, squash\n"+ | 116 | + | "merge modes fast-forward, squash\n"+ |
| 118 | - | "failing checks block true\n"+ | 117 | + | "allow auto-merge false\n"+ |
| 119 | - | "allow auto-merge false\n"+ | 118 | + | "issues off\n", res.stdout) |
| 120 | - | "issues off\n", res.stdout) | |||
| 121 | _, reqs := f.snapshot() | 119 | _, reqs := f.snapshot() | ||
| 122 | assert.Equal(t, "alpha", reqs[0].(*ermite.GetRepoRequest).GetRepo()) | 120 | assert.Equal(t, "alpha", reqs[0].(*ermite.GetRepoRequest).GetRepo()) | ||
| 123 | 121 | ||||
| 124 | res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t) | 122 | res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t) | ||
| 125 | - | assert.JSONEq(t, `{"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"failing_checks_block":true,"allow_auto_merge":false,"issues":"off"}`, res.stdout) | 123 | + | assert.JSONEq(t, `{"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"allow_auto_merge":false,"issues":"off"}`, res.stdout) |
| 126 | } | 124 | } | ||
| 127 | 125 | ||||
| 128 | func TestRepoShowNeedsOneArg(t *testing.T) { | 126 | func TestRepoShowNeedsOneArg(t *testing.T) { | ||
| @@ -178,7 +176,7 @@ func TestRepoCreateJSON(t *testing.T) { | |||||
| 178 | return &ermite.CreateRepoResponse{Repo: repoBeta}, nil | 176 | return &ermite.CreateRepoResponse{Repo: repoBeta}, nil | ||
| 179 | }} | 177 | }} | ||
| 180 | res := runCLI(t, envFor(f.serve(t)), "", "repo", "create", "beta", "--json").ok(t) | 178 | res := runCLI(t, envFor(f.serve(t)), "", "repo", "create", "beta", "--json").ok(t) | ||
| 181 | - | assert.JSONEq(t, `{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"}`, res.stdout) | 179 | + | assert.JSONEq(t, `{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"}`, res.stdout) |
| 182 | } | 180 | } | ||
| 183 | 181 | ||||
| 184 | func TestRepoEditMask(t *testing.T) { | 182 | func TestRepoEditMask(t *testing.T) { | ||
| @@ -209,16 +207,8 @@ func TestRepoEditMask(t *testing.T) { | |||||
| 209 | {"merge modes", []string{"--merge-modes", "squash,ff"}, []string{"settings.allowed_merge_modes"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | 207 | {"merge modes", []string{"--merge-modes", "squash,ff"}, []string{"settings.allowed_merge_modes"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | ||
| 210 | assert.Equal(t, []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_SQUASH, ermite.MergeMode_MERGE_MODE_FAST_FORWARD}, r.GetSettings().GetAllowedMergeModes()) | 208 | assert.Equal(t, []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_SQUASH, ermite.MergeMode_MERGE_MODE_FAST_FORWARD}, r.GetSettings().GetAllowedMergeModes()) | ||
| 211 | }}, | 209 | }}, | ||
| 212 | - | {"failing checks do not block", []string{"--failing-checks-block=false"}, []string{"settings.failing_checks_block"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | |||
| 213 | - | assert.False(t, r.GetSettings().GetFailingChecksBlock()) | |||
| 214 | - | assert.Empty(t, r.GetSettings().GetAllowedMergeModes()) | |||
| 215 | - | }}, | |||
| 216 | - | {"failing checks block", []string{"--failing-checks-block"}, []string{"settings.failing_checks_block"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | |||
| 217 | - | assert.True(t, r.GetSettings().GetFailingChecksBlock()) | |||
| 218 | - | }}, | |||
| 219 | {"allow auto-merge", []string{"--allow-auto-merge"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | 210 | {"allow auto-merge", []string{"--allow-auto-merge"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | ||
| 220 | assert.True(t, r.GetSettings().GetAllowAutoMerge()) | 211 | assert.True(t, r.GetSettings().GetAllowAutoMerge()) | ||
| 221 | - | assert.False(t, r.GetSettings().GetFailingChecksBlock()) | |||
| 222 | }}, | 212 | }}, | ||
| 223 | {"forbid auto-merge", []string{"--allow-auto-merge=false"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | 213 | {"forbid auto-merge", []string{"--allow-auto-merge=false"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) { | ||
| 224 | assert.False(t, r.GetSettings().GetAllowAutoMerge()) | 214 | assert.False(t, r.GetSettings().GetAllowAutoMerge()) | ||
| @@ -460,9 +450,9 @@ func TestRepoEditAllowAutoMerge(t *testing.T) { | |||||
| 460 | assert.Equal(t, []string{"settings.allow_auto_merge"}, got.GetUpdateMask().GetPaths()) | 450 | assert.Equal(t, []string{"settings.allow_auto_merge"}, got.GetUpdateMask().GetPaths()) | ||
| 461 | assert.True(t, got.GetSettings().GetAllowAutoMerge()) | 451 | assert.True(t, got.GetSettings().GetAllowAutoMerge()) | ||
| 462 | 452 | ||||
| 463 | - | res = runCLI(t, envFor(url), "", "repo", "edit", "alpha", "--allow-auto-merge", "--failing-checks-block=false", "--json").ok(t) | 453 | + | res = runCLI(t, envFor(url), "", "repo", "edit", "alpha", "--allow-auto-merge", "--json").ok(t) |
| 464 | assert.Contains(t, res.stdout, `"allow_auto_merge": true`) | 454 | assert.Contains(t, res.stdout, `"allow_auto_merge": true`) | ||
| 465 | _, reqs = f.snapshot() | 455 | _, reqs = f.snapshot() | ||
| 466 | got = reqs[1].(*ermite.UpdateRepoRequest) | 456 | got = reqs[1].(*ermite.UpdateRepoRequest) | ||
| 467 | - | assert.ElementsMatch(t, []string{"settings.allow_auto_merge", "settings.failing_checks_block"}, got.GetUpdateMask().GetPaths()) | 457 | + | assert.ElementsMatch(t, []string{"settings.allow_auto_merge"}, got.GetUpdateMask().GetPaths()) |
| 468 | } | 458 | } | ||
M internal/app/cli_watch_test.go +1 -1
| @@ -95,7 +95,7 @@ func TestWatchNDJSON(t *testing.T) { | |||
| 95 | 95 | assert.True(t, json.Valid([]byte(sc.Text())), "one JSON value per line: %s", sc.Text()) | |
| 96 | 96 | } | |
| 97 | 97 | require.Len(t, lines, 5) | |
| 98 | - | assert.JSONEq(t, `{"id":"1","time":"2026-10-10T12:00:00Z","type":"repo_created","repo":"beta","repository":{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"}}`, lines[0]) | |
| 98 | + | assert.JSONEq(t, `{"id":"1","time":"2026-10-10T12:00:00Z","type":"repo_created","repo":"beta","repository":{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"}}`, lines[0]) | |
| 99 | 99 | assert.JSONEq(t, `{"id":"3","time":"2026-10-10T12:00:00Z","type":"pushed","repo":"alpha","ref_updates":[ | |
| 100 | 100 | {"ref":"refs/heads/main","old_id":"1111111111111111","new_id":"2222222222222222"}, | |
| 101 | 101 | {"ref":"refs/heads/new","old_id":"","new_id":"3333333333333333"}, | |
| @@ -95,7 +95,7 @@ func TestWatchNDJSON(t *testing.T) { | |||||
| 95 | assert.True(t, json.Valid([]byte(sc.Text())), "one JSON value per line: %s", sc.Text()) | 95 | assert.True(t, json.Valid([]byte(sc.Text())), "one JSON value per line: %s", sc.Text()) | ||
| 96 | } | 96 | } | ||
| 97 | require.Len(t, lines, 5) | 97 | require.Len(t, lines, 5) | ||
| 98 | - | assert.JSONEq(t, `{"id":"1","time":"2026-10-10T12:00:00Z","type":"repo_created","repo":"beta","repository":{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"}}`, lines[0]) | 98 | + | assert.JSONEq(t, `{"id":"1","time":"2026-10-10T12:00:00Z","type":"repo_created","repo":"beta","repository":{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"}}`, lines[0]) |
| 99 | assert.JSONEq(t, `{"id":"3","time":"2026-10-10T12:00:00Z","type":"pushed","repo":"alpha","ref_updates":[ | 99 | assert.JSONEq(t, `{"id":"3","time":"2026-10-10T12:00:00Z","type":"pushed","repo":"alpha","ref_updates":[ | ||
| 100 | {"ref":"refs/heads/main","old_id":"1111111111111111","new_id":"2222222222222222"}, | 100 | {"ref":"refs/heads/main","old_id":"1111111111111111","new_id":"2222222222222222"}, | ||
| 101 | {"ref":"refs/heads/new","old_id":"","new_id":"3333333333333333"}, | 101 | {"ref":"refs/heads/new","old_id":"","new_id":"3333333333333333"}, | ||
M internal/app/issue_api_test.go +0 -1
| @@ -221,7 +221,6 @@ func TestIssuesFeatureAndSettingOverTheAPI(t *testing.T) { | |||
| 221 | 221 | updated, err := update(r.root, pb.IssuesMode_ISSUES_MODE_OWNER) | |
| 222 | 222 | require.NoError(t, err) | |
| 223 | 223 | assert.Equal(t, pb.IssuesMode_ISSUES_MODE_OWNER, updated.GetSettings().GetIssues()) | |
| 224 | - | assert.True(t, updated.GetSettings().GetFailingChecksBlock(), "the other settings are kept") | |
| 225 | 224 | _, err = r.root.issue.CreateIssue(ctx, connect.NewRequest(&pb.CreateIssueRequest{Repo: "priv", Title: "t"})) | |
| 226 | 225 | require.NoError(t, err) | |
| 227 | 226 | ||
| @@ -221,7 +221,6 @@ func TestIssuesFeatureAndSettingOverTheAPI(t *testing.T) { | |||||
| 221 | updated, err := update(r.root, pb.IssuesMode_ISSUES_MODE_OWNER) | 221 | updated, err := update(r.root, pb.IssuesMode_ISSUES_MODE_OWNER) | ||
| 222 | require.NoError(t, err) | 222 | require.NoError(t, err) | ||
| 223 | assert.Equal(t, pb.IssuesMode_ISSUES_MODE_OWNER, updated.GetSettings().GetIssues()) | 223 | assert.Equal(t, pb.IssuesMode_ISSUES_MODE_OWNER, updated.GetSettings().GetIssues()) | ||
| 224 | - | assert.True(t, updated.GetSettings().GetFailingChecksBlock(), "the other settings are kept") | |||
| 225 | _, err = r.root.issue.CreateIssue(ctx, connect.NewRequest(&pb.CreateIssueRequest{Repo: "priv", Title: "t"})) | 224 | _, err = r.root.issue.CreateIssue(ctx, connect.NewRequest(&pb.CreateIssueRequest{Repo: "priv", Title: "t"})) | ||
| 226 | require.NoError(t, err) | 225 | require.NoError(t, err) | ||
| 227 | 226 | ||||
M internal/app/mr_api_test.go +9 -9
| @@ -299,7 +299,7 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) { | |||
| 299 | 299 | got, err := h.api("").repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"})) | |
| 300 | 300 | require.NoError(t, err) | |
| 301 | 301 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_MERGE_COMMIT}, got.Msg.GetRepo().GetSettings().GetAllowedMergeModes()) | |
| 302 | - | assert.True(t, got.Msg.GetRepo().GetSettings().GetFailingChecksBlock()) | |
| 302 | + | assert.False(t, got.Msg.GetRepo().GetSettings().GetAllowAutoMerge()) | |
| 303 | 303 | ||
| 304 | 304 | update := func(c *clients, paths []string, s *pb.RepoSettings) (*pb.Repo, error) { | |
| 305 | 305 | res, err := c.repo.UpdateRepo(ctx, connect.NewRequest(&pb.UpdateRepoRequest{ | |
| @@ -311,23 +311,23 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) { | |||
| 311 | 311 | return res.Msg.GetRepo(), nil | |
| 312 | 312 | } | |
| 313 | 313 | repo, err := update(root, []string{"settings.allowed_merge_modes"}, &pb.RepoSettings{ | |
| 314 | - | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, FailingChecksBlock: false, | |
| 314 | + | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, AllowAutoMerge: false, | |
| 315 | 315 | }) | |
| 316 | 316 | require.NoError(t, err) | |
| 317 | 317 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes()) | |
| 318 | - | assert.True(t, repo.GetSettings().GetFailingChecksBlock(), "not named in the mask: unchanged") | |
| 318 | + | assert.False(t, repo.GetSettings().GetAllowAutoMerge(), "not named in the mask: unchanged") | |
| 319 | 319 | ||
| 320 | - | repo, err = update(root, []string{"settings.failing_checks_block"}, &pb.RepoSettings{FailingChecksBlock: false}) | |
| 320 | + | repo, err = update(root, []string{"settings.allow_auto_merge"}, &pb.RepoSettings{AllowAutoMerge: false}) | |
| 321 | 321 | require.NoError(t, err) | |
| 322 | - | assert.False(t, repo.GetSettings().GetFailingChecksBlock()) | |
| 322 | + | assert.False(t, repo.GetSettings().GetAllowAutoMerge()) | |
| 323 | 323 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "not named in the mask: unchanged") | |
| 324 | 324 | ||
| 325 | 325 | repo, err = update(root, []string{"settings"}, &pb.RepoSettings{ | |
| 326 | - | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, FailingChecksBlock: true, | |
| 326 | + | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, AllowAutoMerge: true, | |
| 327 | 327 | }) | |
| 328 | 328 | require.NoError(t, err) | |
| 329 | 329 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "canonical order, no duplicates") | |
| 330 | - | assert.True(t, repo.GetSettings().GetFailingChecksBlock()) | |
| 330 | + | assert.True(t, repo.GetSettings().GetAllowAutoMerge()) | |
| 331 | 331 | ||
| 332 | 332 | for name, s := range map[string]*pb.RepoSettings{ | |
| 333 | 333 | "no modes": {}, | |
| @@ -341,11 +341,11 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) { | |||
| 341 | 341 | assert.Equal(t, "invalid_argument", codeName(err)) | |
| 342 | 342 | ||
| 343 | 343 | // repo:admin is needed. | |
| 344 | - | _, err = update(h.api(h.token(scopes(domain.ScopeRepoRead, domain.ScopeMRMerge))), []string{"settings.failing_checks_block"}, &pb.RepoSettings{}) | |
| 344 | + | _, err = update(h.api(h.token(scopes(domain.ScopeRepoRead, domain.ScopeMRMerge))), []string{"settings.allow_auto_merge"}, &pb.RepoSettings{}) | |
| 345 | 345 | assert.Equal(t, "permission_denied", codeName(err)) | |
| 346 | 346 | got, err = root.repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"})) | |
| 347 | 347 | require.NoError(t, err) | |
| 348 | - | assert.True(t, got.Msg.GetRepo().GetSettings().GetFailingChecksBlock()) | |
| 348 | + | assert.True(t, got.Msg.GetRepo().GetSettings().GetAllowAutoMerge()) | |
| 349 | 349 | } | |
| 350 | 350 | ||
| 351 | 351 | // AC4 over the API: a mode the settings do not allow is refused. | |
| @@ -299,7 +299,7 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) { | |||||
| 299 | got, err := h.api("").repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"})) | 299 | got, err := h.api("").repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"})) | ||
| 300 | require.NoError(t, err) | 300 | require.NoError(t, err) | ||
| 301 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_MERGE_COMMIT}, got.Msg.GetRepo().GetSettings().GetAllowedMergeModes()) | 301 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_MERGE_COMMIT}, got.Msg.GetRepo().GetSettings().GetAllowedMergeModes()) | ||
| 302 | - | assert.True(t, got.Msg.GetRepo().GetSettings().GetFailingChecksBlock()) | 302 | + | assert.False(t, got.Msg.GetRepo().GetSettings().GetAllowAutoMerge()) |
| 303 | 303 | ||||
| 304 | update := func(c *clients, paths []string, s *pb.RepoSettings) (*pb.Repo, error) { | 304 | update := func(c *clients, paths []string, s *pb.RepoSettings) (*pb.Repo, error) { | ||
| 305 | res, err := c.repo.UpdateRepo(ctx, connect.NewRequest(&pb.UpdateRepoRequest{ | 305 | res, err := c.repo.UpdateRepo(ctx, connect.NewRequest(&pb.UpdateRepoRequest{ | ||
| @@ -311,23 +311,23 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) { | |||||
| 311 | return res.Msg.GetRepo(), nil | 311 | return res.Msg.GetRepo(), nil | ||
| 312 | } | 312 | } | ||
| 313 | repo, err := update(root, []string{"settings.allowed_merge_modes"}, &pb.RepoSettings{ | 313 | repo, err := update(root, []string{"settings.allowed_merge_modes"}, &pb.RepoSettings{ | ||
| 314 | - | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, FailingChecksBlock: false, | 314 | + | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, AllowAutoMerge: false, |
| 315 | }) | 315 | }) | ||
| 316 | require.NoError(t, err) | 316 | require.NoError(t, err) | ||
| 317 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes()) | 317 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes()) | ||
| 318 | - | assert.True(t, repo.GetSettings().GetFailingChecksBlock(), "not named in the mask: unchanged") | 318 | + | assert.False(t, repo.GetSettings().GetAllowAutoMerge(), "not named in the mask: unchanged") |
| 319 | 319 | ||||
| 320 | - | repo, err = update(root, []string{"settings.failing_checks_block"}, &pb.RepoSettings{FailingChecksBlock: false}) | 320 | + | repo, err = update(root, []string{"settings.allow_auto_merge"}, &pb.RepoSettings{AllowAutoMerge: false}) |
| 321 | require.NoError(t, err) | 321 | require.NoError(t, err) | ||
| 322 | - | assert.False(t, repo.GetSettings().GetFailingChecksBlock()) | 322 | + | assert.False(t, repo.GetSettings().GetAllowAutoMerge()) |
| 323 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "not named in the mask: unchanged") | 323 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "not named in the mask: unchanged") | ||
| 324 | 324 | ||||
| 325 | repo, err = update(root, []string{"settings"}, &pb.RepoSettings{ | 325 | repo, err = update(root, []string{"settings"}, &pb.RepoSettings{ | ||
| 326 | - | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, FailingChecksBlock: true, | 326 | + | AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, AllowAutoMerge: true, |
| 327 | }) | 327 | }) | ||
| 328 | require.NoError(t, err) | 328 | require.NoError(t, err) | ||
| 329 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "canonical order, no duplicates") | 329 | assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "canonical order, no duplicates") | ||
| 330 | - | assert.True(t, repo.GetSettings().GetFailingChecksBlock()) | 330 | + | assert.True(t, repo.GetSettings().GetAllowAutoMerge()) |
| 331 | 331 | ||||
| 332 | for name, s := range map[string]*pb.RepoSettings{ | 332 | for name, s := range map[string]*pb.RepoSettings{ | ||
| 333 | "no modes": {}, | 333 | "no modes": {}, | ||
| @@ -341,11 +341,11 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) { | |||||
| 341 | assert.Equal(t, "invalid_argument", codeName(err)) | 341 | assert.Equal(t, "invalid_argument", codeName(err)) | ||
| 342 | 342 | ||||
| 343 | // repo:admin is needed. | 343 | // repo:admin is needed. | ||
| 344 | - | _, err = update(h.api(h.token(scopes(domain.ScopeRepoRead, domain.ScopeMRMerge))), []string{"settings.failing_checks_block"}, &pb.RepoSettings{}) | 344 | + | _, err = update(h.api(h.token(scopes(domain.ScopeRepoRead, domain.ScopeMRMerge))), []string{"settings.allow_auto_merge"}, &pb.RepoSettings{}) |
| 345 | assert.Equal(t, "permission_denied", codeName(err)) | 345 | assert.Equal(t, "permission_denied", codeName(err)) | ||
| 346 | got, err = root.repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"})) | 346 | got, err = root.repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"})) | ||
| 347 | require.NoError(t, err) | 347 | require.NoError(t, err) | ||
| 348 | - | assert.True(t, got.Msg.GetRepo().GetSettings().GetFailingChecksBlock()) | 348 | + | assert.True(t, got.Msg.GetRepo().GetSettings().GetAllowAutoMerge()) |
| 349 | } | 349 | } | ||
| 350 | 350 | ||||
| 351 | // AC4 over the API: a mode the settings do not allow is refused. | 351 | // AC4 over the API: a mode the settings do not allow is refused. | ||
M internal/domain/ci.go +1 -1
| @@ -42,7 +42,7 @@ const ( | |||
| 42 | 42 | ) | |
| 43 | 43 | ||
| 44 | 44 | // PassesMerge reports whether a check that ended this way lets a merge go | |
| 45 | - | // through under FailingChecksBlock: success, skipped and neutral do. | |
| 45 | + | // through under the merge checks policy: success, skipped and neutral do. | |
| 46 | 46 | func (c CIConclusion) PassesMerge() bool { | |
| 47 | 47 | return c == CISuccess || c == CISkipped || c == CINeutral | |
| 48 | 48 | } | |
| @@ -42,7 +42,7 @@ const ( | |||||
| 42 | ) | 42 | ) | ||
| 43 | 43 | ||||
| 44 | // PassesMerge reports whether a check that ended this way lets a merge go | 44 | // PassesMerge reports whether a check that ended this way lets a merge go | ||
| 45 | - | // through under FailingChecksBlock: success, skipped and neutral do. | 45 | + | // through under the merge checks policy: success, skipped and neutral do. |
| 46 | func (c CIConclusion) PassesMerge() bool { | 46 | func (c CIConclusion) PassesMerge() bool { | ||
| 47 | return c == CISuccess || c == CISkipped || c == CINeutral | 47 | return c == CISuccess || c == CISkipped || c == CINeutral | ||
| 48 | } | 48 | } | ||
M internal/domain/mergerequest.go +3 -5
| @@ -46,8 +46,6 @@ type RepoSettings struct { | |||
| 46 | 46 | // AllowedMergeModes is never empty, in canonical order, without | |
| 47 | 47 | // duplicates. | |
| 48 | 48 | AllowedMergeModes []MergeMode | |
| 49 | - | // FailingChecksBlock says that a failing check prevents the merge. | |
| 50 | - | FailingChecksBlock bool | |
| 51 | 49 | // Issues says who may write issues; the empty mode means IssuesOff. | |
| 52 | 50 | Issues IssuesMode | |
| 53 | 51 | // AllowAutoMerge says that merge requests may be set to merge themselves | |
| @@ -55,10 +53,10 @@ type RepoSettings struct { | |||
| 55 | 53 | AllowAutoMerge bool | |
| 56 | 54 | } | |
| 57 | 55 | ||
| 58 | - | // DefaultRepoSettings are the settings of a new repository: every mode, and | |
| 59 | - | // failing checks block the merge. | |
| 56 | + | // DefaultRepoSettings are the settings of a new repository: every merge mode, | |
| 57 | + | // with issues and auto-merge disabled. | |
| 60 | 58 | func DefaultRepoSettings() RepoSettings { | |
| 61 | - | return RepoSettings{AllowedMergeModes: MergeModes(), FailingChecksBlock: true, Issues: IssuesOff} | |
| 59 | + | return RepoSettings{AllowedMergeModes: MergeModes(), Issues: IssuesOff} | |
| 62 | 60 | } | |
| 63 | 61 | ||
| 64 | 62 | // IsZero reports whether s holds no mode, which only the zero value does. | |
| @@ -46,8 +46,6 @@ type RepoSettings struct { | |||||
| 46 | // AllowedMergeModes is never empty, in canonical order, without | 46 | // AllowedMergeModes is never empty, in canonical order, without | ||
| 47 | // duplicates. | 47 | // duplicates. | ||
| 48 | AllowedMergeModes []MergeMode | 48 | AllowedMergeModes []MergeMode | ||
| 49 | - | // FailingChecksBlock says that a failing check prevents the merge. | |||
| 50 | - | FailingChecksBlock bool | |||
| 51 | // Issues says who may write issues; the empty mode means IssuesOff. | 49 | // Issues says who may write issues; the empty mode means IssuesOff. | ||
| 52 | Issues IssuesMode | 50 | Issues IssuesMode | ||
| 53 | // AllowAutoMerge says that merge requests may be set to merge themselves | 51 | // AllowAutoMerge says that merge requests may be set to merge themselves | ||
| @@ -55,10 +53,10 @@ type RepoSettings struct { | |||||
| 55 | AllowAutoMerge bool | 53 | AllowAutoMerge bool | ||
| 56 | } | 54 | } | ||
| 57 | 55 | ||||
| 58 | - | // DefaultRepoSettings are the settings of a new repository: every mode, and | 56 | + | // DefaultRepoSettings are the settings of a new repository: every merge mode, |
| 59 | - | // failing checks block the merge. | 57 | + | // with issues and auto-merge disabled. |
| 60 | func DefaultRepoSettings() RepoSettings { | 58 | func DefaultRepoSettings() RepoSettings { | ||
| 61 | - | return RepoSettings{AllowedMergeModes: MergeModes(), FailingChecksBlock: true, Issues: IssuesOff} | 59 | + | return RepoSettings{AllowedMergeModes: MergeModes(), Issues: IssuesOff} |
| 62 | } | 60 | } | ||
| 63 | 61 | ||||
| 64 | // IsZero reports whether s holds no mode, which only the zero value does. | 62 | // IsZero reports whether s holds no mode, which only the zero value does. | ||
M internal/domain/mergerequest_test.go +0 -1
| @@ -25,7 +25,6 @@ func TestDefaultRepoSettings(t *testing.T) { | |||
| 25 | 25 | t.Parallel() | |
| 26 | 26 | s := domain.DefaultRepoSettings() | |
| 27 | 27 | assert.Equal(t, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash, domain.MergeModeMerge}, s.AllowedMergeModes) | |
| 28 | - | assert.True(t, s.FailingChecksBlock) | |
| 29 | 28 | assert.True(t, s.Allows(domain.MergeModeSquash)) | |
| 30 | 29 | assert.False(t, domain.RepoSettings{}.Allows(domain.MergeModeSquash)) | |
| 31 | 30 | assert.True(t, domain.RepoSettings{}.IsZero()) | |
| @@ -25,7 +25,6 @@ func TestDefaultRepoSettings(t *testing.T) { | |||||
| 25 | t.Parallel() | 25 | t.Parallel() | ||
| 26 | s := domain.DefaultRepoSettings() | 26 | s := domain.DefaultRepoSettings() | ||
| 27 | assert.Equal(t, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash, domain.MergeModeMerge}, s.AllowedMergeModes) | 27 | assert.Equal(t, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash, domain.MergeModeMerge}, s.AllowedMergeModes) | ||
| 28 | - | assert.True(t, s.FailingChecksBlock) | |||
| 29 | assert.True(t, s.Allows(domain.MergeModeSquash)) | 28 | assert.True(t, s.Allows(domain.MergeModeSquash)) | ||
| 30 | assert.False(t, domain.RepoSettings{}.Allows(domain.MergeModeSquash)) | 29 | assert.False(t, domain.RepoSettings{}.Allows(domain.MergeModeSquash)) | ||
| 31 | assert.True(t, domain.RepoSettings{}.IsZero()) | 30 | assert.True(t, domain.RepoSettings{}.IsZero()) | ||
M internal/domain/token.go +4 -2
| @@ -30,6 +30,8 @@ const ( | |||
| 30 | 30 | ScopeMRWrite Scope = "mr:write" | |
| 31 | 31 | // ScopeMRMerge merges, and deletes the merged source branch on request. | |
| 32 | 32 | ScopeMRMerge Scope = "mr:merge" | |
| 33 | + | // ScopeMRForceMerge bypasses failing or unfinished merge checks. | |
| 34 | + | ScopeMRForceMerge Scope = "mr:force-merge" | |
| 33 | 35 | // ScopeMRReview sets a verdict on a merge request. | |
| 34 | 36 | ScopeMRReview Scope = "mr:review" | |
| 35 | 37 | // ScopeCIWrite re-runs or cancels CI runs. | |
| @@ -45,11 +47,11 @@ const ( | |||
| 45 | 47 | ScopeIssueWrite Scope = "issue:write" | |
| 46 | 48 | ) | |
| 47 | 49 | ||
| 48 | - | // AllScopes returns the eleven scopes in a stable order. | |
| 50 | + | // AllScopes returns the twelve scopes in a stable order. | |
| 49 | 51 | func AllScopes() []Scope { | |
| 50 | 52 | return []Scope{ | |
| 51 | 53 | ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin, | |
| 52 | - | ScopeMRWrite, ScopeMRMerge, ScopeMRReview, | |
| 54 | + | ScopeMRWrite, ScopeMRMerge, ScopeMRForceMerge, ScopeMRReview, | |
| 53 | 55 | ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister, | |
| 54 | 56 | ScopeTokenAdmin, | |
| 55 | 57 | ScopeIssueWrite, | |
| @@ -30,6 +30,8 @@ const ( | |||||
| 30 | ScopeMRWrite Scope = "mr:write" | 30 | ScopeMRWrite Scope = "mr:write" | ||
| 31 | // ScopeMRMerge merges, and deletes the merged source branch on request. | 31 | // ScopeMRMerge merges, and deletes the merged source branch on request. | ||
| 32 | ScopeMRMerge Scope = "mr:merge" | 32 | ScopeMRMerge Scope = "mr:merge" | ||
| 33 | + | // ScopeMRForceMerge bypasses failing or unfinished merge checks. | |||
| 34 | + | ScopeMRForceMerge Scope = "mr:force-merge" | |||
| 33 | // ScopeMRReview sets a verdict on a merge request. | 35 | // ScopeMRReview sets a verdict on a merge request. | ||
| 34 | ScopeMRReview Scope = "mr:review" | 36 | ScopeMRReview Scope = "mr:review" | ||
| 35 | // ScopeCIWrite re-runs or cancels CI runs. | 37 | // ScopeCIWrite re-runs or cancels CI runs. | ||
| @@ -45,11 +47,11 @@ const ( | |||||
| 45 | ScopeIssueWrite Scope = "issue:write" | 47 | ScopeIssueWrite Scope = "issue:write" | ||
| 46 | ) | 48 | ) | ||
| 47 | 49 | ||||
| 48 | - | // AllScopes returns the eleven scopes in a stable order. | 50 | + | // AllScopes returns the twelve scopes in a stable order. |
| 49 | func AllScopes() []Scope { | 51 | func AllScopes() []Scope { | ||
| 50 | return []Scope{ | 52 | return []Scope{ | ||
| 51 | ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin, | 53 | ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin, | ||
| 52 | - | ScopeMRWrite, ScopeMRMerge, ScopeMRReview, | 54 | + | ScopeMRWrite, ScopeMRMerge, ScopeMRForceMerge, ScopeMRReview, |
| 53 | ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister, | 55 | ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister, | ||
| 54 | ScopeTokenAdmin, | 56 | ScopeTokenAdmin, | ||
| 55 | ScopeIssueWrite, | 57 | ScopeIssueWrite, | ||
M internal/domain/token_test.go +1 -1
| @@ -16,7 +16,7 @@ import ( | |||
| 16 | 16 | func TestScopes(t *testing.T) { | |
| 17 | 17 | t.Parallel() | |
| 18 | 18 | all := domain.AllScopes() | |
| 19 | - | assert.Len(t, all, 11) | |
| 19 | + | assert.Len(t, all, 12) | |
| 20 | 20 | assert.Contains(t, all, domain.ScopeIssueWrite) | |
| 21 | 21 | got, err := domain.ParseScope("issue:write") | |
| 22 | 22 | require.NoError(t, err) | |
| @@ -16,7 +16,7 @@ import ( | |||||
| 16 | func TestScopes(t *testing.T) { | 16 | func TestScopes(t *testing.T) { | ||
| 17 | t.Parallel() | 17 | t.Parallel() | ||
| 18 | all := domain.AllScopes() | 18 | all := domain.AllScopes() | ||
| 19 | - | assert.Len(t, all, 11) | 19 | + | assert.Len(t, all, 12) |
| 20 | assert.Contains(t, all, domain.ScopeIssueWrite) | 20 | assert.Contains(t, all, domain.ScopeIssueWrite) | ||
| 21 | got, err := domain.ParseScope("issue:write") | 21 | got, err := domain.ParseScope("issue:write") | ||
| 22 | require.NoError(t, err) | 22 | require.NoError(t, err) | ||
M internal/ports/dto.go +2 -2
| @@ -47,8 +47,6 @@ type UpdateRepoParams struct { | |||
| 47 | 47 | // AllowedMergeModes replaces the allowed merge modes; it must hold at | |
| 48 | 48 | // least one known mode. | |
| 49 | 49 | AllowedMergeModes *[]domain.MergeMode | |
| 50 | - | // FailingChecksBlock replaces the setting of that name. | |
| 51 | - | FailingChecksBlock *bool | |
| 52 | 50 | // Issues replaces the issues mode. | |
| 53 | 51 | Issues *domain.IssuesMode | |
| 54 | 52 | // AllowAutoMerge replaces the setting of that name. | |
| @@ -381,6 +379,8 @@ type AddCommentParams struct { | |||
| 381 | 379 | ||
| 382 | 380 | // MergeParams describes the merge to perform. | |
| 383 | 381 | type MergeParams struct { | |
| 382 | + | // IgnoreChecks bypasses checks and requires mr:force-merge. | |
| 383 | + | IgnoreChecks bool | |
| 384 | 384 | // Mode defaults to fast-forward. | |
| 385 | 385 | Mode domain.MergeMode | |
| 386 | 386 | // HeadID is the commit the caller reviewed; the source branch must still | |
| @@ -47,8 +47,6 @@ type UpdateRepoParams struct { | |||||
| 47 | // AllowedMergeModes replaces the allowed merge modes; it must hold at | 47 | // AllowedMergeModes replaces the allowed merge modes; it must hold at | ||
| 48 | // least one known mode. | 48 | // least one known mode. | ||
| 49 | AllowedMergeModes *[]domain.MergeMode | 49 | AllowedMergeModes *[]domain.MergeMode | ||
| 50 | - | // FailingChecksBlock replaces the setting of that name. | |||
| 51 | - | FailingChecksBlock *bool | |||
| 52 | // Issues replaces the issues mode. | 50 | // Issues replaces the issues mode. | ||
| 53 | Issues *domain.IssuesMode | 51 | Issues *domain.IssuesMode | ||
| 54 | // AllowAutoMerge replaces the setting of that name. | 52 | // AllowAutoMerge replaces the setting of that name. | ||
| @@ -381,6 +379,8 @@ type AddCommentParams struct { | |||||
| 381 | 379 | ||||
| 382 | // MergeParams describes the merge to perform. | 380 | // MergeParams describes the merge to perform. | ||
| 383 | type MergeParams struct { | 381 | type MergeParams struct { | ||
| 382 | + | // IgnoreChecks bypasses checks and requires mr:force-merge. | |||
| 383 | + | IgnoreChecks bool | |||
| 384 | // Mode defaults to fast-forward. | 384 | // Mode defaults to fast-forward. | ||
| 385 | Mode domain.MergeMode | 385 | Mode domain.MergeMode | ||
| 386 | // HeadID is the commit the caller reviewed; the source branch must still | 386 | // HeadID is the commit the caller reviewed; the source branch must still | ||
M internal/usecase/access.go +1 -1
| @@ -36,7 +36,7 @@ func canReadAs(p domain.Principal, name domain.RepoName, vis domain.Visibility) | |||
| 36 | 36 | // repo:admin on it, and no branch prefixes. A token limited to some branches | |
| 37 | 37 | // may not delete the repository (that deletes every branch, those outside its | |
| 38 | 38 | // prefixes included), nor change its default branch, visibility, description, | |
| 39 | - | // merge modes or failing-checks rule: these decide what happens to branches the | |
| 39 | + | // merge modes or auto-merge settings: these decide what happens to branches the | |
| 40 | 40 | // token may not write. A caller that may not administer it learns nothing it | |
| 41 | 41 | // could not already read: not found when it cannot see the repository, | |
| 42 | 42 | // unauthenticated when it is anonymous, forbidden otherwise. | |
| @@ -36,7 +36,7 @@ func canReadAs(p domain.Principal, name domain.RepoName, vis domain.Visibility) | |||||
| 36 | // repo:admin on it, and no branch prefixes. A token limited to some branches | 36 | // repo:admin on it, and no branch prefixes. A token limited to some branches | ||
| 37 | // may not delete the repository (that deletes every branch, those outside its | 37 | // may not delete the repository (that deletes every branch, those outside its | ||
| 38 | // prefixes included), nor change its default branch, visibility, description, | 38 | // prefixes included), nor change its default branch, visibility, description, | ||
| 39 | - | // merge modes or failing-checks rule: these decide what happens to branches the | 39 | + | // merge modes or auto-merge settings: these decide what happens to branches the |
| 40 | // token may not write. A caller that may not administer it learns nothing it | 40 | // token may not write. A caller that may not administer it learns nothing it | ||
| 41 | // could not already read: not found when it cannot see the repository, | 41 | // could not already read: not found when it cannot see the repository, | ||
| 42 | // unauthenticated when it is anonymous, forbidden otherwise. | 42 | // unauthenticated when it is anonymous, forbidden otherwise. | ||
M internal/usecase/automerge_test.go +1 -4
| @@ -159,7 +159,7 @@ func TestAutoMergeMergesAsEnablerWhenAChecksPassed(t *testing.T) { | |||
| 159 | 159 | require.True(t, found) | |
| 160 | 160 | } | |
| 161 | 161 | ||
| 162 | - | func TestAutoMergeIgnoresFailingChecksBlockOff(t *testing.T) { | |
| 162 | + | func TestAutoMergeRequiresPassingChecks(t *testing.T) { | |
| 163 | 163 | for _, tc := range []struct { | |
| 164 | 164 | name string | |
| 165 | 165 | check domain.CICheck | |
| @@ -170,8 +170,6 @@ func TestAutoMergeIgnoresFailingChecksBlockOff(t *testing.T) { | |||
| 170 | 170 | t.Run(tc.name, func(t *testing.T) { | |
| 171 | 171 | r, mr, checks := autoRig(t) | |
| 172 | 172 | enableAuto(t, r, mr) | |
| 173 | - | _, err := r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{FailingChecksBlock: ptr(false)}) | |
| 174 | - | require.NoError(t, err) | |
| 175 | 173 | checks.EXPECT().CommitChecks(mock.Anything, r.repo, r.head().String()).Return([]domain.CICheck{{Status: domain.CICompleted, Conclusion: domain.CISuccess}, tc.check}, nil) | |
| 176 | 174 | a := autoEvaluator(r, checks, nil, nil) | |
| 177 | 175 | defer a.Close(r.ctx) | |
| @@ -388,7 +386,6 @@ func TestMergeGateSettingsUpdateWaits(t *testing.T) { | |||
| 388 | 386 | for _, params := range []ports.UpdateRepoParams{ | |
| 389 | 387 | {AllowAutoMerge: ptr(false)}, | |
| 390 | 388 | {AllowedMergeModes: ptr([]domain.MergeMode{domain.MergeModeSquash})}, | |
| 391 | - | {FailingChecksBlock: ptr(false)}, | |
| 392 | 389 | } { | |
| 393 | 390 | r, _, _ := autoRig(t) | |
| 394 | 391 | r.repos.WithMergeGate(r.mrs) | |
| @@ -159,7 +159,7 @@ func TestAutoMergeMergesAsEnablerWhenAChecksPassed(t *testing.T) { | |||||
| 159 | require.True(t, found) | 159 | require.True(t, found) | ||
| 160 | } | 160 | } | ||
| 161 | 161 | ||||
| 162 | - | func TestAutoMergeIgnoresFailingChecksBlockOff(t *testing.T) { | 162 | + | func TestAutoMergeRequiresPassingChecks(t *testing.T) { |
| 163 | for _, tc := range []struct { | 163 | for _, tc := range []struct { | ||
| 164 | name string | 164 | name string | ||
| 165 | check domain.CICheck | 165 | check domain.CICheck | ||
| @@ -170,8 +170,6 @@ func TestAutoMergeIgnoresFailingChecksBlockOff(t *testing.T) { | |||||
| 170 | t.Run(tc.name, func(t *testing.T) { | 170 | t.Run(tc.name, func(t *testing.T) { | ||
| 171 | r, mr, checks := autoRig(t) | 171 | r, mr, checks := autoRig(t) | ||
| 172 | enableAuto(t, r, mr) | 172 | enableAuto(t, r, mr) | ||
| 173 | - | _, err := r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{FailingChecksBlock: ptr(false)}) | |||
| 174 | - | require.NoError(t, err) | |||
| 175 | checks.EXPECT().CommitChecks(mock.Anything, r.repo, r.head().String()).Return([]domain.CICheck{{Status: domain.CICompleted, Conclusion: domain.CISuccess}, tc.check}, nil) | 173 | checks.EXPECT().CommitChecks(mock.Anything, r.repo, r.head().String()).Return([]domain.CICheck{{Status: domain.CICompleted, Conclusion: domain.CISuccess}, tc.check}, nil) | ||
| 176 | a := autoEvaluator(r, checks, nil, nil) | 174 | a := autoEvaluator(r, checks, nil, nil) | ||
| 177 | defer a.Close(r.ctx) | 175 | defer a.Close(r.ctx) | ||
| @@ -388,7 +386,6 @@ func TestMergeGateSettingsUpdateWaits(t *testing.T) { | |||||
| 388 | for _, params := range []ports.UpdateRepoParams{ | 386 | for _, params := range []ports.UpdateRepoParams{ | ||
| 389 | {AllowAutoMerge: ptr(false)}, | 387 | {AllowAutoMerge: ptr(false)}, | ||
| 390 | {AllowedMergeModes: ptr([]domain.MergeMode{domain.MergeModeSquash})}, | 388 | {AllowedMergeModes: ptr([]domain.MergeMode{domain.MergeModeSquash})}, | ||
| 391 | - | {FailingChecksBlock: ptr(false)}, | |||
| 392 | } { | 389 | } { | ||
| 393 | r, _, _ := autoRig(t) | 390 | r, _, _ := autoRig(t) | ||
| 394 | r.repos.WithMergeGate(r.mrs) | 391 | r.repos.WithMergeGate(r.mrs) | ||
M internal/usecase/ci_unit_test.go +37 -6
| @@ -24,7 +24,7 @@ type mergeUnit struct { | |||
| 24 | 24 | head domain.ObjectID | |
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | - | func newMergeUnit(t *testing.T, block bool) *mergeUnit { | |
| 27 | + | func newMergeUnit(t *testing.T) *mergeUnit { | |
| 28 | 28 | t.Helper() | |
| 29 | 29 | u := &mrUnit{ | |
| 30 | 30 | meta: mocks.NewRepoMetaStore(t), | |
| @@ -34,7 +34,6 @@ func newMergeUnit(t *testing.T, block bool) *mergeUnit { | |||
| 34 | 34 | repo: domain.Repo{Name: name(t, "proj"), Visibility: domain.VisibilityPrivate}, | |
| 35 | 35 | } | |
| 36 | 36 | u.repo.Settings = domain.DefaultRepoSettings() | |
| 37 | - | u.repo.Settings.FailingChecksBlock = block | |
| 38 | 37 | clock := mocks.NewClock(t) | |
| 39 | 38 | clock.EXPECT().Now().Return(now).Maybe() | |
| 40 | 39 | sched := mocks.NewMaintenanceScheduler(t) | |
| @@ -63,8 +62,8 @@ func (m *mergeUnit) merge() error { | |||
| 63 | 62 | return err | |
| 64 | 63 | } | |
| 65 | 64 | ||
| 66 | - | // AC5: the merge rule under FailingChecksBlock. | |
| 67 | - | func TestMergeRuleOfFailingChecksBlock(t *testing.T) { | |
| 65 | + | // AC5: the merge rule under ChecksPolicy. | |
| 66 | + | func TestMergeRuleOfChecksPolicy(t *testing.T) { | |
| 68 | 67 | t.Parallel() | |
| 69 | 68 | check := func(status domain.CIStatus, c domain.CIConclusion, name string) domain.CICheck { | |
| 70 | 69 | return domain.CICheck{Name: name, Status: status, Conclusion: c} | |
| @@ -84,7 +83,7 @@ func TestMergeRuleOfFailingChecksBlock(t *testing.T) { | |||
| 84 | 83 | for _, tt := range tests { | |
| 85 | 84 | t.Run(tt.name, func(t *testing.T) { | |
| 86 | 85 | t.Parallel() | |
| 87 | - | m := newMergeUnit(t, true) | |
| 86 | + | m := newMergeUnit(t) | |
| 88 | 87 | m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(tt.checks, nil) | |
| 89 | 88 | // The merge stops at the rule: the branch mock expects no move. | |
| 90 | 89 | err := m.merge() | |
| @@ -94,7 +93,7 @@ func TestMergeRuleOfFailingChecksBlock(t *testing.T) { | |||
| 94 | 93 | } | |
| 95 | 94 | t.Run("too many checks to tell", func(t *testing.T) { | |
| 96 | 95 | t.Parallel() | |
| 97 | - | m := newMergeUnit(t, true) | |
| 96 | + | m := newMergeUnit(t) | |
| 98 | 97 | m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(nil, domain.ErrResourceExhausted) | |
| 99 | 98 | err := m.merge() | |
| 100 | 99 | require.ErrorIs(t, err, domain.ErrPreconditionFailed) | |
| @@ -174,3 +173,35 @@ func TestTokenShapes(t *testing.T) { | |||
| 174 | 173 | assert.False(t, domain.IsJobToken("ermite_"+good[10:])) | |
| 175 | 174 | assert.False(t, domain.IsJobToken(good+"=")) | |
| 176 | 175 | } | |
| 176 | + | ||
| 177 | + | func TestMergeChecksAndOverride(t *testing.T) { | |
| 178 | + | for _, tc := range []struct { | |
| 179 | + | name string | |
| 180 | + | principal domain.Principal | |
| 181 | + | checks []domain.CICheck | |
| 182 | + | ignore bool | |
| 183 | + | want error | |
| 184 | + | }{ | |
| 185 | + | {name: "no checks allowed", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge)}, | |
| 186 | + | {name: "failure refused", principal: root, checks: []domain.CICheck{{Name: "build", Status: domain.CICompleted, Conclusion: domain.CIFailure}}, want: domain.ErrPreconditionFailed}, | |
| 187 | + | {name: "pending refused", principal: root, checks: []domain.CICheck{{Name: "build", Status: domain.CIQueued}}, want: domain.ErrPreconditionFailed}, | |
| 188 | + | {name: "force scope bypasses failure", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge, domain.ScopeMRForceMerge), checks: []domain.CICheck{{Status: domain.CICompleted, Conclusion: domain.CIFailure}}, ignore: true}, | |
| 189 | + | {name: "missing force scope denied", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge), ignore: true, want: domain.ErrForbidden}, | |
| 190 | + | {name: "force scope alone insufficient", principal: writer(domain.ScopeRepoRead, domain.ScopeMRForceMerge), ignore: true, want: domain.ErrForbidden}, | |
| 191 | + | {name: "root override", principal: domain.Root(), ignore: true}, | |
| 192 | + | {name: "owner override", principal: domain.Owner(), ignore: true}, | |
| 193 | + | } { | |
| 194 | + | t.Run(tc.name, func(t *testing.T) { | |
| 195 | + | r, mr, checks := autoRig(t) | |
| 196 | + | if !tc.ignore { | |
| 197 | + | checks.EXPECT().CommitChecks(mock.Anything, r.repo, r.head().String()).Return(tc.checks, nil).Once() | |
| 198 | + | } | |
| 199 | + | _, err := r.mrs.Merge(r.ctx, tc.principal, r.repo, mr.Number, ports.MergeParams{HeadID: r.head(), IgnoreChecks: tc.ignore}) | |
| 200 | + | if tc.want != nil { | |
| 201 | + | require.ErrorIs(t, err, tc.want) | |
| 202 | + | } else { | |
| 203 | + | require.NoError(t, err) | |
| 204 | + | } | |
| 205 | + | }) | |
| 206 | + | } | |
| 207 | + | } | |
| @@ -24,7 +24,7 @@ type mergeUnit struct { | |||||
| 24 | head domain.ObjectID | 24 | head domain.ObjectID | ||
| 25 | } | 25 | } | ||
| 26 | 26 | ||||
| 27 | - | func newMergeUnit(t *testing.T, block bool) *mergeUnit { | 27 | + | func newMergeUnit(t *testing.T) *mergeUnit { |
| 28 | t.Helper() | 28 | t.Helper() | ||
| 29 | u := &mrUnit{ | 29 | u := &mrUnit{ | ||
| 30 | meta: mocks.NewRepoMetaStore(t), | 30 | meta: mocks.NewRepoMetaStore(t), | ||
| @@ -34,7 +34,6 @@ func newMergeUnit(t *testing.T, block bool) *mergeUnit { | |||||
| 34 | repo: domain.Repo{Name: name(t, "proj"), Visibility: domain.VisibilityPrivate}, | 34 | repo: domain.Repo{Name: name(t, "proj"), Visibility: domain.VisibilityPrivate}, | ||
| 35 | } | 35 | } | ||
| 36 | u.repo.Settings = domain.DefaultRepoSettings() | 36 | u.repo.Settings = domain.DefaultRepoSettings() | ||
| 37 | - | u.repo.Settings.FailingChecksBlock = block | |||
| 38 | clock := mocks.NewClock(t) | 37 | clock := mocks.NewClock(t) | ||
| 39 | clock.EXPECT().Now().Return(now).Maybe() | 38 | clock.EXPECT().Now().Return(now).Maybe() | ||
| 40 | sched := mocks.NewMaintenanceScheduler(t) | 39 | sched := mocks.NewMaintenanceScheduler(t) | ||
| @@ -63,8 +62,8 @@ func (m *mergeUnit) merge() error { | |||||
| 63 | return err | 62 | return err | ||
| 64 | } | 63 | } | ||
| 65 | 64 | ||||
| 66 | - | // AC5: the merge rule under FailingChecksBlock. | 65 | + | // AC5: the merge rule under ChecksPolicy. |
| 67 | - | func TestMergeRuleOfFailingChecksBlock(t *testing.T) { | 66 | + | func TestMergeRuleOfChecksPolicy(t *testing.T) { |
| 68 | t.Parallel() | 67 | t.Parallel() | ||
| 69 | check := func(status domain.CIStatus, c domain.CIConclusion, name string) domain.CICheck { | 68 | check := func(status domain.CIStatus, c domain.CIConclusion, name string) domain.CICheck { | ||
| 70 | return domain.CICheck{Name: name, Status: status, Conclusion: c} | 69 | return domain.CICheck{Name: name, Status: status, Conclusion: c} | ||
| @@ -84,7 +83,7 @@ func TestMergeRuleOfFailingChecksBlock(t *testing.T) { | |||||
| 84 | for _, tt := range tests { | 83 | for _, tt := range tests { | ||
| 85 | t.Run(tt.name, func(t *testing.T) { | 84 | t.Run(tt.name, func(t *testing.T) { | ||
| 86 | t.Parallel() | 85 | t.Parallel() | ||
| 87 | - | m := newMergeUnit(t, true) | 86 | + | m := newMergeUnit(t) |
| 88 | m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(tt.checks, nil) | 87 | m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(tt.checks, nil) | ||
| 89 | // The merge stops at the rule: the branch mock expects no move. | 88 | // The merge stops at the rule: the branch mock expects no move. | ||
| 90 | err := m.merge() | 89 | err := m.merge() | ||
| @@ -94,7 +93,7 @@ func TestMergeRuleOfFailingChecksBlock(t *testing.T) { | |||||
| 94 | } | 93 | } | ||
| 95 | t.Run("too many checks to tell", func(t *testing.T) { | 94 | t.Run("too many checks to tell", func(t *testing.T) { | ||
| 96 | t.Parallel() | 95 | t.Parallel() | ||
| 97 | - | m := newMergeUnit(t, true) | 96 | + | m := newMergeUnit(t) |
| 98 | m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(nil, domain.ErrResourceExhausted) | 97 | m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(nil, domain.ErrResourceExhausted) | ||
| 99 | err := m.merge() | 98 | err := m.merge() | ||
| 100 | require.ErrorIs(t, err, domain.ErrPreconditionFailed) | 99 | require.ErrorIs(t, err, domain.ErrPreconditionFailed) | ||
| @@ -174,3 +173,35 @@ func TestTokenShapes(t *testing.T) { | |||||
| 174 | assert.False(t, domain.IsJobToken("ermite_"+good[10:])) | 173 | assert.False(t, domain.IsJobToken("ermite_"+good[10:])) | ||
| 175 | assert.False(t, domain.IsJobToken(good+"=")) | 174 | assert.False(t, domain.IsJobToken(good+"=")) | ||
| 176 | } | 175 | } | ||
| 176 | + | ||||
| 177 | + | func TestMergeChecksAndOverride(t *testing.T) { | |||
| 178 | + | for _, tc := range []struct { | |||
| 179 | + | name string | |||
| 180 | + | principal domain.Principal | |||
| 181 | + | checks []domain.CICheck | |||
| 182 | + | ignore bool | |||
| 183 | + | want error | |||
| 184 | + | }{ | |||
| 185 | + | {name: "no checks allowed", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge)}, | |||
| 186 | + | {name: "failure refused", principal: root, checks: []domain.CICheck{{Name: "build", Status: domain.CICompleted, Conclusion: domain.CIFailure}}, want: domain.ErrPreconditionFailed}, | |||
| 187 | + | {name: "pending refused", principal: root, checks: []domain.CICheck{{Name: "build", Status: domain.CIQueued}}, want: domain.ErrPreconditionFailed}, | |||
| 188 | + | {name: "force scope bypasses failure", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge, domain.ScopeMRForceMerge), checks: []domain.CICheck{{Status: domain.CICompleted, Conclusion: domain.CIFailure}}, ignore: true}, | |||
| 189 | + | {name: "missing force scope denied", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge), ignore: true, want: domain.ErrForbidden}, | |||
| 190 | + | {name: "force scope alone insufficient", principal: writer(domain.ScopeRepoRead, domain.ScopeMRForceMerge), ignore: true, want: domain.ErrForbidden}, | |||
| 191 | + | {name: "root override", principal: domain.Root(), ignore: true}, | |||
| 192 | + | {name: "owner override", principal: domain.Owner(), ignore: true}, | |||
| 193 | + | } { | |||
| 194 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 195 | + | r, mr, checks := autoRig(t) | |||
| 196 | + | if !tc.ignore { | |||
| 197 | + | checks.EXPECT().CommitChecks(mock.Anything, r.repo, r.head().String()).Return(tc.checks, nil).Once() | |||
| 198 | + | } | |||
| 199 | + | _, err := r.mrs.Merge(r.ctx, tc.principal, r.repo, mr.Number, ports.MergeParams{HeadID: r.head(), IgnoreChecks: tc.ignore}) | |||
| 200 | + | if tc.want != nil { | |||
| 201 | + | require.ErrorIs(t, err, tc.want) | |||
| 202 | + | } else { | |||
| 203 | + | require.NoError(t, err) | |||
| 204 | + | } | |||
| 205 | + | }) | |||
| 206 | + | } | |||
| 207 | + | } | |||
M internal/usecase/mergerequests.go +9 -3
| @@ -1075,9 +1075,15 @@ var errAutoMergeWait = errors.New("auto-merge is waiting for checks") | |||
| 1075 | 1075 | ||
| 1076 | 1076 | func (m *MergeRequests) mergeAs(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params ports.MergeParams, opts mergeOpts) (domain.MergeRequest, error) { | |
| 1077 | 1077 | if opts.auto == nil { | |
| 1078 | - | if _, err := m.writable(ctx, p, name, domain.ScopeMRMerge); err != nil { | |
| 1078 | + | repo, err := m.writable(ctx, p, name, domain.ScopeMRMerge) | |
| 1079 | + | if err != nil { | |
| 1079 | 1080 | return domain.MergeRequest{}, err | |
| 1080 | 1081 | } | |
| 1082 | + | if params.IgnoreChecks { | |
| 1083 | + | if err := scopeAccess(p, repo, domain.ScopeMRForceMerge); err != nil { | |
| 1084 | + | return domain.MergeRequest{}, err | |
| 1085 | + | } | |
| 1086 | + | } | |
| 1081 | 1087 | } | |
| 1082 | 1088 | mode := params.Mode | |
| 1083 | 1089 | if mode == "" { | |
| @@ -1179,7 +1185,7 @@ func (m *MergeRequests) mergeAs(ctx context.Context, p domain.Principal, name do | |||
| 1179 | 1185 | if opts.auto != nil && !settings.AllowAutoMerge { | |
| 1180 | 1186 | return domain.MergeRequest{}, fmt.Errorf("%w: auto-merge is off in %s", domain.ErrPreconditionFailed, name) | |
| 1181 | 1187 | } | |
| 1182 | - | if settings.FailingChecksBlock || opts.auto != nil { | |
| 1188 | + | if !params.IgnoreChecks || opts.auto != nil { | |
| 1183 | 1189 | checks, err := m.readMergeChecks(ctx, name, head) | |
| 1184 | 1190 | if err == nil { | |
| 1185 | 1191 | err = checksPolicy(checks) | |
| @@ -1362,7 +1368,7 @@ func checksPolicy(checks []domain.CICheck) error { | |||
| 1362 | 1368 | } | |
| 1363 | 1369 | switch { | |
| 1364 | 1370 | case len(failing) > 0: | |
| 1365 | - | return fmt.Errorf("%w: failing checks block the merge: %s (turn off failing_checks_block to merge anyway)", domain.ErrPreconditionFailed, strings.Join(failing, ", ")) | |
| 1371 | + | return fmt.Errorf("%w: failing checks block the merge: %s (merge with --ignore-checks, needs the mr:force-merge scope)", domain.ErrPreconditionFailed, strings.Join(failing, ", ")) | |
| 1366 | 1372 | case len(pending) > 0: | |
| 1367 | 1373 | return fmt.Errorf("%w: checks have not finished: %s", domain.ErrPreconditionFailed, strings.Join(pending, ", ")) | |
| 1368 | 1374 | } | |
| @@ -1075,9 +1075,15 @@ var errAutoMergeWait = errors.New("auto-merge is waiting for checks") | |||||
| 1075 | 1075 | ||||
| 1076 | func (m *MergeRequests) mergeAs(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params ports.MergeParams, opts mergeOpts) (domain.MergeRequest, error) { | 1076 | func (m *MergeRequests) mergeAs(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params ports.MergeParams, opts mergeOpts) (domain.MergeRequest, error) { | ||
| 1077 | if opts.auto == nil { | 1077 | if opts.auto == nil { | ||
| 1078 | - | if _, err := m.writable(ctx, p, name, domain.ScopeMRMerge); err != nil { | 1078 | + | repo, err := m.writable(ctx, p, name, domain.ScopeMRMerge) |
| 1079 | + | if err != nil { | |||
| 1079 | return domain.MergeRequest{}, err | 1080 | return domain.MergeRequest{}, err | ||
| 1080 | } | 1081 | } | ||
| 1082 | + | if params.IgnoreChecks { | |||
| 1083 | + | if err := scopeAccess(p, repo, domain.ScopeMRForceMerge); err != nil { | |||
| 1084 | + | return domain.MergeRequest{}, err | |||
| 1085 | + | } | |||
| 1086 | + | } | |||
| 1081 | } | 1087 | } | ||
| 1082 | mode := params.Mode | 1088 | mode := params.Mode | ||
| 1083 | if mode == "" { | 1089 | if mode == "" { | ||
| @@ -1179,7 +1185,7 @@ func (m *MergeRequests) mergeAs(ctx context.Context, p domain.Principal, name do | |||||
| 1179 | if opts.auto != nil && !settings.AllowAutoMerge { | 1185 | if opts.auto != nil && !settings.AllowAutoMerge { | ||
| 1180 | return domain.MergeRequest{}, fmt.Errorf("%w: auto-merge is off in %s", domain.ErrPreconditionFailed, name) | 1186 | return domain.MergeRequest{}, fmt.Errorf("%w: auto-merge is off in %s", domain.ErrPreconditionFailed, name) | ||
| 1181 | } | 1187 | } | ||
| 1182 | - | if settings.FailingChecksBlock || opts.auto != nil { | 1188 | + | if !params.IgnoreChecks || opts.auto != nil { |
| 1183 | checks, err := m.readMergeChecks(ctx, name, head) | 1189 | checks, err := m.readMergeChecks(ctx, name, head) | ||
| 1184 | if err == nil { | 1190 | if err == nil { | ||
| 1185 | err = checksPolicy(checks) | 1191 | err = checksPolicy(checks) | ||
| @@ -1362,7 +1368,7 @@ func checksPolicy(checks []domain.CICheck) error { | |||||
| 1362 | } | 1368 | } | ||
| 1363 | switch { | 1369 | switch { | ||
| 1364 | case len(failing) > 0: | 1370 | case len(failing) > 0: | ||
| 1365 | - | return fmt.Errorf("%w: failing checks block the merge: %s (turn off failing_checks_block to merge anyway)", domain.ErrPreconditionFailed, strings.Join(failing, ", ")) | 1371 | + | return fmt.Errorf("%w: failing checks block the merge: %s (merge with --ignore-checks, needs the mr:force-merge scope)", domain.ErrPreconditionFailed, strings.Join(failing, ", ")) |
| 1366 | case len(pending) > 0: | 1372 | case len(pending) > 0: | ||
| 1367 | return fmt.Errorf("%w: checks have not finished: %s", domain.ErrPreconditionFailed, strings.Join(pending, ", ")) | 1373 | return fmt.Errorf("%w: checks have not finished: %s", domain.ErrPreconditionFailed, strings.Join(pending, ", ")) | ||
| 1368 | } | 1374 | } | ||
M internal/usecase/mergerequests_integration_test.go +5 -5
| @@ -1205,19 +1205,19 @@ func TestUpdateRepoSettings(t *testing.T) { | |||
| 1205 | 1205 | ||
| 1206 | 1206 | modes := []domain.MergeMode{domain.MergeModeSquash, domain.MergeModeSquash} | |
| 1207 | 1207 | off := false | |
| 1208 | - | got, err := r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{AllowedMergeModes: &modes, FailingChecksBlock: &off}) | |
| 1208 | + | got, err := r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{AllowedMergeModes: &modes, AllowAutoMerge: &off}) | |
| 1209 | 1209 | require.NoError(t, err) | |
| 1210 | - | assert.Equal(t, domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, FailingChecksBlock: false, Issues: domain.IssuesOff}, got.Settings) | |
| 1210 | + | assert.Equal(t, domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, AllowAutoMerge: false, Issues: domain.IssuesOff}, got.Settings) | |
| 1211 | 1211 | again, err := r.repos.Get(r.ctx, root, r.repo) | |
| 1212 | 1212 | require.NoError(t, err) | |
| 1213 | 1213 | assert.Equal(t, got.Settings, again.Settings) | |
| 1214 | 1214 | ||
| 1215 | 1215 | // Only the named setting changes. | |
| 1216 | 1216 | on := true | |
| 1217 | - | got, err = r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{FailingChecksBlock: &on}) | |
| 1217 | + | got, err = r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{AllowAutoMerge: &on}) | |
| 1218 | 1218 | require.NoError(t, err) | |
| 1219 | 1219 | assert.Equal(t, []domain.MergeMode{domain.MergeModeSquash}, got.Settings.AllowedMergeModes) | |
| 1220 | - | assert.True(t, got.Settings.FailingChecksBlock) | |
| 1220 | + | assert.True(t, got.Settings.AllowAutoMerge) | |
| 1221 | 1221 | ||
| 1222 | 1222 | for _, bad := range [][]domain.MergeMode{{}, nil, {"rebase"}} { | |
| 1223 | 1223 | bad := bad | |
| @@ -1229,7 +1229,7 @@ func TestUpdateRepoSettings(t *testing.T) { | |||
| 1229 | 1229 | assert.Equal(t, got.Settings, again.Settings, "a refused change stores nothing") | |
| 1230 | 1230 | ||
| 1231 | 1231 | // repo:admin is needed, like for any other field. | |
| 1232 | - | _, err = r.repos.Update(r.ctx, writer(domain.ScopeRepoRead, domain.ScopeMRMerge), r.repo, ports.UpdateRepoParams{FailingChecksBlock: &off}) | |
| 1232 | + | _, err = r.repos.Update(r.ctx, writer(domain.ScopeRepoRead, domain.ScopeMRMerge), r.repo, ports.UpdateRepoParams{AllowAutoMerge: &off}) | |
| 1233 | 1233 | assert.ErrorIs(t, err, domain.ErrForbidden) | |
| 1234 | 1234 | } | |
| 1235 | 1235 | ||
| @@ -1205,19 +1205,19 @@ func TestUpdateRepoSettings(t *testing.T) { | |||||
| 1205 | 1205 | ||||
| 1206 | modes := []domain.MergeMode{domain.MergeModeSquash, domain.MergeModeSquash} | 1206 | modes := []domain.MergeMode{domain.MergeModeSquash, domain.MergeModeSquash} | ||
| 1207 | off := false | 1207 | off := false | ||
| 1208 | - | got, err := r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{AllowedMergeModes: &modes, FailingChecksBlock: &off}) | 1208 | + | got, err := r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{AllowedMergeModes: &modes, AllowAutoMerge: &off}) |
| 1209 | require.NoError(t, err) | 1209 | require.NoError(t, err) | ||
| 1210 | - | assert.Equal(t, domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, FailingChecksBlock: false, Issues: domain.IssuesOff}, got.Settings) | 1210 | + | assert.Equal(t, domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, AllowAutoMerge: false, Issues: domain.IssuesOff}, got.Settings) |
| 1211 | again, err := r.repos.Get(r.ctx, root, r.repo) | 1211 | again, err := r.repos.Get(r.ctx, root, r.repo) | ||
| 1212 | require.NoError(t, err) | 1212 | require.NoError(t, err) | ||
| 1213 | assert.Equal(t, got.Settings, again.Settings) | 1213 | assert.Equal(t, got.Settings, again.Settings) | ||
| 1214 | 1214 | ||||
| 1215 | // Only the named setting changes. | 1215 | // Only the named setting changes. | ||
| 1216 | on := true | 1216 | on := true | ||
| 1217 | - | got, err = r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{FailingChecksBlock: &on}) | 1217 | + | got, err = r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{AllowAutoMerge: &on}) |
| 1218 | require.NoError(t, err) | 1218 | require.NoError(t, err) | ||
| 1219 | assert.Equal(t, []domain.MergeMode{domain.MergeModeSquash}, got.Settings.AllowedMergeModes) | 1219 | assert.Equal(t, []domain.MergeMode{domain.MergeModeSquash}, got.Settings.AllowedMergeModes) | ||
| 1220 | - | assert.True(t, got.Settings.FailingChecksBlock) | 1220 | + | assert.True(t, got.Settings.AllowAutoMerge) |
| 1221 | 1221 | ||||
| 1222 | for _, bad := range [][]domain.MergeMode{{}, nil, {"rebase"}} { | 1222 | for _, bad := range [][]domain.MergeMode{{}, nil, {"rebase"}} { | ||
| 1223 | bad := bad | 1223 | bad := bad | ||
| @@ -1229,7 +1229,7 @@ func TestUpdateRepoSettings(t *testing.T) { | |||||
| 1229 | assert.Equal(t, got.Settings, again.Settings, "a refused change stores nothing") | 1229 | assert.Equal(t, got.Settings, again.Settings, "a refused change stores nothing") | ||
| 1230 | 1230 | ||||
| 1231 | // repo:admin is needed, like for any other field. | 1231 | // repo:admin is needed, like for any other field. | ||
| 1232 | - | _, err = r.repos.Update(r.ctx, writer(domain.ScopeRepoRead, domain.ScopeMRMerge), r.repo, ports.UpdateRepoParams{FailingChecksBlock: &off}) | 1232 | + | _, err = r.repos.Update(r.ctx, writer(domain.ScopeRepoRead, domain.ScopeMRMerge), r.repo, ports.UpdateRepoParams{AllowAutoMerge: &off}) |
| 1233 | assert.ErrorIs(t, err, domain.ErrForbidden) | 1233 | assert.ErrorIs(t, err, domain.ErrForbidden) | ||
| 1234 | } | 1234 | } | ||
| 1235 | 1235 | ||||
M internal/usecase/repos_write.go +4 -7
| @@ -62,7 +62,7 @@ func (r *Repos) Update(ctx context.Context, p domain.Principal, name domain.Repo | |||
| 62 | 62 | } | |
| 63 | 63 | before := repo | |
| 64 | 64 | if params.Description == nil && params.Topics == nil && params.Visibility == nil && params.DefaultBranch == nil && | |
| 65 | - | params.AllowedMergeModes == nil && params.FailingChecksBlock == nil && params.AllowAutoMerge == nil && params.Issues == nil { | |
| 65 | + | params.AllowedMergeModes == nil && params.AllowAutoMerge == nil && params.Issues == nil { | |
| 66 | 66 | return domain.Repo{}, fmt.Errorf("%w: nothing to update", domain.ErrInvalid) | |
| 67 | 67 | } | |
| 68 | 68 | if params.AllowedMergeModes != nil { | |
| @@ -116,12 +116,12 @@ func (r *Repos) Update(ctx context.Context, p domain.Principal, name domain.Repo | |||
| 116 | 116 | // branch was checked above, so the remaining causes are a vanished | |
| 117 | 117 | // repository or an I/O failure. | |
| 118 | 118 | metaDone := false | |
| 119 | - | if params.Description != nil || params.Topics != nil || params.Visibility != nil || params.AllowedMergeModes != nil || params.FailingChecksBlock != nil || params.AllowAutoMerge != nil || params.Issues != nil { | |
| 119 | + | if params.Description != nil || params.Topics != nil || params.Visibility != nil || params.AllowedMergeModes != nil || params.AllowAutoMerge != nil || params.Issues != nil { | |
| 120 | 120 | update := func() error { | |
| 121 | 121 | repo, err = r.updateMeta(ctx, name, params) | |
| 122 | 122 | return err | |
| 123 | 123 | } | |
| 124 | - | if r.mergeGate != nil && (params.AllowedMergeModes != nil || params.FailingChecksBlock != nil || params.AllowAutoMerge != nil) { | |
| 124 | + | if r.mergeGate != nil && (params.AllowedMergeModes != nil || params.AllowAutoMerge != nil) { | |
| 125 | 125 | err = r.mergeGate.WithMergeLock(ctx, name, update) | |
| 126 | 126 | } else { | |
| 127 | 127 | err = update() | |
| @@ -154,7 +154,7 @@ func (r *Repos) Update(ctx context.Context, p domain.Principal, name domain.Repo | |||
| 154 | 154 | Bool("topics", params.Topics != nil). | |
| 155 | 155 | Bool("visibility", params.Visibility != nil). | |
| 156 | 156 | Bool("default_branch", params.DefaultBranch != nil). | |
| 157 | - | Bool("settings", params.AllowedMergeModes != nil || params.FailingChecksBlock != nil || params.AllowAutoMerge != nil || params.Issues != nil). | |
| 157 | + | Bool("settings", params.AllowedMergeModes != nil || params.AllowAutoMerge != nil || params.Issues != nil). | |
| 158 | 158 | Str("principal", p.Label()).Str("token_id", p.TokenID()). | |
| 159 | 159 | Msg("repository updated") | |
| 160 | 160 | r.bus.Publish(ctx, domain.RepoUpdated{ | |
| @@ -184,9 +184,6 @@ func (r *Repos) updateMeta(ctx context.Context, name domain.RepoName, params por | |||
| 184 | 184 | if params.AllowedMergeModes != nil { | |
| 185 | 185 | repo.Settings.AllowedMergeModes = *params.AllowedMergeModes | |
| 186 | 186 | } | |
| 187 | - | if params.FailingChecksBlock != nil { | |
| 188 | - | repo.Settings.FailingChecksBlock = *params.FailingChecksBlock | |
| 189 | - | } | |
| 190 | 187 | if params.AllowAutoMerge != nil { | |
| 191 | 188 | repo.Settings.AllowAutoMerge = *params.AllowAutoMerge | |
| 192 | 189 | } | |
| @@ -62,7 +62,7 @@ func (r *Repos) Update(ctx context.Context, p domain.Principal, name domain.Repo | |||||
| 62 | } | 62 | } | ||
| 63 | before := repo | 63 | before := repo | ||
| 64 | if params.Description == nil && params.Topics == nil && params.Visibility == nil && params.DefaultBranch == nil && | 64 | if params.Description == nil && params.Topics == nil && params.Visibility == nil && params.DefaultBranch == nil && | ||
| 65 | - | params.AllowedMergeModes == nil && params.FailingChecksBlock == nil && params.AllowAutoMerge == nil && params.Issues == nil { | 65 | + | params.AllowedMergeModes == nil && params.AllowAutoMerge == nil && params.Issues == nil { |
| 66 | return domain.Repo{}, fmt.Errorf("%w: nothing to update", domain.ErrInvalid) | 66 | return domain.Repo{}, fmt.Errorf("%w: nothing to update", domain.ErrInvalid) | ||
| 67 | } | 67 | } | ||
| 68 | if params.AllowedMergeModes != nil { | 68 | if params.AllowedMergeModes != nil { | ||
| @@ -116,12 +116,12 @@ func (r *Repos) Update(ctx context.Context, p domain.Principal, name domain.Repo | |||||
| 116 | // branch was checked above, so the remaining causes are a vanished | 116 | // branch was checked above, so the remaining causes are a vanished | ||
| 117 | // repository or an I/O failure. | 117 | // repository or an I/O failure. | ||
| 118 | metaDone := false | 118 | metaDone := false | ||
| 119 | - | if params.Description != nil || params.Topics != nil || params.Visibility != nil || params.AllowedMergeModes != nil || params.FailingChecksBlock != nil || params.AllowAutoMerge != nil || params.Issues != nil { | 119 | + | if params.Description != nil || params.Topics != nil || params.Visibility != nil || params.AllowedMergeModes != nil || params.AllowAutoMerge != nil || params.Issues != nil { |
| 120 | update := func() error { | 120 | update := func() error { | ||
| 121 | repo, err = r.updateMeta(ctx, name, params) | 121 | repo, err = r.updateMeta(ctx, name, params) | ||
| 122 | return err | 122 | return err | ||
| 123 | } | 123 | } | ||
| 124 | - | if r.mergeGate != nil && (params.AllowedMergeModes != nil || params.FailingChecksBlock != nil || params.AllowAutoMerge != nil) { | 124 | + | if r.mergeGate != nil && (params.AllowedMergeModes != nil || params.AllowAutoMerge != nil) { |
| 125 | err = r.mergeGate.WithMergeLock(ctx, name, update) | 125 | err = r.mergeGate.WithMergeLock(ctx, name, update) | ||
| 126 | } else { | 126 | } else { | ||
| 127 | err = update() | 127 | err = update() | ||
| @@ -154,7 +154,7 @@ func (r *Repos) Update(ctx context.Context, p domain.Principal, name domain.Repo | |||||
| 154 | Bool("topics", params.Topics != nil). | 154 | Bool("topics", params.Topics != nil). | ||
| 155 | Bool("visibility", params.Visibility != nil). | 155 | Bool("visibility", params.Visibility != nil). | ||
| 156 | Bool("default_branch", params.DefaultBranch != nil). | 156 | Bool("default_branch", params.DefaultBranch != nil). | ||
| 157 | - | Bool("settings", params.AllowedMergeModes != nil || params.FailingChecksBlock != nil || params.AllowAutoMerge != nil || params.Issues != nil). | 157 | + | Bool("settings", params.AllowedMergeModes != nil || params.AllowAutoMerge != nil || params.Issues != nil). |
| 158 | Str("principal", p.Label()).Str("token_id", p.TokenID()). | 158 | Str("principal", p.Label()).Str("token_id", p.TokenID()). | ||
| 159 | Msg("repository updated") | 159 | Msg("repository updated") | ||
| 160 | r.bus.Publish(ctx, domain.RepoUpdated{ | 160 | r.bus.Publish(ctx, domain.RepoUpdated{ | ||
| @@ -184,9 +184,6 @@ func (r *Repos) updateMeta(ctx context.Context, name domain.RepoName, params por | |||||
| 184 | if params.AllowedMergeModes != nil { | 184 | if params.AllowedMergeModes != nil { | ||
| 185 | repo.Settings.AllowedMergeModes = *params.AllowedMergeModes | 185 | repo.Settings.AllowedMergeModes = *params.AllowedMergeModes | ||
| 186 | } | 186 | } | ||
| 187 | - | if params.FailingChecksBlock != nil { | |||
| 188 | - | repo.Settings.FailingChecksBlock = *params.FailingChecksBlock | |||
| 189 | - | } | |||
| 190 | if params.AllowAutoMerge != nil { | 187 | if params.AllowAutoMerge != nil { | ||
| 191 | repo.Settings.AllowAutoMerge = *params.AllowAutoMerge | 188 | repo.Settings.AllowAutoMerge = *params.AllowAutoMerge | ||
| 192 | } | 189 | } | ||
M internal/usecase/repos_write_test.go +1 -3
| @@ -224,13 +224,11 @@ func TestUpdate(t *testing.T) { | |||
| 224 | 224 | t.Run("a token limited to branches may not change any setting", func(t *testing.T) { | |
| 225 | 225 | t.Parallel() | |
| 226 | 226 | modes := []domain.MergeMode{domain.MergeModeSquash} | |
| 227 | - | block := true | |
| 228 | 227 | for name, params := range map[string]ports.UpdateRepoParams{ | |
| 229 | 228 | "default branch": {DefaultBranch: ptr("dev")}, | |
| 230 | 229 | "visibility": {Visibility: ptr(domain.VisibilityPublic)}, | |
| 231 | 230 | "description": {Description: ptr("x")}, | |
| 232 | 231 | "merge modes": {AllowedMergeModes: &modes}, | |
| 233 | - | "checks rule": {FailingChecksBlock: &block}, | |
| 234 | 232 | "issues": {Issues: ptr(domain.IssuesOwner)}, | |
| 235 | 233 | } { | |
| 236 | 234 | f := newFixture(t) // no HasBranch, no Update, no SetDefaultBranch, no Publish | |
| @@ -289,7 +287,7 @@ func TestUpdateIssuesMode(t *testing.T) { | |||
| 289 | 287 | f := newFixture(t) | |
| 290 | 288 | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Once() | |
| 291 | 289 | expectRepoMutation(t, f.meta, priv, mock.MatchedBy(func(r domain.Repo) bool { | |
| 292 | - | return r.Settings.Issues == domain.IssuesOwner && r.Settings.FailingChecksBlock | |
| 290 | + | return r.Settings.Issues == domain.IssuesOwner | |
| 293 | 291 | })) | |
| 294 | 292 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | |
| 295 | 293 | repo, err := f.repos.Update(ctx, domain.Root(), n, ports.UpdateRepoParams{Issues: ptr(domain.IssuesOwner)}) | |
| @@ -224,13 +224,11 @@ func TestUpdate(t *testing.T) { | |||||
| 224 | t.Run("a token limited to branches may not change any setting", func(t *testing.T) { | 224 | t.Run("a token limited to branches may not change any setting", func(t *testing.T) { | ||
| 225 | t.Parallel() | 225 | t.Parallel() | ||
| 226 | modes := []domain.MergeMode{domain.MergeModeSquash} | 226 | modes := []domain.MergeMode{domain.MergeModeSquash} | ||
| 227 | - | block := true | |||
| 228 | for name, params := range map[string]ports.UpdateRepoParams{ | 227 | for name, params := range map[string]ports.UpdateRepoParams{ | ||
| 229 | "default branch": {DefaultBranch: ptr("dev")}, | 228 | "default branch": {DefaultBranch: ptr("dev")}, | ||
| 230 | "visibility": {Visibility: ptr(domain.VisibilityPublic)}, | 229 | "visibility": {Visibility: ptr(domain.VisibilityPublic)}, | ||
| 231 | "description": {Description: ptr("x")}, | 230 | "description": {Description: ptr("x")}, | ||
| 232 | "merge modes": {AllowedMergeModes: &modes}, | 231 | "merge modes": {AllowedMergeModes: &modes}, | ||
| 233 | - | "checks rule": {FailingChecksBlock: &block}, | |||
| 234 | "issues": {Issues: ptr(domain.IssuesOwner)}, | 232 | "issues": {Issues: ptr(domain.IssuesOwner)}, | ||
| 235 | } { | 233 | } { | ||
| 236 | f := newFixture(t) // no HasBranch, no Update, no SetDefaultBranch, no Publish | 234 | f := newFixture(t) // no HasBranch, no Update, no SetDefaultBranch, no Publish | ||
| @@ -289,7 +287,7 @@ func TestUpdateIssuesMode(t *testing.T) { | |||||
| 289 | f := newFixture(t) | 287 | f := newFixture(t) | ||
| 290 | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Once() | 288 | f.meta.EXPECT().Get(ctx, n).Return(priv, nil).Once() | ||
| 291 | expectRepoMutation(t, f.meta, priv, mock.MatchedBy(func(r domain.Repo) bool { | 289 | expectRepoMutation(t, f.meta, priv, mock.MatchedBy(func(r domain.Repo) bool { | ||
| 292 | - | return r.Settings.Issues == domain.IssuesOwner && r.Settings.FailingChecksBlock | 290 | + | return r.Settings.Issues == domain.IssuesOwner |
| 293 | })) | 291 | })) | ||
| 294 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | 292 | f.bus.EXPECT().Publish(ctx, mock.Anything).Return().Once() | ||
| 295 | repo, err := f.repos.Update(ctx, domain.Root(), n, ports.UpdateRepoParams{Issues: ptr(domain.IssuesOwner)}) | 293 | repo, err := f.repos.Update(ctx, domain.Root(), n, ports.UpdateRepoParams{Issues: ptr(domain.IssuesOwner)}) | ||
M proto/ermite/storage/merge_request.proto +2 -1
| @@ -22,7 +22,8 @@ enum MergeMode { | |||
| 22 | 22 | message RepoSettings { | |
| 23 | 23 | // Never empty once stored. | |
| 24 | 24 | repeated MergeMode allowed_merge_modes = 1; | |
| 25 | - | bool failing_checks_block = 2; | |
| 25 | + | reserved 2; | |
| 26 | + | reserved "failing_checks_block"; | |
| 26 | 27 | // Who may write issues; unspecified reads as off. | |
| 27 | 28 | IssuesMode issues = 3; | |
| 28 | 29 | // Set on every write since merge commits exist: the modes are the explicit | |
| @@ -22,7 +22,8 @@ enum MergeMode { | |||||
| 22 | message RepoSettings { | 22 | message RepoSettings { | ||
| 23 | // Never empty once stored. | 23 | // Never empty once stored. | ||
| 24 | repeated MergeMode allowed_merge_modes = 1; | 24 | repeated MergeMode allowed_merge_modes = 1; | ||
| 25 | - | bool failing_checks_block = 2; | 25 | + | reserved 2; |
| 26 | + | reserved "failing_checks_block"; | |||
| 26 | // Who may write issues; unspecified reads as off. | 27 | // Who may write issues; unspecified reads as off. | ||
| 27 | IssuesMode issues = 3; | 28 | IssuesMode issues = 3; | ||
| 28 | // Set on every write since merge commits exist: the modes are the explicit | 29 | // Set on every write since merge commits exist: the modes are the explicit | ||