Skip to content

Commit b08922a

Browse files
docs(blog): add GitHub Issues integration article
Add English blog post documenting HagiCode's frontend-direct approach to GitHub Issues integration, covering OAuth flow, security design, and the synchronization data flow. Co-Authored-By: Hagicode <noreply@hagicode.com> Signed-off-by: newbe36524 <newbe36524@qq.com>
1 parent 079f98c commit b08922a

1 file changed

Lines changed: 344 additions & 0 deletions

File tree

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
---
2+
title: "GitHub Issues Integration"
3+
date: 2026-01-22
4+
truncate: true
5+
tags: [GitHub, GitHub Issues, OAuth, Frontend Architecture, HagiCode]
6+
description: "This article documents the full process of integrating GitHub Issues into the HagiCode platform. We explore how a frontend-direct plus minimal-backend architecture can deliver secure OAuth authentication and efficient Issue synchronization while keeping the backend lightweight."
7+
---
8+
9+
## Building GitHub Issues Integration from Scratch: HagiCode's Frontend-Direct Approach
10+
11+
> This article documents the full process of integrating GitHub Issues into the HagiCode platform. We explore how a "frontend-direct + minimal-backend" architecture can deliver secure OAuth authentication and efficient Issues synchronization while keeping the backend lightweight.
12+
13+
## Background: Why Integrate GitHub?
14+
15+
As an AI-assisted development platform, HagiCode's core value lies in connecting ideas with implementation. In real usage, however, we found that after users complete a Proposal in HagiCode, they often still need to manually copy the content into GitHub Issues for project tracking.
16+
17+
This creates several obvious pain points:
18+
19+
1. **Fragmented workflow**: Users have to switch back and forth between two systems. The experience is not only clumsy, but also makes it easy for critical information to get lost during copy and paste.
20+
2. **Inconvenient collaboration**: Other team members are used to reviewing tasks on GitHub and cannot directly see proposal progress inside HagiCode.
21+
3. **Duplicate work**: Every time a proposal is updated, someone has to manually update the corresponding Issue on GitHub, which adds unnecessary maintenance cost.
22+
23+
To solve this problem, we decided to introduce the **GitHub Issues Integration** feature, connecting HagiCode sessions with GitHub repositories and enabling "one-click sync."
24+
25+
## About HagiCode
26+
27+
> Hey, let us introduce what we're building.
28+
29+
We are building **HagiCode**, an AI-powered coding assistant designed to make development smarter, more convenient, and more fun.
30+
31+
**Smart**: AI assists throughout the process, from idea to code, multiplying development efficiency. **Convenient**: Multithreaded concurrent operations make full use of resources and keep the workflow smooth. **Fun**: Gamification and an achievement system make coding less tedious and much more rewarding.
32+
33+
The project is evolving quickly. If you're interested in technical writing, knowledge management, or AI-assisted development, come take a look on [GitHub](https://github.com/HagiCode-org/site).
34+
35+
---
36+
37+
## Architecture Choice: Frontend-Direct vs Backend Proxy
38+
39+
When designing the integration, we had two paths in front of us: the traditional "backend proxy" model and a more aggressive "frontend-direct" model.
40+
41+
### Comparing the Approaches
42+
43+
In the traditional **backend proxy model**, every request from the frontend must first pass through our backend, which then calls the GitHub API. This centralizes the logic, but it also puts a substantial burden on the backend:
44+
45+
1. **Bloated backend**: We would need a dedicated GitHub API client wrapper and would also have to handle OAuth's complex state machine.
46+
2. **Token risk**: The user's GitHub token would have to be stored in the backend database. Even if encrypted, that still increases the security exposure surface.
47+
3. **Development cost**: We would need database migrations to store tokens and an additional synchronization service to maintain.
48+
49+
The **frontend-direct model** is much lighter. In this approach, the backend is used only for the most sensitive "secret exchange" step, namely the OAuth callback. Once the token is obtained, it is stored directly in the browser's `localStorage`. After that, operations such as creating Issues or updating comments are sent straight from the frontend to GitHub over HTTP.
50+
51+
| Comparison Dimension | Backend Proxy Model | Frontend-Direct Model |
52+
| :--- | :--- | :--- |
53+
| **Backend complexity** | Requires a full OAuth service and GitHub API client | Only requires a single OAuth callback endpoint |
54+
| **Token management** | Must be encrypted and stored in the database, with leak risk | Stored in the browser and visible only to the user |
55+
| **Implementation cost** | Requires database migrations and multi-service development | Mostly frontend effort |
56+
| **User experience** | Unified logic, but potentially higher server latency | Extremely responsive, interacts directly with GitHub |
57+
58+
Because we wanted fast integration and minimal backend changes, **we ultimately chose the frontend-direct model**. It is like giving the browser a temporary pass. Once it has that pass, it can go handle business with GitHub on its own without asking the backend administrator for approval every time.
59+
60+
---
61+
62+
## Core Design: Data Flow and Security
63+
64+
Once the architecture was decided, we had to design the actual data flow. The heart of the synchronization process is safely obtaining the token and using it efficiently.
65+
66+
### Overall Architecture Diagram
67+
68+
The whole system can be abstracted into three roles: the browser (frontend), the HagiCode backend, and GitHub.
69+
70+
```text
71+
+--------------+ +--------------+ +--------------+
72+
| Frontend | | Backend | | GitHub |
73+
| React | | ASP.NET | | REST API |
74+
| | | | | |
75+
| +--------+ | | | | |
76+
| | OAuth |--+--------> /callback | | |
77+
| | Flow | | | | | |
78+
| +--------+ | | | | |
79+
| | | | | |
80+
| +--------+ | | +--------+ | | +--------+ |
81+
| | GitHub | +------------>Session | +----------> Issues | |
82+
| | API | | | |Metadata| | | | | |
83+
| | Direct | | | +--------+ | | +--------+ |
84+
| +--------+ | | | | |
85+
+--------------+ +--------------+ +--------------+
86+
```
87+
88+
**The key point is this**: only one small part of OAuth, exchanging the code for a token, needs to go through the backend. After that, the heavy lifting, such as creating the Issue, is handled directly between the frontend and GitHub.
89+
90+
### Detailed Synchronization Data Flow
91+
92+
When a user clicks the "Sync to GitHub" button in the HagiCode interface, a series of complex actions takes place:
93+
94+
```text
95+
User clicks "Sync to GitHub"
96+
97+
98+
1. Frontend checks localStorage for the GitHub token
99+
100+
101+
2. Format the Issue content (convert the Proposal into Markdown)
102+
103+
104+
3. Frontend directly calls the GitHub API to create or update the Issue
105+
106+
107+
4. Call the HagiCode backend API to update Session.metadata (store Issue URL and related info)
108+
109+
110+
5. Backend broadcasts a SessionUpdated event through SignalR
111+
112+
113+
6. Frontend receives the event and updates the UI to show the "Synced" state
114+
```
115+
116+
### Security Design
117+
118+
Security is always the top priority when integrating third-party services. We made the following considerations:
119+
120+
1. **CSRF protection**: During the OAuth redirect, we generate a random `state` parameter and store it in `sessionStorage`. On callback, we strictly validate the state to prevent forged requests.
121+
2. **Isolated token storage**: The token is stored only in the browser's `localStorage`. Thanks to the Same-Origin Policy, only HagiCode's scripts can read it, which avoids putting users at risk from a server-side database leak.
122+
3. **Error boundaries**: We designed dedicated error handling for common GitHub API failures, such as `401` for expired tokens, `422` for validation failures, and `429` for rate limits, so users receive clear and friendly feedback.
123+
124+
---
125+
126+
## In Practice: Implementation Details
127+
128+
Theory is one thing; implementation is another. Let's look at how the code comes together.
129+
130+
### 1. Minimal Backend Changes
131+
132+
The backend only needs to do two things: store synchronization information and handle the OAuth callback.
133+
134+
**Database change**
135+
We only need to add a `Metadata` column to the `Sessions` table to store JSON-formatted extension data.
136+
137+
```sql
138+
-- Add metadata column to Sessions table
139+
ALTER TABLE "Sessions" ADD COLUMN "Metadata" text NULL;
140+
```
141+
142+
**Entity and DTO definitions**
143+
144+
```csharp
145+
// src/HagiCode.DomainServices.Contracts/Entities/Session.cs
146+
public class Session : AuditedAggregateRoot<SessionId>
147+
{
148+
// ... other properties ...
149+
150+
/// <summary>
151+
/// JSON metadata for storing extension data like GitHub integration
152+
/// </summary>
153+
public string? Metadata { get; set; }
154+
}
155+
156+
// DTO definition for easier frontend serialization
157+
public class GitHubIssueMetadata
158+
{
159+
public required string Owner { get; set; }
160+
public required string Repo { get; set; }
161+
public int IssueNumber { get; set; }
162+
public required string IssueUrl { get; set; }
163+
public DateTime SyncedAt { get; set; }
164+
public string LastSyncStatus { get; set; } = "success";
165+
}
166+
167+
public class SessionMetadata
168+
{
169+
public GitHubIssueMetadata? GitHubIssue { get; set; }
170+
}
171+
```
172+
173+
### 2. Frontend OAuth Flow
174+
175+
This is the entry point of the connection. We use the standard Authorization Code Flow.
176+
177+
```typescript
178+
// src/HagiCode.Client/src/services/githubOAuth.ts
179+
180+
// Generate the authorization URL and redirect
181+
export async function generateAuthUrl(): Promise<string> {
182+
const state = generateRandomString(); // Generate a random string for CSRF protection
183+
sessionStorage.setItem('hagicode_github_state', state);
184+
185+
const params = new URLSearchParams({
186+
client_id: clientId,
187+
redirect_uri: window.location.origin + '/settings?tab=github&oauth=callback',
188+
scope: ['repo', 'public_repo'].join(' '),
189+
state: state,
190+
});
191+
192+
return `https://github.com/login/oauth/authorize?${params.toString()}`;
193+
}
194+
195+
// Exchange the code for a token on the callback page
196+
export async function exchangeCodeForToken(code: string, state: string): Promise<GitHubToken> {
197+
// 1. Validate the state to prevent CSRF
198+
const savedState = sessionStorage.getItem('hagicode_github_state');
199+
if (state !== savedState) throw new Error('Invalid state parameter');
200+
201+
// 2. Call the backend API to perform the token exchange
202+
// Note: this step must go through the backend because it requires ClientSecret,
203+
// which must not be exposed in the frontend
204+
const response = await fetch('/api/GitHubOAuth/callback', {
205+
method: 'POST',
206+
headers: { 'Content-Type': 'application/json' },
207+
body: JSON.stringify({ code, state, redirectUri: window.location.origin + '/settings?tab=github&oauth=callback' }),
208+
});
209+
210+
if (!response.ok) throw new Error('Failed to exchange token');
211+
212+
const token = await response.json();
213+
214+
// 3. Save it to LocalStorage
215+
saveToken(token);
216+
return token;
217+
}
218+
```
219+
220+
### 3. GitHub API Client Wrapper
221+
222+
Once we have the token, we need a reliable tool for calling the GitHub API.
223+
224+
```typescript
225+
// src/HagiCode.Client/src/services/githubApiClient.ts
226+
227+
const GITHUB_API_BASE = 'https://api.github.com';
228+
229+
// Core request wrapper
230+
async function githubApi<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
231+
const token = localStorage.getItem('gh_token');
232+
if (!token) throw new Error('Not connected to GitHub');
233+
234+
const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {
235+
...options,
236+
headers: {
237+
...options.headers,
238+
Authorization: `Bearer ${token}`,
239+
Accept: 'application/vnd.github.v3+json', // Specify the API version
240+
},
241+
});
242+
243+
// Error handling logic
244+
if (!response.ok) {
245+
if (response.status === 401) throw new Error('GitHub token expired, please reconnect');
246+
if (response.status === 403) throw new Error('No permission to access this repository or rate limit exceeded');
247+
if (response.status === 422) throw new Error('Issue validation failed, possibly due to a duplicate title');
248+
throw new Error(`GitHub API Error: ${response.statusText}`);
249+
}
250+
251+
return response.json();
252+
}
253+
254+
// Create an Issue
255+
export async function createIssue(owner: string, repo: string, data: { title: string, body: string, labels: string[] }) {
256+
return githubApi(`/repos/${owner}/${repo}/issues`, {
257+
method: 'POST',
258+
body: JSON.stringify(data),
259+
});
260+
}
261+
```
262+
263+
### 4. Content Formatting and Synchronization
264+
265+
The final step is converting HagiCode session data into the GitHub Issue format. It is a bit like doing translation work.
266+
267+
```typescript
268+
// Convert a Session object to a Markdown string
269+
function formatIssueForSession(session: Session): string {
270+
let content = `# ${session.title}\n\n`;
271+
content += `**> HagiCode Session:** #${session.code}\n`;
272+
content += `**> Status:** ${session.status}\n\n`;
273+
content += `## Description\n\n${session.description || 'No description provided.'}\n\n`;
274+
275+
// If this is a Proposal session, add extra fields
276+
if (session.type === 'proposal') {
277+
content += `## Chief Complaint\n\n${session.chiefComplaint || ''}\n\n`;
278+
// Add a deep link so users can jump back from GitHub to HagiCode
279+
content += `---\n\n**[View in HagiCode](hagicode://sessions/${session.id})**\n`;
280+
}
281+
282+
return content;
283+
}
284+
285+
// Main logic for clicking the sync button
286+
const handleSync = async (session: Session) => {
287+
try {
288+
const repoInfo = parseRepositoryFromUrl(session.repoUrl); // Parse the repository URL
289+
if (!repoInfo) throw new Error('Invalid repository URL');
290+
291+
toast.loading('Syncing to GitHub...');
292+
293+
// 1. Format the content
294+
const issueBody = formatIssueForSession(session);
295+
296+
// 2. Call the API
297+
const issue = await githubApiClient.createIssue(repoInfo.owner, repoInfo.repo, {
298+
title: `[HagiCode] ${session.title}`,
299+
body: issueBody,
300+
labels: ['hagicode', 'proposal', `status:${session.status}`],
301+
});
302+
303+
// 3. Update Session Metadata (store the Issue link)
304+
await SessionsService.patchApiSessionsSessionId(session.id, {
305+
metadata: {
306+
githubIssue: {
307+
owner: repoInfo.owner,
308+
repo: repoInfo.repo,
309+
issueNumber: issue.number,
310+
issueUrl: issue.html_url,
311+
syncedAt: new Date().toISOString(),
312+
}
313+
}
314+
});
315+
316+
toast.success('Sync successful!');
317+
} catch (err) {
318+
console.error(err);
319+
toast.error('Sync failed, please check the token or network');
320+
}
321+
};
322+
```
323+
324+
---
325+
326+
## Summary and Outlook
327+
328+
With this frontend-direct approach, we achieved seamless GitHub Issues integration with the smallest possible amount of backend code.
329+
330+
### What We Gained
331+
1. **High development efficiency**: Backend changes were minimal, mainly one extra database field and a simple OAuth callback endpoint, while most of the logic stayed in the frontend.
332+
2. **Strong security**: The token does not pass through the server database, which reduces the risk of leakage.
333+
3. **Better user experience**: Requests are initiated directly from the frontend, so responses are fast and do not need backend relay.
334+
335+
### Things to Watch Out For
336+
When deploying this in practice, there are a few pitfalls to keep in mind:
337+
- **OAuth App configuration**: Make sure the `Authorization callback URL` in your GitHub OAuth App settings is correct, usually `http://localhost:3000/settings?tab=github&oauth=callback`.
338+
- **Rate limits**: GitHub API limits unauthenticated requests heavily, but authenticated requests with a token are usually sufficient at 5000 requests per hour.
339+
- **URL parsing**: Repository URLs entered by users can come in many forms, so your regex should handle `.git` suffixes, SSH format, and similar cases.
340+
341+
### Future Enhancements
342+
The current feature still supports only one-way synchronization, from HagiCode to GitHub. In the future, we plan to implement two-way synchronization through GitHub Webhooks. For example, when an Issue is closed on GitHub, the session status in HagiCode could update automatically as well. That will require the backend to expose a webhook receiver endpoint, which is also the next interesting piece of work on our roadmap.
343+
344+
We hope this article gives you a few ideas for building your own third-party integrations. If you have questions, feel free to open an Issue on [HagiCode GitHub](https://github.com/HagiCode-org/site).

0 commit comments

Comments
 (0)