Skip to content

fix(restore): install APM and unpack via apm CLI to avoid dirtying workspace#27

Merged
danielmeppiel merged 2 commits intomainfrom
fix/restore-mode-installs-apm
Apr 24, 2026
Merged

fix(restore): install APM and unpack via apm CLI to avoid dirtying workspace#27
danielmeppiel merged 2 commits intomainfrom
fix/restore-mode-installs-apm

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

Closes #26.

What this fixes

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, see microsoft/apm#889 for the failing run).

Why this fix

apm unpack 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 (verified against microsoft/apm src/apm_cli/bundle/unpacker.py). The fix is to always install APM in restore mode so extractBundle takes the verified apm unpack path.

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 install itself fails) also gets --exclude=apm.lock.yaml --exclude=apm.lock --exclude=apm.yml so it can never dirty a workspace either.

Behavior change

Before After
apm CLI installed in restore mode? No Yes (consistent with install/pack modes)
Extraction path used in restore mode tar xzf (always; fallback) apm unpack (always; primary path)
apm.lock.yaml written to working-directory Yes No
apm.yml written to working-directory Yes No
Bundle integrity verification No (unverified warning logged) Yes
Action inputs / outputs unchanged unchanged

Tests

  • Unit (src/__tests__/runner.test.ts): new run (restore mode) describe asserts ensureApmInstalled() is invoked, and that its invocation order precedes extractBundle() (so apm is on PATH when extractBundle probes 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.yml flags are present.

  • Integration (.github/workflows/ci.ymltest-restore-clean-workspace): new job that reproduces the gh-aw scenario end-to-end on a real ubuntu-latest runner. It commits a tracked apm.lock.yaml, packs a bundle, resets to the tracked baseline, restores the bundle into the git checkout, then asserts:

    1. git status apm.lock.yaml apm.yml shows no modifications (the metadata files are pristine after restore).
    2. Primitives were deployed under .github/.
    3. git checkout <baseline-sha> -- . succeeds — the exact operation that originally aborted with the apm.lock.yaml overwrite error.

    Wired into the release job's needs: so it gates the v1 tag bump.

All 49 unit tests pass. typecheck + lint + build clean. dist/index.js rebuilt 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):

v1.5.0 — Changed: Restore mode now installs the apm CLI and verifies bundles via apm unpack. As a side-effect, apm.lock.yaml and apm.yml are no longer written to working-directory — they were never supposed to be (per apm unpack's documented contract); the previous tar fallback was leaking them. Consumers who depended on those files appearing post-restore can retrieve them from the bundle artifact directly via tar xzf <bundle> --include='*/apm.lock.yaml' -O.

Migration impact

  • github/gh-aw shared/apm.md: bump pin from microsoft/apm-action@v1.4.1 to @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.)
  • microsoft/apm own copy of shared/apm.md: drop the temporary post-restore git checkout -- . 2>/dev/null || true mitigation once @v1.5 is pinned.
  • External adopters using apm-action@v1.x directly: workspace stops getting dirtied on restore; restore now also gets bundle integrity verification.

References

…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>
Copilot AI review requested due to automatic review settings April 24, 2026 13:08
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 extractBundle can reliably take the verified apm unpack path.
  • Harden the tar fallback with --exclude flags to avoid leaking bundle metadata into working-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

Comment thread src/runner.ts
}

// RESTORE MODE: extract bundle, skip APM installation entirely.
// RESTORE MODE: install APM, then extract via `apm unpack`.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
Comment thread README.md
| `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). |
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
| `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`. |

Copilot uses AI. Check for mistakes.
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>
@danielmeppiel danielmeppiel merged commit 677ddbf into main Apr 24, 2026
12 checks passed
danielmeppiel added a commit to microsoft/apm that referenced this pull request Apr 24, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v1.5: restore mode must install apm CLI so bundles are unpacked via 'apm unpack' (not raw tar fallback)

2 participants