Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 56 additions & 19 deletions pkg/api/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,12 @@ func newCISecretServiceClient() civ1connect.SecretServiceClient {

// CIAddSecret adds a single CI secret to an organization
func CIAddSecret(ctx context.Context, token, orgID, name, value string) error {
return CIAddSecretWithDescription(ctx, token, orgID, name, value, "")
return CIAddSecretWithDescription(ctx, token, orgID, name, value, "", "")
}

// CIAddSecretWithDescription adds a single CI secret to an organization, with an optional description.
func CIAddSecretWithDescription(ctx context.Context, token, orgID, name, value, description string) error {
// CIAddSecretWithDescription adds a single CI secret to an organization, with an optional description and repo scope.
// If repo is non-empty (owner/repo), the secret is scoped to that repo; otherwise it applies to all repos.
func CIAddSecretWithDescription(ctx context.Context, token, orgID, name, value, description, repo string) error {
client := newCISecretServiceClient()
req := &civ1.AddSecretRequest{
Name: name,
Expand All @@ -121,6 +122,9 @@ func CIAddSecretWithDescription(ctx context.Context, token, orgID, name, value,
if description != "" {
req.Description = &description
}
if repo != "" {
req.Repo = &repo
}
_, err := client.AddSecret(ctx, WithAuthenticationAndOrg(connect.NewRequest(req), token, orgID))
return err
}
Expand All @@ -130,12 +134,18 @@ type CISecret struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Repo string `json:"repo,omitempty"` // owner/repo or empty for all repos
}

// CIListSecrets lists all CI secrets for an organization
func CIListSecrets(ctx context.Context, token, orgID string) ([]CISecret, error) {
// CIListSecrets lists all CI secrets for an organization.
// If repoFilter is non-empty (owner/repo), returns secrets that apply to that repo (all-repos + repo-specific).
func CIListSecrets(ctx context.Context, token, orgID, repoFilter string) ([]CISecret, error) {
client := newCISecretServiceClient()
resp, err := client.ListSecrets(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ1.ListSecretsRequest{}), token, orgID))
req := &civ1.ListSecretsRequest{}
if repoFilter != "" {
req.RepoFilter = &repoFilter
}
resp, err := client.ListSecrets(ctx, WithAuthenticationAndOrg(connect.NewRequest(req), token, orgID))
if err != nil {
return nil, err
}
Expand All @@ -150,15 +160,23 @@ func CIListSecrets(ctx context.Context, token, orgID string) ([]CISecret, error)
if s.LastModified != nil {
cs.CreatedAt = s.LastModified.AsTime().Format(time.RFC3339)
}
if s.Repo != nil && *s.Repo != "" {
cs.Repo = *s.Repo
}
secrets = append(secrets, cs)
}
return secrets, nil
}

// CIDeleteSecret deletes a CI secret from an organization
func CIDeleteSecret(ctx context.Context, token, orgID, name string) error {
// CIDeleteSecret deletes a CI secret from an organization.
// If repo is non-empty, removes the repo-scoped entry; otherwise removes the all-repos entry.
func CIDeleteSecret(ctx context.Context, token, orgID, name, repo string) error {
client := newCISecretServiceClient()
_, err := client.RemoveSecret(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ1.RemoveSecretRequest{Name: name}), token, orgID))
req := &civ1.RemoveSecretRequest{Name: name}
if repo != "" {
req.Repo = &repo
}
_, err := client.RemoveSecret(ctx, WithAuthenticationAndOrg(connect.NewRequest(req), token, orgID))
return err
}

Expand All @@ -167,13 +185,18 @@ func newCIVariableServiceClient() civ1connect.VariableServiceClient {
return civ1connect.NewVariableServiceClient(getHTTPClient(baseURL), baseURL, WithUserAgent())
}

// CIAddVariable adds a single CI variable to an organization
func CIAddVariable(ctx context.Context, token, orgID, name, value string) error {
// CIAddVariable adds a single CI variable to an organization.
// If repo is non-empty (owner/repo), the variable is scoped to that repo; otherwise it applies to all repos.
func CIAddVariable(ctx context.Context, token, orgID, name, value, repo string) error {
client := newCIVariableServiceClient()
_, err := client.AddVariable(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ1.AddVariableRequest{
req := &civ1.AddVariableRequest{
Name: name,
Value: value,
}), token, orgID))
}
if repo != "" {
req.Repo = &repo
}
_, err := client.AddVariable(ctx, WithAuthenticationAndOrg(connect.NewRequest(req), token, orgID))
return err
}

Expand All @@ -182,12 +205,18 @@ type CIVariable struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Repo string `json:"repo,omitempty"` // owner/repo or empty for all repos
}

