@flink-app/github-app-plugin 0.12.1-alpha.40 → 0.12.1-alpha.42
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/README.md +53 -0
- package/dist/services/GitHubAPIClient.d.ts +21 -0
- package/dist/services/GitHubAPIClient.js +27 -0
- package/package.json +2 -2
- package/spec/services.spec.ts +166 -0
- package/src/services/GitHubAPIClient.ts +29 -0
package/README.md
CHANGED
|
@@ -417,10 +417,63 @@ const issue = await client.createIssue("facebook", "react", {
|
|
|
417
417
|
body: "Found a bug...",
|
|
418
418
|
});
|
|
419
419
|
|
|
420
|
+
// Remove repository from installation
|
|
421
|
+
await client.removeRepository(987654321);
|
|
422
|
+
|
|
420
423
|
// Generic API call
|
|
421
424
|
const response = await client.request("GET", "/rate_limit");
|
|
422
425
|
```
|
|
423
426
|
|
|
427
|
+
### Removing Repositories from Installation
|
|
428
|
+
|
|
429
|
+
You can remove a repository from a GitHub App installation to revoke access to that specific repository while keeping the installation active for other repositories:
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
const client = await ctx.plugins.githubApp.getClient(installationId);
|
|
433
|
+
|
|
434
|
+
// Remove repository by ID (numeric ID, not name)
|
|
435
|
+
await client.removeRepository(987654321);
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Important Notes:**
|
|
439
|
+
- Use the numeric repository ID, not the repository name or full_name
|
|
440
|
+
- This only removes the repository from the installation; it does not delete the repository itself
|
|
441
|
+
- The installation remains active for other repositories
|
|
442
|
+
- If the repository was already removed manually, the API will return a 404 error
|
|
443
|
+
- Requires appropriate installation permissions
|
|
444
|
+
|
|
445
|
+
**Example: Repository Disconnect Handler**
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
// In a Flink handler (e.g., DeleteRepository.ts)
|
|
449
|
+
const DeleteRepository: Handler = async ({ ctx, req }) => {
|
|
450
|
+
const { id } = req.params;
|
|
451
|
+
|
|
452
|
+
// Fetch repository from database
|
|
453
|
+
const repository = await ctx.repos.repositoryRepo.getById(id);
|
|
454
|
+
if (!repository) {
|
|
455
|
+
return notFound(`Repository with id ${id} not found`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Remove repository from GitHub App installation
|
|
459
|
+
try {
|
|
460
|
+
const client = await ctx.plugins.githubApp.getClient(
|
|
461
|
+
repository.githubInstallationId
|
|
462
|
+
);
|
|
463
|
+
await client.removeRepository(repository.githubRepoId);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
console.error('Failed to remove repository from GitHub:', error);
|
|
466
|
+
// Continue with local cleanup even if GitHub API call fails
|
|
467
|
+
// (repository may have already been removed manually)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Update database status
|
|
471
|
+
await ctx.repos.repositoryRepo.updateStatus(id, "disconnected");
|
|
472
|
+
|
|
473
|
+
return { data: { success: true } };
|
|
474
|
+
};
|
|
475
|
+
```
|
|
476
|
+
|
|
424
477
|
## Authentication Integration
|
|
425
478
|
|
|
426
479
|
This plugin is **auth-agnostic** and works with any authentication system. You implement your own installation callback handler with your own auth logic.
|
|
@@ -125,6 +125,27 @@ export declare class GitHubAPIClient {
|
|
|
125
125
|
* @returns Created issue
|
|
126
126
|
*/
|
|
127
127
|
createIssue(owner: string, repo: string, params: CreateIssueParams): Promise<Issue>;
|
|
128
|
+
/**
|
|
129
|
+
* Remove a repository from this installation
|
|
130
|
+
*
|
|
131
|
+
* Revokes the GitHub App's access to a specific repository while keeping
|
|
132
|
+
* the installation active for other repositories.
|
|
133
|
+
*
|
|
134
|
+
* Note: This requires the installation to have been granted access to the
|
|
135
|
+
* repository. If the app was installed at the account level with access
|
|
136
|
+
* to all repositories, this method can be used to selectively revoke
|
|
137
|
+
* access to specific repositories.
|
|
138
|
+
*
|
|
139
|
+
* @param repositoryId - GitHub repository ID (numeric ID, not full_name)
|
|
140
|
+
* @throws Error if repository not found or access cannot be revoked
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const client = await ctx.plugins.githubApp.getClient(12345);
|
|
145
|
+
* await client.removeRepository(987654321);
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
removeRepository(repositoryId: number): Promise<void>;
|
|
128
149
|
/**
|
|
129
150
|
* Sleep utility for retry delays
|
|
130
151
|
*
|
|
@@ -76,6 +76,10 @@ class GitHubAPIClient {
|
|
|
76
76
|
error.message = errorBody;
|
|
77
77
|
throw error;
|
|
78
78
|
}
|
|
79
|
+
// Handle 204 No Content (void response)
|
|
80
|
+
if (response.status === 204) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
79
83
|
// Parse and return response
|
|
80
84
|
const responseData = await response.json();
|
|
81
85
|
return responseData;
|
|
@@ -132,6 +136,29 @@ class GitHubAPIClient {
|
|
|
132
136
|
async createIssue(owner, repo, params) {
|
|
133
137
|
return this.request("POST", `/repos/${owner}/${repo}/issues`, params);
|
|
134
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Remove a repository from this installation
|
|
141
|
+
*
|
|
142
|
+
* Revokes the GitHub App's access to a specific repository while keeping
|
|
143
|
+
* the installation active for other repositories.
|
|
144
|
+
*
|
|
145
|
+
* Note: This requires the installation to have been granted access to the
|
|
146
|
+
* repository. If the app was installed at the account level with access
|
|
147
|
+
* to all repositories, this method can be used to selectively revoke
|
|
148
|
+
* access to specific repositories.
|
|
149
|
+
*
|
|
150
|
+
* @param repositoryId - GitHub repository ID (numeric ID, not full_name)
|
|
151
|
+
* @throws Error if repository not found or access cannot be revoked
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* const client = await ctx.plugins.githubApp.getClient(12345);
|
|
156
|
+
* await client.removeRepository(987654321);
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
async removeRepository(repositoryId) {
|
|
160
|
+
await this.request("DELETE", `/user/installations/${this.installationId}/repositories/${repositoryId}`);
|
|
161
|
+
}
|
|
135
162
|
/**
|
|
136
163
|
* Sleep utility for retry delays
|
|
137
164
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/github-app-plugin",
|
|
3
|
-
"version": "0.12.1-alpha.
|
|
3
|
+
"version": "0.12.1-alpha.42",
|
|
4
4
|
"description": "Flink plugin for GitHub App integration with installation management and webhook handling",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --preserve-symlinks -r ts-node/register -- node_modules/jasmine/bin/jasmine --config=./spec/support/jasmine.json",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"tsc-watch": "^4.2.9",
|
|
38
38
|
"typescript": "5.4.5"
|
|
39
39
|
},
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "e7a3e35c85ee9cd1eaa9629664460a1936f0698a"
|
|
41
41
|
}
|
package/spec/services.spec.ts
CHANGED
|
@@ -52,6 +52,172 @@ describe("Services", () => {
|
|
|
52
52
|
expect(apiClient).toBeTruthy();
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
describe("removeRepository", () => {
|
|
56
|
+
it("should construct correct DELETE request", () => {
|
|
57
|
+
const appId = "123456";
|
|
58
|
+
const installationId = 12345;
|
|
59
|
+
const repositoryId = 987654321;
|
|
60
|
+
const authService = new GitHubAuthService(appId, testPrivateKeyBase64, "https://api.github.com");
|
|
61
|
+
const apiClient = new GitHubAPIClient(installationId, authService);
|
|
62
|
+
|
|
63
|
+
// Test that the method exists and has the correct signature
|
|
64
|
+
expect(typeof apiClient.removeRepository).toBe("function");
|
|
65
|
+
expect(apiClient.removeRepository.length).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Note: Testing actual API calls with mocked responses
|
|
69
|
+
it("should handle successful removal (204 No Content)", async () => {
|
|
70
|
+
const appId = "123456";
|
|
71
|
+
const installationId = 12345;
|
|
72
|
+
const repositoryId = 987654321;
|
|
73
|
+
const authService = new GitHubAuthService(appId, testPrivateKeyBase64, "https://api.github.com");
|
|
74
|
+
const apiClient = new GitHubAPIClient(installationId, authService);
|
|
75
|
+
|
|
76
|
+
// Mock successful installation token retrieval
|
|
77
|
+
spyOn(authService, "getInstallationToken").and.returnValue(Promise.resolve("test-token"));
|
|
78
|
+
|
|
79
|
+
// Mock fetch to return 204 No Content
|
|
80
|
+
const originalFetch = global.fetch;
|
|
81
|
+
global.fetch = jasmine.createSpy("fetch").and.returnValue(
|
|
82
|
+
Promise.resolve({
|
|
83
|
+
ok: true,
|
|
84
|
+
status: 204,
|
|
85
|
+
headers: new Headers(),
|
|
86
|
+
} as Response)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await apiClient.removeRepository(repositoryId);
|
|
91
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
92
|
+
`https://api.github.com/user/installations/${installationId}/repositories/${repositoryId}`,
|
|
93
|
+
jasmine.objectContaining({
|
|
94
|
+
method: "DELETE",
|
|
95
|
+
headers: jasmine.objectContaining({
|
|
96
|
+
Authorization: "Bearer test-token",
|
|
97
|
+
}),
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
} finally {
|
|
101
|
+
global.fetch = originalFetch;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should handle repository not found error (404)", async () => {
|
|
106
|
+
const appId = "123456";
|
|
107
|
+
const installationId = 12345;
|
|
108
|
+
const repositoryId = 999999999;
|
|
109
|
+
const authService = new GitHubAuthService(appId, testPrivateKeyBase64, "https://api.github.com");
|
|
110
|
+
const apiClient = new GitHubAPIClient(installationId, authService);
|
|
111
|
+
|
|
112
|
+
// Mock installation token retrieval
|
|
113
|
+
spyOn(authService, "getInstallationToken").and.returnValue(Promise.resolve("test-token"));
|
|
114
|
+
|
|
115
|
+
// Mock fetch to return 404
|
|
116
|
+
const originalFetch = global.fetch;
|
|
117
|
+
global.fetch = jasmine.createSpy("fetch").and.returnValue(
|
|
118
|
+
Promise.resolve({
|
|
119
|
+
ok: false,
|
|
120
|
+
status: 404,
|
|
121
|
+
statusText: "Not Found",
|
|
122
|
+
text: () => Promise.resolve(JSON.stringify({ message: "Repository not found" })),
|
|
123
|
+
headers: new Headers(),
|
|
124
|
+
} as any)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
let errorThrown = false;
|
|
129
|
+
try {
|
|
130
|
+
await apiClient.removeRepository(repositoryId);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
errorThrown = true;
|
|
133
|
+
}
|
|
134
|
+
expect(errorThrown).toBe(true);
|
|
135
|
+
} finally {
|
|
136
|
+
global.fetch = originalFetch;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should handle forbidden error (403)", async () => {
|
|
141
|
+
const appId = "123456";
|
|
142
|
+
const installationId = 12345;
|
|
143
|
+
const repositoryId = 987654321;
|
|
144
|
+
const authService = new GitHubAuthService(appId, testPrivateKeyBase64, "https://api.github.com");
|
|
145
|
+
const apiClient = new GitHubAPIClient(installationId, authService);
|
|
146
|
+
|
|
147
|
+
// Mock installation token retrieval
|
|
148
|
+
spyOn(authService, "getInstallationToken").and.returnValue(Promise.resolve("test-token"));
|
|
149
|
+
|
|
150
|
+
let callCount = 0;
|
|
151
|
+
// Mock fetch to return 403 and track retries
|
|
152
|
+
const originalFetch = global.fetch;
|
|
153
|
+
global.fetch = jasmine.createSpy("fetch").and.callFake(() => {
|
|
154
|
+
callCount++;
|
|
155
|
+
return Promise.resolve({
|
|
156
|
+
ok: false,
|
|
157
|
+
status: 403,
|
|
158
|
+
statusText: "Forbidden",
|
|
159
|
+
text: () => Promise.resolve(JSON.stringify({ message: "Insufficient permissions" })),
|
|
160
|
+
headers: new Headers({ "x-ratelimit-remaining": "100" }),
|
|
161
|
+
} as any);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
let errorThrown = false;
|
|
166
|
+
try {
|
|
167
|
+
await apiClient.removeRepository(repositoryId);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
errorThrown = true;
|
|
170
|
+
}
|
|
171
|
+
expect(errorThrown).toBe(true);
|
|
172
|
+
// Should have retried due to 403 status
|
|
173
|
+
expect(callCount).toBeGreaterThan(1);
|
|
174
|
+
} finally {
|
|
175
|
+
global.fetch = originalFetch;
|
|
176
|
+
}
|
|
177
|
+
}, 10000); // Increase timeout since it will retry
|
|
178
|
+
|
|
179
|
+
it("should retry on rate limit (403 with rate limit)", async () => {
|
|
180
|
+
const appId = "123456";
|
|
181
|
+
const installationId = 12345;
|
|
182
|
+
const repositoryId = 987654321;
|
|
183
|
+
const authService = new GitHubAuthService(appId, testPrivateKeyBase64, "https://api.github.com");
|
|
184
|
+
const apiClient = new GitHubAPIClient(installationId, authService);
|
|
185
|
+
|
|
186
|
+
// Mock installation token retrieval
|
|
187
|
+
spyOn(authService, "getInstallationToken").and.returnValue(Promise.resolve("test-token"));
|
|
188
|
+
|
|
189
|
+
let callCount = 0;
|
|
190
|
+
const originalFetch = global.fetch;
|
|
191
|
+
global.fetch = jasmine.createSpy("fetch").and.callFake(() => {
|
|
192
|
+
callCount++;
|
|
193
|
+
if (callCount === 1) {
|
|
194
|
+
// First call: rate limited
|
|
195
|
+
return Promise.resolve({
|
|
196
|
+
ok: false,
|
|
197
|
+
status: 403,
|
|
198
|
+
statusText: "Forbidden",
|
|
199
|
+
text: () => Promise.resolve(JSON.stringify({ message: "Rate limit exceeded" })),
|
|
200
|
+
headers: new Headers({ "x-ratelimit-remaining": "0" }),
|
|
201
|
+
} as Response);
|
|
202
|
+
} else {
|
|
203
|
+
// Second call: success
|
|
204
|
+
return Promise.resolve({
|
|
205
|
+
ok: true,
|
|
206
|
+
status: 204,
|
|
207
|
+
headers: new Headers(),
|
|
208
|
+
} as Response);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await apiClient.removeRepository(repositoryId);
|
|
214
|
+
expect(callCount).toBe(2); // Should have retried
|
|
215
|
+
} finally {
|
|
216
|
+
global.fetch = originalFetch;
|
|
217
|
+
}
|
|
218
|
+
}, 10000); // Increase timeout for retry delays
|
|
219
|
+
});
|
|
220
|
+
|
|
55
221
|
// Note: Testing actual API calls requires mocking GitHub API
|
|
56
222
|
// These are tested in integration tests instead
|
|
57
223
|
});
|
|
@@ -145,6 +145,11 @@ export class GitHubAPIClient {
|
|
|
145
145
|
throw error;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// Handle 204 No Content (void response)
|
|
149
|
+
if (response.status === 204) {
|
|
150
|
+
return undefined as T;
|
|
151
|
+
}
|
|
152
|
+
|
|
148
153
|
// Parse and return response
|
|
149
154
|
const responseData = await response.json();
|
|
150
155
|
return responseData as T;
|
|
@@ -206,6 +211,30 @@ export class GitHubAPIClient {
|
|
|
206
211
|
return this.request<Issue>("POST", `/repos/${owner}/${repo}/issues`, params);
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Remove a repository from this installation
|
|
216
|
+
*
|
|
217
|
+
* Revokes the GitHub App's access to a specific repository while keeping
|
|
218
|
+
* the installation active for other repositories.
|
|
219
|
+
*
|
|
220
|
+
* Note: This requires the installation to have been granted access to the
|
|
221
|
+
* repository. If the app was installed at the account level with access
|
|
222
|
+
* to all repositories, this method can be used to selectively revoke
|
|
223
|
+
* access to specific repositories.
|
|
224
|
+
*
|
|
225
|
+
* @param repositoryId - GitHub repository ID (numeric ID, not full_name)
|
|
226
|
+
* @throws Error if repository not found or access cannot be revoked
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* const client = await ctx.plugins.githubApp.getClient(12345);
|
|
231
|
+
* await client.removeRepository(987654321);
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
async removeRepository(repositoryId: number): Promise<void> {
|
|
235
|
+
await this.request<void>("DELETE", `/user/installations/${this.installationId}/repositories/${repositoryId}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
209
238
|
/**
|
|
210
239
|
* Sleep utility for retry delays
|
|
211
240
|
*
|