fix(gates): waive lockfile-exists for a package, which is not an install root

plugins/onedev is the first plugin package to declare a real dependency, and that arms a
check every previous plugin left vacuous. apm treats any directory holding both apm.yml
and apm.lock.yaml as an install root; a package is not one, so there is no green state for
it. Without a package lockfile, lockfile-exists fails outright. With one, it passes and
thereby arms the other nine checks, where drift then demands the dependency's skills be
deployed inside the package and apm lock leaves an apm_modules/ tree behind.

scripts/apm-audit-ci.sh replaces the inline bash -c loop and waives that single check for a
non-root manifest. It fails closed on three axes: the root is never waived; the failing
check must be lockfile-exists and no other, asserted by matching "1 of 1 check(s) failed";
and unrecognised output fails.

Dropping --ci for package directories was the smaller change and is wrong. Verified on apm
0.28.0 against a scratch package whose dependency entry carried no git/path/registry field:
apm audit --ci exits 1 naming it, while plain apm audit exits 0 and says nothing.
Malformed-dependency detection is the reason gates.md gives for auditing packages at all,
and a package with dependencies is the only kind that can carry a malformed dependency
entry.

The waiver matches on apm's stdout, so an apm upgrade rewording either line turns it off.
That fails the push rather than hiding a defect.

Also records the onedev entry in apm.lock.yaml, which PR #136 could not carry because the
plugin was not yet resolvable from the remote's main.

ADR: 0026

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
This commit is contained in:
2026-09-20 20:04:33 +00:00
parent 34f2df3547
commit fbd030c7ea
6 changed files with 1200 additions and 1295 deletions

View File

