danwel - GitHub Integration

Project management integration with GitHub repositories, issues, and development workflow.

---

Table of Contents



1. [Overview](#overview)
2. [GitHub Provider](#github-provider)
3. [Repository Management](#repository-management)
4. [Issue Synchronization](#issue-synchronization)
5. [Webhook Integration](#webhook-integration)
6. [Entry Linking](#entry-linking)
7. [API Endpoints](#api-endpoints)
8. [Data Synchronization](#data-synchronization)
9. [Usage Examples](#usage-examples)

---

Overview

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.

Key Features



  • • **Repository Linking** - Connect danwel projects to GitHub repositories

  • • **Issue Synchronization** - Sync GitHub issues as danwel entries

  • • **Webhook Support** - Real-time updates from GitHub events

  • • **Time Tracking** - Track development time against specific issues

  • • **Status Mapping** - Bidirectional status updates between platforms

  • • **Label Synchronization** - Sync GitHub labels with danwel labels


  • Integration Scope



    The GitHub integration provides:
  • • **Project Provider** functionality for linking repositories

  • • **OAuth authentication** with GitHub

  • • **Webhook processing** for real-time updates

  • • **API sync services** for bulk operations
  • ---

    GitHub Provider



    Provider Implementation

    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
    ];
    }
    }


    OAuth Configuration



    // 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 Model



    // 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'
    ]
    ]
    ];

    ---

    Repository Management



    Repository Discovery

    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']
    ];
    });
    }


    Repository Linking

    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']);
    }


    Webhook Configuration

    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
    ]);
    }

    ---

    Issue Synchronization



    GitHub Issue Model

    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'
    ];


    Entry Creation from Issues



    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;
    }


    Status Mapping

    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'
    };
    }

    ---

    Webhook Integration



    Webhook Controller

    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);
    }
    }


    Issue Event Handling



    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);
    }


    Comment Synchronization



    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);
    }

    ---

    Entry Linking



    Manual Linking

    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']);
    }
    }
    }


    Automatic Discovery

    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']
    ]);
    }

    ---

    API Endpoints



    GitHub Integration Endpoints



    ``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);
    }
    }
    ``

    Creating Entry from Issue



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