Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions internal/appsetup/appsetup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}

Expand Down
24 changes: 16 additions & 8 deletions internal/cli/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading