From 370edc7ad32c764d57e28806f7c1ef25d36ca005 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:43:55 +0200 Subject: [PATCH 01/13] feat(workflows): add central pkgviz audit policy --- src/policy/resolvePkgvizAuditPolicyAsync.ts | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/policy/resolvePkgvizAuditPolicyAsync.ts diff --git a/src/policy/resolvePkgvizAuditPolicyAsync.ts b/src/policy/resolvePkgvizAuditPolicyAsync.ts new file mode 100644 index 00000000..fb4c1d35 --- /dev/null +++ b/src/policy/resolvePkgvizAuditPolicyAsync.ts @@ -0,0 +1,37 @@ +import { stat } from 'node:fs/promises'; +import { join } from 'node:path'; + +/*** Resolve the centrally pinned PKGViz CI audit for repositories with analyzable source. */ +export async function resolvePkgvizAuditPolicyAsync( + targetDirectory: string, +): Promise { + if (!(await hasSourceDirectoryAsync(targetDirectory))) return undefined; + + return { + artifactName: 'pkgviz-audit', + artifactPath: 'pkgviz-audit.json', + command: + 'bunx pkgviz@0.8.1 --out pkgviz-audit.json --rule cyclic-dependencies=block', + }; +} + +interface PkgvizAuditPolicy { + readonly artifactName: string; + readonly artifactPath: string; + readonly command: string; +} + +/*** Detect whether the managed repository has a source tree that PKGViz can inspect. */ +async function hasSourceDirectoryAsync(targetDirectory: string): Promise { + try { + return (await stat(join(targetDirectory, 'src'))).isDirectory(); + } catch (error) { + if (isNodeError(error) && error.code === 'ENOENT') return false; + throw error; + } +} + +/*** Narrow filesystem failures to Node errors with stable error codes. */ +function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && 'code' in error; +} From cf155500bc744002589906bfd805bcb43f079ade Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:44:11 +0200 Subject: [PATCH 02/13] feat(workflows): render pkgviz audit steps --- src/tools/workflows/renderWorkflowAsync.ts | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/tools/workflows/renderWorkflowAsync.ts b/src/tools/workflows/renderWorkflowAsync.ts index 2b1a71f0..230d3a16 100644 --- a/src/tools/workflows/renderWorkflowAsync.ts +++ b/src/tools/workflows/renderWorkflowAsync.ts @@ -7,6 +7,11 @@ export interface WorkflowPolicy { readonly doctorVersion: string; readonly nodeVersion: string; readonly apmReleaseCommand?: string; + readonly pkgvizAudit?: { + readonly artifactName: string; + readonly artifactPath: string; + readonly command: string; + }; readonly structureReleaseCommand?: string; } @@ -15,6 +20,7 @@ export async function renderWorkflowAsync(sourceUrl: URL, policy: WorkflowPolicy const template = await readFile(sourceUrl, 'utf8'); return template .replaceAll(BUN_VERSION_TOKEN, policy.bunVersion) + .replaceAll(PKGVIZ_AUDIT_STEPS_TOKEN, renderPkgvizAuditSteps(policy)) .replaceAll(CHANGESETS_PUBLISH_COMMAND_TOKEN, changesetsPolicy.workflowCommands.publish) .replaceAll(CHANGESETS_STATUS_COMMAND_TOKEN, changesetsPolicy.workflowCommands.status) .replaceAll(CHANGESETS_VERSION_COMMAND_TOKEN, changesetsPolicy.workflowCommands.version) @@ -36,3 +42,22 @@ const CHANGESETS_STATUS_COMMAND_TOKEN = '__ANKH_CHANGESETS_STATUS_COMMAND__'; const CHANGESETS_VERSION_COMMAND_TOKEN = '__ANKH_CHANGESETS_VERSION_COMMAND__'; const DOCTOR_VERSION_TOKEN = '__ANKH_DOCTOR_VERSION__'; const NODE_VERSION_TOKEN = '__ANKH_NODE_VERSION__'; + + +/*** Render the centrally managed PKGViz audit steps only for applicable repositories. */ +function renderPkgvizAuditSteps(policy: WorkflowPolicy): string { + if (policy.pkgvizAudit === undefined) return ''; + + return ` - name: Enforce PKGViz cyclic-dependencies rule + run: ${policy.pkgvizAudit.command} + + - name: Upload PKGViz audit + if: always() && hashFiles('${policy.pkgvizAudit.artifactPath}') != '' + uses: actions/upload-artifact@v4 + with: + name: ${policy.pkgvizAudit.artifactName} + path: ${policy.pkgvizAudit.artifactPath} +`; +} + +const PKGVIZ_AUDIT_STEPS_TOKEN = '__ANKH_PKGVIZ_AUDIT_STEPS__'; From 7060c2fcad08f96a3c9b075e7211c8752d9649fa Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:44:14 +0200 Subject: [PATCH 03/13] feat(workflows): add managed pkgviz audit slot --- src/tools/workflows/files/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tools/workflows/files/ci.yml b/src/tools/workflows/files/ci.yml index 26b43611..b6b12f18 100644 --- a/src/tools/workflows/files/ci.yml +++ b/src/tools/workflows/files/ci.yml @@ -99,3 +99,5 @@ jobs: else echo "No changeset:status script found; skipping." fi + +__ANKH_PKGVIZ_AUDIT_STEPS__ From 5dd60707db43aa5ae611116c1323713e8bb62a2a Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:44:25 +0200 Subject: [PATCH 04/13] feat(workflows): apply central pkgviz audit policy --- src/tools/workflows/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tools/workflows/index.ts b/src/tools/workflows/index.ts index 9ae0b1b1..88f572f4 100644 --- a/src/tools/workflows/index.ts +++ b/src/tools/workflows/index.ts @@ -4,6 +4,7 @@ import { join } from 'node:path'; import { resolveApmReleaseCommandAsync } from '../../features/apm-release-validation/adapters/outbound/resolveApmReleaseCommandAsync.js'; import { resolveStructureReleaseCommandAsync } from '../../features/structure-descriptor-generation/adapters/outbound/resolveStructureReleaseCommandAsync.js'; import { bunRuntimePolicy, nodeRuntimePolicy } from '../../policy/bunRuntimePolicy.js'; +import { resolvePkgvizAuditPolicyAsync } from '../../policy/resolvePkgvizAuditPolicyAsync.js'; import type { ManagedFileDefinition } from '../shared/managedFiles.js'; import { readCurrentDoctorVersion } from './readCurrentDoctorVersion.js'; import { renderRenovateWorkflowAsync } from './renderRenovateWorkflowAsync.js'; @@ -43,6 +44,7 @@ function createWorkflowDefinition(relativePath: string, sourcePath: string): Man bunVersion: bunRuntimePolicy.version, doctorVersion: readCurrentDoctorVersion(), nodeVersion: nodeRuntimePolicy.setupVersion, + pkgvizAudit: await resolvePkgvizAuditPolicyAsync(targetDirectory), }), }; } From 456bd3ef72c7f5fba8a030c2ccba867d0e92fde1 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:44:49 +0200 Subject: [PATCH 05/13] test(workflows): cover pkgviz audit applicability --- .../resolvePkgvizAuditPolicyAsync.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/policy/resolvePkgvizAuditPolicyAsync.test.ts diff --git a/src/policy/resolvePkgvizAuditPolicyAsync.test.ts b/src/policy/resolvePkgvizAuditPolicyAsync.test.ts new file mode 100644 index 00000000..1cf521a8 --- /dev/null +++ b/src/policy/resolvePkgvizAuditPolicyAsync.test.ts @@ -0,0 +1,40 @@ +import { mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { afterEach, describe, expect, test } from 'bun:test'; + +import { resolvePkgvizAuditPolicyAsync } from './resolvePkgvizAuditPolicyAsync.js'; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { force: true, recursive: true })), + ); +}); + +describe('PKGViz audit policy', () => { + test('pins the blocking cycle audit for repositories with source', async () => { + const target = await createTarget(); + await mkdir(join(target, 'src')); + + await expect(resolvePkgvizAuditPolicyAsync(target)).resolves.toEqual({ + artifactName: 'pkgviz-audit', + artifactPath: 'pkgviz-audit.json', + command: 'bunx pkgviz@0.8.1 --out pkgviz-audit.json --rule cyclic-dependencies=block', + }); + }); + + test('does not enable PKGViz for repositories without a source tree', async () => { + const target = await createTarget(); + + await expect(resolvePkgvizAuditPolicyAsync(target)).resolves.toBeUndefined(); + }); +}); + +/*** Create an isolated managed-repository fixture. */ +async function createTarget(): Promise { + const target = await mkdtemp('/tmp/devtools-pkgviz-audit-'); + temporaryDirectories.push(target); + return target; +} From 108b73b824e87517f6c0ad13ff931eb262c358d6 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:44:53 +0200 Subject: [PATCH 06/13] test(workflows): cover managed pkgviz audit rendering --- src/tools/workflows/index.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/tools/workflows/index.test.ts b/src/tools/workflows/index.test.ts index f459d23b..742ab600 100644 --- a/src/tools/workflows/index.test.ts +++ b/src/tools/workflows/index.test.ts @@ -148,6 +148,31 @@ test('managed release synchronizes main before build and recovers a matching his expect(release).toContain('echo "versioned=false" >> "$GITHUB_OUTPUT"'); }); +describe('managed PKGViz audit', () => { + test('renders the pinned blocking audit and diagnostic artifact for source repositories', async () => { + const target = await createWorkflowTarget(); + await mkdir(join(target, 'src')); + + const ci = await workflowManagedFiles[0].render?.(target); + + expect(ci).toContain( + 'bunx pkgviz@0.8.1 --out pkgviz-audit.json --rule cyclic-dependencies=block', + ); + expect(ci).toContain('if: always() && hashFiles(\'pkgviz-audit.json\') != \'\''); + expect(ci).toContain('name: pkgviz-audit'); + expect(ci).toContain('path: pkgviz-audit.json'); + }); + + test('omits the audit from repositories without analyzable source', async () => { + const target = await createWorkflowTarget(); + + const ci = await workflowManagedFiles[0].render?.(target); + + expect(ci).not.toContain('Enforce PKGViz cyclic-dependencies rule'); + expect(ci).not.toContain('pkgviz-audit.json'); + }); +}); + describe('managed CI Changesets contract', () => { test('keeps the missing-Changeset guard strict for every ordinary pull request', async () => { const ci = await workflowManagedFiles[0].render?.('.'); From 52aa7c6dd5d276e52d5dcd66209e7fd9561c73a9 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:45:06 +0200 Subject: [PATCH 07/13] chore(changeset): document managed pkgviz audit --- .changeset/bright-cycles-sync.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bright-cycles-sync.md diff --git a/.changeset/bright-cycles-sync.md b/.changeset/bright-cycles-sync.md new file mode 100644 index 00000000..908cbda4 --- /dev/null +++ b/.changeset/bright-cycles-sync.md @@ -0,0 +1,5 @@ +--- +'@ankhorage/devtools': minor +--- + +Add a centrally managed PKGViz cyclic-dependency CI audit for repositories with source trees, including a pinned tool invocation and diagnostic audit artifact upload. From 9de2c32269bba3607baa54e092a679f6ee169556 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:46:36 +0200 Subject: [PATCH 08/13] test(workflows): satisfy canonical async assertions --- src/policy/resolvePkgvizAuditPolicyAsync.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/policy/resolvePkgvizAuditPolicyAsync.test.ts b/src/policy/resolvePkgvizAuditPolicyAsync.test.ts index 1cf521a8..eff8dd6b 100644 --- a/src/policy/resolvePkgvizAuditPolicyAsync.test.ts +++ b/src/policy/resolvePkgvizAuditPolicyAsync.test.ts @@ -18,7 +18,9 @@ describe('PKGViz audit policy', () => { const target = await createTarget(); await mkdir(join(target, 'src')); - await expect(resolvePkgvizAuditPolicyAsync(target)).resolves.toEqual({ + const policy = await resolvePkgvizAuditPolicyAsync(target); + + expect(policy).toEqual({ artifactName: 'pkgviz-audit', artifactPath: 'pkgviz-audit.json', command: 'bunx pkgviz@0.8.1 --out pkgviz-audit.json --rule cyclic-dependencies=block', @@ -28,7 +30,9 @@ describe('PKGViz audit policy', () => { test('does not enable PKGViz for repositories without a source tree', async () => { const target = await createTarget(); - await expect(resolvePkgvizAuditPolicyAsync(target)).resolves.toBeUndefined(); + const policy = await resolvePkgvizAuditPolicyAsync(target); + + expect(policy).toBeUndefined(); }); }); From b180226e2dc39b1678fa46b329c272f749027f79 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:46:38 +0200 Subject: [PATCH 09/13] style(workflows): format pkgviz audit policy --- src/policy/resolvePkgvizAuditPolicyAsync.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/policy/resolvePkgvizAuditPolicyAsync.ts b/src/policy/resolvePkgvizAuditPolicyAsync.ts index fb4c1d35..2bd9b4f7 100644 --- a/src/policy/resolvePkgvizAuditPolicyAsync.ts +++ b/src/policy/resolvePkgvizAuditPolicyAsync.ts @@ -10,8 +10,7 @@ export async function resolvePkgvizAuditPolicyAsync( return { artifactName: 'pkgviz-audit', artifactPath: 'pkgviz-audit.json', - command: - 'bunx pkgviz@0.8.1 --out pkgviz-audit.json --rule cyclic-dependencies=block', + command: 'bunx pkgviz@0.8.1 --out pkgviz-audit.json --rule cyclic-dependencies=block', }; } From 15280459ec29cfb9921b988bbd384764b5bf0d47 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:46:42 +0200 Subject: [PATCH 10/13] test(workflows): format pkgviz artifact assertion --- src/tools/workflows/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/workflows/index.test.ts b/src/tools/workflows/index.test.ts index 742ab600..715604b7 100644 --- a/src/tools/workflows/index.test.ts +++ b/src/tools/workflows/index.test.ts @@ -158,7 +158,7 @@ describe('managed PKGViz audit', () => { expect(ci).toContain( 'bunx pkgviz@0.8.1 --out pkgviz-audit.json --rule cyclic-dependencies=block', ); - expect(ci).toContain('if: always() && hashFiles(\'pkgviz-audit.json\') != \'\''); + expect(ci).toContain("if: always() && hashFiles('pkgviz-audit.json') != ''"); expect(ci).toContain('name: pkgviz-audit'); expect(ci).toContain('path: pkgviz-audit.json'); }); From 7449161639118d3ae72ca220f23b938e1615ee42 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:46:45 +0200 Subject: [PATCH 11/13] style(workflows): format pkgviz audit renderer --- src/tools/workflows/renderWorkflowAsync.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/workflows/renderWorkflowAsync.ts b/src/tools/workflows/renderWorkflowAsync.ts index 230d3a16..2969e79b 100644 --- a/src/tools/workflows/renderWorkflowAsync.ts +++ b/src/tools/workflows/renderWorkflowAsync.ts @@ -43,7 +43,6 @@ const CHANGESETS_VERSION_COMMAND_TOKEN = '__ANKH_CHANGESETS_VERSION_COMMAND__'; const DOCTOR_VERSION_TOKEN = '__ANKH_DOCTOR_VERSION__'; const NODE_VERSION_TOKEN = '__ANKH_NODE_VERSION__'; - /*** Render the centrally managed PKGViz audit steps only for applicable repositories. */ function renderPkgvizAuditSteps(policy: WorkflowPolicy): string { if (policy.pkgvizAudit === undefined) return ''; From 14e0fb079359f2a5d5713e0be2aa77a381ff57cc Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:47:40 +0200 Subject: [PATCH 12/13] fix(workflows): keep managed CI template valid yaml --- src/tools/workflows/files/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/workflows/files/ci.yml b/src/tools/workflows/files/ci.yml index b6b12f18..7b8f13f2 100644 --- a/src/tools/workflows/files/ci.yml +++ b/src/tools/workflows/files/ci.yml @@ -100,4 +100,4 @@ jobs: echo "No changeset:status script found; skipping." fi -__ANKH_PKGVIZ_AUDIT_STEPS__ + # __ANKH_PKGVIZ_AUDIT_STEPS__ From e6cda6e0310432dc1f0a3507abe7360f61105e85 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 22 Sep 2026 04:47:44 +0200 Subject: [PATCH 13/13] fix(workflows): replace valid pkgviz audit marker --- src/tools/workflows/renderWorkflowAsync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/workflows/renderWorkflowAsync.ts b/src/tools/workflows/renderWorkflowAsync.ts index 2969e79b..dcf3f890 100644 --- a/src/tools/workflows/renderWorkflowAsync.ts +++ b/src/tools/workflows/renderWorkflowAsync.ts @@ -59,4 +59,4 @@ function renderPkgvizAuditSteps(policy: WorkflowPolicy): string { `; } -const PKGVIZ_AUDIT_STEPS_TOKEN = '__ANKH_PKGVIZ_AUDIT_STEPS__'; +const PKGVIZ_AUDIT_STEPS_TOKEN = ' # __ANKH_PKGVIZ_AUDIT_STEPS__';