// CIListVariables lists all CI variables for an organization
func CIListVariables(ctx context.Context, token, orgID string) ([]CIVariable, error) {
// CIListVariables lists all CI variables for an organization.
// If repoFilter is non-empty (owner/repo), returns variables that apply to that repo (all-repos + repo-specific).
func CIListVariables(ctx context.Context, token, orgID, repoFilter string) ([]CIVariable, error) {
client := newCIVariableServiceClient()
resp, err := client.ListVariables(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ1.ListVariablesRequest{}), token, orgID))
req := &civ1.ListVariablesRequest{}
if repoFilter != "" {
req.RepoFilter = &repoFilter
}
resp, err := client.ListVariables(ctx, WithAuthenticationAndOrg(connect.NewRequest(req), token, orgID))
if err != nil {
return nil, err
}
Expand All @@ -202,14 +231,22 @@ func CIListVariables(ctx context.Context, token, orgID string) ([]CIVariable, er
if v.LastModified != nil {
cv.CreatedAt = v.LastModified.AsTime().Format(time.RFC3339)
}
if v.Repo != nil && *v.Repo != "" {
cv.Repo = *v.Repo
}
variables = append(variables, cv)
}
return variables, nil
}

// CIDeleteVariable deletes a CI variable from an organization
func CIDeleteVariable(ctx context.Context, token, orgID, name string) error {
// CIDeleteVariable deletes a CI variable from an organization.
// If repo is non-empty, removes the repo-scoped entry; otherwise removes the all-repos entry.
func CIDeleteVariable(ctx context.Context, token, orgID, name, repo string) error {
client := newCIVariableServiceClient()
_, err := client.RemoveVariable(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ1.RemoveVariableRequest{Name: name}), token, orgID))
req := &civ1.RemoveVariableRequest{Name: name}
if repo != "" {
req.Repo = &repo
}
_, err := client.RemoveVariable(ctx, WithAuthenticationAndOrg(connect.NewRequest(req), token, orgID))
return err
}
2 changes: 1 addition & 1 deletion pkg/cmd/ci/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ func runMigrate(ctx context.Context, opts migrateOptions) error {
return err
}

if err := api.CIAddVariable(ctx, token, orgID, name, value); err != nil {
if err := api.CIAddVariable(ctx, token, orgID, name, value, ""); err != nil {
return fmt.Errorf("failed to configure variable %s: %w", name, err)
}
configuredVariables = append(configuredVariables, name)
Expand Down
60 changes: 48 additions & 12 deletions pkg/cmd/ci/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func NewCmdSecretsAdd() *cobra.Command {
token string
value string
description string
repo string
)

cmd := &cobra.Command{
Expand All @@ -58,7 +59,10 @@ If --value is not provided, you will be prompted to enter the secret value secur
depot ci secrets add MY_API_KEY --value "secret-value"

# Add a secret with description
depot ci secrets add DATABASE_URL --description "Production database connection string"`,
depot ci secrets add DATABASE_URL --description "Production database connection string"

# Add a repo-scoped secret
depot ci secrets add REPO_TOKEN --repo owner/repo`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
Expand Down Expand Up @@ -88,12 +92,17 @@ If --value is not provided, you will be prompted to enter the secret value secur
}
}

err = api.CIAddSecretWithDescription(ctx, tokenVal, orgID, secretName, secretValue, description)
repoTrimmed := strings.TrimSpace(repo)
err = api.CIAddSecretWithDescription(ctx, tokenVal, orgID, secretName, secretValue, description, repoTrimmed)
if err != nil {
return fmt.Errorf("failed to add secret: %w", err)
}

fmt.Printf("Successfully added CI secret '%s'\n", secretName)
scopeDesc := "(all repos)"
if repoTrimmed != "" {
scopeDesc = repoTrimmed
}
fmt.Printf("Successfully added CI secret '%s' to %s\n", secretName, scopeDesc)
return nil
},
}
Expand All @@ -102,6 +111,7 @@ If --value is not provided, you will be prompted to enter the secret value secur
cmd.Flags().StringVar(&token, "token", "", "Depot API token")
cmd.Flags().StringVar(&value, "value", "", "Secret value (will prompt if not provided)")
cmd.Flags().StringVar(&description, "description", "", "Description of the secret")
cmd.Flags().StringVar(&repo, "repo", "", "Scope to a specific repo (owner/repo); omit for all repos")

return cmd
}
Expand All @@ -111,6 +121,7 @@ func NewCmdSecretsList() *cobra.Command {
orgID string
token string
output string
repo string
)

