mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
secret set and variable set read the value from -b/--body, -F/--body-file or standard input when it is not a terminal (one trailing line break removed), and otherwise prompt for it; --env-file sets every NAME=value of a dotenv file. --value and --value-file keep working. The scope flags are -o/--org and --account on each command, and secret list no longer offers --account, which Gitea cannot list. Commands report what they did, naming the scope and the name Gitea stores: "Set Actions secret LOWER_NAME for alice/x (names are upper-cased)"; --json returns the stored name and omits an unknown creation time. Deleting asks "Delete secret X from alice/x?". Lists show NAME, CREATED and DESCRIPTION for secrets and NAME, VALUE and DESCRIPTION for variables; variable view shows a record and the new variable get prints the raw value. A missing secret or variable is reported by name and scope. The global --user selects the account tea acts as, unlike gh's --user, so secret and variable add a note on a terminal pointing to --account when it is given, as run list does for --actor. Help explains the scopes. Signed-off-by: bircni <bircni@icloud.com>
252 lines
9.5 KiB
Go
252 lines
9.5 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
gitea "gitea.dev/sdk"
|
|
"gitea.dev/tea/internal/transport"
|
|
)
|
|
|
|
// ActionScope identifies exactly one repository, organization, or account scope.
|
|
type ActionScope struct {
|
|
Repository, Organization string
|
|
Account bool
|
|
}
|
|
|
|
// String names the scope for people: "alice/project", "organization acme" or "your account".
|
|
func (s ActionScope) String() string {
|
|
switch {
|
|
case s.Account:
|
|
return "your account"
|
|
case s.Organization != "":
|
|
return "organization " + s.Organization
|
|
}
|
|
return s.Repository
|
|
}
|
|
|
|
// ActionSecret describes a secret without containing its value. CreatedAt is empty for a secret
|
|
// that was just set, as Gitea does not return it.
|
|
type ActionSecret struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
CreatedAt string `json:"created_at,omitempty"`
|
|
}
|
|
|
|
// ActionValueName is the name Gitea stores a secret or variable under: names are upper-cased.
|
|
func ActionValueName(name string) string { return strings.ToUpper(name) }
|
|
|
|
func secretFrom(secret *gitea.Secret) ActionSecret {
|
|
return ActionSecret{Name: secret.Name, Description: secret.Description, CreatedAt: timestamp(secret.Created)}
|
|
}
|
|
|
|
// ActionVariable contains one non-secret workflow variable.
|
|
type ActionVariable struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func variableFrom(variable *gitea.ActionsVariable) ActionVariable {
|
|
return ActionVariable{Name: variable.Name, Value: variable.Data, Description: variable.Description}
|
|
}
|
|
|
|
// ActionSecrets manages scoped Actions secrets without returning secret data.
|
|
type ActionSecrets struct {
|
|
Client *transport.Client
|
|
Scope ActionScope
|
|
}
|
|
|
|
// List returns secret metadata only.
|
|
func (s ActionSecrets) List(ctx context.Context, limit, page int) ([]ActionSecret, error) {
|
|
window, err := s.ListPage(ctx, limit, page)
|
|
return window.Items, err
|
|
}
|
|
|
|
// ListPage is List with what the server reports about the other pages.
|
|
func (s ActionSecrets) ListPage(ctx context.Context, limit, page int) (Page[ActionSecret], error) {
|
|
if s.Scope.Account {
|
|
return Page[ActionSecret]{}, &CapabilityError{Feature: "account secret listing", Reason: "Gitea exposes account secret set/delete operations only"}
|
|
}
|
|
window, err := listPage(limit, page, func(limit, page int) ([]*gitea.Secret, http.Header, error) {
|
|
options := listOptions(limit, page)
|
|
secrets, resp, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Secret, *gitea.Response, error) {
|
|
if s.Scope.Organization != "" {
|
|
return c.Actions.ListOrgSecrets(ctx, s.Scope.Organization, gitea.ListOrgActionsSecretOption{ListOptions: options})
|
|
}
|
|
owner, repo, err := splitRepository(s.Scope.Repository)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return c.Actions.ListRepoSecrets(ctx, owner, repo, gitea.ListRepoActionsSecretOption{ListOptions: options})
|
|
})
|
|
return secrets, headerOf(resp), err
|
|
})
|
|
return Page[ActionSecret]{Items: convertAll(window.Items, secretFrom), More: window.More, Total: window.Total, Counted: window.Counted}, err
|
|
}
|
|
|
|
// Set creates or replaces a secret; the API's PUT operation defines upsert semantics. The result
|
|
// names the secret as Gitea stores it.
|
|
func (s ActionSecrets) Set(ctx context.Context, name, value, description string) (ActionSecret, error) {
|
|
option := gitea.CreateOrUpdateSecretOption{Data: value, Description: description}
|
|
err := do(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
|
switch {
|
|
case s.Scope.Account:
|
|
return c.Actions.CreateUserSecret(ctx, name, option)
|
|
case s.Scope.Organization != "":
|
|
return c.Actions.CreateOrgSecret(ctx, s.Scope.Organization, name, option)
|
|
}
|
|
owner, repo, err := splitRepository(s.Scope.Repository)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.Actions.CreateRepoSecret(ctx, owner, repo, name, option)
|
|
})
|
|
return ActionSecret{Name: ActionValueName(name), Description: description}, err
|
|
}
|
|
|
|
// Delete removes only the named secret in the selected scope.
|
|
func (s ActionSecrets) Delete(ctx context.Context, name string) error {
|
|
err := do(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
|
switch {
|
|
case s.Scope.Account:
|
|
return c.Actions.DeleteUserSecret(ctx, name)
|
|
case s.Scope.Organization != "":
|
|
return c.Actions.DeleteOrgSecret(ctx, s.Scope.Organization, name)
|
|
}
|
|
owner, repo, err := splitRepository(s.Scope.Repository)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO(sdk): DeleteRepoSecret does not escape the secret name.
|
|
return c.Actions.DeleteRepoSecret(ctx, owner, repo, url.PathEscape(name))
|
|
})
|
|
return Classify(err, "secret", ActionValueName(name), s.Scope.String())
|
|
}
|
|
|
|
// ActionVariables manages scoped non-secret Actions variables.
|
|
type ActionVariables struct {
|
|
Client *transport.Client
|
|
Scope ActionScope
|
|
}
|
|
|
|
// repoVariables addresses repository variables, or one of them when name is set, for raw requests.
|
|
// TODO(sdk): RepoActionsVariable.Description, a description on Create/UpdateRepoVariable, and name
|
|
// escaping in the repository variable methods.
|
|
func (s ActionVariables) repoVariables(name string) string {
|
|
endpoint := "repos/" + s.Scope.Repository + "/actions/variables"
|
|
if name != "" {
|
|
endpoint += "/" + url.PathEscape(name)
|
|
}
|
|
return endpoint
|
|
}
|
|
|
|
// List returns a non-nil page of variables.
|
|
func (s ActionVariables) List(ctx context.Context, limit, page int) ([]ActionVariable, error) {
|
|
window, err := s.ListPage(ctx, limit, page)
|
|
return window.Items, err
|
|
}
|
|
|
|
// ListPage is List with what the server reports about the other pages.
|
|
func (s ActionVariables) ListPage(ctx context.Context, limit, page int) (Page[ActionVariable], error) {
|
|
var window Page[*gitea.ActionsVariable]
|
|
var err error
|
|
if s.Scope.Account || s.Scope.Organization != "" {
|
|
window, err = listPage(limit, page, func(limit, page int) ([]*gitea.ActionsVariable, http.Header, error) {
|
|
variables, resp, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.ActionsVariable, *gitea.Response, error) {
|
|
if s.Scope.Account {
|
|
return c.Actions.ListUserVariables(ctx, listOptions(limit, page))
|
|
}
|
|
return c.Actions.ListOrgVariables(ctx, s.Scope.Organization, gitea.ListOrgActionsVariableOption{ListOptions: listOptions(limit, page)})
|
|
})
|
|
return variables, headerOf(resp), err
|
|
})
|
|
} else {
|
|
window, err = rawPage[*gitea.ActionsVariable](ctx, s.Client, s.repoVariables(""), nil, limit, page)
|
|
}
|
|
return Page[ActionVariable]{Items: convertAll(window.Items, variableFrom), More: window.More, Total: window.Total, Counted: window.Counted}, err
|
|
}
|
|
|
|
// View retrieves one variable.
|
|
func (s ActionVariables) View(ctx context.Context, name string) (ActionVariable, error) {
|
|
variable := new(gitea.ActionsVariable)
|
|
var err error
|
|
switch {
|
|
case s.Scope.Account:
|
|
variable, _, err = call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.ActionsVariable, *gitea.Response, error) {
|
|
return c.Actions.GetUserVariable(ctx, name)
|
|
})
|
|
case s.Scope.Organization != "":
|
|
variable, _, err = call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.ActionsVariable, *gitea.Response, error) {
|
|
return c.Actions.GetOrgVariable(ctx, s.Scope.Organization, name)
|
|
})
|
|
default:
|
|
err = getJSON(ctx, s.Client, s.repoVariables(name), variable)
|
|
}
|
|
if err != nil {
|
|
return ActionVariable{}, Classify(err, "variable", ActionValueName(name), s.Scope.String())
|
|
}
|
|
return variableFrom(variable), nil
|
|
}
|
|
|
|
// Set chooses create or update from an authoritative read.
|
|
func (s ActionVariables) Set(ctx context.Context, name, value, description string) (ActionVariable, error) {
|
|
exists := true
|
|
if _, err := s.View(ctx, name); err != nil {
|
|
if !errors.As(err, new(*NotFoundError)) {
|
|
return ActionVariable{}, err
|
|
}
|
|
exists = false
|
|
}
|
|
create := gitea.CreateActionsVariableOption{Value: value, Description: description}
|
|
update := gitea.UpdateActionsVariableOption{Name: name, Value: value, Description: description}
|
|
var err error
|
|
switch {
|
|
case s.Scope.Account:
|
|
err = do(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
|
if exists {
|
|
return c.Actions.UpdateUserVariable(ctx, name, update)
|
|
}
|
|
return c.Actions.CreateUserVariable(ctx, name, create)
|
|
})
|
|
case s.Scope.Organization != "":
|
|
err = do(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
|
if exists {
|
|
return c.Actions.UpdateOrgVariable(ctx, s.Scope.Organization, name, update)
|
|
}
|
|
return c.Actions.CreateOrgVariable(ctx, s.Scope.Organization, name, create)
|
|
})
|
|
default:
|
|
method := http.MethodPost
|
|
if exists {
|
|
method = http.MethodPut
|
|
}
|
|
err = sendJSON(ctx, s.Client, method, s.repoVariables(name), update, nil)
|
|
}
|
|
return ActionVariable{Name: ActionValueName(name), Value: value, Description: description}, err
|
|
}
|
|
|
|
// Delete removes only the named variable in the selected scope.
|
|
func (s ActionVariables) Delete(ctx context.Context, name string) error {
|
|
var err error
|
|
switch {
|
|
case s.Scope.Account:
|
|
err = do(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
|
return c.Actions.DeleteUserVariable(ctx, name)
|
|
})
|
|
case s.Scope.Organization != "":
|
|
err = do(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
|
return c.Actions.DeleteOrgVariable(ctx, s.Scope.Organization, name)
|
|
})
|
|
default:
|
|
err = sendJSON(ctx, s.Client, http.MethodDelete, s.repoVariables(name), nil, nil)
|
|
}
|
|
return Classify(err, "variable", ActionValueName(name), s.Scope.String())
|
|
}
|