diff --git a/.env.example b/.env.example index ef8f534..2d5f4ff 100644 --- a/.env.example +++ b/.env.example @@ -101,3 +101,6 @@ AGENT_TIMEOUT=30 # Mono Service Configuration MONO_URL=http://localhost:8081 MONO_TIMEOUT=30 + +# Git Monitor Configuration +GIT_MONITOR_PROJECTS="service,portal-be,agent-be" diff --git a/app/Console/Commands/EnvCommand.php b/app/Console/Commands/EnvCommand.php index abf8d27..c0f551b 100644 --- a/app/Console/Commands/EnvCommand.php +++ b/app/Console/Commands/EnvCommand.php @@ -18,16 +18,15 @@ class EnvCommand extends Command private EnvService $envManager; - public function __construct(EnvService $envManager) + public function __construct() { parent::__construct(); - $this->envManager = $envManager; } public function handle(): int { $action = $this->argument('action'); - + $this->envManager ??= app(EnvService::class); try { switch ($action) { case 'list': @@ -93,7 +92,7 @@ class EnvCommand extends Command private function listEnvironments(): int { $project = $this->option('project'); - + if (!$project) { $this->error('请指定项目名称: --project=项目名'); return 1; @@ -136,7 +135,7 @@ class EnvCommand extends Command if ($this->confirm("确定要将 {$environment} 环境应用到项目 {$project} 吗?")) { $success = $this->envManager->applyEnv($project, $environment); - + if ($success) { $this->info("成功将 {$environment} 环境应用到项目 {$project}"); return 0; @@ -178,7 +177,7 @@ class EnvCommand extends Command } $success = $this->envManager->saveEnv($project, $environment, $content); - + if ($success) { $this->info("成功保存环境配置 {$project}/{$environment}"); return 0; @@ -202,7 +201,7 @@ class EnvCommand extends Command } $success = $this->envManager->importFromProject($project, $environment); - + if ($success) { $this->info("成功从项目 {$project} 导入环境配置为 {$environment}"); return 0; @@ -227,7 +226,7 @@ class EnvCommand extends Command if ($this->confirm("确定要删除环境配置 {$project}/{$environment} 吗?")) { $success = $this->envManager->deleteEnv($project, $environment); - + if ($success) { $this->info("成功删除环境配置 {$project}/{$environment}"); return 0; @@ -359,9 +358,9 @@ class EnvCommand extends Command $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); - + $bytes /= pow(1024, $pow); - + return round($bytes, 2) . ' ' . $units[$pow]; } } diff --git a/app/Console/Commands/GitMonitorCacheCommand.php b/app/Console/Commands/GitMonitorCacheCommand.php new file mode 100644 index 0000000..e503175 --- /dev/null +++ b/app/Console/Commands/GitMonitorCacheCommand.php @@ -0,0 +1,28 @@ +refreshReleaseCache(true); + + if (empty($cache)) { + $this->warn('未获取到任何 release 版本信息,请检查配置。'); + return; + } + + $this->info(sprintf( + '已缓存 %d 个仓库的 release 分支信息。', + count($cache['repositories'] ?? []) + )); + } +} diff --git a/app/Console/Commands/GitMonitorCheckCommand.php b/app/Console/Commands/GitMonitorCheckCommand.php new file mode 100644 index 0000000..8626ca6 --- /dev/null +++ b/app/Console/Commands/GitMonitorCheckCommand.php @@ -0,0 +1,48 @@ +option('force-cache')) { + $monitor->refreshReleaseCache(true); + } else { + $monitor->ensureReleaseCache(); + } + + $results = $monitor->checkRepositories(false); + + foreach ($results as $repo => $result) { + if (isset($result['error'])) { + $this->error(sprintf('[%s] %s', $repo, $result['error'])); + continue; + } + + $this->line(sprintf( + '[%s] 分支 %s 已对齐 %s,扫描 %d 个提交。', + $repo, + $result['branch'], + $result['head'], + $result['commits_scanned'] + )); + + if (!empty($result['issues']['develop_merges'])) { + $this->warn(sprintf(' - 检测到 %d 个 develop merge', count($result['issues']['develop_merges']))); + } + + if (!empty($result['issues']['missing_functions'])) { + $this->warn(sprintf(' - 检测到 %d 个疑似缺失函数的提交', count($result['issues']['missing_functions']))); + } + } + } +} diff --git a/app/Models/Config.php b/app/Models/Config.php new file mode 100644 index 0000000..d16ca68 --- /dev/null +++ b/app/Models/Config.php @@ -0,0 +1,21 @@ + 'array', + ]; +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0a38c0d..772e511 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -11,8 +11,11 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // 注册 JIRA 服务 + // 注册应用服务 $this->app->singleton(\App\Services\JiraService::class); + $this->app->singleton(\App\Services\ConfigService::class); + $this->app->singleton(\App\Services\DingTalkService::class); + $this->app->singleton(\App\Services\GitMonitorService::class); } /** diff --git a/app/Providers/GitMonitorServiceProvider.php b/app/Providers/GitMonitorServiceProvider.php new file mode 100644 index 0000000..c36da0c --- /dev/null +++ b/app/Providers/GitMonitorServiceProvider.php @@ -0,0 +1,25 @@ +app->runningInConsole()) { + $this->commands([ + GitMonitorCheckCommand::class, + GitMonitorCacheCommand::class, + ]); + } + } +} diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php new file mode 100644 index 0000000..6f1f4f2 --- /dev/null +++ b/app/Services/ConfigService.php @@ -0,0 +1,43 @@ +where('key', $key)->first(); + + return $config?->value ?? $default; + } + + public function getNested(string $key, string $path, mixed $default = null): mixed + { + $value = $this->get($key); + + return Arr::get($value ?? [], $path, $default); + } + + public function set(string $key, mixed $value, ?string $description = null): Config + { + return Config::query()->updateOrCreate( + ['key' => $key], + [ + 'value' => $value, + 'description' => $description, + ] + ); + } + + public function setNested(string $key, string $path, mixed $value, ?string $description = null): Config + { + $payload = $this->get($key, []); + Arr::set($payload, $path, $value); + + return $this->set($key, $payload, $description); + } +} diff --git a/app/Services/DingTalkService.php b/app/Services/DingTalkService.php new file mode 100644 index 0000000..50605c8 --- /dev/null +++ b/app/Services/DingTalkService.php @@ -0,0 +1,56 @@ +webhook = $config['webhook'] ?? null; + $this->secret = $config['secret'] ?? null; + } + + public function sendText(string $message, array $atMobiles = [], bool $atAll = false): void + { + if (empty($this->webhook)) { + Log::warning('DingTalk webhook is not configured, skip sending alert.'); + return; + } + + $payload = [ + 'msgtype' => 'text', + 'text' => [ + 'content' => $message, + ], + 'at' => [ + 'atMobiles' => $atMobiles, + 'isAtAll' => $atAll, + ], + ]; + + $url = $this->webhook; + if (!empty($this->secret)) { + $timestamp = (int) round(microtime(true) * 1000); + $stringToSign = $timestamp . "\n" . $this->secret; + $sign = base64_encode(hash_hmac('sha256', $stringToSign, $this->secret, true)); + $encodedSign = urlencode($sign); + $separator = str_contains($url, '?') ? '&' : '?'; + $url .= "{$separator}timestamp={$timestamp}&sign={$encodedSign}"; + } + + try { + Http::timeout(10)->asJson()->post($url, $payload); + } catch (\Throwable $e) { + Log::error('Failed to send DingTalk alert', [ + 'message' => $e->getMessage(), + ]); + } + } +} diff --git a/app/Services/EnvService.php b/app/Services/EnvService.php index 321f92f..bab7ef5 100644 --- a/app/Services/EnvService.php +++ b/app/Services/EnvService.php @@ -2,11 +2,10 @@ namespace App\Services; +use Carbon\Carbon; use Illuminate\Support\Facades\File; -use Illuminate\Support\Facades\Storage; use InvalidArgumentException; use RuntimeException; -use Carbon\Carbon; class EnvService { @@ -14,23 +13,32 @@ class EnvService private string $envStoragePath; private string $backupStoragePath; - public function __construct() + public function __construct(private readonly ConfigService $configService) { - $this->projectsPath = '/home/tradewind/Projects'; + $this->projectsPath = $this->resolveProjectsPath(); $this->envStoragePath = storage_path('app/env'); $this->backupStoragePath = storage_path('app/env/backups'); - // 确保env存储目录存在 if (!File::exists($this->envStoragePath)) { File::makeDirectory($this->envStoragePath, 0755, true); } - // 确保备份存储目录存在 if (!File::exists($this->backupStoragePath)) { File::makeDirectory($this->backupStoragePath, 0755, true); } } + private function resolveProjectsPath(): string + { + $path = $this->configService->get('workspace.projects_path'); + + if (empty($path)) { + throw new RuntimeException('configs 表未设置 workspace.projects_path。'); + } + + return rtrim($path, '/'); + } + /** * 获取所有项目列表 */ diff --git a/app/Services/GitMonitorService.php b/app/Services/GitMonitorService.php new file mode 100644 index 0000000..465a373 --- /dev/null +++ b/app/Services/GitMonitorService.php @@ -0,0 +1,487 @@ +> + */ + private array $projects = []; + private string $projectsPath; + private int $commitScanLimit; + private int $gitTimeout; + + public function __construct( + private readonly ConfigService $configService, + private readonly JiraService $jiraService, + private readonly DingTalkService $dingTalkService + ) { + $this->projectsPath = $this->resolveProjectsPath(); + $this->projects = $this->resolveProjects(); + + $enabled = config('git-monitor.enabled_projects', []); + if (!empty($enabled)) { + $this->projects = array_intersect_key($this->projects, array_flip($enabled)); + } + + $this->commitScanLimit = (int) config('git-monitor.commit_scan_limit', 30); + $this->gitTimeout = (int) config('git-monitor.git_timeout', 180); + } + + public function refreshReleaseCache(bool $force = false): array + { + if (empty($this->projects)) { + return []; + } + + if (!$force) { + $cached = $this->configService->get(self::RELEASE_CACHE_KEY); + if (!empty($cached) && !$this->shouldRefreshCache($cached)) { + return $cached; + } + } + + $payload = [ + 'cached_at' => Carbon::now()->toDateTimeString(), + 'repositories' => [], + ]; + + foreach ($this->projects as $repoKey => $repoConfig) { + $projectKey = $repoConfig['jira_project'] ?? null; + if (empty($projectKey)) { + Log::warning('Jira project key missing for repository', ['repository' => $repoKey]); + continue; + } + + try { + $version = $this->jiraService->getUpcomingReleaseVersion($projectKey); + if ($version) { + $payload['repositories'][$repoKey] = [ + 'version' => $version['version'], + 'release_date' => $version['release_date'], + 'branch' => 'release/' . $version['version'], + ]; + } + } catch (\Throwable $e) { + Log::error('Failed to fetch release version from Jira', [ + 'project' => $projectKey, + 'error' => $e->getMessage(), + ]); + } + } + + $this->configService->set( + self::RELEASE_CACHE_KEY, + $payload, + 'Cached release versions fetched from Jira' + ); + + return $payload; + } + + public function ensureReleaseCache(): array + { + $cached = $this->configService->get(self::RELEASE_CACHE_KEY); + + if (empty($cached) || $this->shouldRefreshCache($cached)) { + return $this->refreshReleaseCache(true); + } + + return $cached; + } + + public function checkRepositories(bool $refreshCacheIfNeeded = true): array + { + $releaseCache = $refreshCacheIfNeeded + ? $this->ensureReleaseCache() + : ($this->configService->get(self::RELEASE_CACHE_KEY) ?? []); + + $results = []; + $alerts = []; + + foreach ($this->projects as $repoKey => $repoConfig) { + $branch = data_get($releaseCache, "repositories.{$repoKey}.branch"); + if (empty($branch)) { + Log::warning('Missing release branch info for repository', ['repository' => $repoKey]); + continue; + } + + try { + $result = $this->inspectRepository($repoKey, $repoConfig, $branch); + $results[$repoKey] = $result; + + if ($this->hasIssues($result['issues'])) { + $alerts[$repoKey] = $result; + } + } catch (\Throwable $e) { + Log::error('Git monitoring failed for repository', [ + 'repository' => $repoKey, + 'error' => $e->getMessage(), + ]); + + $results[$repoKey] = [ + 'repository' => $repoKey, + 'branch' => $branch, + 'error' => $e->getMessage(), + ]; + } + } + + if (!empty($alerts)) { + $this->dingTalkService->sendText($this->buildAlertMessage($alerts)); + } + + return $results; + } + + private function shouldRefreshCache(array $cached): bool + { + $cachedAt = Arr::get($cached, 'cached_at'); + if (empty($cachedAt)) { + return true; + } + + try { + return Carbon::parse($cachedAt)->lt(Carbon::now()->startOfDay()); + } catch (\Throwable) { + return true; + } + } + + private function inspectRepository(string $repoKey, array $repoConfig, string $branch): array + { + $path = $this->resolveProjectPath($repoKey, $repoConfig); + $restoreBranch = null; + + if (!is_dir($path) || !is_dir($path . DIRECTORY_SEPARATOR . '.git')) { + throw new \RuntimeException("Project path {$path} is not a valid git repository"); + } + + try { + $restoreBranch = $this->synchronizeRepository($path, $branch); + $head = $this->runGit($path, ['git', 'rev-parse', 'HEAD']); + + $lastChecked = $this->configService->getNested(self::LAST_CHECKED_KEY, $repoKey); + $commits = $this->collectCommits($path, $branch, $lastChecked); + + $issues = [ + 'develop_merges' => [], + 'missing_functions' => [], + ]; + + foreach ($commits as $commit) { + if ($this->isDevelopMerge($path, $commit)) { + $issues['develop_merges'][] = $this->getCommitMetadata($path, $commit); + } + + $missingFunctions = $this->detectMissingFunctions($path, $commit); + if (!empty($missingFunctions)) { + $issues['missing_functions'][] = [ + 'commit' => $this->getCommitMetadata($path, $commit), + 'details' => $missingFunctions, + ]; + } + } + + $this->updateLastChecked($repoKey, $head); + + return [ + 'repository' => $repoKey, + 'display' => $repoConfig['display'] ?? ucfirst($repoKey), + 'branch' => $branch, + 'path' => $path, + 'head' => $head, + 'commits_scanned' => count($commits), + 'issues' => $issues, + ]; + } finally { + if ($restoreBranch) { + try { + $this->runGit($path, ['git', 'checkout', $restoreBranch]); + } catch (\Throwable $e) { + Log::warning('Failed to restore branch', [ + 'repository' => $repoKey, + 'branch' => $restoreBranch, + 'error' => $e->getMessage(), + ]); + } + } + } + } + + private function synchronizeRepository(string $path, string $branch): ?string + { + $currentBranch = trim($this->runGit($path, ['git', 'rev-parse', '--abbrev-ref', 'HEAD'])); + $restoreBranch = $currentBranch !== $branch && $currentBranch !== 'HEAD' + ? $currentBranch + : null; + + $this->runGit($path, ['git', 'fetch', 'origin']); + $this->runGit($path, ['git', 'fetch', 'origin', $branch]); + $this->runGit($path, ['git', 'fetch', 'origin', self::DEVELOP_BRANCH]); + + $this->checkoutBranch($path, $branch); + $this->runGit($path, ['git', 'pull', '--ff-only', 'origin', $branch]); + + return $restoreBranch; + } + + private function checkoutBranch(string $path, string $branch): void + { + try { + $this->runGit($path, ['git', 'show-ref', '--verify', "refs/heads/{$branch}"]); + $this->runGit($path, ['git', 'checkout', $branch]); + } catch (ProcessFailedException) { + $this->runGit($path, ['git', 'checkout', '-B', $branch, "origin/{$branch}"]); + } + } + + private function collectCommits(string $repoPath, string $branch, ?string $lastChecked): array + { + try { + $output = $lastChecked + ? $this->runGit($repoPath, ['git', 'log', "{$lastChecked}..{$branch}", '--pretty=%H']) + : $this->runGit($repoPath, ['git', 'log', $branch, '-n', (string) $this->commitScanLimit, '--pretty=%H']); + } catch (ProcessFailedException) { + $output = $this->runGit($repoPath, ['git', 'log', '-n', (string) $this->commitScanLimit, '--pretty=%H']); + } + + return array_values(array_filter(array_map('trim', explode("\n", $output)))); + } + + private function isDevelopMerge(string $repoPath, string $commit): bool + { + $parents = trim($this->runGit($repoPath, ['git', 'show', '-s', '--pretty=%P', $commit])); + $parentShas = array_values(array_filter(explode(' ', $parents))); + + if (count($parentShas) < 2) { + return false; + } + + $message = strtolower($this->runGit($repoPath, ['git', 'show', '-s', '--pretty=%s', $commit])); + if (str_contains($message, self::DEVELOP_BRANCH)) { + return true; + } + + $target = 'origin/' . self::DEVELOP_BRANCH; + + foreach ($parentShas as $parent) { + $branches = $this->runGit($repoPath, ['git', 'branch', '-r', '--contains', $parent]); + foreach (preg_split('/\R/', $branches) as $branchLine) { + $branchLine = trim(str_replace('*', '', $branchLine)); + if (empty($branchLine)) { + continue; + } + + if ($branchLine === $target || str_ends_with($branchLine, '/' . self::DEVELOP_BRANCH)) { + return true; + } + } + } + + return false; + } + + private function detectMissingFunctions(string $repoPath, string $commit): array + { + $diff = $this->runGit($repoPath, ['git', 'show', $commit, '--pretty=format:', '--unified=0']); + $currentFile = null; + $removed = []; + $added = []; + + foreach (preg_split('/\R/', $diff) as $line) { + if (str_starts_with($line, 'diff --git')) { + $currentFile = null; + continue; + } + + if (str_starts_with($line, '+++ b/')) { + $currentFile = substr($line, 6); + continue; + } + + if (str_starts_with($line, '--- a/')) { + continue; + } + + if (!$currentFile || !str_ends_with($currentFile, '.php')) { + continue; + } + + if (str_starts_with($line, '-')) { + $function = $this->extractFunctionName(substr($line, 1)); + if ($function) { + $removed[$currentFile][] = $function; + } + continue; + } + + if (str_starts_with($line, '+')) { + $function = $this->extractFunctionName(substr($line, 1)); + if ($function) { + $added[$currentFile][] = $function; + } + } + } + + $issues = []; + foreach ($removed as $file => $functions) { + $diffed = array_diff($functions, $added[$file] ?? []); + if (!empty($diffed)) { + $issues[] = [ + 'file' => $file, + 'functions' => array_values(array_unique($diffed)), + ]; + } + } + + return $issues; + } + + private function extractFunctionName(string $line): ?string + { + $trimmed = trim($line); + if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '#') || str_starts_with($trimmed, '*')) { + return null; + } + + if (preg_match('/function\s+([A-Za-z0-9_]+)\s*\(/', $trimmed, $matches)) { + return $matches[1]; + } + + return null; + } + + private function updateLastChecked(string $repoKey, string $sha): void + { + $current = $this->configService->get(self::LAST_CHECKED_KEY, []); + $current[$repoKey] = $sha; + + $this->configService->set( + self::LAST_CHECKED_KEY, + $current, + 'Last scanned release commits' + ); + } + + private function getCommitMetadata(string $repoPath, string $commit): array + { + $separator = "\x1F"; + $format = "%H{$separator}%an{$separator}%ad{$separator}%s"; + $raw = $this->runGit($repoPath, ['git', 'show', '-s', "--date=iso-strict", "--pretty={$format}", $commit]); + $parts = array_pad(explode($separator, $raw, 4), 4, ''); + + return [ + 'hash' => $parts[0], + 'author' => $parts[1], + 'date' => $parts[2], + 'subject' => $parts[3], + ]; + } + + private function hasIssues(array $issues): bool + { + return !empty($issues['develop_merges']) || !empty($issues['missing_functions']); + } + + private function buildAlertMessage(array $alerts): string + { + $lines = ['【Git监控告警】']; + + foreach ($alerts as $result) { + $lines[] = sprintf('%s(%s)', $result['display'], $result['branch']); + + if (!empty($result['issues']['develop_merges'])) { + $lines[] = ' - 检测到 develop 合并:'; + foreach ($result['issues']['develop_merges'] as $commit) { + $lines[] = sprintf( + ' • %s %s (%s)', + substr($commit['hash'], 0, 8), + $commit['subject'], + $commit['author'] + ); + } + } + + if (!empty($result['issues']['missing_functions'])) { + $lines[] = ' - 疑似缺失函数:'; + + foreach ($result['issues']['missing_functions'] as $issue) { + $lines[] = sprintf( + ' • %s %s', + substr($issue['commit']['hash'], 0, 8), + $issue['commit']['subject'] + ); + + foreach ($issue['details'] as $detail) { + $functions = implode(', ', array_slice($detail['functions'], 0, 5)); + $lines[] = sprintf(' %s => %s', $detail['file'], $functions); + } + } + } + } + + return implode("\n", $lines); + } + + private function runGit(string $path, array $command): string + { + return $this->runProcess($command, $path); + } + + private function runProcess(array $command, ?string $workingDirectory = null): string + { + $process = new Process($command, $workingDirectory); + $process->setTimeout($this->gitTimeout); + $process->mustRun(); + + return trim($process->getOutput()); + } + + private function resolveProjectsPath(): string + { + $path = $this->configService->get('workspace.projects_path'); + + if (empty($path)) { + throw new \RuntimeException('configs 表未设置 workspace.projects_path。'); + } + + return rtrim($path, '/'); + } + + /** + * @return array> + */ + private function resolveProjects(): array + { + $projects = $this->configService->get('workspace.repositories'); + + if (!is_array($projects) || empty($projects)) { + throw new \RuntimeException('configs 表未设置 workspace.repositories。'); + } + + return $projects; + } + + private function resolveProjectPath(string $repoKey, array $repoConfig): string + { + if (!empty($repoConfig['path'])) { + return rtrim($repoConfig['path'], '/'); + } + + $directory = $repoConfig['directory'] ?? $repoKey; + return $this->projectsPath . '/' . ltrim($directory, '/'); + } +} diff --git a/app/Services/JiraService.php b/app/Services/JiraService.php index 6e2fcbb..0b3765e 100644 --- a/app/Services/JiraService.php +++ b/app/Services/JiraService.php @@ -5,12 +5,14 @@ namespace App\Services; use JiraRestApi\Configuration\ArrayConfiguration; use JiraRestApi\Issue\IssueService; use JiraRestApi\JiraException; +use JiraRestApi\Project\ProjectService; use Carbon\Carbon; use Illuminate\Support\Collection; class JiraService { private IssueService $issueService; + private ProjectService $projectService; private array $config; public function __construct() @@ -19,9 +21,12 @@ class JiraService $this->initializeJiraClient(); } + /** + * @throws JiraException + */ private function initializeJiraClient(): void { - $jiraConfig = new ArrayConfiguration([ + $clientConfig = new ArrayConfiguration([ 'jiraHost' => $this->config['host'], 'jiraUser' => $this->config['username'], 'jiraPassword' => $this->config['auth_type'] === 'token' @@ -30,7 +35,8 @@ class JiraService 'timeout' => $this->config['timeout'], ]); - $this->issueService = new IssueService($jiraConfig); + $this->issueService = new IssueService($clientConfig); + $this->projectService = new ProjectService($clientConfig); } @@ -738,5 +744,47 @@ class JiraService } } + /** + * 获取最近的 release 版本 + */ + public function getUpcomingReleaseVersion(string $projectKey): ?array + { + try { + $versions = $this->projectService->getVersions($projectKey); + } catch (JiraException $e) { + throw new \RuntimeException('获取 release 版本失败: ' . $e->getMessage(), previous: $e); + } + if (empty($versions)) { + return null; + } + + $now = Carbon::now()->startOfDay(); + + $candidate = collect($versions) + ->filter(function ($version) use ($now) { + if (($version->released ?? false) || empty($version->releaseDate)) { + return false; + } + + try { + return Carbon::parse($version->releaseDate)->greaterThanOrEqualTo($now); + } catch (\Throwable) { + return false; + } + }) + ->sortBy(function ($version) { + return Carbon::parse($version->releaseDate); + }) + ->first(); + + if (!$candidate) { + return null; + } + + return [ + 'version' => $candidate->name, + 'release_date' => Carbon::parse($candidate->releaseDate)->toDateString(), + ]; + } } diff --git a/bootstrap/app.php b/bootstrap/app.php index fdfaa9a..3467fe7 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ withExceptions(function (Exceptions $exceptions): void { // - })->create(); + }) + ->withSchedule(function (Schedule $schedule): void { + $schedule->command('git-monitor:check') + ->everyTenMinutes() + ->withoutOverlapping() + ->runInBackground(); + + $schedule->command('git-monitor:cache') + ->dailyAt('02:00') + ->withoutOverlapping(); + }) + ->create(); diff --git a/bootstrap/providers.php b/bootstrap/providers.php index bddfa3b..e412c3d 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -4,4 +4,5 @@ return [ App\Providers\AppServiceProvider::class, App\Providers\ClientServiceProvider::class, App\Providers\EnvServiceProvider::class, + App\Providers\GitMonitorServiceProvider::class, ]; diff --git a/config/git-monitor.php b/config/git-monitor.php new file mode 100644 index 0000000..ace713d --- /dev/null +++ b/config/git-monitor.php @@ -0,0 +1,12 @@ + array_values(array_filter(array_map( + 'trim', + explode(',', env('GIT_MONITOR_PROJECTS', 'service,portal-be,agent-be')) + ))), + + 'commit_scan_limit' => 30, + + 'git_timeout' => 180, +]; diff --git a/config/services.php b/config/services.php index a7c90ff..3ca7edb 100644 --- a/config/services.php +++ b/config/services.php @@ -45,4 +45,9 @@ return [ 'timeout' => env('MONO_TIMEOUT', 30), ], + 'dingtalk' => [ + 'webhook' => env('DINGTALK_WEBHOOK'), + 'secret' => env('DINGTALK_SECRET'), + ], + ]; diff --git a/database/migrations/2025_12_09_171037_create_configs_table.php b/database/migrations/2025_12_09_171037_create_configs_table.php new file mode 100644 index 0000000..a16babe --- /dev/null +++ b/database/migrations/2025_12_09_171037_create_configs_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('key')->unique(); + $table->json('value')->nullable(); + $table->string('description')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('configs'); + } +}; diff --git a/database/migrations/2025_12_10_195658_seed_workspace_projects_path.php b/database/migrations/2025_12_10_195658_seed_workspace_projects_path.php new file mode 100644 index 0000000..d19d5e5 --- /dev/null +++ b/database/migrations/2025_12_10_195658_seed_workspace_projects_path.php @@ -0,0 +1,40 @@ +where('key', 'workspace.projects_path')->exists(); + + if (!$exists) { + Config::query()->create([ + 'key' => 'workspace.projects_path', + 'value' => '/home/tradewind/Projects', + 'description' => '默认的工作空间项目路径', + ]); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (!Schema::hasTable('configs')) { + return; + } + + Config::query()->where('key', 'workspace.projects_path')->delete(); + } +};