Summary
After the AWF JSON-config migration (#29220), the compiler emits the AWF config as a JSON string inside a printf '%s\n' "<json>" step in the generated .lock.yml. Any & characters in that string get JSON-encoded as \u0026 — including the && operators inside ${{ … }} GitHub Actions expressions that are interpolated into network.allowDomains. The Actions expression parser does not decode \u0026 and aborts with:
(Line: 867, Col: 14): Unexpected symbol: '\u0026\u0026'.
Located at position 26 within expression: env.MCP_ENV == 'staging' \u0026\u0026 env.MCP_URL_STAGING || env.MCP_URL_PROD
This blocks every workflow that uses a GitHub Actions expression with && (or &) inside network.allowed. We hit this on six errors-* workflows in microsoft/vscode-engineering after gh aw compile produced lock files with v0.25.40.
Repro
Minimal .md:
---
on: workflow_dispatch
permissions: { contents: read }
env:
MCP_URL_PROD: 'https://prod.example.com/mcp'
MCP_URL_STAGING: 'https://staging.example.com/mcp'
MCP_ENV: 'prod'
engine: { id: copilot }
mcp-servers:
demo:
url: ${{ env.MCP_ENV == 'staging' && env.MCP_URL_STAGING || env.MCP_URL_PROD }}
network:
allowed:
- "prod.example.com"
- "staging.example.com"
---
# demo
Run:
gh aw compile .github/workflows/demo.md
Inspect the generated .lock.yml — the firewall step contains:
run: |
…
printf '%s\n' "{\"$schema\":\"…\",\"network\":{\"allowDomains\":[\"${{ env.MCP_ENV == 'staging' \u0026\u0026 env.MCP_URL_STAGING || env.MCP_URL_PROD }}\", …
The \u0026\u0026 lives inside a ${{ … }} block, so GH Actions expression parser rejects the workflow before any step runs.
Root cause (likely)
In the codepath that builds the AWF config (introduced in #29220), the entire config object is json.Marshal'd (or equivalent), which escapes & → \u0026 per Go's default HTMLEscape behavior. ${{ … }} placeholders are interpolated as raw strings before JSON serialization, so the operators inside them get escaped along with everything else.
Suggested fixes (any one works)
- Disable HTML escaping when serializing the AWF config string. In Go: use
json.Encoder with SetEscapeHTML(false). This is the lowest-risk fix and matches what the Actions runtime / firewall actually consume (raw JSON, not HTML).
- Substitute
${{ … }} placeholders after JSON serialization (template the config, then inject expressions). Avoids any future escape-class bugs.
- Avoid the dynamic expression inside
allowDomains entirely by pre-computing the union of possible hosts at compile time. (Workaround for users today; doesn't fix the underlying bug.)
Workaround
Manually sed -i 's/\\u0026/\&/g' the printf line in each affected .lock.yml. Survives until the next gh aw compile.
Affected versions
Reproduced with gh-aw-firewall schema URL …/v0.25.40/awf-config.schema.json in generated lock files. Likely all gh-aw versions since #29220 landed (closed 2026-04-30).
Impact
All 6 errors-* workflows in microsoft/vscode-engineering that use staging/prod MCP URL switching:
errors-triage, errors-stable-scan, errors-fix, errors-regression-scan, errors-dedup-test, errors-investigate
Summary
After the AWF JSON-config migration (#29220), the compiler emits the AWF config as a JSON string inside a
printf '%s\n' "<json>"step in the generated.lock.yml. Any&characters in that string get JSON-encoded as\u0026— including the&&operators inside${{ … }}GitHub Actions expressions that are interpolated intonetwork.allowDomains. The Actions expression parser does not decode\u0026and aborts with:This blocks every workflow that uses a GitHub Actions expression with
&&(or&) insidenetwork.allowed. We hit this on sixerrors-*workflows inmicrosoft/vscode-engineeringaftergh aw compileproduced lock files with v0.25.40.Repro
Minimal
.md:Run:
Inspect the generated
.lock.yml— the firewall step contains:The
\u0026\u0026lives inside a${{ … }}block, so GH Actions expression parser rejects the workflow before any step runs.Root cause (likely)
In the codepath that builds the AWF config (introduced in #29220), the entire config object is
json.Marshal'd (or equivalent), which escapes&→\u0026per Go's defaultHTMLEscapebehavior.${{ … }}placeholders are interpolated as raw strings before JSON serialization, so the operators inside them get escaped along with everything else.Suggested fixes (any one works)
json.EncoderwithSetEscapeHTML(false). This is the lowest-risk fix and matches what the Actions runtime / firewall actually consume (raw JSON, not HTML).${{ … }}placeholders after JSON serialization (template the config, then inject expressions). Avoids any future escape-class bugs.allowDomainsentirely by pre-computing the union of possible hosts at compile time. (Workaround for users today; doesn't fix the underlying bug.)Workaround
Manually
sed -i 's/\\u0026/\&/g'theprintfline in each affected.lock.yml. Survives until the nextgh aw compile.Affected versions
Reproduced with
gh-aw-firewallschema URL…/v0.25.40/awf-config.schema.jsonin generated lock files. Likely all gh-aw versions since #29220 landed (closed 2026-04-30).Impact
All 6
errors-*workflows inmicrosoft/vscode-engineeringthat use staging/prod MCP URL switching:errors-triage,errors-stable-scan,errors-fix,errors-regression-scan,errors-dedup-test,errors-investigate