Skip to content

feat: bundle-format input + setup-only mode (#24)#31

Merged
danielmeppiel merged 4 commits into
mainfrom
feat/apm-format-flag-default-flip
May 2, 2026
Merged

feat: bundle-format input + setup-only mode (#24)#31
danielmeppiel merged 4 commits into
mainfrom
feat/apm-format-flag-default-flip

Conversation

@danielmeppiel

@danielmeppiel danielmeppiel commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

TL;DR

Microsoft/apm 0.11+ flips apm pack default from apm to plugin format. Plugin tarballs cannot be restored by apm unpack (no apm.lock.yaml), so any apm-action user packing today and restoring tomorrow with apm-version: latest would silently break.

This PR makes apm-action robust to the flip and adds the long-requested setup-only mode (#24).

  • bundle-format: apm | plugin input. Default is apm (the format apm unpack understands). Pack is always invoked with --format <chosen>, decoupling the action from the upstream CLI default.
  • setup-only: true input. Installs the APM CLI onto PATH and exits — like actions/setup-node. Mutually exclusive with all project-level operations.
  • apm-version and apm-path outputs are now always set in every mode.
  • Hardened installer: apm-version: latest no longer silently reuses an arbitrarily-old apm that happened to be on PATH. Resolves the latest tag from GitHub first, only reuses PATH apm when its version equals latest (or when GitHub is unreachable, with a warning).

Empirically verified end-to-end against apm HEAD (0.11.0, commit 2b9501ab) with 10 integration scenarios + 97 unit tests, plus the existing CI matrix expanded with bundle-format axis and dedicated setup-only / plugin-rejection jobs.


Why this matters now

apm HEAD pack --help says:

--format [plugin|apm] Bundle format. 'plugin' (default) emits a Claude Code plugin directory with plugin.json. 'apm' produces the legacy APM bundle layout (kept for tooling that still consumes it).

apm HEAD unpack (verified empirically):

$ apm unpack plugin-bundle.tar.gz
[>] Unpacking plugin-bundle.tar.gz -> .
[x] apm.lock.yaml not found in the bundle  -- the bundle may be incomplete.

So the moment microsoft/apm ships a release, every existing apm-action consumer that uses the default pack/restore round-trip would break — silently if they pin apm-version: latest, loudly otherwise.

apm-action's apm-version defaults to latest and was not pinned. Without this PR, the next microsoft/apm release would break the round-trip immediately.


What changed

1. bundle-format input (defensive default)

- uses: microsoft/apm-action@v1
  with:
    pack: 'true'
    # bundle-format defaults to 'apm' -- the format apm unpack understands.
    # Set to 'plugin' if you need a Claude Code plugin tarball for a different consumer.
    bundle-format: 'apm'
  • bundle-format is wired into pack as apm pack --format <value> --archive.
  • Restore detects the bundle format up front (via tar tzf) and rejects plugin-format bundles with a clear, actionable error pointing the user at bundle-format: apm and apm pack --format apm --archive.
  • Plugin-format restore is intentionally NOT implemented in this action: apm unpack itself rejects plugin tarballs (different deployment contract — no lockfile to drive deployed_files). That belongs upstream in apm unpack, not here.

2. setup-only input (issue #24)

- uses: microsoft/apm-action@v1
  id: apm
  with:
    setup-only: 'true'
    apm-version: '0.11.0'   # optional; pin recommended

- run: apm install --frozen-lockfile && apm run audit
  • Installs the CLI onto PATH and returns. Skips apm install and every project-level operation.
  • Outputs apm-version (resolved version) and apm-path (absolute path to the binary).
  • Mutually exclusive with pack, bundle, bundles-file, isolated, compile, script, dependencies, audit-report, target, bundle-format. Conflicts produce a single consolidated error listing every offending input.

3. Always-set installer outputs

apm-version and apm-path are now emitted in every mode — install, single-bundle restore, multi-bundle restore, pack, and setup-only. apm-path is resolved via tool-cache when the action installed APM, or via which apm when reusing a pre-existing CLI.

4. Installer hardening

  • apm-version: <pinned> (e.g. 0.11.0) always installs that exact version into the tool-cache. No silent short-circuit to a different apm that happens to be on PATH.
  • apm-version: latest resolves the latest GitHub release tag first, then reuses PATH apm only if its version equals latest. Otherwise installs fresh. If the GitHub Releases API is unreachable, falls back to PATH apm with a warning.
  • Bundle-format detection (tar tzf) only matches apm.lock.yaml and plugin.json at the top-level wrapper depth, avoiding false positives from nested files of the same name shipped by dependencies.

Breaking changes

None for existing apm-action users. The defensive bundle-format: apm default preserves the prior pack/restore round-trip behavior even after the upstream CLI default flips. Users who explicitly want a plugin tarball must opt in with bundle-format: 'plugin' (and accept that the action cannot restore that tarball).


How to use

Use case Inputs
Just install the CLI setup-only: 'true'
Pack apm bundle (default) pack: 'true'
Pack plugin bundle pack: 'true', bundle-format: 'plugin'
Restore apm bundle bundle: 'path/to/bundle.tar.gz'
Restore plugin bundle Not supported -- apm unpack itself rejects them

Pin the CLI for reproducibility:

- uses: microsoft/apm-action@v1
  with:
    apm-version: '0.11.0'   # not 'latest'
    pack: 'true'

Validation

  • Unit tests: 97/97 pass (npm test).
  • Lint + typecheck + build: clean.
  • Local integration suite: 10/10 scenarios pass against locally-built apm HEAD binary (0.11.0, commit 2b9501ab):
    • setup-only, mutex-setup-pack, pack-apm, apm-layout, pack-plugin, plugin-layout, restore-apm, restore-plugin-rejected, invalid-format-rejected, fmt-without-pack-rejected.
  • CI on this branch: all required checks pass — including the new test-pack matrix on bundle-format: [apm, plugin], test-setup-only, and test-restore-plugin-rejected jobs.

Review

Drafted via the apm-review-panel personas (devx-ux, supply-chain-security, code-review). Findings adopted in commits 2b6d996 and 82fcbca:

  • Always-set apm-version/apm-path outputs (CHANGELOG promised it; code now matches).
  • Removed the inverted archive mutex check (was rejecting the valid archive: 'false' value while allowing everything else; redundant with the pack rejection).
  • Replaced maintainer-facing "Tracking:" line in plugin-rejection error with operator-facing remediation steps.
  • Supply-chain hardening (this commit): apm-version: latest no longer silently reuses old PATH apm; bundle-format detection tightened to top-level depth only.

Deferred (low-impact, follow-up issues):

  • README @v1 pinning advice and serial tar tzf are minor.
  • tar fallback inheriting env not exploitable today and consistent with prior behavior.

Files

  • src/installer.tsInstallResult return; explicit-version always installs; latest consults GitHub first; which apm resolves binaryPath on PATH-reuse.
  • src/bundler.tsBundleFormat type; detectBundleFormat (top-level-only marker match); extractBundle rejects plugin format with operator-friendly error; runPackStep accepts format.
  • src/multibundle.ts — multi-bundle restore pre-flights every bundle, rejects the whole batch if any is plugin format.
  • src/runner.tssetup-only branch with consolidated mutex; bundle-format wiring on pack and restore; outputs always set.
  • action.ymlbundle-format and setup-only inputs; apm-version, apm-path, bundle-format outputs.
  • README.md, CHANGELOG.md — documented.
  • .github/workflows/ci.ymlbundle-format matrix on test-pack; new test-setup-only and test-restore-plugin-rejected jobs.

Copilot AI review requested due to automatic review settings April 30, 2026 11:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 keeps the action’s pack: true mode compatible with the existing restore pipeline by forcing apm pack to emit the legacy APM bundle layout even after upstream APM changes its default pack format.

Changes:

  • Pin apm pack to --format apm in the bundling step to preserve legacy bundle output.
  • Update unit tests to assert the new CLI argument is included.
  • Rebuild dist/ artifacts and update user-facing docs/metadata to clarify legacy bundle behavior.
Show a summary per file
File Description
src/bundler.ts Adds --format apm to the apm pack invocation and documents why it’s pinned.
src/tests/bundler.test.ts Extends assertions to confirm --format apm is passed.
dist/index.js Compiled output reflecting the pinned --format apm args.
dist/bundler.d.ts Updates generated typings/docs to match the new behavior.
action.yml Clarifies that pack produces a legacy APM bundle layout (not Claude plugin format).
README.md Adds a note explaining the pinned pack format and implications.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/6 changed files
  • Comments generated: 0

@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

APM Review Panel — verdict: panel-rejected

Run locally via the apm-review-panel skill, adapted for this companion repo. Specialists run: DevX UX, Supply Chain Security, APM CEO. Skipped as N/A for apm-action: Python Architect (TypeScript), CLI Logging Expert (no CLI surface). OSS Growth Hacker not run (no WIP/growth-strategy.md in this repo).

Verdict is deterministic: APPROVE iff sum(required) == 0 across active panelists. Two required findings here, so: REJECT. The CEO writes the narrative, not the verdict.

CEO arbitration

This is a sound defensive action, but the narrative framing needs calibration. Calling the APM bundle format "legacy" sends the wrong signal when we're doubling down on supporting it in this action — it telegraphs deprecation without a timeline, which could spook consumers. More critically, this PR skips a versioned comms hook. apm-action ships releases that users pin to (@v1, @v1.5.x); without an explicit release-note entry explaining why the pin exists, this defensive action is invisible to anyone who isn't reading the diff. Timing is correct — merge before upstream ships so consumers never hit a gap.

Required (blocks merge)

  1. Release-note hook missing for the pin. (devx-ux + ceo, agreed)
    This repo has no CHANGELOG.md (releases are GitHub Releases / tags), so the artifact is the release notes for the next tag rather than a CHANGELOG file. Either way, ship the pin with an explicit note like:

    Pin --format apm on apm pack to preserve bundle layout when upstream feat(marketplace): harden apm pack output (#1061) apm#1063 flips the default to the Claude Code plugin format. No behavior change for existing pack: true consumers.

    Cut the point release before the upstream apm release lands. (If the maintainer prefers, this could also be a one-line ## Unreleased section added to a new top-level CHANGELOG.md — but that's a structural choice, not a panel demand.)

  2. action.yml pack: description: drop "legacy". (ceo)
    The marketplace UI / IDE tooltips render this string permanently. We are actively supporting this format in this action, so "legacy" prematurely signals deprecation and contradicts the intent of the PR. Replace with classic APM bundle format (or just APM bundle formatapm.lock.yaml + primitives), keeping the "not the new Claude Code plugin format — run apm pack directly for that" clause.

Nits (apply if you agree)

  • README.md — Replace "legacy APM bundle layout" with "stable / classic APM bundle layout" for the same reason as nit Adding Microsoft SECURITY.MD #2 above. (devx-ux + ceo)
  • README.md — Add one trailing sentence after the new blockquote answering "how do I opt in to the new format from this action?" e.g. "If you need the Claude Code plugin format from a workflow, run apm pack directly instead of using pack: true." (devx-ux)
  • README.md — Optional one-liner: "This format will continue to be supported even after the new Claude Code plugin format becomes the apm pack default." Closes the longevity question. (ceo)
  • action.yml — Consider a single-line description with an ASCII -- instead of the em-dash (-- run \apm pack` directly for that.`) — em-dash already present is fine on most shells but ASCII is safer for the cross-platform tooltip. (devx-ux)
  • README.md / docs — For reproducibility, consumers who care about deterministic builds should pin apm-version rather than relying on whatever version the install step resolves; the --format apm pin protects layout, but it doesn't protect against any other upstream behavior change. Worth a sentence somewhere. (supply-chain)

What we explicitly cleared

  • Supply chaindist/ rebuild looks consistent with the source change (no transitive code drift, no new network/eval surface, no package.json / package-lock.json changes rode along). --format apm is a literal flag passed to the upstream CLI; if upstream ever renames or removes the value, the failure mode is a loud apm pack error rather than a wrong-artifact silent publish. No required findings.
  • action.yml input semantics — only the description changed; no new input, no default flip, no token surface introduced.
  • Tests — both runPackStep arg-assertion tests now pin --format apm, matching the source change.
  • Upstream coordination — pinning defensively before upstream feat(marketplace): harden apm pack output (#1061) apm#1063 lands is the correct sequence.

Future consideration (not blocking)

If/when there's appetite, a format: input on the action (default apm, opt-in plugin) would future-proof this surface so the pin is no longer a one-way door. Don't promise it in this PR — just keep it on the radar for a future minor.


Panel composition: 3 specialists ran (DevX UX, Supply Chain Security, APM CEO); Python Architect and CLI Logging Expert N/A for this repo; OSS Growth Hacker not applicable (no growth-strategy artifact in apm-action).

danielmeppiel added a commit that referenced this pull request Apr 30, 2026
- action.yml: pack: description uses 'classic' (not 'legacy') to avoid
  signaling deprecation in the marketplace UI.
- README.md: parallel rename + reassure longevity, point users to
  apm pack for plugin output, suggest pinning apm-version for full repro.
- CHANGELOG.md: new file with [Unreleased] entry documenting the
  --format apm pin and the upstream coordination requirement.

Per apm-review-panel verdict on PR #31.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two changes folded into one PR:

1. bundle-format input (default 'apm', opt-in 'plugin').
   The upstream apm CLI is flipping its default 'apm pack' format
   from 'apm' to 'plugin' in the next consumer-facing release.
   Plugin bundles do not contain apm.lock.yaml, so 'apm unpack'
   (and therefore this action's restore path) cannot consume them.
   This action now pins bundle-format=apm in the pack call by
   default, so existing pack -> restore round-trips keep working
   regardless of the upstream default. bundle-format=plugin is a
   first-class opt-in for marketplace publishers.

   Single- and multi-bundle restore detect plugin-format archives
   via tar tzf and reject them with an actionable error that names
   the archive and points at the upstream tracking issue. Prevents
   silent corruption when a plugin bundle is fed into restore.

2. setup-only mode (closes #24). Mirrors actions/setup-node:
   install the APM CLI onto PATH and exit. No apm.yml read, no
   apm install, no primitives deployed. Mutually exclusive with
   pack/bundle/bundles-file. New apm-version and apm-path outputs
   let downstream steps branch on the resolved CLI.

Installer refactored: explicit apm-version always installs the
requested version into the tool cache (no PATH short-circuit), so
the resolved version matches the requested version. apm-version
'latest' (the default) still reuses an APM already on PATH when
available.

Validated end-to-end against a locally built apm HEAD binary
(0.11.0) across 10 scenarios: setup-only, mutex rejection,
pack(apm) layout, pack(plugin) layout, restore(apm) round-trip,
restore(plugin) rejection, invalid bundle-format rejection, and
bundle-format set without pack rejection. CI updated with a
matrix on bundle-format, a setup-only smoke test, and a
plugin-restore-rejected job.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel force-pushed the feat/apm-format-flag-default-flip branch from 2c1f21c to 2953cb4 Compare May 1, 2026 13:24
@danielmeppiel danielmeppiel changed the title feat: pin --format apm for upstream apm pack default flip feat: bundle-format input + setup-only mode (#24) May 1, 2026
- Always populate apm-version and apm-path outputs (all modes), not just
  setup-only. CHANGELOG promised always-set; align code with promise.
  Resolve binaryPath via 'which apm' on PATH-reuse so the output is
  meaningful instead of empty.

- Drop 'archive' from the setup-only mutex list. archive is a sub-option
  of pack mode; rejecting pack already covers it. Flagging archive
  separately surprised composite-action templates that emit
  archive: 'true' by default. Old check was also inverted (allowed
  'true', rejected 'false').

- Drop maintainer 'Tracking:' line from plugin-restore-rejected error
  in bundler.ts. Tell the operator what to do, not what we plan to do.

- Update apm-version/apm-path output descriptions in action.yml,
  README.md, and CHANGELOG.md to match new always-set semantics.

Tests: 95/95 unit (added always-set apm-path assertions and
which-probe failure tolerance test). Integration: 10/10 against
apm HEAD binary; apm-path now populated in all modes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

Review panel fixes applied (2b6d996)

Adopted

Finding Source Fix
apm-version/apm-path only set in setup-only despite CHANGELOG promising 'always set' devx-ux N2, code-review Capture InstallResult at every ensureApmInstalled() site (lines 196, 261, 286). Outputs are now emitted in install, single-bundle restore, multi-bundle restore, and pack modes. apm-path resolved via which apm on PATH-reuse so it's never empty when an apm is callable.
Setup-only archive mutex check inverted (allowed 'true', rejected 'false') devx-ux Required, code-review Dropped archive from the conflict list entirely. archive is a sub-option of pack; rejecting pack already covers it. Flagging it separately surprised composite-action templates that emit archive: 'true' by default.
Plugin-restore error message ended with maintainer 'Tracking:' line devx-ux N5 Removed. Operator-facing errors should say what to do, not what we plan to do.
action.yml + README + CHANGELOG output descriptions misaligned with new always-set behavior devx-ux Updated to 'Always set. Resolved via tool-cache when the action installed APM, or via which apm when reusing a pre-existing CLI on PATH'.

Deferred (with rationale)

Finding Why deferred
supply-chain R1: apm-version: latest PATH-reuse can downgrade. Pre-existing behavior, not introduced by this PR. Worth a follow-up issue + RFC, not in scope here.
supply-chain N1: detectBundleFormat plugin.json depth match. Footgun, no real impact today (pack output places plugin.json at root).
supply-chain N2: tar fallback inherits env. Not exploitable today; consistent with prior version.
devx-ux Required #1: action.yml bundle-format default '' vs effective 'apm'. Keeping '' keeps the mutex simple (no false positive for default-emitted templates) and code reliably coalesces empty -> 'apm'. Description was clarified; effective default is documented.
README @v1 pinning advice (N4), serial tar tzf (N6). Minor; address in a follow-up doc/perf pass.

Validation

  • npm test: 95/95 unit (added always-set apm-path and which-probe failure tolerance tests)
  • npm run typecheck && npm run lint && npm run build: clean
  • Local integration suite (10 scenarios) against apm HEAD binary (0.11.0): 10/10 PASS. apm-path now populated in all modes (verified in driver output).

CI run pending on this push.

danielmeppiel and others added 2 commits May 1, 2026 15:34
…apshot

The 'no install ran' check was a false positive on the apm-action repo
because the repo itself ships .github/agents/, .github/aw/, and
.github/workflows/ via checkout. Compare the .github subdirectory tree
before and after invoking the action, and additionally assert that
setup-only does not produce apm.lock.yaml (which apm install would).
Also assert apm-path output is set (now always-set after 2b6d996).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Supply-chain (R1): apm-version: latest no longer silently reuses an
arbitrarily-old apm that happened to be on PATH. Resolve the latest
GitHub release tag first, then reuse PATH apm only if its version
matches latest. Otherwise install fresh. If the GitHub Releases API
is unreachable, fall back to PATH apm with an explicit warning so
operators know they may be running stale.

Supply-chain (N1): tighten detectBundleFormat plugin.json/apm.lock.yaml
detection to match only at the top-level wrapper depth. Avoids false
positives where a nested file inside a dependency payload happens to
be named plugin.json or apm.lock.yaml.

UX: rewrite plugin-restore error messages (single + multi-bundle paths)
to spell out concrete remediation -- including pointing out that
'apm unpack' itself rejects plugin tarballs, so the limitation is
upstream, not just an action constraint. Mention both the action input
('bundle-format: apm') and the upstream CLI flag ('apm pack --format
apm --archive') so users in either context know what to change.

Tests: 97/97 unit (added 'PATH equals latest', 'PATH older than
latest', 'GitHub unreachable' coverage). Integration: 10/10 against
apm HEAD binary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel linked an issue May 2, 2026 that may be closed by this pull request
@danielmeppiel danielmeppiel merged commit 060058b into main May 2, 2026
23 checks passed
danielmeppiel added a commit that referenced this pull request May 2, 2026
The CHANGELOG.md added in PR #31 only carried an Unreleased section.
This commit backfills the historical record by inventorying every
merged PR per release tag (v1.0.0 through v1.5.1) and graduates the
Unreleased section to a [1.6.0] heading dated 2026-05-02.

v1.6.0 delivers the defensive 'bundle-format: apm' default plus the
'setup-only' mode (closes #24), unblocking apm-action consumers
ahead of the upstream apm 0.12 release that flips the default
'apm pack' output to the plugin layout.

Sources:
- gh pr list (--state merged) cross-referenced with git tag dates
- existing GitHub release notes for v1.4.2, v1.5.0, v1.5.1
- commit messages in each release window

No code change. CHANGELOG-only.

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.

Support CLI-only mode

2 participants