From dabfa7ee9d3153b60239351870508f9cc681b8c2 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 02:23:01 +0000 Subject: [PATCH] fix(actions): align actions paths and add user variables Co-Authored-By: Virgil --- actions.go | 104 ++++++++++++++++++++++++++-- actions_test.go | 181 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+), 5 deletions(-) diff --git a/actions.go b/actions.go index 559fb28..68742f1 100644 --- a/actions.go +++ b/actions.go @@ -40,14 +40,14 @@ func (s *ActionsService) IterRepoSecrets(ctx context.Context, owner, repo string // CreateRepoSecret creates or updates a secret in a repository. // Forgejo expects a PUT with {"data": "secret-value"} body. func (s *ActionsService) CreateRepoSecret(ctx context.Context, owner, repo, name string, data string) error { - path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets/{name}", pathParams("owner", owner, "repo", repo, "name", name)) + path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets/{secretname}", pathParams("owner", owner, "repo", repo, "secretname", name)) body := map[string]string{"data": data} return s.client.Put(ctx, path, body, nil) } // DeleteRepoSecret removes a secret from a repository. func (s *ActionsService) DeleteRepoSecret(ctx context.Context, owner, repo, name string) error { - path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets/{name}", pathParams("owner", owner, "repo", repo, "name", name)) + path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets/{secretname}", pathParams("owner", owner, "repo", repo, "secretname", name)) return s.client.Delete(ctx, path) } @@ -66,20 +66,20 @@ func (s *ActionsService) IterRepoVariables(ctx context.Context, owner, repo stri // CreateRepoVariable creates a new action variable in a repository. // Forgejo expects a POST with {"value": "var-value"} body. func (s *ActionsService) CreateRepoVariable(ctx context.Context, owner, repo, name, value string) error { - path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{name}", pathParams("owner", owner, "repo", repo, "name", name)) + path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{variablename}", pathParams("owner", owner, "repo", repo, "variablename", name)) body := types.CreateVariableOption{Value: value} return s.client.Post(ctx, path, body, nil) } // UpdateRepoVariable updates an existing action variable in a repository. func (s *ActionsService) UpdateRepoVariable(ctx context.Context, owner, repo, name string, opts *types.UpdateVariableOption) error { - path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{name}", pathParams("owner", owner, "repo", repo, "name", name)) + path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{variablename}", pathParams("owner", owner, "repo", repo, "variablename", name)) return s.client.Put(ctx, path, opts, nil) } // DeleteRepoVariable removes an action variable from a repository. func (s *ActionsService) DeleteRepoVariable(ctx context.Context, owner, repo, name string) error { - path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{name}", pathParams("owner", owner, "repo", repo, "name", name)) + path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{variablename}", pathParams("owner", owner, "repo", repo, "variablename", name)) return s.client.Delete(ctx, path) } @@ -107,6 +107,100 @@ func (s *ActionsService) IterOrgVariables(ctx context.Context, org string) iter. return ListIter[types.ActionVariable](ctx, s.client, path, nil) } +// GetOrgVariable returns a single action variable for an organisation. +func (s *ActionsService) GetOrgVariable(ctx context.Context, org, name string) (*types.ActionVariable, error) { + path := ResolvePath("/api/v1/orgs/{org}/actions/variables/{variablename}", pathParams("org", org, "variablename", name)) + var out types.ActionVariable + if err := s.client.Get(ctx, path, &out); err != nil { + return nil, err + } + return &out, nil +} + +// CreateOrgVariable creates a new action variable in an organisation. +func (s *ActionsService) CreateOrgVariable(ctx context.Context, org, name, value string) error { + path := ResolvePath("/api/v1/orgs/{org}/actions/variables/{variablename}", pathParams("org", org, "variablename", name)) + body := types.CreateVariableOption{Value: value} + return s.client.Post(ctx, path, body, nil) +} + +// UpdateOrgVariable updates an existing action variable in an organisation. +func (s *ActionsService) UpdateOrgVariable(ctx context.Context, org, name string, opts *types.UpdateVariableOption) error { + path := ResolvePath("/api/v1/orgs/{org}/actions/variables/{variablename}", pathParams("org", org, "variablename", name)) + return s.client.Put(ctx, path, opts, nil) +} + +// DeleteOrgVariable removes an action variable from an organisation. +func (s *ActionsService) DeleteOrgVariable(ctx context.Context, org, name string) error { + path := ResolvePath("/api/v1/orgs/{org}/actions/variables/{variablename}", pathParams("org", org, "variablename", name)) + return s.client.Delete(ctx, path) +} + +// CreateOrgSecret creates or updates a secret in an organisation. +func (s *ActionsService) CreateOrgSecret(ctx context.Context, org, name, data string) error { + path := ResolvePath("/api/v1/orgs/{org}/actions/secrets/{secretname}", pathParams("org", org, "secretname", name)) + body := map[string]string{"data": data} + return s.client.Put(ctx, path, body, nil) +} + +// DeleteOrgSecret removes a secret from an organisation. +func (s *ActionsService) DeleteOrgSecret(ctx context.Context, org, name string) error { + path := ResolvePath("/api/v1/orgs/{org}/actions/secrets/{secretname}", pathParams("org", org, "secretname", name)) + return s.client.Delete(ctx, path) +} + +// ListUserVariables returns all action variables for the authenticated user. +func (s *ActionsService) ListUserVariables(ctx context.Context) ([]types.ActionVariable, error) { + return ListAll[types.ActionVariable](ctx, s.client, "/api/v1/user/actions/variables", nil) +} + +// IterUserVariables returns an iterator over all action variables for the authenticated user. +func (s *ActionsService) IterUserVariables(ctx context.Context) iter.Seq2[types.ActionVariable, error] { + return ListIter[types.ActionVariable](ctx, s.client, "/api/v1/user/actions/variables", nil) +} + +// GetUserVariable returns a single action variable for the authenticated user. +func (s *ActionsService) GetUserVariable(ctx context.Context, name string) (*types.ActionVariable, error) { + path := ResolvePath("/api/v1/user/actions/variables/{variablename}", pathParams("variablename", name)) + var out types.ActionVariable + if err := s.client.Get(ctx, path, &out); err != nil { + return nil, err + } + return &out, nil +} + +// CreateUserVariable creates a new action variable for the authenticated user. +func (s *ActionsService) CreateUserVariable(ctx context.Context, name, value string) error { + path := ResolvePath("/api/v1/user/actions/variables/{variablename}", pathParams("variablename", name)) + body := types.CreateVariableOption{Value: value} + return s.client.Post(ctx, path, body, nil) +} + +// UpdateUserVariable updates an existing action variable for the authenticated user. +func (s *ActionsService) UpdateUserVariable(ctx context.Context, name string, opts *types.UpdateVariableOption) error { + path := ResolvePath("/api/v1/user/actions/variables/{variablename}", pathParams("variablename", name)) + return s.client.Put(ctx, path, opts, nil) +} + +// DeleteUserVariable removes an action variable for the authenticated user. +func (s *ActionsService) DeleteUserVariable(ctx context.Context, name string) error { + path := ResolvePath("/api/v1/user/actions/variables/{variablename}", pathParams("variablename", name)) + return s.client.Delete(ctx, path) +} + +// CreateUserSecret creates or updates a secret for the authenticated user. +func (s *ActionsService) CreateUserSecret(ctx context.Context, name, data string) error { + path := ResolvePath("/api/v1/user/actions/secrets/{secretname}", pathParams("secretname", name)) + body := map[string]string{"data": data} + return s.client.Put(ctx, path, body, nil) +} + +// DeleteUserSecret removes a secret for the authenticated user. +func (s *ActionsService) DeleteUserSecret(ctx context.Context, name string) error { + path := ResolvePath("/api/v1/user/actions/secrets/{secretname}", pathParams("secretname", name)) + return s.client.Delete(ctx, path) +} + // DispatchWorkflow triggers a workflow run. func (s *ActionsService) DispatchWorkflow(ctx context.Context, owner, repo, workflow string, opts map[string]any) error { path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/workflows/{workflow}/dispatches", pathParams("owner", owner, "repo", repo, "workflow", workflow)) diff --git a/actions_test.go b/actions_test.go index 82a4d09..755ce7b 100644 --- a/actions_test.go +++ b/actions_test.go @@ -248,6 +248,187 @@ func TestActionsService_ListOrgVariables_Good(t *testing.T) { } } +func TestActionsService_GetOrgVariable_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/orgs/lethean/actions/variables/ORG_VAR" { + t.Errorf("wrong path: %s", r.URL.Path) + } + json.NewEncoder(w).Encode(types.ActionVariable{Name: "ORG_VAR", Data: "org-value"}) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + variable, err := f.Actions.GetOrgVariable(context.Background(), "lethean", "ORG_VAR") + if err != nil { + t.Fatal(err) + } + if variable.Name != "ORG_VAR" || variable.Data != "org-value" { + t.Fatalf("unexpected variable: %#v", variable) + } +} + +func TestActionsService_CreateUserVariable_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/api/v1/user/actions/variables/CI_ENV" { + t.Errorf("wrong path: %s", r.URL.Path) + } + var body types.CreateVariableOption + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + if body.Value != "production" { + t.Errorf("got value=%q, want %q", body.Value, "production") + } + w.WriteHeader(http.StatusCreated) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + if err := f.Actions.CreateUserVariable(context.Background(), "CI_ENV", "production"); err != nil { + t.Fatal(err) + } +} + +func TestActionsService_ListUserVariables_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/user/actions/variables" { + t.Errorf("wrong path: %s", r.URL.Path) + } + w.Header().Set("X-Total-Count", "1") + json.NewEncoder(w).Encode([]types.ActionVariable{{Name: "CI_ENV", Data: "production"}}) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + vars, err := f.Actions.ListUserVariables(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(vars) != 1 || vars[0].Name != "CI_ENV" { + t.Fatalf("unexpected variables: %#v", vars) + } +} + +func TestActionsService_GetUserVariable_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/user/actions/variables/CI_ENV" { + t.Errorf("wrong path: %s", r.URL.Path) + } + json.NewEncoder(w).Encode(types.ActionVariable{Name: "CI_ENV", Data: "production"}) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + variable, err := f.Actions.GetUserVariable(context.Background(), "CI_ENV") + if err != nil { + t.Fatal(err) + } + if variable.Name != "CI_ENV" || variable.Data != "production" { + t.Fatalf("unexpected variable: %#v", variable) + } +} + +func TestActionsService_UpdateUserVariable_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + if r.URL.Path != "/api/v1/user/actions/variables/CI_ENV" { + t.Errorf("wrong path: %s", r.URL.Path) + } + var body types.UpdateVariableOption + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + if body.Name != "CI_ENV_NEW" || body.Value != "staging" { + t.Fatalf("unexpected body: %#v", body) + } + w.WriteHeader(http.StatusNoContent) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + if err := f.Actions.UpdateUserVariable(context.Background(), "CI_ENV", &types.UpdateVariableOption{ + Name: "CI_ENV_NEW", + Value: "staging", + }); err != nil { + t.Fatal(err) + } +} + +func TestActionsService_DeleteUserVariable_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Errorf("expected DELETE, got %s", r.Method) + } + if r.URL.Path != "/api/v1/user/actions/variables/OLD_VAR" { + t.Errorf("wrong path: %s", r.URL.Path) + } + w.WriteHeader(http.StatusNoContent) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + if err := f.Actions.DeleteUserVariable(context.Background(), "OLD_VAR"); err != nil { + t.Fatal(err) + } +} + +func TestActionsService_CreateUserSecret_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + if r.URL.Path != "/api/v1/user/actions/secrets/DEPLOY_KEY" { + t.Errorf("wrong path: %s", r.URL.Path) + } + var body map[string]string + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + if body["data"] != "secret-value" { + t.Errorf("got data=%q, want %q", body["data"], "secret-value") + } + w.WriteHeader(http.StatusCreated) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + if err := f.Actions.CreateUserSecret(context.Background(), "DEPLOY_KEY", "secret-value"); err != nil { + t.Fatal(err) + } +} + +func TestActionsService_DeleteUserSecret_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Errorf("expected DELETE, got %s", r.Method) + } + if r.URL.Path != "/api/v1/user/actions/secrets/OLD_KEY" { + t.Errorf("wrong path: %s", r.URL.Path) + } + w.WriteHeader(http.StatusNoContent) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + if err := f.Actions.DeleteUserSecret(context.Background(), "OLD_KEY"); err != nil { + t.Fatal(err) + } +} + func TestActionsService_DispatchWorkflow_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost {