From 210a88fe7e54f5e6d2877f4ef7962f191c4753b2 Mon Sep 17 00:00:00 2001 From: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:30:03 +0000 Subject: [PATCH 1/4] feat(#2722): add loader role for self-minting tokens during harness loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The binary needs a GitHub token to create a ForgeClient for LoadWithBase() when fetching URL-based skill directories, but mintAgentToken() requires the harness role — which is only known after loading. PR #2720 worked around this by passing GH_TOKEN from action.yml, leaving an over-privileged ambient token. Add a dedicated "loader" mint role with minimal permissions (contents:read, metadata:read) and a mintLoaderToken() helper that runs before LoadWithBase. The binary now self-mints a short-lived loader token when OIDC is available, falling back to resolveToken() for local dev. The action.yml GH_TOKEN injection in the "Run fullsend" step is removed. Changes: - internal/mintcore/github.go: add "loader" role to canonicalRolePermissions - internal/cli/run.go: add hasOIDCEnv() and mintLoaderToken() helpers; move mintURL resolution before LoadWithBase; wire loader mint before resolveToken() fallback - action.yml: remove GH_TOKEN from "Run fullsend" step - Sync embedded copy per AGENTS.md rules Closes #2722 --- action.yml | 1 - internal/cli/run.go | 58 +++++++++++- internal/cli/run_test.go | 91 +++++++++++++++++++ .../gcf/mintsrc/mintcore/github.go.embed | 1 + internal/mintcore/github.go | 1 + internal/mintcore/github_test.go | 10 +- 6 files changed, 156 insertions(+), 6 deletions(-) diff --git a/action.yml b/action.yml index 2c8df3fb72..b91ac8ffd7 100644 --- a/action.yml +++ b/action.yml @@ -354,7 +354,6 @@ runs: if: inputs.agent != '__install_only__' shell: bash env: - GH_TOKEN: ${{ inputs.github_token }} AGENT: ${{ inputs.agent }} FULLSEND_DIR: ${{ inputs.fullsend-dir }} TARGET_REPO: ${{ inputs.target-repo }} diff --git a/internal/cli/run.go b/internal/cli/run.go index 76694c7fd4..ffa933dc81 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -210,9 +210,18 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep } } + // Resolve mintURL early so it is available for the loader token mint + // before LoadWithBase. The same value is reused for mintAgentToken later. + mintURL := sOpts.mintURL + if mintURL == "" { + mintURL = os.Getenv("FULLSEND_MINT_URL") + } + var composeForgeClient forge.Client if rFlags.forgeClient != nil { composeForgeClient = rFlags.forgeClient + } else if token, loaderErr := mintLoaderToken(ctx, mintURL, printer); loaderErr == nil { + composeForgeClient = gh.New(token) } else if token, tokenErr := resolveToken(); tokenErr == nil { composeForgeClient = gh.New(token) } @@ -342,10 +351,6 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep // Mint agent token when a mint URL and harness role are both available. // Runs before env expansion so minted tokens flow into RunnerEnv and // host_files via os.Getenv automatically. - mintURL := sOpts.mintURL - if mintURL == "" { - mintURL = os.Getenv("FULLSEND_MINT_URL") - } minted, mintCleanup, err := mintAgentToken(ctx, h.Role, mintURL, printer) if err != nil { return fmt.Errorf("agent token minting failed: %w", err) @@ -2127,6 +2132,51 @@ var roleTokenVars = map[string][]tokenVar{ "review": {{Name: "REVIEW_TOKEN"}}, } +// hasOIDCEnv reports whether the GitHub Actions OIDC environment variables +// are present. These are set automatically when the job declares +// id-token: write permission. +func hasOIDCEnv() bool { + return os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL") != "" && + os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") != "" +} + +// mintLoaderToken mints a minimal-permission token using the "loader" role +// for resolving URL-based harness bases and skill directories before the +// harness role is known. Returns the token string on success, or an error +// if OIDC is unavailable or the mint fails. The token is short-lived and +// used only for ForgeClient construction during LoadWithBase. +func mintLoaderToken(ctx context.Context, mintURL string, printer *ui.Printer) (string, error) { + if mintURL == "" || !hasOIDCEnv() { + return "", fmt.Errorf("loader mint unavailable: no mint URL or OIDC env") + } + + repos, err := resolveMintRepos() + if err != nil { + return "", fmt.Errorf("resolving mint repos for loader: %w", err) + } + + printer.StepStart("Minting loader token (contents:read)") + result, err := statusMintToken(ctx, mintclient.MintRequest{ + MintURL: mintURL, + Role: "loader", + Repos: repos, + }) + if err != nil { + return "", fmt.Errorf("minting loader token: %w", err) + } + + if !mintTokenPattern.MatchString(result.Token) { + return "", fmt.Errorf("minted loader token contains unexpected characters") + } + + if os.Getenv("GITHUB_ACTIONS") == "true" { + fmt.Fprintf(os.Stderr, "::add-mask::%s\n", result.Token) + } + + printer.StepDone("Loader token minted") + return result.Token, nil +} + // mintAgentToken mints a GitHub App installation token for the agent's role // and sets the appropriate env vars so RunnerEnv expansion and host_files // expansion pick them up. Returns (minted bool, cleanup func, err). diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index de3ce8dda6..34b24cd553 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -2471,3 +2471,94 @@ func TestRunAgent_StatusNotifierSetup(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "openshell") } + +func TestHasOIDCEnv(t *testing.T) { + tests := []struct { + name string + url string + token string + expected bool + }{ + {"both set", "https://token.actions.githubusercontent.com", "tok", true}, + {"url empty", "", "tok", false}, + {"token empty", "https://token.actions.githubusercontent.com", "", false}, + {"both empty", "", "", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", tc.url) + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", tc.token) + assert.Equal(t, tc.expected, hasOIDCEnv()) + }) + } +} + +func TestMintLoaderToken_NoMintURL(t *testing.T) { + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://example.com") + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "tok") + + var buf bytes.Buffer + printer := ui.New(&buf) + + _, err := mintLoaderToken(context.Background(), "", printer) + require.Error(t, err) + assert.Contains(t, err.Error(), "loader mint unavailable") +} + +func TestMintLoaderToken_NoOIDCEnv(t *testing.T) { + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "") + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "") + + var buf bytes.Buffer + printer := ui.New(&buf) + + _, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + require.Error(t, err) + assert.Contains(t, err.Error(), "loader mint unavailable") +} + +func TestMintLoaderToken_Success(t *testing.T) { + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://oidc.example.com") + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-tok") + t.Setenv("REPO_FULL_NAME", "org/my-repo") + + origMint := statusMintToken + defer func() { statusMintToken = origMint }() + + var capturedReq mintclient.MintRequest + statusMintToken = func(_ context.Context, req mintclient.MintRequest) (*mintclient.MintResult, error) { + capturedReq = req + return &mintclient.MintResult{Token: "ghs_loader_token", ExpiresAt: "2026-06-15T12:00:00Z"}, nil + } + + var buf bytes.Buffer + printer := ui.New(&buf) + + token, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + require.NoError(t, err) + assert.Equal(t, "ghs_loader_token", token) + assert.Equal(t, "loader", capturedReq.Role) + assert.Equal(t, "https://mint.example.com", capturedReq.MintURL) + assert.Contains(t, buf.String(), "Loader token minted") +} + +func TestMintLoaderToken_MintError(t *testing.T) { + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://oidc.example.com") + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-tok") + t.Setenv("REPO_FULL_NAME", "org/my-repo") + + origMint := statusMintToken + defer func() { statusMintToken = origMint }() + + statusMintToken = func(_ context.Context, _ mintclient.MintRequest) (*mintclient.MintResult, error) { + return nil, fmt.Errorf("mint service unavailable") + } + + var buf bytes.Buffer + printer := ui.New(&buf) + + _, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + require.Error(t, err) + assert.Contains(t, err.Error(), "minting loader token") +} diff --git a/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed b/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed index 16170ea105..52d3221ef3 100644 --- a/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed +++ b/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed @@ -70,6 +70,7 @@ var canonicalRolePermissions = map[string]map[string]string{ "organization_administration": "write", "pull_requests": "write", "secrets": "write", "workflows": "write", }, + "loader": {"contents": "read", "metadata": "read"}, } // RolePermissions returns a deep copy of the role-to-permissions map, diff --git a/internal/mintcore/github.go b/internal/mintcore/github.go index 16170ea105..52d3221ef3 100644 --- a/internal/mintcore/github.go +++ b/internal/mintcore/github.go @@ -70,6 +70,7 @@ var canonicalRolePermissions = map[string]map[string]string{ "organization_administration": "write", "pull_requests": "write", "secrets": "write", "workflows": "write", }, + "loader": {"contents": "read", "metadata": "read"}, } // RolePermissions returns a deep copy of the role-to-permissions map, diff --git a/internal/mintcore/github_test.go b/internal/mintcore/github_test.go index ce3339d486..babe96a52e 100644 --- a/internal/mintcore/github_test.go +++ b/internal/mintcore/github_test.go @@ -104,7 +104,7 @@ func TestCreateInstallationToken_UnknownRole(t *testing.T) { } func TestRolePermissions_AllRolesPresent(t *testing.T) { - expectedRoles := []string{"triage", "coder", "review", "fix", "retro", "prioritize", "fullsend", "e2e"} + expectedRoles := []string{"triage", "coder", "review", "fix", "retro", "prioritize", "fullsend", "e2e", "loader"} allPerms := RolePermissions() for _, role := range expectedRoles { perms, ok := allPerms[role] @@ -131,6 +131,14 @@ func TestRolePermissions_E2e(t *testing.T) { assert.Equal(t, "write", perms["workflows"]) } +func TestRolePermissions_Loader(t *testing.T) { + perms := RolePermissionsFor("loader") + require.NotNil(t, perms) + assert.Equal(t, "read", perms["contents"]) + assert.Equal(t, "read", perms["metadata"]) + assert.Len(t, perms, 2, "loader role should have exactly 2 permissions") +} + func TestRolePermissions_ReturnsCopy(t *testing.T) { // Mutating the returned map must not affect the canonical definitions. perms := RolePermissions() From 997872a30b22afdb28f730534620bfee3ec7887a Mon Sep 17 00:00:00 2001 From: fullsend-fix <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 20:06:57 +0000 Subject: [PATCH 2/4] fix(#2722): address review feedback on loader token PR - Reuse composeForgeClient in URL-skill resolution path so the already-minted loader token is used instead of independently calling resolveToken() (error-handling-gap finding) - Update github_token input description in action.yml to reflect its reduced scope after GH_TOKEN removal (documentation-contract-mismatch) - Add Repos field assertion to TestMintLoaderToken_Success for stronger coverage (test-adequacy) - Improve mintLoaderToken error message to name the required env vars (error-message-style) Addresses review feedback on #2725 --- action.yml | 3 ++- internal/cli/run.go | 4 +++- internal/cli/run_test.go | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index b91ac8ffd7..c7e2668a9f 100644 --- a/action.yml +++ b/action.yml @@ -25,7 +25,8 @@ inputs: default: "" github_token: description: >- - GitHub token for authenticated API calls (avoids 60 req/hour unauthenticated rate limit). + GitHub token used by setup steps (detect version, download binary, clone repos). + Not passed to the fullsend run step, which self-mints a scoped loader token via OIDC. default: ${{ github.token }} run-url: description: URL of the CI/CD run for status comments (optional). diff --git a/internal/cli/run.go b/internal/cli/run.go index ffa933dc81..51c9e6202d 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -309,6 +309,8 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep if h.HasURLSkills() { if rFlags.forgeClient != nil { forgeClient = rFlags.forgeClient + } else if composeForgeClient != nil { + forgeClient = composeForgeClient } else { token, tokenErr := resolveToken() if tokenErr != nil { @@ -2147,7 +2149,7 @@ func hasOIDCEnv() bool { // used only for ForgeClient construction during LoadWithBase. func mintLoaderToken(ctx context.Context, mintURL string, printer *ui.Printer) (string, error) { if mintURL == "" || !hasOIDCEnv() { - return "", fmt.Errorf("loader mint unavailable: no mint URL or OIDC env") + return "", fmt.Errorf("loader mint unavailable: mint URL and OIDC env vars (ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN) are both required") } repos, err := resolveMintRepos() diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index 34b24cd553..f89a480645 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -2540,6 +2540,7 @@ func TestMintLoaderToken_Success(t *testing.T) { assert.Equal(t, "ghs_loader_token", token) assert.Equal(t, "loader", capturedReq.Role) assert.Equal(t, "https://mint.example.com", capturedReq.MintURL) + assert.Equal(t, []string{"my-repo"}, capturedReq.Repos) assert.Contains(t, buf.String(), "Loader token minted") } From 2434220b5b683def0674c24647c32499cac7e769 Mon Sep 17 00:00:00 2001 From: fullsend-fix <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:29:22 +0000 Subject: [PATCH 3/4] fix(#2722): add tests for full mintLoaderToken coverage Add three additional test cases to achieve 100% coverage on mintLoaderToken, addressing the >80% patch coverage requirement: - TestMintLoaderToken_InvalidTokenPattern: covers the token validation error branch - TestMintLoaderToken_ResolveMintReposError: covers the resolveMintRepos failure path - TestMintLoaderToken_SuccessWithMasking: covers the GITHUB_ACTIONS ::add-mask:: workflow command path Addresses review feedback on #2725 --- internal/cli/run_test.go | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index f89a480645..fb13853c3e 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -2563,3 +2563,73 @@ func TestMintLoaderToken_MintError(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "minting loader token") } + +func TestMintLoaderToken_InvalidTokenPattern(t *testing.T) { + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://oidc.example.com") + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-tok") + t.Setenv("REPO_FULL_NAME", "org/my-repo") + + origMint := statusMintToken + defer func() { statusMintToken = origMint }() + + statusMintToken = func(_ context.Context, _ mintclient.MintRequest) (*mintclient.MintResult, error) { + return &mintclient.MintResult{Token: "bad token with spaces!", ExpiresAt: "2026-06-15T12:00:00Z"}, nil + } + + var buf bytes.Buffer + printer := ui.New(&buf) + + _, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + require.Error(t, err) + assert.Contains(t, err.Error(), "unexpected characters") +} + +func TestMintLoaderToken_ResolveMintReposError(t *testing.T) { + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://oidc.example.com") + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-tok") + // Neither MINT_REPOS nor REPO_FULL_NAME is set, so resolveMintRepos fails. + t.Setenv("MINT_REPOS", "") + t.Setenv("REPO_FULL_NAME", "") + + var buf bytes.Buffer + printer := ui.New(&buf) + + _, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + require.Error(t, err) + assert.Contains(t, err.Error(), "resolving mint repos for loader") +} + +func TestMintLoaderToken_SuccessWithMasking(t *testing.T) { + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://oidc.example.com") + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-tok") + t.Setenv("REPO_FULL_NAME", "org/my-repo") + t.Setenv("GITHUB_ACTIONS", "true") + + origMint := statusMintToken + defer func() { statusMintToken = origMint }() + + statusMintToken = func(_ context.Context, req mintclient.MintRequest) (*mintclient.MintResult, error) { + return &mintclient.MintResult{Token: "ghs_masked_token", ExpiresAt: "2026-06-15T12:00:00Z"}, nil + } + + var buf bytes.Buffer + printer := ui.New(&buf) + + // Capture stderr for the ::add-mask:: output. + origStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + token, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + + w.Close() + os.Stderr = origStderr + + var stderrBuf bytes.Buffer + stderrBuf.ReadFrom(r) + + require.NoError(t, err) + assert.Equal(t, "ghs_masked_token", token) + assert.Contains(t, stderrBuf.String(), "::add-mask::ghs_masked_token") + assert.Contains(t, buf.String(), "Loader token minted") +} From 303f7870de21c566eedd3a8012cca56dfcdb1649 Mon Sep 17 00:00:00 2001 From: fullsend-fix <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:52:54 +0000 Subject: [PATCH 4/4] fix(#2722): rename loader role to reader across all references Rename the mint role from "loader" to "reader" to better describe its purpose (read-only content access). Updates the role definition, mintReaderToken helper, all tests, action.yml description, and the embedded copy. Addresses review feedback on #2722 --- action.yml | 2 +- internal/cli/run.go | 22 ++++----- internal/cli/run_test.go | 46 +++++++++---------- .../gcf/mintsrc/mintcore/github.go.embed | 2 +- internal/mintcore/github.go | 2 +- internal/mintcore/github_test.go | 8 ++-- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/action.yml b/action.yml index c7e2668a9f..c8c862f027 100644 --- a/action.yml +++ b/action.yml @@ -26,7 +26,7 @@ inputs: github_token: description: >- GitHub token used by setup steps (detect version, download binary, clone repos). - Not passed to the fullsend run step, which self-mints a scoped loader token via OIDC. + Not passed to the fullsend run step, which self-mints a scoped reader token via OIDC. default: ${{ github.token }} run-url: description: URL of the CI/CD run for status comments (optional). diff --git a/internal/cli/run.go b/internal/cli/run.go index 51c9e6202d..2641df2a19 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -210,7 +210,7 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep } } - // Resolve mintURL early so it is available for the loader token mint + // Resolve mintURL early so it is available for the reader token mint // before LoadWithBase. The same value is reused for mintAgentToken later. mintURL := sOpts.mintURL if mintURL == "" { @@ -220,7 +220,7 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep var composeForgeClient forge.Client if rFlags.forgeClient != nil { composeForgeClient = rFlags.forgeClient - } else if token, loaderErr := mintLoaderToken(ctx, mintURL, printer); loaderErr == nil { + } else if token, readerErr := mintReaderToken(ctx, mintURL, printer); readerErr == nil { composeForgeClient = gh.New(token) } else if token, tokenErr := resolveToken(); tokenErr == nil { composeForgeClient = gh.New(token) @@ -2142,40 +2142,40 @@ func hasOIDCEnv() bool { os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") != "" } -// mintLoaderToken mints a minimal-permission token using the "loader" role +// mintReaderToken mints a minimal-permission token using the "reader" role // for resolving URL-based harness bases and skill directories before the // harness role is known. Returns the token string on success, or an error // if OIDC is unavailable or the mint fails. The token is short-lived and // used only for ForgeClient construction during LoadWithBase. -func mintLoaderToken(ctx context.Context, mintURL string, printer *ui.Printer) (string, error) { +func mintReaderToken(ctx context.Context, mintURL string, printer *ui.Printer) (string, error) { if mintURL == "" || !hasOIDCEnv() { - return "", fmt.Errorf("loader mint unavailable: mint URL and OIDC env vars (ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN) are both required") + return "", fmt.Errorf("reader mint unavailable: mint URL and OIDC env vars (ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN) are both required") } repos, err := resolveMintRepos() if err != nil { - return "", fmt.Errorf("resolving mint repos for loader: %w", err) + return "", fmt.Errorf("resolving mint repos for reader: %w", err) } - printer.StepStart("Minting loader token (contents:read)") + printer.StepStart("Minting reader token (contents:read)") result, err := statusMintToken(ctx, mintclient.MintRequest{ MintURL: mintURL, - Role: "loader", + Role: "reader", Repos: repos, }) if err != nil { - return "", fmt.Errorf("minting loader token: %w", err) + return "", fmt.Errorf("minting reader token: %w", err) } if !mintTokenPattern.MatchString(result.Token) { - return "", fmt.Errorf("minted loader token contains unexpected characters") + return "", fmt.Errorf("minted reader token contains unexpected characters") } if os.Getenv("GITHUB_ACTIONS") == "true" { fmt.Fprintf(os.Stderr, "::add-mask::%s\n", result.Token) } - printer.StepDone("Loader token minted") + printer.StepDone("Reader token minted") return result.Token, nil } diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index fb13853c3e..04e2a0a140 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -2494,31 +2494,31 @@ func TestHasOIDCEnv(t *testing.T) { } } -func TestMintLoaderToken_NoMintURL(t *testing.T) { +func TestMintReaderToken_NoMintURL(t *testing.T) { t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://example.com") t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "tok") var buf bytes.Buffer printer := ui.New(&buf) - _, err := mintLoaderToken(context.Background(), "", printer) + _, err := mintReaderToken(context.Background(), "", printer) require.Error(t, err) - assert.Contains(t, err.Error(), "loader mint unavailable") + assert.Contains(t, err.Error(), "reader mint unavailable") } -func TestMintLoaderToken_NoOIDCEnv(t *testing.T) { +func TestMintReaderToken_NoOIDCEnv(t *testing.T) { t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "") t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "") var buf bytes.Buffer printer := ui.New(&buf) - _, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + _, err := mintReaderToken(context.Background(), "https://mint.example.com", printer) require.Error(t, err) - assert.Contains(t, err.Error(), "loader mint unavailable") + assert.Contains(t, err.Error(), "reader mint unavailable") } -func TestMintLoaderToken_Success(t *testing.T) { +func TestMintReaderToken_Success(t *testing.T) { t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://oidc.example.com") t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-tok") t.Setenv("REPO_FULL_NAME", "org/my-repo") @@ -2529,22 +2529,22 @@ func TestMintLoaderToken_Success(t *testing.T) { var capturedReq mintclient.MintRequest statusMintToken = func(_ context.Context, req mintclient.MintRequest) (*mintclient.MintResult, error) { capturedReq = req - return &mintclient.MintResult{Token: "ghs_loader_token", ExpiresAt: "2026-06-15T12:00:00Z"}, nil + return &mintclient.MintResult{Token: "ghs_reader_token", ExpiresAt: "2026-06-15T12:00:00Z"}, nil } var buf bytes.Buffer printer := ui.New(&buf) - token, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + token, err := mintReaderToken(context.Background(), "https://mint.example.com", printer) require.NoError(t, err) - assert.Equal(t, "ghs_loader_token", token) - assert.Equal(t, "loader", capturedReq.Role) + assert.Equal(t, "ghs_reader_token", token) + assert.Equal(t, "reader", capturedReq.Role) assert.Equal(t, "https://mint.example.com", capturedReq.MintURL) assert.Equal(t, []string{"my-repo"}, capturedReq.Repos) - assert.Contains(t, buf.String(), "Loader token minted") + assert.Contains(t, buf.String(), "Reader token minted") } -func TestMintLoaderToken_MintError(t *testing.T) { +func TestMintReaderToken_MintError(t *testing.T) { t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://oidc.example.com") t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-tok") t.Setenv("REPO_FULL_NAME", "org/my-repo") @@ -2559,12 +2559,12 @@ func TestMintLoaderToken_MintError(t *testing.T) { var buf bytes.Buffer printer := ui.New(&buf) - _, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + _, err := mintReaderToken(context.Background(), "https://mint.example.com", printer) require.Error(t, err) - assert.Contains(t, err.Error(), "minting loader token") + assert.Contains(t, err.Error(), "minting reader token") } -func TestMintLoaderToken_InvalidTokenPattern(t *testing.T) { +func TestMintReaderToken_InvalidTokenPattern(t *testing.T) { t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://oidc.example.com") t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-tok") t.Setenv("REPO_FULL_NAME", "org/my-repo") @@ -2579,12 +2579,12 @@ func TestMintLoaderToken_InvalidTokenPattern(t *testing.T) { var buf bytes.Buffer printer := ui.New(&buf) - _, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + _, err := mintReaderToken(context.Background(), "https://mint.example.com", printer) require.Error(t, err) assert.Contains(t, err.Error(), "unexpected characters") } -func TestMintLoaderToken_ResolveMintReposError(t *testing.T) { +func TestMintReaderToken_ResolveMintReposError(t *testing.T) { t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://oidc.example.com") t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-tok") // Neither MINT_REPOS nor REPO_FULL_NAME is set, so resolveMintRepos fails. @@ -2594,12 +2594,12 @@ func TestMintLoaderToken_ResolveMintReposError(t *testing.T) { var buf bytes.Buffer printer := ui.New(&buf) - _, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + _, err := mintReaderToken(context.Background(), "https://mint.example.com", printer) require.Error(t, err) - assert.Contains(t, err.Error(), "resolving mint repos for loader") + assert.Contains(t, err.Error(), "resolving mint repos for reader") } -func TestMintLoaderToken_SuccessWithMasking(t *testing.T) { +func TestMintReaderToken_SuccessWithMasking(t *testing.T) { t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "https://oidc.example.com") t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-tok") t.Setenv("REPO_FULL_NAME", "org/my-repo") @@ -2620,7 +2620,7 @@ func TestMintLoaderToken_SuccessWithMasking(t *testing.T) { r, w, _ := os.Pipe() os.Stderr = w - token, err := mintLoaderToken(context.Background(), "https://mint.example.com", printer) + token, err := mintReaderToken(context.Background(), "https://mint.example.com", printer) w.Close() os.Stderr = origStderr @@ -2631,5 +2631,5 @@ func TestMintLoaderToken_SuccessWithMasking(t *testing.T) { require.NoError(t, err) assert.Equal(t, "ghs_masked_token", token) assert.Contains(t, stderrBuf.String(), "::add-mask::ghs_masked_token") - assert.Contains(t, buf.String(), "Loader token minted") + assert.Contains(t, buf.String(), "Reader token minted") } diff --git a/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed b/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed index 52d3221ef3..a58105bace 100644 --- a/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed +++ b/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed @@ -70,7 +70,7 @@ var canonicalRolePermissions = map[string]map[string]string{ "organization_administration": "write", "pull_requests": "write", "secrets": "write", "workflows": "write", }, - "loader": {"contents": "read", "metadata": "read"}, + "reader": {"contents": "read", "metadata": "read"}, } // RolePermissions returns a deep copy of the role-to-permissions map, diff --git a/internal/mintcore/github.go b/internal/mintcore/github.go index 52d3221ef3..a58105bace 100644 --- a/internal/mintcore/github.go +++ b/internal/mintcore/github.go @@ -70,7 +70,7 @@ var canonicalRolePermissions = map[string]map[string]string{ "organization_administration": "write", "pull_requests": "write", "secrets": "write", "workflows": "write", }, - "loader": {"contents": "read", "metadata": "read"}, + "reader": {"contents": "read", "metadata": "read"}, } // RolePermissions returns a deep copy of the role-to-permissions map, diff --git a/internal/mintcore/github_test.go b/internal/mintcore/github_test.go index babe96a52e..a263fb37ca 100644 --- a/internal/mintcore/github_test.go +++ b/internal/mintcore/github_test.go @@ -104,7 +104,7 @@ func TestCreateInstallationToken_UnknownRole(t *testing.T) { } func TestRolePermissions_AllRolesPresent(t *testing.T) { - expectedRoles := []string{"triage", "coder", "review", "fix", "retro", "prioritize", "fullsend", "e2e", "loader"} + expectedRoles := []string{"triage", "coder", "review", "fix", "retro", "prioritize", "fullsend", "e2e", "reader"} allPerms := RolePermissions() for _, role := range expectedRoles { perms, ok := allPerms[role] @@ -131,12 +131,12 @@ func TestRolePermissions_E2e(t *testing.T) { assert.Equal(t, "write", perms["workflows"]) } -func TestRolePermissions_Loader(t *testing.T) { - perms := RolePermissionsFor("loader") +func TestRolePermissions_Reader(t *testing.T) { + perms := RolePermissionsFor("reader") require.NotNil(t, perms) assert.Equal(t, "read", perms["contents"]) assert.Equal(t, "read", perms["metadata"]) - assert.Len(t, perms, 2, "loader role should have exactly 2 permissions") + assert.Len(t, perms, 2, "reader role should have exactly 2 permissions") } func TestRolePermissions_ReturnsCopy(t *testing.T) {