Skip to content

[compiler-bug] Cross-repo safe-outputs PR creation broken — allowed-repos ${{ }} expressions never resolved, all dynamic workflows affected #30867

@kkruel8100

Description

@kkruel8100

Environment

  • Compiler version: v0.71.5
  • Central repo dispatching agents to multiple target repos
  • Dynamic target_repo passed as workflow input

Steps to Reproduce

  1. Create a workflow .md with allowed-repos using a ${{ inputs.target_repo }} expression (see frontmatter below)
  2. Run gh aw compile
  3. Trigger the workflow with a target_repo input pointing to a different repo than the one hosting the workflow
  4. Observe the Process Safe Outputs step in the safe_outputs job

Problem Statement

When allowed-repos in the .md frontmatter uses a ${{ }} expression,
the compiled config.json receives the literal string
${{ inputs.target_repo }} instead of the resolved value.
The handler then cannot match the actual repo name and falls back to the
source repo, causing all cross-repo PRs to fail validation.

Root Cause

The compiler generates the Write Safe Outputs Config step in the agent
job using a single-quoted heredoc:

cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF'
{"create_pull_request":{"allowed_repos":["${{ inputs.target_repo }}"],...}}
GH_AW_SAFE_OUTPUTS_CONFIG_EOF

Single-quoted heredoc delimiters in bash suppress all expansion.
So config.json is written with ${{ inputs.target_repo }} as a literal string.

GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG (a step-level env var) correctly resolves
the expression via the Actions engine — but safe_output_handler_manager.cjs
reads validation config from config.json (downloaded via artifact),
not from the env var.

.md Frontmatter

safe-outputs:
  github-app:
    app-id: "1234567"
    private-key: ${{ secrets.GH_AW_DA_AUTHOR_PRIVATE_KEY }}
  create-pull-request:
    allowed-repos:
      - ${{ inputs.target_repo }}
    allowed-base-branches:
      - ${{ inputs.base_branch }}
    title-prefix: "[ai] "
    labels: [automation, my-agent]
    draft: false
    max: 1
    fallback-as-issue: false

Evidence: Config Loads Correctly in Logs

GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG (env var — step level, resolved by Actions engine)
shows the correct resolved value:

{
  "create_pull_request": {
    "allowed_repos": ["MY-Org/MYREPO"],
    "allowed_base_branches": ["main"]
  }
}

⚠️ Logs sanitized — private repository. Repo names replaced with MY-Org/MYREPO
and MY-Org/Source-Repo as placeholders. Behavior is reproducible with any
workflow using a dynamic allowed-repos expression targeting a different repo
than the one hosting the workflow.

Evidence: Validation Then Fails

ERR_VALIDATION: Repository 'MY-Org/MYREPO' is not in the allowed-repos list.
Allowed: MY-Org/Source-Repo

MY-Org/Source-Repo is github.repository — the running workflow's own repo.
This confirms the handler ignored GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG and
read from config.json which contains an unresolved literal.

⚠️ Logs sanitized — private repository. Repo names replaced with MY-Org/MYREPO
and MY-Org/Source-Repo as placeholders. Behaviour is reproducible with any
workflow using a dynamic allowed-repos expression targeting a different repo
than the one hosting the workflow.

Failing Run

Workflow runs in a private enterprise repository and cannot be shared publicly.
The log evidence below has been sanitized with placeholder values.

Impact

  • Any workflow using a dynamic allowed-repos value is broken
  • Cross-repo agent deployments are not functional without a workaround
  • Affects all orgs running centralized repo patterns with multiple target repos
  • Blocks legitimate use cases: multiple target repos, multi-environment (dev/prod),
    100s of agents that cannot have repo names hardcoded

Expected Behaviour

config.json should contain the resolved value MY-Org/MYREPO,
matching what GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG correctly shows.

Possible Fixes

Option A — Compiler fix: Write config.json by setting env vars first,
then use an unquoted heredoc so the shell expands them at runtime:

# Actions expression engine resolves this before the step runs
export GH_AW_ALLOWED_REPO="${{ inputs.target_repo }}"
export GH_AW_ALLOWED_BRANCH="${{ inputs.base_branch }}"
# Unquoted delimiter — bash now expands ${GH_AW_ALLOWED_REPO}
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << GH_AW_SAFE_OUTPUTS_CONFIG_EOF
{"create_pull_request":{"allowed_repos":["${GH_AW_ALLOWED_REPO}"],"allowed_base_branches":["${GH_AW_ALLOWED_BRANCH}"],...}}
GH_AW_SAFE_OUTPUTS_CONFIG_EOF

Option B — Handler fix: Have safe_output_handler_manager.cjs prefer
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG over config.json for validation,
since the env var is correctly resolved by the Actions expression engine
and is already present in the same step.

Workarounds (both are not viable at scale)

  • Hardcode repo names in .md frontmatter — breaks multi-repo and multi-environment setups
  • Manually edit .lock.yml — overwritten every time gh aw compile is run

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions