@@ -12,6 +12,7 @@ import (
1212 "github.com/infobloxopen/apx/internal/interactive"
1313 "github.com/infobloxopen/apx/internal/schema"
1414 "github.com/infobloxopen/apx/internal/ui"
15+ "github.com/infobloxopen/apx/pkg/githubauth"
1516 "github.com/spf13/cobra"
1617)
1718
@@ -54,7 +55,7 @@ func newInitCanonicalCmd() *cobra.Command {
5455 cmd .Flags ().String ("site-url" , "" , "Custom domain for the catalog site (e.g. apis.internal.infoblox.dev)" )
5556 cmd .Flags ().Bool ("skip-git" , false , "Skip git initialization" )
5657 cmd .Flags ().Bool ("non-interactive" , false , "Disable interactive prompts and require all flags" )
57- cmd .Flags ().Bool ("setup-github" , false , "Configure GitHub repo settings (branch/tag protection, org secrets) via gh CLI " )
58+ cmd .Flags ().Bool ("setup-github" , false , "Configure GitHub repo settings (apps, branch/tag protection, org secrets)" )
5859 cmd .Flags ().String ("app-id" , "" , "GitHub App ID for org secrets (used with --setup-github)" )
5960 cmd .Flags ().String ("app-pem-file" , "" , "Path to GitHub App private key PEM file (used with --setup-github)" )
6061 return cmd
@@ -71,7 +72,7 @@ func newInitAppCmd() *cobra.Command {
7172 cmd .Flags ().String ("repo" , "" , "Repository name" )
7273 cmd .Flags ().String ("import-root" , "" , "Custom public Go import prefix (e.g. go.acme.dev/apis)" )
7374 cmd .Flags ().Bool ("non-interactive" , false , "Disable interactive prompts and require all flags" )
74- cmd .Flags ().Bool ("setup-github" , false , "Configure GitHub repo settings (branch protection) via gh CLI " )
75+ cmd .Flags ().Bool ("setup-github" , false , "Configure GitHub repo settings (branch protection)" )
7576 return cmd
7677}
7778
@@ -251,70 +252,117 @@ func initCanonicalAction(cmd *cobra.Command, args []string) error {
251252 ui .Success ("\u2713 Generated .github/workflows/ci.yml" )
252253 ui .Success ("\u2713 Generated .github/workflows/on-merge.yml" )
253254
254- // --setup-github: configure GitHub repo settings via gh CLI
255+ // --setup-github: configure GitHub repo settings
255256 setupGitHub , _ := cmd .Flags ().GetBool ("setup-github" )
256257 if setupGitHub {
257- // Preflight: verify gh is installed, authenticated, and has required scopes.
258- if err := gh .CheckGHAuth (); err != nil {
259- return fmt .Errorf ("GitHub setup preflight failed: %w" , err )
260- }
261- if err := gh .CheckGHScopes (); err != nil {
262- return fmt .Errorf ("GitHub setup preflight failed: %w" , err )
263- }
264-
265258 appID , _ := cmd .Flags ().GetString ("app-id" )
266259 pemFile , _ := cmd .Flags ().GetString ("app-pem-file" )
267260
268- // Try to resolve from cache if flags are not provided.
261+ // ── Step 1: User App ────────────────────────────────────────
262+ // Create the user-facing GitHub App (apx-{org}-user) if not cached.
263+ // This app provides device-flow OAuth for human users.
264+ userClientID := gh .GetCachedUserAppClientID (org )
265+ if userClientID == "" {
266+ if nonInteractive {
267+ return fmt .Errorf ("user app not configured for org %q; run interactively first" , org )
268+ }
269+ ui .Info ("\n Creating user app %q via GitHub App manifest flow..." , gh .UserAppName (org ))
270+ creds , createErr := gh .CreateAppViaManifest (org , gh .UserAppName (org ), gh .UserAppPermissions )
271+ if createErr != nil {
272+ return fmt .Errorf ("failed to create user app: %w" , createErr )
273+ }
274+ if err := gh .CacheUserAppClientID (org , creds .ClientID ); err != nil {
275+ return fmt .Errorf ("failed to cache user app client ID: %w" , err )
276+ }
277+ if err := gh .CacheUserAppID (org , fmt .Sprintf ("%d" , creds .ID )); err != nil {
278+ return fmt .Errorf ("failed to cache user app ID: %w" , err )
279+ }
280+ if creds .Slug != "" {
281+ if err := gh .CacheUserAppSlug (org , creds .Slug ); err != nil {
282+ ui .Warning ("Failed to cache user app slug: %v" , err )
283+ }
284+ }
285+ userClientID = creds .ClientID
286+ ui .Success ("User app created! Client ID: %s" , userClientID )
287+ } else {
288+ ui .Info ("User app already configured (client_id cached)." )
289+ }
290+
291+ // ── Step 2: Device flow login ───────────────────────────────
292+ // Authenticate the user via OAuth device flow to get a token.
293+ token , tokenErr := githubauth .EnsureToken (org )
294+ if tokenErr != nil {
295+ return fmt .Errorf ("GitHub authentication failed: %w" , tokenErr )
296+ }
297+ client := githubauth .NewClient (token )
298+
299+ // ── Step 3: Ensure user app is installed ────────────────────
300+ userAppIDStr := gh .GetCachedUserAppID (org )
301+ userAppSlug := gh .GetCachedUserAppSlug (org )
302+ if userAppIDStr != "" && userAppSlug != "" {
303+ userAppIDInt , _ := strconv .Atoi (userAppIDStr )
304+ if userAppIDInt > 0 {
305+ if err := gh .EnsureAppInstalled (client , org , userAppIDInt , userAppSlug ); err != nil {
306+ ui .Warning ("Could not verify user app installation: %v" , err )
307+ }
308+ }
309+ }
310+
311+ // ── Step 4: CI App ──────────────────────────────────────────
312+ // Create the CI GitHub App (apx-{repo}-{org}) if not cached.
269313 if appID == "" {
270314 appID = gh .GetCachedAppID (org )
271315 }
272316 pemCached := false
273- if cachePath , err := gh .PEMCachePath (org ); err == nil {
317+ if cachePath , pemErr := gh .PEMCachePath (org ); pemErr == nil {
274318 if _ , statErr := os .Stat (cachePath ); statErr == nil {
275319 pemCached = true
276320 }
277321 }
278322
279323 needsApp := appID == "" || (! pemCached && pemFile == "" )
280324 if needsApp && ! nonInteractive {
281- ui .Info ("\n No GitHub App configured for org %q." , org )
282- ui .Info ("Creating one via the GitHub App manifest flow...\n " )
283-
284- newAppID , appSlug , pemContents , err := gh .CreateAppViaManifest (org , repo )
285- if err != nil {
286- return fmt .Errorf ("failed to create GitHub App: %w" , err )
325+ ui .Info ("\n Creating CI app %q via GitHub App manifest flow..." , gh .CIAppName (repo , org ))
326+ creds , createErr := gh .CreateAppViaManifest (org , gh .CIAppName (repo , org ), gh .CIAppPermissions )
327+ if createErr != nil {
328+ return fmt .Errorf ("failed to create CI app: %w" , createErr )
287329 }
288-
289- if err := gh .CachePEMFromContents (org , pemContents ); err != nil {
330+ if err := gh .CachePEMFromContents (org , creds .PEM ); err != nil {
290331 return fmt .Errorf ("failed to cache PEM: %w" , err )
291332 }
292- if err := gh .CacheAppID (org , newAppID ); err != nil {
293- return fmt .Errorf ("failed to cache app ID: %w" , err )
333+ if err := gh .CacheAppID (org , fmt . Sprintf ( "%d" , creds . ID ) ); err != nil {
334+ return fmt .Errorf ("failed to cache CI app ID: %w" , err )
294335 }
295- if appSlug != "" {
296- if err := gh .CacheAppSlug (org , appSlug ); err != nil {
297- ui .Warning ("Failed to cache app slug: %v" , err )
336+ if creds . Slug != "" {
337+ if err := gh .CacheAppSlug (org , creds . Slug ); err != nil {
338+ ui .Warning ("Failed to cache CI app slug: %v" , err )
298339 }
299340 }
341+ appID = fmt .Sprintf ("%d" , creds .ID )
342+ ui .Success ("CI app created! App ID: %s" , appID )
300343
301- appID = newAppID
302- ui .Success ("GitHub App created! App ID: %s" , appID )
344+ // Ensure CI app is installed on the org.
345+ if creds .Slug != "" {
346+ if err := gh .EnsureAppInstalled (client , org , creds .ID , creds .Slug ); err != nil {
347+ ui .Warning ("Could not verify CI app installation: %v" , err )
348+ }
349+ }
303350 } else if needsApp {
304351 return fmt .Errorf ("--app-id and --app-pem-file are required with --setup-github in non-interactive mode" )
305352 } else {
306- // App already exists – ensure it is installed on the org
307- appSlug := gh .GetCachedAppSlug (org )
308- if appSlug != "" {
353+ // CI app already exists – ensure it is installed on the org.
354+ ciSlug := gh .GetCachedAppSlug (org )
355+ if ciSlug != "" {
309356 appIDInt , _ := strconv .Atoi (appID )
310357 if appIDInt > 0 {
311- if err := gh .EnsureAppInstalled (org , appIDInt , appSlug ); err != nil {
312- ui .Warning ("Could not verify app installation: %v" , err )
358+ if err := gh .EnsureAppInstalled (client , org , appIDInt , ciSlug ); err != nil {
359+ ui .Warning ("Could not verify CI app installation: %v" , err )
313360 }
314361 }
315362 }
316363 }
317364
365+ // ── Step 5: Set up canonical repo ───────────────────────────
318366 // Resolve siteURL from config if not provided via flag.
319367 if siteURL == "" {
320368 if cfg , cfgErr := config .LoadRaw ("apx.yaml" ); cfgErr == nil && cfg .SiteURL != "" {
@@ -323,9 +371,9 @@ func initCanonicalAction(cmd *cobra.Command, args []string) error {
323371 }
324372
325373 ui .Info ("\n Configuring GitHub repository..." )
326- res , err := gh .SetupCanonicalRepo (org , repo , appID , pemFile , siteURL )
327- if err != nil {
328- return fmt .Errorf ("GitHub setup failed: %w" , err )
374+ res , setupErr := gh .SetupCanonicalRepo (client , org , repo , appID , pemFile , siteURL )
375+ if setupErr != nil {
376+ return fmt .Errorf ("GitHub setup failed: %w" , setupErr )
329377 }
330378 res .Print ()
331379 }
@@ -342,7 +390,7 @@ func initCanonicalAction(cmd *cobra.Command, args []string) error {
342390 ui .Info (" - Restrict direct pushes to main" )
343391 ui .Info ("5. Push: git remote add origin <url> && git push -u origin main" )
344392 ui .Info ("" )
345- ui .Info ("Or re-run with --setup-github to configure automatically via gh CLI ." )
393+ ui .Info ("Or re-run with --setup-github to configure automatically." )
346394 }
347395
348396 ui .Success ("\n \u2713 Canonical API repository initialized successfully!" )
@@ -460,16 +508,19 @@ func initAppAction(cmd *cobra.Command, args []string) error {
460508 ui .Success ("\u2713 Generated buf.work.yaml" )
461509 ui .Success ("\u2713 Generated .github/workflows/apx-release.yml" )
462510
463- // --setup-github: configure GitHub repo settings via gh CLI
511+ // --setup-github: configure GitHub repo settings
464512 setupGitHub , _ := cmd .Flags ().GetBool ("setup-github" )
465513 if setupGitHub {
466- if err := gh .CheckGHAuth (); err != nil {
467- return fmt .Errorf ("GitHub setup preflight failed: %w" , err )
514+ token , tokenErr := githubauth .EnsureToken (org )
515+ if tokenErr != nil {
516+ return fmt .Errorf ("GitHub authentication failed: %w" , tokenErr )
468517 }
518+ client := githubauth .NewClient (token )
519+
469520 ui .Info ("\n Configuring GitHub repository..." )
470- res , err := gh .SetupAppRepo (org , repo )
471- if err != nil {
472- return fmt .Errorf ("GitHub setup failed: %w" , err )
521+ res , setupErr := gh .SetupAppRepo (client , org , repo )
522+ if setupErr != nil {
523+ return fmt .Errorf ("GitHub setup failed: %w" , setupErr )
473524 }
474525 res .Print ()
475526 }
0 commit comments