Skip to content

Commit 061fd76

Browse files
Copilothotlong
andcommitted
feat: add DevOps Agent and GitHub/Vercel integration connectors
- Add GitHub connector schema with repository management, CI/CD workflows, and release automation - Add Vercel connector schema with deployment configuration and edge functions - Add DevOps Agent schema for self-iterating enterprise software development - Include comprehensive tests for all new schemas - Update exports in AI and integration index files Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent f7a92b3 commit 061fd76

File tree

8 files changed

+3422
-0
lines changed

8 files changed

+3422
-0
lines changed

packages/spec/src/ai/devops-agent.test.ts

Lines changed: 596 additions & 0 deletions
Large diffs are not rendered by default.

packages/spec/src/ai/devops-agent.zod.ts

Lines changed: 869 additions & 0 deletions
Large diffs are not rendered by default.

packages/spec/src/ai/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*
44
* AI/ML Capabilities
55
* - Agent Configuration
6+
* - DevOps Agent (Self-iterating Development)
67
* - Model Registry & Selection
78
* - RAG Pipeline
89
* - Natural Language Query (NLQ)
@@ -14,6 +15,7 @@
1415

1516
export * from './agent.zod';
1617
export * from './agent-action.zod';
18+
export * from './devops-agent.zod';
1719
export * from './model-registry.zod';
1820
export * from './rag-pipeline.zod';
1921
export * from './nlq.zod';
Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
GitHubConnectorSchema,
4+
GitHubRepositorySchema,
5+
GitHubCommitConfigSchema,
6+
GitHubPullRequestConfigSchema,
7+
GitHubActionsWorkflowSchema,
8+
GitHubReleaseConfigSchema,
9+
GitHubIssueTrackingSchema,
10+
githubPublicConnectorExample,
11+
githubEnterpriseConnectorExample,
12+
type GitHubConnector,
13+
} from './github.zod';
14+
15+
describe('GitHubRepositorySchema', () => {
16+
it('should accept minimal repository config', () => {
17+
const repo = {
18+
owner: 'objectstack-ai',
19+
name: 'spec',
20+
};
21+
22+
const result = GitHubRepositorySchema.parse(repo);
23+
expect(result.defaultBranch).toBe('main');
24+
expect(result.autoMerge).toBe(false);
25+
});
26+
27+
it('should accept repository with branch protection', () => {
28+
const repo = {
29+
owner: 'objectstack-ai',
30+
name: 'spec',
31+
defaultBranch: 'main',
32+
branchProtection: {
33+
requiredReviewers: 2,
34+
requireStatusChecks: true,
35+
enforceAdmins: true,
36+
},
37+
};
38+
39+
expect(() => GitHubRepositorySchema.parse(repo)).not.toThrow();
40+
});
41+
42+
it('should accept repository with topics', () => {
43+
const repo = {
44+
owner: 'objectstack-ai',
45+
name: 'spec',
46+
topics: ['objectstack', 'low-code', 'metadata'],
47+
};
48+
49+
expect(() => GitHubRepositorySchema.parse(repo)).not.toThrow();
50+
});
51+
});
52+
53+
describe('GitHubCommitConfigSchema', () => {
54+
it('should accept minimal commit config', () => {
55+
const config = {};
56+
57+
const result = GitHubCommitConfigSchema.parse(config);
58+
expect(result.signCommits).toBe(false);
59+
expect(result.useConventionalCommits).toBe(true);
60+
});
61+
62+
it('should accept full commit config', () => {
63+
const config = {
64+
authorName: 'ObjectStack Bot',
65+
authorEmail: 'bot@objectstack.ai',
66+
signCommits: true,
67+
messageTemplate: '{{type}}: {{message}}',
68+
useConventionalCommits: true,
69+
};
70+
71+
expect(() => GitHubCommitConfigSchema.parse(config)).not.toThrow();
72+
});
73+
74+
it('should validate email format', () => {
75+
expect(() => GitHubCommitConfigSchema.parse({
76+
authorEmail: 'invalid-email',
77+
})).toThrow();
78+
79+
expect(() => GitHubCommitConfigSchema.parse({
80+
authorEmail: 'valid@email.com',
81+
})).not.toThrow();
82+
});
83+
});
84+
85+
describe('GitHubPullRequestConfigSchema', () => {
86+
it('should accept minimal PR config', () => {
87+
const config = {};
88+
89+
const result = GitHubPullRequestConfigSchema.parse(config);
90+
expect(result.draftByDefault).toBe(false);
91+
expect(result.deleteHeadBranch).toBe(true);
92+
});
93+
94+
it('should accept PR config with templates and reviewers', () => {
95+
const config = {
96+
titleTemplate: '{{type}}: {{description}}',
97+
bodyTemplate: '## Changes\n\n{{changes}}',
98+
defaultReviewers: ['reviewer1', 'reviewer2'],
99+
defaultAssignees: ['assignee1'],
100+
defaultLabels: ['automated', 'needs-review'],
101+
};
102+
103+
expect(() => GitHubPullRequestConfigSchema.parse(config)).not.toThrow();
104+
});
105+
});
106+
107+
describe('GitHubActionsWorkflowSchema', () => {
108+
it('should accept minimal workflow', () => {
109+
const workflow = {
110+
name: 'CI',
111+
path: '.github/workflows/ci.yml',
112+
};
113+
114+
const result = GitHubActionsWorkflowSchema.parse(workflow);
115+
expect(result.enabled).toBe(true);
116+
});
117+
118+
it('should accept all trigger types', () => {
119+
const triggers = ['push', 'pull_request', 'release', 'schedule', 'workflow_dispatch', 'repository_dispatch'] as const;
120+
121+
triggers.forEach(trigger => {
122+
const workflow = {
123+
name: 'Test',
124+
path: '.github/workflows/test.yml',
125+
triggers: [trigger],
126+
};
127+
expect(() => GitHubActionsWorkflowSchema.parse(workflow)).not.toThrow();
128+
});
129+
});
130+
131+
it('should accept workflow with env and secrets', () => {
132+
const workflow = {
133+
name: 'Deploy',
134+
path: '.github/workflows/deploy.yml',
135+
env: {
136+
NODE_ENV: 'production',
137+
API_URL: 'https://api.example.com',
138+
},
139+
secrets: ['DEPLOY_TOKEN', 'AWS_SECRET_KEY'],
140+
};
141+
142+
expect(() => GitHubActionsWorkflowSchema.parse(workflow)).not.toThrow();
143+
});
144+
});
145+
146+
describe('GitHubReleaseConfigSchema', () => {
147+
it('should accept minimal release config', () => {
148+
const config = {};
149+
150+
const result = GitHubReleaseConfigSchema.parse(config);
151+
expect(result.tagPattern).toBe('v*');
152+
expect(result.semanticVersioning).toBe(true);
153+
expect(result.autoReleaseNotes).toBe(true);
154+
});
155+
156+
it('should accept full release config', () => {
157+
const config = {
158+
tagPattern: 'release/*',
159+
semanticVersioning: true,
160+
autoReleaseNotes: true,
161+
releaseNameTemplate: 'Release {{version}}',
162+
preReleasePattern: '*-rc*',
163+
draftByDefault: true,
164+
};
165+
166+
expect(() => GitHubReleaseConfigSchema.parse(config)).not.toThrow();
167+
});
168+
});
169+
170+
describe('GitHubIssueTrackingSchema', () => {
171+
it('should accept minimal issue tracking config', () => {
172+
const config = {};
173+
174+
const result = GitHubIssueTrackingSchema.parse(config);
175+
expect(result.enabled).toBe(true);
176+
expect(result.autoAssign).toBe(false);
177+
});
178+
179+
it('should accept auto-close stale issues config', () => {
180+
const config = {
181+
autoCloseStale: {
182+
enabled: true,
183+
daysBeforeStale: 30,
184+
daysBeforeClose: 7,
185+
staleLabel: 'wontfix',
186+
},
187+
};
188+
189+
const result = GitHubIssueTrackingSchema.parse(config);
190+
expect(result.autoCloseStale?.enabled).toBe(true);
191+
expect(result.autoCloseStale?.daysBeforeStale).toBe(30);
192+
});
193+
});
194+
195+
describe('GitHubConnectorSchema', () => {
196+
describe('Basic Properties', () => {
197+
it('should accept minimal GitHub connector', () => {
198+
const connector: GitHubConnector = {
199+
name: 'github_test',
200+
label: 'GitHub Test',
201+
type: 'saas',
202+
provider: 'github',
203+
authentication: {
204+
type: 'oauth2',
205+
clientId: 'test-client-id',
206+
clientSecret: 'test-client-secret',
207+
authorizationUrl: 'https://github.com/login/oauth/authorize',
208+
tokenUrl: 'https://github.com/login/oauth/access_token',
209+
grantType: 'authorization_code',
210+
},
211+
repositories: [
212+
{
213+
owner: 'test-org',
214+
name: 'test-repo',
215+
},
216+
],
217+
};
218+
219+
const result = GitHubConnectorSchema.parse(connector);
220+
expect(result.baseUrl).toBe('https://api.github.com');
221+
expect(result.enableWebhooks).toBe(true);
222+
});
223+
224+
it('should enforce snake_case for connector name', () => {
225+
const validNames = ['github_test', 'github_production', '_internal'];
226+
validNames.forEach(name => {
227+
expect(() => GitHubConnectorSchema.parse({
228+
name,
229+
label: 'Test',
230+
type: 'saas',
231+
provider: 'github',
232+
authentication: { type: 'oauth2', clientId: 'x', clientSecret: 'y', authorizationUrl: 'https://x.com', tokenUrl: 'https://y.com', grantType: 'authorization_code' },
233+
repositories: [{ owner: 'x', name: 'y' }],
234+
})).not.toThrow();
235+
});
236+
237+
const invalidNames = ['githubTest', 'GitHub-Test', '123github'];
238+
invalidNames.forEach(name => {
239+
expect(() => GitHubConnectorSchema.parse({
240+
name,
241+
label: 'Test',
242+
type: 'saas',
243+
provider: 'github',
244+
authentication: { type: 'oauth2', clientId: 'x', clientSecret: 'y', authorizationUrl: 'https://x.com', tokenUrl: 'https://y.com', grantType: 'authorization_code' },
245+
repositories: [{ owner: 'x', name: 'y' }],
246+
})).toThrow();
247+
});
248+
});
249+
250+
it('should accept GitHub Enterprise provider', () => {
251+
const connector: GitHubConnector = {
252+
name: 'github_enterprise',
253+
label: 'GitHub Enterprise',
254+
type: 'saas',
255+
provider: 'github_enterprise',
256+
baseUrl: 'https://github.enterprise.com/api/v3',
257+
authentication: {
258+
type: 'oauth2',
259+
clientId: 'test-client-id',
260+
clientSecret: 'test-client-secret',
261+
authorizationUrl: 'https://github.enterprise.com/login/oauth/authorize',
262+
tokenUrl: 'https://github.enterprise.com/login/oauth/access_token',
263+
grantType: 'authorization_code',
264+
},
265+
repositories: [
266+
{
267+
owner: 'enterprise-org',
268+
name: 'app',
269+
},
270+
],
271+
};
272+
273+
expect(() => GitHubConnectorSchema.parse(connector)).not.toThrow();
274+
});
275+
});
276+
277+
describe('Complete Configuration', () => {
278+
it('should accept full GitHub connector with all features', () => {
279+
const connector: GitHubConnector = {
280+
name: 'github_full',
281+
label: 'GitHub Full Config',
282+
type: 'saas',
283+
provider: 'github',
284+
baseUrl: 'https://api.github.com',
285+
286+
authentication: {
287+
type: 'oauth2',
288+
clientId: '${GITHUB_CLIENT_ID}',
289+
clientSecret: '${GITHUB_CLIENT_SECRET}',
290+
authorizationUrl: 'https://github.com/login/oauth/authorize',
291+
tokenUrl: 'https://github.com/login/oauth/access_token',
292+
grantType: 'authorization_code',
293+
scopes: ['repo', 'workflow'],
294+
},
295+
296+
repositories: [
297+
{
298+
owner: 'objectstack-ai',
299+
name: 'spec',
300+
defaultBranch: 'main',
301+
autoMerge: false,
302+
branchProtection: {
303+
requiredReviewers: 1,
304+
requireStatusChecks: true,
305+
},
306+
topics: ['objectstack', 'metadata'],
307+
},
308+
],
309+
310+
commitConfig: {
311+
authorName: 'Bot',
312+
authorEmail: 'bot@example.com',
313+
useConventionalCommits: true,
314+
},
315+
316+
pullRequestConfig: {
317+
defaultReviewers: ['reviewer'],
318+
defaultLabels: ['automated'],
319+
},
320+
321+
workflows: [
322+
{
323+
name: 'CI',
324+
path: '.github/workflows/ci.yml',
325+
triggers: ['push', 'pull_request'],
326+
},
327+
],
328+
329+
releaseConfig: {
330+
semanticVersioning: true,
331+
autoReleaseNotes: true,
332+
},
333+
334+
issueTracking: {
335+
enabled: true,
336+
autoCloseStale: {
337+
enabled: true,
338+
daysBeforeStale: 60,
339+
daysBeforeClose: 7,
340+
staleLabel: 'stale',
341+
},
342+
},
343+
344+
enableWebhooks: true,
345+
webhookEvents: ['push', 'pull_request', 'release'],
346+
347+
status: 'active',
348+
enabled: true,
349+
};
350+
351+
expect(() => GitHubConnectorSchema.parse(connector)).not.toThrow();
352+
});
353+
});
354+
355+
describe('Example Configurations', () => {
356+
it('should accept GitHub.com public connector example', () => {
357+
expect(() => GitHubConnectorSchema.parse(githubPublicConnectorExample)).not.toThrow();
358+
});
359+
360+
it('should accept GitHub Enterprise connector example', () => {
361+
expect(() => GitHubConnectorSchema.parse(githubEnterpriseConnectorExample)).not.toThrow();
362+
});
363+
});
364+
});

0 commit comments

Comments
 (0)