diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index a59d91f..09ef1d7 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,7 +1,6 @@ name: backend-ci-cd -# CI/CD: только push git-тега (ручное тегирование на ветке prod|test|stage). -# Push в ветки и feature-ветки pipeline не запускают. +# CI/CD: только push git-тега. Pre-deploy tests на test|stage; prod — без тестов (ручной аппрув релиза). on: push: @@ -14,25 +13,6 @@ env: IMAGE_DEPLOY: git.sova.local/sova/backend jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.4' - extensions: pdo_pgsql, redis, intl, zip, gd - - name: Prepare CI environment - run: | - cp .env.ci .env.local - mkdir -p config/jwt var - openssl genrsa -out config/jwt/private.pem 2048 - openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem - - run: composer install --prefer-dist --no-interaction - - run: composer phpunit || true - - run: composer audit || true - parse-tag: runs-on: ubuntu-latest outputs: @@ -48,8 +28,67 @@ jobs: echo "env=$(echo "$TAG" | sed -E 's/backend-v([0-9.]+)-([a-z]+)/\2/')" >> "$GITHUB_OUTPUT" echo "version=$(echo "$TAG" | sed -E 's/backend-v([0-9.]+).*/\1/')" >> "$GITHUB_OUTPUT" + test: + needs: [parse-tag] + if: needs.parse-tag.outputs.env != 'prod' + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: ci + POSTGRES_PASSWORD: ci + POSTGRES_DB: ci + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U ci -d ci" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + steps: + - uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: pdo_pgsql, redis, intl, zip, gd + coverage: xdebug + - name: Prepare CI environment + run: | + cp .env.ci .env.local + mkdir -p config/jwt var/coverage + openssl genrsa -out config/jwt/private.pem 2048 + openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem + - run: composer install --prefer-dist --no-interaction + - name: Unit tests + coverage + run: composer phpunit:coverage + - name: Integration tests (test contour only) + if: needs.parse-tag.outputs.env == 'test' + continue-on-error: true + run: | + php bin/console doctrine:database:create --if-not-exists --env=test || true + php bin/console doctrine:migrations:migrate --no-interaction --env=test || true + composer phpunit:integration + - name: Upload coverage + if: always() + uses: actions/upload-artifact@v4 + with: + name: backend-coverage-${{ needs.parse-tag.outputs.full_tag }} + path: var/coverage/ + retention-days: 14 + - run: composer audit || true + + test-prod-skip: + needs: [parse-tag] + if: needs.parse-tag.outputs.env == 'prod' + runs-on: ubuntu-latest + steps: + - run: echo "Prod tag — automated tests skipped (no tests on production contour)." + build-and-push: - needs: [test, parse-tag] + needs: [parse-tag, test, test-prod-skip] + if: always() && (needs.test.result == 'success' || needs.test.result == 'skipped') && (needs.test-prod-skip.result == 'success' || needs.test-prod-skip.result == 'skipped') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/composer.json b/composer.json index cd80aa0..bca9d1b 100644 --- a/composer.json +++ b/composer.json @@ -101,6 +101,9 @@ "@php bin/console cache:clear" ], "phpunit": "phpunit", + "phpunit:unit": "phpunit --testsuite=unit", + "phpunit:integration": "phpunit --testsuite=integration", + "phpunit:coverage": "phpunit --testsuite=unit --coverage-clover var/coverage/clover.xml --coverage-text", "generate-swagger": "php ./vendor/bin/openapi --output ./public/swagger.json ./src", "auto-scripts": { "cache:clear": "symfony-cmd", diff --git a/config/services.yaml b/config/services.yaml index 81c413c..1b3739c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -30,6 +30,10 @@ services: - '../src/DependencyInjection/' - '../src/Entity/' - '../src/Kernel.php' + + App\Log\TestTraceProcessor: + tags: + - { name: monolog.processor } # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/phpunit.dist.xml b/phpunit.dist.xml index 0b31da2..641759d 100644 --- a/phpunit.dist.xml +++ b/phpunit.dist.xml @@ -18,8 +18,13 @@ - - tests + + tests/Unit + tests/Service + + + tests/Controller + tests/Integration @@ -27,8 +32,19 @@ src + + src/Entity + + + + + + + + + diff --git a/src/Log/TestTraceProcessor.php b/src/Log/TestTraceProcessor.php new file mode 100644 index 0000000..8209972 --- /dev/null +++ b/src/Log/TestTraceProcessor.php @@ -0,0 +1,33 @@ +requestStack->getCurrentRequest(); + if ($request === null || !$request->headers->has('X-Is-Auto-Test')) { + return $record; + } + + return $record->with(extra: array_merge($record->extra, [ + 'is_test' => true, + 'test_trace_id' => $request->headers->get('X-Test-Trace-Id', 'unknown'), + ])); + } +} diff --git a/tests/Unit/Log/TestTraceProcessorTest.php b/tests/Unit/Log/TestTraceProcessorTest.php new file mode 100644 index 0000000..de3e323 --- /dev/null +++ b/tests/Unit/Log/TestTraceProcessorTest.php @@ -0,0 +1,44 @@ +headers->set('X-Is-Auto-Test', 'true'); + $request->headers->set('X-Test-Trace-Id', 'run-42'); + + $stack = new RequestStack(); + $stack->push($request); + + $processor = new TestTraceProcessor($stack); + $record = new LogRecord(new \DateTimeImmutable(), 'app', Level::Info, 'msg'); + + $out = $processor($record); + + self::assertTrue($out->extra['is_test']); + self::assertSame('run-42', $out->extra['test_trace_id']); + } + + public function testLeavesRecordUntouchedWithoutHeader(): void + { + $stack = new RequestStack(); + $processor = new TestTraceProcessor($stack); + $record = new LogRecord(new \DateTimeImmutable(), 'app', Level::Info, 'msg', extra: ['foo' => 1]); + + $out = $processor($record); + + self::assertSame(['foo' => 1], $out->extra); + } +}