You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
.github/workflows/shared/apm.md (the recommended APM integration template documented at https://githubnext.github.io/gh-aw/reference/dependencies/) crashes any pull_request_target agentic workflow whose triggering PR modifies apm.lock.yaml (or any other tracked file that overlaps the bundle's contents). Failure surfaces in gh-aw's own checkout_pr_branch.cjs step with no mention of APM, so adopters debug the wrong layer or abandon.
This affects every adopter who uses APM dependencies with gh-aw, on the canonical first-run PR ("I added a dependency, please review.").
error: Your local changes to the following files would be overwritten by checkout:
apm.lock.yaml
ERR_API: Failed to checkout PR branch: The process '/usr/bin/git' failed with exit code 1
Root cause
shared/apm.md has an asymmetric isolation contract between the Pack job and the Restore pre-agent-step:
Step
isolated
working-directory
Pack
'true'
/tmp/gh-aw/apm-workspace
Restore
unset
'.' (= ${{ github.workspace }})
Restore extracts the bundle (which contains .github/{skills,agents,instructions,prompts}/, apm.lock.yaml, apm.yml, apm_modules/) directly into ${{ github.workspace }}. Verified in microsoft/apm-action's src/runner.ts:
actionOwnsDir = isolated || packInput || !!bundleInput — bundle mode already implies the action creates the working directory if missing, so symmetric isolation is a one-line change with no side-effects.
The unpacker contract (apm unpack, unpacker.py:37-39) is documented as: "If a local file has the same name as a bundle file, the bundle file wins (overwrite)" — so any tracked file in the consumer repo whose path collides with the bundle gets dirtied.
In pull_request_target flows, gh-aw's checkout_pr_branch.cjs then runs git checkout -B <branch> origin/pr-head, which aborts on dirty tracked files. The agent never starts.
Why this matters beyond one PR
Triggers on the canonical first-run PR for new APM adopters (a PR that adds a dependency is exactly a PR that modifies apm.lock.yaml).
Error message names neither APM nor microsoft/apm-action. Adopters file against gh-aw or assume their checkout is broken.
shared/apm.md ships from gh-aw but executes in every external repo that imports it. It is effectively a public API surface; regressions have cross-repo blast radius.
Restore extracts into a temp directory (symmetric with Pack), and a small bridge step stages only what the agent needs into ${{ github.workspace }} using no-clobber copy semantics so the consumer's tracked files always win. This mirrors APM's own discovery priority (discover_primitives_with_dependencies — local primitives have highest priority).
Exact YAML diff
In .github/workflows/shared/apm.md, change ONLY the pre-agent-steps: block. jobs:, import-schema:, the Pack step, and everything else are untouched:
pre-agent-steps:
- name: Download APM bundle artifact
uses: actions/[email protected]
with:
name: ${{ needs.activation.outputs.artifact_prefix }}apm
path: /tmp/gh-aw/apm-bundle
- name: Find APM bundle path
id: apm_bundle
run: echo "path=$(find /tmp/gh-aw/apm-bundle -name '*.tar.gz' | head -1)" >> "$GITHUB_OUTPUT"
- name: Restore APM packages
uses: microsoft/[email protected]
with:
bundle: ${{ steps.apm_bundle.outputs.path }}
+ working-directory: /tmp/gh-aw/apm-restore+ - name: Stage APM primitives for agent discovery+ run: |+ src=/tmp/gh-aw/apm-restore+ # apm_modules/ is gitignored and fully package-owned; safe to copy wholesale.+ [ -d "$src/apm_modules" ] && cp -a "$src/apm_modules" .+ # Primitives: add only new files. No-clobber ensures repo-tracked files+ # are never overwritten -- adopter's primitives always win over bundle contents.+ for d in skills agents instructions prompts; do+ [ -d "$src/.github/$d" ] || continue+ mkdir -p ".github/$d"+ cp -rn "$src/.github/$d/." ".github/$d/"+ done
That is the entire change. No new import-schema inputs. No adopter-facing API surface change. Adopters keep importing exactly as before:
push / schedule / workflow_dispatch flows (no subsequent PR checkout)
No regression: bridge runs unconditionally, no git dependency, no error swallowing.
apm_modules/ (gitignored, package-owned)
Wholesale cp -a is safe.
Why not the simpler alternatives
We considered and rejected:
git checkout -- . 2>/dev/null || true after Restore (the workaround we shipped in microsoft/apm's own copy of shared/apm.md, see apm unpack writes apm.lock.yaml / apm.yml to output dir, violating documented metadata-only contract microsoft/apm#901): blunt revert of all tracked file modifications (not just APM's), git-dependent (silently swallows errors in no-checkout flows), and reads as a workaround in a public template. Fine as a single-repo internal mitigation; wrong shape for the shared template that every adopter imports.
isolated: 'true' on the Restore step with working-directory: '.': catastrophic — isolated mode in apm-action clears existing primitive dirs under .github/ first, which would WIPE the consumer's existing primitives. (Verified in microsoft/apm-actionsrc/runner.ts:316 — clearExistingPrimitives is invoked in isolated mode.)
isolated: 'true' + working-directory: /tmp/...: redundant — bundleInput already triggers actionOwnsDir = true, so the action creates the temp dir without isolated. Adding the flag adds reader cognitive load with no behavior change.
Edge cases / hardening notes
cp -rn portability: -n (no-clobber) is supported by GNU coreutils (all GitHub-hosted Ubuntu runners) and BSD cp (macOS runners). For self-hosted runners on exotic distros, a safe fallback is rsync -a --ignore-existing (also available on all GitHub-hosted runners). Not a blocker for hosted runners.
Package intentionally replaces a repo-tracked primitive: If an APM package ships, e.g., .github/agents/foo.agent.md and the consumer has the same path tracked, cp -rn keeps the consumer's version. This is the correct semantic (repo authority > package defaults) and matches APM's local discovery priority. If a consumer ever needs the package version to win, the right place to add a --force-primitives flag is microsoft/apm-action, not the shared template.
Pre-existing apm_modules/ in workspace: in normal CI, apm_modules/ is gitignored and absent. Hardening option (paranoia-level, not required): replace cp -a "$src/apm_modules" . with rm -rf apm_modules; cp -a "$src/apm_modules" ..
Symmetry with Pack: Pack uses working-directory: /tmp/gh-aw/apm-workspace; this fix uses /tmp/gh-aw/apm-restore. Both stay under gh-aw's /tmp/gh-aw/* convention. Mental model: "APM operates in temp space; only primitives bridge to the workspace."
Future apm-action v2: a native restore-to mode that does the isolated extract + selective stage in one step would let the bridge step disappear. Out of scope for this issue; track separately against microsoft/apm-action.
A workflow built on shared/apm.md whose triggering PR modifies a tracked .github/agents/*.agent.md succeeds, and the PR's version of that primitive is the one the agent discovers (not the bundle's).
A push-triggered workflow built on shared/apm.md discovers all bundle-installed primitives at ${{ github.workspace }}/.github/{skills,agents,instructions,prompts}/.
A workflow_dispatch-triggered workflow with no actions/checkout at all discovers all bundle-installed primitives.
Summary
.github/workflows/shared/apm.md(the recommended APM integration template documented at https://githubnext.github.io/gh-aw/reference/dependencies/) crashes anypull_request_targetagentic workflow whose triggering PR modifiesapm.lock.yaml(or any other tracked file that overlaps the bundle's contents). Failure surfaces in gh-aw's owncheckout_pr_branch.cjsstep with no mention of APM, so adopters debug the wrong layer or abandon.This affects every adopter who uses APM dependencies with gh-aw, on the canonical first-run PR ("I added a dependency, please review.").
Originating failure
Root cause
shared/apm.mdhas an asymmetric isolation contract between the Pack job and the Restore pre-agent-step:isolatedworking-directory'true'/tmp/gh-aw/apm-workspace'.'(=${{ github.workspace }})Restore extracts the bundle (which contains
.github/{skills,agents,instructions,prompts}/,apm.lock.yaml,apm.yml,apm_modules/) directly into${{ github.workspace }}. Verified inmicrosoft/apm-action'ssrc/runner.ts:actionOwnsDir = isolated || packInput || !!bundleInput— bundle mode already implies the action creates the working directory if missing, so symmetric isolation is a one-line change with no side-effects.apm unpack,unpacker.py:37-39) is documented as: "If a local file has the same name as a bundle file, the bundle file wins (overwrite)" — so any tracked file in the consumer repo whose path collides with the bundle gets dirtied.In
pull_request_targetflows, gh-aw'scheckout_pr_branch.cjsthen runsgit checkout -B <branch> origin/pr-head, which aborts on dirty tracked files. The agent never starts.Why this matters beyond one PR
apm.lock.yaml).microsoft/apm-action. Adopters file against gh-aw or assume their checkout is broken.shared/apm.mdships from gh-aw but executes in every external repo that imports it. It is effectively a public API surface; regressions have cross-repo blast radius.pr-review-panel-style trusted-base workflows) that we will file as a follow-up against gh-aw once this is fixed; our microsoft/apm tracking issue ([WIP] Generate a documentation section on agentic engines with a separate page for each engine (Claude, codex, custom). For each engine, - document the front matter format, - document specific configuration for that engine - explain how the network isolat... #901) cross-references it.Recommended fix
Restore extracts into a temp directory (symmetric with Pack), and a small bridge step stages only what the agent needs into
${{ github.workspace }}using no-clobber copy semantics so the consumer's tracked files always win. This mirrors APM's own discovery priority (discover_primitives_with_dependencies— local primitives have highest priority).Exact YAML diff
In
.github/workflows/shared/apm.md, change ONLY thepre-agent-steps:block.jobs:,import-schema:, the Pack step, and everything else are untouched:pre-agent-steps: - name: Download APM bundle artifact uses: actions/[email protected] with: name: ${{ needs.activation.outputs.artifact_prefix }}apm path: /tmp/gh-aw/apm-bundle - name: Find APM bundle path id: apm_bundle run: echo "path=$(find /tmp/gh-aw/apm-bundle -name '*.tar.gz' | head -1)" >> "$GITHUB_OUTPUT" - name: Restore APM packages uses: microsoft/[email protected] with: bundle: ${{ steps.apm_bundle.outputs.path }} + working-directory: /tmp/gh-aw/apm-restore + - name: Stage APM primitives for agent discovery + run: | + src=/tmp/gh-aw/apm-restore + # apm_modules/ is gitignored and fully package-owned; safe to copy wholesale. + [ -d "$src/apm_modules" ] && cp -a "$src/apm_modules" . + # Primitives: add only new files. No-clobber ensures repo-tracked files + # are never overwritten -- adopter's primitives always win over bundle contents. + for d in skills agents instructions prompts; do + [ -d "$src/.github/$d" ] || continue + mkdir -p ".github/$d" + cp -rn "$src/.github/$d/." ".github/$d/" + doneThat is the entire change. No new
import-schemainputs. No adopter-facing API surface change. Adopters keep importing exactly as before:Why this shape
apm.lock.yamlcollision with PR checkout/tmp/gh-aw/apm-restore, never touches the workspace..github/agents/reviewer.agent.md)cp -rnskips them. Repo's version wins. PR checkout succeeds.${{ github.workspace }}/.github/{skills,agents,instructions,prompts}/)actions/checkout)push/schedule/workflow_dispatchflows (no subsequent PR checkout)apm_modules/(gitignored, package-owned)cp -ais safe.Why not the simpler alternatives
We considered and rejected:
git checkout -- . 2>/dev/null || trueafter Restore (the workaround we shipped in microsoft/apm's own copy of shared/apm.md, see apm unpack writes apm.lock.yaml / apm.yml to output dir, violating documented metadata-only contract microsoft/apm#901): blunt revert of all tracked file modifications (not just APM's), git-dependent (silently swallows errors in no-checkout flows), and reads as a workaround in a public template. Fine as a single-repo internal mitigation; wrong shape for the shared template that every adopter imports.isolated: 'true'on the Restore step withworking-directory: '.': catastrophic —isolatedmode inapm-actionclears existing primitive dirs under.github/first, which would WIPE the consumer's existing primitives. (Verified inmicrosoft/apm-actionsrc/runner.ts:316—clearExistingPrimitivesis invoked in isolated mode.)isolated: 'true'+working-directory: /tmp/...: redundant —bundleInputalready triggersactionOwnsDir = true, so the action creates the temp dir withoutisolated. Adding the flag adds reader cognitive load with no behavior change.Edge cases / hardening notes
cp -rnportability:-n(no-clobber) is supported by GNU coreutils (all GitHub-hosted Ubuntu runners) and BSDcp(macOS runners). For self-hosted runners on exotic distros, a safe fallback isrsync -a --ignore-existing(also available on all GitHub-hosted runners). Not a blocker for hosted runners.Package intentionally replaces a repo-tracked primitive: If an APM package ships, e.g.,
.github/agents/foo.agent.mdand the consumer has the same path tracked,cp -rnkeeps the consumer's version. This is the correct semantic (repo authority > package defaults) and matches APM's local discovery priority. If a consumer ever needs the package version to win, the right place to add a--force-primitivesflag ismicrosoft/apm-action, not the shared template.Pre-existing
apm_modules/in workspace: in normal CI,apm_modules/is gitignored and absent. Hardening option (paranoia-level, not required): replacecp -a "$src/apm_modules" .withrm -rf apm_modules; cp -a "$src/apm_modules" ..Symmetry with Pack: Pack uses
working-directory: /tmp/gh-aw/apm-workspace; this fix uses/tmp/gh-aw/apm-restore. Both stay under gh-aw's/tmp/gh-aw/*convention. Mental model: "APM operates in temp space; only primitives bridge to the workspace."Future apm-action v2: a native
restore-tomode that does the isolated extract + selective stage in one step would let the bridge step disappear. Out of scope for this issue; track separately againstmicrosoft/apm-action.Acceptance criteria
pull_request_targetagentic workflow built onshared/apm.mdsucceeds on a PR that modifiesapm.lock.yaml. Verify against the failing job above (https://github.com/microsoft/apm/actions/runs/24883083247/job/72856352586?pr=889) once microsoft/apm pulls the new shared/apm.md.shared/apm.mdwhose triggering PR modifies a tracked.github/agents/*.agent.mdsucceeds, and the PR's version of that primitive is the one the agent discovers (not the bundle's).push-triggered workflow built onshared/apm.mddiscovers all bundle-installed primitives at${{ github.workspace }}/.github/{skills,agents,instructions,prompts}/.workflow_dispatch-triggered workflow with noactions/checkoutat all discovers all bundle-installed primitives.References
microsoft/apm-actionsource (verifiesactionOwnsDir,isolatedclearing behavior, restore semantics): https://github.com/microsoft/apm-action/blob/main/src/runner.ts