From 46ecdab80611b34e0f93ecdc50857d19b77c5520 Mon Sep 17 00:00:00 2001 From: nadavy Date: Tue, 16 Jun 2026 15:40:07 +0300 Subject: [PATCH] fix: correct stale tar.gz/tarball refs and publish UX gaps after zip default (#1779) apm publish and apm pack --archive now produce .zip by default (#1720). This commit cleans all remaining stale references and fixes five UX gaps surfaced in the post-merge panel review. Stale reference cleanup: - docs/reference/cli/publish.md: .tar.gz -> .zip, --tarball -> --zip, tarball root -> archive root, remove tar czf example, fix ASCII - docs/guides/registries.md: same fixes + add cross-platform zip-build example in Custom layouts section - docs/reference/registry-http-api.md: correct publish format note - packages/apm-guide/.apm/skills/apm-usage/package-authoring.md: .tar.gz -> .zip, tarball -> archive, --tarball -> --zip - packages/apm-guide/.apm/skills/apm-usage/workflow.md: .zip as default - src/apm_cli/commands/publish.py: module docstring + em dash + tarball - src/apm_cli/deps/registry/extractor.py: "tarball sha256 mismatch" -> "archive sha256 mismatch" - src/apm_cli/deps/registry/resolver.py: em dash + tar.gz default Panel review fixes (post-merge): - publish.py _PUBLISH_HELP: add --package to all four examples (required=True flag was missing from every example -- copy-paste crash) - publish.py line 272: replace em dash with -- in Forbidden ClickException (user-visible on 403 responses; prior commit missed this one) - resolver.py lines 98, 104, 363, 369: replace "registry tarball for" with "registry archive for" in RegistryResolutionError strings (same class of fix as extractor.py, missed in first pass) - package-authoring.md: add --package to all publish examples - registries.md Custom layouts: add --package + python -m zipfile build step (Windows users had no actionable guidance after tar czf example was removed) - publish.md --zip row: add "(renamed from --tarball in v0.20.0)" migration note so CI pipelines using --tarball get a recovery path Co-Authored-By: Claude Sonnet 4.6 --- docs/src/content/docs/guides/registries.md | 20 +++++---- .../src/content/docs/reference/cli/publish.md | 45 +++++++++---------- .../docs/reference/registry-http-api.md | 4 +- .../skills/apm-usage/package-authoring.md | 14 +++--- .../.apm/skills/apm-usage/workflow.md | 2 +- src/apm_cli/commands/publish.py | 18 ++++---- src/apm_cli/deps/registry/extractor.py | 2 +- src/apm_cli/deps/registry/resolver.py | 12 ++--- 8 files changed, 58 insertions(+), 59 deletions(-) diff --git a/docs/src/content/docs/guides/registries.md b/docs/src/content/docs/guides/registries.md index 8feef0af6..6ea6eafae 100644 --- a/docs/src/content/docs/guides/registries.md +++ b/docs/src/content/docs/guides/registries.md @@ -291,22 +291,24 @@ apm publish --package acme/my-skill apm install acme/internal-tools#^1.0.0 ``` -[`apm publish`](../../reference/cli/publish/) reads `apm.yml`, builds a **flat registry archive** (`.tar.gz` with `apm.yml`, `.apm/`, and standard documentation files at the tarball root), and uploads via `PUT /v1/packages/{owner}/{repo}/versions/{version}`. Consumers with a default registry configured install with the same `owner/repo#version` shorthand they would use for GitHub. +[`apm publish`](../../reference/cli/publish/) reads `apm.yml`, builds a **flat registry archive** (`.zip` with `apm.yml`, `.apm/`, and standard documentation files at the archive root), and uploads via `PUT /v1/packages/{owner}/{repo}/versions/{version}`. Consumers with a default registry configured install with the same `owner/repo#version` shorthand they would use for GitHub. -Registry archives use the **APM source layout** that `apm install` and the [Registry HTTP API section 6](../../reference/registry-http-api/#6-server-validation-rules-publish) expect -- not the plugin bundle wrapper from `apm pack --archive` (`{name}-{version}/plugin.json`). If you already ship marketplace plugin bundles, either repack as a flat archive or pass `--tarball`. +Registry archives use the **APM source layout** that `apm install` and the [Registry HTTP API section 6](../../reference/registry-http-api/#6-server-validation-rules-publish) expect -- not the plugin bundle wrapper from `apm pack --archive` (`{name}-{version}/plugin.json`). If you already ship marketplace plugin bundles, either repack as a flat archive or pass `--zip`. **Auto-pack requirements:** - `apm.yml` with `name:` and `version:` - A `.apm/` directory with your primitives (skills, instructions, hooks, etc.) -Auto-pack writes `{name}-{version}.tar.gz` in the project root, includes `README.md`, `CHANGELOG.md`, and `LICENSE` / `LICENCE` when present (case-insensitive, symlinks excluded — matching npm's behaviour), and skips macOS `._*` / `.DS_Store` sidecars. +Auto-pack writes `{name}-{version}.zip` in the project root, includes `README.md`, `CHANGELOG.md`, and `LICENSE` / `LICENCE` when present (case-insensitive, symlinks excluded), and skips macOS `._*` / `.DS_Store` sidecars. -**Skill-only or custom layouts** -- build the tarball yourself and pass `--tarball`: +**Custom layouts** -- build the zip yourself and pass `--zip`: ```bash -tar czf my-skill-0.0.1.tar.gz apm.yml SKILL.md -apm publish --tarball my-skill-0.0.1.tar.gz +# Cross-platform zip build (Python stdlib -- no extra tools needed) +python -m zipfile -c ./build/my-skill-0.0.1.zip apm.yml .apm/ + +apm publish --package acme/my-skill --zip ./build/my-skill-0.0.1.zip ``` Some registries accept archives without validating `apm.yml` on upload; APM still validates on install. Prefer a valid flat layout at publish time. @@ -318,8 +320,8 @@ apm publish --package acme/my-skill # Choose a registry when multiple are configured apm publish --package acme/my-skill --registry corp-main -# Publish a pre-built flat tarball (skip auto-pack) -apm publish --package acme/my-skill --tarball ./build/my-package-1.0.0.tar.gz +# Publish a pre-built zip (skip auto-pack) +apm publish --package acme/my-skill --zip ./build/my-package-1.0.0.zip # Preview what would be uploaded without uploading apm publish --package acme/my-skill --dry-run @@ -329,7 +331,7 @@ apm publish --package acme/my-skill --dry-run |---|---| | `--registry NAME` | Registry name from the `registries:` block. Required when multiple registries are configured. | | `--package OWNER/REPO` | Package identity to publish as (required, e.g. `acme/my-skill`). | -| `--tarball PATH` | Path to a pre-built flat `.tar.gz` tarball. Skips auto-pack. | +| `--zip PATH` | Path to a pre-built flat `.zip` archive. Skips auto-pack. | | `--dry-run` | Preview without uploading. | | `--verbose` / `-v` | Show detailed output. | diff --git a/docs/src/content/docs/reference/cli/publish.md b/docs/src/content/docs/reference/cli/publish.md index 101c3b89b..d5ebabd33 100644 --- a/docs/src/content/docs/reference/cli/publish.md +++ b/docs/src/content/docs/reference/cli/publish.md @@ -15,7 +15,7 @@ apm publish [OPTIONS] `apm publish` uploads a package version to a configured registry via `PUT /v1/packages/{owner}/{repo}/versions/{version}`. -By default the command **auto-packs** a flat registry archive in the project root (`{name}-{version}.tar.gz`) containing `apm.yml`, `.apm/`, and standard root-level documentation files (`README.md`, `CHANGELOG.md`, `LICENSE` / `LICENCE`, matched case-insensitively) at the tarball root. Symlinks are excluded. This is **not** the plugin bundle layout from [`apm pack`](../pack/) (`{name}-{version}/plugin.json`). +By default the command **auto-packs** a flat registry archive in the project root (`{name}-{version}.zip`) containing `apm.yml`, `.apm/`, and standard root-level documentation files (`README.md`, `CHANGELOG.md`, `LICENSE` / `LICENCE`, matched case-insensitively) at the archive root. Symlinks are excluded. This is **not** the plugin bundle layout from [`apm pack`](../pack/) (`{name}-{version}/plugin.json`). Requires the experimental `registries` feature: @@ -31,30 +31,29 @@ The project's `apm.yml` must declare a `registries:` block with at least one reg |---|---|---| | `--registry NAME` | _(required when multiple registries configured)_ | Registry name from the `registries:` block. | | `--package OWNER/REPO` | _(required)_ | Package identity to publish as (e.g. `acme/my-skill`). | -| `--tarball PATH` | auto-pack | Path to a pre-built `.tar.gz`. Skips auto-pack. | +| `--zip PATH` | auto-pack | Path to a pre-built `.zip`. Skips auto-pack. (renamed from `--tarball` in v0.20.0) | | `--dry-run` | off | Print what would be uploaded; do not call the registry. | -| `--verbose`, `-v` | off | Show auto-pack details (tarball path). | +| `--verbose`, `-v` | off | Show auto-pack details (archive path). | ## Examples Auto-pack and publish when only one registry is configured: ```bash -apm publish +apm publish --package acme/my-skill ``` Choose a registry and preview first: ```bash -apm publish --registry corp-main --dry-run -v -apm publish --registry corp-main +apm publish --package acme/my-skill --registry corp-main --dry-run -v +apm publish --package acme/my-skill --registry corp-main ``` -Publish a skill-only or custom tarball: +Publish a pre-built zip: ```bash -tar czf my-skill-0.0.1.tar.gz apm.yml SKILL.md -apm publish --tarball my-skill-0.0.1.tar.gz +apm publish --package acme/my-skill --zip ./build/my-skill-0.0.1.zip ``` Specify the registry package identity explicitly: @@ -68,9 +67,9 @@ apm publish --package acme/my-package --registry corp-main ### Successful publish ``` -[i] Publishing acme/my-package@1.2.3 to corp-main … +[i] Publishing acme/my-package@1.2.3 to corp-main... [+] Published acme/my-package@1.2.3 - digest : sha256:abc123… + digest : sha256:abc123... published_at: 2026-05-26T10:15:00Z registry : https://registry.example.com/apm/corp-main ``` @@ -78,15 +77,15 @@ apm publish --package acme/my-package --registry corp-main With `--verbose`, auto-pack also prints: ``` -[i] Packing flat registry archive -> my-package-1.2.3.tar.gz +[i] Packing flat registry archive -> my-package-1.2.3.zip ``` ### Dry run ``` [i] Would publish acme/my-package@1.2.3 to corp-main (https://registry.example.com/apm/corp-main) -[i] tarball : /path/to/project/my-package-1.2.3.tar.gz (12,345 bytes) -[i] (dry-run — nothing uploaded) +[i] archive : /path/to/project/my-package-1.2.3.zip (12,345 bytes) +[i] (dry-run -- nothing uploaded) ``` ### Common errors @@ -95,12 +94,12 @@ With `--verbose`, auto-pack also prints: |---|---| | `requires the experimental registries feature` | Run `apm experimental enable registries`. | | `apm.yml not found` | Run from the package root. | -| `requires a flat APM package (.apm/ directory)` | Add `.apm/` or pass `--tarball`. | +| `requires a flat APM package (.apm/ directory)` | Add `.apm/` or pass `--zip`. | | `Multiple registries configured` | Pass `--registry NAME`. | -| `Version '…' already exists … immutable` | HTTP 409 — bump `version:` in `apm.yml`. | -| `Registry rejected the package (validation failed)` | HTTP 422 — tarball layout invalid for the server. | -| `Forbidden — your token does not have publish permission` | HTTP 403 — check `APM_REGISTRY_TOKEN_{NAME}`. | -| `401` / credentials remediation | HTTP 401 — token missing or expired. | +| `Version '...' already exists ... immutable` | HTTP 409 -- bump `version:` in `apm.yml`. | +| `Registry rejected the package (validation failed)` | HTTP 422 -- archive layout invalid for the server. | +| `Forbidden -- your token does not have publish permission` | HTTP 403 -- check `APM_REGISTRY_TOKEN_{NAME}`. | +| `401` / credentials remediation | HTTP 401 -- token missing or expired. | Some registries return `201` with an empty body; APM still treats the upload as successful when the HTTP status is success-class. @@ -114,7 +113,7 @@ Some registries return `201` with an empty body; APM still treats the upload as ## Related -- [Registries (guide)](../../../guides/registries/) — declare registries, auth, default routing, and policy. -- [`apm pack`](../pack/) — plugin bundles and marketplace artifacts (different layout from registry publish). -- [`apm install`](../install/) — consumer side; installs registry packages with `resolved_hash` verification. -- [Registry HTTP API](../../registry-http-api/) — wire contract for `PUT …/versions/{version}`. +- [Registries (guide)](../../../guides/registries/) -- declare registries, auth, default routing, and policy. +- [`apm pack`](../pack/) -- plugin bundles and marketplace artifacts (different layout from registry publish). +- [`apm install`](../install/) -- consumer side; installs registry packages with `resolved_hash` verification. +- [Registry HTTP API](../../registry-http-api/) -- wire contract for `PUT .../versions/{version}`. diff --git a/docs/src/content/docs/reference/registry-http-api.md b/docs/src/content/docs/reference/registry-http-api.md index 533d156df..a02e2644d 100644 --- a/docs/src/content/docs/reference/registry-http-api.md +++ b/docs/src/content/docs/reference/registry-http-api.md @@ -243,7 +243,7 @@ ETag: "sha256:abc123..." **Body.** Raw archive bytes. The same bytes that hash to the `digest` advertised in `/versions`. -**Format selection at publish time.** APM publishes via `apm pack` (tar.gz). Anthropic skills publish via standard zip. Servers store and return whatever was uploaded; format conversion is NOT a server responsibility. +**Format selection at publish time.** `apm publish` sends `application/zip` by default (modern client). Legacy clients or manual uploads may use `application/gzip`. Servers store and replay whatever was uploaded; format conversion is NOT a server responsibility. **Errors** @@ -267,7 +267,7 @@ Uploads a new version. Versions are immutable: re-publishing returns `409`. PUT /v1/packages/acme/web-skills/versions/1.2.0 HTTP/1.1 Host: registry.example.com Authorization: Bearer -Content-Type: application/gzip +Content-Type: application/zip Content-Length: 24576 diff --git a/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md b/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md index 836d0c61c..186bda02d 100644 --- a/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md +++ b/packages/apm-guide/.apm/skills/apm-usage/package-authoring.md @@ -475,13 +475,13 @@ EOF export APM_REGISTRY_TOKEN_CORP_MAIN=eyJ... # 4. Preview then publish -apm publish --registry corp-main --dry-run -v -apm publish --registry corp-main +apm publish --package acme/my-skill --registry corp-main --dry-run -v +apm publish --package acme/my-skill --registry corp-main ``` `apm publish` auto-packs a **flat registry archive** in the project root -(`{name}-{version}.tar.gz`) containing `apm.yml` and `.apm/` at the -tarball root. This layout differs from the plugin bundle that +(`{name}-{version}.zip`) containing `apm.yml` and `.apm/` at the +archive root. This layout differs from the plugin bundle that `apm pack` produces (`{name}-{version}/plugin.json`). Auto-pack skips macOS `._*` / `.DS_Store` sidecars. @@ -490,12 +490,10 @@ Auto-pack requires: identity differs from the package name) - A `.apm/` directory with at least one primitive -Skill-only or custom layouts: build the tarball yourself and pass -`--tarball`: +Custom layouts: build the zip yourself and pass `--zip`: ```bash -tar czf my-skill-0.0.1.tar.gz apm.yml SKILL.md -apm publish --tarball my-skill-0.0.1.tar.gz --registry corp-main +apm publish --package acme/my-skill --zip ./build/my-skill-0.0.1.zip --registry corp-main ``` Upload contract: `PUT /v1/packages/{owner}/{repo}/versions/{version}`. diff --git a/packages/apm-guide/.apm/skills/apm-usage/workflow.md b/packages/apm-guide/.apm/skills/apm-usage/workflow.md index c86a9876c..2d4bf2e7d 100644 --- a/packages/apm-guide/.apm/skills/apm-usage/workflow.md +++ b/packages/apm-guide/.apm/skills/apm-usage/workflow.md @@ -113,4 +113,4 @@ Use `apm install --update` to refresh to latest refs. ## Local bundle install -`apm install ` accepts a directory or `.tar.gz` produced by `apm pack` and deploys its contents into the consumer's resolved target. Bundles are target-agnostic; the project decides where files land (same precedence as registry installs: `--target` > `apm.yml` > directory detection). For compile-only targets (OpenCode, Codex, Gemini, Antigravity) instructions stage under `apm_modules//.apm/instructions/` and the install prints a hint to run `apm compile` to merge them into the target's single-file format (`AGENTS.md`, `GEMINI.md`). +`apm install ` accepts a directory, `.zip` (default), or legacy `.tar.gz` produced by `apm pack` and deploys its contents into the consumer's resolved target. Bundles are target-agnostic; the project decides where files land (same precedence as registry installs: `--target` > `apm.yml` > directory detection). For compile-only targets (OpenCode, Codex, Gemini, Antigravity) instructions stage under `apm_modules//.apm/instructions/` and the install prints a hint to run `apm compile` to merge them into the target's single-file format (`AGENTS.md`, `GEMINI.md`). diff --git a/src/apm_cli/commands/publish.py b/src/apm_cli/commands/publish.py index 72ae41e0b..dc93b772e 100644 --- a/src/apm_cli/commands/publish.py +++ b/src/apm_cli/commands/publish.py @@ -1,4 +1,4 @@ -"""``apm publish`` command — upload a packed tarball to a registry. +"""``apm publish`` command -- upload a packed zip archive to a registry. Implements docs/proposals/registry-api.md §5.3: ``PUT /v1/packages/{owner}/{repo}/versions/{version}`` @@ -23,7 +23,7 @@ Publish a package to a registry. Reads apm.yml for the package name/version, packs a flat registry zip archive -(``apm.yml`` + ``.apm/`` at the archive root — not ``apm pack`` plugin bundles), +(``apm.yml`` + ``.apm/`` at the archive root -- not ``apm pack`` plugin bundles), or uses a pre-built zip via --zip, then uploads to the registry via PUT /v1/packages/{owner}/{repo}/versions/{version}. @@ -33,16 +33,16 @@ Examples: # Auto-pack and publish to the only configured registry: - apm publish + apm publish --package acme/my-skill # Choose a registry when multiple are configured: - apm publish --registry corp-main + apm publish --package acme/my-skill --registry corp-main # Publish a pre-built zip (skip the pack step): - apm publish --zip ./build/my-package-1.0.0.zip + apm publish --package acme/my-skill --zip ./build/my-package-1.0.0.zip # Preview what would be uploaded: - apm publish --dry-run + apm publish --package acme/my-skill --dry-run """ @@ -113,7 +113,7 @@ def publish_cmd(ctx, registry_name, package_id, zip_path, dry_run, verbose): if dry_run: logger.info(f"Would publish {owner}/{repo}@{version} to {registry_name} ({base_url})") logger.info(f" archive : {archive} ({archive_size:,} bytes)") - logger.info("(dry-run — nothing uploaded)") + logger.info("(dry-run -- nothing uploaded)") return # ----------------------------------------------------------- upload @@ -123,7 +123,7 @@ def publish_cmd(ctx, registry_name, package_id, zip_path, dry_run, verbose): auth = make_auth_context(registry_name) client = RegistryClient(base_url, auth) - logger.info(f"Publishing {owner}/{repo}@{version} to {registry_name} …") + logger.info(f"Publishing {owner}/{repo}@{version} to {registry_name}...") archive_bytes = archive.read_bytes() try: @@ -269,7 +269,7 @@ def _handle_publish_error( from ..deps.registry.auth import registry_token_env_var raise click.ClickException( - f"Forbidden — your token does not have publish permission for " + f"Forbidden -- your token does not have publish permission for " f"{owner}/{repo} in {registry_name!r}.\n" f"Check the token configured via {registry_token_env_var(registry_name)}." ) diff --git a/src/apm_cli/deps/registry/extractor.py b/src/apm_cli/deps/registry/extractor.py index 69feb0d45..a2d34ca31 100644 --- a/src/apm_cli/deps/registry/extractor.py +++ b/src/apm_cli/deps/registry/extractor.py @@ -121,7 +121,7 @@ def verify_sha256(data: bytes, expected_digest: str) -> str: actual = hashlib.sha256(data).hexdigest() expected = _normalize_digest(expected_digest) if actual != expected: - raise HashMismatchError(f"tarball sha256 mismatch: expected {expected}, got {actual}") + raise HashMismatchError(f"archive sha256 mismatch: expected {expected}, got {actual}") return actual diff --git a/src/apm_cli/deps/registry/resolver.py b/src/apm_cli/deps/registry/resolver.py index de0aac5c4..1387d687a 100644 --- a/src/apm_cli/deps/registry/resolver.py +++ b/src/apm_cli/deps/registry/resolver.py @@ -95,13 +95,13 @@ def _package_info_from_extracted_registry_tree( if not validation_result.is_valid: errs = "\n - ".join(validation_result.errors) raise RegistryResolutionError( - f"registry tarball for {dep_ref.repo_url!r} did not validate " + f"registry archive for {dep_ref.repo_url!r} did not validate " f"as an APM package:\n - {errs}" ) package = validation_result.package if package is None: raise RegistryResolutionError( - f"registry tarball for {dep_ref.repo_url!r} validated but produced no package metadata" + f"registry archive for {dep_ref.repo_url!r} validated but produced no package metadata" ) resolved_url = client.archive_url(owner, repo, chosen.version) package.source = resolved_url @@ -290,8 +290,8 @@ def download_package( _clear_install_target(target_path) # extract_archive dispatches on Content-Type (with magic-bytes - # fallback) — supports both tar.gz (default) and zip (Anthropic - # skills format). Hash check happens before any extraction. + # fallback) -- supports both zip (default) and legacy tar.gz. + # Hash check happens before any extraction. actual_hash = extract_archive( archive_bytes, chosen.digest, @@ -360,13 +360,13 @@ def download_from_lockfile( if not validation_result.is_valid: errs = "\n - ".join(validation_result.errors) raise RegistryResolutionError( - f"registry tarball for {dep_ref.repo_url!r} did not validate " + f"registry archive for {dep_ref.repo_url!r} did not validate " f"as an APM package:\n - {errs}" ) package = validation_result.package if package is None: raise RegistryResolutionError( - f"registry tarball for {dep_ref.repo_url!r} validated but " + f"registry archive for {dep_ref.repo_url!r} validated but " f"produced no package metadata" ) package.source = resolved_url