Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Github/Database/.DS_Store
Jira/*
vendor
File renamed without changes.
70 changes: 70 additions & 0 deletions Console/InstallCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Modules\Github\Console;

use App\Misc\Helper;
use Illuminate\Console\Command;
use Nwidart\Modules\Facades\Module;

class InstallCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'freescout-github:install {--force : Force the operation to run when in production}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the database migrations for the FreeScout GitHub module.';

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$moduleName = 'Github';

$module = Module::find($moduleName);

if (!$module) {
$this->error('The GitHub module is not registered.');

return 1;
}

$this->info('Running migrations for the GitHub module...');

$parameters = ['module' => $moduleName];

$force = $this->option('force');

if ($force) {
$parameters['--force'] = true;
}

try {
$exitCode = $this->call('module:migrate', $parameters);
} catch (\Throwable $exception) {
$this->error('Running GitHub module migrations failed: '.$exception->getMessage());
Helper::logException($exception, '[GitHub] install command');

return 1;
}

if ($exitCode === 0) {
$this->info('GitHub module migrations completed successfully.');
} else {
$this->error('GitHub module migrations finished with errors.');
}

return $exitCode;
}
}

File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,42 @@
use Modules\Github\Entities\GithubIssue;
use Modules\Github\Entities\GithubLabelMapping;
use App\Conversation;
use Modules\Github\Support\RepositoryCache;

class GithubController extends Controller
{
/**
* Normalize request values to boolean for older Laravel versions.
*
* @param mixed $value
* @param bool $default
* @return bool
*/
private function toBoolean($value, bool $default = false): bool
{
if (is_null($value)) {
return $default;
}

if (is_bool($value)) {
return $value;
}

if (is_string($value)) {
$value = strtolower(trim($value));
if ($value === '') {
return $default;
}
return in_array($value, ['1', 'true', 'on', 'yes'], true);
}

if (is_numeric($value)) {
return (int) $value === 1;
}

return $default;
}

/**
* Test GitHub connection
*/
Expand Down Expand Up @@ -56,25 +89,33 @@ public function testConnection(Request $request)
public function getRepositories(Request $request)
{
try {
$result = GithubApiClient::getRepositories();

\Helper::log('github_debug', 'Repository fetch result: ' . json_encode([
'status' => $result['status'],
'data_count' => isset($result['data']) ? count($result['data']) : 'no data',
'message' => $result['message'] ?? 'no message'
]));

if ($this->toBoolean($request->input('refresh'))) {
RepositoryCache::clear();
}

$result = RepositoryCache::getRepositories(true);

if ($result['status'] === 'success') {
return response()->json([
'status' => 'success',
'repositories' => $result['data']
'repositories' => $result['repositories'],
'source' => $result['source'] ?? 'cache',
'fetched_at' => $result['fetched_at'] ?? null,
]);
} else {
}

if ($result['status'] === 'throttled') {
return response()->json([
'status' => 'error',
'message' => $result['message']
]);
'status' => 'throttled',
'message' => $result['message'],
'retry_after' => $result['retry_after'] ?? 60,
], 429);
}

return response()->json([
'status' => 'error',
'message' => $result['message'] ?? 'Failed to load repositories',
], 400);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Get Repositories Error');
return response()->json([
Expand All @@ -84,6 +125,89 @@ public function getRepositories(Request $request)
}
}

/**
* Refresh repository cache.
*/
public function refreshRepositories(): \Illuminate\Http\JsonResponse
{
try {
RepositoryCache::clear();

return response()->json([
'status' => 'success',
'message' => 'Repository cache cleared.',
]);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Refresh Repositories Error');

return response()->json([
'status' => 'error',
'message' => 'Failed to clear repository cache.',
], 500);
}
}

/**
* Search repositories with cache + throttled API fallback.
*/
public function searchRepositories(Request $request): \Illuminate\Http\JsonResponse
{
try {
$query = (string) $request->input('q', $request->input('term', ''));
$limit = (int) $request->input('limit', 20);

$result = RepositoryCache::search($query, $limit);

if ($result['status'] === 'success') {
$repositories = $result['repositories'];

$formatted = array_map(function ($repo) {
return [
'id' => $repo['full_name'],
'text' => $repo['full_name'],
'name' => $repo['name'] ?? $repo['full_name'],
'private' => $repo['private'] ?? false,
'has_issues' => $repo['has_issues'] ?? true,
];
}, $repositories);

return response()->json([
'status' => 'success',
'results' => $formatted,
'meta' => [
'count' => count($formatted),
'source' => $result['source'] ?? 'cache',
'fetched_at' => $result['fetched_at'] ?? null,
'throttled' => $result['throttled'] ?? false,
'retry_after' => $result['retry_after'] ?? null,
],
]);
}

if ($result['status'] === 'throttled') {
return response()->json([
'status' => 'throttled',
'message' => $result['message'],
'retry_after' => $result['retry_after'] ?? 60,
], 429);
}

$message = $result['message'] ?? 'Failed to search repositories.';

return response()->json([
'status' => 'error',
'message' => $message,
], 400);
} catch (\Exception $e) {
\Helper::logException($e, '[GitHub] Search Repositories Error');

return response()->json([
'status' => 'error',
'message' => 'Failed to search repositories: ' . $e->getMessage(),
], 500);
}
}

