Skip to content

Commit d6a85d9

Browse files
committed
streamline auth configure/reset UX
- single prompt gate: gh-auth mode choice replaces separate Proceed? confirm - banner shows bot identity before prompting - auth reset runs without confirmation - --gh-auth flag makes auth configure fully non-interactive - remove --yes flag (no longer needed)
1 parent 58ff56f commit d6a85d9

File tree

4 files changed

+103
-88
lines changed

4 files changed

+103
-88
lines changed

README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,30 +67,49 @@ Select **Repository permissions** based on what you need:
6767

6868
## Quick Start
6969

70-
```bash
71-
# 1. Setup — enter App ID, Installation ID, key path
72-
# (optionally configures git + gh auth at the end)
73-
ghapp setup
70+
### Interactive
7471

75-
# 2. Use git/gh normally — auth is transparent
72+
```bash
73+
ghapp setup # prompts for App ID, Installation ID, key path
7674
git clone https://github.com/org/repo.git
7775
gh pr list
7876
```
7977

8078
> If you skipped auth configuration during setup, run `ghapp auth configure` separately.
8179
80+
### Non-interactive (scripted / CI / LLM)
81+
82+
```bash
83+
ghapp config set --app-id 123 --installation-id 456 --private-key-path /path/to/key.pem
84+
ghapp auth configure --gh-auth shell-function
85+
```
86+
8287
## Commands
8388

8489
| Command | Description |
8590
|---------|-------------|
8691
| `ghapp setup [--import-key]` | Interactive setup — App ID, Installation ID, PEM key |
92+
| `ghapp config set [flags]` | Set config values non-interactively (see below) |
93+
| `ghapp config get [key]` | Print config values (all or single key) |
94+
| `ghapp config path` | Print config file location |
8795
| `ghapp token [--no-cache]` | Print an installation token (cached; `--no-cache` forces fresh) |
8896
| `ghapp auth configure [--gh-auth MODE]` | Configure git credential helper, gh CLI, and git identity |
8997
| `ghapp auth status` | Show current auth configuration and diagnostics |
9098
| `ghapp auth reset [--remove-key]` | Remove all auth config and restore previous git identity |
9199
| `ghapp update` | Self-update to the latest release |
92100
| `ghapp version` | Print version info |
93101

102+
### `ghapp config set` flags
103+
104+
| Flag | Description |
105+
|------|-------------|
106+
| `--app-id` | GitHub App ID |
107+
| `--installation-id` | GitHub App installation ID |
108+
| `--private-key-path` | Path to private key PEM file |
109+
| `--import-key` | Import private key into OS keyring from PEM file (mutually exclusive with `--private-key-path`) |
110+
111+
Only flags that are explicitly provided are written — omitted fields are preserved from the existing config.
112+
94113
### `--gh-auth` modes
95114

96115
During `auth configure`, you're prompted to choose how `gh` CLI gets authenticated. You can also pass it non-interactively:

internal/cmd/auth.go

Lines changed: 61 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -68,96 +68,100 @@ func runAuthConfigure(cmd *cobra.Command, args []string) error {
6868
return err
6969
}
7070

71-
fmt.Fprintln(out, "This will:")
71+
// Resolve bot identity early so we can show it in the banner
72+
identityLine := "app bot identity"
73+
slug, botUserID, identityErr := resolveAppIdentity()
74+
if identityErr == nil {
75+
botName := fmt.Sprintf("%s[bot]", slug)
76+
botEmail := fmt.Sprintf("%d+%s[bot]@users.noreply.github.com", botUserID, slug)
77+
identityLine = fmt.Sprintf("%s <%s>", botName, botEmail)
78+
}
79+
80+
// Banner
81+
fmt.Fprintln(out, "Auth will automatically configure:")
7282
fmt.Fprintf(out, " 1. Set git credential helper: %s\n", helperCmd)
7383
fmt.Fprintln(out, " 2. Write github.com entry to gh hosts.yml")
7484
fmt.Fprintln(out, " 3. Rewrite git@github.com SSH URLs to HTTPS (insteadOf)")
85+
fmt.Fprintf(out, " 4. Set git identity to %s\n", identityLine)
7586
fmt.Fprintln(out)
7687

77-
if !confirm(cmd, "Proceed?") {
78-
fmt.Fprintln(out, "Aborted.")
79-
return nil
88+
// Resolve gh-auth mode — this is the single gate for interactive use
89+
mode := ghAuthFlag
90+
if mode == "" {
91+
if !isInteractive(cmd) {
92+
mode = "none"
93+
} else {
94+
fmt.Fprintln(out, "How would you like to authenticate gh CLI commands?")
95+
fmt.Fprintln(out, " 1. Shell function (recommended) — auto-refreshes token per invocation")
96+
fmt.Fprintln(out, " 2. PATH binary — wrapper binary that injects token")
97+
fmt.Fprintln(out, " 3. None — keep hosts.yml only (token expires in ~1hr)")
98+
fmt.Fprintln(out)
99+
100+
choice := promptChoice(cmd, "Choice [1/2/3]:", "")
101+
switch choice {
102+
case "1":
103+
mode = "shell-function"
104+
case "2":
105+
mode = "path-shim"
106+
case "3":
107+
mode = "none"
108+
default:
109+
fmt.Fprintln(out, "Skipped. Run 'ghapp auth configure' to set up later.")
110+
return nil
111+
}
112+
}
80113
}
81114

82-
// Configure git credential helper
115+
// --- Execute all steps ---
116+
117+
// 1. Git credential helper
83118
gitHelper := fmt.Sprintf("!%s credential-helper", helperCmd)
84119
gitCmd := exec.Command("git", "config", "--global", "credential.https://github.com.helper", gitHelper)
85120
if output, err := gitCmd.CombinedOutput(); err != nil {
86121
return fmt.Errorf("git config: %s: %w", strings.TrimSpace(string(output)), err)
87122
}
88123
fmt.Fprintln(out, "Git credential helper configured.")
89124

90-
// Configure gh CLI (hosts.yml as baseline)
125+
// 2. gh CLI (hosts.yml as baseline)
91126
if err := configureGhHosts(); err != nil {
92127
fmt.Fprintf(out, "Warning: could not configure gh CLI: %v\n", err)
93128
fmt.Fprintln(out, "Use: export GH_TOKEN=$(ghapp token)")
94129
} else {
95130
fmt.Fprintln(out, "gh CLI configured.")
96131
}
97132

98-
// Configure git identity as the app bot
99-
if err := configureGitIdentity(cmd); err != nil {
100-
fmt.Fprintf(out, "Warning: could not configure git identity: %v\n", err)
133+
// 3. Git identity
134+
if identityErr != nil {
135+
fmt.Fprintf(out, "Warning: could not configure git identity: %v\n", identityErr)
136+
} else {
137+
if err := configureGitIdentity(cmd, slug, botUserID); err != nil {
138+
fmt.Fprintf(out, "Warning: could not configure git identity: %v\n", err)
139+
}
101140
}
102141

103-
// Rewrite git@github.com SSH URLs to HTTPS
142+
// 4. SSH-to-HTTPS URL rewrite
104143
configureInsteadOf(cmd)
105144

106-
// gh CLI dynamic auth setup
107-
if err := configureGhAuth(cmd, helperCmd); err != nil {
108-
fmt.Fprintf(out, "Warning: gh dynamic auth: %v\n", err)
109-
}
110-
111-
return nil
112-
}
113-
114-
// configureGhAuth handles the interactive gh auth mode selection.
115-
func configureGhAuth(cmd *cobra.Command, ghappBin string) error {
116-
out := cmd.OutOrStdout()
117-
118-
mode := ghAuthFlag
119-
if mode == "" {
120-
// Check if interactive
121-
if !isInteractive(cmd) {
122-
fmt.Fprintln(out)
123-
fmt.Fprintln(out, "Note: gh hosts.yml token expires in ~1hr.")
124-
fmt.Fprintln(out, "For long sessions, use: export GH_TOKEN=$(ghapp token)")
125-
return nil
126-
}
127-
128-
fmt.Fprintln(out)
129-
fmt.Fprintln(out, "How would you like to authenticate gh CLI commands?")
130-
fmt.Fprintln(out, " 1. Shell function (recommended) — auto-refreshes token per invocation")
131-
fmt.Fprintln(out, " 2. PATH binary — wrapper binary that injects token")
132-
fmt.Fprintln(out, " 3. None — keep hosts.yml only (token expires in ~1hr)")
133-
fmt.Fprintln(out)
134-
135-
choice := promptChoice(cmd, "Choice [1/2/3]", "1")
136-
switch choice {
137-
case "1":
138-
mode = "shell-function"
139-
case "2":
140-
mode = "path-shim"
141-
default:
142-
mode = "none"
143-
}
144-
}
145-
145+
// 5. gh CLI dynamic auth
146146
switch mode {
147147
case "shell-function":
148-
return configureShellFunction(cmd, ghappBin)
148+
if err := configureShellFunction(cmd, helperCmd); err != nil {
149+
fmt.Fprintf(out, "Warning: gh dynamic auth: %v\n", err)
150+
}
149151
case "path-shim":
150-
return configurePathShim(cmd, ghappBin)
152+
if err := configurePathShim(cmd, helperCmd); err != nil {
153+
fmt.Fprintf(out, "Warning: gh dynamic auth: %v\n", err)
154+
}
151155
case "none":
152156
fmt.Fprintln(out)
153157
fmt.Fprintln(out, "Note: gh hosts.yml token expires in ~1hr.")
154158
fmt.Fprintln(out, "For long sessions, use: export GH_TOKEN=$(ghapp token)")
155-
return nil
156-
default:
157-
return fmt.Errorf("unknown --gh-auth mode: %s", mode)
158159
}
160+
161+
return nil
159162
}
160163

164+
161165
func configureShellFunction(cmd *cobra.Command, ghappBin string) error {
162166
out := cmd.OutOrStdout()
163167

@@ -427,11 +431,6 @@ func runAuthStatus(cmd *cobra.Command, args []string) error {
427431
func runAuthReset(cmd *cobra.Command, args []string) error {
428432
out := cmd.OutOrStdout()
429433

430-
if !confirm(cmd, "Remove git credential helper and gh auth config?") {
431-
fmt.Fprintln(out, "Aborted.")
432-
return nil
433-
}
434-
435434
// Remove git credential helper
436435
gitCmd := exec.Command("git", "config", "--global", "--unset", "credential.https://github.com.helper")
437436
if output, err := gitCmd.CombinedOutput(); err != nil {
@@ -562,31 +561,18 @@ func confirm(cmd *cobra.Command, prompt string) bool {
562561

563562
// git identity management
564563

565-
func configureGitIdentity(cmd *cobra.Command) error {
564+
func configureGitIdentity(cmd *cobra.Command, slug string, botUserID int64) error {
566565
out := cmd.OutOrStdout()
567566

568-
// Fetch app slug + bot user ID (or use cached values)
569-
slug, botUserID, err := resolveAppIdentity()
570-
if err != nil {
571-
return err
572-
}
573-
574567
botName := fmt.Sprintf("%s[bot]", slug)
575568
botEmail := fmt.Sprintf("%d+%s[bot]@users.noreply.github.com", botUserID, slug)
576569

577-
// Check existing git identity
570+
// Backup existing identity if present
578571
existingName := gitConfigGet("user.name")
579572
existingEmail := gitConfigGet("user.email")
580-
581573
if existingName != "" || existingEmail != "" {
582-
fmt.Fprintf(out, "\nGit identity already set as %q <%s>.\n", existingName, existingEmail)
583-
if !confirm(cmd, "Switch to app bot identity?") {
584-
return nil
585-
}
586-
// Backup previous identity
587574
cfg.PrevGitUserName = existingName
588575
cfg.PrevGitUserEmail = existingEmail
589-
fmt.Fprintln(out, "Previous identity saved — will be restored on 'ghapp auth reset'.")
590576
}
591577

592578
// Set bot identity

internal/cmd/cmd_test.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -468,15 +468,17 @@ func gitCfg(t *testing.T, key string) string {
468468
func TestAuthConfigure_E2E(t *testing.T) {
469469
_, buf := setupE2E(t)
470470

471-
// "y" = proceed, "y" = switch to bot identity
472-
rootCmd.SetIn(strings.NewReader("y\ny\n"))
471+
// --gh-auth flag = non-interactive, no prompts needed
473472
rootCmd.SetArgs([]string{"auth", "configure", "--gh-auth", "none"})
474473
require.NoError(t, rootCmd.Execute())
475474

476475
output := buf.String()
477476
assert.Contains(t, output, "Git credential helper configured")
478477
assert.Contains(t, output, "Git identity set to testbot[bot]")
479478

479+
// Verify banner shows bot identity
480+
assert.Contains(t, output, "Set git identity to testbot[bot]")
481+
480482
// Verify git config was written to isolated gitconfig
481483
assert.Contains(t, gitCfg(t, "credential.https://github.com.helper"), "credential-helper")
482484
assert.Equal(t, "testbot[bot]", gitCfg(t, "user.name"))
@@ -501,16 +503,14 @@ func TestAuthReset_E2E(t *testing.T) {
501503
_, buf := setupE2E(t)
502504

503505
// First: configure
504-
rootCmd.SetIn(strings.NewReader("y\ny\n"))
505506
rootCmd.SetArgs([]string{"auth", "configure", "--gh-auth", "none"})
506507
require.NoError(t, rootCmd.Execute())
507508

508509
// Sanity: credential helper is set
509510
assert.NotEmpty(t, gitCfg(t, "credential.https://github.com.helper"))
510511

511-
// Now: reset
512+
// Now: reset (no confirm needed)
512513
buf.Reset()
513-
rootCmd.SetIn(strings.NewReader("y\n"))
514514
rootCmd.SetArgs([]string{"auth", "reset"})
515515
require.NoError(t, rootCmd.Execute())
516516

@@ -526,7 +526,6 @@ func TestAuthStatus_E2E(t *testing.T) {
526526
_, buf := setupE2E(t)
527527

528528
// Configure first
529-
rootCmd.SetIn(strings.NewReader("y\ny\n"))
530529
rootCmd.SetArgs([]string{"auth", "configure", "--gh-auth", "none"})
531530
require.NoError(t, rootCmd.Execute())
532531

@@ -548,8 +547,7 @@ func TestAuthConfigure_ShellFunction_E2E(t *testing.T) {
548547
// Override shell detection so the test doesn't depend on parent process
549548
shellinit.ShellOverride = shellinit.ShellByName("bash")
550549

551-
// "y" = proceed, "y" = switch to bot identity
552-
rootCmd.SetIn(strings.NewReader("y\ny\n"))
550+
// --gh-auth flag = non-interactive, no prompts needed
553551
rootCmd.SetArgs([]string{"auth", "configure", "--gh-auth", "shell-function"})
554552
require.NoError(t, rootCmd.Execute())
555553

internal/cmd/config_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,15 @@ func TestConfigPath(t *testing.T) {
187187

188188
assert.Equal(t, cfgFile+"\n", buf.String())
189189
}
190+
191+
func TestAuthConfigure_NonInteractive(t *testing.T) {
192+
_, buf := setupE2E(t)
193+
194+
// --gh-auth flag makes it fully non-interactive
195+
rootCmd.SetArgs([]string{"auth", "configure", "--gh-auth", "none"})
196+
require.NoError(t, rootCmd.Execute())
197+
198+
output := buf.String()
199+
assert.Contains(t, output, "Git credential helper configured")
200+
assert.Contains(t, output, "gh CLI configured")
201+
}

0 commit comments

Comments
 (0)