mirror of
				https://gitea.com/gitea/tea.git
				synced 2025-10-31 09:15:26 +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
	 Ross Golder
					Ross Golder