已合并
feat(actions): add repository Actions permission controls (single & org-wide batch) #550
feat(actions): add repository Actions permission controls (single & org-wide batch) #550
已合并
wangwq129创建于 9月17日
共 11 个文件变更+1860-8
@@ -390,6 +390,12 @@ gitcode precommit check
390# 查看流水线运行记录390# 查看流水线运行记录
391gitcode actions run list -R owner/repo --status FAILED391gitcode actions run list -R owner/repo --status FAILED
392 392 
393+# 启用单个仓库的 Actions(交互确认)
394+gitcode actions setting enable -R owner/repo
395+ 
396+# 启用组织下全部仓库的 Actions(非交互)
397+gitcode actions setting enable --org my-org --yes
398+ 
393# 查看流水线运行详情399# 查看流水线运行详情
394gitcode actions run view <run-id> -R owner/repo400gitcode actions run view <run-id> -R owner/repo
395 401 
@@ -237,7 +237,18 @@ func (c *Client) RawRESTToHost(method, host, endpoint string, body io.Reader, he
237 return nil, fmt.Errorf("failed to read response: %w", err)237 return nil, fmt.Errorf("failed to read response: %w", err)
238 }238 }
239 if resp.StatusCode >= 400 {239 if resp.StatusCode >= 400 {
240- return nil, decodeAPIError(respBody, resp.StatusCode, resp.Status)240+ err := decodeAPIError(respBody, resp.StatusCode, resp.Status)
241+ // Endpoints on the web API host authenticate with the browser session
242+ // JWT, so the generic "run gc auth login" advice would send the user
243+ // after a credential that cannot work here. Replace that advice instead
244+ // of appending a second, contradictory instruction.
245+ if resp.StatusCode == http.StatusUnauthorized && host == WebAPIHost {
246+ if apiErr, ok := err.(*APIError); ok {
247+ apiErr.Guidance = fmt.Sprintf("The %s web API rejected the credential. This host authenticates with the gitcode.com web session JWT, not a classic personal access token: refresh gitcode.com, run copy(localStorage.getItem('access_token')) in the browser Console, and pass the value with --with-token.", host)
248+ return nil, apiErr
249+ }
250+ }
251+ return nil, err
241 }252 }
242 253 
243 return &RawResponse{254 return &RawResponse{
@@ -506,6 +517,12 @@ type APIError struct {
506 ErrorMessage string `json:"error_message"`517 ErrorMessage string `json:"error_message"`
507 ErrorName string `json:"error"`518 ErrorName string `json:"error"`
508 ErrorCodeName string `json:"error_code_name"`519 ErrorCodeName string `json:"error_code_name"`
520+ 
521+ // Guidance replaces the status-specific guidance that Error appends. A
522+ // caller sets it when the generic advice would be wrong for the endpoint,
523+ // for example the personal-access-token hint on a 401 from a host that
524+ // authenticates with a browser session JWT.
525+ Guidance string `json:"-"`
509}526}
510 527 
511func (e *APIError) Error() string {528func (e *APIError) Error() string {
@@ -518,6 +535,12 @@ func (e *APIError) Error() string {
518 msg = "unknown error"535 msg = "unknown error"
519 }536 }
520 537 
538+ // Caller-supplied guidance replaces the generic status guidance so the two
539+ // never contradict each other.
540+ if e.Guidance != "" {
541+ return fmt.Sprintf("HTTP %d: %s\n\n%s", e.StatusCode, msg, e.Guidance)
542+ }
543+ 
521 // Add actionable guidance for permission errors544 // Add actionable guidance for permission errors
522 if e.StatusCode == 403 {545 if e.StatusCode == 403 {
523 return fmt.Sprintf("HTTP %d: %s\n\nYou don't have permission for this action. Possible reasons:\n- You are not the owner or a project maintainer\n- The project requires specific permissions\n\nFor permission details, check the project settings or contact a maintainer.", e.StatusCode, msg)546 return fmt.Sprintf("HTTP %d: %s\n\nYou don't have permission for this action. Possible reasons:\n- You are not the owner or a project maintainer\n- The project requires specific permissions\n\nFor permission details, check the project settings or contact a maintainer.", e.StatusCode, msg)
@@ -2,6 +2,7 @@ package api
2 2 
3import (3import (
4 "encoding/json"4 "encoding/json"
5+ "errors"
5 "io"6 "io"
6 "net/http"7 "net/http"
7 "net/url"8 "net/url"
@@ -354,6 +355,52 @@ func TestRawRESTToHostError(t *testing.T) {
354 }355 }
355}356}
356 357 
358+// TestRawRESTToHostWebAPIUnauthorizedReplacesPATGuidance asserts that a 401
359+// from the web API host returns the session-JWT guidance instead of the generic
360+// personal-access-token advice, so the two cannot contradict each other.
361+func TestRawRESTToHostWebAPIUnauthorizedReplacesPATGuidance(t *testing.T) {
362+ client := newAuthTestClient(func(req *http.Request) (*http.Response, error) {
363+ return authTestResponse(http.StatusUnauthorized, `{"message":"unauthorized"}`), nil
364+ })
365+ client.SetToken("test-token", "test")
366+ 
367+ _, err := client.RawRESTToHost("GET", WebAPIHost, "/api/v2/test", nil, nil)
368+ if err == nil {
369+ t.Fatal("expected error for 401")
370+ }
371+ var apiErr *APIError
372+ if !errors.As(err, &apiErr) || apiErr.StatusCode != http.StatusUnauthorized {
373+ t.Fatalf("error = %v, want an *APIError carrying status 401", err)
374+ }
375+ message := err.Error()
376+ if !strings.Contains(message, "web API rejected the credential") || !strings.Contains(message, "web session JWT") {
377+ t.Fatalf("message = %q, want host-specific session JWT guidance", message)
378+ }
379+ if strings.Contains(message, "gc auth login") {
380+ t.Fatalf("message = %q, must not keep the generic personal access token advice", message)
381+ }
382+}
383+ 
384+// TestRawRESTToHostDefaultHostKeepsPATGuidance pins the other side of the
385+// branch: the default REST host still gets the generic authentication advice.
386+func TestRawRESTToHostDefaultHostKeepsPATGuidance(t *testing.T) {
387+ client := newAuthTestClient(func(req *http.Request) (*http.Response, error) {
388+ return authTestResponse(http.StatusUnauthorized, `{"message":"unauthorized"}`), nil
389+ })
390+ client.SetToken("test-token", "test")
391+ 
392+ _, err := client.RawRESTToHost("GET", DefaultHost, "/api/v2/test", nil, nil)
393+ if err == nil {
394+ t.Fatal("expected error for 401")
395+ }
396+ if !strings.Contains(err.Error(), "gc auth login") {
397+ t.Fatalf("message = %q, want the generic authentication advice", err.Error())
398+ }
399+ if strings.Contains(err.Error(), "web session JWT") {
400+ t.Fatalf("message = %q, must not claim the default host needs a web session JWT", err.Error())
401+ }
402+}
403+ 
357func TestRawRESTToHostCustomHeaders(t *testing.T) {404func TestRawRESTToHostCustomHeaders(t *testing.T) {
358 var gotAccept string405 var gotAccept string
359 client := newAuthTestClient(func(req *http.Request) (*http.Response, error) {406 client := newAuthTestClient(func(req *http.Request) (*http.Response, error) {
@@ -0,0 +1,75 @@
1+package api
2+ 
3+import (
4+ "bytes"
5+ "encoding/json"
6+ "fmt"
7+ "net/url"
8+)
9+ 
10+// ActionsSetting represents the repository-level Actions permission settings.
11+// Pointer fields preserve false values and allow fields absent from a response
12+// to remain absent from an update request.
13+type ActionsSetting struct {
14+ ActionEnabled *bool `json:"action_enabled,omitempty"`
15+ BlockAllNewPipelines *bool `json:"block_all_new_pipelines,omitempty"`
16+ BlockCrossRepoPRTriggers *bool `json:"block_cross_repo_pr_triggers,omitempty"`
17+}
18+ 
19+// GetActionsSetting gets the repository-level Actions permission settings.
20+//
21+// It calls GET /api/v2/projects/{project_id}/actions/setting on the GitCode
22+// web API host. The endpoint currently returns either the setting object or an
23+// object envelope containing it in data.
24+func GetActionsSetting(client *Client, projectID string) (*ActionsSetting, error) {
25+ endpoint := actionsSettingEndpoint(projectID)
26+ resp, err := client.RawRESTToHost("GET", WebAPIHost, endpoint, nil, webAPIHeaders())
27+ if err != nil {
28+ return nil, err
29+ }
30+ 
31+ settingBody, err := unwrapActionsSetting(resp.Body)
32+ if err != nil {
33+ return nil, err
34+ }
35+ 
36+ var setting ActionsSetting
37+ if err := json.Unmarshal(settingBody, &setting); err != nil {
38+ return nil, fmt.Errorf("failed to parse actions setting response: %w", err)
39+ }
40+ return &setting, nil
41+}
42+ 
43+// UpdateActionsSetting updates the repository-level Actions permission
44+// settings without sending a project_id in the request body.
45+//
46+// The PUT body is built only from the modelled fields above. The endpoint
47+// replaces the whole setting object, so a field the server returns but this
48+// struct does not model is not echoed back and will fall back to the server
49+// default. That is a known trade-off: the alternative would be to round-trip
50+// unknown fields verbatim, which risks sending back stale or read-only values.
51+func UpdateActionsSetting(client *Client, projectID string, setting *ActionsSetting) error {
52+ body, err := json.Marshal(setting)
53+ if err != nil {
54+ return fmt.Errorf("failed to marshal actions setting: %w", err)
55+ }
56+ _, err = client.RawRESTToHost("PUT", WebAPIHost, actionsSettingEndpoint(projectID), bytes.NewReader(body), webAPIHeaders())
57+ return err
58+}
59+ 
60+func actionsSettingEndpoint(projectID string) string {
61+ return "/api/v2/projects/" + url.PathEscape(projectID) + "/actions/setting"
62+}
63+ 
64+func unwrapActionsSetting(body []byte) ([]byte, error) {
65+ var envelope struct {
66+ Data json.RawMessage `json:"data"`
67+ }
68+ if err := json.Unmarshal(body, &envelope); err != nil {
69+ return nil, fmt.Errorf("failed to parse actions setting response: %w", err)
70+ }
71+ if len(envelope.Data) > 0 && string(envelope.Data) != "null" {
72+ return envelope.Data, nil
73+ }
74+ return body, nil
75+}
@@ -0,0 +1,122 @@
1+package api
2+ 
3+import (
4+ "encoding/json"
5+ "io"
6+ "net/http"
7+ "testing"
8+ 
9+ "gitcode.com/gitcode-cli/cli/pkg/testutil"
10+)
11+ 
12+func TestGetActionsSettingUnwrapsData(t *testing.T) {
13+ var gotPath string
14+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15+ gotPath = r.URL.EscapedPath()
16+ if got := r.Header.Get("Referer"); got != "https://gitcode.com/" {
17+ t.Errorf("Referer = %q, want %q", got, "https://gitcode.com/")
18+ }
19+ _, _ = w.Write([]byte(`{"data":{"action_enabled":true,"block_all_new_pipelines":false,"block_cross_repo_pr_triggers":true}}`))
20+ })
21+ client := NewClientFromHTTP(testutil.NewTestHTTPClient(handler))
22+ 
23+ setting, err := GetActionsSetting(client, "12345")
24+ if err != nil {
25+ t.Fatalf("GetActionsSetting() error = %v", err)
26+ }
27+ if gotPath != "/api/v2/projects/12345/actions/setting" {
28+ t.Fatalf("path = %q", gotPath)
29+ }
30+ if setting.ActionEnabled == nil || !*setting.ActionEnabled {
31+ t.Fatalf("ActionEnabled = %#v, want true", setting.ActionEnabled)
32+ }
33+ if setting.BlockAllNewPipelines == nil || *setting.BlockAllNewPipelines {
34+ t.Fatalf("BlockAllNewPipelines = %#v, want false", setting.BlockAllNewPipelines)
35+ }
36+ if setting.BlockCrossRepoPRTriggers == nil || !*setting.BlockCrossRepoPRTriggers {
37+ t.Fatalf("BlockCrossRepoPRTriggers = %#v, want true", setting.BlockCrossRepoPRTriggers)
38+ }
39+}
40+ 
41+func TestGetActionsSettingAcceptsFlatResponse(t *testing.T) {
42+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43+ _, _ = w.Write([]byte(`{"action_enabled":false}`))
44+ })
45+ client := NewClientFromHTTP(testutil.NewTestHTTPClient(handler))
46+ 
47+ setting, err := GetActionsSetting(client, "12345")
48+ if err != nil {
49+ t.Fatalf("GetActionsSetting() error = %v", err)
50+ }
51+ if setting.ActionEnabled == nil || *setting.ActionEnabled {
52+ t.Fatalf("ActionEnabled = %#v, want false", setting.ActionEnabled)
53+ }
54+ if setting.BlockAllNewPipelines != nil || setting.BlockCrossRepoPRTriggers != nil {
55+ t.Fatalf("unexpected absent fields: %#v", setting)
56+ }
57+}
58+ 
59+func TestUpdateActionsSettingPreservesReadFields(t *testing.T) {
60+ var gotPath string
61+ var gotMethod string
62+ var gotBody map[string]any
63+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
64+ gotPath = r.URL.EscapedPath()
65+ gotMethod = r.Method
66+ body, err := io.ReadAll(r.Body)
67+ if err != nil {
68+ t.Errorf("read body: %v", err)
69+ return
70+ }
71+ if err := json.Unmarshal(body, &gotBody); err != nil {
72+ t.Errorf("decode body: %v", err)
73+ }
74+ if got := r.Header.Get("Referer"); got != "https://gitcode.com/" {
75+ t.Errorf("Referer = %q, want %q", got, "https://gitcode.com/")
76+ }
77+ w.WriteHeader(http.StatusNoContent)
78+ })
79+ client := NewClientFromHTTP(testutil.NewTestHTTPClient(handler))
80+ client.SetToken("test-token", "env")
81+ 
82+ setting := &ActionsSetting{
83+ ActionEnabled: boolPointer(false),
84+ BlockAllNewPipelines: boolPointer(true),
85+ BlockCrossRepoPRTriggers: boolPointer(false),
86+ }
87+ if err := UpdateActionsSetting(client, "12345", setting); err != nil {
88+ t.Fatalf("UpdateActionsSetting() error = %v", err)
89+ }
90+ if gotMethod != http.MethodPut || gotPath != "/api/v2/projects/12345/actions/setting" {
91+ t.Fatalf("request = %s %s", gotMethod, gotPath)
92+ }
93+ if len(gotBody) != 3 {
94+ t.Fatalf("body fields = %#v, want only three setting fields", gotBody)
95+ }
96+ if gotBody["action_enabled"] != false || gotBody["block_all_new_pipelines"] != true || gotBody["block_cross_repo_pr_triggers"] != false {
97+ t.Fatalf("body = %#v", gotBody)
98+ }
99+ if _, ok := gotBody["project_id"]; ok {
100+ t.Fatalf("body unexpectedly contains project_id: %#v", gotBody)
101+ }
102+}
103+ 
104+func TestActionsSettingEndpointEscapesProjectID(t *testing.T) {
105+ var gotPath string
106+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
107+ gotPath = r.URL.EscapedPath()
108+ _, _ = w.Write([]byte(`{"action_enabled":true}`))
109+ })
110+ client := NewClientFromHTTP(testutil.NewTestHTTPClient(handler))
111+ 
112+ if _, err := GetActionsSetting(client, "owner/repo"); err != nil {
113+ t.Fatalf("GetActionsSetting() error = %v", err)
114+ }
115+ if gotPath != "/api/v2/projects/owner%2Frepo/actions/setting" {
116+ t.Fatalf("path = %q", gotPath)
117+ }
118+}
119+ 
120+func boolPointer(value bool) *bool {
121+ return &value
122+}
Mdocs/COMMANDS.md+50-1文件内容审核中,请稍后刷新重试
Mgo.mod+1-0
@@ -7,6 +7,7 @@ require (
7 github.com/rogpeppe/go-internal v1.13.17 github.com/rogpeppe/go-internal v1.13.1
8 github.com/spf13/cobra v1.8.08 github.com/spf13/cobra v1.8.0
9 github.com/spf13/pflag v1.0.59 github.com/spf13/pflag v1.0.5
10+ golang.org/x/term v0.25.0
10 golang.org/x/text v0.21.011 golang.org/x/text v0.21.0
11 gopkg.in/yaml.v3 v3.0.112 gopkg.in/yaml.v3 v3.0.1
12)13)
Mgo.sum+2-0
@@ -12,6 +12,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
12github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=12github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
13golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=13golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
14golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=14golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
15+golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
16+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
15golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=17golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
16golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=18golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
17golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=19golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
@@ -12,6 +12,7 @@ import (
12 runnercmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/runner"12 runnercmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/runner"
13 runnergroupcmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/runner-group"13 runnergroupcmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/runner-group"
14 runnersetcmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/runner-set"14 runnersetcmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/runner-set"
15+ settingcmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/setting"
15 workflowcmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/workflow"16 workflowcmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/workflow"
16 yamlcmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/yaml"17 yamlcmd "gitcode.com/gitcode-cli/cli/pkg/cmd/actions/yaml"
17 cmdutil "gitcode.com/gitcode-cli/cli/pkg/cmdutil"18 cmdutil "gitcode.com/gitcode-cli/cli/pkg/cmdutil"
@@ -21,14 +22,13 @@ import (
21func NewCmdActions(f *cmdutil.Factory) *cobra.Command {22func NewCmdActions(f *cmdutil.Factory) *cobra.Command {
22 cmd := &cobra.Command{23 cmd := &cobra.Command{
23 Use: "actions <command>",24 Use: "actions <command>",
24- Short: "Manage GitCode Actions (pipeline runs and workflow jobs)",25+ Short: "Manage GitCode Actions",
25 Long: heredoc.Doc(`26 Long: heredoc.Doc(`
26- Work with GitCode Actions: inspect pipeline runs, workflow jobs and27+ Work with GitCode Actions: inspect pipeline runs and workflow jobs,
27- job logs.28+ or enable and disable repository Actions.
28 29 
29- GitCode Actions exposes pipeline run records and workflow jobs through30+ Changing repository Actions permissions is a dangerous operation and
30- the Actions v8 API. This command group provides read-only inspection31+ requires confirmation unless --yes is provided.
31- of CI run status.
32 `),32 `),
33 Example: heredoc.Doc(`33 Example: heredoc.Doc(`
34 # List recent pipeline runs34 # List recent pipeline runs
@@ -55,6 +55,7 @@ func NewCmdActions(f *cmdutil.Factory) *cobra.Command {
55 cmd.AddCommand(runnergroupcmd.NewCmdRunnerGroup(f))55 cmd.AddCommand(runnergroupcmd.NewCmdRunnerGroup(f))
56 cmd.AddCommand(runnercmd.NewCmdRunner(f))56 cmd.AddCommand(runnercmd.NewCmdRunner(f))
57 cmd.AddCommand(runnersetcmd.NewCmdRunnerSet(f))57 cmd.AddCommand(runnersetcmd.NewCmdRunnerSet(f))
58+ cmd.AddCommand(settingcmd.NewCmdSetting(f, nil))
58 cmd.AddCommand(yamlcmd.NewCmdYaml(f))59 cmd.AddCommand(yamlcmd.NewCmdYaml(f))
59 cmd.AddCommand(workflowcmd.NewCmdWorkflow(f))60 cmd.AddCommand(workflowcmd.NewCmdWorkflow(f))
60 61 
@@ -0,0 +1,669 @@
1+// Package setting implements the actions setting command.
2+package setting
3+ 
4+import (
5+ "encoding/base64"
6+ "encoding/json"
7+ "fmt"
8+ "io"
9+ "math"
10+ "net/http"
11+ "os"
12+ "strconv"
13+ "strings"
14+ "time"
15+ 
16+ "github.com/MakeNowJust/heredoc/v2"
17+ "github.com/spf13/cobra"
18+ "golang.org/x/term"
19+ 
20+ "gitcode.com/gitcode-cli/cli/api"
21+ cmdutil "gitcode.com/gitcode-cli/cli/pkg/cmdutil"
22+ "gitcode.com/gitcode-cli/cli/pkg/iostreams"
23+)
24+ 
25+const organizationRepoPageSize = 100
26+ 
27+// organizationRepoMaxPages bounds organization listing so a server that keeps
28+// returning non-empty pages cannot make the command loop forever.
29+const organizationRepoMaxPages = 1000
30+ 
31+// Per-repository result statuses reported in the summary output.
32+const (
33+ statusUnchanged = "unchanged"
34+ statusUpdated = "updated"
35+ statusFailed = "failed"
36+)
37+ 
38+const actionsSettingRiskMessage = "This command calls a GitCode Web API endpoint that is not documented in the official GitCode API reference. Use of this command is at your own risk."
39+ 
40+type terminalPasswordReader func(int) ([]byte, error)
41+ 
42+// webJWTHelpText explains why the command needs the web session JWT and
43+// where to obtain it. It is shown before the interactive paste prompt.
44+const webJWTHelpText = `The Actions settings endpoint requires a GitCode web-session JWT;
45+a classic personal access token cannot be used for this request.
46+To obtain the JWT:
47+ 1. Open https://gitcode.com and sign in with an account that can manage
48+ the target repository.
49+ 2. Click the account avatar in the top-right corner.
50+ 3. Press F12 and switch to the Console tab.
51+ 4. Run:
52+ copy(localStorage.getItem('access_token'))
53+ 5. Paste the copied value at the next prompt and press Enter.
54+The JWT is entered without terminal echo and held in memory only for this
55+command. Do not share it or save it to a file.`
56+ 
57+// SettingOptions configures the actions setting command.
58+type SettingOptions struct {
59+ IO *iostreams.IOStreams
60+ HttpClient func() (*http.Client, error)
61+ BaseRepo func() (string, error)
62+ 
63+ Repository string
64+ Org string
65+ Action string
66+ WithToken bool
67+ Yes bool
68+ JSON bool
69+ 
70+ readPassword terminalPasswordReader
71+}
72+ 
73+// SettingResult represents the result for one repository.
74+type SettingResult struct {
75+ Repository string `json:"repository"`
76+ ProjectID string `json:"project_id"`
77+ PreviousActionEnabled bool `json:"previous_action_enabled"`
78+ ActionEnabled bool `json:"action_enabled"`
79+ Status string `json:"status"`
80+}
81+ 
82+// SettingSummary represents the result of an Actions permission update.
83+type SettingSummary struct {
84+ Action string `json:"action"`
85+ ActionEnabled bool `json:"action_enabled"`
86+ Organization string `json:"organization,omitempty"`
87+ Repositories []SettingResult `json:"repositories"`
88+}
89+ 
90+type repositoryTarget struct {
91+ Name string
92+ ProjectID string
93+}
94+ 
95+type repositoryState struct {
96+ target repositoryTarget
97+ setting *api.ActionsSetting
98+ previous bool
99+ status string
100+ err error