Project management integration with GitHub repositories, issues, and development workflow.
------
The GitHub integration bridges development workflow with project management, allowing seamless connection between danwel projects and GitHub repositories. This enables automatic issue synchronization, time tracking for development work, and comprehensive project visibility.
---
The GitHub provider implements ProjectProviderInterface:
class GitHubProvider implements ProjectProviderInterface
{
private const AUTH_URL = 'https://github.com/login/oauth/authorize';
private const TOKEN_URL = 'https://github.com/login/oauth/access_token';
private const API_BASE = 'https://api.github.com';
public function getIdentifier(): string
{
return 'github';
}
public function getScopes(): array
{
return [
'repo', // Access repositories
'read:user', // Read user information
'read:org' // Read organization membership
];
}
}// config/services.php
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('GITHUB_REDIRECT_URI'),
'webhook_secret' => env('GITHUB_WEBHOOK_SECRET'),
],// Integration record for GitHub
$integration = [
'provider' => 'github',
'provider_type' => 'project',
'name' => 'MyOrg GitHub',
'provider_account_id' => '12345',
'provider_account_email' => 'user@example.com',
'settings' => [
'sync_issues' => true,
'sync_labels' => true,
'create_entries' => true,
'webhook_events' => [
'issues',
'issue_comment',
'label'
]
]
];---
After connecting GitHub, danwel discovers available repositories:
// Fetch user's repositories
public function getRepositories(): Collection
{
$response = Http::withToken($this->integration->access_token)
->get(self::API_BASE . '/user/repos', [
'type' => 'owner',
'sort' => 'updated',
'per_page' => 100
]);
return collect($response->json())->map(function ($repo) {
return [
'id' => $repo['id'],
'name' => $repo['name'],
'full_name' => $repo['full_name'],
'description' => $repo['description'],
'private' => $repo['private'],
'html_url' => $repo['html_url'],
'updated_at' => $repo['updated_at']
];
});
}Projects can be linked to GitHub repositories:
// Link project to GitHub repository
public function linkRepository(Project $project, array $repoData): void
{
$project->update([
'integration_id' => $this->integration->id,
'external_id' => $repoData['id'],
'settings' => array_merge($project->settings ?? [], [
'github_repo' => $repoData['full_name'],
'github_url' => $repoData['html_url'],
])
]);
// Setup webhook for this repository
$this->setupRepositoryWebhook($repoData['full_name']);
}Webhooks are automatically configured when linking repositories:
public function setupRepositoryWebhook(string $repoFullName): void
{
$webhookUrl = route('webhooks.github', $this->integration);
Http::withToken($this->integration->access_token)
->post(self::API_BASE . "/repos/{$repoFullName}/hooks", [
'name' => 'web',
'config' => [
'url' => $webhookUrl,
'content_type' => 'json',
'secret' => config('services.github.webhook_secret')
],
'events' => [
'issues',
'issue_comment',
'label'
],
'active' => true
]);
}---
Issues are synchronized to danwel entries:
// GitHub issue structure
$githubIssue = [
'id' => 123456,
'number' => 42,
'title' => 'Add user authentication',
'body' => 'Implement OAuth and 2FA support',
'state' => 'open',
'labels' => [
['name' => 'feature', 'color' => '0e8a16'],
['name' => 'priority:high', 'color' => 'd93f0b']
],
'assignee' => ['login' => 'username'],
'created_at' => '2024-01-01T00:00:00Z',
'updated_at' => '2024-01-01T10:00:00Z',
'html_url' => 'https://github.com/org/repo/issues/42'
];public function createEntryFromIssue(array $issueData, Project $project): Entry
{
$entry = Entry::create([
'organization_id' => $project->organization_id,
'project_id' => $project->id,
'created_by' => auth()->id(),
'title' => $issueData['title'],
'description' => $issueData['body'],
'status' => $this->mapGitHubStatusToEntryStatus($issueData['state']),
'github_issue_number' => $issueData['number'],
'github_issue_url' => $issueData['html_url'],
'external_id' => $issueData['id']
]);
// Sync labels
$this->syncLabelsFromIssue($entry, $issueData['labels']);
return $entry;
}GitHub states map to danwel entry statuses:
public function mapGitHubStatusToEntryStatus(string $githubState): string
{
return match ($githubState) {
'open' => 'todo',
'closed' => 'done',
default => 'todo'
};
}
public function mapEntryStatusToGitHubState(string $entryStatus): string
{
return match ($entryStatus) {
'todo', 'in_progress', 'review', 'blocked' => 'open',
'done' => 'closed',
default => 'open'
};
}---
GitHub webhooks are handled by a dedicated controller:
class GitHubWebhookController extends Controller
{
public function handle(Request $request, Integration $integration)
{
// Verify webhook signature
if (!$this->verifySignature($request, $integration)) {
abort(403, 'Invalid webhook signature');
}
$event = $request->header('X-GitHub-Event');
$payload = $request->json()->all();
// Dispatch to appropriate handler
return match ($event) {
'issues' => $this->handleIssueEvent($payload, $integration),
'issue_comment' => $this->handleCommentEvent($payload, $integration),
'label' => $this->handleLabelEvent($payload, $integration),
default => response('Webhook event not supported', 200)
};
}
private function verifySignature(Request $request, Integration $integration): bool
{
$signature = $request->header('X-Hub-Signature-256');
$payload = $request->getContent();
$secret = config('services.github.webhook_secret'); $expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($signature, $expectedSignature);
}
}public function handleIssueEvent(array $payload, Integration $integration): Response
{
$action = $payload['action'];
$issue = $payload['issue'];
$repository = $payload['repository'];
// Find linked project
$project = Project::where('integration_id', $integration->id)
->where('external_id', $repository['id'])
->first();
if (!$project) {
return response('Repository not linked', 200);
}
switch ($action) {
case 'opened':
$this->createEntryFromIssue($issue, $project);
break;
case 'edited':
case 'labeled':
case 'unlabeled':
$this->updateExistingEntry($issue, $project);
break;
case 'closed':
case 'reopened':
$this->updateEntryStatus($issue, $project);
break;
}
return response('Webhook processed', 200);
}public function handleCommentEvent(array $payload, Integration $integration): Response
{
$action = $payload['action'];
$comment = $payload['comment'];
$issue = $payload['issue'];
// Find corresponding entry
$entry = Entry::where('github_issue_number', $issue['number'])
->whereHas('project', function ($query) use ($integration) {
$query->where('integration_id', $integration->id);
})
->first();
if (!$entry) {
return response('Entry not found', 200);
}
if ($action === 'created') {
// Create comment in danwel
EntryComment::create([
'entry_id' => $entry->id,
'user_id' => $this->findOrCreateGitHubUser($comment['user']),
'content' => $comment['body'],
'external_id' => $comment['id'],
'created_at' => $comment['created_at']
]);
}
return response('Comment processed', 200);
}---
Users can manually link danwel entries to GitHub issues:
// Link entry to GitHub issue
public function linkToGitHubIssue(Entry $entry, string $issueUrl): void
{
// Parse GitHub issue URL
if (preg_match('#github\.com/([^/]+)/([^/]+)/issues/(\d+)#', $issueUrl, $matches)) {
$owner = $matches[1];
$repo = $matches[2];
$issueNumber = $matches[3];
// Fetch issue data
$issueData = $this->fetchGitHubIssue($owner, $repo, $issueNumber);
if ($issueData) {
$entry->update([
'github_issue_number' => $issueNumber,
'github_issue_url' => $issueUrl,
'external_id' => $issueData['id']
]);
// Sync labels and other metadata
$this->syncLabelsFromIssue($entry, $issueData['labels']);
}
}
}The system can discover and suggest GitHub issues for linking:
public function suggestGitHubIssues(Project $project): Collection
{
if (!$project->integration || $project->integration->provider !== 'github') {
return collect();
}
$repoFullName = $project->getSetting('github_repo');
if (!$repoFullName) {
return collect();
}
// Fetch recent issues
$response = Http::withToken($project->integration->access_token)
->get(self::API_BASE . "/repos/{$repoFullName}/issues", [
'state' => 'open',
'sort' => 'updated',
'per_page' => 50
]);
// Filter out issues already linked
$linkedIssueNumbers = Entry::where('project_id', $project->id)
->whereNotNull('github_issue_number')
->pluck('github_issue_number')
->toArray();
return collect($response->json())
->reject(fn($issue) => in_array($issue['number'], $linkedIssueNumbers))
->map(fn($issue) => [
'number' => $issue['number'],
'title' => $issue['title'],
'url' => $issue['html_url'],
'labels' => array_column($issue['labels'], 'name'),
'updated_at' => $issue['updated_at']
]);
}---
http
Repository management
GET /api/integrations/{integration}/github-repositories
POST /api/integrations/{integration}/github-link-repo
DELETE /api/integrations/{integration}/github-unlink-repo
Data access
GET /api/integrations/{integration}/github-data
GET /api/projects/{project}/github-issues
POST /api/entries/{entry}/link-github-issue
Webhook endpoint
POST /webhooks/github/{integration}
Example API Responses
**List Repositories:**
json
{
"repositories": [
{
"id": 123456,
"name": "web-app",
"full_name": "myorg/web-app",
"description": "Main web application",
"private": true,
"html_url": "https://github.com/myorg/web-app",
"updated_at": "2024-01-01T12:00:00Z",
"is_linked": true
}
]
}
**GitHub Integration Data:**
json
{
"account": {
"login": "myorg",
"name": "My Organization",
"avatar_url": "https://avatars.githubusercontent.com/u/12345"
},
"repositories_count": 15,
"linked_repositories": [
{
"project_name": "Web App v2",
"repo_name": "myorg/web-app",
"issues_count": 23,
"linked_entries": 12
}
]
}
---
Data Synchronization
Sync Service
A dedicated sync service manages GitHub data synchronization:
class GitHubSyncService
{
public function syncRepository(Integration $integration, Project $project): void
{
$repoFullName = $project->getSetting('github_repo');
if (!$repoFullName) return;
// Sync issues
$this->syncIssues($integration, $project, $repoFullName);
// Sync labels
$this->syncLabels($integration, $repoFullName);
}
private function syncIssues(Integration $integration, Project $project, string $repo): void
{
$issues = $this->fetchAllIssues($integration, $repo);
foreach ($issues as $issueData) {
$existingEntry = Entry::where('project_id', $project->id)
->where('github_issue_number', $issueData['number'])
->first();
if ($existingEntry) {
$this->updateEntryFromIssue($existingEntry, $issueData);
} elseif ($integration->getSetting('create_entries', true)) {
$this->createEntryFromIssue($issueData, $project);
}
}
}
}
Scheduled Synchronization
// Console command for periodic sync
class SyncGitHubData extends Command
{
protected $signature = 'github:sync {--integration=}';
public function handle(GitHubSyncService $syncService): void
{
$integrations = Integration::where('provider', 'github')
->where('is_active', true)
->when($this->option('integration'), function ($query, $id) {
$query->where('id', $id);
})
->get();
foreach ($integrations as $integration) {
$projects = Project::where('integration_id', $integration->id)->get();
foreach ($projects as $project) {
$syncService->syncRepository($integration, $project);
$this->info("Synced {$project->name}");
}
}
}
}
---
Usage Examples
Connecting GitHub
// Initiate GitHub connection
Route::post('/integrations/github/connect', function () {
$provider = app(GitHubProvider::class);
$authUrl = $provider->getAuthorizationUrl(session('state'));
return redirect($authUrl);
});
// Handle OAuth callback
Route::get('/integrations/github/callback', function (Request $request) {
$provider = app(GitHubProvider::class);
$tokenData = $provider->exchangeCodeForToken($request->code);
// Create integration record
$integration = Integration::create([
'organization_id' => auth()->user()->current_organization_id,
'connected_by' => auth()->id(),
'provider' => 'github',
'provider_type' => 'project',
'access_token' => $tokenData->accessToken,
'provider_account_id' => $tokenData->metadata['user_id'],
'provider_account_email' => $tokenData->metadata['email'],
]);
return redirect()->route('settings.integrations')
->with('success', 'GitHub connected successfully');
});
Linking Repository
javascript
// Frontend: Link project to GitHub repository
async function linkGitHubRepo(projectId, repoData) {
const response = await fetch(/api/integrations/${integrationId}/github-link-repo, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
project_id: projectId,
repository: repoData
})
});
if (response.ok) {
alert('Repository linked successfully');
// Refresh project data
loadProjectData(projectId);
}
}
``// API endpoint to import GitHub issue as entry
Route::post('/api/github/import-issue', function (Request $request) {
$issueUrl = $request->input('issue_url');
$projectId = $request->input('project_id');
$project = Project::find($projectId);
if (!$project->integration || $project->integration->provider !== 'github') {
return response()->json(['error' => 'Project not linked to GitHub'], 400);
}
$provider = app(GitHubProvider::class);
$provider->setIntegration($project->integration);
// Parse and fetch issue
$issueData = $provider->parseAndFetchIssue($issueUrl);
if ($issueData) {
$entry = $provider->createEntryFromIssue($issueData, $project);
return response()->json(['entry' => $entry], 201);
}
return response()->json(['error' => 'Could not fetch issue'], 400);
});---
This GitHub integration provides seamless connection between development workflow and project management, enabling teams to track development time accurately and maintain visibility across both platforms.