@agent-relay/github-primitive 4.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +1055 -0
- package/README.md +151 -0
- package/docs/actions.md +49 -0
- package/examples/end-to-end-pr-workflow.ts +286 -0
- package/examples/github-client.ts +38 -0
- package/examples/github-step.ts +105 -0
- package/examples/multi-tenant-pr-workflow.ts +201 -0
- package/package.json +50 -0
- package/templates/repository-inspection.yaml +28 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
# GitHub Workflow Primitive
|
|
2
|
+
|
|
3
|
+
A workflow primitive that enables agents to interact with GitHub repositories using both local `gh` CLI commands and cloud-based Nango-connected GitHub API, designed to complement existing integration primitives.
|
|
4
|
+
|
|
5
|
+
## Package Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
packages/github-primitive/
|
|
9
|
+
├── DESIGN.md # This design document
|
|
10
|
+
├── README.md # Package overview
|
|
11
|
+
├── package.json # Package manifest
|
|
12
|
+
├── vitest.config.ts # Package test config
|
|
13
|
+
├── src/
|
|
14
|
+
│ ├── index.ts # Main exports
|
|
15
|
+
│ ├── constants.ts # Shared runtime defaults
|
|
16
|
+
│ ├── types.ts # TypeScript interfaces
|
|
17
|
+
│ ├── client.ts # High-level typed client facade
|
|
18
|
+
│ ├── adapter.ts # Runtime detection, factory, base adapter
|
|
19
|
+
│ ├── local-runtime.ts # Local gh CLI implementation
|
|
20
|
+
│ ├── cloud-runtime.ts # Cloud Nango and relay-cloud implementation
|
|
21
|
+
│ ├── workflow-step.ts # Workflow step executor
|
|
22
|
+
│ ├── actions/ # GitHub action implementations
|
|
23
|
+
│ │ ├── branches.ts # listBranches, createBranch operations
|
|
24
|
+
│ │ ├── commits.ts # listCommits, createCommit operations
|
|
25
|
+
│ │ ├── repos.ts # listRepos, getRepo operations
|
|
26
|
+
│ │ ├── issues.ts # listIssues, createIssue, etc.
|
|
27
|
+
│ │ ├── pulls.ts # listPRs, getPR, createPR, updatePR, mergePR
|
|
28
|
+
│ │ ├── files.ts # listFiles, readFile, createFile, updateFile, deleteFile
|
|
29
|
+
│ │ ├── users.ts # getUser, listOrganizations operations
|
|
30
|
+
│ │ └── utils.ts # Shared request, mapping, and validation helpers
|
|
31
|
+
│ └── __tests__/
|
|
32
|
+
│ └── github-actions.test.ts
|
|
33
|
+
├── templates/ # Workflow templates
|
|
34
|
+
│ └── repository-inspection.yaml
|
|
35
|
+
├── docs/
|
|
36
|
+
│ └── actions.md # Action reference
|
|
37
|
+
└── examples/
|
|
38
|
+
├── github-client.ts # Standalone client usage
|
|
39
|
+
└── github-step.ts # Workflow step usage
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## TypeScript Interfaces
|
|
43
|
+
|
|
44
|
+
### Core Action Types
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// GitHub action types that map to workflow step actions
|
|
48
|
+
export type GitHubAction =
|
|
49
|
+
| 'listRepos'
|
|
50
|
+
| 'getRepo'
|
|
51
|
+
| 'listIssues'
|
|
52
|
+
| 'createIssue'
|
|
53
|
+
| 'updateIssue'
|
|
54
|
+
| 'closeIssue'
|
|
55
|
+
| 'listPRs'
|
|
56
|
+
| 'getPR'
|
|
57
|
+
| 'createPR'
|
|
58
|
+
| 'updatePR'
|
|
59
|
+
| 'mergePR'
|
|
60
|
+
| 'listFiles'
|
|
61
|
+
| 'readFile'
|
|
62
|
+
| 'createFile'
|
|
63
|
+
| 'updateFile'
|
|
64
|
+
| 'deleteFile'
|
|
65
|
+
| 'createBranch'
|
|
66
|
+
| 'listBranches'
|
|
67
|
+
| 'listCommits'
|
|
68
|
+
| 'createCommit'
|
|
69
|
+
| 'getUser'
|
|
70
|
+
| 'listOrganizations';
|
|
71
|
+
|
|
72
|
+
// Runtime detection
|
|
73
|
+
export type GitHubRuntime = 'local' | 'cloud';
|
|
74
|
+
|
|
75
|
+
// GitHub configuration for both runtimes
|
|
76
|
+
export interface GitHubConfig {
|
|
77
|
+
/** Runtime mode - auto-detected if not specified */
|
|
78
|
+
runtime?: GitHubRuntime;
|
|
79
|
+
/** For local runtime: gh CLI path (default: 'gh') */
|
|
80
|
+
ghPath?: string;
|
|
81
|
+
/** For cloud runtime: Nango connection details */
|
|
82
|
+
nango?: {
|
|
83
|
+
connectionId?: string;
|
|
84
|
+
providerConfigKey?: string;
|
|
85
|
+
};
|
|
86
|
+
/** Request timeout in ms (default: 30000) */
|
|
87
|
+
timeout?: number;
|
|
88
|
+
/** Enable retry on rate limit (default: true) */
|
|
89
|
+
retryOnRateLimit?: boolean;
|
|
90
|
+
/** Maximum retry attempts (default: 3) */
|
|
91
|
+
maxRetries?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Repository reference
|
|
95
|
+
export interface RepositoryRef {
|
|
96
|
+
/** Repository owner/organization */
|
|
97
|
+
owner: string;
|
|
98
|
+
/** Repository name */
|
|
99
|
+
repo: string;
|
|
100
|
+
/** Full repository name (owner/repo) */
|
|
101
|
+
fullName?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// File reference within repository
|
|
105
|
+
export interface FileRef extends RepositoryRef {
|
|
106
|
+
/** File path within repository */
|
|
107
|
+
path: string;
|
|
108
|
+
/** Git branch/ref (default: main/master) */
|
|
109
|
+
ref?: string;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Action Parameter Interfaces
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Repository operations
|
|
117
|
+
export interface ListReposParams {
|
|
118
|
+
/** Filter by visibility */
|
|
119
|
+
visibility?: 'all' | 'public' | 'private';
|
|
120
|
+
/** Filter by affiliation */
|
|
121
|
+
affiliation?: 'owner' | 'collaborator' | 'organization_member';
|
|
122
|
+
/** Sort order */
|
|
123
|
+
sort?: 'created' | 'updated' | 'pushed' | 'full_name';
|
|
124
|
+
/** Sort direction */
|
|
125
|
+
direction?: 'asc' | 'desc';
|
|
126
|
+
/** Maximum results (default: 30) */
|
|
127
|
+
perPage?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface GetRepoParams {
|
|
131
|
+
/** Repository owner */
|
|
132
|
+
owner: string;
|
|
133
|
+
/** Repository name */
|
|
134
|
+
repo: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Issue operations
|
|
138
|
+
export interface ListIssuesParams extends RepositoryRef {
|
|
139
|
+
/** Filter by state */
|
|
140
|
+
state?: 'open' | 'closed' | 'all';
|
|
141
|
+
/** Filter by assignee */
|
|
142
|
+
assignee?: string;
|
|
143
|
+
/** Filter by labels (comma-separated) */
|
|
144
|
+
labels?: string;
|
|
145
|
+
/** Sort order */
|
|
146
|
+
sort?: 'created' | 'updated' | 'comments';
|
|
147
|
+
/** Sort direction */
|
|
148
|
+
direction?: 'asc' | 'desc';
|
|
149
|
+
/** Maximum results (default: 30) */
|
|
150
|
+
perPage?: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface CreateIssueParams extends RepositoryRef {
|
|
154
|
+
/** Issue title */
|
|
155
|
+
title: string;
|
|
156
|
+
/** Issue body */
|
|
157
|
+
body?: string;
|
|
158
|
+
/** Assignee username */
|
|
159
|
+
assignee?: string;
|
|
160
|
+
/** Labels to apply */
|
|
161
|
+
labels?: string[];
|
|
162
|
+
/** Milestone number */
|
|
163
|
+
milestone?: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface UpdateIssueParams extends RepositoryRef {
|
|
167
|
+
/** Issue number */
|
|
168
|
+
issueNumber: number;
|
|
169
|
+
/** Updated title */
|
|
170
|
+
title?: string;
|
|
171
|
+
/** Updated body */
|
|
172
|
+
body?: string;
|
|
173
|
+
/** Updated state */
|
|
174
|
+
state?: 'open' | 'closed';
|
|
175
|
+
/** Updated assignee */
|
|
176
|
+
assignee?: string;
|
|
177
|
+
/** Updated labels */
|
|
178
|
+
labels?: string[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Pull request operations
|
|
182
|
+
export interface ListPRsParams extends RepositoryRef {
|
|
183
|
+
/** Filter by state */
|
|
184
|
+
state?: 'open' | 'closed' | 'all';
|
|
185
|
+
/** Filter by base branch */
|
|
186
|
+
base?: string;
|
|
187
|
+
/** Filter by head branch */
|
|
188
|
+
head?: string;
|
|
189
|
+
/** Sort order */
|
|
190
|
+
sort?: 'created' | 'updated' | 'popularity';
|
|
191
|
+
/** Sort direction */
|
|
192
|
+
direction?: 'asc' | 'desc';
|
|
193
|
+
/** Maximum results (default: 30) */
|
|
194
|
+
perPage?: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface CreatePRParams extends RepositoryRef {
|
|
198
|
+
/** PR title */
|
|
199
|
+
title: string;
|
|
200
|
+
/** PR body */
|
|
201
|
+
body?: string;
|
|
202
|
+
/** Base branch to merge into */
|
|
203
|
+
base: string;
|
|
204
|
+
/** Head branch to merge from */
|
|
205
|
+
head: string;
|
|
206
|
+
/** Mark as draft */
|
|
207
|
+
draft?: boolean;
|
|
208
|
+
/** Maintainer can modify */
|
|
209
|
+
maintainerCanModify?: boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface MergePRParams extends RepositoryRef {
|
|
213
|
+
/** PR number */
|
|
214
|
+
pullNumber: number;
|
|
215
|
+
/** Merge method */
|
|
216
|
+
mergeMethod?: 'merge' | 'squash' | 'rebase';
|
|
217
|
+
/** Commit title for merge */
|
|
218
|
+
commitTitle?: string;
|
|
219
|
+
/** Commit message for merge */
|
|
220
|
+
commitMessage?: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// File operations
|
|
224
|
+
export interface ListFilesParams extends RepositoryRef {
|
|
225
|
+
/** Directory path (default: root) */
|
|
226
|
+
path?: string;
|
|
227
|
+
/** Git branch/ref (default: main/master) */
|
|
228
|
+
ref?: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface ReadFileParams extends FileRef {}
|
|
232
|
+
|
|
233
|
+
export interface CreateFileParams extends FileRef {
|
|
234
|
+
/** File content */
|
|
235
|
+
content: string;
|
|
236
|
+
/** Commit message */
|
|
237
|
+
message: string;
|
|
238
|
+
/** Branch to commit to (default: main/master) */
|
|
239
|
+
branch?: string;
|
|
240
|
+
/** Author information */
|
|
241
|
+
author?: {
|
|
242
|
+
name: string;
|
|
243
|
+
email: string;
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface UpdateFileParams extends CreateFileParams {
|
|
248
|
+
/** SHA of file being replaced */
|
|
249
|
+
sha: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface DeleteFileParams extends FileRef {
|
|
253
|
+
/** SHA of file being deleted */
|
|
254
|
+
sha: string;
|
|
255
|
+
/** Commit message */
|
|
256
|
+
message: string;
|
|
257
|
+
/** Branch to commit to (default: main/master) */
|
|
258
|
+
branch?: string;
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Response Types
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Repository information
|
|
266
|
+
export interface Repository {
|
|
267
|
+
id: number;
|
|
268
|
+
name: string;
|
|
269
|
+
fullName: string;
|
|
270
|
+
owner: {
|
|
271
|
+
login: string;
|
|
272
|
+
type: string;
|
|
273
|
+
};
|
|
274
|
+
description?: string;
|
|
275
|
+
private: boolean;
|
|
276
|
+
fork: boolean;
|
|
277
|
+
createdAt: string;
|
|
278
|
+
updatedAt: string;
|
|
279
|
+
pushedAt: string;
|
|
280
|
+
size: number;
|
|
281
|
+
stargazersCount: number;
|
|
282
|
+
watchersCount: number;
|
|
283
|
+
language?: string;
|
|
284
|
+
forksCount: number;
|
|
285
|
+
openIssuesCount: number;
|
|
286
|
+
defaultBranch: string;
|
|
287
|
+
topics: string[];
|
|
288
|
+
visibility: 'public' | 'private' | 'internal';
|
|
289
|
+
permissions?: {
|
|
290
|
+
admin: boolean;
|
|
291
|
+
maintain: boolean;
|
|
292
|
+
push: boolean;
|
|
293
|
+
triage: boolean;
|
|
294
|
+
pull: boolean;
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Issue information
|
|
299
|
+
export interface Issue {
|
|
300
|
+
number: number;
|
|
301
|
+
id: number;
|
|
302
|
+
title: string;
|
|
303
|
+
body?: string;
|
|
304
|
+
user: {
|
|
305
|
+
login: string;
|
|
306
|
+
type: string;
|
|
307
|
+
};
|
|
308
|
+
labels: Array<{
|
|
309
|
+
name: string;
|
|
310
|
+
color: string;
|
|
311
|
+
description?: string;
|
|
312
|
+
}>;
|
|
313
|
+
state: 'open' | 'closed';
|
|
314
|
+
locked: boolean;
|
|
315
|
+
assignee?: {
|
|
316
|
+
login: string;
|
|
317
|
+
};
|
|
318
|
+
assignees: Array<{
|
|
319
|
+
login: string;
|
|
320
|
+
}>;
|
|
321
|
+
milestone?: {
|
|
322
|
+
number: number;
|
|
323
|
+
title: string;
|
|
324
|
+
};
|
|
325
|
+
commentsCount: number;
|
|
326
|
+
createdAt: string;
|
|
327
|
+
updatedAt: string;
|
|
328
|
+
closedAt?: string;
|
|
329
|
+
authorAssociation: string;
|
|
330
|
+
reactions: {
|
|
331
|
+
totalCount: number;
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Pull request information
|
|
336
|
+
export interface PullRequest {
|
|
337
|
+
number: number;
|
|
338
|
+
id: number;
|
|
339
|
+
title: string;
|
|
340
|
+
body?: string;
|
|
341
|
+
user: {
|
|
342
|
+
login: string;
|
|
343
|
+
type: string;
|
|
344
|
+
};
|
|
345
|
+
state: 'open' | 'closed';
|
|
346
|
+
draft: boolean;
|
|
347
|
+
locked: boolean;
|
|
348
|
+
mergeable?: boolean;
|
|
349
|
+
mergeableState: string;
|
|
350
|
+
merged: boolean;
|
|
351
|
+
mergedAt?: string;
|
|
352
|
+
mergedBy?: {
|
|
353
|
+
login: string;
|
|
354
|
+
};
|
|
355
|
+
base: {
|
|
356
|
+
ref: string;
|
|
357
|
+
sha: string;
|
|
358
|
+
repo: {
|
|
359
|
+
name: string;
|
|
360
|
+
fullName: string;
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
head: {
|
|
364
|
+
ref: string;
|
|
365
|
+
sha: string;
|
|
366
|
+
repo?: {
|
|
367
|
+
name: string;
|
|
368
|
+
fullName: string;
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
requestedReviewers: Array<{
|
|
372
|
+
login: string;
|
|
373
|
+
}>;
|
|
374
|
+
labels: Array<{
|
|
375
|
+
name: string;
|
|
376
|
+
color: string;
|
|
377
|
+
}>;
|
|
378
|
+
commentsCount: number;
|
|
379
|
+
reviewCommentsCount: number;
|
|
380
|
+
commitsCount: number;
|
|
381
|
+
additionsCount: number;
|
|
382
|
+
deletionsCount: number;
|
|
383
|
+
changedFilesCount: number;
|
|
384
|
+
createdAt: string;
|
|
385
|
+
updatedAt: string;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// File information
|
|
389
|
+
export interface FileInfo {
|
|
390
|
+
name: string;
|
|
391
|
+
path: string;
|
|
392
|
+
sha: string;
|
|
393
|
+
size: number;
|
|
394
|
+
url: string;
|
|
395
|
+
htmlUrl: string;
|
|
396
|
+
gitUrl: string;
|
|
397
|
+
downloadUrl?: string;
|
|
398
|
+
type: 'file' | 'dir';
|
|
399
|
+
content?: string; // Base64 encoded for files
|
|
400
|
+
encoding?: string;
|
|
401
|
+
target?: string; // For symlinks
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Action execution result
|
|
405
|
+
export interface GitHubActionResult {
|
|
406
|
+
/** Whether action succeeded */
|
|
407
|
+
success: boolean;
|
|
408
|
+
/** Action output (JSON stringified for complex objects) */
|
|
409
|
+
output: string;
|
|
410
|
+
/** Error message if action failed */
|
|
411
|
+
error?: string;
|
|
412
|
+
/** Additional metadata */
|
|
413
|
+
metadata?: {
|
|
414
|
+
/** API rate limit remaining */
|
|
415
|
+
rateLimitRemaining?: number;
|
|
416
|
+
/** Rate limit reset time */
|
|
417
|
+
rateLimitReset?: string;
|
|
418
|
+
/** Runtime used (local/cloud) */
|
|
419
|
+
runtime?: GitHubRuntime;
|
|
420
|
+
/** Request execution time in ms */
|
|
421
|
+
executionTime?: number;
|
|
422
|
+
/** Whether request was retried */
|
|
423
|
+
retried?: boolean;
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Abstract Client Interface
|
|
429
|
+
|
|
430
|
+
The core abstraction that enables dual runtime support:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
/**
|
|
434
|
+
* Abstract GitHub client that can be implemented for both local and cloud runtimes
|
|
435
|
+
*/
|
|
436
|
+
export abstract class GitHubClient {
|
|
437
|
+
protected config: GitHubConfig;
|
|
438
|
+
|
|
439
|
+
constructor(config: GitHubConfig) {
|
|
440
|
+
this.config = config;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Runtime detection
|
|
444
|
+
abstract getRuntime(): GitHubRuntime;
|
|
445
|
+
abstract isAuthenticated(): Promise<boolean>;
|
|
446
|
+
abstract getCurrentUser(): Promise<{ login: string; name?: string }>;
|
|
447
|
+
|
|
448
|
+
// Repository operations
|
|
449
|
+
abstract listRepositories(params?: ListReposParams): Promise<Repository[]>;
|
|
450
|
+
abstract getRepository(params: GetRepoParams): Promise<Repository>;
|
|
451
|
+
|
|
452
|
+
// Issue operations
|
|
453
|
+
abstract listIssues(params: ListIssuesParams): Promise<Issue[]>;
|
|
454
|
+
abstract createIssue(params: CreateIssueParams): Promise<Issue>;
|
|
455
|
+
abstract updateIssue(params: UpdateIssueParams): Promise<Issue>;
|
|
456
|
+
abstract closeIssue(params: { owner: string; repo: string; issueNumber: number }): Promise<Issue>;
|
|
457
|
+
|
|
458
|
+
// Pull request operations
|
|
459
|
+
abstract listPullRequests(params: ListPRsParams): Promise<PullRequest[]>;
|
|
460
|
+
abstract getPullRequest(params: { owner: string; repo: string; pullNumber: number }): Promise<PullRequest>;
|
|
461
|
+
abstract createPullRequest(params: CreatePRParams): Promise<PullRequest>;
|
|
462
|
+
abstract updatePullRequest(params: UpdatePRParams): Promise<PullRequest>;
|
|
463
|
+
abstract mergePullRequest(params: MergePRParams): Promise<PullRequest>;
|
|
464
|
+
|
|
465
|
+
// File operations
|
|
466
|
+
abstract listFiles(params: ListFilesParams): Promise<FileInfo[]>;
|
|
467
|
+
abstract readFile(params: ReadFileParams): Promise<string>;
|
|
468
|
+
abstract createFile(params: CreateFileParams): Promise<void>;
|
|
469
|
+
abstract updateFile(params: UpdateFileParams): Promise<FileInfo>;
|
|
470
|
+
abstract deleteFile(params: DeleteFileParams): Promise<void>;
|
|
471
|
+
|
|
472
|
+
// Branch operations
|
|
473
|
+
abstract listBranches(params: RepositoryRef): Promise<Array<{ name: string; commit: { sha: string } }>>;
|
|
474
|
+
abstract createBranch(params: RepositoryRef & { branch: string; fromBranch?: string }): Promise<void>;
|
|
475
|
+
|
|
476
|
+
// Commit and identity operations
|
|
477
|
+
abstract listCommits(params: ListCommitsParams): Promise<CommitInfo[]>;
|
|
478
|
+
abstract createCommit(params: CreateCommitParams): Promise<CommitInfo>;
|
|
479
|
+
abstract getUser(params?: GetUserParams): Promise<GitHubUserSummary>;
|
|
480
|
+
abstract listOrganizations(params?: ListOrganizationsParams): Promise<OrganizationInfo[]>;
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## Runtime Detection and Client Factory
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
/**
|
|
488
|
+
* Detects the appropriate runtime and creates the correct client
|
|
489
|
+
*/
|
|
490
|
+
export class GitHubClientFactory {
|
|
491
|
+
/**
|
|
492
|
+
* Auto-detect runtime and create appropriate client
|
|
493
|
+
*/
|
|
494
|
+
static async create(config: GitHubConfig = {}): Promise<GitHubClient> {
|
|
495
|
+
const runtime = config.runtime || (await this.detectRuntime());
|
|
496
|
+
|
|
497
|
+
switch (runtime) {
|
|
498
|
+
case 'local':
|
|
499
|
+
return new LocalGitHubClient(config);
|
|
500
|
+
case 'cloud':
|
|
501
|
+
return new CloudGitHubClient(config);
|
|
502
|
+
default:
|
|
503
|
+
throw new Error(`Unsupported GitHub runtime: ${runtime}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Detect runtime based on environment
|
|
509
|
+
*/
|
|
510
|
+
private static async detectRuntime(): Promise<GitHubRuntime> {
|
|
511
|
+
// Check for cloud environment indicators
|
|
512
|
+
if (process.env.NODE_ENV === 'production' || process.env.VERCEL || process.env.RAILWAY_ENVIRONMENT) {
|
|
513
|
+
return 'cloud';
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Check for Nango configuration
|
|
517
|
+
if (process.env.NANGO_SECRET_KEY) {
|
|
518
|
+
return 'cloud';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Check if gh CLI is available
|
|
522
|
+
try {
|
|
523
|
+
const { execSync } = await import('child_process');
|
|
524
|
+
execSync('gh --version', { stdio: 'pipe' });
|
|
525
|
+
return 'local';
|
|
526
|
+
} catch {
|
|
527
|
+
// Fall back to cloud if gh CLI not available
|
|
528
|
+
return 'cloud';
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Test runtime availability and authentication
|
|
534
|
+
*/
|
|
535
|
+
static async testRuntime(runtime: GitHubRuntime, config: GitHubConfig): Promise<boolean> {
|
|
536
|
+
try {
|
|
537
|
+
const client = runtime === 'local' ? new LocalGitHubClient(config) : new CloudGitHubClient(config);
|
|
538
|
+
|
|
539
|
+
return await client.isAuthenticated();
|
|
540
|
+
} catch {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## Step Configuration Schema
|
|
548
|
+
|
|
549
|
+
GitHub steps integrate into workflows using the existing integration step pattern:
|
|
550
|
+
|
|
551
|
+
```yaml
|
|
552
|
+
steps:
|
|
553
|
+
- name: list-user-repos
|
|
554
|
+
type: integration
|
|
555
|
+
integration: github
|
|
556
|
+
action: listRepos
|
|
557
|
+
params:
|
|
558
|
+
visibility: 'all'
|
|
559
|
+
sort: 'updated'
|
|
560
|
+
perPage: 50
|
|
561
|
+
|
|
562
|
+
- name: get-repo-details
|
|
563
|
+
type: integration
|
|
564
|
+
integration: github
|
|
565
|
+
action: getRepo
|
|
566
|
+
params:
|
|
567
|
+
owner: 'octocat'
|
|
568
|
+
repo: 'Hello-World'
|
|
569
|
+
|
|
570
|
+
- name: create-feature-branch
|
|
571
|
+
type: integration
|
|
572
|
+
integration: github
|
|
573
|
+
action: createBranch
|
|
574
|
+
params:
|
|
575
|
+
owner: '{{steps.get-repo-details.output.owner.login}}'
|
|
576
|
+
repo: '{{steps.get-repo-details.output.name}}'
|
|
577
|
+
branch: 'feature/{{workflow.featureName}}'
|
|
578
|
+
fromBranch: 'main'
|
|
579
|
+
|
|
580
|
+
- name: update-readme
|
|
581
|
+
type: integration
|
|
582
|
+
integration: github
|
|
583
|
+
action: updateFile
|
|
584
|
+
params:
|
|
585
|
+
owner: '{{steps.get-repo-details.output.owner.login}}'
|
|
586
|
+
repo: '{{steps.get-repo-details.output.name}}'
|
|
587
|
+
path: 'README.md'
|
|
588
|
+
content: '{{steps.generate-readme.output}}'
|
|
589
|
+
message: 'Update README with new features'
|
|
590
|
+
branch: 'feature/{{workflow.featureName}}'
|
|
591
|
+
sha: '{{steps.read-current-readme.output.sha}}'
|
|
592
|
+
|
|
593
|
+
- name: create-pull-request
|
|
594
|
+
type: integration
|
|
595
|
+
integration: github
|
|
596
|
+
action: createPR
|
|
597
|
+
params:
|
|
598
|
+
owner: '{{steps.get-repo-details.output.owner.login}}'
|
|
599
|
+
repo: '{{steps.get-repo-details.output.name}}'
|
|
600
|
+
title: 'Add {{workflow.featureName}} feature'
|
|
601
|
+
body: |
|
|
602
|
+
## Summary
|
|
603
|
+
{{steps.generate-pr-description.output}}
|
|
604
|
+
|
|
605
|
+
## Changes
|
|
606
|
+
- Updated README.md
|
|
607
|
+
|
|
608
|
+
Auto-generated by relay workflow
|
|
609
|
+
base: 'main'
|
|
610
|
+
head: 'feature/{{workflow.featureName}}'
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Global GitHub Configuration
|
|
614
|
+
|
|
615
|
+
GitHub configuration can be set at the workflow level:
|
|
616
|
+
|
|
617
|
+
```yaml
|
|
618
|
+
# Global GitHub configuration
|
|
619
|
+
githubConfig:
|
|
620
|
+
runtime: 'auto' # or "local" or "cloud"
|
|
621
|
+
timeout: 30000
|
|
622
|
+
retryOnRateLimit: true
|
|
623
|
+
maxRetries: 3
|
|
624
|
+
# For cloud runtime
|
|
625
|
+
nango:
|
|
626
|
+
connectionId: 'github-main'
|
|
627
|
+
providerConfigKey: 'github'
|
|
628
|
+
|
|
629
|
+
steps:
|
|
630
|
+
# GitHub steps inherit global config
|
|
631
|
+
- name: list-issues
|
|
632
|
+
type: integration
|
|
633
|
+
integration: github
|
|
634
|
+
action: listIssues
|
|
635
|
+
params:
|
|
636
|
+
owner: 'myorg'
|
|
637
|
+
repo: 'myrepo'
|
|
638
|
+
state: 'open'
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Step-Level Configuration Override
|
|
642
|
+
|
|
643
|
+
Individual steps can override global GitHub config:
|
|
644
|
+
|
|
645
|
+
```yaml
|
|
646
|
+
steps:
|
|
647
|
+
- name: emergency-hotfix
|
|
648
|
+
type: integration
|
|
649
|
+
integration: github
|
|
650
|
+
action: createPR
|
|
651
|
+
params:
|
|
652
|
+
owner: 'myorg'
|
|
653
|
+
repo: 'myrepo'
|
|
654
|
+
title: 'HOTFIX: Critical security patch'
|
|
655
|
+
body: 'Emergency security fix'
|
|
656
|
+
base: 'main'
|
|
657
|
+
head: 'hotfix/security'
|
|
658
|
+
# Step-specific GitHub config
|
|
659
|
+
githubConfig:
|
|
660
|
+
timeout: 60000 # Longer timeout for critical operations
|
|
661
|
+
maxRetries: 5
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
## Example Workflow Usage
|
|
665
|
+
|
|
666
|
+
### 1. Issue Triage Workflow
|
|
667
|
+
|
|
668
|
+
```yaml
|
|
669
|
+
version: '1.0'
|
|
670
|
+
name: github-issue-triage
|
|
671
|
+
description: Automatically triage and label new GitHub issues
|
|
672
|
+
|
|
673
|
+
githubConfig:
|
|
674
|
+
runtime: 'auto'
|
|
675
|
+
retryOnRateLimit: true
|
|
676
|
+
|
|
677
|
+
steps:
|
|
678
|
+
- name: fetch-new-issues
|
|
679
|
+
type: integration
|
|
680
|
+
integration: github
|
|
681
|
+
action: listIssues
|
|
682
|
+
params:
|
|
683
|
+
owner: '{{workflow.repoOwner}}'
|
|
684
|
+
repo: '{{workflow.repoName}}'
|
|
685
|
+
state: 'open'
|
|
686
|
+
sort: 'created'
|
|
687
|
+
perPage: 20
|
|
688
|
+
|
|
689
|
+
- name: analyze-issues
|
|
690
|
+
type: agent
|
|
691
|
+
agent: issue-analyzer
|
|
692
|
+
task: |
|
|
693
|
+
Analyze these GitHub issues and suggest labels and priorities:
|
|
694
|
+
{{steps.fetch-new-issues.output}}
|
|
695
|
+
|
|
696
|
+
- name: apply-labels
|
|
697
|
+
type: integration
|
|
698
|
+
integration: github
|
|
699
|
+
action: updateIssue
|
|
700
|
+
params:
|
|
701
|
+
owner: '{{workflow.repoOwner}}'
|
|
702
|
+
repo: '{{workflow.repoName}}'
|
|
703
|
+
issueNumber: '{{steps.analyze-issues.output.issueNumber}}'
|
|
704
|
+
labels: '{{steps.analyze-issues.output.suggestedLabels}}'
|
|
705
|
+
|
|
706
|
+
- name: create-triage-report
|
|
707
|
+
type: integration
|
|
708
|
+
integration: github
|
|
709
|
+
action: createIssue
|
|
710
|
+
params:
|
|
711
|
+
owner: '{{workflow.repoOwner}}'
|
|
712
|
+
repo: '{{workflow.repoName}}'
|
|
713
|
+
title: 'Daily Issue Triage Report - {{workflow.date}}'
|
|
714
|
+
body: |
|
|
715
|
+
## Triaged Issues
|
|
716
|
+
{{steps.analyze-issues.output.report}}
|
|
717
|
+
|
|
718
|
+
## Summary
|
|
719
|
+
- New issues: {{steps.analyze-issues.output.newCount}}
|
|
720
|
+
- High priority: {{steps.analyze-issues.output.highPriorityCount}}
|
|
721
|
+
- Needs attention: {{steps.analyze-issues.output.needsAttentionCount}}
|
|
722
|
+
labels: ['triage', 'automation']
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### 2. Pull Request Review Workflow
|
|
726
|
+
|
|
727
|
+
```yaml
|
|
728
|
+
version: '1.0'
|
|
729
|
+
name: automated-pr-review
|
|
730
|
+
description: Automated code review and feedback for pull requests
|
|
731
|
+
|
|
732
|
+
githubConfig:
|
|
733
|
+
runtime: 'cloud' # Use cloud for webhook integration
|
|
734
|
+
nango:
|
|
735
|
+
connectionId: 'github-bot'
|
|
736
|
+
|
|
737
|
+
steps:
|
|
738
|
+
- name: get-pr-details
|
|
739
|
+
type: integration
|
|
740
|
+
integration: github
|
|
741
|
+
action: getPR
|
|
742
|
+
params:
|
|
743
|
+
owner: '{{workflow.prOwner}}'
|
|
744
|
+
repo: '{{workflow.prRepo}}'
|
|
745
|
+
pullNumber: '{{workflow.prNumber}}'
|
|
746
|
+
|
|
747
|
+
- name: get-changed-files
|
|
748
|
+
type: integration
|
|
749
|
+
integration: github
|
|
750
|
+
action: listFiles
|
|
751
|
+
params:
|
|
752
|
+
owner: '{{workflow.prOwner}}'
|
|
753
|
+
repo: '{{workflow.prRepo}}'
|
|
754
|
+
ref: '{{steps.get-pr-details.output.head.sha}}'
|
|
755
|
+
|
|
756
|
+
- name: review-code-changes
|
|
757
|
+
type: agent
|
|
758
|
+
agent: code-reviewer
|
|
759
|
+
task: |
|
|
760
|
+
Review this pull request:
|
|
761
|
+
|
|
762
|
+
**PR Details:**
|
|
763
|
+
{{steps.get-pr-details.output}}
|
|
764
|
+
|
|
765
|
+
**Changed Files:**
|
|
766
|
+
{{steps.get-changed-files.output}}
|
|
767
|
+
|
|
768
|
+
Provide feedback on code quality, security, and best practices.
|
|
769
|
+
|
|
770
|
+
- name: update-pr-description
|
|
771
|
+
type: integration
|
|
772
|
+
integration: github
|
|
773
|
+
action: updatePR
|
|
774
|
+
params:
|
|
775
|
+
owner: '{{workflow.prOwner}}'
|
|
776
|
+
repo: '{{workflow.prRepo}}'
|
|
777
|
+
pullNumber: '{{workflow.prNumber}}'
|
|
778
|
+
body: |
|
|
779
|
+
{{steps.get-pr-details.output.body}}
|
|
780
|
+
|
|
781
|
+
---
|
|
782
|
+
|
|
783
|
+
## 🤖 Automated Review
|
|
784
|
+
{{steps.review-code-changes.output.feedback}}
|
|
785
|
+
|
|
786
|
+
### 📊 Analysis Summary
|
|
787
|
+
{{steps.review-code-changes.output.summary}}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### 3. Repository Sync Workflow
|
|
791
|
+
|
|
792
|
+
```yaml
|
|
793
|
+
version: '1.0'
|
|
794
|
+
name: multi-repo-sync
|
|
795
|
+
description: Synchronize changes across multiple repositories
|
|
796
|
+
|
|
797
|
+
githubConfig:
|
|
798
|
+
runtime: 'local' # Use local for development workflow
|
|
799
|
+
ghPath: '/usr/local/bin/gh'
|
|
800
|
+
|
|
801
|
+
steps:
|
|
802
|
+
- name: list-target-repos
|
|
803
|
+
type: integration
|
|
804
|
+
integration: github
|
|
805
|
+
action: listRepos
|
|
806
|
+
params:
|
|
807
|
+
affiliation: 'owner'
|
|
808
|
+
sort: 'updated'
|
|
809
|
+
perPage: 100
|
|
810
|
+
|
|
811
|
+
- name: filter-repos
|
|
812
|
+
type: agent
|
|
813
|
+
agent: repo-filter
|
|
814
|
+
task: |
|
|
815
|
+
Filter repositories that need the update:
|
|
816
|
+
{{steps.list-target-repos.output}}
|
|
817
|
+
|
|
818
|
+
Only include repositories with topics: ["{{workflow.targetTopic}}"]
|
|
819
|
+
|
|
820
|
+
- name: create-sync-branches
|
|
821
|
+
type: integration
|
|
822
|
+
integration: github
|
|
823
|
+
action: createBranch
|
|
824
|
+
params:
|
|
825
|
+
owner: '{{item.owner.login}}'
|
|
826
|
+
repo: '{{item.name}}'
|
|
827
|
+
branch: 'sync/{{workflow.syncId}}'
|
|
828
|
+
fromBranch: '{{item.defaultBranch}}'
|
|
829
|
+
# This would iterate over filtered repos
|
|
830
|
+
|
|
831
|
+
- name: apply-template-changes
|
|
832
|
+
type: integration
|
|
833
|
+
integration: github
|
|
834
|
+
action: createFile
|
|
835
|
+
params:
|
|
836
|
+
owner: '{{item.owner.login}}'
|
|
837
|
+
repo: '{{item.name}}'
|
|
838
|
+
path: '{{workflow.templateFile}}'
|
|
839
|
+
content: '{{steps.generate-template.output}}'
|
|
840
|
+
message: 'Sync: Update {{workflow.templateFile}}'
|
|
841
|
+
branch: 'sync/{{workflow.syncId}}'
|
|
842
|
+
|
|
843
|
+
- name: create-sync-prs
|
|
844
|
+
type: integration
|
|
845
|
+
integration: github
|
|
846
|
+
action: createPR
|
|
847
|
+
params:
|
|
848
|
+
owner: '{{item.owner.login}}'
|
|
849
|
+
repo: '{{item.name}}'
|
|
850
|
+
title: 'Sync: {{workflow.changeDescription}}'
|
|
851
|
+
body: |
|
|
852
|
+
## 🔄 Repository Sync
|
|
853
|
+
|
|
854
|
+
This PR synchronizes changes across the organization:
|
|
855
|
+
|
|
856
|
+
### Changes
|
|
857
|
+
{{workflow.changeDescription}}
|
|
858
|
+
|
|
859
|
+
### Files Modified
|
|
860
|
+
- {{workflow.templateFile}}
|
|
861
|
+
|
|
862
|
+
Auto-generated by repo sync workflow
|
|
863
|
+
base: '{{item.defaultBranch}}'
|
|
864
|
+
head: 'sync/{{workflow.syncId}}'
|
|
865
|
+
draft: false
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
## Error Handling and Fallback Strategy
|
|
869
|
+
|
|
870
|
+
The GitHub primitive implements robust error handling with automatic fallback:
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
export class GitHubExecutor implements WorkflowExecutor {
|
|
874
|
+
async executeIntegrationStep(
|
|
875
|
+
step: WorkflowStep,
|
|
876
|
+
resolvedParams: Record<string, string>,
|
|
877
|
+
context: { workspaceId?: string }
|
|
878
|
+
): Promise<{ output: string; success: boolean }> {
|
|
879
|
+
if (step.integration !== 'github') {
|
|
880
|
+
throw new Error(`GitHubExecutor only handles github integration steps`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
try {
|
|
884
|
+
// Try primary client first
|
|
885
|
+
const primaryClient = await this.createClient(step.githubConfig);
|
|
886
|
+
const result = await this.executeAction(primaryClient, step.action, resolvedParams);
|
|
887
|
+
|
|
888
|
+
return {
|
|
889
|
+
output: JSON.stringify(result.output),
|
|
890
|
+
success: result.success,
|
|
891
|
+
};
|
|
892
|
+
} catch (primaryError) {
|
|
893
|
+
// Try fallback runtime if primary fails
|
|
894
|
+
try {
|
|
895
|
+
const fallbackClient = await this.createFallbackClient(step.githubConfig);
|
|
896
|
+
const result = await this.executeAction(fallbackClient, step.action, resolvedParams);
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
output: JSON.stringify({
|
|
900
|
+
...result.output,
|
|
901
|
+
_fallbackUsed: true,
|
|
902
|
+
_primaryError: primaryError.message,
|
|
903
|
+
}),
|
|
904
|
+
success: result.success,
|
|
905
|
+
};
|
|
906
|
+
} catch (fallbackError) {
|
|
907
|
+
return {
|
|
908
|
+
output: JSON.stringify({
|
|
909
|
+
error: primaryError.message,
|
|
910
|
+
fallbackError: fallbackError.message,
|
|
911
|
+
runtime: await this.detectRuntime(),
|
|
912
|
+
}),
|
|
913
|
+
success: false,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
private async createClient(config: GitHubConfig = {}): Promise<GitHubClient> {
|
|
920
|
+
return await GitHubClientFactory.create(config);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
private async createFallbackClient(config: GitHubConfig = {}): Promise<GitHubClient> {
|
|
924
|
+
const currentRuntime = config.runtime || (await GitHubClientFactory.detectRuntime());
|
|
925
|
+
const fallbackRuntime = currentRuntime === 'local' ? 'cloud' : 'local';
|
|
926
|
+
|
|
927
|
+
const fallbackConfig = {
|
|
928
|
+
...config,
|
|
929
|
+
runtime: fallbackRuntime,
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
return await GitHubClientFactory.create(fallbackConfig);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
## Migration Path from sage/github-tool.ts
|
|
938
|
+
|
|
939
|
+
For existing workflows using sage/github-tool.ts, migration is straightforward:
|
|
940
|
+
|
|
941
|
+
### Before (sage/github-tool.ts)
|
|
942
|
+
|
|
943
|
+
```yaml
|
|
944
|
+
steps:
|
|
945
|
+
- name: create-issue
|
|
946
|
+
type: sage
|
|
947
|
+
sage: github-tool
|
|
948
|
+
params:
|
|
949
|
+
action: 'create_issue'
|
|
950
|
+
repo: 'owner/repo'
|
|
951
|
+
title: 'Bug report'
|
|
952
|
+
body: 'Description'
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### After (GitHub Primitive)
|
|
956
|
+
|
|
957
|
+
```yaml
|
|
958
|
+
steps:
|
|
959
|
+
- name: create-issue
|
|
960
|
+
type: integration
|
|
961
|
+
integration: github
|
|
962
|
+
action: createIssue
|
|
963
|
+
params:
|
|
964
|
+
owner: 'owner'
|
|
965
|
+
repo: 'repo'
|
|
966
|
+
title: 'Bug report'
|
|
967
|
+
body: 'Description'
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
### Migration Utility
|
|
971
|
+
|
|
972
|
+
A migration utility helps convert existing workflows:
|
|
973
|
+
|
|
974
|
+
```typescript
|
|
975
|
+
/**
|
|
976
|
+
* Migrate sage github-tool steps to GitHub primitive
|
|
977
|
+
*/
|
|
978
|
+
export function migrateSageGithubSteps(workflow: any): any {
|
|
979
|
+
const migratedWorkflow = { ...workflow };
|
|
980
|
+
|
|
981
|
+
migratedWorkflow.steps = workflow.steps.map((step: any) => {
|
|
982
|
+
if (step.type === 'sage' && step.sage === 'github-tool') {
|
|
983
|
+
return {
|
|
984
|
+
...step,
|
|
985
|
+
type: 'integration',
|
|
986
|
+
integration: 'github',
|
|
987
|
+
action: convertSageAction(step.params.action),
|
|
988
|
+
params: convertSageParams(step.params),
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
return step;
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
return migratedWorkflow;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function convertSageAction(sageAction: string): string {
|
|
998
|
+
const actionMap: Record<string, string> = {
|
|
999
|
+
create_issue: 'createIssue',
|
|
1000
|
+
list_issues: 'listIssues',
|
|
1001
|
+
create_pr: 'createPR',
|
|
1002
|
+
list_repos: 'listRepos',
|
|
1003
|
+
read_file: 'readFile',
|
|
1004
|
+
create_file: 'createFile',
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
return actionMap[sageAction] || sageAction;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function convertSageParams(sageParams: any): any {
|
|
1011
|
+
// Convert repo: "owner/repo" to owner: "owner", repo: "repo"
|
|
1012
|
+
if (sageParams.repo && typeof sageParams.repo === 'string' && sageParams.repo.includes('/')) {
|
|
1013
|
+
const [owner, repo] = sageParams.repo.split('/', 2);
|
|
1014
|
+
return {
|
|
1015
|
+
...sageParams,
|
|
1016
|
+
owner,
|
|
1017
|
+
repo,
|
|
1018
|
+
repo: undefined, // Remove old format
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return sageParams;
|
|
1023
|
+
}
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
## Implementation Status
|
|
1027
|
+
|
|
1028
|
+
### Core Infrastructure
|
|
1029
|
+
|
|
1030
|
+
- [x] Abstract GitHub client interface
|
|
1031
|
+
- [x] Runtime detection and client factory
|
|
1032
|
+
- [x] Local client implementation (gh CLI wrapper)
|
|
1033
|
+
- [x] Cloud client implementation (Nango plus relay-cloud fallback)
|
|
1034
|
+
- [x] Workflow step executor integration
|
|
1035
|
+
|
|
1036
|
+
### Action Coverage
|
|
1037
|
+
|
|
1038
|
+
- [x] Repository operations (list, get)
|
|
1039
|
+
- [x] Issue operations (list, create, update, close)
|
|
1040
|
+
- [x] Pull request operations (list, create, get, update, merge)
|
|
1041
|
+
- [x] File operations (list, read, create, update, delete)
|
|
1042
|
+
- [x] Branch operations (list, create)
|
|
1043
|
+
- [x] Commit operations (list, create)
|
|
1044
|
+
- [x] User and organization operations
|
|
1045
|
+
|
|
1046
|
+
### Package Readiness
|
|
1047
|
+
|
|
1048
|
+
- [x] Error handling and retry logic
|
|
1049
|
+
- [x] Unit tests for action dispatch and fallback behavior
|
|
1050
|
+
- [x] README, action docs, workflow template, and examples
|
|
1051
|
+
- [ ] Migration utility from sage/github-tool
|
|
1052
|
+
- [ ] Performance optimization and caching
|
|
1053
|
+
- [ ] CI/CD integration coverage in the top-level workflow
|
|
1054
|
+
|
|
1055
|
+
This design provides a comprehensive GitHub integration primitive that seamlessly supports both local development and cloud production environments, with automatic fallback capabilities and a clear migration path from existing tools.
|