feat(secret,variable): read values like gh and report stored names

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>
This commit is contained in:
bircni committed 2026-10-11 23:46:57 +02:00
1 parent 4edb836416
commit 0dfe7def9b
5 files changed
+641 -185

No files matched your search

+144
View File
@@ -7,9 +7,12 @@ import (
"context"
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"gitea.dev/tea/internal/clierr"
"github.com/stretchr/testify/require"
)
@@ -32,3 +35,144 @@ func TestSecretValueFileDropsTrailingLineEnding(t *testing.T) {
require.NoError(t, Execute(context.Background(), deps, []string{"secret", "set", "BUILD_TOKEN", "-R", "alice/project", "--value-file", "-", "--yes"}))
require.Equal(t, "sensitive", stored)
}
func TestActionValueSetReadsValuesAndReportsStoredNames(t *testing.T) {
envFile := filepath.Join(t.TempDir(), ".env")
require.NoError(t, os.WriteFile(envFile, []byte("# deploy\nAPI_KEY=abc\nexport region=\"eu\\nwest\"\nMODE=fast # inline\n"), 0o600))
for _, tc := range []struct {
name string
args []string
stdin string
requests []string
out, err string
}{
{
"lower-case name from --body",
[]string{"secret", "set", "lower_name", "-b", "x"},
"",
[]string{`PUT actions/secrets/lower_name {"data":"x","description":""}`},
"",
"✓ Set Actions secret LOWER_NAME for alice/project (names are upper-cased)\n",
},
{
"standard input",
[]string{"secret", "set", "TOKEN"},
"piped\n",
[]string{`PUT actions/secrets/TOKEN {"data":"piped","description":""}`},
"", "✓ Set Actions secret TOKEN for alice/project\n",
},
{
"JSON names the stored secret",
[]string{"secret", "set", "token", "--body", "x", "--json"},
"",
[]string{`PUT actions/secrets/token {"data":"x","description":""}`},
`{"name":"TOKEN","description":""}`, "",
},
{
"organization",
[]string{"secret", "set", "TOKEN", "-o", "acme", "-b", "x"},
"",
[]string{`PUT api/v1/orgs/acme/actions/secrets/TOKEN {"data":"x","description":""}`},
"", "✓ Set Actions secret TOKEN for organization acme\n",
},
{
"account",
[]string{"secret", "set", "TOKEN", "--account", "-b", "x"},
"",
[]string{`PUT api/v1/user/actions/secrets/TOKEN {"data":"x","description":""}`},
"", "✓ Set Actions secret TOKEN for your account\n",
},
{
"dotenv file",
[]string{"variable", "set", "--env-file", envFile, "-o", "acme"},
"",
[]string{
"GET api/v1/orgs/acme/actions/variables/API_KEY", `POST api/v1/orgs/acme/actions/variables/API_KEY {"value":"abc","description":""}`,
"GET api/v1/orgs/acme/actions/variables/region", `POST api/v1/orgs/acme/actions/variables/region {"value":"eu\nwest","description":""}`,
"GET api/v1/orgs/acme/actions/variables/MODE", `POST api/v1/orgs/acme/actions/variables/MODE {"value":"fast","description":""}`,
},
"",
"✓ Set Actions variable API_KEY for organization acme\n✓ Set Actions variable REGION for organization acme (names are upper-cased)\n✓ Set Actions variable MODE for organization acme\n",
},
} {
t.Run(tc.name, func(t *testing.T) {
server, requests := actionsAPI(t, map[string]string{
"PUT actions/secrets/lower_name": "204 ", "PUT actions/secrets/TOKEN": "204 ", "PUT actions/secrets/token": "204 ",
"PUT api/v1/orgs/acme/actions/secrets/TOKEN": "204 ", "PUT api/v1/user/actions/secrets/TOKEN": "204 ",
"GET api/v1/orgs/acme/actions/variables/API_KEY": `404 {}`, "POST api/v1/orgs/acme/actions/variables/API_KEY": "201 ",
"GET api/v1/orgs/acme/actions/variables/region": `404 {}`, "POST api/v1/orgs/acme/actions/variables/region": "201 ",
"GET api/v1/orgs/acme/actions/variables/MODE": `404 {}`, "POST api/v1/orgs/acme/actions/variables/MODE": "201 ",
})
deps, out, diagnostics := actionsDeps(t, server.URL, true)
deps.Streams.In = strings.NewReader(tc.stdin)
args := tc.args
if !strings.Contains(strings.Join(args, " "), "-o acme") && !strings.Contains(strings.Join(args, " "), "--account") {
args = append(args, "-R", "alice/project")
}
require.NoError(t, Execute(context.Background(), deps, args))
require.Equal(t, tc.requests, *requests)
if tc.out != "" {
require.JSONEq(t, tc.out, out.String())
} else {
require.Empty(t, out.String())
}
require.Equal(t, tc.err, diagnostics.String())
})
}
}
func TestActionValueListsShowScopeAndExplainTheAccountFlags(t *testing.T) {
server, _ := actionsAPI(t, map[string]string{
"GET actions/secrets": `[{"name":"DEPLOY_TOKEN","description":"Deploy token","created_at":"2028-01-01T10:00:00Z"}]`,
"GET actions/variables": `[{"name":"ENVIRONMENT","data":"staging","description":"Target environment"}]`,
})
deps, out, diagnostics := actionsDeps(t, server.URL, true)
deps.Streams.ErrTTY = true
require.NoError(t, Execute(context.Background(), deps, []string{"secret", "list", "-R", "alice/project", "--user", "alice"}))
require.Equal(t, "Showing 1 of 1 secrets in alice/project\n\nNAME CREATED DESCRIPTION\nDEPLOY_TOKEN about 2 hours ago Deploy token\n", out.String())
require.Equal(t, "note: --user selects the account tea acts as (alice); Gitea cannot list your account's secrets; set or delete them with --account\n", diagnostics.String())
out.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"variable", "list", "-R", "alice/project"}))
require.Equal(t, "Showing 1 of 1 variables in alice/project\n\nNAME VALUE DESCRIPTION\nENVIRONMENT staging Target environment\n", out.String())
err := Execute(context.Background(), deps, []string{"secret", "list", "--account"})
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
require.ErrorContains(t, err, "unknown flag: --account")
}
func TestVariableGetAndView(t *testing.T) {
server, _ := actionsAPI(t, map[string]string{"GET actions/variables/region": `{"name":"REGION","data":"eu\nwest","description":"Where to deploy"}`})
for _, tc := range []struct {
args []string
tty bool
want string
}{
{[]string{"variable", "get", "region"}, false, "eu\nwest\n"},
{[]string{"variable", "view", "region"}, true, "REGION\nScope: alice/project\nDescription: Where to deploy\n\neu\nwest\n"},
{[]string{"variable", "view", "region"}, false, "title:\tREGION\nscope:\talice/project\ndescription:\tWhere to deploy\n--\neu\nwest\n"},
} {
deps, out, _ := actionsDeps(t, server.URL, tc.tty)
require.NoError(t, Execute(context.Background(), deps, append(tc.args, "-R", "alice/project")))
require.Equal(t, tc.want, out.String(), "%v tty=%t", tc.args, tc.tty)
}
}
func TestActionValueDeleteConfirmsWithTheScope(t *testing.T) {
server, requests := actionsAPI(t, map[string]string{"DELETE actions/secrets/deploy_token": "204 "})
for _, accept := range []bool{false, true} {
*requests = nil
deps, _, diagnostics := actionsDeps(t, server.URL, true)
prompter := &scriptedPrompter{accept: accept}
deps.Prompter = prompter
deps.Streams.InTTY, deps.Streams.ErrTTY = true, true
err := Execute(context.Background(), deps, []string{"secret", "delete", "deploy_token", "-R", "alice/project"})
require.Equal(t, []string{"Delete secret DEPLOY_TOKEN from alice/project?"}, prompter.confirmations)
if !accept {
require.Equal(t, clierr.Canceled, clierr.ExitCode(err))
require.Empty(t, *requests)
continue
}
require.NoError(t, err)
require.Equal(t, []string{"DELETE actions/secrets/deploy_token"}, *requests)
require.Equal(t, "✓ Deleted Actions secret DEPLOY_TOKEN from alice/project\n", diagnostics.String())
}
}