Skip to content

Commit b6ec710

Browse files
committed
Add magic setup: gpc setup --auto
Automates Google Play API setup via gcloud CLI: - Auto-installs gcloud if missing (Homebrew on macOS, curl on Linux) - Auto-launches gcloud auth login if not authenticated - Creates service account and downloads credentials - Smart handoff for the one manual step (Play Console access grant) - Configures CLI profile automatically Also updates README Quick Start to lead with one-command setup.
1 parent 996867a commit b6ec710

File tree

2 files changed

+304
-2
lines changed

2 files changed

+304
-2
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,24 @@ After install, you can use either `playconsole-cli` or the shorter alias `gpc`.
5858

5959
## ⚡ Quick Start
6060

61+
### One-command setup (recommended)
62+
63+
```bash
64+
gpc setup --auto
65+
```
66+
67+
That's it. This will:
68+
- Install `gcloud` if needed (via Homebrew on macOS, or curl on Linux)
69+
- Log you into Google Cloud
70+
- Create a service account and download credentials
71+
- Open Play Console for the one manual step (granting access)
72+
- Configure everything automatically
73+
74+
### Manual setup
75+
76+
<details>
77+
<summary>Prefer to do it yourself?</summary>
78+
6179
**1. Create a service account** ([Google Cloud Console](https://console.cloud.google.com/iam-admin/serviceaccounts))
6280
```bash
6381
mkdir -p ~/.config/gpc
@@ -75,7 +93,9 @@ gpc auth login --credentials ~/.config/gpc/service-account.json
7593
gpc apps list # See your apps
7694
```
7795

78-
**5. Deploy!** 🎉
96+
</details>
97+
98+
### Deploy! 🎉
7999
```bash
80100
gpc bundles upload --file app.aab --track internal
81101
gpc tracks promote --from internal --to production --rollout 10

cmd/playconsole-cli/commands/setup/setup.go

Lines changed: 283 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,16 @@ This wizard will help you:
2121
1. Create a Google Cloud Service Account
2222
2. Enable the Google Play Android Developer API
2323
3. Download and configure your credentials
24-
4. Link the service account in Play Console`,
24+
4. Link the service account in Play Console
25+
26+
Use --auto to automate steps 1-4 using the gcloud CLI.`,
2527
Run: runSetup,
2628
}
2729

30+
func init() {
31+
SetupCmd.Flags().Bool("auto", false, "Automate setup using gcloud CLI (requires gcloud installed and authenticated)")
32+
}
33+
2834
type step struct {
2935
title string
3036
description string
@@ -60,6 +66,12 @@ var setupSteps = []step{
6066
}
6167

6268
func runSetup(cmd *cobra.Command, args []string) {
69+
autoMode, _ := cmd.Flags().GetBool("auto")
70+
if autoMode {
71+
runAutoSetup(cmd)
72+
return
73+
}
74+
6375
clearScreen()
6476
printHeader()
6577

@@ -346,3 +358,273 @@ func copyToClipboard(text string) error {
346358
cmd.Stdin = strings.NewReader(text)
347359
return cmd.Run()
348360
}
361+
362+
// runGcloud executes a gcloud command and returns trimmed stdout.
363+
func runGcloud(args ...string) (string, error) {
364+
cmd := exec.Command("gcloud", args...)
365+
var stdout, stderr strings.Builder
366+
cmd.Stdout = &stdout
367+
cmd.Stderr = &stderr
368+
if err := cmd.Run(); err != nil {
369+
return "", fmt.Errorf("%s: %s", err, strings.TrimSpace(stderr.String()))
370+
}
371+
return strings.TrimSpace(stdout.String()), nil
372+
}
373+
374+
const serviceAccountName = "play-console-cli"
375+
376+
// installGcloud attempts to install the gcloud CLI for the current platform.
377+
func installGcloud() bool {
378+
fmt.Println()
379+
switch runtime.GOOS {
380+
case "darwin":
381+
// Try Homebrew first
382+
if _, err := exec.LookPath("brew"); err == nil {
383+
fmt.Println(" Installing via Homebrew...")
384+
cmd := exec.Command("brew", "install", "--cask", "google-cloud-sdk")
385+
cmd.Stdout = os.Stdout
386+
cmd.Stderr = os.Stderr
387+
if err := cmd.Run(); err != nil {
388+
fmt.Printf(" Homebrew install failed: %v\n", err)
389+
fmt.Println(" Install manually: https://cloud.google.com/sdk/install")
390+
return false
391+
}
392+
fmt.Println(" ✓ gcloud installed via Homebrew")
393+
return true
394+
}
395+
// Fall through to curl installer
396+
fmt.Println(" Installing via Google Cloud SDK installer...")
397+
cmd := exec.Command("bash", "-c", "curl -fsSL https://sdk.cloud.google.com | bash -s -- --disable-prompts")
398+
cmd.Stdout = os.Stdout
399+
cmd.Stderr = os.Stderr
400+
if err := cmd.Run(); err != nil {
401+
fmt.Printf(" Install failed: %v\n", err)
402+
fmt.Println(" Install manually: https://cloud.google.com/sdk/install")
403+
return false
404+
}
405+
fmt.Println(" ✓ gcloud installed")
406+
return true
407+
408+
case "linux":
409+
fmt.Println(" Installing via Google Cloud SDK installer...")
410+
cmd := exec.Command("bash", "-c", "curl -fsSL https://sdk.cloud.google.com | bash -s -- --disable-prompts")
411+
cmd.Stdout = os.Stdout
412+
cmd.Stderr = os.Stderr
413+
if err := cmd.Run(); err != nil {
414+
fmt.Printf(" Install failed: %v\n", err)
415+
fmt.Println(" Install manually: https://cloud.google.com/sdk/install")
416+
return false
417+
}
418+
fmt.Println(" ✓ gcloud installed")
419+
return true
420+
421+
default:
422+
fmt.Println(" Auto-install not supported on this platform.")
423+
fmt.Println(" Install manually: https://cloud.google.com/sdk/install")
424+
return false
425+
}
426+
}
427+
428+
func runAutoSetup(cmd *cobra.Command) {
429+
clearScreen()
430+
431+
fmt.Println("╔═══════════════════════════════════════════════════════════════╗")
432+
fmt.Println("║ MAGIC SETUP — AUTOMATED MODE ║")
433+
fmt.Println("╚═══════════════════════════════════════════════════════════════╝")
434+
fmt.Println()
435+
436+
reader := bufio.NewReader(os.Stdin)
437+
438+
// --- Check gcloud ---
439+
fmt.Print("==> Checking gcloud CLI... ")
440+
gcloudPath, lookErr := exec.LookPath("gcloud")
441+
if lookErr != nil {
442+
fmt.Println("not found")
443+
fmt.Println()
444+
fmt.Print(" Install gcloud now? [Y/n]: ")
445+
if input := readInput(reader); input == "n" || input == "no" {
446+
fmt.Println()
447+
fmt.Println("Run 'gpc setup' without --auto for the manual wizard.")
448+
return
449+
}
450+
if !installGcloud() {
451+
return
452+
}
453+
gcloudPath, lookErr = exec.LookPath("gcloud")
454+
if lookErr != nil {
455+
fmt.Println()
456+
fmt.Println("gcloud still not found. You may need to restart your terminal.")
457+
fmt.Println("Then try 'gpc setup --auto' again.")
458+
return
459+
}
460+
}
461+
_ = gcloudPath
462+
fmt.Println("✓")
463+
464+
// --- Check auth ---
465+
fmt.Print("==> Checking gcloud auth... ")
466+
account, err := runGcloud("auth", "list", "--filter=status:ACTIVE", "--format=value(account)")
467+
if err != nil || account == "" {
468+
fmt.Println("not logged in")
469+
fmt.Println()
470+
fmt.Println(" Launching gcloud login...")
471+
loginCmd := exec.Command("gcloud", "auth", "login")
472+
loginCmd.Stdin = os.Stdin
473+
loginCmd.Stdout = os.Stdout
474+
loginCmd.Stderr = os.Stderr
475+
if err := loginCmd.Run(); err != nil {
476+
fmt.Println()
477+
fmt.Println("Login failed. Try running 'gcloud auth login' manually,")
478+
fmt.Println("then 'gpc setup --auto' again.")
479+
return
480+
}
481+
// Re-check
482+
account, _ = runGcloud("auth", "list", "--filter=status:ACTIVE", "--format=value(account)")
483+
if account == "" {
484+
fmt.Println("Login did not complete. Try 'gpc setup --auto' again.")
485+
return
486+
}
487+
}
488+
fmt.Printf("✓ (%s)\n", account)
489+
fmt.Println()
490+
491+
// --- Step 1: Project ---
492+
fmt.Print("==> [1/5] Google Cloud Project... ")
493+
project, _ := runGcloud("config", "get-value", "project")
494+
if project == "" || project == "(unset)" {
495+
fmt.Println()
496+
fmt.Print(" No project set. Enter your GCP project ID: ")
497+
project = readInput(reader)
498+
if project == "" {
499+
fmt.Println(" Project ID is required. Aborting.")
500+
return
501+
}
502+
runGcloud("config", "set", "project", project)
503+
fmt.Printf(" Set project: %s\n", project)
504+
} else {
505+
fmt.Printf("✓ (%s)\n", project)
506+
}
507+
508+
// --- Step 2: Enable API ---
509+
fmt.Print("==> [2/5] Enabling Google Play Android Developer API... ")
510+
_, err = runGcloud("services", "enable", "androidpublisher.googleapis.com", "--project", project)
511+
if err != nil {
512+
fmt.Println("✗")
513+
fmt.Printf(" Error: %v\n", err)
514+
return
515+
}
516+
fmt.Println("✓")
517+
518+
// --- Step 3: Create service account ---
519+
saEmail := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", serviceAccountName, project)
520+
fmt.Print("==> [3/5] Creating service account... ")
521+
_, err = runGcloud("iam", "service-accounts", "create", serviceAccountName,
522+
"--display-name", "Play Console CLI",
523+
"--project", project)
524+
if err != nil {
525+
if strings.Contains(err.Error(), "already exists") {
526+
fmt.Println("✓ (already exists)")
527+
} else {
528+
fmt.Println("✗")
529+
fmt.Printf(" Error: %v\n", err)
530+
return
531+
}
532+
} else {
533+
fmt.Println("✓")
534+
}
535+
fmt.Printf(" Email: %s\n", saEmail)
536+
537+
// --- Step 4: Download key ---
538+
homeDir, _ := os.UserHomeDir()
539+
configDir := filepath.Join(homeDir, ".config", "gpc")
540+
keyPath := filepath.Join(configDir, "service-account.json")
541+
542+
fmt.Print("==> [4/5] Downloading credentials... ")
543+
544+
// Check if key file already exists
545+
if _, err := os.Stat(keyPath); err == nil {
546+
fmt.Println()
547+
fmt.Printf(" Credentials already exist at %s\n", keyPath)
548+
fmt.Print(" Overwrite? [y/N]: ")
549+
if input := readInput(reader); input != "y" && input != "yes" {
550+
fmt.Println(" Keeping existing credentials.")
551+
goto step5
552+
}
553+
}
554+
555+
os.MkdirAll(configDir, 0700)
556+
_, err = runGcloud("iam", "service-accounts", "keys", "create", keyPath,
557+
"--iam-account", saEmail,
558+
"--project", project)
559+
if err != nil {
560+
fmt.Println("✗")
561+
fmt.Printf(" Error: %v\n", err)
562+
return
563+
}
564+
os.Chmod(keyPath, 0600)
565+
fmt.Println("✓")
566+
fmt.Printf(" Saved to %s\n", keyPath)
567+
568+
step5:
569+
// --- Step 5: Manual — Grant access in Play Console ---
570+
playConsoleURL := "https://play.google.com/console/developers/api-access"
571+
fmt.Println()
572+
fmt.Println("==> [5/5] Grant access in Play Console")
573+
fmt.Println()
574+
fmt.Println(" Almost done! This is the one step that can't be automated.")
575+
fmt.Println(" Opening Play Console now...")
576+
fmt.Println()
577+
fmt.Println(" Just do this:")
578+
fmt.Println(" 1. Click \"Link\" next to your Cloud project")
579+
fmt.Printf(" 2. Find: %s\n", saEmail)
580+
fmt.Println(" 3. Click \"Grant access\" → set permissions → \"Invite user\"")
581+
fmt.Println()
582+
copyToClipboard(saEmail)
583+
fmt.Println(" (Service account email copied to clipboard)")
584+
openBrowser(playConsoleURL)
585+
waitForUser(reader, " Press ENTER when done...")
586+
587+
// --- Configure CLI profile ---
588+
execPath, _ := os.Executable()
589+
590+
fmt.Println()
591+
fmt.Print("Enter your app's package name (or ENTER to skip): ")
592+
packageName := readInput(reader)
593+
594+
fmt.Print("==> Configuring CLI profile... ")
595+
596+
authArgs := []string{"auth", "login", "--name", "default", "--credentials", keyPath}
597+
if packageName != "" {
598+
authArgs = append(authArgs, "--default-package", packageName)
599+
}
600+
601+
authCmd := exec.Command(execPath, authArgs...)
602+
authCmd.Stdout = nil
603+
authCmd.Stderr = nil
604+
605+
if err := authCmd.Run(); err != nil {
606+
fmt.Println("✗")
607+
fmt.Printf(" Run manually: gpc auth login --name default --credentials %s\n", keyPath)
608+
} else {
609+
fmt.Println("✓")
610+
}
611+
612+
// --- Done ---
613+
fmt.Println()
614+
fmt.Println("╔═══════════════════════════════════════════════════════════════╗")
615+
fmt.Println("║ SETUP COMPLETE! ║")
616+
fmt.Println("╚═══════════════════════════════════════════════════════════════╝")
617+
fmt.Println()
618+
fmt.Println("You can now use gpc commands:")
619+
fmt.Println(" gpc tracks list")
620+
fmt.Println(" gpc bundles upload --file app.aab --track internal")
621+
fmt.Println(" gpc reviews list")
622+
fmt.Println()
623+
if packageName != "" {
624+
fmt.Printf("Default package: %s\n", packageName)
625+
} else {
626+
fmt.Println("Tip: Use --package <name> or set GPC_PACKAGE env var")
627+
}
628+
fmt.Println()
629+
fmt.Println("Run 'gpc doctor' to verify everything is connected.")
630+
}

0 commit comments

Comments
 (0)