From 1289d99f20863627085db3b0c4474cfd35838d02 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Tue, 31 Mar 2026 14:25:07 +0200 Subject: [PATCH 1/2] feat(readiness): add APM awareness to ai-tooling pillar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three new readiness criteria to the ai-tooling pillar that detect APM (Agent Package Manager) usage in repositories: - apm-config (level 2): detects apm.yml manifest presence - apm-locked-deps (level 3): detects apm.lock.yaml (skipped if no config) - apm-ci-integration (level 4): scans CI workflows for microsoft/apm-action or apm audit/install commands Criteria are ordered by level within the pillar (L2 → L3 → L4). Implementation lives in the monolithic readiness.ts (the active source of truth used by the build and tests). Closes #91 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/core/src/services/readiness.ts | 97 ++++++++++++++ .../__tests__/readiness-baseline.test.ts | 3 + src/services/__tests__/readiness.test.ts | 122 ++++++++++++++++++ 3 files changed, 222 insertions(+) diff --git a/packages/core/src/services/readiness.ts b/packages/core/src/services/readiness.ts index 2932028..14253a5 100644 --- a/packages/core/src/services/readiness.ts +++ b/packages/core/src/services/readiness.ts @@ -801,6 +801,25 @@ export function buildCriteria(): ReadinessCriterion[] { }; } }, + { + id: "apm-config", + title: "APM package manifest present", + pillar: "ai-tooling", + level: 2, + scope: "repo", + impact: "medium", + effort: "low", + check: async (context) => { + const found = await hasApmConfig(context.repoPath); + return { + status: found ? "pass" : "fail", + reason: found + ? undefined + : "No apm.yml found. Use APM to install shared agent packages and keep instructions in sync across repos. See: https://github.com/microsoft/apm", + evidence: ["apm.yml"] + }; + } + }, { id: "custom-agents", title: "Custom AI agents configured", @@ -839,6 +858,52 @@ export function buildCriteria(): ReadinessCriterion[] { }; } }, + { + id: "apm-locked-deps", + title: "APM dependencies locked", + pillar: "ai-tooling", + level: 3, + scope: "repo", + impact: "medium", + effort: "low", + check: async (context) => { + const hasConfig = await hasApmConfig(context.repoPath); + if (!hasConfig) { + return { status: "skip", reason: "No apm.yml found — skipping lockfile check." }; + } + const found = await hasApmLockfile(context.repoPath); + return { + status: found ? "pass" : "fail", + reason: found + ? undefined + : "apm.yml found but dependencies are not locked. Run `apm install` to generate apm.lock.yaml.", + evidence: ["apm.lock.yaml"] + }; + } + }, + { + id: "apm-ci-integration", + title: "APM integrated in CI pipeline", + pillar: "ai-tooling", + level: 4, + scope: "repo", + impact: "high", + effort: "medium", + check: async (context) => { + const hasConfig = await hasApmConfig(context.repoPath); + if (!hasConfig) { + return { status: "skip", reason: "No apm.yml found — skipping CI check." }; + } + const found = await hasApmInWorkflows(context.repoPath); + return { + status: found ? "pass" : "fail", + reason: found + ? undefined + : "No APM step found in CI. Add `microsoft/apm-action` to your workflow to audit agent packages on every PR. See: https://github.com/microsoft/apm-action", + evidence: [".github/workflows/*.yml"] + }; + } + }, // ── Area-scoped criteria (only run when areaPath is set) ── { id: "area-readme", @@ -1379,3 +1444,35 @@ async function readAllDependencies(context: ReadinessContext): Promise return Array.from(new Set(dependencies)); } + +// ── APM (Agent Package Manager) helpers ── + +async function hasApmConfig(repoPath: string): Promise { + return fileExists(path.join(repoPath, "apm.yml")); +} + +async function hasApmLockfile(repoPath: string): Promise { + return fileExists(path.join(repoPath, "apm.lock.yaml")); +} + +async function hasApmInWorkflows(repoPath: string): Promise { + const workflowDir = path.join(repoPath, ".github", "workflows"); + let files: string[]; + try { + files = await fs.readdir(workflowDir); + } catch { + return false; + } + for (const file of files) { + if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue; + try { + const content = await fs.readFile(path.join(workflowDir, file), "utf8"); + if (/\bmicrosoft\/apm-action\b/.test(content) || /\bapm\s+(audit|install)\b/.test(content)) { + return true; + } + } catch { + // skip unreadable files + } + } + return false; +} diff --git a/src/services/__tests__/readiness-baseline.test.ts b/src/services/__tests__/readiness-baseline.test.ts index abee593..8a1e5aa 100644 --- a/src/services/__tests__/readiness-baseline.test.ts +++ b/src/services/__tests__/readiness-baseline.test.ts @@ -171,6 +171,9 @@ describe("buildCriteria baseline", () => { const criteria = buildCriteria(); const ids = criteria.map((c) => c.id).sort(); expect(ids).toEqual([ + "apm-ci-integration", + "apm-config", + "apm-locked-deps", "area-build-script", "area-instructions", "area-readme", diff --git a/src/services/__tests__/readiness.test.ts b/src/services/__tests__/readiness.test.ts index 43a84c3..d558634 100644 --- a/src/services/__tests__/readiness.test.ts +++ b/src/services/__tests__/readiness.test.ts @@ -765,4 +765,126 @@ describe("runReadinessReport", () => { ); }); }); + + describe("ai-tooling pillar — APM criteria", () => { + it("fails apm-config when apm.yml does not exist", async () => { + await writePackageJson({ name: "test-repo" }); + + const report = await runReadinessReport({ repoPath }); + const criterion = report.criteria.find((c) => c.id === "apm-config"); + + expect(criterion).toBeDefined(); + expect(criterion?.status).toBe("fail"); + expect(criterion?.reason).toContain("No apm.yml found"); + expect(criterion?.pillar).toBe("ai-tooling"); + }); + + it("passes apm-config when apm.yml exists", async () => { + await writePackageJson({ name: "test-repo" }); + await writeFile("apm.yml", "name: test-package\nversion: 1.0.0"); + + const report = await runReadinessReport({ repoPath }); + const criterion = report.criteria.find((c) => c.id === "apm-config"); + + expect(criterion?.status).toBe("pass"); + }); + + it("skips apm-locked-deps when apm.yml does not exist", async () => { + await writePackageJson({ name: "test-repo" }); + + const report = await runReadinessReport({ repoPath }); + const criterion = report.criteria.find((c) => c.id === "apm-locked-deps"); + + expect(criterion?.status).toBe("skip"); + expect(criterion?.reason).toContain("No apm.yml found"); + }); + + it("fails apm-locked-deps when apm.yml exists but no lockfile", async () => { + await writePackageJson({ name: "test-repo" }); + await writeFile("apm.yml", "name: test-package\nversion: 1.0.0"); + + const report = await runReadinessReport({ repoPath }); + const criterion = report.criteria.find((c) => c.id === "apm-locked-deps"); + + expect(criterion?.status).toBe("fail"); + expect(criterion?.reason).toContain("dependencies are not locked"); + }); + + it("passes apm-locked-deps when both apm.yml and apm.lock.yaml exist", async () => { + await writePackageJson({ name: "test-repo" }); + await writeFile("apm.yml", "name: test-package\nversion: 1.0.0"); + await writeFile("apm.lock.yaml", "dependencies: {}"); + + const report = await runReadinessReport({ repoPath }); + const criterion = report.criteria.find((c) => c.id === "apm-locked-deps"); + + expect(criterion?.status).toBe("pass"); + }); + + it("skips apm-ci-integration when apm.yml does not exist", async () => { + await writePackageJson({ name: "test-repo" }); + + const report = await runReadinessReport({ repoPath }); + const criterion = report.criteria.find((c) => c.id === "apm-ci-integration"); + + expect(criterion?.status).toBe("skip"); + }); + + it("fails apm-ci-integration when apm.yml exists but no workflow uses apm", async () => { + await writePackageJson({ name: "test-repo" }); + await writeFile("apm.yml", "name: test-package\nversion: 1.0.0"); + await writeFile( + ".github/workflows/ci.yml", + "name: CI\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: npm install" + ); + + const report = await runReadinessReport({ repoPath }); + const criterion = report.criteria.find((c) => c.id === "apm-ci-integration"); + + expect(criterion?.status).toBe("fail"); + expect(criterion?.reason).toContain("No APM step found in CI"); + }); + + it("passes apm-ci-integration when apm install is in a workflow", async () => { + await writePackageJson({ name: "test-repo" }); + await writeFile("apm.yml", "name: test-package\nversion: 1.0.0"); + await writeFile( + ".github/workflows/ci.yml", + "name: CI\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: apm install" + ); + + const report = await runReadinessReport({ repoPath }); + const criterion = report.criteria.find((c) => c.id === "apm-ci-integration"); + + expect(criterion?.status).toBe("pass"); + }); + + it("passes apm-ci-integration when apm audit is in a workflow", async () => { + await writePackageJson({ name: "test-repo" }); + await writeFile("apm.yml", "name: test-package\nversion: 1.0.0"); + await writeFile( + ".github/workflows/security.yml", + "name: Security\njobs:\n audit:\n runs-on: ubuntu-latest\n steps:\n - run: apm audit --ci" + ); + + const report = await runReadinessReport({ repoPath }); + const criterion = report.criteria.find((c) => c.id === "apm-ci-integration"); + + expect(criterion?.status).toBe("pass"); + }); + + it("passes apm-ci-integration when microsoft/apm-action is used", async () => { + await writePackageJson({ name: "test-repo" }); + await writeFile("apm.yml", "name: test-package\nversion: 1.0.0"); + await writeFile( + ".github/workflows/apm.yml", + "name: APM\njobs:\n apm:\n runs-on: ubuntu-latest\n steps:\n - uses: microsoft/apm-action@v1" + ); + + const report = await runReadinessReport({ repoPath }); + const criterion = report.criteria.find((c) => c.id === "apm-ci-integration"); + + expect(criterion?.status).toBe("pass"); + }); + }); }); From 92819f460e9a1017cd998ee281d75e8b189e2901 Mon Sep 17 00:00:00 2001 From: Daniel Meppiel <51440732+danielmeppiel@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:30:02 +0200 Subject: [PATCH 2/2] Update packages/core/src/services/readiness.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/core/src/services/readiness.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/services/readiness.ts b/packages/core/src/services/readiness.ts index 14253a5..e056a4a 100644 --- a/packages/core/src/services/readiness.ts +++ b/packages/core/src/services/readiness.ts @@ -900,7 +900,7 @@ export function buildCriteria(): ReadinessCriterion[] { reason: found ? undefined : "No APM step found in CI. Add `microsoft/apm-action` to your workflow to audit agent packages on every PR. See: https://github.com/microsoft/apm-action", - evidence: [".github/workflows/*.yml"] + evidence: [".github/workflows/*.{yml,yaml}"] }; } },