/**
* Get repository labels
*/
Expand Down Expand Up @@ -550,16 +674,39 @@ public function saveLabelMappings(Request $request)
{
$request->validate([
'repository' => 'required|string',
'mappings' => 'required|array',
'mappings.*.freescout_tag' => 'required|string',
'mappings.*.github_label' => 'required|string',
'mappings' => 'nullable|array',
'mappings.*.freescout_tag' => 'required_with:mappings|string',
'mappings.*.github_label' => 'required_with:mappings|string',
'mappings.*.confidence_threshold' => 'nullable|numeric|min:0|max:1'
]);

$repository = $request->get('repository');
$mappings = $request->get('mappings');
$mappings = collect($request->get('mappings', []))
->map(function ($mapping) {
return [
'freescout_tag' => trim($mapping['freescout_tag'] ?? ''),
'github_label' => trim($mapping['github_label'] ?? ''),
'confidence_threshold' => isset($mapping['confidence_threshold'])
? (float) $mapping['confidence_threshold']
: 0.80,
];
})
->filter(function ($mapping) {
return !empty($mapping['freescout_tag']) && !empty($mapping['github_label']);
})
->values();

try {
$activeTags = $mappings->pluck('freescout_tag')->unique()->all();

if (empty($activeTags)) {
GithubLabelMapping::where('repository', $repository)->delete();
} else {
GithubLabelMapping::where('repository', $repository)
->whereNotIn('freescout_tag', $activeTags)
->delete();
}

foreach ($mappings as $mapping) {
GithubLabelMapping::createOrUpdateMapping(
$mapping['freescout_tag'],
Expand All @@ -571,7 +718,8 @@ public function saveLabelMappings(Request $request)

return response()->json([
'status' => 'success',
'message' => 'Label mappings saved successfully'
'message' => 'Label mappings saved successfully',
'data' => GithubLabelMapping::getRepositoryMappings($repository)
]);

} catch (\Exception $e) {
Expand Down
2 changes: 2 additions & 0 deletions Github/Http/routes.php → Http/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
// Settings routes
Route::post('/github/test-connection', 'GithubController@testConnection')->name('github.test_connection');
Route::post('/github/repositories', 'GithubController@getRepositories')->name('github.repositories');
Route::get('/github/repositories/search', 'GithubController@searchRepositories')->name('github.repositories.search');
Route::post('/github/repositories/refresh', 'GithubController@refreshRepositories')->name('github.repositories.refresh');
Route::get('/github/labels/{repository}', 'GithubController@getLabels')->name('github.labels')->where('repository', '.*');
Route::post('/github/save-settings', 'GithubController@saveSettings')->name('github.save_settings');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Modules\Github\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Route;
use Modules\Github\Console\InstallCommand;

// Module alias
define('GITHUB_MODULE', 'github');
Expand All @@ -23,6 +23,10 @@ public function boot()
$this->loadMigrations();
$this->registerHooks();
$this->loadAssets();

if ($this->app->runningInConsole()) {
$this->registerCommands();
}
}

/**
Expand Down Expand Up @@ -88,14 +92,30 @@ protected function registerHooks()
{
// Add module's CSS file to the application layout
\Eventy::addFilter('stylesheets', function($styles) {
$styles[] = \Module::getPublicPath(GITHUB_MODULE).'/css/module.css';
$cssPath = \Module::getPublicPath(GITHUB_MODULE).'/css/module.css';
if (file_exists(public_path($cssPath))) {
$styles[] = $cssPath;
} else {
\Log::warning('[GitHub] Public CSS asset not found: '.public_path($cssPath));
}
return $styles;
});

// Add module's JS file to the application layout
\Eventy::addFilter('javascripts', function($javascripts) {
$javascripts[] = \Module::getPublicPath(GITHUB_MODULE).'/js/laroute.js';
$javascripts[] = \Module::getPublicPath(GITHUB_MODULE).'/js/module.js';
$jsFiles = [
\Module::getPublicPath(GITHUB_MODULE).'/js/laroute.js',
\Module::getPublicPath(GITHUB_MODULE).'/js/module.js',
];

foreach ($jsFiles as $jsPath) {
if (file_exists(public_path($jsPath))) {
$javascripts[] = $jsPath;
} else {
\Log::warning('[GitHub] Public JS asset not found: '.public_path($jsPath));
}
}

return $javascripts;
});

Expand Down Expand Up @@ -187,4 +207,16 @@ protected function loadAssets()
{
// Assets are loaded via Eventy filters in registerHooks()
}

/**
* Register Artisan commands provided by the module.
*
* @return void
*/
protected function registerCommands()
{
$this->commands([
InstallCommand::class,
]);
}
}
Loading