Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
offline, or hosted catalogs without publishing a GitHub repo (closes
#676). (#1739)
- Enterprise bootstrap mirror mode (`APM_RELEASE_METADATA_URL`, `APM_RELEASE_BASE_URL`, `APM_INSTALLER_BASE_URL`, `APM_PYPI_INDEX_URL`, `APM_NO_DIRECT_FALLBACK`) routes `install.sh`, `install.ps1`, and `apm self-update` through internal mirrors with fail-closed public fallback; closes #1680. (#1733)
- `apm pack --archive-format [zip|tar.gz]` escape hatch (default `zip`) lets
CI pipelines that depended on the previous `.tar.gz` default opt back in without
changing the project default. Passing `--archive-format` without `--archive` is
now a `UsageError`. (#1720)

### Changed

- **BREAKING:** `apm pack --archive` now produces `.zip` by default instead of `.tar.gz`, matching the format produced by `apm publish` and expected by Claude Code and plugin hosts while staying natively extractable on Windows without WSL or a tar binary. Note: ZIP archives are typically 30-130% larger than `.tar.gz` for text-heavy skill bundles due to per-file compression; use `--archive-format tar.gz` if archive size is a priority. (#1720)

### Fixed

Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/concepts/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ Source: `src/apm_cli/commands/audit.py`.
### bundle

A local-install artifact produced by `apm pack`. Either a directory or a
`.tar.gz` containing `plugin.json` at the root and (in current versions)
an embedded `apm.lock.yaml` with per-file SHA-256 hashes. Installed via
`apm install <path-or-tarball>`.
`.zip` (or legacy `.tar.gz`) containing `plugin.json` at the root and (in
current versions) an embedded `apm.lock.yaml` with per-file SHA-256 hashes.
Installed via `apm install <path-or-archive>`.

NOT a package source repository. A bundle is the packed, hash-verified
output of one; you ship bundles, you author packages.
Expand Down
20 changes: 10 additions & 10 deletions docs/src/content/docs/consumer/deploy-a-bundle.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
---
title: Deploy a local bundle
description: Install a plugin-format bundle from a directory or tarball without touching apm.yml.
description: Install a plugin-format bundle from a directory or archive without touching apm.yml.
---

You have a bundle on disk -- a directory or `.tar.gz` someone handed you, or
the output of `apm pack --format plugin`. Drop it into a project with one
command:
You have a bundle on disk -- a directory or `.zip` someone handed you (or a
legacy `.tar.gz`), or the output of `apm pack --format plugin`. Drop it into a
project with one command:

```bash
apm install ./path/to/bundle
apm install ./dist/my-pkg-1.0.0.tar.gz
apm install ./dist/my-pkg-1.0.0.zip
```

This is a sibling flow to [Install packages](../install-packages/). Instead
Expand All @@ -19,8 +19,8 @@ the install is imperative, like `dpkg -i` next to `apt install`.

## What counts as a bundle

A plugin-format bundle is a directory (or gzipped tarball of one) with a
`plugin.json` at the root and primitive folders alongside it:
A plugin-format bundle is a directory, zip archive, or legacy gzipped tarball
with a `plugin.json` at the root and primitive folders alongside it:

```
my-bundle/
Expand All @@ -44,16 +44,16 @@ warns and proceeds.
## How the install works

```
$ apm install ./dist/my-pkg-1.0.0.tar.gz --target copilot
[>] Installing local bundle from ./dist/my-pkg-1.0.0.tar.gz
$ apm install ./dist/my-pkg-1.0.0.zip --target copilot
[>] Installing local bundle from ./dist/my-pkg-1.0.0.zip
[+] Bundle integrity verified
[+] Deployed 7 files to .github/
```

Steps APM runs:

1. **Detect.** Path exists and contains `plugin.json` at the bundle root
(tarballs are extracted to a temp directory first).
(zip archives and legacy tarballs are extracted to a temp directory first).
2. **Verify integrity.** Hash every file listed in `pack.bundle_files`;
reject any symlink, hash mismatch, or unlisted file.
3. **Deploy.** Map `agents/`, `skills/`, `commands/`, `hooks/` into the
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/consumer/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ You're here because you want to install someone else's APM packages and use them
| You manage `apm.yml` and need lockfile / dependency commands | [Manage dependencies](./manage-dependencies/) |
| You want APM to wire MCP servers (GitHub, Atlassian, ...) into your tools | [Install MCP servers](./install-mcp-servers/) |
| You want APM to wire LSP servers into supported runtimes | [Install LSP servers](./install-lsp-servers/) |
| You received a local `.tar.gz` bundle and need to install it | [Deploy a local bundle](./deploy-a-bundle/) |
| You received a local `.zip` or `.tar.gz` bundle and need to install it | [Deploy a local bundle](./deploy-a-bundle/) |
| You hit `Drift detected` after a `git pull` | [Drift and secure-by-default](./drift-and-secure-by-default/) |
| Your org rolled out `apm-policy.yml` and your install is now blocked | [Governance on the consumer ramp](./governance-on-the-consumer-ramp/) |

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/enterprise/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ A path must pass all three checks. Failure on any check prevents the file from b

### Local bundle install trust model

`apm install <bundle>` accepts a directory or `.tar.gz` produced by `apm pack`. Bundles are imperative (no policy / dependency-resolver / network) and target-agnostic; the consumer's project drives where files land. Trust boundaries:
`apm install <bundle>` accepts a directory or `.zip` (or legacy `.tar.gz`) produced by `apm pack`. Bundles are imperative (no policy / dependency-resolver / network) and target-agnostic; the consumer's project drives where files land. Trust boundaries:

1. **`bundle_files` keys are untrusted.** They come from the bundle's own `apm.lock.yaml` and are validated for traversal sequences before any filesystem path is constructed; resolved destinations must remain within the deploy root. Unsafe entries are skipped with a warning.
2. **`plugin.json` is bundle metadata, never deployed.** It is recognized case-insensitively and skipped in both the manifest-driven deploy loop and the lockfile-less fallback walk so case-folding filesystems (HFS+, NTFS) cannot smuggle a renamed file past the skip.
Expand Down
14 changes: 10 additions & 4 deletions docs/src/content/docs/integrations/ci-cd.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Use `apm pack` in CI to build a distributable bundle once, then consume it in do

### Pack in CI (build once)

`apm-action@v1` with `pack: true` emits an APM-format bundle (`--format apm --archive`) so downstream jobs can restore it via `tar xzf` or the action's restore mode.
`apm-action@v1` with `pack: true` emits an APM-format bundle (`--format apm --archive`) so downstream jobs can restore it via `unzip` or the action's restore mode.

```yaml
- uses: microsoft/apm-action@v1
Expand All @@ -186,7 +186,7 @@ Use `apm pack` in CI to build a distributable bundle once, then consume it in do
- uses: actions/upload-artifact@v4
with:
name: agent-config
path: build/*.tar.gz
path: build/*.zip
```

### Pack as standalone plugin
Expand All @@ -204,19 +204,25 @@ Use `apm pack` in CI to build a distributable bundle once, then consume it in do

The APM bundle layout below assumes the upstream job ran `apm-action@v1` with `pack: true` (or `apm pack --format apm --archive`). Plugin-format output cannot be restored this way because it does not carry the install-time directory tree.

:::caution[Migrating from the previous `.tar.gz` default?]
`apm pack --archive` now writes `.zip`. If a downstream job still expects
`build/*.tar.gz`, add `--archive-format tar.gz` to the pack step instead of
switching the restore step to `unzip`.
:::

```yaml
- uses: actions/download-artifact@v4
with:
name: agent-config
- run: tar xzf build/*.tar.gz -C ./
- run: unzip -o build/*.zip -d ./
```

Or use the apm-action restore mode to unpack a bundle directly:

```yaml
- uses: microsoft/apm-action@v1
with:
bundle: ./agent-config.tar.gz
bundle: ./agent-config.zip
```

See the [Pack a bundle guide](../../producer/pack-a-bundle/) for the full workflow.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/integrations/gh-aw.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ The repo needs an `apm.yml` with dependencies and `apm.lock.yaml` for reproducib

For sandboxed environments where network access is restricted during workflow execution, use pre-built APM bundles:

1. Run `apm pack --format apm --archive` in your CI pipeline to produce a self-contained APM bundle (the format restorable via `tar xzf` or `apm-action` restore mode).
1. Run `apm pack --format apm --archive` in your CI pipeline to produce a self-contained APM bundle (`.zip` by default; restorable via `unzip` or `apm-action` restore mode).
2. Distribute the bundle as a workflow artifact or commit it to the repository.
3. Reference the bundled primitives directly from `.github/agents/` in your workflow.

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/producer/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Five steps, in order. Each links to the page that owns it:
| 1 | [Author primitives](./author-primitives/) | Skills, prompts, instructions, agents, hooks, commands, MCP under `.apm/` |
| 2 | [Compile your package](./compile/) | `apm compile` writes deterministic per-target output you can git-diff |
| 3 | [Preview and validate](./preview-and-validate/) | `apm preview` and `apm view` confirm what consumers will receive |
| 4 | [Pack a bundle](./pack-a-bundle/) | `apm pack` produces a `.tar.gz` you can ship offline or to a marketplace |
| 4 | [Pack a bundle](./pack-a-bundle/) | `apm pack` produces a `.zip` you can ship offline or to a marketplace |
| 5 | [Publish to a marketplace](./publish-to-a-marketplace/) | Others install your package with `apm install <owner>/<repo>` |

You don't need a marketplace to start. Step 4 is enough for internal teams; the marketplace step is for public discovery.
Expand Down
19 changes: 13 additions & 6 deletions docs/src/content/docs/producer/pack-a-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ description: Build a plugin-format bundle from your .apm/ source so others can d
---

A bundle is the artifact you hand to a consumer when you do not want to publish
to a registry. It is a directory (or `.tar.gz` of one) containing a
to a registry. It is a directory (or archive -- `.zip` by default, `.tar.gz` via
`--archive-format tar.gz`) containing a
`plugin.json`, your primitive folders, and an embedded `apm.lock.yaml` that
Comment on lines 6 to 9
pins every file by SHA-256. Build it with one command from a project that has
`.apm/` and `apm.yml`:
Expand Down Expand Up @@ -42,12 +43,18 @@ $ apm pack
[i] Share with: apm install build/my-pkg
```

Add `--archive` to get a single `.tar.gz` instead of a directory; use `-o` to
change the output location (default `./build`).
Add `--archive` to get a single archive (`.zip` by default; use `--archive-format tar.gz`
for legacy CI pipelines) instead of a directory; use `-o` to change the output location
(default `./build`).

:::tip[Windows-native handoff]
ZIP archives are natively extractable on Windows -- no WSL, tar, or additional
tooling required. That is why `.zip` is the default archive format.
:::

```bash
apm pack --archive -o ./dist
# -> ./dist/my-pkg-<version>.tar.gz
# -> ./dist/my-pkg-<version>.zip
```

## The plugin.json contract
Expand Down Expand Up @@ -109,8 +116,8 @@ Three common ways to hand off a bundle:
- **Directory + git.** Commit `build/<pkg>/` to a release branch or a separate
artifacts repo. Consumers `git clone` and run `apm install ./build/<pkg>`.
- **Archive + GitHub release.** `apm pack --archive` then upload the
`.tar.gz` as a release asset. Consumers download and run
`apm install ./<pkg>-<version>.tar.gz`.
`.zip` as a release asset. Consumers download and run
`apm install ./<pkg>-<version>.zip`.
- **Marketplace entry.** If your project also has a `marketplace:` block in
`apm.yml`, `apm pack` builds `marketplace.json` alongside the bundle. See
[Publish to a marketplace](./publish-to-a-marketplace/).
Expand Down
31 changes: 19 additions & 12 deletions docs/src/content/docs/producer/releasing-from-any-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ VERSION="${VERSION:?VERSION must be set, e.g. v1.2.3}"

apm pack --check-versions --check-clean --json > pack-report.json

for f in build/*.tar.gz .claude-plugin/marketplace.json; do
for f in build/*.zip .claude-plugin/marketplace.json; do
[ -f "$f" ] || continue
sha256sum "$f" > "${f}.sha256"
done

gh release create "$VERSION" \
build/*.tar.gz \
build/*.tar.gz.sha256 \
build/*.zip \
build/*.zip.sha256 \
.claude-plugin/marketplace.json \
.claude-plugin/marketplace.json.sha256 \
--title "$VERSION" \
Expand Down Expand Up @@ -85,23 +85,30 @@ you need to customise any step.
> **Reference deployment.** [`DevExpGbb/zava-agent-config`](https://github.com/DevExpGbb/zava-agent-config)
> runs this exact pipeline. The
> [v6.1.2 release](https://github.com/DevExpGbb/zava-agent-config/releases/tag/v6.1.2)
> attaches 7 per-plugin tarballs + their `.sha256` companions +
> attaches 7 per-plugin bundles + their `.sha256` companions +
> `marketplace-6.1.2.json` (15 assets total) via the workflow in
> [`.github/workflows/release.yml`](https://github.com/DevExpGbb/zava-agent-config/blob/main/.github/workflows/release.yml).
> APM `0.16.0` and apm-action `v1.9.1` or newer required.

:::caution[Migrating release workflows from `.tar.gz`?]
The examples below assume the new `.zip` default from `apm pack --archive`.
If your release job still uploads or hashes `build/*.tar.gz`, either update
those globs to `.zip` or add `--archive-format tar.gz` to preserve the previous
artifact format.
:::

```yaml
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install apm-cli
- run: |
apm pack --check-versions --check-clean --json > pack-report.json
for f in build/*.tar.gz .claude-plugin/marketplace.json; do
for f in build/*.zip .claude-plugin/marketplace.json; do
[ -f "$f" ] || continue
sha256sum "$f" > "${f}.sha256"
done
gh release create "${GITHUB_REF_NAME}" \
build/*.tar.gz build/*.tar.gz.sha256 \
build/*.zip build/*.zip.sha256 \
.claude-plugin/marketplace.json* \
--title "${GITHUB_REF_NAME}" --notes-file CHANGELOG.md
env:
Expand All @@ -120,13 +127,13 @@ release:
- pip install apm-cli
- apm pack --check-versions --check-clean --json > pack-report.json
- |
for f in build/*.tar.gz .claude-plugin/marketplace.json; do
for f in build/*.zip .claude-plugin/marketplace.json; do
[ -f "$f" ] || continue
sha256sum "$f" > "${f}.sha256"
done
- |
glab release create "$CI_COMMIT_TAG" \
build/*.tar.gz build/*.tar.gz.sha256 \
build/*.zip build/*.zip.sha256 \
.claude-plugin/marketplace.json* \
--notes-file CHANGELOG.md
```
Expand All @@ -143,12 +150,12 @@ pipeline {
sh '''
pip install apm-cli
apm pack --check-versions --check-clean --json > pack-report.json
for f in build/*.tar.gz .claude-plugin/marketplace.json; do
for f in build/*.zip .claude-plugin/marketplace.json; do
[ -f "$f" ] || continue
sha256sum "$f" > "${f}.sha256"
done
gh release create "${TAG_NAME}" \
build/*.tar.gz build/*.tar.gz.sha256 \
build/*.zip build/*.zip.sha256 \
.claude-plugin/marketplace.json* \
--notes-file CHANGELOG.md
'''
Expand All @@ -171,13 +178,13 @@ steps:
- script: pip install apm-cli
- script: apm pack --check-versions --check-clean --json > pack-report.json
- script: |
for f in build/*.tar.gz .claude-plugin/marketplace.json; do
for f in build/*.zip .claude-plugin/marketplace.json; do
[ -f "$f" ] || continue
sha256sum "$f" > "${f}.sha256"
done
- script: |
gh release create "$(Build.SourceBranchName)" \
build/*.tar.gz build/*.tar.gz.sha256 \
build/*.zip build/*.zip.sha256 \
.claude-plugin/marketplace.json* \
--notes-file CHANGELOG.md
env:
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/reference/cli/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ apm install [PACKAGE_REF...] [OPTIONS]

With no arguments it installs everything from `apm.yml`. With one or more `PACKAGE_REF` arguments it adds those packages to `apm.yml` (creating one if needed) and installs only what was added. `apm install --mcp NAME` is the dedicated path for adding an MCP server entry.

`PACKAGE_REF` accepts: shorthand (`owner/repo`), HTTPS or SSH Git URLs, FQDN shorthand (`host/owner/repo`), local paths (`./path`, `/abs/path`, `~/path`), packed bundles (`./bundle.tar.gz`), and marketplace refs (`NAME@MARKETPLACE[#ref]`).
`PACKAGE_REF` accepts: shorthand (`owner/repo`), HTTPS or SSH Git URLs, FQDN shorthand (`host/owner/repo`), local paths (`./path`, `/abs/path`, `~/path`), packed bundles (`./bundle.zip`, `./bundle.tar.gz`), and marketplace refs (`NAME@MARKETPLACE[#ref]`).

:::caution
`http://` dependencies are refused unless you pass `--allow-insecure` (direct) or `--allow-insecure-host HOSTNAME` (transitive).
Expand Down Expand Up @@ -176,7 +176,7 @@ ls /tmp/apm-out # apm_modules/ apm.lock.yaml .github/ .gitignore

```bash
apm install ./build/my-bundle
apm install ./my-bundle.tar.gz --as custom-name
apm install ./my-bundle.zip --as custom-name
apm install ./my-bundle --target opencode
```

Expand Down
Loading
Loading