diff --git a/.gitea/workflows/commit.yaml b/.gitea/workflows/commit.yaml index af82206..9ccb960 100644 --- a/.gitea/workflows/commit.yaml +++ b/.gitea/workflows/commit.yaml @@ -6,9 +6,46 @@ on: - dev - master jobs: - deploy: - if: gitea.ref_name == 'master' + stamp_build: + if: gitea.ref_name == 'master' && !contains(gitea.event.head_commit.message, '[skip ci]') runs-on: self-hosted + steps: + - name: Stamp composer build from origin/master + env: + SPEEDBB_REPO: ${{ vars.SPEEDBB_REPO }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_ACTOR: ${{ gitea.actor }} + run: | + set -e + REPO="$SPEEDBB_REPO" + if [ -n "$GITEA_TOKEN" ]; then + REPO=$(echo "$SPEEDBB_REPO" | sed "s#https://#https://${GITEA_ACTOR}:${GITEA_TOKEN}@#") + fi + + git clone --quiet --branch=master "$REPO" repo + cd repo + git fetch origin master + + BUILD="$(git rev-list --count origin/master)" + CURRENT="$(php -r 'echo (string) ((json_decode(file_get_contents("composer.json"), true)["build"] ?? ""));')" + + if [ "$CURRENT" = "$BUILD" ]; then + echo "composer.json build already $BUILD; no changes." + exit 0 + fi + + BUILD="$BUILD" php -r '$p="composer.json"; $d=json_decode(file_get_contents($p), true); if (!is_array($d)) { fwrite(STDERR, "Invalid composer.json\n"); exit(1);} $d["build"]=getenv("BUILD"); file_put_contents($p, json_encode($d, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES).PHP_EOL);' + + git config user.name "speedbb-ci" + git config user.email "ci@24unix.net" + git add composer.json + git commit -m "ci: sync composer build to ${BUILD} [skip ci]" + git push origin master + + deploy: + if: gitea.ref_name == 'master' && !contains(gitea.event.head_commit.message, '[skip ci]') + runs-on: self-hosted + needs: stamp_build steps: - name: Custom Checkout env: @@ -32,7 +69,7 @@ jobs: rm .vault_pass.txt promote_stable: - if: gitea.ref_name == 'master' + if: gitea.ref_name == 'master' && !contains(gitea.event.head_commit.message, '[skip ci]') runs-on: self-hosted needs: deploy steps: diff --git a/app/Console/Commands/VersionFetch.php b/app/Console/Commands/VersionFetch.php index d8d5f13..f58848a 100644 --- a/app/Console/Commands/VersionFetch.php +++ b/app/Console/Commands/VersionFetch.php @@ -4,28 +4,23 @@ namespace App\Console\Commands; use App\Models\Setting; use Illuminate\Console\Command; -use Symfony\Component\Process\Process; class VersionFetch extends Command { protected $signature = 'version:fetch'; - protected $description = 'Sync version/build metadata into settings using composer.json version and git build count.'; + protected $description = 'Sync version/build metadata into settings using composer.json as source of truth.'; public function handle(): int { - $version = $this->resolveComposerVersion(); - $build = $this->resolveBuildCount(); - - if ($version === null) { - $this->error('Unable to determine version from composer.json.'); + $meta = $this->resolveComposerMetadata(); + if ($meta === null) { + $this->error('Unable to determine version/build from composer.json.'); return self::FAILURE; } - if ($build === null) { - $this->error('Unable to determine build number from git.'); - return self::FAILURE; - } + $version = $meta['version']; + $build = $meta['build']; Setting::updateOrCreate( ['key' => 'version'], @@ -37,17 +32,12 @@ class VersionFetch extends Command ['value' => (string) $build], ); - if (!$this->syncComposerBuild($build)) { - $this->error('Failed to sync version/build to composer.json.'); - return self::FAILURE; - } - $this->info("Version/build synced: {$version} (build {$build})."); return self::SUCCESS; } - private function resolveComposerVersion(): ?string + private function resolveComposerMetadata(): ?array { $composerPath = base_path('composer.json'); @@ -66,7 +56,9 @@ class VersionFetch extends Command } $version = trim((string) ($data['version'] ?? '')); - if ($version === '') { + $buildRaw = trim((string) ($data['build'] ?? '')); + + if ($version === '' || $buildRaw === '') { return null; } @@ -74,58 +66,13 @@ class VersionFetch extends Command return null; } - return $version; - } + if (!ctype_digit($buildRaw)) { + return null; + } - private function resolveBuildCount(): ?int - { - $commands = [ - ['git', 'rev-list', '--count', 'master'], - ['git', 'rev-list', '--count', 'HEAD'], + return [ + 'version' => $version, + 'build' => (int) $buildRaw, ]; - - foreach ($commands as $command) { - $process = new Process($command, base_path()); - $process->run(); - - if ($process->isSuccessful()) { - $output = trim($process->getOutput()); - if (is_numeric($output)) { - return (int) $output; - } - } - } - - return null; - } - - private function syncComposerBuild(int $build): bool - { - $composerPath = base_path('composer.json'); - - if (!is_file($composerPath) || !is_readable($composerPath)) { - return false; - } - - $raw = file_get_contents($composerPath); - if ($raw === false) { - return false; - } - - $data = json_decode($raw, true); - if (!is_array($data)) { - return false; - } - - $data['build'] = (string) $build; - - $encoded = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - if ($encoded === false) { - return false; - } - - $encoded .= "\n"; - - return file_put_contents($composerPath, $encoded) !== false; } } diff --git a/tests/Unit/VersionFetchCommandTest.php b/tests/Unit/VersionFetchCommandTest.php index f2acba8..9d68403 100644 --- a/tests/Unit/VersionFetchCommandTest.php +++ b/tests/Unit/VersionFetchCommandTest.php @@ -11,17 +11,6 @@ namespace App\Console\Commands { return \file_get_contents($path); } } - - if (!function_exists(__NAMESPACE__ . '\\json_encode')) { - function json_encode($value, int $flags = 0): string|false - { - if (!empty($GLOBALS['version_fetch_json_encode_false']) && is_array($value) && array_key_exists('build', $value)) { - return false; - } - - return \json_encode($value, $flags); - } - } } namespace { @@ -40,87 +29,47 @@ namespace { file_put_contents($path, $original); } $GLOBALS['version_fetch_file_get_contents_false'] = false; - $GLOBALS['version_fetch_json_encode_false'] = false; - $originalPath = $GLOBALS['version_fetch_path'] ?? null; - if ($originalPath !== null) { - putenv("PATH={$originalPath}"); - $_ENV['PATH'] = $originalPath; - $_SERVER['PATH'] = $originalPath; - unset($GLOBALS['version_fetch_path']); - } } } - it('fetches build count and syncs composer metadata', function (): void { + it('syncs version and build from composer metadata', function (): void { withComposerBackupForFetch(function (): void { Setting::updateOrCreate(['key' => 'version'], ['value' => '0.0.0']); + Setting::updateOrCreate(['key' => 'build'], ['value' => '0']); + $composer = json_decode((string) file_get_contents(base_path('composer.json')), true); $expectedVersion = (string) ($composer['version'] ?? ''); + $expectedBuild = (string) ($composer['build'] ?? ''); $exitCode = Artisan::call('version:fetch'); expect($exitCode)->toBe(0); - - $build = Setting::where('key', 'build')->value('value'); - expect(is_numeric($build))->toBeTrue(); expect(Setting::where('key', 'version')->value('value'))->toBe($expectedVersion); - }); - }); - - it('fails when build count cannot be resolved', function (): void { - withComposerBackupForFetch(function (): void { - $GLOBALS['version_fetch_path'] = getenv('PATH') ?: ''; - putenv('PATH=/nope'); - $_ENV['PATH'] = '/nope'; - $_SERVER['PATH'] = '/nope'; - - Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']); - - $exitCode = Artisan::call('version:fetch'); - expect($exitCode)->toBe(1); + expect(Setting::where('key', 'build')->value('value'))->toBe($expectedBuild); }); }); it('fails when composer.json cannot be decoded', function (): void { withComposerBackupForFetch(function (string $path): void { file_put_contents($path, 'not-json'); - - Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']); - $exitCode = Artisan::call('version:fetch'); expect($exitCode)->toBe(1); }); }); - it('fails when composer.json is not readable', function (): void { + it('fails when composer.json is missing build', function (): void { withComposerBackupForFetch(function (string $path): void { - chmod($path, 0000); - - Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']); + $data = json_decode((string) file_get_contents($path), true); + unset($data['build']); + file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); $exitCode = Artisan::call('version:fetch'); expect($exitCode)->toBe(1); - - chmod($path, 0644); }); }); it('fails when file_get_contents returns false', function (): void { withComposerBackupForFetch(function (): void { $GLOBALS['version_fetch_file_get_contents_false'] = true; - - Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']); - - $exitCode = Artisan::call('version:fetch'); - expect($exitCode)->toBe(1); - }); - }); - - it('fails when json_encode returns false', function (): void { - withComposerBackupForFetch(function (): void { - $GLOBALS['version_fetch_json_encode_false'] = true; - - Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']); - $exitCode = Artisan::call('version:fetch'); expect($exitCode)->toBe(1); });