Skip to content

[ARC/DinD] safe-outputs: dynamic base-branch unresolved — GH_AW_INPUT_* not passed to MCP container #47795

Description

@r-garcia-de-oliveira

Summary

For safe-outputs.create-pull-request with a dynamic base-branch that references a workflow input:

on:
  workflow_dispatch:
    inputs:
      base_branch: { type: string, default: develop }
safe-outputs:
  create-pull-request:
    base-branch: ${{ inputs.base_branch }}

the value never reaches the safe-outputs MCP server, so create_pull_request fails with:

ERR_SYSTEM: No remote refs available for merge-base calculation

and no pull request is created.

The compiler stores the value in config.json as the placeholder "base_branch":"${GH_AW_INPUT_BASE_BRANCH}" and relies on the MCP server resolving it at read-time from process.env.GH_AW_INPUT_BASE_BRANCH. Since the safe-outputs MCP server was moved into a container (#39100), GH_AW_INPUT_* is not in the container's -e env allowlist, so the placeholder is never substituted. The MCP then tries to compute a merge-base against a branch literally named ${GH_AW_INPUT_BASE_BRANCH} and throws.

This is a regression: it worked on v0.79.8 (MCP ran as an HTTP sidecar that inherited the full step env) and broke in v0.80.0 (MCP containerized with a filtered env allowlist).

Impact

  • create_pull_request is broken for any workflow using a dynamic base-branch: ${{ inputs.* }} — no PR is produced, only a report_incomplete/noop.
  • The same read-time-substitution path is used for other input-derived safe-outputs config values (e.g. allowed_base_branches, target-repo inputs), so those are affected too.
  • Compilation succeeds; the failure only surfaces at runtime inside the MCP container, making it hard to diagnose.
  • A static base-branch: develop works (it compiles to a literal, needing no substitution), which can mask the bug.

Environment

  • Reproduced on v0.82.14; re-verified byte-identical on v0.83.1 (latest stable) and v0.83.2 (pre-release).
  • Regression bisected to v0.80.0 (worked on v0.79.8).
  • Engine: copilot. Observed with runner.topology: arc-dind, but the cause (container env allowlist) is topology-independent.

Minimal reproduction

.github/workflows/repro.md:

---
on:
  workflow_dispatch:
    inputs:
      base_branch:
        required: false
        default: develop
        type: string
runs-on: ubuntu-latest
permissions:
  contents: read
engine:
  id: copilot
safe-outputs:
  create-pull-request:
    base-branch: ${{ inputs.base_branch }}
---
# Repro
Make a trivial change and open a PR.

Then gh aw compile repro and inspect repro.lock.yml:

  1. The config is written with the placeholder:
    "create_pull_request":{"base_branch":"${GH_AW_INPUT_BASE_BRANCH}", ...}
    
  2. The step env sets GH_AW_INPUT_BASE_BRANCH: ${{ inputs.base_branch }} (correct).
  3. But the safe-outputs MCP docker run command's -e allowlist contains no GH_AW_INPUT_* entry — so the container process never sees GH_AW_INPUT_BASE_BRANCH.

At runtime the MCP logs (with DEBUG) show the literal placeholder:

[generate_git_patch] Strategy 1 (full): Computing merge-base with ${GH_AW_INPUT_BASE_BRANCH} ...
git show-ref --verify --quiet refs/remotes/origin/${GH_AW_INPUT_BASE_BRANCH}   (fails)
Strategy 1 (full): No remote refs available ... ERR_SYSTEM: No remote refs available for merge-base calculation

Root cause

Permalinks pinned to 6e6860cce13c4eff003828de391f7db980e60920 (main; identical behavior on v0.82.14/v0.83.1/v0.83.2):

  1. By design — inputs are extracted to a GH_AW_INPUT_* step env and written into config.json as a shell-style placeholder:

    • pkg/workflow/secret_extraction.go maps ${{ inputs['base-branch'] }} → env GH_AW_INPUT_BASE_BRANCH + placeholder ${GH_AW_INPUT_BASE_BRANCH} (see the doc comment around L305–L346).
    • pkg/workflow/mcp_setup_generator.go#L283 writes config.json via a single-quoted heredoc (no shell expansion — intended; resolution is deferred to the reader).
    • actions/setup/js/safe_outputs_config.cjs#L20 resolves placeholders at read-time:
      return value.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (match, envName) => process.env[envName] ?? match);
      Note the ?? match: if the env var is absent, the literal placeholder is kept.
  2. The regression — the safe-outputs MCP server was moved into a container in Run safe-outputs MCP in the gh-aw node container #39100 (ce429bfcc3, v0.80.0). The container is launched with an explicit -e VAR allowlist built by pkg/workflow/mcp_setup_generator.go#L783 (appendMCPGatewayBaseEnvFlags) — it passes GITHUB_*, DEFAULT_BRANCH, RUNNER_TEMP, GH_AW_SAFE_OUTPUTS*, etc., but no GH_AW_INPUT_*. So inside the container process.env.GH_AW_INPUT_BASE_BRANCH is undefined, safe_outputs_config.cjs#L20 keeps the literal, and actions/setup/js/generate_git_bundle.cjs#L212 throws No remote refs available for merge-base calculation.

Before #39100 the MCP ran as an HTTP sidecar that inherited the whole step environment (including GH_AW_INPUT_BASE_BRANCH), so the substitution resolved and PRs were created — which is why v0.79.8 works and v0.80.0+ does not.

Regression bisection

  • v0.79.8: MCP = HTTP sidecar, full step env → ${GH_AW_INPUT_BASE_BRANCH} resolves → PR created. ✅
  • v0.80.0 (ce429bfcc3 / Run safe-outputs MCP in the gh-aw node container #39100 "Run safe-outputs MCP in the gh-aw node container"): MCP containerized with filtered -e allowlist omitting GH_AW_INPUT_* → placeholder unresolved → merge-base throw. ❌
  • Still reproduces on v0.83.1 and v0.83.2 (GH_AW_INPUT_* still absent from the -e list).

Implementation plan

  1. Pass GH_AW_INPUT_* into the MCP containerpkg/workflow/mcp_setup_generator.go, appendMCPGatewayBaseEnvFlags (~L783): add the input-derived env vars the safe-outputs config depends on to the -e allowlist. Prefer forwarding all GH_AW_INPUT_* that were emitted for this workflow (they're already computed by secret_extraction.go) rather than hard-coding GH_AW_INPUT_BASE_BRANCH, so allowed_base_branches / target-repo inputs are covered too.
  2. Test — extend the safe-outputs compile tests (e.g. pkg/workflow/safe_outputs_dynamic_allowed_repos_test.go, which already checks GH_AW_INPUT_BASE_BRANCH in the step env and the ${GH_AW_INPUT_BASE_BRANCH} config placeholder): add an assertion that the generated MCP container docker run command includes -e GH_AW_INPUT_BASE_BRANCH (and generally the GH_AW_INPUT_* the config references).
  3. Optional hardening — in safe_outputs_config.cjs, when a ${GH_AW_INPUT_*} placeholder cannot be resolved, emit a clear error (or core.warning) instead of silently passing the literal through to generate_git_bundle.cjs, so the failure mode is diagnosable rather than surfacing later as No remote refs available for merge-base calculation.
  4. Docs/guidelines — follow the console/error-message style guide; run make agent-finish before completing.

Notes

  • Diagnosed with the agentic-workflows dispatcher + source bisection on the local github/gh-aw clone.
  • Workaround for consumers meanwhile: hardcode a literal create-pull-request.base-branch: <branch> (compiles to a literal, needs no substitution).

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions