Skip to content
Merged
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
97 changes: 97 additions & 0 deletions packages/core/src/services/readiness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,yaml}"]
};
}
},
// ── Area-scoped criteria (only run when areaPath is set) ──
{
id: "area-readme",
Expand Down Expand Up @@ -1379,3 +1444,35 @@ async function readAllDependencies(context: ReadinessContext): Promise<string[]>

return Array.from(new Set(dependencies));
}

// ── APM (Agent Package Manager) helpers ──

async function hasApmConfig(repoPath: string): Promise<boolean> {
return fileExists(path.join(repoPath, "apm.yml"));
}

async function hasApmLockfile(repoPath: string): Promise<boolean> {
return fileExists(path.join(repoPath, "apm.lock.yaml"));
}

async function hasApmInWorkflows(repoPath: string): Promise<boolean> {
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;
}
3 changes: 3 additions & 0 deletions src/services/__tests__/readiness-baseline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
122 changes: 122 additions & 0 deletions src/services/__tests__/readiness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
Loading