@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 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.