cmd := &cobra.Command{
Expand All @@ -124,7 +135,10 @@ func NewCmdSecretsList() *cobra.Command {
depot ci secrets list --org my-org-id

# List secrets in JSON format
depot ci secrets list --output json`,
depot ci secrets list --output json

# List secrets for a specific repo
depot ci secrets list --repo owner/repo`,
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
Expand All @@ -141,7 +155,7 @@ func NewCmdSecretsList() *cobra.Command {
return fmt.Errorf("missing API token, please run `depot login`")
}

secrets, err := api.CIListSecrets(ctx, tokenVal, orgID)
secrets, err := api.CIListSecrets(ctx, tokenVal, orgID, strings.TrimSpace(repo))
if err != nil {
return fmt.Errorf("failed to list secrets: %w", err)
}
Expand All @@ -157,8 +171,8 @@ func NewCmdSecretsList() *cobra.Command {
return nil
}

fmt.Printf("%-30s %-50s %s\n", "NAME", "DESCRIPTION", "CREATED")
fmt.Printf("%-30s %-50s %s\n", strings.Repeat("-", 30), strings.Repeat("-", 50), strings.Repeat("-", 20))
fmt.Printf("%-30s %-50s %-25s %s\n", "NAME", "DESCRIPTION", "REPO", "CREATED")
fmt.Printf("%-30s %-50s %-25s %s\n", strings.Repeat("-", 30), strings.Repeat("-", 50), strings.Repeat("-", 25), strings.Repeat("-", 20))

for _, secret := range secrets {
name := secret.Name
Expand All @@ -171,9 +185,17 @@ func NewCmdSecretsList() *cobra.Command {
description = description[:47] + "..."
}

repoScope := secret.Repo
if repoScope == "" {
repoScope = "(all repos)"
}
if len(repoScope) > 25 {
repoScope = repoScope[:22] + "..."
}

created := secret.CreatedAt

fmt.Printf("%-30s %-50s %s\n", name, description, created)
fmt.Printf("%-30s %-50s %-25s %s\n", name, description, repoScope, created)
}

return nil
Expand All @@ -183,6 +205,7 @@ func NewCmdSecretsList() *cobra.Command {
cmd.Flags().StringVar(&orgID, "org", "", "Organization ID (required when user is a member of multiple organizations)")
cmd.Flags().StringVar(&token, "token", "", "Depot API token")
cmd.Flags().StringVar(&output, "output", "", "Output format (json)")
cmd.Flags().StringVar(&repo, "repo", "", "Filter to secrets that apply to this repo (owner/repo); omit for all")

return cmd
}
Expand All @@ -192,6 +215,7 @@ func NewCmdSecretsRemove() *cobra.Command {
orgID string
token string
force bool
repo string
)

cmd := &cobra.Command{
Expand All @@ -205,7 +229,10 @@ func NewCmdSecretsRemove() *cobra.Command {
depot ci secrets remove GITHUB_TOKEN MY_API_KEY DATABASE_URL

# Remove secrets without confirmation prompt
depot ci secrets remove GITHUB_TOKEN MY_API_KEY --force`,
depot ci secrets remove GITHUB_TOKEN MY_API_KEY --force

# Remove a repo-scoped secret
depot ci secrets remove REPO_TOKEN --repo owner/repo`,
Aliases: []string{"rm"},
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -225,7 +252,11 @@ func NewCmdSecretsRemove() *cobra.Command {

if !force {
names := strings.Join(args, ", ")
prompt := fmt.Sprintf("Are you sure you want to remove CI secret(s) %s? (y/N): ", names)
scopeDesc := "(all repos)"
if strings.TrimSpace(repo) != "" {
scopeDesc = strings.TrimSpace(repo)
}
prompt := fmt.Sprintf("Are you sure you want to remove CI secret(s) %s from %s? (y/N): ", names, scopeDesc)
y, err := helpers.PromptForYN(prompt)
if err != nil {
return fmt.Errorf("failed to read confirmation: %w", err)
Expand All @@ -234,12 +265,16 @@ func NewCmdSecretsRemove() *cobra.Command {
}
}

scopeDesc := "(all repos)"
if strings.TrimSpace(repo) != "" {
scopeDesc = strings.TrimSpace(repo)
}
for _, secretName := range args {
err := api.CIDeleteSecret(ctx, tokenVal, orgID, secretName)
err := api.CIDeleteSecret(ctx, tokenVal, orgID, secretName, strings.TrimSpace(repo))
if err != nil {
return fmt.Errorf("failed to remove secret '%s': %w", secretName, err)
}
fmt.Printf("Successfully removed CI secret '%s'\n", secretName)
fmt.Printf("Successfully removed CI secret '%s' from %s\n", secretName, scopeDesc)
}

return nil
Expand All @@ -249,6 +284,7 @@ func NewCmdSecretsRemove() *cobra.Command {
cmd.Flags().StringVar(&orgID, "org", "", "Organization ID (required when user is a member of multiple organizations)")
cmd.Flags().StringVar(&token, "token", "", "Depot API token")
cmd.Flags().BoolVar(&force, "force", false, "Skip confirmation prompt")
cmd.Flags().StringVar(&repo, "repo", "", "Remove the repo-scoped entry (owner/repo); omit for all-repos entry")

return cmd
}
Loading
Loading