diff --git a/internal/appsetup/appsetup_test.go b/internal/appsetup/appsetup_test.go index 219d548a47..49a3ce961d 100644 --- a/internal/appsetup/appsetup_test.go +++ b/internal/appsetup/appsetup_test.go @@ -365,6 +365,36 @@ func TestSetup_ExistingApp_OrgOwned_NoSecret_EntersRecovery(t *testing.T) { assert.Contains(t, err.Error(), "private key secret is missing") } +func TestSetup_ExistingApp_NoSecretExistsFunc_ReuseSilently(t *testing.T) { + // When no secretExists callback is configured (e.g. github setup in + // OIDC mint mode with no local GCP project), existing apps should be + // reused silently without prompting for a PEM file. + client := &forge.FakeClient{ + Installations: []forge.Installation{ + {ID: 100, AppID: 10, AppSlug: "fullsend-triage"}, + }, + AppClientIDs: map[string]string{ + "fullsend-triage": "Iv1.triage123", + }, + } + prompter := &fakePrompter{} + printer := ui.New(&discardWriter{}) + + s := NewSetup(client, prompter, newFakeBrowser(), printer). + WithAppSet("fullsend") + // Deliberately NOT calling WithSecretExists — simulates the OIDC + // mint-URL-only path where the remote mint manages PEMs. + + creds, err := s.Run(context.Background(), "myorg", "triage") + require.NoError(t, err) + assert.Equal(t, 10, creds.AppID) + assert.Equal(t, "fullsend-triage", creds.Slug) + assert.Equal(t, "Iv1.triage123", creds.ClientID) + assert.Empty(t, creds.PEM, "PEM should be empty to signal reuse") + assert.False(t, prompter.confirmCalled, "should not prompt for PEM recovery") + assert.False(t, prompter.readLineCalled, "should not ask for PEM file path") +} + func TestIsOrgOwned(t *testing.T) { s := &Setup{} diff --git a/internal/cli/admin.go b/internal/cli/admin.go index 0e23ad809d..3244ea40b0 100644 --- a/internal/cli/admin.go +++ b/internal/cli/admin.go @@ -532,7 +532,7 @@ Inference authentication: if err := ensureConfigRepoExists(ctx, client, printer, org); err != nil { return err } - creds, err := runAppSetup(ctx, client, printer, org, roles, mintProject, publicApps, sharedSlugs, appSet, perOrgStoredIDs) + creds, err := runAppSetup(ctx, client, printer, org, roles, mintProject, mintURL, publicApps, sharedSlugs, appSet, perOrgStoredIDs) if err != nil { return err } @@ -893,7 +893,7 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error { } } - creds, credErr := runAppSetup(ctx, client, printer, owner, roles, mintProject, publicApps, sharedSlugs, c.AppSet, existingIDs) + creds, credErr := runAppSetup(ctx, client, printer, owner, roles, mintProject, mintURL, publicApps, sharedSlugs, c.AppSet, existingIDs) if credErr != nil { return credErr } @@ -1310,8 +1310,11 @@ func detectSharedApps(ctx context.Context, client forge.Client, printer *ui.Prin // runAppSetup creates or reuses GitHub Apps for each role. When mintProject is // non-empty, PEMs are also stored in GCP Secret Manager during app creation so -// they survive partial provisioning failures. -func runAppSetup(ctx context.Context, client forge.Client, printer *ui.Printer, org string, roles []string, mintProject string, publicApps bool, sharedSlugs map[string]string, appSet string, storedAppIDs map[string]string) ([]layers.AgentCredentials, error) { +// they survive partial provisioning failures. When mintURL is non-empty but +// mintProject is empty (e.g. the "github setup" flow), PEMs are managed by a +// remote mint — the secret-existence check is skipped and existing apps are +// reused silently. +func runAppSetup(ctx context.Context, client forge.Client, printer *ui.Printer, org string, roles []string, mintProject string, mintURL string, publicApps bool, sharedSlugs map[string]string, appSet string, storedAppIDs map[string]string) ([]layers.AgentCredentials, error) { printer.Header("Setting up GitHub Apps") printer.Blank() @@ -1343,26 +1346,31 @@ func runAppSetup(ctx context.Context, client forge.Client, printer *ui.Printer, }, gcf.NewLiveGCFClient(mintProject)) } - // In OIDC mint mode, PEMs live in Secret Manager — check there. + // In OIDC mint mode with direct GCP access, PEMs live in Secret + // Manager — check there. When only a mint URL is available (no local + // GCP project), the remote mint manages PEMs and we cannot verify + // their existence — skip the check so handleExistingApp assumes reuse. // Otherwise, check GitHub repo secrets. if pemProvisioner != nil { setup = setup.WithSecretExists(func(role string) (bool, error) { return pemProvisioner.SecretExists(ctx, role) }) - } else { + } else if mintURL == "" { setup = setup.WithSecretExists(func(role string) (bool, error) { secretName := fmt.Sprintf("FULLSEND_%s_APP_PRIVATE_KEY", strings.ToUpper(role)) return client.RepoSecretExists(ctx, org, forge.ConfigRepoName, secretName) }) } - // In OIDC mint mode, store PEMs only in Secret Manager. + // In OIDC mint mode with direct GCP access, store PEMs only in Secret + // Manager. When only a mint URL is available, PEM storage is handled + // by the remote mint — skip local storage. // Otherwise, store in GitHub repo secrets. if pemProvisioner != nil { setup = setup.WithStoreSecret(func(sctx context.Context, role, pem string) error { return pemProvisioner.StoreAgentPEM(sctx, role, []byte(pem)) }) - } else { + } else if mintURL == "" { setup = setup.WithStoreSecret(func(sctx context.Context, role, pem string) error { secretName := fmt.Sprintf("FULLSEND_%s_APP_PRIVATE_KEY", strings.ToUpper(role)) return client.CreateRepoSecret(sctx, org, forge.ConfigRepoName, secretName, pem) diff --git a/internal/cli/github.go b/internal/cli/github.go index ed695b7213..6e5d8b9393 100644 --- a/internal/cli/github.go +++ b/internal/cli/github.go @@ -499,7 +499,7 @@ func runGitHubSetupPerOrg(ctx context.Context, client forge.Client, printer *ui. return err } - creds, credErr := runAppSetup(ctx, client, printer, org, roles, "", cfg.publicApps, nil, cfg.appSet, nil) + creds, credErr := runAppSetup(ctx, client, printer, org, roles, "", cfg.mintURL, cfg.publicApps, nil, cfg.appSet, nil) if credErr != nil { return credErr }