Merge pull request #10139 from ChandranshuRao14/feat/repo-edit-security-analysis

Feat: Allow setting security_and_analysis settings in gh repo edit
This commit is contained in:
Tyler McGoffin 2025-01-03 16:22:10 -08:00 committed by GitHub
commit 2ec473ff2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 233 additions and 16 deletions

View file

@ -66,22 +66,27 @@ type EditOptions struct {
}
type EditRepositoryInput struct {
AllowForking *bool `json:"allow_forking,omitempty"`
AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"`
DefaultBranch *string `json:"default_branch,omitempty"`
DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"`
Description *string `json:"description,omitempty"`
EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"`
EnableIssues *bool `json:"has_issues,omitempty"`
EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"`
EnableProjects *bool `json:"has_projects,omitempty"`
EnableDiscussions *bool `json:"has_discussions,omitempty"`
EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"`
EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"`
EnableWiki *bool `json:"has_wiki,omitempty"`
Homepage *string `json:"homepage,omitempty"`
IsTemplate *bool `json:"is_template,omitempty"`
Visibility *string `json:"visibility,omitempty"`
enableAdvancedSecurity *bool
enableSecretScanning *bool
enableSecretScanningPushProtection *bool
AllowForking *bool `json:"allow_forking,omitempty"`
AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"`
DefaultBranch *string `json:"default_branch,omitempty"`
DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"`
Description *string `json:"description,omitempty"`
EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"`
EnableIssues *bool `json:"has_issues,omitempty"`
EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"`
EnableProjects *bool `json:"has_projects,omitempty"`
EnableDiscussions *bool `json:"has_discussions,omitempty"`
EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"`
EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"`
EnableWiki *bool `json:"has_wiki,omitempty"`
Homepage *string `json:"homepage,omitempty"`
IsTemplate *bool `json:"is_template,omitempty"`
SecurityAndAnalysis *SecurityAndAnalysisInput `json:"security_and_analysis,omitempty"`
Visibility *string `json:"visibility,omitempty"`
}
func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobra.Command {
@ -157,6 +162,10 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
return cmdutil.FlagErrorf("use of --visibility flag requires --accept-visibility-change-consequences flag")
}
if hasSecurityEdits(opts.Edits) {
opts.Edits.SecurityAndAnalysis = transformSecurityAndAnalysisOpts(opts)
}
if runF != nil {
return runF(opts)
}
@ -177,6 +186,9 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableSquashMerge, "enable-squash-merge", "", "Enable merging pull requests via squashed commit")
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableRebaseMerge, "enable-rebase-merge", "", "Enable merging pull requests via rebase")
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableAutoMerge, "enable-auto-merge", "", "Enable auto-merge functionality")
cmdutil.NilBoolFlag(cmd, &opts.Edits.enableAdvancedSecurity, "enable-advanced-security", "", "Enable advanced security in the repository")
cmdutil.NilBoolFlag(cmd, &opts.Edits.enableSecretScanning, "enable-secret-scanning", "", "Enable secret scanning in the repository")
cmdutil.NilBoolFlag(cmd, &opts.Edits.enableSecretScanningPushProtection, "enable-secret-scanning-push-protection", "", "Enable secret scanning push protection in the repository. Secret scanning must be enabled first")
cmdutil.NilBoolFlag(cmd, &opts.Edits.DeleteBranchOnMerge, "delete-branch-on-merge", "", "Delete head branch when pull requests are merged")
cmdutil.NilBoolFlag(cmd, &opts.Edits.AllowForking, "allow-forking", "", "Allow forking of an organization repository")
cmdutil.NilBoolFlag(cmd, &opts.Edits.AllowUpdateBranch, "allow-update-branch", "", "Allow a pull request head branch that is behind its base branch to be updated")
@ -240,6 +252,17 @@ func editRun(ctx context.Context, opts *EditOptions) error {
}
}
if opts.Edits.SecurityAndAnalysis != nil {
apiClient := api.NewClientFromHTTP(opts.HTTPClient)
repo, err := api.FetchRepository(apiClient, opts.Repository, []string{"viewerCanAdminister"})
if err != nil {
return err
}
if !repo.ViewerCanAdminister {
return fmt.Errorf("you do not have sufficient permissions to edit repository security and analysis features")
}
}
apiPath := fmt.Sprintf("repos/%s/%s", repo.RepoOwner(), repo.RepoName())
body := &bytes.Buffer{}
@ -560,3 +583,49 @@ func isIncluded(value string, opts []string) bool {
}
return false
}
func boolToStatus(status bool) *string {
var result string
if status {
result = "enabled"
} else {
result = "disabled"
}
return &result
}
func hasSecurityEdits(edits EditRepositoryInput) bool {
return edits.enableAdvancedSecurity != nil || edits.enableSecretScanning != nil || edits.enableSecretScanningPushProtection != nil
}
type SecurityAndAnalysisInput struct {
EnableAdvancedSecurity *SecurityAndAnalysisStatus `json:"advanced_security,omitempty"`
EnableSecretScanning *SecurityAndAnalysisStatus `json:"secret_scanning,omitempty"`
EnableSecretScanningPushProtection *SecurityAndAnalysisStatus `json:"secret_scanning_push_protection,omitempty"`
}
type SecurityAndAnalysisStatus struct {
Status *string `json:"status,omitempty"`
}
// Transform security and analysis parameters to properly serialize EditRepositoryInput
// See API Docs: https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#update-a-repository
func transformSecurityAndAnalysisOpts(opts *EditOptions) *SecurityAndAnalysisInput {
securityOptions := &SecurityAndAnalysisInput{}
if opts.Edits.enableAdvancedSecurity != nil {
securityOptions.EnableAdvancedSecurity = &SecurityAndAnalysisStatus{
Status: boolToStatus(*opts.Edits.enableAdvancedSecurity),
}
}
if opts.Edits.enableSecretScanning != nil {
securityOptions.EnableSecretScanning = &SecurityAndAnalysisStatus{
Status: boolToStatus(*opts.Edits.enableSecretScanning),
}
}
if opts.Edits.enableSecretScanningPushProtection != nil {
securityOptions.EnableSecretScanningPushProtection = &SecurityAndAnalysisStatus{
Status: boolToStatus(*opts.Edits.enableSecretScanningPushProtection),
}
}
return securityOptions
}

