Files
holocron/scripts/apm-audit-ci.sh
Defame1297 fbd030c7ea 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
2026-09-20 20:20:50 +00:00

90 lines
3.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# apm-audit-ci pre-push hook.
#
# Runs `apm audit --ci` once per manifest -- the root one and each plugin
# package -- because the root-only invocation audits the marketplace manifest
# and nothing else, and `apm pack --check-clean` does not parse plugin
# `dependencies:` blocks either. Full rationale: docs/spec/gates.md,
# "apm-audit-ci".
#
# WHY THIS IS A SCRIPT AND NOT THE ONE-LINE `for` LOOP IT REPLACED
#
# apm treats any directory holding both apm.yml and apm.lock.yaml as an INSTALL
# ROOT. A plugin package is not one: it is content to be installed elsewhere.
# While every plugin declared `dependencies: {apm: [], mcp: []}` the distinction
# never surfaced, because the plugin-level `lockfile-exists` check reported
# `No dependencies declared -- lockfile not required` and passed vacuously.
#
# plugins/onedev is the first package to declare a real dependency (it pins
# OneDev's TOD skills so the marketplace can redistribute them), which arms that
# check and leaves no green state:
#
# * no apm.lock.yaml in the package -> `lockfile-exists` fails with
# "apm.yml declares dependencies but apm.lock.yaml is absent"
# * an apm.lock.yaml in the package -> `lockfile-exists` passes and thereby
# arms the other nine checks, and `drift` then fails demanding the
# dependency's skills be DEPLOYED inside the package
# (plugins/onedev/.agents/skills/...), which is meaningless for a package
# and additionally litters it with an apm_modules/ tree
#
# So this hook waives exactly one failure: a plugin package whose ONLY failing
# check is `lockfile-exists`. Verified against apm 0.28.0.
#
# WHAT IS DELIBERATELY NOT WAIVED
#
# Dropping `--ci` in package directories would have been 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 the
# field, while plain `apm audit` prints "No apm.lock.yaml found -- nothing to
# scan" and exits 0. 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 -- so the check would have been
# discarded precisely where it earns its keep.
#
# The waiver is therefore narrow on three axes, and fails closed on each:
# 1. the root manifest is never waived, whatever it reports
# 2. the failing check must be `lockfile-exists` and no other -- the
# "1 of 1 check(s) failed" assertion is what makes that true, since any
# second failing check changes the count and the run fails normally
# 3. output apm does not produce in the recognised shape is a failure
#
# Matching on apm's stdout is the weak point: an apm upgrade that rewords either
# line silently turns the waiver off, which fails the push rather than hiding a
# defect. If that happens, re-verify against the new output and update the two
# patterns below rather than widening them.
set -uo pipefail
readonly WAIVED_CHECK='declares dependencies but apm.lock.yaml is absent'
readonly SOLE_FAILURE='1 of 1 check(s) failed'
status=0
for manifest_dir in . plugins/*/; do
output="$(cd "$manifest_dir" && apm audit --ci 2>&1)"
exit_code=$?
if [ "$exit_code" -eq 0 ]; then
continue
fi
# Axis 1: the root is never waived.
# Here-strings, not `printf ... | grep -q`: under `set -o pipefail` grep -q
# exits on its first match, SIGPIPEs the writer, and the writer's death
# becomes the pipeline's status -- a race tests/test-no-pipefail-early-exit-grep.sh
# scans every tracked script for.
if [ "$manifest_dir" != "." ] &&
grep -qF "$WAIVED_CHECK" <<<"$output" &&
grep -qF "$SOLE_FAILURE" <<<"$output"; then
printf 'apm audit --ci: waived lockfile-exists in %s (package, not an install root)\n' \
"$manifest_dir"
continue
fi
printf '%s\n' "$output" >&2
printf 'apm audit --ci failed in %s\n' "$manifest_dir" >&2
status=1
done
exit "$status"