#add git monitor

This commit is contained in:
2025-12-18 10:18:25 +08:00
parent 2ec44b5665
commit 5f6bba1d9f
18 changed files with 889 additions and 20 deletions

View File

@@ -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"

View File

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

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Console\Commands;
use App\Services\GitMonitorService;
use Illuminate\Console\Command;
class GitMonitorCacheCommand extends Command
{
protected $signature = 'git-monitor:cache';
protected $description = '刷新 release 版本缓存,保存到 configs 表';
public function handle(GitMonitorService $monitor): void
{
$cache = $monitor->refreshReleaseCache(true);
if (empty($cache)) {
$this->warn('未获取到任何 release 版本信息,请检查配置。');
return;
}
$this->info(sprintf(
'已缓存 %d 个仓库的 release 分支信息。',
count($cache['repositories'] ?? [])
));
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands;
use App\Services\GitMonitorService;
use Illuminate\Console\Command;
class GitMonitorCheckCommand extends Command
{
protected $signature = 'git-monitor:check
{--force-cache : 强制从 JIRA 刷新 release 缓存后再检查}';
protected $description = '巡检 release 分支是否包含 develop merge 或因为冲突导致的函数缺失';
public function handle(GitMonitorService $monitor): void
{
if ($this->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'])));
}
}
}
}

21
app/Models/Config.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Config extends Model
{
use HasFactory;
protected $fillable = [
'key',
'value',
'description',
];
protected $casts = [
'value' => 'array',
];
}

View File

@@ -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);
}
/**

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Providers;
use App\Console\Commands\GitMonitorCacheCommand;
use App\Console\Commands\GitMonitorCheckCommand;
use Illuminate\Support\ServiceProvider;
class GitMonitorServiceProvider extends ServiceProvider
{
public function register(): void
{
//
}
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->commands([
GitMonitorCheckCommand::class,
GitMonitorCacheCommand::class,
]);
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Services;
use App\Models\Config;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Schema;
class ConfigService
{
public function get(string $key, mixed $default = null): mixed
{
$config = Config::query()->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);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class DingTalkService
{
private ?string $webhook;
private ?string $secret;
public function __construct()
{
$config = config('services.dingtalk', []);
$this->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(),
]);
}
}
}

View File

@@ -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, '/');
}
/**
* 获取所有项目列表
*/

View File

@@ -0,0 +1,487 @@
<?php
namespace App\Services;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class GitMonitorService
{
private const RELEASE_CACHE_KEY = 'git_monitor.current_versions';
private const LAST_CHECKED_KEY = 'git_monitor.last_checked_commits';
private const DEVELOP_BRANCH = 'develop';
/**
* 项目配置(只包含允许巡检的项目)
* @var array<string, array<string, mixed>>
*/
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<string, array<string, mixed>>
*/
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, '/');
}
}

View File

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

View File

@@ -1,5 +1,6 @@
<?php
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
@@ -22,4 +23,15 @@ return Application::configure(basePath: dirname(__DIR__))
})
->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();

View File

@@ -4,4 +4,5 @@ return [
App\Providers\AppServiceProvider::class,
App\Providers\ClientServiceProvider::class,
App\Providers\EnvServiceProvider::class,
App\Providers\GitMonitorServiceProvider::class,
];

12
config/git-monitor.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
return [
'enabled_projects' => array_values(array_filter(array_map(
'trim',
explode(',', env('GIT_MONITOR_PROJECTS', 'service,portal-be,agent-be'))
))),
'commit_scan_limit' => 30,
'git_timeout' => 180,
];

View File

@@ -45,4 +45,9 @@ return [
'timeout' => env('MONO_TIMEOUT', 30),
],
'dingtalk' => [
'webhook' => env('DINGTALK_WEBHOOK'),
'secret' => env('DINGTALK_SECRET'),
],
];

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('configs', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,40 @@
<?php
use App\Models\Config;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (!Schema::hasTable('configs')) {
return;
}
$exists = Config::query()->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();
}
};