View file

@ -201,6 +201,65 @@ func Test_editRun(t *testing.T) {
}))
},
},
{
name: "enable/disable security and analysis settings",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{
SecurityAndAnalysis: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanning: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
},
},
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`{"data": { "repository": { "viewerCanAdminister": true } } }`))
r.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO"),
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
assert.Equal(t, 1, len(payload))
securityAndAnalysis := payload["security_and_analysis"].(map[string]interface{})
assert.Equal(t, "enabled", securityAndAnalysis["advanced_security"].(map[string]interface{})["status"])
assert.Equal(t, "enabled", securityAndAnalysis["secret_scanning"].(map[string]interface{})["status"])
assert.Equal(t, "disabled", securityAndAnalysis["secret_scanning_push_protection"].(map[string]interface{})["status"])
}))
},
},
{
name: "does not have sufficient permissions for security edits",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{
SecurityAndAnalysis: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanning: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
},
},
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`{"data": { "repository": { "viewerCanAdminister": false } } }`))
},
wantsErr: "you do not have sufficient permissions to edit repository security and analysis features",
},
}
for _, tt := range tests {
@ -670,6 +729,95 @@ func Test_editRun_interactive(t *testing.T) {
}
}
func Test_transformSecurityAndAnalysisOpts(t *testing.T) {
tests := []struct {
name string
opts EditOptions
want *SecurityAndAnalysisInput
}{
{
name: "Enable all security and analysis settings",
opts: EditOptions{
Edits: EditRepositoryInput{
enableAdvancedSecurity: bp(true),
enableSecretScanning: bp(true),
enableSecretScanningPushProtection: bp(true),
},
},
want: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanning: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
},
},
{
name: "Disable all security and analysis settings",
opts: EditOptions{
Edits: EditRepositoryInput{
enableAdvancedSecurity: bp(false),
enableSecretScanning: bp(false),
enableSecretScanningPushProtection: bp(false),
},
},
want: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
EnableSecretScanning: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
},
},
{
name: "Enable only advanced security",
opts: EditOptions{
Edits: EditRepositoryInput{
enableAdvancedSecurity: bp(true),
},
},
want: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanning: nil,
EnableSecretScanningPushProtection: nil,
},
},
{
name: "Disable only secret scanning",
opts: EditOptions{
Edits: EditRepositoryInput{
enableSecretScanning: bp(false),
},
},
want: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: nil,
EnableSecretScanning: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
EnableSecretScanningPushProtection: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := &tt.opts
transformed := transformSecurityAndAnalysisOpts(opts)
assert.Equal(t, tt.want, transformed)
})
}
}
func sp(v string) *string {
return &v
}