diff --git a/.github/workflows/bundle-analysis.yml b/.github/workflows/bundle-analysis.yml index f6dbaaae..ce0b8f11 100644 --- a/.github/workflows/bundle-analysis.yml +++ b/.github/workflows/bundle-analysis.yml @@ -1,21 +1,62 @@ -name: Bundle Size Analysis +name: Bundle Size Monitoring on: - workflow_dispatch: + push: + branches: [main, dev, develop] pull_request: - branches: [main] + branches: [main, dev, develop] + workflow_dispatch: + +env: + EXPO_NO_TELEMETRY: 1 jobs: - analyze: + bundle-size: + name: Bundle Size Check & Regression Alert runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - - uses: ./.github/actions/setup-node + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci --legacy-peer-deps + - name: Install web dependencies run: npm install react-dom@19.0.0 react-native-web@0.20.0 @expo/metro-runtime@5.0.5 --no-save --legacy-peer-deps - - run: npx expo export --platform web --output-dir dist - continue-on-error: true - - name: Analyze bundle + + - name: Build web bundle + run: npx expo export --platform web --output-dir dist + + - name: Run size-limit with regression prevention + run: npx size-limit + + - name: Generate bundle size report + run: npx size-limit --json > bundle-size-report.json + + - name: Comment PR with bundle size changes + if: github.event_name == 'pull_request' + uses: andresz1/size-limit-action@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + build_script: 'build' + + - name: Upload bundle size report + if: always() + uses: actions/upload-artifact@v4 + with: + name: bundle-size-report-${{ github.sha }} + path: bundle-size-report.json + retention-days: 30 + + - name: Alert on bundle size regression + if: failure() run: | - npx size-limit || true - echo "Bundle size analysis complete" + echo "::warning::Bundle size regression detected!" + echo "::error::Bundle size exceeds configured limits. Review the bundle-size-report artifact for details." + exit 1 diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 00000000..01b98ab8 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,71 @@ +name: Changelog & Release Notes + +on: + push: + branches: [main, dev, develop] + pull_request: + branches: [main, dev, develop] + workflow_dispatch: + +jobs: + changelog: + name: Generate Changelog from Conventional Commits + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Validate conventional commits + if: github.event_name == 'pull_request' + run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.sha }} --verbose + + - name: Generate changelog preview + run: | + npx conventional-changelog-cli -p angular -i CHANGELOG.md -s -r 0 || true + echo "Changelog preview generated" + cat CHANGELOG.md || true + + - name: Upload changelog preview + if: always() + uses: actions/upload-artifact@v4 + with: + name: changelog-preview + path: CHANGELOG.md + if-no-files-found: ignore + + semantic-release-dry-run: + name: Semantic Release Dry Run + runs-on: ubuntu-latest + needs: changelog + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Run semantic-release dry-run + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npx semantic-release --dry-run || true diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..438aadc8 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,67 @@ +name: Code Coverage Reporting + +on: + push: + branches: [main, dev, develop] + pull_request: + branches: [main, dev, develop] + workflow_dispatch: + +env: + NODE_VERSION: '20' + +jobs: + coverage: + name: Code Coverage with Thresholds + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Cache node modules + uses: actions/cache@v4 + id: cache-node-modules + with: + path: node_modules + key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: npm ci --legacy-peer-deps + + - name: Run tests with coverage + run: npm run test:coverage + env: + NODE_ENV: test + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: ./coverage/lcov.info + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + continue-on-error: true + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report-${{ github.sha }} + path: coverage/ + retention-days: 30 + + - name: Comment coverage on PR + if: github.event_name == 'pull_request' + uses: romeovs/lcov-coverage-report-action@v0.4.0 + with: + lcov-file: ./coverage/lcov.info + minimum-coverage: 80 + fail-on-coverage-decrease: true diff --git a/.size-limit.json b/.size-limit.json index 5575112a..0a430fec 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -3,24 +3,28 @@ "name": "Web JS Bundle", "path": ["dist/_expo/static/js/**/*.js"], "limit": "420 KB", - "gzip": true + "gzip": true, + "preventRegression": true }, { "name": "Web Total Assets", "path": ["dist/**/*.{js,css}"], "limit": "630 KB", - "gzip": true + "gzip": true, + "preventRegression": true }, { "name": "Native iOS Bundle", "path": ["dist/bundles/ios-*.js", "dist/bundles/*.ios.js"], "limit": "1.4 MB", - "gzip": true + "gzip": true, + "preventRegression": true }, { "name": "Native Android Bundle", "path": ["dist/bundles/android-*.js", "dist/bundles/*.android.js"], "limit": "1.4 MB", - "gzip": true + "gzip": true, + "preventRegression": true } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 6361e43e..9cfc01a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,87 @@ # Changelog All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) +and [Conventional Commits](https://www.conventionalcommits.org/). + +## [Unreleased] + +### Added +- Automated changelog generation from conventional commits via semantic-release. +- GitHub Actions workflow for changelog preview and validation. +- Conventional commit validation in CI with strict commitlint rules. + +### Changed +- Enforced conventional commit format for all PRs and pushes. +- Updated `.releaserc` to generate `CHANGELOG.md` on release. + +### Documentation +- Added changelog generation examples and usage instructions. + +--- + +## Automated Changelog Process + +This repository uses [semantic-release](https://github.com/semantic-release/semantic-release) +with the [conventional commits](https://www.conventionalcommits.org/) specification. + +### How It Works + +1. Contributors write commits following the conventional commit format: + - `feat: add new feature` + - `fix: resolve bug in billing cycle` + - `docs: update README` + - `chore: update dependencies` + +2. On every push to `main`, the Release workflow triggers semantic-release. + +3. semantic-release: + - Analyzes commit messages since the last release. + - Determines the next version (`patch`, `minor`, or `major`). + - Generates release notes from conventional commits. + - Updates `CHANGELOG.md` automatically. + - Creates a GitHub Release. + - Publishes to npm (if configured). + +### Commit Format Examples + +```bash +# Feature +feat(subscription): add grace period support + +# Bug fix +fix(billing): resolve double-charge edge case + +# Documentation +docs(api): add payment webhook examples + +# Breaking change +feat(api)!: change subscription status enum + +# Chore +chore(deps): upgrade react-native to 0.73 +``` + +### Release Workflow + +| Trigger | Action | +|---------|--------| +| Push to `main` | semantic-release analyzes commits and may publish | +| PR to `main` | commitlint validates commit messages | +| Manual dispatch | semantic-release dry-run preview | + +### Local Development + +Preview the changelog locally: + +```bash +npx conventional-changelog-cli -p angular -i CHANGELOG.md -s -r 0 +``` + +Validate commit messages locally: + +```bash +npx commitlint --from HEAD~1 --to HEAD --verbose +``` diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..7c03a384 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,47 @@ +codecov: + require_ci_to_pass: yes + +coverage: + status: + project: + default: + target: 80% + threshold: 2% + informational: false + patch: + default: + target: 80% + threshold: 5% + informational: false + changes: + default: + target: 80% + threshold: 5% + informational: false + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,tree" + behavior: default + require_changes: yes + require_base: yes + require_head: yes + +ignore: + - "src/**/*.d.ts" + - "src/**/index.ts" + - "src/**/*.stories.tsx" + - "src/**/*.test.tsx" + - "src/**/*.test.ts" + - "e2e/**" + - "backend/**" + - "contracts/**" + - "app/**" + - "src/animations/**" diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 7d1d3317..0549a95d 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -1,11 +1,23 @@ module.exports = { extends: ['@commitlint/config-conventional'], rules: { - 'header-max-length': [0, 'always'], - 'body-max-line-length': [0, 'always'], - 'subject-case': [0, 'always'], - 'type-empty': [0, 'always'], - 'subject-empty': [0, 'always'], - 'subject-full-stop': [0, 'never'], + 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert']], + 'type-case': [2, 'always', 'lower-case'], + 'type-empty': [2, 'never'], + 'subject-empty': [2, 'never'], + 'subject-case': [1, 'always', 'lower-case'], + 'subject-full-stop': [2, 'never', '.'], + 'header-max-length': [2, 'always', 100], + 'body-leading-blank': [1, 'always'], + 'body-case': [0, 'always'], + 'body-max-line-length': [2, 'always', 100], + 'footer-leading-blank': [1, 'always'], + 'footer-max-line-length': [2, 'always', 100], + }, + parserPreset: { + parserOpts: { + headerPattern: /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|release)(\(.+\))?!?:\s(.+)$/, + headerCorrespondence: ['type', 'scope', 'subject'], + }, }, }; diff --git a/jest.config.js b/jest.config.js index cdec0bc0..dd43b32f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,22 @@ module.exports = { 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|@walletconnect/.*)', ], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/index.ts'], + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/index.ts', + '!src/**/*.stories.tsx', + '!src/**/*.test.tsx', + '!src/**/*.test.ts' + ], + coverageThresholds: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80 + } + }, testMatch: ['**/__tests__/**/*.(test|spec).[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], modulePathIgnorePatterns: ['/e2e'], testPathIgnorePatterns: [ diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..2cef3839 --- /dev/null +++ b/renovate.json @@ -0,0 +1,65 @@ +{ + "extends": ["config:base"], + "packageRules": [ + { + "matchUpdateTypes": ["patch"], + "groupName": "patch dependencies", + "automerge": true, + "automergeType": "branch" + }, + { + "matchUpdateTypes": ["minor"], + "groupName": "minor dependencies", + "automerge": false + }, + { + "matchDepTypes": ["devDependencies"], + "matchUpdateTypes": ["minor", "patch"], + "groupName": "devDependencies (non-major)", + "automerge": true + }, + { + "matchDepTypes": ["dependencies"], + "matchUpdateTypes": ["minor", "patch"], + "groupName": "dependencies (non-major)", + "automerge": false + }, + { + "matchPackagePatterns": ["^@types/"], + "groupName": "type definitions", + "automerge": true + }, + { + "matchPackagePatterns": ["eslint", "prettier", "typescript"], + "groupName": "linting and formatting", + "automerge": false + }, + { + "matchPackagePatterns": ["react", "react-native", "expo"], + "groupName": "react and expo", + "automerge": false, + "labels": ["dependencies", "react"] + }, + { + "matchPackagePatterns": ["@stellar", "stellar-sdk", "soroban"], + "groupName": "stellar dependencies", + "automerge": false, + "labels": ["dependencies", "stellar"] + } + ], + "vulnerabilityAlerts": { + "enabled": true, + "automerge": false, + "labels": ["dependencies", "security"] + }, + "prConcurrentLimit": 5, + "prHourlyLimit": 2, + "schedule": ["before 6am on monday"], + "timezone": "UTC", + "labels": ["dependencies", "renovate"], + "assignees": ["Smartdevs17"], + "reviewers": ["Smartdevs17"], + "semanticCommits": "enabled", + "semanticCommitType": "chore", + "semanticCommitScope": "deps" +}