Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/apm_cli/commands/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ def _audit_ci_gate(
fail_fast = not no_fail_fast

# Always run baseline checks
ci_result = run_baseline_checks(cfg.project_root, fail_fast=fail_fast)
ci_result = run_baseline_checks(cfg.project_root, fail_fast=fail_fast, ci_mode=True)

# Resolve policy source: explicit --policy wins; otherwise mirror
# install's auto-discovery (closes #827) so CI catches sideloaded
Expand Down
24 changes: 22 additions & 2 deletions src/apm_cli/policy/ci_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Sequence # noqa: F401, UP035

from ..deps.lockfile import _SELF_KEY
from ..deps.lockfile import _SELF_KEY, LEGACY_LOCKFILE_NAME, LOCKFILE_NAME
from .models import CheckResult, CIAuditResult

if TYPE_CHECKING:
Expand Down Expand Up @@ -503,11 +503,14 @@ def run_baseline_checks(
project_root: Path,
*,
fail_fast: bool = True,
ci_mode: bool = False,
) -> CIAuditResult:
"""Run all baseline CI checks against a project directory.

When *fail_fast* is ``True`` (default), stops after the first
failing check to skip expensive I/O (e.g. content integrity scan).
When *ci_mode* is ``True``, the ``manifest-missing`` check is a hard
failure (``passed=False``); otherwise it is an advisory warning only.
Returns :class:`CIAuditResult` with individual check results.
"""
from ..deps.lockfile import LockFile, get_lockfile_path
Expand Down Expand Up @@ -545,8 +548,25 @@ def run_baseline_checks(
lockfile_path = get_lockfile_path(project_root)

# If there's no apm.yml or no lockfile, the first check already passed
# (no deps needed). Skip remaining checks.
# (no deps needed). Skip remaining checks -- but warn if APM artifacts
# exist without a manifest (evidence of a deleted apm.yml).
if not apm_yml_path.exists() or not lockfile_path.exists():
if not apm_yml_path.exists():
apm_dir = project_root / ".apm"
lock_file = project_root / LOCKFILE_NAME
legacy_lock_file = project_root / LEGACY_LOCKFILE_NAME
if apm_dir.is_dir() or lock_file.exists() or legacy_lock_file.exists():
result.checks.append(
Comment thread
sergio-sisternes-epam marked this conversation as resolved.
CheckResult(
name="manifest-missing",
passed=not ci_mode,
message=(
"apm.yml is missing but APM artifacts"
" (.apm/ or apm.lock.yaml or apm.lock) were found"
" -- this may indicate a deleted manifest"
),
)
)
return result

lock = LockFile.read(lockfile_path)
Expand Down
1 change: 1 addition & 0 deletions src/apm_cli/policy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"scripts-policy": "apm.yml",
"unmanaged-files": "apm.yml",
"manifest-parse": "apm.yml",
"manifest-missing": "apm.yml",
}


Expand Down
77 changes: 77 additions & 0 deletions tests/unit/policy/test_ci_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1119,3 +1119,80 @@ def test_remediation_hint_present_in_error_message(self, tmp_path: Path) -> None
assert parse_check.name == "manifest-parse"
assert "Cannot parse apm.yml" in parse_check.message
assert "fix the YAML syntax error in apm.yml and re-run" in parse_check.message


class TestManifestMissingWarning:
"""Tests for the manifest-missing warning when apm.yml is absent."""

def test_no_artifacts_no_warning(self, tmp_path: Path) -> None:
"""Clean non-APM project: no apm.yml, no .apm/, no lockfile -> no warning."""
result = run_baseline_checks(tmp_path)
names = [c.name for c in result.checks]
assert "manifest-missing" not in names
assert result.passed

def test_apm_dir_triggers_warning(self, tmp_path: Path) -> None:
"""apm.yml absent but .apm/ dir exists -> manifest-missing warning."""
(tmp_path / ".apm").mkdir()
result = run_baseline_checks(tmp_path)
names = [c.name for c in result.checks]
assert "manifest-missing" in names
check = next(c for c in result.checks if c.name == "manifest-missing")
assert check.passed is True
assert ".apm/" in check.message or "apm.lock.yaml" in check.message

def test_lockfile_triggers_warning(self, tmp_path: Path) -> None:
"""apm.yml absent but apm.lock.yaml exists -> manifest-missing warning."""
(tmp_path / "apm.lock.yaml").write_text("packages: []\n", encoding="utf-8")
result = run_baseline_checks(tmp_path)
names = [c.name for c in result.checks]
assert "manifest-missing" in names
check = next(c for c in result.checks if c.name == "manifest-missing")
assert check.passed is True

def test_manifest_present_no_warning(self, tmp_path: Path) -> None:
"""apm.yml present -> no manifest-missing check at all."""
_write_apm_yml(tmp_path)
result = run_baseline_checks(tmp_path)
names = [c.name for c in result.checks]
assert "manifest-missing" not in names

def test_apm_dir_ci_mode_fails(self, tmp_path: Path) -> None:
"""In CI mode, .apm/ without apm.yml fails the check."""
(tmp_path / ".apm").mkdir()
result = run_baseline_checks(tmp_path, ci_mode=True)
check = next(c for c in result.checks if c.name == "manifest-missing")
assert check.passed is False
assert not result.passed

def test_lockfile_ci_mode_fails(self, tmp_path: Path) -> None:
"""In CI mode, apm.lock.yaml without apm.yml fails the check."""
(tmp_path / "apm.lock.yaml").write_text("packages: []\n", encoding="utf-8")
result = run_baseline_checks(tmp_path, ci_mode=True)
check = next(c for c in result.checks if c.name == "manifest-missing")
assert check.passed is False
assert not result.passed

def test_legacy_lockfile_triggers_warning(self, tmp_path: Path) -> None:
"""apm.yml absent but legacy apm.lock exists -> manifest-missing warning."""
(tmp_path / "apm.lock").write_text("packages: []\n", encoding="utf-8")
result = run_baseline_checks(tmp_path)
names = [c.name for c in result.checks]
assert "manifest-missing" in names
check = next(c for c in result.checks if c.name == "manifest-missing")
assert check.passed is True

def test_legacy_lockfile_ci_mode_fails(self, tmp_path: Path) -> None:
"""In CI mode, legacy apm.lock without apm.yml fails the check."""
(tmp_path / "apm.lock").write_text("packages: []\n", encoding="utf-8")
result = run_baseline_checks(tmp_path, ci_mode=True)
check = next(c for c in result.checks if c.name == "manifest-missing")
assert check.passed is False
assert not result.passed

def test_no_artifacts_ci_mode_still_passes(self, tmp_path: Path) -> None:
"""Clean project with no APM artifacts passes even in CI mode."""
result = run_baseline_checks(tmp_path, ci_mode=True)
names = [c.name for c in result.checks]
assert "manifest-missing" not in names
assert result.passed
Loading