diff --git a/docs/cli/repos.md b/docs/cli/repos.md index a3e97e10a..1b0b776dc 100644 --- a/docs/cli/repos.md +++ b/docs/cli/repos.md @@ -53,7 +53,7 @@ fullsend repos init --mint-project The command discovers repos by checking: 1. **Per-repo guard variable** (`FULLSEND_PER_REPO_INSTALL`) — identifies per-repo installations -2. **Per-org config enrollment** (`config.yaml` in `.fullsend` repo) — identifies per-org installations +2. **Per-org config enrollment** (`config.yaml` in `.fullsend` repo) — identifies per-org installations; if no mint URL is set in the org config, falls back to the `FULLSEND_MINT_URL` org-level Actions variable 3. **Workflow ref** — extracts the `@ref` from scaffold shim workflow files ### Defaults computation diff --git a/docs/plans/repos-init.md b/docs/plans/repos-init.md index fe84a0dda..a2503a50f 100644 --- a/docs/plans/repos-init.md +++ b/docs/plans/repos-init.md @@ -166,7 +166,9 @@ manifest contains one entry. - Mark `source: per-repo`. - Else if repo appears in per-org enrollment with `enabled: true`: - - Use mint URL and config from per-org `config.yaml`. + - Use mint URL and config from per-org `config.yaml`. If no + mint URL is set in `config.yaml`, fall back to reading the + `FULLSEND_MINT_URL` org-level variable via `GetOrgVariable`. - Read the per-org shim workflow file and extract `@ref` from the `uses:` line (same as per-repo discovery). Do not use `config.DefaultUpstreamRef` — it is `v0`, a major-version diff --git a/internal/repos/init.go b/internal/repos/init.go index 2ab660246..4082ce9c1 100644 --- a/internal/repos/init.go +++ b/internal/repos/init.go @@ -367,6 +367,15 @@ func discoverRepo(ctx context.Context, client forge.Client, if mintURL := orgCfg.DispatchSettings().MintURL; mintURL != "" { d.MintURL = mintURL } + if d.MintURL == "" { + v, exists, err := client.GetOrgVariable(ctx, owner, "FULLSEND_MINT_URL") + if err != nil { + progress(fullName, "discover", fmt.Sprintf("warning: could not read org variable FULLSEND_MINT_URL: %v", err)) + } + if err == nil && exists { + d.MintURL = v + } + } d.FullsendRef = ref return d, nil } diff --git a/internal/repos/init_test.go b/internal/repos/init_test.go index f7099a29c..cefd15635 100644 --- a/internal/repos/init_test.go +++ b/internal/repos/init_test.go @@ -3,6 +3,7 @@ package repos import ( "context" "fmt" + "strings" "testing" "github.com/fullsend-ai/fullsend/internal/config" @@ -1026,6 +1027,111 @@ func TestDiscoverRepo_New(t *testing.T) { assert.Empty(t, d.FullsendRef) } +// --- discoverRepo: org variable fallback tests --- + +func TestDiscoverRepo_PerOrg_OrgVarFallback(t *testing.T) { + fc := forge.NewFakeClient() + setWorkflowFile(fc, "acme", "api", + " uses: fullsend-ai/fullsend/.github/workflows/reusable-dispatch.yml@v2.1.0") + + // Org config has no mint_url in dispatch settings. + orgCfg, parseErr := config.ParseOrgConfig([]byte(`version: "1" +dispatch: + platform: github-actions +defaults: + roles: [triage] +repos: + api: + enabled: true +`)) + require.NoError(t, parseErr) + + // Set org-level variable as fallback. + fc.OrgVariables = map[string]bool{ + "acme/FULLSEND_MINT_URL": true, + } + fc.OrgVariableValues = map[string]string{ + "acme/FULLSEND_MINT_URL": "https://mint.example.com", + } + + d, err := discoverRepo(context.Background(), fc, "acme", "api", orgCfg, nopProgress) + require.NoError(t, err) + assert.Equal(t, "per-org", d.Source) + assert.Equal(t, "https://mint.example.com", d.MintURL) +} + +func TestDiscoverRepo_PerOrg_OrgConfigTakesPrecedence(t *testing.T) { + fc := forge.NewFakeClient() + setWorkflowFile(fc, "acme", "api", + " uses: fullsend-ai/fullsend/.github/workflows/reusable-dispatch.yml@v2.1.0") + + // Org config has a mint_url. + orgCfg, parseErr := config.ParseOrgConfig([]byte(`version: "1" +dispatch: + platform: github-actions + mint_url: https://config-mint.example.com +defaults: + roles: [triage] +repos: + api: + enabled: true +`)) + require.NoError(t, parseErr) + + // Org variable also set — should NOT be used. + fc.OrgVariables = map[string]bool{ + "acme/FULLSEND_MINT_URL": true, + } + fc.OrgVariableValues = map[string]string{ + "acme/FULLSEND_MINT_URL": "https://var-mint.example.com", + } + + d, err := discoverRepo(context.Background(), fc, "acme", "api", orgCfg, nopProgress) + require.NoError(t, err) + assert.Equal(t, "per-org", d.Source) + assert.Equal(t, "https://config-mint.example.com", d.MintURL) +} + +func TestDiscoverRepo_PerOrg_OrgVarError_NonFatal(t *testing.T) { + fc := forge.NewFakeClient() + setWorkflowFile(fc, "acme", "api", + " uses: fullsend-ai/fullsend/.github/workflows/reusable-dispatch.yml@v2.1.0") + fc.Errors["GetOrgVariable"] = fmt.Errorf("forbidden") + + // Org config has no mint_url. + orgCfg, parseErr := config.ParseOrgConfig([]byte(`version: "1" +dispatch: + platform: github-actions +defaults: + roles: [triage] +repos: + api: + enabled: true +`)) + require.NoError(t, parseErr) + + var warnings []string + progress := func(_, _, msg string) { + warnings = append(warnings, msg) + } + + d, err := discoverRepo(context.Background(), fc, "acme", "api", orgCfg, progress) + require.NoError(t, err) + assert.Equal(t, "per-org", d.Source) + assert.Empty(t, d.MintURL) + + // Should have logged a warning about the org variable. + hasWarning := false + for _, w := range warnings { + if strings.Contains(w, "FULLSEND_MINT_URL") && + strings.Contains(w, "warning") { + hasWarning = true + break + } + } + assert.True(t, hasWarning, "expected a warning about GetOrgVariable failure") +} + // --- readWorkflowRef tests --- func TestReadWorkflowRef_YmlExtension(t *testing.T) {