#!/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"