Skip to content

Commit 570ddf4

Browse files
committed
feat: add production gotchas reference for runtime debugging
- Expression interpolation in code blocks - GitHub App token permission inheritance - tools.github vs gh CLI conflicts - add-comment discussions:write default - Missing merge safe-output workaround - Write permission compiler rejection - .lock.yml workflow push exemption - MCP JSON escaping and stdout constraints - pull_request trigger merge ref timing
1 parent 85d1d81 commit 570ddf4

1 file changed

Lines changed: 329 additions & 0 deletions

File tree

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
# Production Gotchas
2+
3+
Hard-won learnings from debugging gh-aw workflows in production. Each item was verified through real production failures and hours of debugging. Organized by category for quick reference.
4+
5+
---
6+
7+
## Expression Interpolation
8+
9+
### Code Block Interpolation Behavior
10+
11+
`${{ }}` expressions in the workflow markdown body **are** interpolated when they appear in plain text, but are **NOT** interpolated inside fenced code blocks (` ```bash `, ` ```yaml `, etc.).
12+
13+
**Symptom:** The agent receives the literal string `${{ github.event.issue.number }}` inside a code block. Bash cannot parse `${{`, and the command fails silently or produces unexpected results.
14+
15+
**Fix:** Declare environment variables in frontmatter `env:` (or in the prose as env var instructions), then reference them as `$VARIABLE_NAME` inside code blocks.
16+
17+
**Example — WRONG:**
18+
19+
````markdown
20+
```bash
21+
gh issue view ${{ github.event.issue.number }}
22+
```
23+
````
24+
25+
The agent receives the literal text `${{ github.event.issue.number }}` and bash fails.
26+
27+
**Example — CORRECT:**
28+
29+
Declare in prose (before code blocks):
30+
```markdown
31+
The issue number is available as `$ISSUE_NUMBER` (from `${{ github.event.issue.number }}`).
32+
```
33+
34+
Or instruct the agent to read event context:
35+
```markdown
36+
Read the issue number from the event payload or the `event.json` file.
37+
```
38+
39+
Then in code blocks, use shell variables:
40+
````markdown
41+
```bash
42+
gh issue view "$ISSUE_NUMBER"
43+
```
44+
````
45+
46+
**Key principle:** Plain text expressions are interpolated at compile time. Fenced code block contents are passed to the agent verbatim.
47+
48+
---
49+
50+
## GitHub App & Token Conflicts
51+
52+
### tools.github.app Permission Inheritance
53+
54+
When using `tools.github.app` in frontmatter, the GitHub MCP server requests **ALL** workflow-level permissions for the App token — not just the ones the MCP tools need.
55+
56+
**Symptom:** HTTP 422 error: `"permissions requested are not granted to this app"`. The App token creation fails because the App installation doesn't have a permission that's declared in the workflow's `permissions:` block but isn't relevant to the MCP tools.
57+
58+
**Example:** Workflow has `packages: read` (for GHCR docker login in `steps:`), and `tools.github.app` is configured. The App token requests `packages:read`, but the GitHub App doesn't have the Packages permission installed. Result: 422 error on App token creation.
59+
60+
**Fix:** NEVER use `tools.github.app` unless the GitHub App has **ALL** permissions declared in the workflow's `permissions:` block. For read-only GitHub MCP access, omit `app:` entirely — `GITHUB_TOKEN` works fine for read operations.
61+
62+
**Decision tree:**
63+
1. Does the agent need to **write** via MCP tools (not safe-outputs)? → Use `tools.github.app`
64+
2. Does the agent only need to **read** via MCP tools? → Omit `app:`, use default `GITHUB_TOKEN`
65+
3. Does the workflow have permissions the App doesn't have? → Remove `app:` or remove those permissions
66+
67+
### tools.github MCP Conflicts with gh CLI
68+
69+
When `tools.github` is configured in frontmatter, the GitHub MCP server takes ownership of the `GITHUB_TOKEN`. This prevents the `gh` CLI from authenticating in bash tool calls.
70+
71+
**Symptom:** Agent logs show `"Detected conflict with gh CLI authentication"` or `"missing tool"`. Bash commands using `gh` fail with authentication errors.
72+
73+
**Fix:** If the workflow **only** uses `gh` CLI via bash (not GitHub MCP tools), remove `tools.github` entirely from the frontmatter. The `gh` CLI authenticates directly with `GITHUB_TOKEN` and doesn't need the MCP server.
74+
75+
**Coexistence rule:** You can use `tools.github` AND `gh` CLI in the same workflow, but be aware that the MCP server may interfere with `gh` CLI auth. If you hit auth conflicts, choose one approach:
76+
- MCP tools only (via `tools.github`) for structured API access
77+
- `gh` CLI only (via `tools.bash: ["gh:*"]`) for shell-based access
78+
79+
---
80+
81+
## Safe-Output Gotchas
82+
83+
### add-comment Defaults to discussions:write
84+
85+
The `add-comment` safe-output silently requests `discussions:write` permission by default, even if the workflow never touches discussions.
86+
87+
**Symptom:** HTTP 422 error when the safe-output execution job tries to create the App token — the App doesn't have Discussions permission.
88+
89+
**Fix:** Explicitly set `discussions: false` under `add-comment:` in safe-outputs:
90+
91+
```yaml
92+
safe-outputs:
93+
add-comment:
94+
discussions: false
95+
max: 1
96+
```
97+
98+
### No merge-pull-request Safe Output
99+
100+
gh-aw has **NO** safe-output for merging pull requests. The complete list of valid safe-outputs is:
101+
102+
- `add-comment`
103+
- `add-labels`
104+
- `add-reviewer`
105+
- `assign-milestone`
106+
- `assign-to-agent`
107+
- `assign-to-user`
108+
- `autofix-code-scanning-alert`
109+
- `close-issue`
110+
- `close-pull-request`
111+
- `create-agent-session`
112+
- `create-code-scanning-alert`
113+
- `create-discussion`
114+
- `create-issue`
115+
- `create-project`
116+
- `create-project-status-update`
117+
- `create-pull-request`
118+
- `create-pull-request-review-comment`
119+
- `dispatch-workflow`
120+
- `hide-comment`
121+
- `link-sub-issue`
122+
- `push-to-pull-request-branch`
123+
- `remove-labels`
124+
- `reply-to-pull-request-review-comment`
125+
- `resolve-pull-request-review-thread`
126+
- `submit-pull-request-review`
127+
- `unassign-from-user`
128+
- `update-discussion`
129+
- `update-issue`
130+
- `update-project`
131+
- `update-pull-request`
132+
- `update-release`
133+
- `upload-asset`
134+
135+
**If you need to merge a PR**, use `post-steps` with a fresh App token:
136+
137+
```yaml
138+
post-steps:
139+
- name: Generate merge token
140+
id: merge-token
141+
uses: actions/create-github-app-token@v2
142+
with:
143+
app-id: ${{ vars.APP_ID }}
144+
private-key: ${{ secrets.APP_KEY }}
145+
owner: org-name
146+
repositories: repo-name
147+
- name: Auto-merge PR
148+
env:
149+
GH_TOKEN: ${{ steps.merge-token.outputs.token }}
150+
PR_NUMBER: ${{ github.event.pull_request.number }}
151+
run: |
152+
gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --squash --auto
153+
```
154+
155+
---
156+
157+
## Permissions & Compilation
158+
159+
### Write Permissions Not Allowed in Frontmatter
160+
161+
The gh-aw compiler rejects write permissions in the `permissions:` block. For example, `permissions: issues: write` fails compilation.
162+
163+
**Reason:** ALL write operations must go through safe-outputs, which use the App token internally. The workflow `permissions:` block is strictly for `GITHUB_TOKEN` scoping and is read-only.
164+
165+
**Fix:** Declare only `read` permissions in frontmatter. Use safe-outputs for all write operations.
166+
167+
**WRONG:**
168+
```yaml
169+
permissions:
170+
issues: write
171+
pull-requests: write
172+
```
173+
174+
**CORRECT:**
175+
```yaml
176+
permissions:
177+
issues: read
178+
pull-requests: read
179+
180+
safe-outputs:
181+
add-labels:
182+
allowed: [bug, feature]
183+
add-comment:
184+
discussions: false
185+
```
186+
187+
---
188+
189+
## Workflow Files & Push Restrictions
190+
191+
### .lock.yml Files Exempt from Workflow Push Restriction
192+
193+
Standard `.yml`/`.yaml` files in `.github/workflows/` block GitHub App token branch pushes. The error is:
194+
195+
> "refusing to allow a GitHub App to create or update workflow without `workflows` permission"
196+
197+
This happens because GitHub treats any `.yml`/`.yaml` file in `.github/workflows/` as a workflow definition and requires the `workflows` permission scope for modifications — a scope that GitHub Apps cannot have.
198+
199+
**Key insight:** gh-aw compiled `.lock.yml` files are **EXEMPT** from this restriction. The `.lock.yml` extension is specifically excluded from GitHub's workflow file detection.
200+
201+
**Rule:** NEVER add standard GitHub Actions `.yml`/`.yaml` workflow files alongside gh-aw workflows if using App tokens for safe-outputs (e.g., `create-pull-request`). The App token push will fail because the `.yml` file triggers the workflow push restriction.
202+
203+
**If you need standard GH Actions functionality:**
204+
- Move the logic into a gh-aw workflow as a `post-steps` block
205+
- Or use a completely separate repository for standard workflows
206+
- Or ensure the PR branch never contains `.yml`/`.yaml` files in `.github/workflows/`
207+
208+
---
209+
210+
## Frontmatter Features
211+
212+
### `if` Guard (Conditional Execution)
213+
214+
The frontmatter-level `if:` field prevents workflow runs from starting entirely. This is evaluated **before** any jobs run, saving compute compared to job-level conditionals.
215+
216+
```yaml
217+
if: contains(github.event.issue.labels.*.name, 'status:assessed') && contains(github.event.issue.labels.*.name, 'source:jira')
218+
```
219+
220+
**Use for:** Scoping event-triggered workflows to specific label combinations, issue states, or other event conditions that should prevent the agent from running at all.
221+
222+
### post-steps Feature
223+
224+
`post-steps:` is a valid frontmatter field for defining custom workflow steps that run **after** AI execution completes.
225+
226+
**Characteristics:**
227+
- Runs in the agent job after the Codex/Claude/Copilot agent finishes
228+
- Has access to full job context: `${{ github.event.* }}`, `${{ secrets.* }}`, `${{ vars.* }}`
229+
- Can reference step outputs from earlier steps via `${{ steps.*.outputs.* }}`
230+
- Compiles into GitHub Actions job steps that execute sequentially after the agent step
231+
232+
**Use cases:**
233+
- Merging PRs (no safe-output exists for this — see above)
234+
- Closing issues with custom logic
235+
- Cleanup operations
236+
- Anything requiring write access via a fresh App token
237+
238+
```yaml
239+
post-steps:
240+
- name: Generate token
241+
id: app-token
242+
uses: actions/create-github-app-token@v2
243+
with:
244+
app-id: ${{ vars.APP_ID }}
245+
private-key: ${{ secrets.APP_KEY }}
246+
- name: Cleanup
247+
env:
248+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
249+
run: |
250+
echo "Post-agent cleanup"
251+
```
252+
253+
---
254+
255+
## MCP Server Constraints
256+
257+
### JSON Escaping in entrypointArgs
258+
259+
`gh aw compile` does **NOT** escape double quotes (`"`) in `entrypointArgs` values for MCP servers. If a command string contains double quotes, the compiled JSON will be malformed.
260+
261+
**Symptom:** Compilation succeeds but the MCP server fails to start at runtime because the Docker CMD array has broken JSON.
262+
263+
**Fix:** NEVER use double quotes in command strings passed to MCP server configs. Use alternative approaches:
264+
- Use `grep` instead of `jq` if the jq expression would contain quotes
265+
- Use single-quoted strings where the shell allows it
266+
- Use `sed` or `awk` with patterns that avoid quotes
267+
268+
**WRONG:**
269+
```yaml
270+
mcp-servers:
271+
my-server:
272+
container: ghcr.io/org/image:latest
273+
entrypointArgs:
274+
- "jq '.data[\"key\"]' input.json"
275+
```
276+
277+
**CORRECT:**
278+
```yaml
279+
mcp-servers:
280+
my-server:
281+
container: ghcr.io/org/image:latest
282+
entrypointArgs:
283+
- "grep key input.json"
284+
```
285+
286+
### MCP Server Stdout Constraint
287+
288+
The MCP gateway reads server stdout as JSON-RPC. **ANY** stdout output before the MCP handshake breaks server initialization.
289+
290+
**Symptom:** MCP server silently fails to initialize. The agent gets no tools from that server. No obvious error in the workflow logs — check `agent-artifacts/mcp-logs/{server}.log`.
291+
292+
**Common causes:**
293+
- `apk add` or `apt-get install` printing progress to stdout
294+
- `pip install` or `npm install` printing to stdout
295+
- `curl` printing download progress
296+
- Python `print()` statements in server startup code
297+
- Shell scripts echoing status messages
298+
299+
**Fix:** Redirect ALL installation/setup output to `/dev/null`:
300+
301+
```yaml
302+
entrypointArgs:
303+
- "sh"
304+
- "-c"
305+
- "apk add --no-cache curl >/dev/null 2>&1 && exec my-server"
306+
```
307+
308+
### `gh aw mcp inspect/list` Limitation
309+
310+
The `gh aw mcp inspect` and `gh aw mcp list` commands do **NOT** follow `imports:` directives. They only see MCP servers declared in the direct frontmatter of the workflow file being inspected.
311+
312+
**Fix:** To verify imported MCP servers, check the compiled `.lock.yml` file instead, which contains the fully resolved configuration.
313+
314+
---
315+
316+
## Trigger Behavior
317+
318+
### pull_request Trigger and Workflow File Resolution
319+
320+
The `pull_request` trigger uses the workflow definition from the **merge commit** (base branch + PR branch merged together). This means:
321+
322+
1. If you push a new `.lock.yml` to `main` and immediately reopen/re-trigger a PR, the merge ref may still use the **old** version of `main`
323+
2. GitHub needs a moment to update the merge ref after pushes to the base branch
324+
325+
**Symptom:** A PR re-run uses an outdated workflow definition even though you just pushed the correct one to `main`.
326+
327+
**Fix:** Wait a few seconds between pushing to `main` and triggering `pull_request` events. If testing workflow changes, close and reopen the PR (or push a new commit to the PR branch) to force a merge ref rebuild.
328+
329+
**Related:** For `push` triggers, the workflow is always from the pushed commit. For `workflow_dispatch`, it's from the branch selected in the UI.

0 commit comments

Comments
 (0)