diff --git a/.github/workflows/deploy-langgraph.yml b/.github/workflows/deploy-langgraph.yml index 7685c0995..2087479b5 100644 --- a/.github/workflows/deploy-langgraph.yml +++ b/.github/workflows/deploy-langgraph.yml @@ -7,6 +7,8 @@ on: - '.github/workflows/deploy-langgraph.yml' - 'cockpit/langgraph/**/python/**' - 'cockpit/deep-agents/**/python/**' + - 'cockpit/chat/**/python/**' + - 'cockpit/render/**/python/**' - 'examples/chat/python/**' - 'libs/cockpit-registry/src/lib/capability-registry.ts' - 'scripts/generate-shared-deployment-config.ts' diff --git a/scripts/shared-deployment-routing.spec.ts b/scripts/shared-deployment-routing.spec.ts index 0cdd8812d..b9b0cf562 100644 --- a/scripts/shared-deployment-routing.spec.ts +++ b/scripts/shared-deployment-routing.spec.ts @@ -1,12 +1,77 @@ -import { readFileSync } from 'fs'; +import { existsSync, readFileSync } from 'fs'; import { resolve } from 'path'; +import { minimatch } from 'minimatch'; import { describe, expect, it } from 'vitest'; +import { parse } from 'yaml'; +import { capabilities } from '@threadplane/cockpit-registry'; const root = resolve(__dirname, '..'); const sharedUrl = 'https://cockpit-dev-free-6957db75280055dcbbf2a4916c04b66f.us.langgraph.app'; +const workflow = readFileSync( + resolve(root, '.github/workflows/deploy-langgraph.yml'), + 'utf8' +); +// Read the push filter itself: paths mentioned in jobs or comments must not +// satisfy trigger coverage. These positive globs use GitHub's ** semantics. +const pushPaths = (parse(workflow) as { on: { push: { paths: string[] } } }).on + .push.paths; +const triggersDeployment = (file: string): boolean => + pushPaths.some((pattern) => minimatch(file, pattern, { dot: true })); +// Keep this selection aligned with generate-shared-deployment-config.ts. +const sharedCapabilities = capabilities.filter( + (capability) => + capability.pythonDir && + capability.product !== 'ag-ui' && + capability.product !== 'runtimes' +); describe('shared free deployment routing', () => { + it.each(sharedCapabilities)( + 'redeploys for staged Python inputs of $id ($product)', + (capability) => { + const manifestPath = `${capability.pythonDir}/langgraph.json`; + const manifest = JSON.parse( + readFileSync(resolve(root, manifestPath), 'utf8') + ) as { graphs: Record }; + const files = [ + manifestPath, + ...Object.values(manifest.graphs).map( + (entrypoint) => + `${capability.pythonDir}/${entrypoint.split(':')[0].replace(/^\.\//, '')}` + ), + ]; + for (const file of files) { + expect(existsSync(resolve(root, file)), file).toBe(true); + expect(triggersDeployment(file), file).toBe(true); + } + } + ); + + it.each([ + 'examples/chat/python/langgraph.json', + 'examples/chat/python/src/graph.py', + '.github/workflows/deploy-langgraph.yml', + 'scripts/generate-shared-deployment-config.ts', + 'libs/cockpit-registry/src/lib/capability-registry.ts', + 'deployments/shared-dev/langgraph.json', + ])('redeploys when shared deployment input %s changes', (file) => { + expect(existsSync(resolve(root, file)), file).toBe(true); + expect(triggersDeployment(file), file).toBe(true); + }); + + it.each( + capabilities.filter( + (capability) => + capability.pythonDir && + (capability.product === 'ag-ui' || capability.product === 'runtimes') + ) + )('excludes separately deployed Python inputs of $id', (capability) => { + const file = `${capability.pythonDir}/pyproject.toml`; + expect(existsSync(resolve(root, file)), file).toBe(true); + expect(triggersDeployment(file), file).toBe(false); + }); + it('routes every active capability and both production proxy defaults to the free deployment', () => { const urls = JSON.parse( readFileSync(resolve(root, 'deployment-urls.json'), 'utf8')