diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index c90da45..f356707 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,4 +1,4 @@ -name: '🚀 Feature Request' +name: 'Feature Request' description: 'Предложить новую идею или улучшение' labels: ['enhancement'] body: diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..35ee000 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,56 @@ +core: + - changed-files: + - any-glob-to-any-file: 'src/**/*' + - all-globs-to-all-files: + - '!src/shared/entities/**/*' + - '!src/**/*.spec.{ts,js}' + +database: + - changed-files: + - any-glob-to-any-file: + - 'src/shared/entities/**/*' + - 'libs/database/**/*' + - 'migrations/**/*' + - 'drizzle.config.ts' + +dependencies: + - changed-files: + - any-glob-to-any-file: + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + +devops: + - changed-files: + - any-glob-to-any-file: + - 'infra/**/*' + - '.github/workflows/**/*' + - 'Dockerfile*' + - '.dockerignore' + +testing: + - changed-files: + - any-glob-to-any-file: + - 'test/**/*' + - 'src/**/*.spec.{ts,js}' + - 'k6/**/*' + - 'vitest.config*' + +libs: + - changed-files: + - any-glob-to-any-file: 'libs/**/*' + - all-globs-to-all-files: + - '!libs/database/**/*' + +dx: + - changed-files: + - any-glob-to-any-file: + - 'pnpm-workspace.yaml' + - '.*' + - '!package.json' + +documentation: + - changed-files: + - any-glob-to-any-file: + - '**/*.md' + - 'LICENSE' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1a92a4..fdc2afc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,26 +2,47 @@ name: Build and Push on: push: - branches: [dev, main, feat/**] + branches: [main, dev, 'feat/**', 'fix/**', 'refactor/**', 'chore/**'] + pull_request: + branches: [main, dev] + workflow_dispatch: + inputs: + force_push: + description: 'Force push image to registry?' + type: boolean + default: false + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-push: runs-on: ubuntu-latest + env: + IS_BASE_BRANCH: ${{ github.ref_name == 'main' || github.ref_name == 'dev' }} + IS_PUSH: ${{ github.event_name == 'push' }} + FORCE_PUSH: ${{ github.event.inputs.force_push == 'true' }} + permissions: contents: read packages: write steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to the Container registry + if: ${{ (env.IS_PUSH == 'true' && env.IS_BASE_BRANCH == 'true') || + env.FORCE_PUSH == 'true' }} uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} @@ -36,14 +57,16 @@ jobs: tags: | type=ref,event=branch type=sha,format=short - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + # latest вешаем только когда мерджим в main + type=raw,value=latest,enable=${{ github.ref_name == 'main' }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile.prod - push: true + push: ${{ (env.IS_PUSH == 'true' && env.IS_BASE_BRANCH == 'true') || + env.FORCE_PUSH == 'true' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93a1c10..763a6e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,38 @@ name: CI on: - pull_request: - branches: [dev, main, 'feat/**'] push: - branches: [dev, main, 'feat/**'] + branches: + - main + - dev + - 'feat/**' + - 'fix/**' + - 'refactor/**' + - 'chore/**' + - 'perf/**' + - 'build/**' + - 'ci/**' + paths-ignore: + - '**.md' + - 'infra/**' + - '.gitignore' + - 'docker-compose.yml' + + pull_request: + branches: + - main + - dev + paths-ignore: + - '**.md' + - 'infra/**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: quality-check: @@ -21,11 +49,11 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: 'pnpm' - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile --prefer-offline - name: Run Lint run: pnpm run lint diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml new file mode 100644 index 0000000..407b8a9 --- /dev/null +++ b/.github/workflows/cleanup.yml @@ -0,0 +1,45 @@ +name: 'Cleanup' + +on: + schedule: + - cron: '0 0 * * 0' + workflow_dispatch: + +permissions: + actions: write + contents: read + +jobs: + garbage-collector: + name: 'Purge Storage' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: 'Clean Actions Cache' + shell: bash + run: | + echo "::group::Deleting Caches" + gh cache delete --all --succeed-on-no-caches || echo "Caches already empty or cleared" + echo "::endgroup::" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 'Clean Old Artifacts' + shell: bash + run: | + echo "::group::Deleting Artifacts" + artifacts=$(gh api repos/${{ github.repository }}/actions/artifacts --paginate -q '.artifacts[].id' || echo "") + + if [ -n "$artifacts" ]; then + for id in $artifacts; do + gh api -X DELETE repos/${{ github.repository }}/actions/artifacts/$id || true + done + echo "Artifacts cleared." + else + echo "No artifacts found." + fi + echo "::endgroup::" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 129d47e..cc77c3f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,16 +2,26 @@ name: 'CodeQL' on: push: - branches: [main, dev, feat/**, chore/**, build/**] + branches: [main, dev] + paths-ignore: + - '**.md' + - 'infra/**' + - 'migrations/**' pull_request: - branches: [main] + branches: [main, dev] + paths-ignore: + - '**.md' schedule: - cron: '15 13 * * 5' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + jobs: analyze: name: Analyze runs-on: ubuntu-latest + timeout-minutes: 360 permissions: actions: read contents: read @@ -25,9 +35,12 @@ jobs: uses: github/codeql-action/init@v3 with: languages: javascript-typescript + queries: security-extended - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 + with: + category: '/language:javascript-typescript' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..7b94cf6 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,38 @@ +name: 'Pull Request Labeler' + +on: + # Важно: target позволяет работать в PR из форков + pull_request_target: + types: [opened, synchronize] + +jobs: + label: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Ensure Labels Exist + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + labels=( + "database:5319e7:Database schema and migrations" + "core:e99695:Main application logic" + "testing:ff69b4:Unit and E2E tests" + "devops:006b75:Infrastructure and CI/CD" + "shared-libs:bfdadc:Shared libraries in libs/" + "dx:eeeeee:Developer experience and configs" + "documentation:0075ca:Documentation and markdown files" + ) + + for label in "${labels[@]}"; do + IFS=":" read -r name color desc <<< "$label" + gh label create "$name" --color "$color" --description "$desc" --repo ${{ github.repository }} || true + done + + - name: Run Labeler + uses: actions/labeler@v5 + with: + configuration-path: .github/labeler.yml + sync-labels: true diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml new file mode 100644 index 0000000..4847ac5 --- /dev/null +++ b/.github/workflows/migrations.yml @@ -0,0 +1,85 @@ +name: 'Database Consistency Check' + +on: + pull_request: + branches: [main, dev] + paths: + - 'src/shared/entities/**' + - 'migrations/**' + - 'drizzle.config.ts' + - 'package.json' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + DB_USER: tracker + DB_PASSWORD: super-tracker-password + DB_NAME: test + DB_URL: postgres://tracker:super-tracker-password@localhost:5432/test + +jobs: + check: + name: 'Check & Test' + runs-on: ubuntu-latest + timeout-minutes: 10 + + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_USER: ${{ env.DB_USER }} + POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }} + POSTGRES_DB: ${{ env.DB_NAME }} + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'pnpm' + + - name: Install + run: | + echo "::group::pnpm install" + pnpm install --frozen-lockfile + echo "::endgroup::" + + - name: Drift Check + env: + DATABASE_URL: ${{ env.DB_URL }} + run: | + echo "::group::Drizzle Check" + pnpm drizzle-kit check + echo "::endgroup::" + + - name: Seed + run: | + echo "::group::Seed Data" + # pnpm ts-node ./scripts/seed-ci.ts + echo "Done" + echo "::endgroup::" + + - name: Migrate + env: + DATABASE_URL: ${{ env.DB_URL }} + run: | + echo "::group::Apply Schema" + pnpm drizzle-kit push --force + echo "::endgroup::" diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 95b126c..c7cfee1 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -4,15 +4,69 @@ on: push: branches: - main + workflow_dispatch: permissions: contents: write pull-requests: write +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + jobs: release-please: runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} steps: - uses: googleapis/release-please-action@v4 + id: release with: release-type: node + + publish-release: + needs: release-please + if: ${{ needs.release-please.outputs.release_created }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + # Версия из тега (например, 1.2.3) + type=semver,pattern={{version}},value=${{ needs.release-please.outputs.tag_name }} + # Мажорная версия (например, 1) + type=semver,pattern={{major}},value=${{ needs.release-please.outputs.tag_name }} + # Всегда обновляем latest для продакшена + type=raw,value=latest + + - name: Build and push Production Image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.prod + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f812282..fac0b76 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -3,6 +3,7 @@ name: 'Close stale issues and PRs' on: schedule: - cron: '30 1 * * *' + workflow_dispatch: jobs: stale: @@ -10,10 +11,27 @@ jobs: permissions: issues: write pull-requests: write + steps: - uses: actions/stale@v9 with: - stale-issue-message: 'Эта задача давно не обновлялась. Она будет закрыта через 5 дней, если не появится новой активности.' - stale-pr-message: 'Этот PR замер. Мы закроем его через 5 дней, чтобы не копить очередь, но вы всегда можете переоткрыть его позже.' + stale-issue-message: >- + Эта задача давно не обновлялась. Она будет закрыта через 5 + дней, если не появится новой активности. 🤖 + stale-pr-message: >- + Этот PR замер. Мы закроем его через 5 дней, чтобы не + копить очередь, но вы всегда можете переоткрыть его + позже. 🤖 + days-before-stale: 30 days-before-close: 5 + + exempt-issue-labels: 'bug,priority:high,pinned,security' + exempt-pr-labels: 'dependencies,security' + exempt-all-milestones: true + + stale-issue-label: 'stale' + stale-pr-label: 'stale' + + operations-per-run: 50 + ascending: true