@@ -71,7 +71,7 @@ version-blind, so a stale key deploys fine (see [apm gates](#apm-gates)).
| Hook | Guards |
|---|---|
| `apm-audit-ci` | `apm audit --ci` once per manifest — root plus each of the six plugin packages |
| `apm-audit-ci` | `scripts/apm-audit-ci.sh` — `apm audit --ci` once per manifest, root plus each of the seven plugin packages, waiving only a package's `lockfile-exists` (see [below](#apm-audit-ci)) |
| `apm-pack-check-clean` | `apm pack --check-versions --check-clean --dry-run` — the compiled marketplace still matches what `apm.yml` + `.apm/` would generate, and per-package versions agree with the `per_package` strategy |
**Host validators** (needs the `claude` CLI on PATH)
@@ -87,8 +87,8 @@ version-blind, so a stale key deploys fine (see [apm gates](#apm-gates)).
| `check-skill-version-bump` | fails if a skill directory changed since the pushed commit's merge-base with `main` without its `metadata.version` rising above both the merge-base's and `main`'s tip's (see [below](#check-skill-version-bump)) |
Two of these shell out to `apm`: `apm-audit-ci` and `apm-pack-check-clean`. The second is a bare
`apm …` entry and the first is a `bash -c` loop calling `apm` once per package, so without the CLI
the push dies with an unhelpful "command not found". Install with `apm-install`, or
`apm …` entry and the first is `scripts/apm-audit-ci.sh`, which calls `apm` once per manifest, so
without the CLI on PATH the push dies on a "command not found" from inside the hook. Install with `apm-install`, or
`curl -sSL https://aka.ms/apm-unix | sh`; verify with `apm --version`.
### `check-skill-version-bump`
@@ -1059,11 +1059,12 @@ exclusion landed.
### `apm-audit-ci`
Runs `apm audit --ci` **once per manifest** — the root one and each of the six plugin packages —
because the root-only invocation audits the marketplace manifest and **nothing else**, and
`apm-pack-check-clean` does not parse plugin `dependencies:` blocks either. Verified: a malformed
dependency entry passes `apm pack --check-versions --check-clean --dry-run` and fails
`apm audit --ci` in that package's directory. Costs ~0.5s per package.
`scripts/apm-audit-ci.sh` runs `apm audit --ci` **once per manifest** — the root one and each of the
seven plugin packages — because the root-only invocation audits the marketplace manifest and
**nothing else**, and `apm-pack-check-clean` does not parse plugin `dependencies:` blocks either.
Verified: a malformed dependency entry passes
`apm pack --check-versions --check-clean --dry-run` and fails `apm audit --ci` in that package's
directory. Costs ~0.5s per package.
**What it actually runs is asymmetric**, and the two manifest classes are not comparable. Verified by
running `apm audit --ci` (apm 0.28.0) at the repo root and in `plugins/lint/`, reading the check
@@ -1074,10 +1075,40 @@ On the **root** manifest, **10 checks**: `lockfile-exists`, `ref-consistency`,
`skill-subset-consistency`, `config-consistency`, `content-integrity`, `includes-consent`, `drift`.
On each **plugin** manifest, **1 check**: `lockfile-exists`. Conditional, and vacuous while every
plugin `apm.yml` declares `dependencies: {apm: [], mcp: []}` — it reports `No dependencies declared
-- lockfile not required` and arms itself the moment one does not (verified by adding a git
dependency to `plugins/lint/apm.yml`). Everything else in the list above is root-only, because it is
the root install that has a lockfile, a deployment ledger and deployed files to check.
plugin `apm.yml` declared `dependencies: {apm: [], mcp: []}` — it reports `No dependencies declared
-- lockfile not required`. An earlier revision of this section said it would arm the moment one did
not. **It has armed.** `plugins/onedev` is the first plugin package to declare a real dependency — it
pins `code.onedev.io/onedev/tod#v4.3.4` so the marketplace can redistribute OneDev's TOD skills — and
the check now fires on it for real. Everything else in the list above is root-only, because it is the
root install that has a lockfile, a deployment ledger and deployed files to check.
**A plugin package that declares dependencies has no green state, so the hook waives exactly one
failure.** Verified against apm 0.28.0 in `plugins/onedev/`:
- **Without a package `apm.lock.yaml`**, `lockfile-exists` fails — `apm.yml declares dependencies but
apm.lock.yaml is absent` — reported as `1 of 1 check(s) failed`.
- **With one**, generated by `apm lock` in the package directory, `lockfile-exists` passes and
thereby arms the other nine checks; `drift` then fails reporting **8 unintegrated files** at
`.agents/skills/<name>/SKILL.md`, i.e. demanding the dependency's skills be *deployed inside the
package*. `apm lock` also leaves an `apm_modules/` tree inside the package.
The cause is that apm treats any directory holding both `apm.yml` and `apm.lock.yaml` as an **install
root**, and a plugin package is not one. `scripts/apm-audit-ci.sh` therefore waives `lockfile-exists`
and nothing else, and only for a non-root manifest: it asserts the string `1 of 1 check(s) failed`,
so any second failing check changes the count and the run fails normally, and output it does not
recognise fails closed. The root manifest is never waived. Recorded as ADR-0026.
**Dropping `--ci` for package directories was considered and rejected.** It is the smaller change and
it is wrong. Verified on apm 0.28.0 against a scratch package whose dependency entry carried no
`git`/`path`/`registry` field: `apm audit --ci` exits 1 naming the field, while plain `apm audit`
prints `No apm.lock.yaml found -- nothing to scan` and exits 0. Malformed-dependency detection is the
reason this section gives for auditing packages at all, and a package *with* dependencies is the only
kind that can carry a malformed dependency entry — so dropping `--ci` would discard the check
precisely where it earns its keep.
**Known weak point: the waiver matches on apm's stdout.** An apm upgrade that rewords either line
turns the waiver off. That fails the push rather than hiding a defect; re-verify against the new
output and update the patterns rather than widening them.
**`manifest-parse` is not a named check** in apm 0.28.0's output, and an earlier revision of this
section listed it as one. Parsing is still enforced — a dependency entry missing its
@@ -1108,7 +1139,9 @@ or hash drift detected` — so the root invocation already covers it and nothing
remains true is that the *standalone* mode is different: plain `apm audit` (`--ci` refuses to combine
with `--file`/`--strip`/`--dry-run`/`PACKAGE`) run in a plugin directory reports
`No apm.lock.yaml found -- nothing to scan` and exits 0, because only the root has a lockfile.
Plugin manifests get `lockfile-exists` and nothing else; they are not Unicode-scanned.
Plugin manifests get `lockfile-exists` and nothing else; they are not Unicode-scanned. That holds
because no package carries an `apm.lock.yaml` — one would arm the other nine checks, `content-integrity`
among them, which is the state ADR-0026 rules out rather than a second scan worth having.
### `check-executables-allow-sync`