Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `apm pack --create-tag` (optionally with `--push`) creates -- and pushes -- release git tags from a clean tree after `--check-versions` succeeds, collapsing the bump/tag/push handshake into one command. Tag names come from `marketplace.versioning` (`lockstep` -> `v<version>`; `tag_pattern` -> templated; `per_package` -> `<name>-v<version>`). `--push` requires `--create-tag` and uses explicit refspecs, never `git push --tags`. Refusals are first-class stable codes (`dirty_tree`, `tag_exists`, `version_mismatch`, `no_remote`, `push_without_tag`, `no_check_versions`, `no_marketplace`, `git_failure`) surfaced under `tag_creation.refusal_code` / `tag_push.refusal_code` in `--json` and exit `1`; the existing version-gate (`3`) and drift-gate (`4`) exit codes are unchanged. `--dry-run` previews tag names and push targets without touching git. (#1489)
- `apm deps why <package>` explains why a transitive dependency is installed by walking the lockfile's `resolved_by` chain back to the user's direct declaration in `apm.yml`. Supports `--global` for user-scope lockfiles and `--json` for scriptable output (JSON to stdout, all logs to stderr; analogue of `npm why` / `yarn why`). Exits `0` on success, `1` when the package isn't installed or the query is ambiguous, `2` when no lockfile exists. (#1490)

## [0.15.0] - 2026-05-27
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 @@ -42,7 +42,7 @@ Once the 5-rung ladder works end to end, three pages cover the operational conce
|------------------------------------------|----------------------------------------------------------------------|
| Picking a repo layout before you author | [Repo shapes](./repo-shapes/) -- two starting layouts plus a hybrid composition for teams that ship their own plugin alongside a curated marketplace of others |
| Aligning versions across local packages | [Versioning strategies](./versioning-strategies/) |
| Wiring the release into any CI provider | [Releasing from any CI](./releasing-from-any-ci/) |
| Wiring the release into any CI provider | [Releasing from any CI](./releasing-from-any-ci/) -- includes a [one-shot tagging flow](./releasing-from-any-ci/#one-shot-tagging-from-a-clean-tree) (`apm pack --create-tag --push`) for maintainers who want the CLI to materialise and push the release tag for them. |

## The producer mental model

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ $EDITOR apm.yml # 2. describe each package
apm marketplace check # 3. validate refs resolve
apm pack # 4. build marketplace artifacts
git add apm.yml .claude-plugin/marketplace.json
git commit -m "Release v1.0.0" && git tag v1.0.0 && git push --tags
git commit -m "Release v1.0.0" # 5. commit
apm pack --check-versions --create-tag --push # 6. tag and push from a clean tree
```

Step 6 collapses tag-create-and-push into one command and refuses
on a dirty tree, a tag collision, or a version mismatch. See
[Releasing from any CI -> One-shot tagging](../releasing-from-any-ci/#one-shot-tagging-from-a-clean-tree)
for the full refusal contract and exit codes.

A consumer in another repo then runs:

```bash
Expand Down
34 changes: 34 additions & 0 deletions docs/src/content/docs/producer/releasing-from-any-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,40 @@ Authenticate `gh` with a token that has `contents: write` on the
repo. Substitute the equivalent verb for non-GitHub forges
(`glab release create`, `az repos`, REST upload).

## One-shot tagging from a clean tree

When you have already committed the version bump and want the CLI
to materialise (and optionally push) the release tag for you, add
`--create-tag` and, if you also want it on `origin`, `--push`:

```bash
apm pack --check-versions --create-tag --push
```

The block runs only after `--check-versions` succeeds, so a version
mismatch still exits `3` and never produces a tag. `--push` requires
`--create-tag`. The tag name is derived from `marketplace.versioning`:

- `lockstep` (default) -> one tag, `v<version>`.
- `tag_pattern` -> one tag named by your `tag_pattern` template (e.g. `release-v<version>`).
- `per_package` -> one tag per local package, e.g. `<name>-v<version>`.

`--dry-run` previews exactly which tags would be created and pushed
without touching git. Push uses explicit refspecs
(`refs/tags/<name>:refs/tags/<name>`), never `git push --tags`, so
only the planned tags move. Refusals are stable JSON contract codes
(`dirty_tree`, `tag_exists`, `version_mismatch`, `no_remote`,
`push_without_tag`, `no_check_versions`, `no_marketplace`,
`git_failure`) emitted under `tag_creation.refusal_code` and exit
with code `1`. The existing version-gate (exit `3`) and drift-gate
(exit `4`) codes are unchanged.

Use this flow from a maintainer workstation or from CI -- the
refusal codes give pipelines a deterministic surface to react to.
The forge-native flows shown below remain valid if you prefer to
keep tag creation in `gh release create` or an equivalent.


## GitHub Actions

```yaml
Expand Down
10 changes: 9 additions & 1 deletion docs/src/content/docs/producer/versioning-strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,13 @@ Use per_package when:
apm pack --check-versions --dry-run
```

To collapse the gate-check, tag-create, and push into a single
command:

```bash
apm pack --check-versions --create-tag --push
```

See [Releasing from any CI](../releasing-from-any-ci/) for the full
release pipeline that runs both gates.
release pipeline and the
[one-shot tagging flow](../releasing-from-any-ci/#one-shot-tagging-from-a-clean-tree).
6 changes: 5 additions & 1 deletion docs/src/content/docs/reference/cli/pack.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Bundles are target-agnostic. The consumer's project decides where files land at
| `--marketplace-output PATH` | _(hidden)_ | **Deprecated.** Translates to `--marketplace-path claude=PATH` with a stderr warning. Will be removed in v0.15 (see #1318). |
| `--legacy-skill-paths` | off | Bundle skills under per-client paths (e.g. `.cursor/skills/`) instead of the converged `.agents/skills/`. Compatibility flag. |
| `--target`, `-t VALUE` | auto-detect | **Deprecated.** Recorded as informational `pack.target` metadata only; ignored by `apm install`. Will be removed in a future release. |
| `--create-tag` | off | Create annotated git tag(s) from `marketplace.versioning` after `--check-versions` succeeds on a clean tree. Requires `--check-versions`. Tag names: `lockstep` -> `v<version>`; `tag_pattern` -> templated; `per_package` -> `<name>-v<version>`. |
| `--push` | off | Push the just-created tag(s) to `origin` by explicit refspec (never `git push --tags`). Requires `--create-tag`. |

## Examples

Expand Down Expand Up @@ -143,8 +145,10 @@ Configure marketplace artifact paths in `apm.yml`: `marketplace.claude.output` c
| Code | Meaning |
|---|---|
| `0` | Success. Requested artifacts written (or, with `--dry-run`, planned). |
| `1` | Build or runtime error: network failure, ref not found, no tag matches a marketplace range, lockfile read error, or unhandled packer exception. |
| `1` | Build or runtime error: network failure, ref not found, no tag matches a marketplace range, lockfile read error, unhandled packer exception, **or** `--create-tag` / `--push` refusal (`tag_creation.refusal_code` / `tag_push.refusal_code` carries the stable code: `dirty_tree`, `tag_exists`, `version_mismatch`, `no_remote`, `push_without_tag`, `no_check_versions`, `no_marketplace`, `git_failure`). |
| `2` | `apm.yml` schema validation error. |
| `3` | `--check-versions` gate failure: bumped local package version is not strictly greater than the latest matching git tag. |
| `4` | `--check-clean` gate failure: working tree or staging area has uncommitted changes. |

## Related

Expand Down
2 changes: 1 addition & 1 deletion packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ If no `--target`, no `targets:` in `apm.yml`, and no harness signal is present,

| Command | Purpose | Key flags |
|---------|---------|-----------|
| `apm pack` | Build distributable artifacts (bundle and/or marketplace.json -- driven by `apm.yml`). Default output is a Claude Code plugin directory. Bundles are **target-agnostic**: `pack.target` is recorded in every bundle for diagnostic purposes (typically `"all"` for target-agnostic packs, or the project's detected target) and is not authoritative at install time; `pack.bundle_files` (path -> sha256) drives integrity verification. The consumer's project decides where files land. Marketplace-publishing projects (`marketplace:` block, no `dependencies:`) no longer emit the misleading "No plugin.json found" warning; after a successful build, a vendor-neutral catalog of artifact paths is appended together with a single docs pointer (`producer/publish-to-a-marketplace/#consume-from-any-assistant`) listing per-assistant install paths. Release-time gates `--check-versions` and `--check-clean` are opt-in: when present, they run after the build and exit non-zero on misalignment / drift (codes 3 and 4 respectively) so release pipelines can fail fast. | `-o PATH`, `--archive`, `--dry-run`, `--format [plugin\|apm]` (default `plugin`), `--force`, `--offline`, `--include-prerelease`, `--marketplace=FORMATS`, `--marketplace-path FORMAT=PATH`, `--json`, `--check-versions` (release gate: per-package versions match `marketplace.versioning.strategy`; exit 3 on failure), `--check-clean` (release gate: regenerate-and-diff against the committed `marketplace.json`; exit 4 on drift). `--marketplace-output PATH` and `-t/--target` are **deprecated** (warn + auto-translate where applicable). Exit codes: `0` success, `1` build/runtime error, `2` schema validation error, `3` `--check-versions` misalignment, `4` `--check-clean` drift. |
| `apm pack` | Build distributable artifacts (bundle and/or marketplace.json -- driven by `apm.yml`). Default output is a Claude Code plugin directory. Bundles are **target-agnostic**: `pack.target` is recorded in every bundle for diagnostic purposes (typically `"all"` for target-agnostic packs, or the project's detected target) and is not authoritative at install time; `pack.bundle_files` (path -> sha256) drives integrity verification. The consumer's project decides where files land. Marketplace-publishing projects (`marketplace:` block, no `dependencies:`) no longer emit the misleading "No plugin.json found" warning; after a successful build, a vendor-neutral catalog of artifact paths is appended together with a single docs pointer (`producer/publish-to-a-marketplace/#consume-from-any-assistant`) listing per-assistant install paths. Release-time gates `--check-versions` and `--check-clean` are opt-in: when present, they run after the build and exit non-zero on misalignment / drift (codes 3 and 4 respectively) so release pipelines can fail fast. `--create-tag` (with optional `--push`) materialises -- and pushes -- the release git tag(s) after a successful `--check-versions`, refusing on dirty tree / existing tag / version mismatch / missing remote with stable refusal codes (exit `1`). | `-o PATH`, `--archive`, `--dry-run`, `--format [plugin\|apm]` (default `plugin`), `--force`, `--offline`, `--include-prerelease`, `--marketplace=FORMATS`, `--marketplace-path FORMAT=PATH`, `--json`, `--check-versions` (release gate: per-package versions match `marketplace.versioning.strategy`; exit 3 on failure), `--check-clean` (release gate: regenerate-and-diff against the committed `marketplace.json`; exit 4 on drift), `--create-tag` (after `--check-versions`: create local git tag(s) from `marketplace.versioning`; refusals exit 1), `--push` (push the just-created tag(s) to `origin` via explicit refspecs; requires `--create-tag`). `--marketplace-output PATH` and `-t/--target` are **deprecated** (warn + auto-translate where applicable). Exit codes: `0` success, `1` build/runtime error or tag refusal, `2` schema validation error, `3` `--check-versions` misalignment, `4` `--check-clean` drift. |
| `apm unpack BUNDLE` | **[Deprecated]** Extract a bundle. Use `apm install <bundle-path>` instead -- it deploys directly with integrity verification and target resolution. | `-o PATH`, `--skip-verify`, `--force`, `--dry-run` |

`apm install <BUNDLE-PATH>` -- when the positional argument resolves to a directory containing `plugin.json` at its root, or to a `.tar.gz`/`.tgz` archive whose extracted root contains `plugin.json`, install switches to local-bundle mode: the bundle is integrity-verified against its embedded `apm.lock.yaml` (`pack.bundle_files`) and deployed into the consumer's resolved target. Target resolution follows the same precedence as registry installs (`--target` > `apm.yml` > directory detection); the bundle itself carries no target binding. Compile-only targets (opencode, codex, gemini) receive instructions staged under `apm_modules/<slug>/.apm/instructions/` and the install emits a hint to run `apm compile` to merge them. Other existing paths (e.g. a source-package directory without `plugin.json`) still flow through the normal local-path dependency-resolver pipeline. Files are recorded under `local_deployed_files` in the project lockfile -- `apm.yml` is **never** mutated. Honours `--target`, `--global`, `--force`, `--dry-run`, `--verbose`, plus `--as ALIAS` (log/display label only). Resolver/MCP/registry/policy flags (`--update`, `--mcp`, `--parallel-downloads`, `--allow-insecure-host`, `--skill`, ...) are rejected with a single consolidated error -- local-bundle install is an imperative deploy and bypasses those subsystems.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ ignore = [
# Prevents new code from exceeding the worst existing violations.
# Tighten these over time via dedicated refactoring PRs.
max-statements = 275 # current max: 269 (mcp_integrator.py::install)
max-args = 18 # current max: 16 (commands/install.py)
max-args = 19 # current max: 19 (commands/pack.py with --create-tag/--push)
max-branches = 115 # current max: 108 (mcp_integrator.py::install)
max-returns = 18 # current max: 16 (marketplace/publisher.py)

Expand Down
Loading
Loading