fix: address verified review findings (actions-workflows-values-1)

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-18 17:40:06 +02:00
1 parent 401650b489
commit 432d139071
12 files changed
+268 -34

No files matched your search

+22 -8
View File
@@ -36,12 +36,21 @@ func (s ActionScope) String() string {
type ActionSecret struct {
Name string `json:"name"`
Description string `json:"description"`
CreatedAt string `json:"created_at,omitempty"`
CreatedAt string `json:"created_at"`
}
// ActionValueName is the name Gitea stores a secret or variable under: names are upper-cased.
func ActionValueName(name string) string { return strings.ToUpper(name) }
// classifyValue classifies err as a missing secret or variable only when the 404 says so: Gitea
// answers a plain "not found" when the repository or organization itself does not exist.
func classifyValue(err error, kind, name string, scope ActionScope) error {
if apiErr, ok := errors.AsType[*transport.APIError](err); ok && apiErr.Status == http.StatusNotFound && !strings.Contains(apiErr.Message, kind) {
return err
}
return Classify(err, kind, ActionValueName(name), scope.String())
}
func secretFrom(secret *gitea.Secret) ActionSecret {
return ActionSecret{Name: secret.Name, Description: secret.Description, CreatedAt: timestamp(secret.Created)}
}
@@ -117,7 +126,7 @@ func (s ActionSecrets) Delete(ctx context.Context, name string) error {
// 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())
return classifyValue(err, "secret", name, s.Scope)
}
// ActionVariables manages scoped non-secret Actions variables.
@@ -171,23 +180,28 @@ func (s ActionVariables) View(ctx context.Context, name string) (ActionVariable,
err = getJSON(ctx, s.Client, s.repoVariables(name), variable)
}
if err != nil {
return ActionVariable{}, Classify(err, "variable", ActionValueName(name), s.Scope.String())
return ActionVariable{}, classifyValue(err, "variable", name, s.Scope)
}
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) {
// Set chooses create or update from an authoritative read. A nil description keeps the one of an
// existing variable, as Gitea replaces it on every update.
func (s ActionVariables) Set(ctx context.Context, name, value string, newDescription *string) (ActionVariable, error) {
exists := true
if _, err := s.View(ctx, name); err != nil {
current, err := s.View(ctx, name)
if err != nil {
if !errors.As(err, new(*NotFoundError)) {
return ActionVariable{}, err
}
exists = false
}
description := current.Description
if newDescription != nil {
description = *newDescription
}
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) {
@@ -228,5 +242,5 @@ func (s ActionVariables) Delete(ctx context.Context, name string) error {
default:
err = sendJSON(ctx, s.Client, http.MethodDelete, s.repoVariables(name), nil, nil)
}
return Classify(err, "variable", ActionValueName(name), s.Scope.String())
return classifyValue(err, "variable", name, s.Scope)
}
+63 -2
View File
@@ -6,8 +6,10 @@ package services
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"testing"
"gitea.dev/tea/internal/transport"
@@ -37,7 +39,7 @@ func TestVariableSetSelectsCreateOrUpdate(t *testing.T) {
assert.Equal(t, scope.path+"/variables/BUILD_MODE", r.URL.Path)
if r.Method == http.MethodGet {
w.WriteHeader(test.read)
_, _ = w.Write([]byte(`{"name":"BUILD_MODE","data":"old"}`))
_, _ = w.Write([]byte(`{"name":"BUILD_MODE","data":"old","message":"variable not found"}`))
return
}
mutations = append(mutations, r.Method)
@@ -47,7 +49,7 @@ func TestVariableSetSelectsCreateOrUpdate(t *testing.T) {
assert.Equal(t, "build flavor", payload["description"])
w.WriteHeader(http.StatusNoContent)
}))
item, err := (ActionVariables{Client: client, Scope: scope.scope}).Set(context.Background(), "BUILD_MODE", "new\nvalue", "build flavor")
item, err := (ActionVariables{Client: client, Scope: scope.scope}).Set(context.Background(), "BUILD_MODE", "new\nvalue", new("build flavor"))
if test.wantMethod == "" {
var apiErr *transport.APIError
require.ErrorAs(t, err, &apiErr)
@@ -139,3 +141,62 @@ func TestSecretListReturnsMetadataOnly(t *testing.T) {
})
}
}
func TestVariableSetKeepsTheDescriptionUnlessGiven(t *testing.T) {
for _, test := range []struct {
name string
description *string
want string
}{{"omitted", nil, "build flavor"}, {"given", new("other"), "other"}, {"cleared", new(""), ""}} {
t.Run(test.name, func(t *testing.T) {
var sent string
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
_, _ = w.Write([]byte(`{"name":"MODE","data":"old","description":"build flavor"}`))
return
}
var payload map[string]string
assert.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
sent = payload["description"]
w.WriteHeader(http.StatusNoContent)
}))
item, err := (ActionVariables{Client: client, Scope: ActionScope{Organization: "team"}}).Set(context.Background(), "MODE", "new", test.description)
require.NoError(t, err)
require.Equal(t, test.want, sent)
require.Equal(t, test.want, item.Description)
})
}
}
func TestMissingScopeIsNotReportedAsAMissingValue(t *testing.T) {
for _, test := range []struct {
message string
missing bool
}{{"not found", false}, {"secret was not found [name: ]", true}, {"variable not found", true}} {
t.Run(test.message, func(t *testing.T) {
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message":"` + test.message + `"}`))
}))
scope := ActionScope{Organization: "acme"}
for name, err := range map[string]error{
"secret": (ActionSecrets{Client: client, Scope: scope}).Delete(context.Background(), "TOKEN"),
"variable": (ActionVariables{Client: client, Scope: scope}).Delete(context.Background(), "MODE"),
} {
var missing *NotFoundError
require.Error(t, err)
if strings.Contains(test.message, name) {
require.ErrorAs(t, err, &missing, name)
} else {
require.False(t, errors.As(err, &missing), name)
}
}
})
}
}
func TestSecretSetResultHasEveryJSONField(t *testing.T) {
data, err := json.Marshal(ActionSecret{Name: "TOKEN"})
require.NoError(t, err)
require.JSONEq(t, `{"name":"TOKEN","description":"","created_at":""}`, string(data))
}
+6
View File
@@ -121,6 +121,12 @@ func (s Actions) Dispatch(ctx context.Context, id, ref string, inputs map[string
return c.Actions.DispatchRepoWorkflow(ctx, owner, repo, id, gitea.CreateActionsWorkflowDispatchOption{Ref: ref, Inputs: inputs}, true)
})
if api, ok := errors.AsType[*transport.APIError](err); ok && api.Status == http.StatusNotFound && strings.Contains(api.Message, "ref") {
// Gitea resolves a bare ref as a branch only; a tag needs its full name.
if !strings.HasPrefix(ref, "refs/") {
if retried, retryErr := s.Dispatch(ctx, id, "refs/tags/"+ref, inputs); retryErr == nil {
return retried, nil
}
}
return DispatchResult{}, &NotFoundError{Kind: "ref", Key: strconv.Quote(ref), Scope: s.Repository, Err: err}
}
if err != nil {
+26
View File
@@ -336,3 +336,29 @@ func TestWorkflowFileTellsAMissingRefFromAMissingFile(t *testing.T) {
_, err = service.WorkflowFile(context.Background(), workflow, "main")
require.EqualError(t, err, "workflow file .gitea/workflows/ci.yml not found in owner/repo")
}
func TestDispatchFallsBackToATagRef(t *testing.T) {
var refs []string
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var payload struct {
Ref string `json:"ref"`
}
require.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
refs = append(refs, payload.Ref)
if payload.Ref == "v1.0" || strings.HasSuffix(payload.Ref, "nope") {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message":"ref \"` + payload.Ref + `\" not found"}`))
return
}
_, _ = w.Write([]byte(`{"workflow_run_id":12}`))
}))
service := Actions{Client: client, Repository: "owner/repo"}
result, err := service.Dispatch(context.Background(), "ci.yml", "v1.0", nil)
require.NoError(t, err)
require.Equal(t, int64(12), result.ID)
_, err = service.Dispatch(context.Background(), "ci.yml", "nope", nil)
var missing *NotFoundError
require.ErrorAs(t, err, &missing)
require.Equal(t, "ref", missing.Kind)
require.Equal(t, []string{"v1.0", "refs/tags/v1.0", "nope", "refs/tags/nope"}, refs)
}