@relayflows/github-primitive 0.1.0

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