Skip to content

fix(#7140): map hyphens to underscores in role identifiers - #7215

Open
rh-hemartin wants to merge 1 commit into
mainfrom
fix/7140-hyphenated-role-identifiers
Open

fix(#7140): map hyphens to underscores in role identifiers#7215
rh-hemartin wants to merge 1 commit into
mainfrom
fix/7140-hyphenated-role-identifiers

Conversation

@rh-hemartin

Copy link
Copy Markdown
Member

Summary

Custom harness roles with hyphens (ci-check) failed in Setup agent environment because the dispatch job uppercased matrix.role and exported names like CI-CHECK_TARGET_REPO_DIR, which bash rejects. Role-prefixed identifiers now share mintcore.RoleIdentifier: uppercase the role and replace hyphens with underscores (ci-checkCI_CHECK).

Related Issue

Fixes #7140

Changes

  • Map hyphens to underscores in the custom-harness env prefix (reusable-dispatch.yml) so ci-check becomes CI_CHECK_TARGET_REPO_DIR / CI_CHECK_.
  • Use the same helper for GitHub App secrets/variables (FULLSEND_<ROLE>_APP_PRIVATE_KEY, FULLSEND_<ROLE>_CLIENT_ID), FOREIGN allow-lists (FULLSEND_FOREIGN_<ROLE>_REPOS), and CF PEM names.
  • Document the mapping once, in standalone-mint role naming rules. ADR 0014 and ADR 0060 get short later-notes; other guides do not restate that hyphens are allowed (the examples already show them).
  • fullsend github status prints FOREIGN variable names without a reconstructed role in parentheses (CI_CHECK is not uniquely ci-check or ci_check).

Testing

  • make lint passes (stage changes first, then run)
  • Tests added/updated for new or modified logic (RoleIdentifier, hyphenated foreign allow, github status output, dispatch hyphen mapping pin)

Checklist

  • PR title follows Conventional Commits (correct type, ! for breaking changes)
  • Commits are signed off (DCO) — human and human-directed agent sessions only
  • I wrote this contribution myself and can explain all changes in it

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Normalize hyphenated role identifiers across integration surfaces

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Normalize hyphenated role names into valid uppercase underscore identifiers.
• Apply shared mapping across dispatch, credentials, FOREIGN variables, and Cloudflare PEM secrets.
• Document collision semantics and test hyphenated roles across affected surfaces.
Diagram

graph TD
  ROLE["Role Name"] --> IDENT["RoleIdentifier"] --> DISPATCH["Dispatch Env"]
  IDENT --> CREDS["App Credentials"]
  IDENT --> FOREIGN["FOREIGN Variables"] --> STATUS["GitHub Status"]
  IDENT --> CF["CF PEM Secret"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use an injective role encoding
  • ➕ Keeps ci-check and ci_check identifiers distinct.
  • ➕ Allows normalized identifiers to be reversed into original roles.
  • ➖ Would rename established secrets, variables, and environment prefixes.
  • ➖ Would require migration logic across GitHub Actions, Cloudflare, and deployments.
  • ➖ Would produce less conventional environment-variable names.
2. Reject colliding role configurations
  • ➕ Preserves the conventional mapping while preventing ambiguous simultaneous roles.
  • ➕ Provides an explicit configuration error instead of shared credentials or overrides.
  • ➖ Requires config-wide collision validation beyond this targeted fix.
  • ➖ Could invalidate existing configurations that intentionally share identifiers.

Recommendation: Keep the PR's centralized RoleIdentifier contract and matching workflow transformation: it fixes invalid identifiers while preserving all names for roles without hyphens. The documented hyphen/underscore collision is the main tradeoff; configuration-level collision validation could be considered separately if both forms may coexist in one deployment.

Files changed (23) +150 / -17

Bug fix (9) +43 / -14
reusable-dispatch.ymlNormalize hyphenated dispatch role prefixes +4/-1

Normalize hyphenated dispatch role prefixes

• The agent environment setup now uppercases matrix roles and replaces hyphens with underscores. This prevents invalid bash exports such as CI-CHECK_TARGET_REPO_DIR.

.github/workflows/reusable-dispatch.yml

admin.goNormalize admin-managed App secret names +9/-4

Normalize admin-managed App secret names

• Routes GitHub App private-key secret lookup and storage through a helper backed by mintcore.RoleIdentifier.

internal/cli/admin.go

github.goStop reconstructing roles in GitHub status +2/-2

Stop reconstructing roles in GitHub status

• GitHub status now prints FOREIGN variable names directly without an ambiguous parsed role in parentheses.

internal/cli/github.go

provisioner.goReuse RoleIdentifier for Cloudflare PEM names +1/-2

Reuse RoleIdentifier for Cloudflare PEM names

• Cloudflare PEM secret naming now delegates uppercase and hyphen mapping to the shared normalization helper after applying role aliases.

internal/dispatch/cf/provisioner.go

foreign.go.embedNormalize embedded mint FOREIGN variable names +1/-1

Normalize embedded mint FOREIGN variable names

• The embedded GCF mint source now uses RoleIdentifier when constructing cross-organization allow-list variable names.

internal/dispatch/gcf/mintsrc/mintcore/foreign.go.embed

patterns.go.embedAdd RoleIdentifier to embedded mint source +11/-0

Add RoleIdentifier to embedded mint source

• Introduces the shared uppercase and hyphen-to-underscore transformation in the embedded GCF mintcore copy.

internal/dispatch/gcf/mintsrc/mintcore/patterns.go.embed

secrets.goNormalize layered credential identifiers +3/-3

Normalize layered credential identifiers

• GitHub App private-key secret and client-ID variable names now use mintcore.RoleIdentifier instead of uppercase-only conversion.

internal/layers/secrets.go

foreign.goNormalize FOREIGN allow-list variable suffixes +1/-1

Normalize FOREIGN allow-list variable suffixes

• ForeignVariableName now uses the shared role identifier mapping, producing valid names for hyphenated roles.

internal/mintcore/foreign.go

patterns.goIntroduce shared role identifier normalization +11/-0

Introduce shared role identifier normalization

• Adds RoleIdentifier to uppercase role names and replace hyphens with underscores. Its documentation explicitly describes the hyphen/underscore collision behavior.

internal/mintcore/patterns.go

Tests (8) +90 / -0
admin_test.goTest normalized App private-key secret names +5/-0

Test normalized App private-key secret names

• Verifies standard and hyphenated roles produce the expected repository secret names.

internal/cli/admin_test.go

foreign_test.goTest FOREIGN handling for hyphenated roles +14/-0

Test FOREIGN handling for hyphenated roles

• Covers parsing normalized identifiers and creating allow-list variables for a ci-check role.

internal/cli/foreign_test.go

github_test.goVerify unambiguous FOREIGN status output +23/-0

Verify unambiguous FOREIGN status output

• Confirms status output includes the exact normalized variable name and omits reconstructed underscore or hyphen role forms.

internal/cli/github_test.go

provisioner_test.goCover hyphenated and aliased PEM secret names +2/-0

Cover hyphenated and aliased PEM secret names

• Adds expectations for ci-check normalization and the fix-to-coder role alias.

internal/dispatch/cf/provisioner_test.go

secrets_test.goTest layered names for hyphenated roles +6/-0

Test layered names for hyphenated roles

• Verifies normalized private-key and client-ID names while retaining standard role behavior.

internal/layers/secrets_test.go

foreign_test.goTest hyphenated FOREIGN variable generation +3/-0

Test hyphenated FOREIGN variable generation

• Adds coverage proving ci-check maps to FULLSEND_FOREIGN_CI_CHECK_REPOS.

internal/mintcore/foreign_test.go

patterns_test.goCover RoleIdentifier normalization cases +23/-0

Cover RoleIdentifier normalization cases

• Adds table-driven tests for standard, hyphenated, underscored, mixed, and numeric role names.

internal/mintcore/patterns_test.go

workflow_call_alignment_test.goPin dispatch hyphen normalization +14/-0

Pin dispatch hyphen normalization

• Adds a workflow alignment test ensuring agent setup applies the same hyphen mapping as mintcore.RoleIdentifier.

internal/scaffold/workflow_call_alignment_test.go

Documentation (6) +17 / -3
0014-admin-install-github-apps-secrets-v1.mdRecord hyphen mapping for GitHub App credentials +2/-0

Record hyphen mapping for GitHub App credentials

• Adds a later note explaining that role suffixes in GitHub Actions secrets and variables use RoleIdentifier normalization.

docs/ADRs/0014-admin-install-github-apps-secrets-v1.md

0060-cross-org-mint-authorization-via-org-variables.mdDocument normalized FOREIGN variable names +6/-0

Document normalized FOREIGN variable names

• Records how hyphenated roles map to underscore-based cross-organization allow-list variables.

docs/ADRs/0060-cross-org-mint-authorization-via-org-variables.md

mint-administration.mdStandardize FOREIGN role placeholder casing +1/-1

Standardize FOREIGN role placeholder casing

• Updates the cross-organization authorization reference to use the normalized uppercase ROLE placeholder.

docs/guides/infrastructure/mint-administration.md

standalone-mint.mdDefine role identifier mapping and collisions +2/-0

Define role identifier mapping and collisions

• Documents uppercase and hyphen-to-underscore normalization for role-prefixed identifiers. It also explains that roles differing only by hyphens and underscores share an identifier.

docs/guides/infrastructure/standalone-mint.md

foreign.goExplain normalized FOREIGN role identifiers +3/-1

Explain normalized FOREIGN role identifiers

• Updates command help to describe uppercase role placeholders and hyphen-to-underscore mapping.

internal/cli/foreign.go

setup-agent-env.shDocument normalized scaffold agent prefixes +3/-1

Document normalized scaffold agent prefixes

• Clarifies that AGENT_PREFIX uses an uppercase role identifier with hyphens mapped to underscores.

internal/scaffold/fullsend-repo/.github/scripts/setup-agent-env.sh

Custom harness roles such as ci-check produced invalid bash identifiers
in Setup agent environment. Role-prefixed env vars, GitHub Actions
secrets/variables, and FOREIGN allow-list names now share
mintcore.RoleIdentifier: uppercase the role and replace hyphens with
underscores.

github status prints FOREIGN variable names without a reconstructed
role; CI_CHECK is not uniquely ci-check or ci_check.

Assisted-By: grok-4.6(pi)
Signed-off-by: Hector Martinez <hemartin@redhat.com>
@rh-hemartin
rh-hemartin force-pushed the fix/7140-hyphenated-role-identifiers branch from 02cb454 to d7fb5a7 Compare September 10, 2026 15:15
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 10, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 3:15 PM UTC · Ended 3:16 PM UTC

Commit: 02cb454 · View workflow run →

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Workflow changes bypass guide review 📘 Rule violation § Compliance
Description
.github/workflows/reusable-dispatch.yml changes secret-derived environment setup without an inline
reference to docs/contributing/ci-workflows.md. The PR description also omits any statement that
the guide was consulted, so this workflow change reaches review without the required security and
context check.
Code

.github/workflows/reusable-dispatch.yml[R1737-1740]

+          # GitHub Actions / bash identifiers cannot contain hyphens.
+          # Map the role the same way mintcore.RoleIdentifier does: uppercase
+          # and replace '-' with '_' so ci-check becomes CI_CHECK.
+          ROLE_UPPER=$(echo "${MATRIX_ROLE}" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
Relevance

●● Moderate

Compliance rule supports the finding, but related protected-workflow authorization requests were
rejected historically.

PR-#1063
PR-#1211

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3201588 requires explicit evidence that the CI workflow guide was consulted whenever a GitHub
Actions workflow is modified. The cited workflow lines are changed, while neither their comments nor
the supplied PR description mention the guide.

Rule 3201588: Consult CI workflow guide when modifying GitHub Actions workflows or secrets
.github/workflows/reusable-dispatch.yml[1737-1740]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The reusable workflow was modified, but neither the workflow comment nor the PR description records consultation of `docs/contributing/ci-workflows.md` as required for GitHub Actions changes.

## Fix Focus Areas
- .github/workflows/reusable-dispatch.yml[1737-1740]

## Recommended Fix
Review `docs/contributing/ci-workflows.md`, verify the changed environment and identifier handling follows its security and context guidance, then add a concise inline comment referencing that guide or update the PR description to explicitly record the review.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Two roles share secrets and access 🐞 Bug ⛨ Security
Description
RoleIdentifier maps hyphens to underscores, while validateCustomRoleLevels and
RegisterCustomRolePermissions accept both original role names without checking normalized
uniqueness. When an operator defines ci-check and ci_check, they retain separate permission maps
but resolve to the same app secrets, environment overrides, and FOREIGN allow-list, so granting a
caller or storing credentials for one also affects the other.
Code

internal/mintcore/foreign.go[15]

+	return foreignVarPrefix + RoleIdentifier(role) + foreignVarSuffix
Relevance

● Weak

Repository knowingly documents hyphen/underscore identifier sharing; closely matching secret-name
collision concerns were rejected.

PR-#5898
PR-#2331

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Role validation explicitly permits both hyphens and underscores, while custom-role registration
rejects only exact built-in names and keeps permissions keyed by the original role. The changed
resource-name functions then normalize both names to the same identifier; FOREIGN authorization
reads that shared variable, and secret installation writes both roles to the same repository secret
and client-ID variable.

internal/mintcore/patterns.go[22-23]
internal/mintcore/patterns.go[40-57]
internal/mintcore/github.go[164-171]
internal/mintcore/github.go[204-231]
internal/mintcore/foreign.go[13-15]
internal/mintcore/github.go[757-790]
internal/layers/secrets.go[87-104]
internal/layers/secrets.go[183-188]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Distinct custom roles can normalize to the same identifier and consequently share credentials, environment settings, and authorization policy.

## Fix Focus Areas
- internal/mintcore/github.go[164-171]
- internal/mintcore/github.go[204-215]
- internal/mintcore/patterns.go[49-58]

## Recommended Fix
During both flat and multi-level custom-role registration, track each role by `RoleIdentifier` and reject configurations where different role names produce the same identifier. Add tests proving that `ci-check` and `ci_check` cannot be registered together while noncolliding roles remain valid.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 77 rules
✅ Cross-repo context — repo relationships
  Explored: repo: fullsend-ai/experiments (sha: 25946a93)
  Explored: repo: fullsend-ai/.fullsend (sha: 7c163cad)
  Explored: repo: fullsend-ai/pi-xai-vertex (sha: 5216a88c)
  Explored: repo: fullsend-ai/pi-anthropic-vertex (sha: 477b3546)
  Explored: repo: fullsend-ai/scribe (sha: 0f3eb528)
  Explored: repo: fullsend-ai/metrics (sha: 66c66aa2)
  Explored: repo: fullsend-ai/agents (sha: 883141b9)
Review mode: ⚖️ Balanced: This is a cross-cutting behavioral change affecting shell/Actions identifiers, secrets, authorization variables, CLI output, and mirrored runtime sources, so it warrants a complete single-pass review.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display preferences

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +1737 to +1740
# GitHub Actions / bash identifiers cannot contain hyphens.
# Map the role the same way mintcore.RoleIdentifier does: uppercase
# and replace '-' with '_' so ci-check becomes CI_CHECK.
ROLE_UPPER=$(echo "${MATRIX_ROLE}" | tr '[:lower:]' '[:upper:]' | tr '-' '_')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Workflow changes bypass guide review 📘 Rule violation § Compliance

.github/workflows/reusable-dispatch.yml changes secret-derived environment setup without an inline
reference to docs/contributing/ci-workflows.md. The PR description also omits any statement that
the guide was consulted, so this workflow change reaches review without the required security and
context check.
Agent Prompt
## Issue description
The reusable workflow was modified, but neither the workflow comment nor the PR description records consultation of `docs/contributing/ci-workflows.md` as required for GitHub Actions changes.

## Fix Focus Areas
- .github/workflows/reusable-dispatch.yml[1737-1740]

## Recommended Fix
Review `docs/contributing/ci-workflows.md`, verify the changed environment and identifier handling follows its security and context guidance, then add a concise inline comment referencing that guide or update the PR description to explicitly record the review.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 3:18 PM UTC · Completed 3:37 PM UTC

Commit: d7fb5a7 · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $7.88

@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/cli/admin.go 50.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

Medium

  • [protected-path] .github/workflows/reusable-dispatch.yml — This PR modifies a file under a protected governance/infrastructure path (.github/). The PR links to issue Custom harness dispatch fails when role contains a hyphen #7140 and its description explains the rationale for the change (mapping hyphens to underscores in the custom-harness dispatch job's env-var prefix), so sufficient context exists — but human approval is always required for protected-path changes, regardless of context.

  • [role-escalation] internal/mintcore/foreign.go:15 (root helper: internal/mintcore/patterns.go:56) — The new mintcore.RoleIdentifier helper is not injective: RolePattern (^[a-z][a-z0-9_-]*$) still permits both hyphens and underscores in role names, and every admission path (RegisterCustomRolePermissions/RegisterCustomRoleLevels/validateCustomRoleLevels, per-repo config Validate, harness discovery loadKnownSlugs) only rejects exact string duplicates — not identifier collisions. As a result, two distinct, individually valid roles that differ only by hyphen vs. underscore (e.g. ci-check and ci_check) now collapse onto the same identifier (CI_CHECK) and therefore the same GitHub Actions secret/variable names: FULLSEND_FOREIGN_CI_CHECK_REPOS (a FOREIGN cross-org/cross-repo mint allow-list grant for one role now authorizes the other), FULLSEND_CI_CHECK_APP_PRIVATE_KEY / FULLSEND_CI_CHECK_CLIENT_ID (SecretsLayer.Install is last-write-wins on the non-OIDC path, so one role's GitHub App credentials can silently overwrite the other's), and the harness-run <ROLE>_FULLSEND_MODEL override prefix. This is a regression specifically on the GHA/FOREIGN path: before this PR, ForeignVariableName/secretName/variableName used strings.ToUpper(role) with no hyphen folding, so ci-check and ci_check produced different (and, for ci-check, actually GitHub-Actions-invalid) strings — they did not collide. GCF Secret Manager IDs (fullsend-%s-app-pem, built from PemSecretRole only) remain distinct, which makes the FOREIGN-allow-list aliasing worse on the OIDC path: two GitHub Apps with different PEMs/permission envelopes can end up sharing one allow-list variable. Triggering this requires org-admin-level configuration of two RolePattern-valid custom roles (not an external/untrusted-input path), but it is a new authorization-surface invariant this PR introduces without any uniqueness check, despite the docs (standalone-mint.md) explicitly acknowledging the alias.
    Remediation: Keep RoleIdentifier for producing GitHub-Actions-safe names, but reject identifier collisions wherever roles are admitted — mintcore.RegisterCustomRolePermissions/RegisterCustomRoleLevels/validateCustomRoleLevels, per-repo config role lists, harness discovery (loadKnownSlugs), SecretsLayer.Install, admin app setup, and CF StoreAgentPEM. Compare mintcore.RoleIdentifier(role) across the full configured role set (and against built-in role names) and error when two distinct role strings map to the same identifier.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

// ForeignVariableName returns the org variable name for cross-org allowlist policy.
func ForeignVariableName(role string) string {
return foreignVarPrefix + strings.ToUpper(role) + foreignVarSuffix
return foreignVarPrefix + RoleIdentifier(role) + foreignVarSuffix

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] role-escalation

The new mintcore.RoleIdentifier helper (internal/mintcore/patterns.go:56) is not injective: RolePattern (^[a-z][a-z0-9_-]*$) still permits both hyphens and underscores in role names, and every admission path (RegisterCustomRolePermissions/RegisterCustomRoleLevels/validateCustomRoleLevels, per-repo config Validate, harness discovery loadKnownSlugs) only rejects exact string duplicates, not identifier collisions. Two distinct, individually valid roles differing only by hyphen vs underscore (e.g. ci-check and ci_check) now collapse onto the same identifier (CI_CHECK) and therefore the same GitHub Actions secret/variable names: FULLSEND_FOREIGN_CI_CHECK_REPOS (a FOREIGN cross-org/cross-repo mint allow-list grant for one role now authorizes the other), FULLSEND_CI_CHECK_APP_PRIVATE_KEY / FULLSEND_CI_CHECK_CLIENT_ID (SecretsLayer.Install is last-write-wins on the non-OIDC path, so one role credential can silently overwrite the other), and the harness-run _FULLSEND_MODEL override prefix. This is a regression on the GHA/FOREIGN path specifically: before this PR, ForeignVariableName/secretName/variableName used strings.ToUpper(role) with no hyphen folding, so ci-check and ci_check produced different (and, for ci-check, GitHub-Actions-invalid) strings — they did not collide. GCF Secret Manager IDs (fullsend-%s-app-pem, built from PemSecretRole only) remain distinct, which makes the FOREIGN-allow-list aliasing worse on the OIDC path: two GitHub Apps with different PEMs/permission envelopes can end up sharing one allow-list variable. Triggering this requires org-admin-level configuration of two RolePattern-valid custom roles (not an external/untrusted-input path), but it is a new authorization-surface invariant this PR introduces without any uniqueness check, despite the docs (standalone-mint.md) explicitly acknowledging the alias.

Suggested fix: Keep RoleIdentifier for producing GitHub-Actions-safe names, but reject identifier collisions wherever roles are admitted: mintcore.RegisterCustomRolePermissions/RegisterCustomRoleLevels/validateCustomRoleLevels, per-repo config role lists, harness discovery (loadKnownSlugs), SecretsLayer.Install, admin app setup, and CF StoreAgentPEM. Compare mintcore.RoleIdentifier(role) across the full configured role set (and against built-in role names) and error when two distinct role strings map to the same identifier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom harness dispatch fails when role contains a hyphen

1 participant