Skip to content

Commit 7fe09a3

Browse files
committed
feat: enhance tokens bootstrap command with repository owner and name flags
1 parent 907bd59 commit 7fe09a3

File tree

1 file changed

+76
-8
lines changed

1 file changed

+76
-8
lines changed

pkg/cli/tokens_bootstrap.go

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package cli
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"os"
7+
"os/exec"
8+
"strings"
69

710
"github.com/githubnext/gh-aw/pkg/console"
11+
"github.com/githubnext/gh-aw/pkg/workflow"
812
"github.com/spf13/cobra"
913
)
1014

@@ -116,6 +120,8 @@ var recommendedTokenSpecs = []tokenSpec{
116120
// NewTokensBootstrapSubcommand creates the `tokens bootstrap` subcommand
117121
func NewTokensBootstrapSubcommand() *cobra.Command {
118122
var engineFlag string
123+
var ownerFlag string
124+
var repoFlag string
119125

120126
cmd := &cobra.Command{
121127
Use: "bootstrap",
@@ -131,19 +137,31 @@ available) and prints the exact secrets to add and suggested scopes.
131137
For full details, including precedence rules, see the GitHub Tokens
132138
reference in the documentation.`,
133139
RunE: func(cmd *cobra.Command, args []string) error {
134-
return runTokensBootstrap(engineFlag)
140+
return runTokensBootstrap(engineFlag, ownerFlag, repoFlag)
135141
},
136142
}
137143

138144
cmd.Flags().StringVarP(&engineFlag, "engine", "e", "", "Check tokens for specific engine (copilot, claude, codex)")
145+
cmd.Flags().StringVar(&ownerFlag, "owner", "", "Repository owner (defaults to current repository)")
146+
cmd.Flags().StringVar(&repoFlag, "repo", "", "Repository name (defaults to current repository)")
139147

140148
return cmd
141149
}
142150

143-
func runTokensBootstrap(engine string) error {
144-
repoSlug, err := GetCurrentRepoSlug()
145-
if err != nil {
146-
return fmt.Errorf("failed to detect current repository: %w", err)
151+
func runTokensBootstrap(engine, owner, repo string) error {
152+
var repoSlug string
153+
var err error
154+
155+
// Determine target repository
156+
if owner != "" && repo != "" {
157+
repoSlug = fmt.Sprintf("%s/%s", owner, repo)
158+
} else if owner != "" || repo != "" {
159+
return fmt.Errorf("both --owner and --repo must be specified together")
160+
} else {
161+
repoSlug, err = GetCurrentRepoSlug()
162+
if err != nil {
163+
return fmt.Errorf("failed to detect current repository: %w", err)
164+
}
147165
}
148166

149167
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Checking recommended gh-aw token secrets in %s...", repoSlug)))
@@ -160,7 +178,7 @@ func runTokensBootstrap(engine string) error {
160178
missing := make([]tokenSpec, 0, len(tokensToCheck))
161179

162180
for _, spec := range tokensToCheck {
163-
exists, err := checkSecretExists(spec.Name)
181+
exists, err := checkSecretExistsInRepo(spec.Name, repoSlug)
164182
if err != nil {
165183
// If we hit a 403 or other error, surface a friendly message and abort
166184
return fmt.Errorf("unable to inspect repository secrets (gh secret list failed for %s): %w", spec.Name, err)
@@ -185,14 +203,19 @@ func runTokensBootstrap(engine string) error {
185203
}
186204
}
187205

206+
// Extract owner and repo from slug for command examples
207+
parts := splitRepoSlug(repoSlug)
208+
cmdOwner := parts[0]
209+
cmdRepo := parts[1]
210+
188211
if len(requiredMissing) > 0 {
189212
fmt.Fprintln(os.Stderr, console.FormatErrorMessage("Required gh-aw token secrets are missing:"))
190213
for _, spec := range requiredMissing {
191214
fmt.Fprintln(os.Stderr, "")
192215
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Secret: %s", spec.Name)))
193216
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("When needed: %s", spec.When)))
194217
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Recommended scopes: %s", spec.Description)))
195-
fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf("gh aw secret set %s --owner <owner> --repo <repo>", spec.Name)))
218+
fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf("gh aw secret set %s --owner %s --repo %s", spec.Name, cmdOwner, cmdRepo)))
196219
}
197220
}
198221

@@ -204,7 +227,7 @@ func runTokensBootstrap(engine string) error {
204227
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Secret: %s (optional)", spec.Name)))
205228
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("When needed: %s", spec.When)))
206229
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Recommended scopes: %s", spec.Description)))
207-
fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf("gh aw secret set %s --owner <owner> --repo <repo>", spec.Name)))
230+
fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf("gh aw secret set %s --owner %s --repo %s", spec.Name, cmdOwner, cmdRepo)))
208231
}
209232
}
210233

@@ -213,3 +236,48 @@ func runTokensBootstrap(engine string) error {
213236

214237
return nil
215238
}
239+
240+
// checkSecretExistsInRepo checks if a secret exists in a specific repository
241+
func checkSecretExistsInRepo(secretName, repoSlug string) (bool, error) {
242+
secretsLog.Printf("Checking if secret exists in %s: %s", repoSlug, secretName)
243+
244+
// Use gh CLI to list repository secrets
245+
cmd := workflow.ExecGH("secret", "list", "--repo", repoSlug, "--json", "name")
246+
output, err := cmd.Output()
247+
if err != nil {
248+
// Check if it's a 403 error by examining the error
249+
if exitError, ok := err.(*exec.ExitError); ok {
250+
if strings.Contains(string(exitError.Stderr), "403") {
251+
return false, fmt.Errorf("403 access denied")
252+
}
253+
}
254+
return false, fmt.Errorf("failed to list secrets: %w", err)
255+
}
256+
257+
// Parse the JSON output
258+
var secrets []struct {
259+
Name string `json:"name"`
260+
}
261+
262+
if err := json.Unmarshal(output, &secrets); err != nil {
263+
return false, fmt.Errorf("failed to parse secrets list: %w", err)
264+
}
265+
266+
// Check if our secret exists
267+
for _, secret := range secrets {
268+
if secret.Name == secretName {
269+
return true, nil
270+
}
271+
}
272+
273+
return false, nil
274+
}
275+
276+
// splitRepoSlug splits "owner/repo" into [owner, repo]
277+
func splitRepoSlug(slug string) [2]string {
278+
parts := strings.SplitN(slug, "/", 2)
279+
if len(parts) == 2 {
280+
return [2]string{parts[0], parts[1]}
281+
}
282+
return [2]string{slug, ""}
283+
}

0 commit comments

Comments
 (0)