mirror of
https://gitea.com/gitea/tea.git
synced 2025-10-30 00:35:27 +01:00
feat: add actions management commands (#796)
## Summary This PR adds comprehensive Actions secrets and variables management functionality to the tea CLI, enabling users to manage their repository's CI/CD configuration directly from the command line. ## Features Added ### Actions Secrets Management - **List secrets**: `tea actions secrets list` - Display all repository action secrets - **Create secrets**: `tea actions secrets create <name>` - Create new secrets with interactive prompts - **Delete secrets**: `tea actions secrets delete <name>` - Remove existing secrets ### Actions Variables Management - **List variables**: `tea actions variables list` - Display all repository action variables - **Set variables**: `tea actions variables set <name> <value>` - Create or update variables - **Delete variables**: `tea actions variables delete <name>` - Remove existing variables ## Implementation Details - **Interactive prompts**: Secure input handling for sensitive secret values - **Input validation**: Proper validation for secret/variable names and values - **Table formatting**: Consistent output formatting with existing tea commands - **Error handling**: Comprehensive error handling and user feedback - **Test coverage**: Full test suite for all functionality ## Usage Examples ```bash # Secrets management tea actions secrets list tea actions secrets create API_KEY # Will prompt securely for value tea actions secrets delete OLD_SECRET # Variables management tea actions variables list tea actions variables set API_URL https://api.example.com tea actions variables delete UNUSED_VAR ``` ## Related Issue Resolves #797 ## Testing - All new functionality includes comprehensive unit tests - Integration with existing tea CLI patterns and conventions - Validated against Gitea Actions API Reviewed-on: https://gitea.com/gitea/tea/pulls/796 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Ross Golder <ross@golder.org> Co-committed-by: Ross Golder <ross@golder.org>
This commit is contained in:
76
modules/print/actions.go
Normal file
76
modules/print/actions.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
// ActionSecretsList prints a list of action secrets
|
||||
func ActionSecretsList(secrets []*gitea.Secret, output string) {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"Name",
|
||||
"Created",
|
||||
},
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
t.addRow(
|
||||
secret.Name,
|
||||
FormatTime(secret.Created, output != ""),
|
||||
)
|
||||
}
|
||||
|
||||
if len(secrets) == 0 {
|
||||
fmt.Printf("No secrets found\n")
|
||||
return
|
||||
}
|
||||
|
||||
t.sort(0, true)
|
||||
t.print(output)
|
||||
}
|
||||
|
||||
// ActionVariableDetails prints details of a specific action variable
|
||||
func ActionVariableDetails(variable *gitea.RepoActionVariable) {
|
||||
fmt.Printf("Name: %s\n", variable.Name)
|
||||
fmt.Printf("Value: %s\n", variable.Value)
|
||||
fmt.Printf("Repository ID: %d\n", variable.RepoID)
|
||||
fmt.Printf("Owner ID: %d\n", variable.OwnerID)
|
||||
}
|
||||
|
||||
// ActionVariablesList prints a list of action variables
|
||||
func ActionVariablesList(variables []*gitea.RepoActionVariable, output string) {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"Name",
|
||||
"Value",
|
||||
"Repository ID",
|
||||
},
|
||||
}
|
||||
|
||||
for _, variable := range variables {
|
||||
// Truncate long values for table display
|
||||
value := variable.Value
|
||||
if len(value) > 50 {
|
||||
value = value[:47] + "..."
|
||||
}
|
||||
|
||||
t.addRow(
|
||||
variable.Name,
|
||||
value,
|
||||
fmt.Sprintf("%d", variable.RepoID),
|
||||
)
|
||||
}
|
||||
|
||||
if len(variables) == 0 {
|
||||
fmt.Printf("No variables found\n")
|
||||
return
|
||||
}
|
||||
|
||||
t.sort(0, true)
|
||||
t.print(output)
|
||||
}
|
||||
214
modules/print/actions_test.go
Normal file
214
modules/print/actions_test.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
func TestActionSecretsListEmpty(t *testing.T) {
|
||||
// Test with empty secrets - should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionSecretsList panicked with empty list: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionSecretsList([]*gitea.Secret{}, "")
|
||||
}
|
||||
|
||||
func TestActionSecretsListWithData(t *testing.T) {
|
||||
secrets := []*gitea.Secret{
|
||||
{
|
||||
Name: "TEST_SECRET_1",
|
||||
Created: time.Now().Add(-24 * time.Hour),
|
||||
},
|
||||
{
|
||||
Name: "TEST_SECRET_2",
|
||||
Created: time.Now().Add(-48 * time.Hour),
|
||||
},
|
||||
}
|
||||
|
||||
// Test that it doesn't panic with real data
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionSecretsList panicked with data: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionSecretsList(secrets, "")
|
||||
|
||||
// Test JSON output format to verify structure
|
||||
var buf bytes.Buffer
|
||||
testTable := table{
|
||||
headers: []string{"Name", "Created"},
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
testTable.addRow(secret.Name, FormatTime(secret.Created, true))
|
||||
}
|
||||
|
||||
testTable.fprint(&buf, "json")
|
||||
output := buf.String()
|
||||
|
||||
if !strings.Contains(output, "TEST_SECRET_1") {
|
||||
t.Error("Expected TEST_SECRET_1 in JSON output")
|
||||
}
|
||||
if !strings.Contains(output, "TEST_SECRET_2") {
|
||||
t.Error("Expected TEST_SECRET_2 in JSON output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionVariableDetails(t *testing.T) {
|
||||
variable := &gitea.RepoActionVariable{
|
||||
Name: "TEST_VARIABLE",
|
||||
Value: "test_value",
|
||||
RepoID: 123,
|
||||
OwnerID: 456,
|
||||
}
|
||||
|
||||
// Test that it doesn't panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionVariableDetails panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionVariableDetails(variable)
|
||||
}
|
||||
|
||||
func TestActionVariablesListEmpty(t *testing.T) {
|
||||
// Test with empty variables - should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionVariablesList panicked with empty list: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionVariablesList([]*gitea.RepoActionVariable{}, "")
|
||||
}
|
||||
|
||||
func TestActionVariablesListWithData(t *testing.T) {
|
||||
variables := []*gitea.RepoActionVariable{
|
||||
{
|
||||
Name: "TEST_VARIABLE_1",
|
||||
Value: "short_value",
|
||||
RepoID: 123,
|
||||
OwnerID: 456,
|
||||
},
|
||||
{
|
||||
Name: "TEST_VARIABLE_2",
|
||||
Value: strings.Repeat("a", 60), // Long value to test truncation
|
||||
RepoID: 124,
|
||||
OwnerID: 457,
|
||||
},
|
||||
}
|
||||
|
||||
// Test that it doesn't panic with real data
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionVariablesList panicked with data: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionVariablesList(variables, "")
|
||||
|
||||
// Test JSON output format to verify structure and truncation
|
||||
var buf bytes.Buffer
|
||||
testTable := table{
|
||||
headers: []string{"Name", "Value", "Repository ID"},
|
||||
}
|
||||
|
||||
for _, variable := range variables {
|
||||
value := variable.Value
|
||||
if len(value) > 50 {
|
||||
value = value[:47] + "..."
|
||||
}
|
||||
testTable.addRow(variable.Name, value, strconv.Itoa(int(variable.RepoID)))
|
||||
}
|
||||
|
||||
testTable.fprint(&buf, "json")
|
||||
output := buf.String()
|
||||
|
||||
if !strings.Contains(output, "TEST_VARIABLE_1") {
|
||||
t.Error("Expected TEST_VARIABLE_1 in JSON output")
|
||||
}
|
||||
if !strings.Contains(output, "TEST_VARIABLE_2") {
|
||||
t.Error("Expected TEST_VARIABLE_2 in JSON output")
|
||||
}
|
||||
|
||||
// Check that long value is truncated in our test table
|
||||
if strings.Contains(output, strings.Repeat("a", 60)) {
|
||||
t.Error("Long value should be truncated in table output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionVariablesListValueTruncation(t *testing.T) {
|
||||
variable := &gitea.RepoActionVariable{
|
||||
Name: "LONG_VALUE_VARIABLE",
|
||||
Value: strings.Repeat("abcdefghij", 10), // 100 characters
|
||||
RepoID: 123,
|
||||
OwnerID: 456,
|
||||
}
|
||||
|
||||
// Test that it doesn't panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionVariablesList panicked with long value: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionVariablesList([]*gitea.RepoActionVariable{variable}, "")
|
||||
|
||||
// Test the truncation logic directly
|
||||
value := variable.Value
|
||||
if len(value) > 50 {
|
||||
value = value[:47] + "..."
|
||||
}
|
||||
|
||||
if len(value) != 50 { // 47 chars + "..." = 50
|
||||
t.Errorf("Truncated value should be 50 characters, got %d", len(value))
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(value, "...") {
|
||||
t.Error("Truncated value should end with '...'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableSorting(t *testing.T) {
|
||||
// Test that the table sorting works correctly
|
||||
secrets := []*gitea.Secret{
|
||||
{Name: "Z_SECRET", Created: time.Now()},
|
||||
{Name: "A_SECRET", Created: time.Now()},
|
||||
{Name: "M_SECRET", Created: time.Now()},
|
||||
}
|
||||
|
||||
// Test the table sorting logic
|
||||
table := table{
|
||||
headers: []string{"Name", "Created"},
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
table.addRow(secret.Name, FormatTime(secret.Created, true))
|
||||
}
|
||||
|
||||
// Sort by first column (Name) in ascending order (false = ascending)
|
||||
table.sort(0, false)
|
||||
|
||||
// Check that the first row is A_SECRET after ascending sorting
|
||||
if table.values[0][0] != "A_SECRET" {
|
||||
t.Errorf("Expected first sorted value to be 'A_SECRET', got '%s'", table.values[0][0])
|
||||
}
|
||||
|
||||
// Check that the last row is Z_SECRET after ascending sorting
|
||||
if table.values[2][0] != "Z_SECRET" {
|
||||
t.Errorf("Expected last sorted value to be 'Z_SECRET', got '%s'", table.values[2][0])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user