fix(restore): install APM and unpack via apm CLI to avoid dirtying workspace#27
Conversation
…rkspace
In restore mode the action previously skipped 'ensureApmInstalled()'
(runner.ts:90 — 'skip APM installation entirely'), forcing extractBundle
through its raw 'tar xzf --strip-components=1' fallback. That fallback
extracted the *entire* bundle — including 'apm.lock.yaml' and 'apm.yml'
— into 'working-directory'. When 'working-directory' was a git checkout
(the default '${{ github.workspace }}'), those tracked files became
dirty and any subsequent 'git checkout' aborted with:
error: Your local changes to the following files would be overwritten
by checkout: apm.lock.yaml
This broke every 'pull_request_target' agentic workflow whose triggering
PR modified the lockfile — i.e., the canonical first-run PR for any new
APM adopter (originally surfaced via gh-aw 'shared/apm.md' callers).
The 'apm unpack' CLI honors the bundle contract: it copies only files
listed in the lockfile's 'deployed_files' (primitives + apm_modules) and
never writes the lockfile or manifest to the output dir. The fix is to
always install APM in restore mode so extractBundle takes the verified
'apm unpack' path.
Tool-cached install adds at most a single small download per runner
(negligible vs. agent-job cost), and we get bundle integrity verification
for free. The previous 'unverified — install APM for integrity checks'
warning was already advertising the fallback as the inferior code path.
Defense-in-depth: the tar fallback (now reached only if 'apm install'
itself fails) also gets '--exclude=apm.lock.yaml --exclude=apm.lock
--exclude=apm.yml' so it can never dirty a workspace either.
Tests:
- new unit test in runner.test.ts asserts ensureApmInstalled() runs
before extractBundle() in restore mode
- existing bundler tar-fallback test extended to assert exclude flags
- new integration job 'test-restore-clean-workspace' in CI reproduces
the gh-aw scenario end-to-end on a real ubuntu-latest runner: pack a
bundle, commit a tracked apm.lock.yaml, restore the bundle, then
assert (a) 'git status' shows no modifications to apm.lock.yaml /
apm.yml and (b) 'git checkout <baseline-sha> -- .' succeeds without
the regression error
Refs: #26
Refs: microsoft/apm#889
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes restore-mode workspace pollution by ensuring the APM CLI is installed and using apm unpack (verified) rather than the raw tar fallback that could extract apm.lock.yaml / apm.yml into a git checkout.
Changes:
- Install APM in restore mode so
extractBundlecan reliably take the verifiedapm unpackpath. - Harden the tar fallback with
--excludeflags to avoid leaking bundle metadata intoworking-directory. - Add unit + CI integration coverage to prevent regressions (including the git-dirty checkout scenario).
Show a summary per file
| File | Description |
|---|---|
src/runner.ts |
Calls ensureApmInstalled() in restore mode; adds detailed rationale comment block. |
src/bundler.ts |
Adds tar fallback --exclude flags to avoid extracting lockfile/manifest metadata. |
src/__tests__/runner.test.ts |
Adds restore-mode regression test asserting APM install happens before extraction. |
src/__tests__/bundler.test.ts |
Extends tar-fallback test to assert new exclusion flags. |
dist/index.js |
Rebuilt distribution bundle reflecting the TypeScript changes. |
README.md |
Updates restore-mode documentation and bundle input description to reflect verified extraction via APM. |
.github/workflows/ci.yml |
Adds an integration job that reproduces and guards against the dirty-workspace git checkout failure. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (2)
src/runner.ts:116
- Restore mode now always calls ensureApmInstalled() before extractBundle(), but the restore log message later still includes guidance like “install APM for integrity checks” when extraction is unverified. Consider adjusting the wording to reflect the real remediation (APM wasn’t available so tar fallback ran / check install logs) rather than telling users to install APM.
const bundlePath = await resolveLocalBundle(bundleInput, resolvedDir);
core.info(`Restoring bundle: ${bundlePath}`);
const result = await extractBundle(bundlePath, resolvedDir);
src/runner.ts:94
- This comment embeds a specific release version (“added in v1.5”). That tends to go stale over time and makes backports/cherry-picks confusing. Consider removing the version reference and leaving the rationale only.
// Why install APM in restore mode (added in v1.5):
// `apm unpack` honors the bundle contract — it copies only files listed in
- Files reviewed: 6/7 changed files
- Comments generated: 2
| } | ||
|
|
||
| // RESTORE MODE: extract bundle, skip APM installation entirely. | ||
| // RESTORE MODE: install APM, then extract via `apm unpack`. |
There was a problem hiding this comment.
The file-level doc comment for run() still says bundle restore needs “no APM install needed”, but restore mode now always calls ensureApmInstalled(). Please update that header comment so it doesn’t contradict the actual behavior (and the README).
This issue also appears in the following locations of the same file:
- line 93
- line 114
| | `compile` | No | `false` | Run `apm compile` after install to generate AGENTS.md | | ||
| | `pack` | No | `false` | Pack a bundle after install (produces `.tar.gz` by default) | | ||
| | `bundle` | No | | Restore from a bundle (local path or glob). Skips APM installation entirely. | | ||
| | `bundle` | No | | Restore from a bundle (local path or glob). Installs APM and unpacks via `apm unpack` (verified). | |
There was a problem hiding this comment.
README now documents that bundle restore installs APM and unpacks via apm unpack, but action.yml still describes bundle as “Skips APM installation entirely.” Please update action.yml to match so Marketplace/input docs aren’t misleading.
| | `bundle` | No | | Restore from a bundle (local path or glob). Installs APM and unpacks via `apm unpack` (verified). | | |
| | `bundle` | No | | Restore from a bundle (local path or glob). Installs the APM CLI and restores the bundle via `apm unpack`. | |
Addresses two low-confidence Copilot review nits on #27: - runner.ts:94 — drop the 'added in v1.5' marker from the rationale comment so it doesn't go stale on backports/cherry-picks; the rationale stands on its own. - runner.ts:116 — restore mode now installs APM up-front, so the unverified branch only runs if that install failed transiently and extractBundle fell through to tar. Point operators at the install logs instead of telling them to install APM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
v1.4.2 fixes the restore-mode workspace pollution that caused #901 and the CI failure surfaced in #889. With v1.4.2, restore mode installs APM and uses 'apm unpack' to extract bundles, writing only files declared in the lockfile's deployed_files instead of overwriting tracked apm.lock.yaml / apm.yml / apm_modules in the caller's git workspace. - shared/apm.md: bump both pack + restore steps to @v1.4.2 - pr-review-panel.lock.yml: regenerated via 'gh aw compile' (SHA pin updated) - actions-lock.json: SHA pin updated Closes #902. Upstream: microsoft/apm-action#27, release v1.4.2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes #26.
What this fixes
In restore mode the action previously skipped
ensureApmInstalled()(runner.ts:90— "skip APM installation entirely"), forcingextractBundlethrough its rawtar xzf --strip-components=1fallback. That fallback extracted the entire bundle — includingapm.lock.yamlandapm.yml— intoworking-directory. Whenworking-directorywas a git checkout (the default${{ github.workspace }}), those tracked files became dirty and any subsequentgit checkoutaborted with:This broke every
pull_request_targetagentic workflow whose triggering PR modified the lockfile — i.e., the canonical first-run PR for any new APM adopter (originally surfaced via gh-awshared/apm.mdcallers, see microsoft/apm#889 for the failing run).Why this fix
apm unpackhonors the bundle contract: it copies only files listed in the lockfile'sdeployed_files(primitives +apm_modules) and never writes the lockfile or manifest to the output dir (verified againstmicrosoft/apmsrc/apm_cli/bundle/unpacker.py). The fix is to always install APM in restore mode soextractBundletakes the verifiedapm unpackpath.The install is tool-cached, so this adds at most a single small download per runner — negligible vs. the cost of a typical agent job, and we get bundle integrity verification for free. The previous
"unverified — install APM for integrity checks"log line was already advertising the fallback as the inferior code path on every restore.Defense-in-depth: the tar fallback (now reached only if
apm installitself fails) also gets--exclude=apm.lock.yaml --exclude=apm.lock --exclude=apm.ymlso it can never dirty a workspace either.Behavior change
apmCLI installed in restore mode?tar xzf(always; fallback)apm unpack(always; primary path)apm.lock.yamlwritten toworking-directoryapm.ymlwritten toworking-directoryunverifiedwarning logged)Tests
Unit (
src/__tests__/runner.test.ts): newrun (restore mode)describe assertsensureApmInstalled()is invoked, and that its invocation order precedesextractBundle()(soapmis on PATH whenextractBundleprobes for it).Unit (
src/__tests__/bundler.test.ts): existing tar-fallback test extended to assert the new--exclude=apm.lock.yaml/--exclude=apm.lock/--exclude=apm.ymlflags are present.Integration (
.github/workflows/ci.yml→test-restore-clean-workspace): new job that reproduces the gh-aw scenario end-to-end on a realubuntu-latestrunner. It commits a trackedapm.lock.yaml, packs a bundle, resets to the tracked baseline, restores the bundle into the git checkout, then asserts:git status apm.lock.yaml apm.ymlshows no modifications (the metadata files are pristine after restore)..github/.git checkout <baseline-sha> -- .succeeds — the exact operation that originally aborted with theapm.lock.yamloverwrite error.Wired into the
releasejob'sneeds:so it gates the v1 tag bump.All 49 unit tests pass.
typecheck+lint+buildclean.dist/index.jsrebuilt and committed.Versioning
Ship as v1.5. The visible inputs/outputs are unchanged, and the dropped behavior was a contract-violating side-effect of a fallback path the action itself self-warned about. Per semver-pragmatic convention this is a bug fix, not a major.
CHANGELOG entry to draft alongside the tag (out of scope for this PR — repo currently has no CHANGELOG file):
Migration impact
shared/apm.md: bump pin frommicrosoft/apm-action@v1.4.1to@v1.5. No template change beyond the version bump. (The bridge step the gh-aw team would otherwise have had to add — see shared/apm.md restore step crashes on PRs that modify tracked files (e.g. apm.lock.yaml) github/gh-aw#28256, since closed — becomes unnecessary.)shared/apm.md: drop the temporary post-restoregit checkout -- . 2>/dev/null || truemitigation once@v1.5is pinned.apm-action@v1.xdirectly: workspace stops getting dirtied on restore; restore now also gets bundle integrity verification.References