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
2 changes: 2 additions & 0 deletions .github/workflows/deploy-langgraph.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
67 changes: 66 additions & 1 deletion scripts/shared-deployment-routing.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> };
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')
Expand Down
Loading