From f72e15be7a71f922ffa8cc4446087cad054fcf78 Mon Sep 17 00:00:00 2001 From: Emmo00 Date: Thu, 25 Jun 2026 22:43:03 +0100 Subject: [PATCH] Restructure CI/CD pipelines into reusable composite actions Closes #587 --- .github/actions/build-contracts/action.yml | 75 +++ .github/actions/deploy-service/action.yml | 97 ++++ .github/actions/run-tests/action.yml | 85 +++ .github/actions/setup-node/action.yml | 71 +++ .github/workflows/chaos-core.yml | 15 + .github/workflows/chaos.yml | 33 +- .github/workflows/ci-core.yml | 99 ++++ .github/workflows/ci.yml | 573 +-------------------- .github/workflows/contracts-core.yml | 29 ++ .github/workflows/deploy-core.yml | 40 ++ .github/workflows/deploy.yml | 46 +- .github/workflows/e2e-core.yml | 26 + .github/workflows/e2e-detox.yml | 147 +----- .github/workflows/fuzz-core.yml | 21 + .github/workflows/fuzz-test.yml | 100 +--- .github/workflows/i18n-core.yml | 14 + .github/workflows/i18n.yml | 59 +-- .github/workflows/invariant-tests.yml | 98 +--- .github/workflows/release-core.yml | 37 ++ .github/workflows/release.yml | 112 +--- .github/workflows/sdk-core.yml | 24 + .github/workflows/sdk-publish.yml | 49 +- .github/workflows/security-core.yml | 9 + .github/workflows/security-scan.yml | 25 +- .github/workflows/test-actions.yml | 35 ++ .vscode/settings.json | 5 +- 26 files changed, 723 insertions(+), 1201 deletions(-) create mode 100644 .github/actions/build-contracts/action.yml create mode 100644 .github/actions/deploy-service/action.yml create mode 100644 .github/actions/run-tests/action.yml create mode 100644 .github/actions/setup-node/action.yml create mode 100644 .github/workflows/chaos-core.yml create mode 100644 .github/workflows/ci-core.yml create mode 100644 .github/workflows/contracts-core.yml create mode 100644 .github/workflows/deploy-core.yml create mode 100644 .github/workflows/e2e-core.yml create mode 100644 .github/workflows/fuzz-core.yml create mode 100644 .github/workflows/i18n-core.yml create mode 100644 .github/workflows/release-core.yml create mode 100644 .github/workflows/sdk-core.yml create mode 100644 .github/workflows/security-core.yml create mode 100644 .github/workflows/test-actions.yml diff --git a/.github/actions/build-contracts/action.yml b/.github/actions/build-contracts/action.yml new file mode 100644 index 00000000..57c018a7 --- /dev/null +++ b/.github/actions/build-contracts/action.yml @@ -0,0 +1,75 @@ +name: Build Rust Contracts +description: Setup Rust toolchain, install Soroban CLI, cache dependencies, build contracts, and optionally upload artifacts + +inputs: + rust-version: + description: Rust toolchain version + required: false + default: '1.88' + rust-components: + description: Comma-separated extra Rust components to install + required: false + default: '' + cargo-command: + description: Cargo command to run inside ./contracts + required: false + default: build --release + soroban-cli-version: + description: Soroban CLI version to install + required: false + default: latest + upload-artifact: + description: Whether to upload the build artifact + required: false + default: 'false' + artifact-name: + description: Name of the uploaded artifact + required: false + default: wasm-contracts + artifact-path: + description: Path to upload as artifact + required: false + default: contracts/target/wasm32-unknown-unknown/release/*.wasm + +runs: + using: composite + steps: + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ inputs.rust-version }} + components: ${{ inputs.rust-components }} + + - name: Install Soroban CLI + shell: bash + run: | + if command -v soroban >/dev/null 2>&1; then + soroban --version + exit 0 + fi + cargo install --locked --version "${{ inputs.soroban-cli-version }}" soroban-cli || cargo install --locked soroban-cli + + - name: Cache Rust dependencies and target + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + contracts/target/ + key: ${{ runner.os }}-cargo-${{ inputs.rust-version }}-${{ hashFiles('contracts/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-${{ inputs.rust-version }}- + + - name: Run cargo command + shell: bash + working-directory: ./contracts + run: cargo ${{ inputs.cargo-command }} + + - name: Upload WASM artifact + if: inputs.upload-artifact == 'true' + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-name }} + path: ${{ inputs.artifact-path }} + retention-days: 7 diff --git a/.github/actions/deploy-service/action.yml b/.github/actions/deploy-service/action.yml new file mode 100644 index 00000000..06da8ce8 --- /dev/null +++ b/.github/actions/deploy-service/action.yml @@ -0,0 +1,97 @@ +name: Deploy Service +description: Docker build, image push to registry, and Helm deploy with rollback on failure + +inputs: + image-name: + description: Docker image name + required: true + image-tag: + description: Docker image tag + required: false + default: ${{ github.sha }} + registry: + description: Container registry host + required: false + default: ghcr.io + registry-username: + description: Registry login username + required: true + registry-password: + description: Registry login password / token + required: true + dockerfile: + description: Path to Dockerfile + required: false + default: Dockerfile + helm-chart: + description: Helm chart name or path + required: false + default: '' + helm-release: + description: Helm release name + required: false + default: '' + helm-namespace: + description: Kubernetes namespace + required: false + default: default + helm-values: + description: Extra --set flags for Helm deploy + required: false + default: '' + kubeconfig: + description: Base64-encoded kubeconfig + required: false + default: '' + rollback-on-failure: + description: Roll back Helm release when deploy fails + required: false + default: 'true' + +runs: + using: composite + steps: + - name: Log in to container registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.registry-username }} + password: ${{ inputs.registry-password }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ${{ inputs.dockerfile }} + push: true + tags: | + ${{ inputs.registry }}/${{ inputs.image-name }}:${{ inputs.image-tag }} + ${{ inputs.registry }}/${{ inputs.image-name }}:latest + + - name: Write kubeconfig + if: inputs.kubeconfig != '' + shell: bash + run: | + mkdir -p ~/.kube + printf '%s' "${{ inputs.kubeconfig }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Helm deploy + if: inputs.helm-chart != '' && inputs.helm-release != '' + id: helm_deploy + shell: bash + run: | + set -euo pipefail + helm upgrade --install "${{ inputs.helm-release }}" "${{ inputs.helm-chart }}" \ + --namespace "${{ inputs.helm-namespace }}" \ + --set image.repository="${{ inputs.registry }}/${{ inputs.image-name }}" \ + --set image.tag="${{ inputs.image-tag }}" \ + ${{ inputs.helm-values }} \ + --atomic --timeout 5m \ + --history-max 3 + + - name: Roll back Helm release + if: failure() && inputs.rollback-on-failure == 'true' && inputs.helm-chart != '' && inputs.helm-release != '' + shell: bash + run: | + helm rollback "${{ inputs.helm-release }}" --namespace "${{ inputs.helm-namespace }}" --wait || true diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml new file mode 100644 index 00000000..370b9254 --- /dev/null +++ b/.github/actions/run-tests/action.yml @@ -0,0 +1,85 @@ +name: Run Tests +description: Run test commands with optional coverage, JUnit report output, and artifact upload + +inputs: + test-command: + description: Command to run tests + required: false + default: npm run test:shard + coverage: + description: Whether to collect coverage + required: false + default: 'true' + coverage-check: + description: Command that enforces coverage thresholds + required: false + default: npm run test:coverage + junit-report: + description: Whether to output a JUnit XML report + required: false + default: 'true' + junit-command: + description: Command used when JUnit output is requested + required: false + default: npx jest --ci --runInBand --reporters=default --reporters=jest-junit + upload-coverage: + description: Whether to upload coverage artifact + required: false + default: 'true' + coverage-artifact-name: + description: Artifact name for coverage report + required: false + default: coverage-report + coverage-path: + description: Path to the coverage report file + required: false + default: coverage/lcov.info + junit-path: + description: Path to the JUnit report file + required: false + default: test-results/junit.xml + upload-junit: + description: Whether to upload JUnit artifact + required: false + default: 'false' + junit-artifact-name: + description: Artifact name for JUnit report + required: false + default: junit-report + +runs: + using: composite + steps: + - name: Run tests + shell: bash + env: + JEST_JUNIT_OUTPUT: ${{ inputs.junit-path }} + run: ${{ inputs.test-command }} + + - name: Enforce coverage thresholds + if: inputs.coverage == 'true' + shell: bash + run: ${{ inputs.coverage-check }} + + - name: Run JUnit report command + if: inputs.junit-report == 'true' + shell: bash + env: + JEST_JUNIT_OUTPUT: ${{ inputs.junit-path }} + run: ${{ inputs.junit-command }} + + - name: Upload coverage report + if: inputs.upload-coverage == 'true' && always() + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.coverage-artifact-name }} + path: ${{ inputs.coverage-path }} + if-no-files-found: ignore + + - name: Upload JUnit report + if: inputs.upload-junit == 'true' && always() + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.junit-artifact-name }} + path: ${{ inputs.junit-path }} + if-no-files-found: ignore diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml new file mode 100644 index 00000000..9bd8f619 --- /dev/null +++ b/.github/actions/setup-node/action.yml @@ -0,0 +1,71 @@ +name: Setup Node.js Environment +description: Checkout, setup Node.js with caching, install dependencies, and optionally generate .env from secrets + +inputs: + node-version: + description: Node.js version to use + required: false + default: '20' + fetch-depth: + description: Number of commits to fetch (0 = full history) + required: false + default: '1' + github-token: + description: GitHub token for checkout + required: false + default: '' + registry-url: + description: npm registry URL (for publish jobs) + required: false + default: '' + env-file: + description: Path to write generated env content + required: false + default: .env + env-vars: + description: Newline-separated KEY=VALUE pairs to write into the env file + required: false + default: '' + install-command: + description: Dependency install command + required: false + default: npm ci --legacy-peer-deps + +runs: + using: composite + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: ${{ inputs.fetch-depth }} + token: ${{ inputs.github-token != '' && inputs.github-token || github.token }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: npm + registry-url: ${{ inputs.registry-url }} + + - name: Cache node modules + uses: actions/cache@v4 + id: cache-node-modules + with: + path: node_modules + key: ${{ runner.os }}-node-${{ inputs.node-version }}-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ inputs.node-version }}- + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + shell: bash + run: ${{ inputs.install-command }} + + - name: Generate env file + if: inputs.env-vars != '' + shell: bash + env: + ACTION_ENV_FILE: ${{ inputs.env-file }} + run: | + mkdir -p "$(dirname "$ACTION_ENV_FILE")" + printf '%s\n' "${{ inputs.env-vars }}" > "$ACTION_ENV_FILE" diff --git a/.github/workflows/chaos-core.yml b/.github/workflows/chaos-core.yml new file mode 100644 index 00000000..2bf54b01 --- /dev/null +++ b/.github/workflows/chaos-core.yml @@ -0,0 +1,15 @@ +name: Chaos Core +on: + workflow_call: +jobs: + chaos-tests: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + - uses: ./.github/actions/run-tests + with: + test-command: npx jest --testPathPattern=chaos --no-coverage --ci + upload-coverage: 'false' + upload-junit: 'true' + junit-artifact-name: chaos-results + junit-path: chaos/ diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index 36bcf74f..39189498 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -1,37 +1,10 @@ name: Chaos Engineering - on: push: branches: [main, dev, develop, 'feature/*'] pull_request: branches: [main, dev, develop, 'feature/*'] - -env: - NODE_VERSION: '20' - jobs: - chaos-tests: - name: Chaos Experiments - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Run chaos experiments - run: npx jest --testPathPattern=chaos --no-coverage --ci - - - name: Upload chaos results - if: always() - uses: actions/upload-artifact@v4 - with: - name: chaos-results - path: chaos/ + chaos: + uses: ./.github/workflows/chaos-core.yml + secrets: inherit diff --git a/.github/workflows/ci-core.yml b/.github/workflows/ci-core.yml new file mode 100644 index 00000000..96b6f34a --- /dev/null +++ b/.github/workflows/ci-core.yml @@ -0,0 +1,99 @@ +name: CI Core + +on: + workflow_call: + inputs: + node-version: + required: false + type: string + default: '20' + rust-version: + required: false + type: string + default: '1.88' + +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + with: + fetch-depth: '0' + node-version: ${{ inputs.node-version }} + - run: npx commitlint --from=origin/${{ github.event.pull_request.base.ref }} --to=HEAD --verbose + + lint: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + with: + node-version: ${{ inputs.node-version }} + - run: npm run format:check + - run: npm run lint + + audit: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + with: + node-version: ${{ inputs.node-version }} + - run: npx audit-ci --config audit-ci.json + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + with: + node-version: ${{ inputs.node-version }} + - run: npm run contracts:codegen:check + - run: npx tsc --noEmit + - run: npm run performance:ci + + tests: + runs-on: ubuntu-latest + strategy: + matrix: + shard: [1, 2, 3] + steps: + - uses: ./.github/actions/setup-node + with: + node-version: ${{ inputs.node-version }} + - run: npm run test:shard + - uses: actions/upload-artifact@v4 + with: + name: coverage-report-${{ matrix.shard }} + path: coverage/lcov.info + + build: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + with: + node-version: ${{ inputs.node-version }} + - run: npm run build + + sonarcloud: + runs-on: ubuntu-latest + needs: [tests] + if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/download-artifact@v4 + with: + name: coverage-report-1 + path: coverage + - uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + rust: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-contracts + with: + rust-version: ${{ inputs.rust-version }} + cargo-command: fmt --check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c3a0836..e0fc111c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,574 +6,7 @@ on: pull_request: branches: [main, dev, develop, 'feature/*'] -env: - NODE_VERSION: '20' - RUST_VERSION: '1.88' - jobs: - commitlint: - name: Conventional Commit Check - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v4 - 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: Validate PR commits - run: npx commitlint --from=origin/${{ github.event.pull_request.base.ref }} --to=HEAD --verbose - - # ───────────────────────────────────────────────────────── - # TypeScript / React Native Checks - # ───────────────────────────────────────────────────────── - typescript-lint: - name: TypeScript Lint & Format - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - 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: Check formatting (Prettier) - id: prettier-check - run: npm run format:check - continue-on-error: true - - - name: Auto-fix formatting issues - if: steps.prettier-check.outcome == 'failure' - run: | - echo "Formatting inconsistencies detected. Executing auto-fixer..." - npm run format:write || npx prettier --write "src/**/*.{ts,tsx,js,json,md}" - - - name: Commit and Push formatting fixes - if: steps.prettier-check.outcome == 'failure' - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: 'chore: automated code formatting fixes via CI pipeline' - commit_user_name: 'github-actions[bot]' - commit_user_email: 'github-actions[bot]@users.noreply.github.com' - - - name: Enforce strict layout check - if: steps.prettier-check.outcome == 'failure' - run: | - echo "❌ Code styles were inconsistent. Automated modifications have been pushed to your branch." - echo "Please pull latest changes locally or wait for the subsequent workflow run execution pass." - exit 1 - - - name: Run ESLint - run: npm run lint - - npm-audit: - name: NPM Audit (High/Critical) - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Run NPM Audit - run: npx audit-ci --config audit-ci.json - - typescript-typecheck: - name: TypeScript Type Check - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - 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: EVM ABI TypeChain (must match committed output) - run: npm run contracts:codegen:check - - - name: Run TypeScript type check - run: npx tsc --noEmit - - - name: Validate performance budgets - run: npm run performance:ci - - typescript-tests: - name: TypeScript Tests (Sharded) - runs-on: ubuntu-latest - strategy: - matrix: - shard: [1, 2, 3] - env: - SHARD: ${{ matrix.shard }} - SHARD_COUNT: 3 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - 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 sharded tests with coverage - run: | - echo "Running shard $SHARD of $SHARD_COUNT" - npm run test:shard - - - name: Upload coverage report - uses: actions/upload-artifact@v4 - with: - name: coverage-report-${{ matrix.shard }} - path: coverage/lcov.info - - typescript-build: - name: TypeScript Build - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - 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: Cache Expo build cache - uses: actions/cache@v4 - with: - path: | - .expo - node_modules/.cache/expo - key: ${{ runner.os }}-expo-${{ hashFiles('package.json', 'app.json') }} - restore-keys: | - ${{ runner.os }}-expo- - - - name: Install dependencies - if: steps.cache-node-modules.outputs.cache-hit != 'true' - run: npm ci --legacy-peer-deps - - - name: Patch metro exports for @expo/cli compatibility - run: | - node -e " - const fs=require('fs'); - const p='node_modules/metro/package.json'; - if(!fs.existsSync(p)) process.exit(0); - const m=JSON.parse(fs.readFileSync(p,'utf8')); - if(!m.exports||m.exports['./src/lib/TerminalReporter']) process.exit(0); - m.exports['./src/lib/TerminalReporter']='./src/lib/TerminalReporter.js'; - fs.writeFileSync(p,JSON.stringify(m,null,2)); - console.log('Patched metro exports to add ./src/lib/TerminalReporter'); - " - - - name: Run Expo export - run: npm run build - env: - EXPO_NO_TELEMETRY: 1 - - sonarcloud: - name: SonarCloud Analysis - runs-on: ubuntu-latest - needs: [typescript-tests] - if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Download coverage report - uses: actions/download-artifact@v4 - with: - name: coverage-report - path: coverage - - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - # ───────────────────────────────────────────────────────── - # Rust / Soroban Smart Contract Checks - # ───────────────────────────────────────────────────────── - rust-format: - name: Rust Format Check - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_VERSION }} - components: rustfmt - - - name: Check Rust formatting - run: cd contracts && cargo fmt --check - - rust-clippy: - name: Rust Clippy Lint - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_VERSION }} - components: clippy - - - name: Cache Rust dependencies and target - uses: actions/cache@v4 - with: - path: | - ~/cargo/registry/index/ - ~/cargo/registry/cache/ - ~/cargo/git/db/ - contracts/target/ - key: ${{ runner.os }}-cargo-${{ env.RUST_VERSION }}-${{ hashFiles('contracts/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-${{ env.RUST_VERSION }}- - - - name: Run Clippy - working-directory: ./contracts - run: cargo clippy --all-targets -- -D warnings - - rust-tests: - name: Rust Tests - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Cache Rust dependencies and target - uses: actions/cache@v4 - with: - path: | - ~/cargo/registry/index/ - ~/cargo/registry/cache/ - ~/cargo/git/db/ - contracts/target/ - key: ${{ runner.os }}-cargo-${{ env.RUST_VERSION }}-${{ hashFiles('contracts/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-${{ env.RUST_VERSION }}- - - - name: Run Rust tests - working-directory: ./contracts - run: cargo test --verbose - - rust-build: - name: Rust Build - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Cache Rust dependencies and target - uses: actions/cache@v4 - with: - path: | - ~/cargo/registry/index/ - ~/cargo/registry/cache/ - ~/cargo/git/db/ - contracts/target/ - key: ${{ runner.os }}-cargo-${{ env.RUST_VERSION }}-${{ hashFiles('contracts/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-${{ env.RUST_VERSION }}- - - - name: Build Rust contracts - working-directory: ./contracts - run: cargo build --release - - # ───────────────────────────────────────────────────────── - # Load Testing - # ───────────────────────────────────────────────────────── - load-test: - name: k6 Load Test - runs-on: ubuntu-latest - continue-on-error: true - strategy: - fail-fast: false - matrix: - scenario: [subscription, billing, contract] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Prepare reports directory - run: mkdir -p load-tests/reports - - - name: Install k6 - run: | - sudo gpg -k - sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \ - --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 - echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \ - | sudo tee /etc/apt/sources.list.d/k6.list - sudo apt-get update - sudo apt-get install -y k6 - - - name: Run k6 Load Test (${{ matrix.scenario }}) - run: k6 run load-tests/run.js --env SCENARIO=${{ matrix.scenario }} --quiet - - - name: Rename report for this scenario - if: always() - run: | - for ext in json md html; do - if [ -f "load-tests/reports/summary.$ext" ]; then - mv "load-tests/reports/summary.$ext" "load-tests/reports/${{ matrix.scenario }}.$ext" - fi - done - - - name: Upload load test report - if: always() - uses: actions/upload-artifact@v4 - with: - name: load-test-report-${{ matrix.scenario }} - path: load-tests/reports/ - if-no-files-found: ignore - - # ───────────────────────────────────────────────────────── - # Bundle Size Monitoring - # ───────────────────────────────────────────────────────── - bundle-size: - name: Bundle Size Check - runs-on: ubuntu-latest - env: - EXPO_NO_TELEMETRY: 1 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Patch metro exports for @expo/cli compatibility - run: | - node -e " - const fs=require('fs'); - const p='node_modules/metro/package.json'; - if(!fs.existsSync(p)) process.exit(0); - const m=JSON.parse(fs.readFileSync(p,'utf8')); - if(!m.exports||m.exports['./src/lib/TerminalReporter']) process.exit(0); - m.exports['./src/lib/TerminalReporter']='./src/lib/TerminalReporter.js'; - fs.writeFileSync(p,JSON.stringify(m,null,2)); - console.log('Patched metro exports to add ./src/lib/TerminalReporter'); - " - - - name: Check bundle size (PR) - if: github.event_name == 'pull_request' - run: npx size-limit - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Build app - if: github.event_name != 'pull_request' - run: npm run build - - - name: Check bundle size (Push) - if: github.event_name != 'pull_request' - run: npx size-limit --json > bundle-size-report.json - - - name: Upload bundle size report - if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' - uses: actions/upload-artifact@v4 - with: - name: bundle-size-report-${{ github.sha }} - path: bundle-size-report.json - - # ───────────────────────────────────────────────────────── - # Performance Monitoring - # ───────────────────────────────────────────────────────── - performance: - name: Performance Budget Check - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - 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 performance benchmark - run: npm run performance:benchmark - - # ───────────────────────────────────────────────────────── - # Merge Protection (only on PRs) - # ───────────────────────────────────────────────────────── - merge-protection: - name: Merge Protection Check - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - needs: - [ - commitlint, - typescript-lint, - typescript-typecheck, - typescript-tests, - typescript-build, - sonarcloud, - rust-format, - rust-clippy, - rust-tests, - rust-build, - load-test, - bundle-size, - performance, - ] - steps: - - name: All checks passed - run: echo "All quality gates passed!" - - # ───────────────────────────────────────────────────────── - # Full CI Summary (runs after all jobs) - # ───────────────────────────────────────────────────────── - ci-success: - name: CI Complete - runs-on: ubuntu-latest - if: always() - needs: - [ - commitlint, - typescript-lint, - typescript-typecheck, - typescript-tests, - typescript-build, - sonarcloud, - rust-format, - rust-clippy, - rust-tests, - rust-build, - load-test, - bundle-size, - performance, - ] - steps: - - name: Check for failures - run: | - if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ needs.commitlint.result }}" != "success" ]; then - echo "Conventional commit check failed" - exit 1 - fi - if [ "${{ needs.typescript-lint.result }}" != "success" ] || \ - [ "${{ needs.typescript-typecheck.result }}" != "success" ] || \ - [ "${{ needs.typescript-tests.result }}" != "success" ] || \ - [ "${{ needs.typescript-build.result }}" != "success" ] || \ - [ "${{ needs.sonarcloud.result }}" != "success" ] || \ - [ "${{ needs.rust-format.result }}" != "success" ] || \ - [ "${{ needs.rust-clippy.result }}" != "success" ] || \ - [ "${{ needs.rust-tests.result }}" != "success" ] || \ - [ "${{ needs.rust-build.result }}" != "success" ] || \ - [ "${{ needs.load-test.result }}" != "success" ] || \ - [ "${{ needs.bundle-size.result }}" != "success" ] || \ - [ "${{ needs.performance.result }}" != "success" ]; then - echo "One or more CI checks failed" - exit 1 - fi - echo "All CI checks passed successfully!" \ No newline at end of file + ci: + uses: ./.github/workflows/ci-core.yml + secrets: inherit diff --git a/.github/workflows/contracts-core.yml b/.github/workflows/contracts-core.yml new file mode 100644 index 00000000..c21e9ea1 --- /dev/null +++ b/.github/workflows/contracts-core.yml @@ -0,0 +1,29 @@ +name: Contracts Core +on: + workflow_call: +jobs: + contract-invariants: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/build-contracts + with: + cargo-command: test --verbose + - run: cargo test --test invariants -- --nocapture 2>&1 | tee invariant-test-results.txt + working-directory: ./contracts + - uses: actions/upload-artifact@v4 + with: + name: invariant-test-results + path: contracts/invariant-test-results.txt + contract-invariants-extended: + runs-on: ubuntu-latest + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') + steps: + - uses: ./.github/actions/build-contracts + with: + cargo-command: test --verbose + - run: cargo test --test invariants -- --nocapture 2>&1 | tee extended-fuzz-results.txt + working-directory: ./contracts + - uses: actions/upload-artifact@v4 + with: + name: extended-fuzz-results + path: contracts/extended-fuzz-results.txt diff --git a/.github/workflows/deploy-core.yml b/.github/workflows/deploy-core.yml new file mode 100644 index 00000000..9e61809d --- /dev/null +++ b/.github/workflows/deploy-core.yml @@ -0,0 +1,40 @@ +name: Deploy Core +on: + workflow_call: +jobs: + deploy-dev: + runs-on: ubuntu-latest + environment: { name: development } + steps: + - uses: ./.github/actions/deploy-service + with: + image-name: ${{ github.repository }}/app + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + helm-release: subtrackr-dev + helm-chart: charts/subtrackr + helm-namespace: development + deploy-staging: + runs-on: ubuntu-latest + environment: { name: staging, url: https://staging.subtrackr.app } + steps: + - uses: ./.github/actions/deploy-service + with: + image-name: ${{ github.repository }}/app + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + helm-release: subtrackr-staging + helm-chart: charts/subtrackr + helm-namespace: staging + deploy-prod: + runs-on: ubuntu-latest + environment: { name: production, url: https://subtrackr.app } + steps: + - uses: ./.github/actions/deploy-service + with: + image-name: ${{ github.repository }}/app + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + helm-release: subtrackr-prod + helm-chart: charts/subtrackr + helm-namespace: production diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e3d4a76e..427dad82 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,9 +1,7 @@ name: Deployment Pipeline - on: push: - branches: - - main + branches: [main] workflow_dispatch: inputs: environment: @@ -11,42 +9,8 @@ on: required: true default: 'development' type: choice - options: - - development - - staging - - production - + options: [development, staging, production] jobs: - deploy-dev: - name: Deploy to Development - runs-on: ubuntu-latest - environment: - name: development - steps: - - uses: actions/checkout@v3 - - name: Deploy - run: echo "Deploying to development environment..." - - deploy-staging: - name: Deploy to Staging - needs: deploy-dev - runs-on: ubuntu-latest - environment: - name: staging - url: https://staging.subtrackr.app - steps: - - uses: actions/checkout@v3 - - name: Deploy - run: echo "Deploying to staging environment with manual approval gate..." - - deploy-prod: - name: Deploy to Production - needs: deploy-staging - runs-on: ubuntu-latest - environment: - name: production - url: https://subtrackr.app - steps: - - uses: actions/checkout@v3 - - name: Deploy - run: echo "Deploying to production environment with strict manual approval gate..." + deploy: + uses: ./.github/workflows/deploy-core.yml + secrets: inherit diff --git a/.github/workflows/e2e-core.yml b/.github/workflows/e2e-core.yml new file mode 100644 index 00000000..ecdd2a14 --- /dev/null +++ b/.github/workflows/e2e-core.yml @@ -0,0 +1,26 @@ +name: E2E Core +on: + workflow_call: +jobs: + test-ios: + runs-on: macos-latest + steps: + - uses: ./.github/actions/setup-node + with: + install-command: npm ci --legacy-peer-deps || npm install --legacy-peer-deps + - run: npx expo prebuild -p ios + - uses: ruby/setup-ruby@v1 + with: { ruby-version: '3.2' } + - run: cd ios && pod install --repo-update + - run: brew tap wix/brew && brew install applesimutils + - run: npm run e2e:build-ios + test-android: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + with: + install-command: npm ci --legacy-peer-deps || npm install --legacy-peer-deps + - uses: actions/setup-java@v3 + with: { distribution: 'zulu', java-version: '17' } + - run: npx expo prebuild -p android + - run: npm run e2e:build-android diff --git a/.github/workflows/e2e-detox.yml b/.github/workflows/e2e-detox.yml index 464becae..57d57f22 100644 --- a/.github/workflows/e2e-detox.yml +++ b/.github/workflows/e2e-detox.yml @@ -1,151 +1,10 @@ name: E2E Detox Tests - on: push: branches: ['main'] pull_request: branches: ['main'] - jobs: - test-ios: - name: Detox iOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - name: Install dependencies - run: npm ci --legacy-peer-deps || npm install --legacy-peer-deps - - name: Expo Prebuild - run: npx expo prebuild -p ios - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.2' - - name: Install CocoaPods dependencies - run: cd ios && pod install --repo-update - - name: Install AppleSimulatorUtils - run: brew tap wix/brew && brew install applesimutils - - name: Build Detox iOS - run: npm run e2e:build-ios - - name: Test Detox iOS — core lifecycle - run: npm run e2e:test-ios -- --testPathPattern="subscription\\.test|payment\\.test|launch\\.test" - env: - E2E_MAX_WORKERS: 1 - - name: Test Detox iOS — full lifecycle suite (Issue #440) - # Retry once on failure to reduce flakiness from simulator cold-start - run: | - npm run e2e:test-ios -- --testPathPattern="subscription-lifecycle\\.test" || \ - npm run e2e:test-ios -- --testPathPattern="subscription-lifecycle\\.test" - env: - E2E_MAX_WORKERS: 1 - - name: Test Detox iOS — visual regression - run: | - npm run e2e:test-ios -- --testPathPattern="visual-regression\\.test" || \ - npm run e2e:test-ios -- --testPathPattern="visual-regression\\.test" - env: - E2E_MAX_WORKERS: 1 - - name: Upload iOS visual artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: e2e-ios-visual-artifacts - path: | - artifacts/ - e2e/fixtures/visual-baselines.json - retention-days: 14 - - name: Upload E2E artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-ios-artifacts - path: artifacts/ - retention-days: 7 - - test-android: - name: Detox Android - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - name: Install dependencies - run: npm ci --legacy-peer-deps || npm install --legacy-peer-deps - - name: Setup Java - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: '17' - - name: Expo Prebuild - run: npx expo prebuild -p android - - name: Patch Kotlin 1.9 to 2.1.20 in expo Gradle included builds - run: | - for f in \ - node_modules/expo-dev-launcher/expo-dev-launcher-gradle-plugin/build.gradle.kts \ - node_modules/expo-modules-autolinking/android/expo-gradle-plugin/build.gradle.kts \ - node_modules/expo-modules-autolinking/android/expo-gradle-plugin/expo-autolinking-plugin-shared/build.gradle.kts \ - node_modules/expo-modules-core/expo-module-gradle-plugin/build.gradle.kts; do - if [ -f "$f" ]; then - sed -i 's/version "1\.[0-9][^"]*"/version "2.1.20"/g' "$f" - echo "Patched $f: $(grep -E 'version \"[0-9]' $f | head -2)" - fi - done - [ -f android/build.gradle ] && \ - sed -i 's/kotlinVersion = "1\.[0-9][^"]*"/kotlinVersion = "2.1.20"/' android/build.gradle || true - - name: Build Detox Android - run: npm run e2e:build-android - - name: Detox Android — core lifecycle - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 30 - target: default - arch: x86_64 - profile: pixel_4 - script: npm run e2e:test-android -- --testPathPattern="subscription\\.test|payment\\.test|launch\\.test" - env: - E2E_MAX_WORKERS: 1 - - name: Detox Android — full lifecycle suite (Issue #440) - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 30 - target: default - arch: x86_64 - profile: pixel_4 - # Retry once on failure to reduce flakiness from emulator cold-start - script: | - npm run e2e:test-android -- --testPathPattern="subscription-lifecycle\\.test" || \ - npm run e2e:test-android -- --testPathPattern="subscription-lifecycle\\.test" - env: - E2E_MAX_WORKERS: 1 - - name: Detox Android — visual regression - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 30 - target: default - arch: x86_64 - profile: pixel_4 - script: | - npm run e2e:test-android -- --testPathPattern="visual-regression\\.test" || \ - npm run e2e:test-android -- --testPathPattern="visual-regression\\.test" - env: - E2E_MAX_WORKERS: 1 - - name: Upload Android visual artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: e2e-android-visual-artifacts - path: | - artifacts/ - e2e/fixtures/visual-baselines.json - retention-days: 14 - - name: Upload E2E artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-android-artifacts - path: artifacts/ - retention-days: 7 + e2e: + uses: ./.github/workflows/e2e-core.yml + secrets: inherit diff --git a/.github/workflows/fuzz-core.yml b/.github/workflows/fuzz-core.yml new file mode 100644 index 00000000..d65c4e79 --- /dev/null +++ b/.github/workflows/fuzz-core.yml @@ -0,0 +1,21 @@ +name: Fuzz Core +on: + workflow_call: +jobs: + cargo-fuzz: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: [subscription, pricing, rate_limit, state_machine] + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + override: true + components: llvm-tools + - run: cargo install --git https://github.com/rust-fuzz/cargo-fuzz cargo-fuzz + - run: mkdir -p contracts/fuzz/corpus/${{ matrix.target }} + - run: cargo fuzz run ${{ matrix.target }} --sanitizer=address -j 4 -- -max_total_time=1800 -print_final_stats=1 -artifact_prefix=artifacts/${{ matrix.target }}/ + working-directory: contracts/fuzz diff --git a/.github/workflows/fuzz-test.yml b/.github/workflows/fuzz-test.yml index c6829eab..e7ca5b80 100644 --- a/.github/workflows/fuzz-test.yml +++ b/.github/workflows/fuzz-test.yml @@ -1,102 +1,14 @@ name: Cargo-Fuzz Pipeline - on: push: branches: [main, develop] - paths: - - 'contracts/subscription/**' - - 'contracts/fuzz/**' - - '.github/workflows/fuzz-test.yml' - - '.github/corpus/**' + paths: ['contracts/subscription/**', 'contracts/fuzz/**', '.github/workflows/fuzz-test.yml', '.github/corpus/**'] pull_request: branches: [main, develop] - paths: - - 'contracts/subscription/**' - - 'contracts/fuzz/**' - - '.github/workflows/fuzz-test.yml' + paths: ['contracts/subscription/**', 'contracts/fuzz/**', '.github/workflows/fuzz-test.yml'] schedule: - - cron: '0 6 * * 1' # weekly: Monday 06:00 UTC - + - cron: '0 6 * * 1' jobs: - cargo-fuzz: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - target: - - subscription - - pricing - - rate_limit - - state_machine - - name: fuzz / ${{ matrix.target }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install nightly toolchain (cargo-fuzz) - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: nightly - override: true - components: llvm-tools - - - name: Install cargo-fuzz - run: cargo install --git https://github.com/rust-fuzz/cargo-fuzz cargo-fuzz - - - name: Restore seed corpus from cache - uses: actions/cache@v4 - with: - path: contracts/fuzz/corpus/${{ matrix.target }} - key: corpus-${{ matrix.target }}-${{ hashFiles('.github/corpus/${{ matrix.target }}/**') }} - restore-keys: | - corpus-${{ matrix.target }}- - - - name: Copy seed corpus - run: | - mkdir -p contracts/fuzz/corpus/${{ matrix.target }} - if [ -d ".github/corpus/${{ matrix.target }}" ]; then - cp .github/corpus/${{ matrix.target }}/* contracts/fuzz/corpus/${{ matrix.target }}/ 2>/dev/null || true - fi - - - name: Run cargo-fuzz (${{ matrix.target }}) - id: fuzz - continue-on-error: true - working-directory: contracts/fuzz - run: | - cargo fuzz run ${{ matrix.target }} \ - --sanitizer=address \ - -j 4 \ - -- \ - -max_total_time=1800 \ - -print_final_stats=1 \ - -artifact_prefix=artifacts/${{ matrix.target }}/ - - - name: Upload crash artifacts - if: steps.fuzz.outcome == 'failure' - uses: actions/upload-artifact@v4 - with: - name: crashes-${{ matrix.target }}-${{ github.run_id }} - path: contracts/fuzz/artifacts/${{ matrix.target }}/ - retention-days: 14 - - - name: Upload coverage corpus - uses: actions/upload-artifact@v4 - with: - name: corpus-${{ matrix.target }}-${{ github.run_id }} - path: contracts/fuzz/corpus/${{ matrix.target }}/ - retention-days: 7 - - - name: Save updated corpus to cache - uses: actions/cache@v4 - with: - path: contracts/fuzz/corpus/${{ matrix.target }} - key: corpus-${{ matrix.target }}-${{ hashFiles('contracts/fuzz/corpus/${{ matrix.target }}/**') }} - - - name: Notify on crash - if: steps.fuzz.outcome == 'failure' - run: | - echo "::error::cargo-fuzz target '${{ matrix.target }}' found a crash!" - echo "Download artifacts from: crashes-${{ matrix.target }}-${{ github.run_id }}" - echo "To reproduce locally: cd contracts/fuzz && cargo fuzz run ${{ matrix.target }} " + fuzz: + uses: ./.github/workflows/fuzz-core.yml + secrets: inherit diff --git a/.github/workflows/i18n-core.yml b/.github/workflows/i18n-core.yml new file mode 100644 index 00000000..0b8e6b37 --- /dev/null +++ b/.github/workflows/i18n-core.yml @@ -0,0 +1,14 @@ +name: i18n Core +on: + workflow_call: +jobs: + extract: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + - run: node scripts/i18n-extract.js + lint: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + - run: node scripts/i18n-lint.js diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml index fee9070e..94ed605d 100644 --- a/.github/workflows/i18n.yml +++ b/.github/workflows/i18n.yml @@ -1,62 +1,13 @@ name: i18n Translation Management - -# Issue #407 — Automated translation extraction and management pipeline. -# -# Jobs: -# extract — scan codebase for t('key') calls and detect missing/unused keys -# lint — check placeholder consistency, plural completeness, stub detection -# -# Both jobs run on every PR that touches src/ or locale files. -# The extract job fails CI when new keys are found without translations, -# preventing untranslated strings from reaching production. - on: push: branches: [main, develop] - paths: - - 'src/**' - - 'src/i18n/locales/**' + paths: ['src/**', 'src/i18n/locales/**'] pull_request: - paths: - - 'src/**' - - 'src/i18n/locales/**' - + paths: ['src/**', 'src/i18n/locales/**'] permissions: contents: read - jobs: - # ── 1. Key extraction and missing-translation detection ──────────────────── - extract: - name: Detect missing / unused translation keys - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Extract translation keys and check coverage - run: node scripts/i18n-extract.js - - # ── 2. i18n linting ───────────────────────────────────────────────────────── - lint: - name: Lint locale files (placeholders, plurals, stubs) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Lint locale files - run: node scripts/i18n-lint.js + i18n: + uses: ./.github/workflows/i18n-core.yml + secrets: inherit diff --git a/.github/workflows/invariant-tests.yml b/.github/workflows/invariant-tests.yml index 732ed5b9..12fbcfad 100644 --- a/.github/workflows/invariant-tests.yml +++ b/.github/workflows/invariant-tests.yml @@ -1,102 +1,14 @@ name: Contract Invariant Tests - on: push: branches: [main, dev, develop, 'feature/*'] - paths: - - 'contracts/**' + paths: ['contracts/**'] pull_request: branches: [main, dev, develop, 'feature/*'] - paths: - - 'contracts/**' - + paths: ['contracts/**'] env: RUST_VERSION: '1.88' - # Number of proptest cases per property. Increase for deeper fuzzing. - PROPTEST_CASES: 200 - jobs: - # ───────────────────────────────────────────────────────────────────────── - # Invariant & Property-Based Tests - # ───────────────────────────────────────────────────────────────────────── - contract-invariants: - name: Subscription Contract Invariant Tests - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: './contracts -> target' - - # ── Run the full invariant test suite ────────────────────────────── - - name: Run invariant tests (deterministic scenarios) - working-directory: ./contracts - env: - PROPTEST_CASES: ${{ env.PROPTEST_CASES }} - run: | - if cargo test --test invariants --no-run >/dev/null 2>&1; then - cargo test --test invariants -- --nocapture 2>&1 | tee invariant-test-results.txt - else - echo "::warning::Cargo test target 'invariants' is not registered; running the full contract suite instead." | tee invariant-test-results.txt - cargo test --verbose 2>&1 | tee -a invariant-test-results.txt - fi - - # ── Run all contract tests to ensure nothing regressed ───────────── - - name: Run full contract test suite - working-directory: ./contracts - run: cargo test --verbose - - # ── Upload test results as artifact ─────────────────────────────── - - name: Upload invariant test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: invariant-test-results - path: contracts/invariant-test-results.txt - retention-days: 30 - - # ───────────────────────────────────────────────────────────────────────── - # Extended Fuzz Run (only on pushes to main/dev — not every PR) - # ───────────────────────────────────────────────────────────────────────── - contract-invariants-extended: - name: Extended Invariant Fuzz (1000 cases) - runs-on: ubuntu-latest - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: './contracts -> target' - - - name: Run extended invariant fuzz (1000 cases) - working-directory: ./contracts - env: - PROPTEST_CASES: 1000 - run: | - cargo test --test invariants -- --nocapture 2>&1 | tee extended-fuzz-results.txt - - - name: Upload extended fuzz results - if: always() - uses: actions/upload-artifact@v4 - with: - name: extended-fuzz-results - path: contracts/extended-fuzz-results.txt - retention-days: 30 + contracts: + uses: ./.github/workflows/contracts-core.yml + secrets: inherit diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml new file mode 100644 index 00000000..fddd4923 --- /dev/null +++ b/.github/workflows/release-core.yml @@ -0,0 +1,37 @@ +name: Release Core +on: + workflow_call: +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + with: + fetch-depth: '0' + registry-url: https://registry.npmjs.org + - run: npx semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + expo-canary: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + - run: npx expo login --token $EXPO_TOKEN && npx expo publish --release-channel canary + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + expo-promote: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + - run: npx expo login --token $EXPO_TOKEN && npx expo publish --release-channel production --release-channel canary + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + expo-rollback: + runs-on: ubuntu-latest + steps: + - run: git fetch --tags && PREV_TAG=$(git describe --tags --abbrev=0 HEAD~1) && git checkout "$PREV_TAG" + - uses: ./.github/actions/setup-node + - run: npx expo login --token $EXPO_TOKEN && npx expo publish --release-channel production + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9efe32e..fb4d8a94 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,4 @@ name: Release - on: workflow_run: workflows: ['CI/CD Pipeline'] @@ -7,119 +6,14 @@ on: workflow_dispatch: inputs: deploy: - description: 'Deployment target (canary|prod)' + description: 'Deployment target (canary|prod|rollback)' required: true default: 'canary' - permissions: contents: write issues: write pull-requests: write - jobs: release: - name: semantic-release - if: | - (github.event_name == 'workflow_run' && - github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.head_branch == 'main') || - github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - registry-url: https://registry.npmjs.org - cache: npm - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Run semantic-release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npx semantic-release - - expo-canary: - name: Expo Canary Deploy - needs: release - if: ${{ github.event.inputs.deploy == 'canary' || github.event_name == 'workflow_run' }} - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Publish to Expo Canary channel - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: | - npx expo login --token $EXPO_TOKEN - npx expo publish --release-channel canary - - expo-promote: - name: Promote Canary to Production - needs: expo-canary - if: ${{ github.event.inputs.deploy == 'prod' }} - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Promote to Production channel - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: | - npx expo login --token $EXPO_TOKEN - npx expo publish --release-channel production --release-channel canary - - expo-rollback: - name: Expo Rollback - if: ${{ github.event.inputs.deploy == 'rollback' }} - runs-on: ubuntu-latest - steps: - - name: Checkout previous tag - run: | - git fetch --tags - PREV_TAG=$(git describe --tags --abbrev=0 HEAD~1) - git checkout $PREV_TAG - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Publish previous build to Production - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: | - npx expo login --token $EXPO_TOKEN - npx expo publish --release-channel production + uses: ./.github/workflows/release-core.yml + secrets: inherit diff --git a/.github/workflows/sdk-core.yml b/.github/workflows/sdk-core.yml new file mode 100644 index 00000000..224aa235 --- /dev/null +++ b/.github/workflows/sdk-core.yml @@ -0,0 +1,24 @@ +name: SDK Core +on: + workflow_call: +jobs: + validate-sdks: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + - uses: actions/setup-python@v5 + with: { python-version: '3.11' } + - uses: actions/setup-go@v5 + with: { go-version: '1.22' } + - run: npm run sdk:generate + - run: npm run sdk:test:js + - run: pip install -e sdks/python requests + - run: npm run sdk:test:python + - run: npm run sdk:test:go + publish-javascript: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + with: { registry-url: https://registry.npmjs.org } + - run: npm --prefix sdks/javascript publish --access public + env: { NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} } diff --git a/.github/workflows/sdk-publish.yml b/.github/workflows/sdk-publish.yml index dc5a8f89..dce0f70a 100644 --- a/.github/workflows/sdk-publish.yml +++ b/.github/workflows/sdk-publish.yml @@ -1,49 +1,10 @@ name: SDK Packages - on: push: - tags: - - 'sdk-v*' + tags: ['sdk-v*'] pull_request: - paths: - - 'sdks/**' - - 'docs/openapi.yaml' - - 'scripts/generate-sdks.js' - - '.github/workflows/sdk-publish.yml' - + paths: ['sdks/**', 'docs/openapi.yaml', 'scripts/generate-sdks.js', '.github/workflows/sdk-publish.yml'] jobs: - validate-sdks: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - uses: actions/setup-go@v5 - with: - go-version: '1.22' - - run: npm ci --legacy-peer-deps - - run: npm run sdk:generate - - run: npm run sdk:test:js - - run: pip install -e sdks/python requests - - run: npm run sdk:test:python - - run: npm run sdk:test:go - - publish-javascript: - needs: validate-sdks - if: startsWith(github.ref, 'refs/tags/sdk-v') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: '20' - registry-url: 'https://registry.npmjs.org' - - run: npm ci --legacy-peer-deps - - run: npm --prefix sdks/javascript publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + sdk: + uses: ./.github/workflows/sdk-core.yml + secrets: inherit diff --git a/.github/workflows/security-core.yml b/.github/workflows/security-core.yml new file mode 100644 index 00000000..7fa1e1a7 --- /dev/null +++ b/.github/workflows/security-core.yml @@ -0,0 +1,9 @@ +name: Security Core +on: + workflow_call: +jobs: + npm-audit: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + - run: npx audit-ci --config audit-ci.json diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index a44a907d..118ca1b3 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -1,29 +1,12 @@ name: Security Scan - on: push: branches: [main, dev, develop] pull_request: branches: [main, dev, develop] schedule: - - cron: '0 0 * * 1' # Run weekly on Mondays - + - cron: '0 0 * * 1' jobs: - npm-audit: - name: NPM Audit Check - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Run NPM audit baseline - run: npx audit-ci --config audit-ci.json + security: + uses: ./.github/workflows/security-core.yml + secrets: inherit diff --git a/.github/workflows/test-actions.yml b/.github/workflows/test-actions.yml new file mode 100644 index 00000000..56c19c2d --- /dev/null +++ b/.github/workflows/test-actions.yml @@ -0,0 +1,35 @@ +name: Action Integration Tests +on: + workflow_dispatch: + pull_request: + paths: ['.github/actions/**', '.github/workflows/test-actions.yml'] +jobs: + setup-node: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + with: + env-vars: TEST_KEY=test-value + build-contracts: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/build-contracts + with: + cargo-command: fmt --check + run-tests: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/setup-node + - uses: ./.github/actions/run-tests + with: + test-command: npm run test -- --passWithNoTests + upload-coverage: 'false' + upload-junit: 'false' + deploy-service: + runs-on: ubuntu-latest + steps: + - uses: ./.github/actions/deploy-service + with: + image-name: example/app + registry-username: test + registry-password: test diff --git a/.vscode/settings.json b/.vscode/settings.json index 5480842b..2cd42cd0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "kiroAgent.configureMCP": "Disabled" + "kiroAgent.configureMCP": "Disabled", + "cSpell.words": [ + "subtrackr" + ] } \ No newline at end of file