From da4250c1027a3725eb947a29fc97e49c0526234f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 00:52:29 +0000 Subject: [PATCH 01/13] chore: converge workflows to deterministic ci architecture Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/3c2458ad-668b-49fc-9f9e-5bf4a0fdb2f1 Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- .github/workflows/auto-label.yml | 127 ---- .../workflows/autonomous-oracle-pipeline.yml | 75 --- .github/workflows/ci.yml | 588 ++---------------- .github/workflows/codeql-analysis.yml | 98 --- .../complete-production-pipeline.yml | 371 ----------- .github/workflows/deploy-preview.yml | 197 ------ .github/workflows/deploy-railway-preview.yml | 358 ----------- .github/workflows/deploy-railway.yml | 230 ------- .github/workflows/deploy.yml | 67 ++ .github/workflows/docker-build.yml | 142 ----- .github/workflows/failed-job-handler.yml | 204 ------ .github/workflows/gxq-master-ci.yml | 314 ---------- .github/workflows/gxq-pr-check.yml | 153 ----- .github/workflows/gxq-scheduled-health.yml | 193 ------ .github/workflows/performance-monitoring.yml | 157 ----- .../workflows/powershell-merge-automation.yml | 298 --------- .github/workflows/rename-vercel-hosts.yml | 109 ---- .github/workflows/security.yml | 55 ++ .github/workflows/self-heal.yml | 29 + .github/workflows/self-optimize.yml | 582 ----------------- .github/workflows/sync-railway-secrets.yml | 274 -------- docs/branch-protection.md | 34 + docs/ci-audit.md | 87 +++ package.json | 7 +- scripts/converge.sh | 162 +++++ scripts/doctor.ts | 383 ++++++++++++ 26 files changed, 880 insertions(+), 4414 deletions(-) delete mode 100644 .github/workflows/auto-label.yml delete mode 100644 .github/workflows/autonomous-oracle-pipeline.yml delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/complete-production-pipeline.yml delete mode 100644 .github/workflows/deploy-preview.yml delete mode 100644 .github/workflows/deploy-railway-preview.yml delete mode 100644 .github/workflows/deploy-railway.yml create mode 100644 .github/workflows/deploy.yml delete mode 100644 .github/workflows/docker-build.yml delete mode 100644 .github/workflows/failed-job-handler.yml delete mode 100644 .github/workflows/gxq-master-ci.yml delete mode 100644 .github/workflows/gxq-pr-check.yml delete mode 100644 .github/workflows/gxq-scheduled-health.yml delete mode 100644 .github/workflows/performance-monitoring.yml delete mode 100644 .github/workflows/powershell-merge-automation.yml delete mode 100644 .github/workflows/rename-vercel-hosts.yml create mode 100644 .github/workflows/security.yml create mode 100644 .github/workflows/self-heal.yml delete mode 100644 .github/workflows/self-optimize.yml delete mode 100644 .github/workflows/sync-railway-secrets.yml create mode 100644 docs/branch-protection.md create mode 100644 docs/ci-audit.md create mode 100755 scripts/converge.sh create mode 100644 scripts/doctor.ts diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml deleted file mode 100644 index 3641889e..00000000 --- a/.github/workflows/auto-label.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: Auto-label PRs - -on: - pull_request: - types: [opened, reopened, synchronize] - workflow_dispatch: - inputs: - apply_to_all_open_prs: - description: "Also apply labels to all open PRs" - type: boolean - default: false - -permissions: - contents: read - issues: write - pull-requests: write - -jobs: - label-single: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - name: Ensure labels exist - uses: actions/github-script@v9 - with: - script: | - const labels = [ - { name: "auto-merge", color: "2ecc71", description: "Enable auto-merge when checks pass" }, - { name: "skip-deployment", color: "b31f45", description: "Skip deployment checks" }, - ]; - for (const l of labels) { - try { - await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: l.name, - }); - } catch (e) { - if (e.status === 404) { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: l.name, - color: l.color, - description: l.description, - }); - } else { - throw e; - } - } - } - - name: Add labels to this PR - uses: actions/github-script@v9 - with: - script: | - const number = context.payload.pull_request.number; - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: number, - labels: ["auto-merge", "skip-deployment"], - }); - - label-all-open: - if: github.event_name == 'workflow_dispatch' && inputs.apply_to_all_open_prs == true - runs-on: ubuntu-latest - steps: - - name: Ensure labels exist - uses: actions/github-script@v9 - with: - script: | - const labels = [ - { name: "auto-merge", color: "2ecc71", description: "Enable auto-merge when checks pass" }, - { name: "skip-deployment", color: "b31f45", description: "Skip deployment checks" }, - ]; - for (const l of labels) { - try { - await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: l.name, - }); - } catch (e) { - if (e.status === 404) { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: l.name, - color: l.color, - description: l.description, - }); - } else { - throw e; - } - } - } - - name: Add labels to all open PRs - uses: actions/github-script@v9 - with: - script: | - async function paginateAllOpenPRs() { - const results = []; - let page = 1; - while (true) { - const { data } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: "open", - per_page: 100, - page, - }); - results.push(...data); - if (data.length < 100) break; - page++; - } - return results; - } - - const prs = await paginateAllOpenPRs(); - for (const pr of prs) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - labels: ["auto-merge", "skip-deployment"], - }); - } diff --git a/.github/workflows/autonomous-oracle-pipeline.yml b/.github/workflows/autonomous-oracle-pipeline.yml deleted file mode 100644 index 112bebe3..00000000 --- a/.github/workflows/autonomous-oracle-pipeline.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Autonomous Oracle CI/CD - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - checks: write - statuses: write - -jobs: - oracle-analysis: - name: Oracle Analysis & Fix - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install Dependencies - run: npm ci - - - name: Run GXQ Autonomous Oracle - id: oracle - env: - ADMIN_TOKEN: ${{ secrets.GXQ_ADMIN_TOKEN }} - ADMIN_API_URL: ${{ secrets.GXQ_ADMIN_API_URL }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx ts-node scripts/autonomous-oracle.ts - - - name: Commit Auto-Fixes - if: always() - run: | - git config user.name "GXQ Oracle Bot" - git config user.email "oracle@gxq.studio" - git add . - if ! git diff --staged --quiet; then - git commit -m "chore: autonomous oracle auto-fix [skip ci]" - git push origin HEAD:${{ github.head_ref || github.ref_name }} - fi - - deployment-gateway: - name: Deployment Gateway - needs: oracle-analysis - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Deploy to Vercel - uses: amondnet/vercel-action@v42 - with: - vercel-token: ${{ secrets.VERCEL_TOKEN }} - vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} - vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} - vercel-args: ${{ github.ref == 'refs/heads/main' && '--prod' || '' }} - - - name: Post-Deploy Health Check - run: | - URL=${{ github.ref == 'refs/heads/main' && 'https://gxq.studio' || 'https://dev.gxq.studio' }} - echo "Checking health of $URL..." - curl -f "$URL/api/admin/status" || (echo "Health check failed!" && exit 1) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b394dbab..ed2c7c7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,559 +1,87 @@ -name: CI - Comprehensive Pipeline +name: ci on: pull_request: - branches: - - main - - develop - - dev push: branches: - main - - develop - - dev workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ci-${{ github.ref }} cancel-in-progress: true permissions: contents: read - checks: write - pull-requests: write jobs: - install: - name: Install Dependencies - runs-on: ubuntu-latest - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - node-version: [24] - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Install backend dependencies - run: npm ci - - - name: Cache backend node_modules - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-backend-${{ hashFiles('package-lock.json') }} - - - name: Install webapp dependencies - working-directory: ./webapp - run: npm ci - - - name: Cache webapp node_modules - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-webapp-${{ hashFiles('webapp/package-lock.json') }} - lint: - name: Lint (Node ${{ matrix.node-version }}) + name: lint runs-on: ubuntu-latest - needs: install - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - node-version: [24] - + timeout-minutes: 20 steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Restore backend cache - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-backend-${{ hashFiles('package-lock.json') }} - - - name: Install backend dependencies (if cache miss) - run: npm ci --prefer-offline || npm ci - - - name: Run backend ESLint - run: npm run lint - # TODO: Remove continue-on-error once lint issues are fixed (see issue #TBD) - # Currently 122 linting problems (17 errors, 105 warnings) exist in the codebase - continue-on-error: true - - - name: Restore webapp cache - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-webapp-${{ hashFiles('webapp/package-lock.json') }} - - - name: Install webapp dependencies (if cache miss) - working-directory: ./webapp - run: npm ci --prefer-offline || npm ci - - - name: Run webapp ESLint - run: npm run lint:webapp - # TODO: Remove continue-on-error once lint issues are fixed (see issue #TBD) - # Currently has linting warnings that need to be addressed - continue-on-error: true + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + webapp/package-lock.json + - run: npm ci --no-audit --no-fund + - run: npm --prefix webapp ci --no-audit --no-fund + - run: npm run lint + - run: npm run lint:webapp typecheck: - name: Type Check (Node ${{ matrix.node-version }}) - runs-on: ubuntu-latest - needs: install - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - node-version: [24] - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Restore backend cache - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-backend-${{ hashFiles('package-lock.json') }} - - - name: Install backend dependencies (if cache miss) - run: npm ci --prefer-offline || npm ci - - - name: Run backend type check - run: npm run type-check - - - name: Restore webapp cache - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-webapp-${{ hashFiles('webapp/package-lock.json') }} - - - name: Install webapp dependencies (if cache miss) - working-directory: ./webapp - run: npm ci --prefer-offline || npm ci - - - name: Run webapp type check - run: npm run type-check:webapp - - unit-tests-backend: - name: Backend Tests (Node ${{ matrix.node-version }}) - runs-on: ubuntu-latest - needs: [lint, typecheck] - timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - node-version: [24] - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Restore backend cache - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-backend-${{ hashFiles('package-lock.json') }} - - - name: Install backend dependencies (if cache miss) - run: npm ci --prefer-offline || npm ci - - - name: Run backend tests - run: npm test - env: - NODE_ENV: test - JEST_TIMEOUT: 30000 - - - name: Upload backend coverage - uses: actions/upload-artifact@v7 - if: matrix.node-version == 24 - with: - name: backend-coverage - path: coverage/ - retention-days: 7 - if-no-files-found: ignore - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v7 - with: - name: backend-test-results-node-${{ matrix.node-version }} - path: | - coverage/ - test-results/ - retention-days: 7 - if-no-files-found: ignore - - unit-tests-webapp: - name: Webapp Tests (Node ${{ matrix.node-version }}) - runs-on: ubuntu-latest - needs: [lint, typecheck] - timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - node-version: [24] - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - cache-dependency-path: webapp/package-lock.json - - - name: Restore webapp cache - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-webapp-${{ hashFiles('webapp/package-lock.json') }} - - - name: Install webapp dependencies (if cache miss) - working-directory: ./webapp - run: npm ci --prefer-offline || npm ci - - - name: Run webapp tests - run: npm run test:webapp - continue-on-error: true - env: - NODE_ENV: test - - - name: Upload webapp coverage - uses: actions/upload-artifact@v7 - if: matrix.node-version == 24 - with: - name: webapp-coverage - path: webapp/coverage/ - retention-days: 7 - if-no-files-found: ignore - - coverage-merge: - name: Merge Coverage & Upload + name: typecheck runs-on: ubuntu-latest - needs: [unit-tests-backend, unit-tests-webapp] - if: always() - timeout-minutes: 10 - + timeout-minutes: 20 steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - - - name: Download backend coverage - uses: actions/download-artifact@v8 - with: - name: backend-coverage - path: coverage/ - continue-on-error: true - - - name: Download webapp coverage - uses: actions/download-artifact@v8 - with: - name: webapp-coverage - path: webapp/coverage/ - continue-on-error: true - - - name: Merge coverage reports - run: bash scripts/merge-coverage.sh - continue-on-error: true - - - name: Upload to Codecov - uses: codecov/codecov-action@v6 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage/merged/lcov.info,./coverage/lcov.info - flags: unittests - name: codecov-umbrella - fail_ci_if_error: false - verbose: true - continue-on-error: true + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + webapp/package-lock.json + - run: npm ci --no-audit --no-fund + - run: npm --prefix webapp ci --no-audit --no-fund + - run: npm run type-check + - run: npm run type-check:webapp - health-check: - name: Health Check Tests (Node ${{ matrix.node-version }}) + test: + name: test runs-on: ubuntu-latest - needs: [build] - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - node-version: [24] - + timeout-minutes: 30 steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Restore backend cache - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-backend-${{ hashFiles('package-lock.json') }} - - - name: Install backend dependencies (if cache miss) - run: npm ci --prefer-offline || npm ci - - - name: Download backend build artifacts - uses: actions/download-artifact@v8 - with: - name: backend-dist - path: dist/ - - - name: Restore webapp cache - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-webapp-${{ hashFiles('webapp/package-lock.json') }} - - - name: Install webapp dependencies (if cache miss) - working-directory: ./webapp - run: npm ci --prefer-offline || npm ci - - - name: Download webapp build artifacts - uses: actions/download-artifact@v8 - with: - name: webapp-build - path: webapp/.next/ - continue-on-error: true - - - name: Validate build artifacts - run: bash scripts/validate-build.sh - - - name: Test environment configuration - run: bash scripts/env-sync-check.sh - continue-on-error: true - - - name: Verify health check script - run: | - # Create minimal .env for testing - cat > .env << EOF - SOLANA_RPC_URL=https://api.mainnet-beta.solana.com - WALLET_PRIVATE_KEY=test_key_placeholder_not_real - JWT_SECRET=test_jwt_secret_for_ci_only - ADMIN_USERNAME=admin - ADMIN_PASSWORD=test_password - EOF - - # Run health check in dry-run mode - bash scripts/health-check.sh || echo "Health check completed with warnings (expected in CI)" - - security-scan: - name: Security Scan (Node ${{ matrix.node-version }}) - runs-on: ubuntu-latest - needs: install - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - node-version: [24] - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Restore backend cache - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-backend-${{ hashFiles('package-lock.json') }} - - - name: Install backend dependencies (if cache miss) - run: npm ci --prefer-offline || npm ci - - - name: Run npm audit (backend) - run: npm audit --audit-level=high || echo "⚠️ Security vulnerabilities found - review required" - continue-on-error: true - - - name: Restore webapp cache - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-webapp-${{ hashFiles('webapp/package-lock.json') }} - - - name: Install webapp dependencies (if cache miss) - working-directory: ./webapp - run: npm ci --prefer-offline || npm ci - - - name: Run npm audit (webapp) - working-directory: ./webapp - run: npm audit --audit-level=high || echo "⚠️ Security vulnerabilities found - review required" - continue-on-error: true + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + webapp/package-lock.json + - run: npm ci --no-audit --no-fund + - run: npm --prefix webapp ci --no-audit --no-fund + - run: npm test + - run: npm run test:webapp build: - name: Build (Node ${{ matrix.node-version }}) + name: build runs-on: ubuntu-latest - needs: [lint, typecheck] - timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - node-version: [24] - + timeout-minutes: 30 steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Restore backend cache - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-backend-${{ hashFiles('package-lock.json') }} - - - name: Install backend dependencies (if cache miss) - run: npm ci --prefer-offline || npm ci - - - name: Build backend - run: npm run build:backend - - - name: Upload backend build artifacts - uses: actions/upload-artifact@v7 - if: matrix.node-version == 24 - with: - name: backend-dist - path: dist/ - retention-days: 3 - - - name: Restore webapp cache - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-webapp-${{ hashFiles('webapp/package-lock.json') }} - - - name: Install webapp dependencies (if cache miss) - working-directory: ./webapp - run: npm ci --prefer-offline || npm ci - - - name: Build webapp - run: npm run build:webapp - env: - NEXT_PUBLIC_RPC_URL: ${{ secrets.NEXT_PUBLIC_RPC_URL || 'https://api.mainnet-beta.solana.com' }} - - - name: Upload webapp build artifacts - uses: actions/upload-artifact@v7 - if: matrix.node-version == 24 - with: - name: webapp-build - path: webapp/.next/ - retention-days: 3 - if-no-files-found: ignore - - ci-summary: - name: CI Summary - runs-on: ubuntu-latest - needs: - - lint - - typecheck - - unit-tests-backend - - unit-tests-webapp - - coverage-merge - - security-scan - - build - - health-check - if: always() - - steps: - - name: Check CI status - run: | - echo "## CI Pipeline Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Lint | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Type Check | ${{ needs.typecheck.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Backend Tests | ${{ needs.unit-tests-backend.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Webapp Tests | ${{ needs.unit-tests-webapp.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Coverage Merge | ${{ needs.coverage-merge.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Security Scan | ${{ needs.security-scan.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Health Check | ${{ needs.health-check.result }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [[ "${{ needs.lint.result }}" == "failure" ]] || \ - [[ "${{ needs.typecheck.result }}" == "failure" ]] || \ - [[ "${{ needs.unit-tests-backend.result }}" == "failure" ]] || \ - [[ "${{ needs.build.result }}" == "failure" ]] || \ - [[ "${{ needs.health-check.result }}" == "failure" ]]; then - echo "❌ CI checks failed!" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "✅ All CI checks passed!" >> $GITHUB_STEP_SUMMARY - fi - - - name: Comment on PR - if: github.event_name == 'pull_request' - uses: actions/github-script@v9 - with: - script: | - const summary = `## 🔍 CI Pipeline Results - - | Job | Status | - |-----|--------| - | Lint | ${{ needs.lint.result == 'success' ? '✅' : '❌' }} | - | Type Check | ${{ needs.typecheck.result == 'success' ? '✅' : '❌' }} | - | Backend Tests | ${{ needs.unit-tests-backend.result == 'success' ? '✅' : '❌' }} | - | Webapp Tests | ${{ needs.unit-tests-webapp.result == 'success' ? '✅' : '⚠️' }} | - | Coverage | ${{ needs.coverage-merge.result == 'success' ? '✅' : '⚠️' }} | - | Security Scan | ${{ needs.security-scan.result == 'success' ? '✅' : '⚠️' }} | - | Build | ${{ needs.build.result == 'success' ? '✅' : '❌' }} | - | Health Check | ${{ needs.health-check.result == 'success' ? '✅' : '❌' }} | - - ${needs.lint.result == 'failure' || needs.typecheck.result == 'failure' || needs.unit-tests-backend.result == 'failure' || needs.build.result == 'failure' || needs.health-check.result == 'failure' ? '❌ **CI checks failed!** Please review the errors above.' : '✅ **All CI checks passed!**'} - `; - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: summary - }); + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + webapp/package-lock.json + - run: npm ci --no-audit --no-fund + - run: npm --prefix webapp ci --no-audit --no-fund + - run: npm run build diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 9e5ee632..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: CodeQL Security Analysis - -on: - push: - branches: - - main - - develop - pull_request: - branches: - - main - - develop - schedule: - # Run at 02:00 UTC every Monday - - cron: '0 2 * * 1' - workflow_dispatch: - -permissions: - actions: read - contents: read - security-events: write - -jobs: - analyze: - name: CodeQL Analysis - runs-on: ubuntu-latest - timeout-minutes: 30 - - strategy: - fail-fast: false - matrix: - language: ['javascript'] - # CodeQL supports: 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' - # Learn more: https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - # If you want to specify custom queries, you can do so here - # queries: security-extended,security-and-quality - config-file: ./.github/codeql-config.yml - - - name: Install dependencies (for better analysis) - run: | - npm ci || echo "Dependency install failed, continuing with available code" - cd webapp && npm ci || echo "Webapp dependency install failed" - continue-on-error: true - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # For JavaScript/TypeScript, autobuild will install dependencies and build if a build script exists - - name: Autobuild - uses: github/codeql-action/autobuild@v4 - continue-on-error: true - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{ matrix.language }}" - upload: true - - - name: Upload SARIF results - if: always() - uses: github/codeql-action/upload-sarif@v4 - with: - sarif_file: ../results - continue-on-error: true - - - name: Check for high severity alerts - if: always() - uses: actions/github-script@v9 - with: - script: | - const { data: alerts } = await github.rest.codeScanning.listAlertsForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - severity: 'high' - }); - - const criticalAlerts = alerts.filter(a => a.rule.severity === 'error' || a.rule.severity === 'warning'); - - if (criticalAlerts.length > 0) { - console.log(`⚠️ Found ${criticalAlerts.length} high/critical severity alerts`); - core.warning(`Found ${criticalAlerts.length} high/critical severity CodeQL alerts. Please review in the Security tab.`); - } else { - console.log('✅ No high/critical severity alerts found'); - } - continue-on-error: true diff --git a/.github/workflows/complete-production-pipeline.yml b/.github/workflows/complete-production-pipeline.yml deleted file mode 100644 index 86a6feb4..00000000 --- a/.github/workflows/complete-production-pipeline.yml +++ /dev/null @@ -1,371 +0,0 @@ -name: Complete Production Pipeline - -on: - push: - branches: - - main - - develop - - staging - pull_request: - branches: - - main - - develop - workflow_dispatch: - inputs: - environment: - description: 'Target environment' - required: true - default: 'staging' - type: choice - options: - - staging - - production - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write - checks: write - pull-requests: write - issues: write - actions: write - -jobs: - security-scan: - name: Security Scan - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run npm audit - run: npm audit --audit-level=high || echo "⚠️ Security vulnerabilities found" - continue-on-error: true - - - name: Run Snyk security scan - uses: snyk/actions/node@master - continue-on-error: true - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - args: --severity-threshold=high - - build-and-test: - name: Build and Test - runs-on: ubuntu-latest - needs: security-scan - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - retry: [1, 2, 3] - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install backend dependencies (with retry) - uses: nick-fields/retry@v4 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: npm ci - - - name: Install webapp dependencies (with retry) - uses: nick-fields/retry@v4 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: cd webapp && npm ci - - - name: Lint backend - run: npm run lint - continue-on-error: true - - - name: Lint webapp - run: npm run lint:webapp - continue-on-error: true - - - name: Type check - run: npm run type-check && npm run type-check:webapp - - - name: Run tests (with retry) - uses: nick-fields/retry@v4 - with: - timeout_minutes: 15 - max_attempts: 2 - retry_wait_seconds: 60 - command: npm test - - - name: Build backend - run: npm run build:backend - - - name: Build webapp - run: npm run build:webapp - env: - NEXT_PUBLIC_RPC_URL: ${{ secrets.NEXT_PUBLIC_RPC_URL || 'https://api.mainnet-beta.solana.com' }} - - - name: Upload artifacts - if: matrix.retry == 1 - uses: actions/upload-artifact@v7 - with: - name: build-artifacts - path: | - dist/ - webapp/.next/ - retention-days: 3 - - - name: Success on first try - if: matrix.retry == 1 && success() - run: echo "::notice::Build succeeded on first attempt" - - canary-deployment: - name: Canary Deployment - runs-on: ubuntu-latest - needs: build-and-test - if: github.ref == 'refs/heads/staging' || github.event.inputs.environment == 'staging' - timeout-minutes: 30 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run canary deployment - run: npm run canary-deploy - env: - STAGING_URL: ${{ secrets.STAGING_URL || 'https://gxq-staging.vercel.app' }} - PRODUCTION_URL: ${{ secrets.PRODUCTION_URL || 'https://gxq.vercel.app' }} - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - - - name: Comment on PR - if: github.event_name == 'pull_request' - uses: actions/github-script@v9 - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: '✅ Canary deployment completed successfully! Ready for production.' - }); - - post-deployment-analysis: - name: Post-Deployment Analysis - runs-on: ubuntu-latest - needs: [build-and-test] - if: always() - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Fetch deployment logs - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' - run: | - echo "Fetching logs from Vercel..." - # In a real setup, fetch logs from Vercel API - touch combined.log error.log - continue-on-error: true - - - name: Run post-deployment analysis - run: npm run analyze-deployment - env: - AUTO_FIX_ENABLED: ${{ secrets.AUTO_FIX_ENABLED || 'false' }} - AUTO_REDEPLOY_ENABLED: ${{ secrets.AUTO_REDEPLOY_ENABLED || 'false' }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - continue-on-error: true - - - name: Commit auto-fixes - if: env.AUTO_FIX_ENABLED == 'true' - run: | - git config user.name "GXQ Auto-Fix Bot" - git config user.email "bot@gxq.studio" - git add . - if ! git diff --staged --quiet; then - git commit -m "Auto-fix: Applied fixes from post-deployment analysis [skip ci]" - git push - fi - continue-on-error: true - - setup-monitoring: - name: Setup Monitoring - runs-on: ubuntu-latest - needs: build-and-test - if: github.ref == 'refs/heads/main' - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Generate monitoring configs - run: npm run setup-monitoring - - - name: Upload monitoring configs - uses: actions/upload-artifact@v7 - with: - name: monitoring-configs - path: monitoring/ - retention-days: 30 - - - name: Deploy to Grafana - if: env.GRAFANA_API_KEY != '' - run: | - echo "Deploying dashboard to Grafana..." - # In a real setup, use Grafana API to import dashboard - env: - GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }} - GRAFANA_URL: ${{ secrets.GRAFANA_URL }} - continue-on-error: true - - auto-ticketing: - name: Auto-Ticketing - runs-on: ubuntu-latest - needs: [build-and-test, post-deployment-analysis] - if: failure() - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Create failure issue - uses: actions/github-script@v9 - with: - script: | - const workflowRun = await github.rest.actions.getWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.runId - }); - - const issueBody = `## 🚨 Production Pipeline Failure - - **Workflow:** ${workflowRun.data.name} - **Run:** [#${workflowRun.data.run_number}](${workflowRun.data.html_url}) - **Branch:** ${context.ref} - **Commit:** ${context.sha.substring(0, 7)} - **Triggered by:** ${context.actor} - - ### Failed Jobs - Check the workflow run for details on which jobs failed. - - ### Recommended Actions - 1. Review the failed job logs - 2. Check for dependency or configuration issues - 3. Verify all required secrets are set - 4. Run the pipeline locally to reproduce - 5. Fix the issue and re-run the pipeline - - --- - *This issue was automatically created by the production pipeline* - `; - - await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: `Production Pipeline Failure: ${context.ref}`, - body: issueBody, - labels: ['pipeline-failure', 'automated', 'high-priority'] - }); - - pipeline-summary: - name: Pipeline Summary - runs-on: ubuntu-latest - needs: - - security-scan - - build-and-test - - canary-deployment - - post-deployment-analysis - - setup-monitoring - if: always() - - steps: - - name: Generate summary - run: | - echo "## 🚀 Production Pipeline Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Security Scan | ${{ needs.security-scan.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build and Test | ${{ needs.build-and-test.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Canary Deployment | ${{ needs.canary-deployment.result == 'success' && '✅' || needs.canary-deployment.result == 'skipped' && '⏭️' || '❌' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Post-Deployment | ${{ needs.post-deployment-analysis.result == 'success' && '✅' || '⚠️' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Monitoring Setup | ${{ needs.setup-monitoring.result == 'success' && '✅' || needs.setup-monitoring.result == 'skipped' && '⏭️' || '❌' }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [[ "${{ needs.build-and-test.result }}" == "failure" ]] || \ - [[ "${{ needs.security-scan.result }}" == "failure" ]]; then - echo "❌ **Pipeline failed!** Check the jobs above for details." >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "✅ **Pipeline completed successfully!**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Notify on Slack - if: failure() && github.ref == 'refs/heads/main' - run: | - curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} \ - -H 'Content-Type: application/json' \ - -d '{ - "text": "🚨 Production pipeline failed!", - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "*Production Pipeline Failure*\n\nWorkflow: ${{ github.workflow }}\nBranch: ${{ github.ref }}\nCommit: ${{ github.sha }}" - } - } - ] - }' - continue-on-error: true diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml deleted file mode 100644 index 96e18902..00000000 --- a/.github/workflows/deploy-preview.yml +++ /dev/null @@ -1,197 +0,0 @@ -name: Deploy Preview - -on: - pull_request: - types: [opened, synchronize, reopened] - branches: - - main - - develop - - dev - -permissions: - contents: read - pull-requests: write - deployments: write - -jobs: - validate-secrets: - name: Validate Required Secrets - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - has-secrets: ${{ steps.check.outputs.has-secrets }} - - steps: - - name: Check required secrets - id: check - run: | - MISSING_SECRETS="" - - if [ -z "${{ secrets.VERCEL_TOKEN }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}VERCEL_TOKEN, " - fi - - if [ -z "${{ secrets.VERCEL_PROJECT_ID }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}VERCEL_PROJECT_ID, " - fi - - if [ -z "${{ secrets.VERCEL_ORG_ID }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}VERCEL_ORG_ID, " - fi - - if [ -n "$MISSING_SECRETS" ]; then - echo "has-secrets=false" >> $GITHUB_OUTPUT - echo "⚠️ Missing required secrets: $MISSING_SECRETS" - echo "::warning::Missing required secrets: $MISSING_SECRETS" - else - echo "has-secrets=true" >> $GITHUB_OUTPUT - echo "✅ All required secrets are configured" - fi - - deploy-preview: - name: Deploy to Vercel Preview - runs-on: ubuntu-latest - needs: validate-secrets - if: needs.validate-secrets.outputs.has-secrets == 'true' - timeout-minutes: 15 - environment: - name: preview - url: ${{ steps.deploy.outputs.url }} - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - cache-dependency-path: webapp/package-lock.json - - - name: Install webapp dependencies - working-directory: ./webapp - run: npm ci - - - name: Build webapp - working-directory: ./webapp - run: npm run build - env: - NEXT_PUBLIC_RPC_URL: ${{ secrets.NEXT_PUBLIC_RPC_URL || 'https://api.mainnet-beta.solana.com' }} - - - name: Deploy to Vercel - id: deploy - uses: amondnet/vercel-action@v42 - with: - vercel-token: ${{ secrets.VERCEL_TOKEN }} - vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} - vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} - working-directory: ./webapp - scope: ${{ secrets.VERCEL_ORG_ID }} - - - name: Add preview-deployed label - uses: actions/github-script@v9 - with: - script: | - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: ['preview-deployed'] - }); - } catch (error) { - console.log('Label may already exist:', error.message); - } - - - name: Comment preview URL on PR - uses: actions/github-script@v9 - with: - script: | - const url = '${{ steps.deploy.outputs.preview-url }}'; - const comment = `## 🚀 Preview Deployment - - Your preview deployment is ready! - - **Preview URL**: ${url} - - ### Quick Links - - [🌐 Preview Site](${url}) - - [🔍 Health Check](${url}/api/health) - - [📊 Vercel Dashboard](https://vercel.com/${{ secrets.VERCEL_ORG_ID }}/${{ secrets.VERCEL_PROJECT_ID }}) - - ### Deployment Details - - **Commit**: ${context.sha.substring(0, 7)} - - **Branch**: ${context.ref.replace('refs/heads/', '')} - - **Author**: @${context.actor} - - --- - - *This preview will be automatically updated when you push new commits.* - `; - - // Check if a preview comment already exists - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('Preview Deployment') - ); - - if (botComment) { - // Update existing comment - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: comment - }); - } else { - // Create new comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); - } - - skip-preview: - name: Skip Preview (Missing Secrets) - runs-on: ubuntu-latest - needs: validate-secrets - if: needs.validate-secrets.outputs.has-secrets == 'false' - - steps: - - name: Comment on PR - uses: actions/github-script@v9 - with: - script: | - const comment = `## ⚠️ Preview Deployment Skipped - - Preview deployment was skipped because required secrets are not configured. - - ### Required Secrets - Repository maintainers need to configure these secrets in repository settings: - - \`VERCEL_TOKEN\` - Vercel authentication token - - \`VERCEL_PROJECT_ID\` - Vercel project ID - - \`VERCEL_ORG_ID\` - Vercel organization/team ID - - ### How to Configure - 1. Go to repository **Settings** → **Secrets and variables** → **Actions** - 2. Add the required secrets - 3. Re-run this workflow or push a new commit - - For more information, see the [Vercel deployment documentation](https://vercel.com/docs/deployments/git). - `; - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); diff --git a/.github/workflows/deploy-railway-preview.yml b/.github/workflows/deploy-railway-preview.yml deleted file mode 100644 index d639253a..00000000 --- a/.github/workflows/deploy-railway-preview.yml +++ /dev/null @@ -1,358 +0,0 @@ -name: Deploy Railway Preview - -on: - pull_request: - types: [opened, synchronize, reopened] - branches: - - main - - develop - -permissions: - contents: read - pull-requests: write - deployments: write - -jobs: - check-skip: - name: Check Skip Deployment - runs-on: ubuntu-latest - timeout-minutes: 2 - outputs: - should-deploy: ${{ steps.check.outputs.should-deploy }} - - steps: - - name: Check for skip-deployment label - id: check - uses: actions/github-script@v9 - with: - script: | - const labels = context.payload.pull_request.labels.map(label => label.name); - const shouldSkip = labels.includes('skip-deployment'); - - if (shouldSkip) { - console.log('⏭️ Skipping deployment due to skip-deployment label'); - core.setOutput('should-deploy', 'false'); - } else { - console.log('✅ Deployment will proceed'); - core.setOutput('should-deploy', 'true'); - } - - validate-secrets: - name: Validate Required Secrets - runs-on: ubuntu-latest - needs: check-skip - if: needs.check-skip.outputs.should-deploy == 'true' - timeout-minutes: 5 - outputs: - has-secrets: ${{ steps.check.outputs.has-secrets }} - - steps: - - name: Check required secrets - id: check - run: | - MISSING_SECRETS="" - - if [ -z "${{ secrets.RAILWAY_TOKEN }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}RAILWAY_TOKEN, " - fi - - if [ -z "${{ secrets.RAILWAY_PROJECT_ID }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}RAILWAY_PROJECT_ID, " - fi - - if [ -z "${{ secrets.SOLANA_RPC_URL }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}SOLANA_RPC_URL, " - fi - - if [ -z "${{ secrets.WALLET_PRIVATE_KEY }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}WALLET_PRIVATE_KEY, " - fi - - if [ -z "${{ secrets.ADMIN_USERNAME }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}ADMIN_USERNAME, " - fi - - if [ -z "${{ secrets.ADMIN_PASSWORD }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}ADMIN_PASSWORD, " - fi - - if [ -z "${{ secrets.JWT_SECRET }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}JWT_SECRET, " - fi - - if [ -n "$MISSING_SECRETS" ]; then - echo "has-secrets=false" >> $GITHUB_OUTPUT - echo "⚠️ Missing required secrets: $MISSING_SECRETS" - echo "::warning::Missing required secrets: $MISSING_SECRETS" - else - echo "has-secrets=true" >> $GITHUB_OUTPUT - echo "✅ All required secrets are configured" - fi - - deploy-preview: - name: Deploy to Railway Preview - runs-on: ubuntu-latest - needs: validate-secrets - if: needs.validate-secrets.outputs.has-secrets == 'true' - timeout-minutes: 15 - environment: - name: preview-railway - url: ${{ steps.deploy.outputs.url }} - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build project - run: npm run build - env: - NODE_ENV: production - - - name: Install Railway CLI - run: | - curl -fsSL https://railway.app/install.sh | sh - echo "$HOME/.railway/bin" >> $GITHUB_PATH - - - name: Deploy to Railway Preview - id: deploy - run: | - # Link to project - railway link ${{ secrets.RAILWAY_PROJECT_ID }} - - # Create preview environment name - PR_NUMBER=${{ github.event.pull_request.number }} - PREVIEW_ENV="pr-${PR_NUMBER}" - - echo "Creating preview environment: $PREVIEW_ENV" - - # Set environment variables for preview - railway variables --set SOLANA_RPC_URL="${{ secrets.SOLANA_RPC_URL }}" - railway variables --set WALLET_PRIVATE_KEY="${{ secrets.WALLET_PRIVATE_KEY }}" - railway variables --set ADMIN_USERNAME="${{ secrets.ADMIN_USERNAME }}" - railway variables --set ADMIN_PASSWORD="${{ secrets.ADMIN_PASSWORD }}" - railway variables --set JWT_SECRET="${{ secrets.JWT_SECRET }}" - railway variables --set NODE_ENV="preview" - railway variables --set PR_NUMBER="${PR_NUMBER}" - - # Deploy to preview environment - railway up --detach - - # Wait for deployment - sleep 20 - - # Get preview URL - PREVIEW_URL=$(railway domain 2>&1 | grep -oP 'https://[^\s]+' | head -1) - echo "url=$PREVIEW_URL" >> $GITHUB_OUTPUT - echo "Preview deployed to: $PREVIEW_URL" - env: - RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} - - - name: Wait for deployment - run: | - echo "⏳ Waiting for preview deployment to be ready..." - sleep 30 - - - name: Run health check - id: health-check - run: | - echo "🔍 Running health check..." - HEALTH_URL="${{ steps.deploy.outputs.url }}/api/health" - - MAX_RETRIES=5 - RETRY_COUNT=0 - HEALTH_STATUS="unknown" - - while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do - echo "Attempt $((RETRY_COUNT + 1))/$MAX_RETRIES: Checking $HEALTH_URL" - - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL" || echo "000") - - if [ "$RESPONSE" == "200" ] || [ "$RESPONSE" == "503" ]; then - echo "✅ Health check passed (HTTP $RESPONSE)" - HEALTH_STATUS="✅ Healthy (HTTP $RESPONSE)" - echo "status=$HEALTH_STATUS" >> $GITHUB_OUTPUT - exit 0 - fi - - echo "⚠️ Health check returned HTTP $RESPONSE, retrying..." - RETRY_COUNT=$((RETRY_COUNT + 1)) - sleep 10 - done - - echo "❌ Health check failed after $MAX_RETRIES attempts" - HEALTH_STATUS="❌ Failed" - echo "status=$HEALTH_STATUS" >> $GITHUB_OUTPUT - exit 1 - continue-on-error: true - - - name: Add preview-deployed label - if: success() - uses: actions/github-script@v9 - with: - script: | - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: ['preview-deployed', 'railway'] - }); - } catch (error) { - console.log('Label may already exist:', error.message); - } - - - name: Comment preview URL on PR - uses: actions/github-script@v9 - with: - script: | - const url = '${{ steps.deploy.outputs.url }}'; - const healthStatus = '${{ steps.health-check.outputs.status }}' || '⏳ Checking...'; - const prNumber = context.issue.number; - - const comment = `## 🚂 Railway Preview Deployment - - Your Railway preview deployment is ready! - - **Preview URL**: ${url} - **Health Status**: ${healthStatus} - **PR Number**: #${prNumber} - - ### Quick Links - - [🌐 Preview Application](${url}) - - [🏥 Health Check](${url}/api/health) - - [📊 Metrics](${url}/api/metrics) - - [🚂 Railway Dashboard](https://railway.app/project/${{ secrets.RAILWAY_PROJECT_ID }}) - - ### Deployment Details - - **Commit**: ${context.sha.substring(0, 7)} - - **Branch**: ${context.ref.replace('refs/heads/', '')} - - **Author**: @${context.actor} - - **Environment**: preview-railway - - --- - - *This preview will be automatically updated when you push new commits.* - *Add the \`skip-deployment\` label to skip future deployments for this PR.* - `; - - // Check if a Railway preview comment already exists - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('Railway Preview Deployment') - ); - - if (botComment) { - // Update existing comment - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: comment - }); - } else { - // Create new comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); - } - - - name: Create deployment record - if: success() - uses: actions/github-script@v9 - with: - script: | - const deployment = await github.rest.repos.createDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: context.sha, - environment: 'preview-railway', - required_contexts: [], - auto_merge: false, - }); - - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.data.id, - state: 'success', - environment_url: '${{ steps.deploy.outputs.url }}', - description: 'Railway preview deployment successful', - }); - - skip-preview: - name: Skip Preview (Missing Secrets or Label) - runs-on: ubuntu-latest - needs: [check-skip, validate-secrets] - if: | - always() && ( - needs.check-skip.outputs.should-deploy == 'false' || - (needs.check-skip.outputs.should-deploy == 'true' && needs.validate-secrets.outputs.has-secrets == 'false') - ) - - steps: - - name: Comment on PR - uses: actions/github-script@v9 - with: - script: | - const skipLabel = '${{ needs.check-skip.outputs.should-deploy }}' === 'false'; - const missingSecrets = '${{ needs.validate-secrets.outputs.has-secrets }}' === 'false'; - - let comment = ''; - - if (skipLabel) { - comment = `## ⏭️ Railway Preview Deployment Skipped - - Preview deployment was skipped because the \`skip-deployment\` label is present. - - To enable preview deployments: - 1. Remove the \`skip-deployment\` label - 2. Push a new commit or re-run this workflow - `; - } else if (missingSecrets) { - comment = `## ⚠️ Railway Preview Deployment Skipped - - Preview deployment was skipped because required secrets are not configured. - - ### Required Secrets - Repository maintainers need to configure these secrets in repository settings: - - \`RAILWAY_TOKEN\` - Railway API authentication token - - \`RAILWAY_PROJECT_ID\` - Railway project ID (should be \`2077acd9-f81f-47ba-b8c7-8bf6905f45fc\`) - - \`SOLANA_RPC_URL\` - Solana RPC endpoint URL - - \`WALLET_PRIVATE_KEY\` - Wallet private key (base58 format) - - \`ADMIN_USERNAME\` - Admin panel username - - \`ADMIN_PASSWORD\` - Admin panel password - - \`JWT_SECRET\` - JWT secret for authentication - - ### How to Configure - 1. Go to repository **Settings** → **Secrets and variables** → **Actions** - 2. Add the required secrets - 3. Re-run this workflow or push a new commit - - For more information, see the [Railway deployment documentation](./docs/RAILWAY_DEPLOYMENT.md). - `; - } - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); diff --git a/.github/workflows/deploy-railway.yml b/.github/workflows/deploy-railway.yml deleted file mode 100644 index f9e7dd8d..00000000 --- a/.github/workflows/deploy-railway.yml +++ /dev/null @@ -1,230 +0,0 @@ -name: Deploy to Railway - -on: - push: - branches: - - main - workflow_dispatch: - -permissions: - contents: read - deployments: write - -jobs: - validate-secrets: - name: Validate Required Secrets - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - has-secrets: ${{ steps.check.outputs.has-secrets }} - - steps: - - name: Check required secrets - id: check - run: | - MISSING_SECRETS="" - - if [ -z "${{ secrets.RAILWAY_TOKEN }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}RAILWAY_TOKEN, " - fi - - if [ -z "${{ secrets.RAILWAY_PROJECT_ID }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}RAILWAY_PROJECT_ID, " - fi - - if [ -z "${{ secrets.SOLANA_RPC_URL }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}SOLANA_RPC_URL, " - fi - - if [ -z "${{ secrets.WALLET_PRIVATE_KEY }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}WALLET_PRIVATE_KEY, " - fi - - if [ -z "${{ secrets.ADMIN_USERNAME }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}ADMIN_USERNAME, " - fi - - if [ -z "${{ secrets.ADMIN_PASSWORD }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}ADMIN_PASSWORD, " - fi - - if [ -z "${{ secrets.JWT_SECRET }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}JWT_SECRET, " - fi - - if [ -n "$MISSING_SECRETS" ]; then - echo "has-secrets=false" >> $GITHUB_OUTPUT - echo "⚠️ Missing required secrets: $MISSING_SECRETS" - echo "::warning::Missing required secrets: $MISSING_SECRETS" - exit 1 - else - echo "has-secrets=true" >> $GITHUB_OUTPUT - echo "✅ All required secrets are configured" - fi - - deploy: - name: Deploy to Railway - runs-on: ubuntu-latest - needs: validate-secrets - if: needs.validate-secrets.outputs.has-secrets == 'true' - timeout-minutes: 15 - environment: - name: production-railway - url: ${{ steps.deploy.outputs.url }} - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm test || echo "No tests configured" - continue-on-error: true - env: - NODE_ENV: test - - - name: Build project - run: npm run build - - - name: Install Railway CLI - run: | - curl -fsSL https://railway.app/install.sh | sh - echo "$HOME/.railway/bin" >> $GITHUB_PATH - - - name: Deploy to Railway - id: deploy - run: | - railway link ${{ secrets.RAILWAY_PROJECT_ID }} - - # Set environment variables from secrets - railway variables --set SOLANA_RPC_URL="${{ secrets.SOLANA_RPC_URL }}" - railway variables --set WALLET_PRIVATE_KEY="${{ secrets.WALLET_PRIVATE_KEY }}" - railway variables --set ADMIN_USERNAME="${{ secrets.ADMIN_USERNAME }}" - railway variables --set ADMIN_PASSWORD="${{ secrets.ADMIN_PASSWORD }}" - railway variables --set JWT_SECRET="${{ secrets.JWT_SECRET }}" - railway variables --set NODE_ENV="production" - - # Deploy - railway up --detach - - # Wait for deployment to complete - sleep 15 - - # Get deployment URL - DEPLOYMENT_URL=$(railway domain 2>&1 | grep -oP 'https://[^\s]+' | head -1) - echo "url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT - echo "Deployed to: $DEPLOYMENT_URL" - env: - RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} - - - name: Wait for deployment - run: | - echo "⏳ Waiting for deployment to be ready..." - sleep 30 - - - name: Run health check - run: | - echo "🔍 Running health check..." - HEALTH_URL="${{ steps.deploy.outputs.url }}/api/health" - - MAX_RETRIES=5 - RETRY_COUNT=0 - - while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do - echo "Attempt $((RETRY_COUNT + 1))/$MAX_RETRIES: Checking $HEALTH_URL" - - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL" || echo "000") - - if [ "$RESPONSE" == "200" ] || [ "$RESPONSE" == "503" ]; then - echo "✅ Health check passed (HTTP $RESPONSE)" - exit 0 - fi - - echo "⚠️ Health check returned HTTP $RESPONSE, retrying..." - RETRY_COUNT=$((RETRY_COUNT + 1)) - sleep 10 - done - - echo "❌ Health check failed after $MAX_RETRIES attempts" - exit 1 - - - name: Create deployment record - uses: actions/github-script@v9 - with: - script: | - const deployment = await github.rest.repos.createDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: context.sha, - environment: 'production-railway', - required_contexts: [], - auto_merge: false, - }); - - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.data.id, - state: 'success', - environment_url: '${{ steps.deploy.outputs.url }}', - description: 'Railway deployment successful', - }); - - console.log('✅ Deployment successful'); - console.log('🔗 URL:', '${{ steps.deploy.outputs.url }}'); - - - name: Notify on failure - if: failure() - uses: actions/github-script@v9 - with: - script: | - const issue = await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: '🚨 Railway Deployment Failed', - body: `### Deployment Failure Report - - **Workflow**: ${context.workflow} - **Run**: ${context.runNumber} - **Branch**: ${context.ref} - **Commit**: ${context.sha} - **Actor**: @${context.actor} - - **Details**: [View workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) - - Please investigate and retry the deployment. - `, - labels: ['deployment', 'railway', 'bug', 'high-priority'] - }); - - console.log('Created issue:', issue.data.number); - - test-endpoints: - name: Test Deployed Endpoints - runs-on: ubuntu-latest - needs: deploy - timeout-minutes: 10 - - steps: - - name: Test health endpoint - run: | - echo "Testing /api/health..." - curl -f "${{ needs.deploy.outputs.url }}/api/health" || exit 1 - - - name: Test monitor endpoint - run: | - echo "Testing /api/monitor..." - curl "${{ needs.deploy.outputs.url }}/api/monitor" || echo "Monitor endpoint requires authentication" - - - name: Summary - run: | - echo "✅ Endpoint tests complete" - echo "🔗 Deployment URL: ${{ needs.deploy.outputs.url }}" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..d3e2a4e4 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,67 @@ +name: deploy + +on: + workflow_run: + workflows: ["ci"] + types: [completed] + pull_request: + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + deployments: write + +jobs: + preview: + name: preview + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v6 + - uses: amondnet/vercel-action@v42 + if: ${{ secrets.VERCEL_TOKEN != '' && secrets.VERCEL_ORG_ID != '' && secrets.VERCEL_PROJECT_ID != '' }} + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + working-directory: ./webapp + + production: + name: production + if: >- + github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'main' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + webapp/package-lock.json + - run: npm ci --no-audit --no-fund + - run: npm --prefix webapp ci --no-audit --no-fund + - run: npm run build + - name: Validate deploy secrets + run: | + test -n "${{ secrets.VERCEL_TOKEN }}" + test -n "${{ secrets.VERCEL_ORG_ID }}" + test -n "${{ secrets.VERCEL_PROJECT_ID }}" + - uses: amondnet/vercel-action@v42 + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + vercel-args: --prod + working-directory: ./webapp diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index 1cc93cdf..00000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -1,142 +0,0 @@ -name: Build and Push Docker Images - -on: - push: - branches: - - main - - develop - tags: - - 'v*' - pull_request: - branches: - - main - workflow_dispatch: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Log in to Container Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v4 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Backend - id: meta-backend - uses: docker/metadata-action@v6 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push Backend image - uses: docker/build-push-action@v7 - with: - context: . - target: backend - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta-backend.outputs.tags }} - labels: ${{ steps.meta-backend.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Extract metadata for Webapp - id: meta-webapp - uses: docker/metadata-action@v6 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/webapp - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push Webapp image - uses: docker/build-push-action@v7 - with: - context: ./webapp - file: ./webapp/Dockerfile - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta-webapp.outputs.tags }} - labels: ${{ steps.meta-webapp.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Extract metadata for Fullstack - id: meta-fullstack - uses: docker/metadata-action@v6 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/fullstack - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push Fullstack image - uses: docker/build-push-action@v7 - with: - context: . - target: fullstack - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta-fullstack.outputs.tags }} - labels: ${{ steps.meta-fullstack.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Generate artifact attestation - if: github.event_name != 'pull_request' - uses: actions/attest-build-provenance@v4 - with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend - subject-digest: ${{ steps.build-backend.outputs.digest }} - push-to-registry: true - - - name: Summary - if: github.event_name != 'pull_request' - run: | - echo "## Docker Images Built and Pushed 🐳" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Backend" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "${{ steps.meta-backend.outputs.tags }}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Webapp" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "${{ steps.meta-webapp.outputs.tags }}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Fullstack" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "${{ steps.meta-fullstack.outputs.tags }}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/failed-job-handler.yml b/.github/workflows/failed-job-handler.yml deleted file mode 100644 index 7d50e776..00000000 --- a/.github/workflows/failed-job-handler.yml +++ /dev/null @@ -1,204 +0,0 @@ -name: Failed Job Handler - -on: - workflow_run: - workflows: ["CI - Continuous Integration"] - types: [completed] - workflow_dispatch: - inputs: - run_id: - description: 'Workflow run ID to analyze' - required: true - type: string - -permissions: - actions: write - issues: write - contents: read - -jobs: - analyze-failure: - name: Analyze Failed Jobs - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'failure' || github.event_name == 'workflow_dispatch' }} - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Get workflow run details - id: workflow - uses: actions/github-script@v9 - with: - script: | - let runId; - if (context.eventName === 'workflow_dispatch') { - runId = parseInt('${{ inputs.run_id }}'); - } else { - runId = context.payload.workflow_run.id; - } - - console.log(`Analyzing workflow run: ${runId}`); - - // Get workflow run details - const { data: workflowRun } = await github.rest.actions.getWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: runId - }); - - console.log(`Workflow: ${workflowRun.name}`); - console.log(`Conclusion: ${workflowRun.conclusion}`); - console.log(`Attempt: ${workflowRun.run_attempt}`); - - // Get failed jobs - const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: runId - }); - - const failedJobs = jobs.jobs.filter(job => job.conclusion === 'failure'); - - if (failedJobs.length === 0) { - console.log('No failed jobs found'); - return { shouldRetry: false, failedJobs: [] }; - } - - console.log(`Found ${failedJobs.length} failed jobs`); - - // Analyze failure reasons - const failureReasons = []; - for (const job of failedJobs) { - console.log(`\nFailed job: ${job.name}`); - console.log(`Status: ${job.status}, Conclusion: ${job.conclusion}`); - - // Get job logs - try { - const { data: logs } = await github.rest.actions.downloadJobLogsForWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - job_id: job.id - }); - - // Analyze common transient failures - const logsStr = typeof logs === 'string' ? logs : ''; - const isTransient = - logsStr.includes('ECONNRESET') || - logsStr.includes('ETIMEDOUT') || - logsStr.includes('network') || - logsStr.includes('timeout') || - logsStr.includes('429') || // Rate limit - logsStr.includes('503') || // Service unavailable - logsStr.includes('502'); // Bad gateway - - failureReasons.push({ - jobName: job.name, - jobId: job.id, - isTransient, - logSnippet: logsStr.slice(0, 500) - }); - } catch (error) { - console.log(`Could not fetch logs for job ${job.id}: ${error.message}`); - } - } - - // Decide if should retry - const shouldRetry = - workflowRun.run_attempt < 3 && // Max 3 attempts - failureReasons.some(f => f.isTransient); - - core.setOutput('run_id', runId); - core.setOutput('should_retry', shouldRetry); - core.setOutput('attempt', workflowRun.run_attempt); - core.setOutput('failed_jobs_count', failedJobs.length); - core.setOutput('failure_details', JSON.stringify(failureReasons)); - - return { shouldRetry, failedJobs: failureReasons }; - - - name: Retry workflow - if: steps.workflow.outputs.should_retry == 'true' - uses: actions/github-script@v9 - with: - script: | - const runId = parseInt('${{ steps.workflow.outputs.run_id }}'); - - console.log(`Retrying workflow run ${runId}...`); - - try { - await github.rest.actions.reRunWorkflowFailedJobs({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: runId - }); - - console.log('✅ Successfully triggered retry for failed jobs'); - } catch (error) { - console.error('Failed to retry workflow:', error); - } - - - name: Create issue for persistent failure - if: | - steps.workflow.outputs.should_retry == 'false' && - steps.workflow.outputs.attempt >= 3 - uses: actions/github-script@v9 - with: - script: | - const runId = '${{ steps.workflow.outputs.run_id }}'; - const failureDetails = JSON.parse('${{ steps.workflow.outputs.failure_details }}'); - - // Check if issue already exists - const { data: existingIssues } = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - labels: 'ci-failure', - state: 'open' - }); - - const issueTitle = `CI Failure: Workflow Run ${runId}`; - const existingIssue = existingIssues.find(issue => issue.title === issueTitle); - - if (existingIssue) { - console.log(`Issue already exists: #${existingIssue.number}`); - return; - } - - // Create detailed issue body - let issueBody = `## 🚨 Persistent CI Failure\n\n`; - issueBody += `**Workflow Run:** [#${runId}](${context.payload.workflow_run?.html_url || 'N/A'})\n`; - issueBody += `**Attempt:** ${{ steps.workflow.outputs.attempt }}\n`; - issueBody += `**Failed Jobs:** ${{ steps.workflow.outputs.failed_jobs_count }}\n\n`; - issueBody += `### Failed Jobs Details\n\n`; - - for (const failure of failureDetails) { - issueBody += `#### ${failure.jobName}\n`; - issueBody += `- **Job ID:** ${failure.jobId}\n`; - issueBody += `- **Transient:** ${failure.isTransient ? '✅' : '❌'}\n`; - issueBody += `\n
Log Snippet\n\n\`\`\`\n${failure.logSnippet}\n\`\`\`\n\n
\n\n`; - } - - issueBody += `### Action Required\n\n`; - issueBody += `This workflow has failed after multiple retry attempts. Manual intervention is required.\n\n`; - issueBody += `**Recommended Actions:**\n`; - issueBody += `1. Review the failed job logs\n`; - issueBody += `2. Check for dependency or configuration issues\n`; - issueBody += `3. Verify that all required secrets and environment variables are set\n`; - issueBody += `4. Fix the underlying issue and close this issue\n`; - - // Create issue - const { data: issue } = await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: issueTitle, - body: issueBody, - labels: ['ci-failure', 'bug', 'automated'] - }); - - console.log(`✅ Created issue: #${issue.number}`); - - - name: Notify on failure - if: failure() - run: | - echo "::warning::Failed job handler encountered an error" - echo "Run ID: ${{ steps.workflow.outputs.run_id }}" - echo "Failed Jobs: ${{ steps.workflow.outputs.failed_jobs_count }}" diff --git a/.github/workflows/gxq-master-ci.yml b/.github/workflows/gxq-master-ci.yml deleted file mode 100644 index 661207aa..00000000 --- a/.github/workflows/gxq-master-ci.yml +++ /dev/null @@ -1,314 +0,0 @@ -name: GXQ Master CI Pipeline - -on: - push: - branches: - - master - - main - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write - checks: write - pull-requests: write - -jobs: - validate-environment: - name: Validate Environment - runs-on: ubuntu-latest - timeout-minutes: 5 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js 24.x - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Create mock environment file - run: | - cat > .env << EOF - SOLANA_RPC_URL=https://api.mainnet-beta.solana.com - WALLET_PRIVATE_KEY=mock_key_for_testing - JWT_SECRET=mock_jwt_secret_for_ci - ADMIN_USERNAME=admin - ADMIN_PASSWORD=mock_password - EOF - - - name: Run environment check - run: bash scripts/env-check.sh - - install-dependencies: - name: Install Dependencies - runs-on: ubuntu-latest - needs: validate-environment - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js 24.x - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install backend dependencies - run: npm ci - - - name: Cache backend node_modules - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-24-backend-${{ hashFiles('package-lock.json') }} - - - name: Install webapp dependencies - working-directory: ./webapp - run: npm ci - - - name: Cache webapp node_modules - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-24-webapp-${{ hashFiles('webapp/package-lock.json') }} - - lint-and-typecheck: - name: Lint and Type Check - runs-on: ubuntu-latest - needs: install-dependencies - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js 24.x - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Restore backend cache - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-24-backend-${{ hashFiles('package-lock.json') }} - - - name: Install backend dependencies (if cache miss) - run: npm ci --prefer-offline || npm ci - - - name: Run backend lint - run: npm run lint - continue-on-error: true - - - name: Run backend type-check - run: npm run type-check - - - name: Restore webapp cache - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-24-webapp-${{ hashFiles('webapp/package-lock.json') }} - - - name: Install webapp dependencies (if cache miss) - working-directory: ./webapp - run: npm ci --prefer-offline || npm ci - - - name: Run webapp lint - run: npm run lint:webapp - continue-on-error: true - - - name: Run webapp type-check - run: npm run type-check:webapp - - build-backend: - name: Build Backend - runs-on: ubuntu-latest - needs: lint-and-typecheck - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js 24.x - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Restore backend cache - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-24-backend-${{ hashFiles('package-lock.json') }} - - - name: Install backend dependencies (if cache miss) - run: npm ci --prefer-offline || npm ci - - - name: Build backend - run: npm run build:backend - - - name: Upload backend artifacts - uses: actions/upload-artifact@v7 - with: - name: backend-dist - path: dist/ - retention-days: 7 - - build-webapp: - name: Build Webapp - runs-on: ubuntu-latest - needs: lint-and-typecheck - timeout-minutes: 15 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js 24.x - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Restore webapp cache - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-24-webapp-${{ hashFiles('webapp/package-lock.json') }} - - - name: Install webapp dependencies (if cache miss) - working-directory: ./webapp - run: npm ci --prefer-offline || npm ci - - - name: Build webapp - run: npm run build:webapp - env: - NEXT_PUBLIC_RPC_URL: https://api.mainnet-beta.solana.com - - - name: Upload webapp artifacts - uses: actions/upload-artifact@v7 - with: - name: webapp-build - path: webapp/.next/ - retention-days: 7 - if-no-files-found: warn - - validate-build: - name: Validate Build Artifacts - runs-on: ubuntu-latest - needs: [build-backend, build-webapp] - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Download backend artifacts - uses: actions/download-artifact@v8 - with: - name: backend-dist - path: dist/ - - - name: Download webapp artifacts - uses: actions/download-artifact@v8 - with: - name: webapp-build - path: webapp/.next/ - continue-on-error: true - - - name: Run build validation - run: bash scripts/validate-build.sh - - security-scan: - name: Security Scan - runs-on: ubuntu-latest - needs: install-dependencies - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js 20.x - uses: actions/setup-node@v6 - with: - node-version: '20' - cache: 'npm' - - - name: Restore backend cache - uses: actions/cache@v5 - with: - path: node_modules - key: ${{ runner.os }}-node-20-backend-${{ hashFiles('package-lock.json') }} - - - name: Install backend dependencies (if cache miss) - run: npm ci --prefer-offline || npm ci - - - name: Run npm audit (backend) - run: npm audit --production --audit-level=high || echo "⚠️ Security vulnerabilities found" - continue-on-error: true - - - name: Restore webapp cache - uses: actions/cache@v5 - with: - path: webapp/node_modules - key: ${{ runner.os }}-node-20-webapp-${{ hashFiles('webapp/package-lock.json') }} - - - name: Install webapp dependencies (if cache miss) - working-directory: ./webapp - run: npm ci --prefer-offline || npm ci - - - name: Run npm audit (webapp) - working-directory: ./webapp - run: npm audit --production --audit-level=high || echo "⚠️ Security vulnerabilities found" - continue-on-error: true - - master-ci-summary: - name: Master CI Summary - runs-on: ubuntu-latest - needs: - - validate-environment - - install-dependencies - - lint-and-typecheck - - build-backend - - build-webapp - - validate-build - - security-scan - if: always() - - steps: - - name: Check CI status - run: | - echo "## Master CI Pipeline Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Validate Environment | ${{ needs.validate-environment.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Install Dependencies | ${{ needs.install-dependencies.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Lint & Type Check | ${{ needs.lint-and-typecheck.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build Backend | ${{ needs.build-backend.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Build Webapp | ${{ needs.build-webapp.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Validate Build | ${{ needs.validate-build.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Security Scan | ${{ needs.security-scan.result }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [[ "${{ needs.validate-environment.result }}" == "failure" ]] || \ - [[ "${{ needs.lint-and-typecheck.result }}" == "failure" ]] || \ - [[ "${{ needs.build-backend.result }}" == "failure" ]] || \ - [[ "${{ needs.build-webapp.result }}" == "failure" ]] || \ - [[ "${{ needs.validate-build.result }}" == "failure" ]]; then - echo "❌ Master CI checks failed!" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "✅ All Master CI checks passed!" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/gxq-pr-check.yml b/.github/workflows/gxq-pr-check.yml deleted file mode 100644 index a60ebfd9..00000000 --- a/.github/workflows/gxq-pr-check.yml +++ /dev/null @@ -1,153 +0,0 @@ -name: GXQ PR Check - -on: - pull_request: - branches: - - master - - main - types: [opened, synchronize, reopened, ready_for_review] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - checks: write - pull-requests: write - -jobs: - pr-validation: - name: PR Validation - runs-on: ubuntu-latest - timeout-minutes: 30 - if: github.event.pull_request.draft == false - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js 24.x - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Create mock environment file - run: | - cat > .env << EOF - SOLANA_RPC_URL=https://api.mainnet-beta.solana.com - WALLET_PRIVATE_KEY=mock_key_for_testing - JWT_SECRET=mock_jwt_secret_for_ci - ADMIN_USERNAME=admin - ADMIN_PASSWORD=mock_password - EOF - - - name: Install backend dependencies - run: npm ci - - - name: Install webapp dependencies - working-directory: ./webapp - run: npm ci - - - name: Run lint - run: | - npm run lint || echo "Backend lint issues found (non-blocking)" - npm run lint:webapp || echo "Webapp lint issues found (non-blocking)" - continue-on-error: true - - - name: Run type-check - run: | - npm run type-check - npm run type-check:webapp - - - name: Build backend - run: npm run build:backend - - - name: Build webapp - run: npm run build:webapp - env: - NEXT_PUBLIC_RPC_URL: https://api.mainnet-beta.solana.com - - - name: Validate build - run: bash scripts/validate-build.sh - - - name: Calculate artifact sizes - id: sizes - run: | - DIST_SIZE=$(du -sh dist 2>/dev/null | cut -f1 || echo "N/A") - WEBAPP_SIZE=$(du -sh webapp/.next 2>/dev/null | cut -f1 || echo "N/A") - echo "dist_size=$DIST_SIZE" >> $GITHUB_OUTPUT - echo "webapp_size=$WEBAPP_SIZE" >> $GITHUB_OUTPUT - - - name: Comment PR with results - uses: actions/github-script@v9 - with: - script: | - const distSize = '${{ steps.sizes.outputs.dist_size }}'; - const webappSize = '${{ steps.sizes.outputs.webapp_size }}'; - - const comment = `## 🔍 GXQ PR Check Results - - ✅ **All validation checks passed!** - - ### Build Artifacts - - | Component | Size | - |-----------|------| - | Backend (dist/) | ${distSize} | - | Webapp (.next/) | ${webappSize} | - - ### Checks Performed - - - ✅ Environment validation - - ✅ Dependency installation - - ✅ Linting (warnings allowed) - - ✅ Type checking - - ✅ Backend build - - ✅ Webapp build - - ✅ Build artifact validation - - --- - - *Ready for review and merge* 🚀 - `; - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); - - pr-security-check: - name: PR Security Check - runs-on: ubuntu-latest - timeout-minutes: 10 - if: github.event.pull_request.draft == false - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js 24.x - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install backend dependencies - run: npm ci - - - name: Install webapp dependencies - working-directory: ./webapp - run: npm ci - - - name: Run npm audit (backend) - run: npm audit --production --audit-level=high || echo "⚠️ Security vulnerabilities found (review required)" - continue-on-error: true - - - name: Run npm audit (webapp) - working-directory: ./webapp - run: npm audit --production --audit-level=high || echo "⚠️ Security vulnerabilities found (review required)" - continue-on-error: true diff --git a/.github/workflows/gxq-scheduled-health.yml b/.github/workflows/gxq-scheduled-health.yml deleted file mode 100644 index 17802d43..00000000 --- a/.github/workflows/gxq-scheduled-health.yml +++ /dev/null @@ -1,193 +0,0 @@ -name: GXQ Scheduled Health Check - -on: - schedule: - - cron: '0 */6 * * *' # Every 6 hours - workflow_dispatch: # Allow manual trigger - -permissions: - contents: read - issues: write - -jobs: - health-check: - name: Production Health Check - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Check Vercel (Webapp) - id: vercel_check - run: | - VERCEL_URL="${{ secrets.VERCEL_PRODUCTION_URL || 'https://your-app.vercel.app' }}" - echo "Checking: $VERCEL_URL" - - if curl -f -s -o /tmp/vercel-response "$VERCEL_URL" 2>&1; then - echo "status=healthy" >> $GITHUB_OUTPUT - echo "✅ Vercel is healthy" - else - echo "status=unhealthy" >> $GITHUB_OUTPUT - echo "❌ Vercel is unhealthy" - fi - continue-on-error: true - - - name: Check Railway (Backend) - id: railway_check - run: | - RAILWAY_URL="${{ secrets.RAILWAY_PRODUCTION_URL || 'https://your-app.railway.app' }}" - echo "Checking: $RAILWAY_URL/health" - - if curl -f -s -o /tmp/railway-response "$RAILWAY_URL/health" 2>&1; then - echo "status=healthy" >> $GITHUB_OUTPUT - echo "✅ Railway is healthy" - cat /tmp/railway-response - else - echo "status=unhealthy" >> $GITHUB_OUTPUT - echo "❌ Railway is unhealthy" - fi - continue-on-error: true - - - name: Check Solana RPC - id: rpc_check - run: | - RPC_URL="${{ secrets.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com' }}" - echo "Checking: $RPC_URL" - - RESPONSE=$(curl -s -X POST "$RPC_URL" \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}' 2>&1) - - if echo "$RESPONSE" | grep -q "ok\|result"; then - echo "status=healthy" >> $GITHUB_OUTPUT - echo "✅ Solana RPC is healthy" - else - echo "status=unhealthy" >> $GITHUB_OUTPUT - echo "❌ Solana RPC is unhealthy" - fi - continue-on-error: true - - - name: Generate health report - id: report - run: | - echo "# GXQ Studio Health Report" > /tmp/health-report.md - echo "" >> /tmp/health-report.md - echo "**Timestamp:** $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> /tmp/health-report.md - echo "" >> /tmp/health-report.md - echo "## Service Status" >> /tmp/health-report.md - echo "" >> /tmp/health-report.md - echo "| Service | Status |" >> /tmp/health-report.md - echo "|---------|--------|" >> /tmp/health-report.md - - VERCEL_STATUS="${{ steps.vercel_check.outputs.status }}" - RAILWAY_STATUS="${{ steps.railway_check.outputs.status }}" - RPC_STATUS="${{ steps.rpc_check.outputs.status }}" - - if [ "$VERCEL_STATUS" == "healthy" ]; then - echo "| Vercel (Webapp) | ✅ Healthy |" >> /tmp/health-report.md - else - echo "| Vercel (Webapp) | ❌ Unhealthy |" >> /tmp/health-report.md - fi - - if [ "$RAILWAY_STATUS" == "healthy" ]; then - echo "| Railway (Backend) | ✅ Healthy |" >> /tmp/health-report.md - else - echo "| Railway (Backend) | ❌ Unhealthy |" >> /tmp/health-report.md - fi - - if [ "$RPC_STATUS" == "healthy" ]; then - echo "| Solana RPC | ✅ Healthy |" >> /tmp/health-report.md - else - echo "| Solana RPC | ❌ Unhealthy |" >> /tmp/health-report.md - fi - - echo "" >> /tmp/health-report.md - - # Check if any service is unhealthy - if [ "$VERCEL_STATUS" == "unhealthy" ] || [ "$RAILWAY_STATUS" == "unhealthy" ] || [ "$RPC_STATUS" == "unhealthy" ]; then - echo "has_issues=true" >> $GITHUB_OUTPUT - echo "## ⚠️ Action Required" >> /tmp/health-report.md - echo "" >> /tmp/health-report.md - echo "One or more services are unhealthy. Please investigate immediately." >> /tmp/health-report.md - else - echo "has_issues=false" >> $GITHUB_OUTPUT - echo "## ✅ All Systems Operational" >> /tmp/health-report.md - echo "" >> /tmp/health-report.md - echo "All services are functioning normally." >> /tmp/health-report.md - fi - - cat /tmp/health-report.md - cat /tmp/health-report.md >> $GITHUB_STEP_SUMMARY - - - name: Create issue if services are down - if: steps.report.outputs.has_issues == 'true' - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const report = fs.readFileSync('/tmp/health-report.md', 'utf8'); - - const title = '🚨 Health Check Failed - Services Down'; - const body = `${report}\n\n---\n\n**Automated health check detected issues.**\n\nPlease investigate and resolve as soon as possible.\n\n- Check service logs\n- Verify deployments\n- Check for resource issues\n\nWorkflow run: ${context.payload.repository.html_url}/actions/runs/${context.runId}`; - - // Check if there's already an open issue - const issues = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: 'health-check,automated' - }); - - if (issues.data.length === 0) { - // Create new issue - await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: title, - body: body, - labels: ['health-check', 'automated', 'priority-high'] - }); - console.log('Created new health check issue'); - } else { - // Comment on existing issue - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issues.data[0].number, - body: `## Updated Health Check Report\n\n${report}\n\n**Still experiencing issues.** Workflow run: ${context.payload.repository.html_url}/actions/runs/${context.runId}` - }); - console.log('Updated existing health check issue'); - } - - - name: Close health check issues if all healthy - if: steps.report.outputs.has_issues == 'false' - uses: actions/github-script@v9 - with: - script: | - // Find and close open health check issues - const issues = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: 'health-check,automated' - }); - - for (const issue of issues.data) { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - state: 'closed' - }); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: '✅ All services are now healthy. Closing this issue.\n\nWorkflow run: ' + context.payload.repository.html_url + '/actions/runs/' + context.runId - }); - - console.log(`Closed issue #${issue.number}`); - } diff --git a/.github/workflows/performance-monitoring.yml b/.github/workflows/performance-monitoring.yml deleted file mode 100644 index 84ebc7de..00000000 --- a/.github/workflows/performance-monitoring.yml +++ /dev/null @@ -1,157 +0,0 @@ -name: Performance Monitoring - -on: - schedule: - # Run every 6 hours - - cron: '0 */6 * * *' - workflow_dispatch: - -permissions: - contents: read - issues: write - -jobs: - monitor-dependencies: - name: Monitor Dependencies - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - - - name: Check for outdated dependencies - run: | - echo "## Backend Dependencies" >> $GITHUB_STEP_SUMMARY - echo "### Outdated packages:" >> $GITHUB_STEP_SUMMARY - npm outdated --json || echo "All packages up to date" >> $GITHUB_STEP_SUMMARY - - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Webapp Dependencies" >> $GITHUB_STEP_SUMMARY - echo "### Outdated packages:" >> $GITHUB_STEP_SUMMARY - cd webapp - npm outdated --json || echo "All packages up to date" >> $GITHUB_STEP_SUMMARY - - - name: Check security vulnerabilities - run: | - echo "## Security Audit" >> $GITHUB_STEP_SUMMARY - echo "### Backend:" >> $GITHUB_STEP_SUMMARY - npm audit --json || true - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Webapp:" >> $GITHUB_STEP_SUMMARY - cd webapp - npm audit --json || true - - build-size-analysis: - name: Build Size Analysis - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Install backend dependencies - run: npm ci - - - name: Build backend - run: npm run build - - - name: Analyze backend build size - run: | - echo "## Backend Build Analysis" >> $GITHUB_STEP_SUMMARY - echo "### Dist size:" >> $GITHUB_STEP_SUMMARY - du -sh dist >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Largest files:" >> $GITHUB_STEP_SUMMARY - find dist -type f -exec du -h {} + | sort -rh | head -n 10 >> $GITHUB_STEP_SUMMARY - - - name: Install webapp dependencies - working-directory: ./webapp - run: npm ci - - - name: Build webapp - working-directory: ./webapp - run: npm run build - env: - NEXT_PUBLIC_RPC_URL: https://api.mainnet-beta.solana.com - - - name: Analyze webapp build size - working-directory: ./webapp - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Webapp Build Analysis" >> $GITHUB_STEP_SUMMARY - echo "### .next size:" >> $GITHUB_STEP_SUMMARY - du -sh .next >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Largest files:" >> $GITHUB_STEP_SUMMARY - find .next -type f -exec du -h {} + | sort -rh | head -n 10 >> $GITHUB_STEP_SUMMARY - - code-quality-metrics: - name: Code Quality Metrics - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 # Full history for better analysis - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - - - name: Count lines of code - run: | - echo "## Code Metrics" >> $GITHUB_STEP_SUMMARY - echo "### Lines of Code:" >> $GITHUB_STEP_SUMMARY - echo "Backend TypeScript:" >> $GITHUB_STEP_SUMMARY - find src -name "*.ts" | xargs wc -l | tail -n 1 >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Webapp TypeScript/TSX:" >> $GITHUB_STEP_SUMMARY - find webapp -name "*.ts" -o -name "*.tsx" | xargs wc -l | tail -n 1 >> $GITHUB_STEP_SUMMARY - - - name: Count test files - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Test Coverage:" >> $GITHUB_STEP_SUMMARY - TEST_FILES=$(find . -name "*.test.ts" -o -name "*.test.tsx" -o -name "*.spec.ts" | wc -l) - echo "Test files: $TEST_FILES" >> $GITHUB_STEP_SUMMARY - - - name: Analyze git activity - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Recent Activity (Last 30 days):" >> $GITHUB_STEP_SUMMARY - echo "Commits: $(git log --since='30 days ago' --oneline | wc -l)" >> $GITHUB_STEP_SUMMARY - echo "Contributors: $(git log --since='30 days ago' --format='%an' | sort -u | wc -l)" >> $GITHUB_STEP_SUMMARY - - performance-report: - name: Create Performance Report - runs-on: ubuntu-latest - needs: [monitor-dependencies, build-size-analysis, code-quality-metrics] - if: always() - - steps: - - name: Create summary - run: | - echo "# 📊 Performance Monitoring Report" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Date:** $(date)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Status" >> $GITHUB_STEP_SUMMARY - echo "- Dependencies: ${{ needs.monitor-dependencies.result }}" >> $GITHUB_STEP_SUMMARY - echo "- Build Analysis: ${{ needs.build-size-analysis.result }}" >> $GITHUB_STEP_SUMMARY - echo "- Code Quality: ${{ needs.code-quality-metrics.result }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "View individual job outputs for detailed metrics." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/powershell-merge-automation.yml b/.github/workflows/powershell-merge-automation.yml deleted file mode 100644 index c3521c95..00000000 --- a/.github/workflows/powershell-merge-automation.yml +++ /dev/null @@ -1,298 +0,0 @@ -name: PowerShell Merge Automation (Optional) - -# This workflow demonstrates the PowerShell merge automation script. -# It is disabled by default. To enable, remove the `if: false` condition. - -on: - schedule: - # Run daily at 2 AM UTC (disabled by default) - - cron: '0 2 * * *' - workflow_dispatch: - inputs: - source_branches: - description: 'Comma-separated list of branches to merge (leave empty for auto-sweep)' - required: false - type: string - target_branch: - description: 'Target branch' - required: false - default: 'develop' - type: string - max_parallel_jobs: - description: 'Maximum parallel jobs (1-8)' - required: false - default: '4' - type: choice - options: - - '1' - - '2' - - '4' - - '6' - - '8' - dry_run: - description: 'Dry run mode (test without making changes)' - required: false - default: true - type: boolean - benchmark_mode: - description: 'Enable performance benchmarking' - required: false - default: false - type: boolean - -permissions: - contents: write - pull-requests: write - issues: write - -jobs: - merge-branches: - name: Automated Branch Merge - runs-on: ubuntu-latest - # Disabled by default - remove this line to enable - if: false - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 # Full history needed for merge operations - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Configure Git - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - - name: Validate PowerShell Installation - shell: pwsh - run: | - Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)" - Write-Host "Git Version: $(git --version)" - - - name: Run Test Suite - shell: pwsh - run: | - Write-Host "Running validation tests..." - ./scripts/Test-MergeBranches.ps1 - - - name: Prepare Merge Parameters - id: params - shell: pwsh - run: | - $isAutoSweep = "${{ inputs.source_branches }}" -eq "" - $isDryRun = "${{ inputs.dry_run }}" -eq "true" - $isBenchmark = "${{ inputs.benchmark_mode }}" -eq "true" - - Write-Output "auto_sweep=$isAutoSweep" >> $env:GITHUB_OUTPUT - Write-Output "dry_run=$isDryRun" >> $env:GITHUB_OUTPUT - Write-Output "benchmark=$isBenchmark" >> $env:GITHUB_OUTPUT - - Write-Host "Configuration:" - Write-Host " Auto-Sweep: $isAutoSweep" - Write-Host " Dry Run: $isDryRun" - Write-Host " Benchmark: $isBenchmark" - Write-Host " Target Branch: ${{ inputs.target_branch }}" - Write-Host " Max Parallel Jobs: ${{ inputs.max_parallel_jobs }}" - - - name: Run Merge Script - shell: pwsh - run: | - $params = @{ - TargetBranch = "${{ inputs.target_branch }}" - MaxParallelJobs = [int]"${{ inputs.max_parallel_jobs }}" - UseWorktrees = $true - IncrementalMode = $true - } - - # Add source branches if specified - if ("${{ inputs.source_branches }}" -ne "") { - $branches = "${{ inputs.source_branches }}" -split ',' | - ForEach-Object { $_.Trim() } | - Where-Object { $_ } - $params.SourceBranches = $branches - Write-Host "Merging specific branches: $($branches -join ', ')" - } else { - $params.AutoSweep = $true - Write-Host "Auto-sweep mode: will merge all feature/* branches" - } - - # Add optional flags - if ("${{ inputs.dry_run }}" -eq "true") { - $params.DryRun = $true - Write-Host "DRY RUN MODE - No changes will be made" - } - - if ("${{ inputs.benchmark_mode }}" -eq "true") { - $params.BenchmarkMode = $true - Write-Host "Benchmark mode enabled" - } - - # Execute merge script - Write-Host "`nExecuting merge script with parameters:" - $params.GetEnumerator() | ForEach-Object { - Write-Host " $($_.Key): $($_.Value)" - } - Write-Host "" - - ./scripts/Merge-Branches.ps1 @params - - - name: Upload Merge Logs - if: always() - uses: actions/upload-artifact@v7 - with: - name: merge-logs-${{ github.run_number }}-${{ github.run_attempt }} - path: scripts/merge-log-*.txt - retention-days: 30 - if-no-files-found: warn - - - name: Create Summary - if: always() - shell: pwsh - run: | - Write-Host "Creating job summary..." - - # Find latest log file - $logFile = Get-ChildItem -Path scripts -Filter "merge-log-*.txt" | - Sort-Object LastWriteTime -Descending | - Select-Object -First 1 - - if ($logFile) { - Write-Host "Found log file: $($logFile.Name)" - - $logContent = Get-Content $logFile.FullName -Raw - - # Extract summary information - $successLines = ($logContent -split "`n" | Where-Object { $_ -match '\[Success\]' }).Count - $errorLines = ($logContent -split "`n" | Where-Object { $_ -match '\[Error\]' }).Count - $warningLines = ($logContent -split "`n" | Where-Object { $_ -match '\[Warning\]' }).Count - - # Create GitHub summary - @" - ## 🔄 Merge Automation Summary - - **Configuration:** - - Target Branch: \`${{ inputs.target_branch }}\` - - Max Parallel Jobs: ${{ inputs.max_parallel_jobs }} - - Dry Run: ${{ inputs.dry_run }} - - Benchmark Mode: ${{ inputs.benchmark_mode }} - - **Results:** - - ✅ Success Events: $successLines - - ⚠️ Warnings: $warningLines - - ❌ Errors: $errorLines - - **Logs:** See artifacts for detailed logs - "@ | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append - } else { - Write-Host "No log file found" - } - - - name: Create Issue on Failure - if: failure() && github.event_name == 'schedule' - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const path = require('path'); - - // Find latest log file - const scriptsDir = path.join(process.env.GITHUB_WORKSPACE, 'scripts'); - let errorDetails = 'Log file not found'; - - try { - const logFiles = fs.readdirSync(scriptsDir) - .filter(f => f.startsWith('merge-log-')) - .sort() - .reverse(); - - if (logFiles.length > 0) { - const logPath = path.join(scriptsDir, logFiles[0]); - const logContent = fs.readFileSync(logPath, 'utf8'); - const errors = logContent.split('\n') - .filter(line => line.includes('[Error]')) - .slice(-10) - .join('\n'); - errorDetails = errors || 'No specific errors found in log'; - } - } catch (err) { - errorDetails = `Error reading log: ${err.message}`; - } - - await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: `🔴 Automated Merge Failed - ${new Date().toISOString().split('T')[0]}`, - body: `## PowerShell Merge Automation Failure\n\n` + - `**Workflow Run:** [#${context.runNumber}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n` + - `**Run Attempt:** ${context.runAttempt}\n` + - `**Trigger:** ${context.eventName}\n\n` + - `### Recent Errors\n\`\`\`\n${errorDetails}\n\`\`\`\n\n` + - `### Actions Required\n` + - `1. Review the [workflow logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n` + - `2. Check the uploaded log artifacts\n` + - `3. Investigate and resolve merge conflicts\n` + - `4. Re-run the workflow or perform manual merge\n\n` + - `### Troubleshooting\n` + - `See [Merge Automation Documentation](./docs/MERGE_AUTOMATION.md#troubleshooting) for common issues.`, - labels: ['automation', 'merge-failure', 'needs-investigation'] - }); - - - name: Comment Success - if: success() && github.event_name == 'workflow_dispatch' - uses: actions/github-script@v9 - with: - script: | - // If triggered manually, post a success comment to a tracking issue - console.log('✅ Merge automation completed successfully'); - - // Optionally create a success notification issue - if ("${{ inputs.dry_run }}" === "false") { - await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: `✅ Merge Automation Success - ${new Date().toISOString().split('T')[0]}`, - body: `## PowerShell Merge Automation Success\n\n` + - `**Workflow Run:** [#${context.runNumber}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n` + - `**Target Branch:** \`${{ inputs.target_branch }}\`\n` + - `**Max Parallel Jobs:** ${{ inputs.max_parallel_jobs }}\n\n` + - `All branches merged successfully! 🎉`, - labels: ['automation', 'merge-success'] - }); - } - - # Optional: Add a job to clean up old merge logs - cleanup-old-logs: - name: Cleanup Old Merge Logs - runs-on: ubuntu-latest - if: false # Disabled by default - needs: merge-branches - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Remove Old Log Files - shell: pwsh - run: | - # Remove log files older than 7 days - $cutoffDate = (Get-Date).AddDays(-7) - Get-ChildItem -Path scripts -Filter "merge-log-*.txt" | - Where-Object { $_.LastWriteTime -lt $cutoffDate } | - ForEach-Object { - Write-Host "Removing old log: $($_.Name)" - Remove-Item $_.FullName -Force - } - - - name: Commit Cleanup - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - if [[ -n $(git status --porcelain) ]]; then - git add scripts/ - git commit -m "chore: cleanup old merge logs [skip ci]" - git push - else - echo "No old logs to remove" - fi diff --git a/.github/workflows/rename-vercel-hosts.yml b/.github/workflows/rename-vercel-hosts.yml deleted file mode 100644 index 07a49b5b..00000000 --- a/.github/workflows/rename-vercel-hosts.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: Rename Vercel Hosts and Create GXQ Branch - -on: - push: - branches: - - main - -jobs: - sync-to-gxq: - runs-on: ubuntu-latest - - permissions: - contents: write - pull-requests: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Configure Git - run: | - git config --global user.name 'github-actions[bot]' - git config --global user.email 'github-actions[bot]@users.noreply.github.com' - - - name: Create or update gxq branch - run: | - # Fetch all branches - git fetch origin - - # Check if gxq branch exists remotely - if git ls-remote --heads origin gxq | grep -q 'refs/heads/gxq$'; then - echo "gxq branch exists, syncing with main" - git checkout -B gxq origin/gxq - git merge origin/main -m "Sync: Merge latest changes from main to gxq" - else - echo "Creating new gxq branch from main" - git checkout -b gxq origin/main - fi - - - name: Apply hostname/metadata replacements - run: | - echo "Applying hostname/metadata replacements for Vercel deployment..." - - # Create marker file to track replacements - mkdir -p .github - cat > .github/HOSTNAME_REPLACEMENTS.txt << EOF - Hostname/metadata replacements applied on $(date) - - This branch (gxq) is synchronized with main and configured for Vercel deployment. - - Environment Configuration: - - NEXT_PUBLIC_RPC_URL should be set in Vercel dashboard - - Root directory: webapp - - Framework: Next.js - EOF - - # Add any specific hostname replacements here if needed - # For example, updating API endpoints, base URLs, etc. - - git add .github/HOSTNAME_REPLACEMENTS.txt - - - name: Commit and push changes - run: | - if ! git diff --staged --quiet; then - git commit -m "Apply hostname/metadata replacements for Vercel deployment" - fi - git push origin gxq - - - name: Create or Update Pull Request - uses: peter-evans/create-pull-request@v8 - with: - token: ${{ secrets.GITHUB_TOKEN }} - branch: gxq - base: main - title: '[Auto] Sync from main with Vercel deployment configuration' - body: | - ## Automated Sync from main to gxq - - This PR was automatically created by the GitHub Actions workflow when changes were pushed to `main`. - - ### Changes - - Synced latest changes from main branch - - Applied hostname/metadata replacements for Vercel deployment - - Updated deployment configuration markers - - ### Deployment - - **Branch**: `gxq` - - **Root Directory**: `webapp` - - **Framework**: Next.js - - **Environment**: Configure `NEXT_PUBLIC_RPC_URL` in Vercel - - This branch is ready to be deployed to Vercel with appropriate configurations. - - ### What's Next - 1. Review the changes in this PR - 2. Merge to main if everything looks good - 3. The gxq branch will be automatically redeployed on Vercel - - --- - - 🤖 **Auto-generated** by GitHub Actions - labels: | - automated - deployment - vercel - sync diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..2332268a --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,55 @@ +name: security + +on: + pull_request: + push: + branches: + - main + schedule: + - cron: '17 3 * * 1' + workflow_dispatch: + +permissions: + contents: read + security-events: write + +jobs: + audit: + name: audit + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + webapp/package-lock.json + - run: npm ci --no-fund + - run: npm audit --audit-level=high + - run: npm --prefix webapp ci --no-fund + - run: npm --prefix webapp audit --audit-level=high + + dependency-review: + name: dependency-review + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + pull-requests: read + steps: + - uses: actions/checkout@v6 + - uses: actions/dependency-review-action@v5 + + secret-scan: + name: secret-scan + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/self-heal.yml b/.github/workflows/self-heal.yml new file mode 100644 index 00000000..2cc46ce0 --- /dev/null +++ b/.github/workflows/self-heal.yml @@ -0,0 +1,29 @@ +name: self-heal + +on: + schedule: + - cron: '41 2 * * *' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: self-heal-${{ github.ref }} + cancel-in-progress: true + +jobs: + converge: + name: converge + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + webapp/package-lock.json + - run: bash scripts/converge.sh --ci --self-heal diff --git a/.github/workflows/self-optimize.yml b/.github/workflows/self-optimize.yml deleted file mode 100644 index c3c852fe..00000000 --- a/.github/workflows/self-optimize.yml +++ /dev/null @@ -1,582 +0,0 @@ -name: Continuous Self-Optimization - -on: - pull_request: - branches: - - main - - develop - - dev - types: [opened, synchronize, reopened] - -permissions: - contents: read - pull-requests: write - issues: read - -concurrency: - group: self-optimize-${{ github.ref }} - cancel-in-progress: true - -jobs: - analyze-and-optimize: - name: Analyze & Optimize Codebase - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - ref: ${{ github.event.pull_request.head.ref }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: | - npm ci - cd webapp && npm ci - - - name: Run ESLint with auto-fix - id: eslint-fix - run: | - echo "## ESLint Auto-Fix Results" > /tmp/eslint-report.md - echo "" >> /tmp/eslint-report.md - - # Backend fixes - echo "### Backend Fixes" >> /tmp/eslint-report.md - npx eslint 'src/**/*.ts' 'api/**/*.ts' 'scripts/**/*.ts' --fix --format=json --output-file=/tmp/backend-lint.json || true - - # Webapp fixes - echo "### Webapp Fixes" >> /tmp/eslint-report.md - cd webapp - npx eslint . --fix --format=json --output-file=/tmp/webapp-lint.json || true - cd .. - - # Check if files were modified - if [[ -n $(git status --porcelain) ]]; then - echo "fixed=true" >> $GITHUB_OUTPUT - git diff --stat >> /tmp/eslint-report.md - else - echo "fixed=false" >> $GITHUB_OUTPUT - echo "No auto-fixable issues found." >> /tmp/eslint-report.md - fi - - - name: Detect and remove unused code - id: unused-code - run: | - echo "## Unused Code Detection" > /tmp/unused-code-report.md - echo "" >> /tmp/unused-code-report.md - - # Use ts-prune from pinned devDependencies (installed via npm ci) - - # Detect unused exports - echo "### Unused Exports" >> /tmp/unused-code-report.md - npx ts-prune --error || echo "Unused exports detected" >> /tmp/unused-code-report.md - npx ts-prune > /tmp/unused-exports.txt || true - - # Count unused exports - UNUSED_COUNT=$(cat /tmp/unused-exports.txt | grep -c "used in module" || echo "0") - echo "Found $UNUSED_COUNT unused exports" >> /tmp/unused-code-report.md - echo "unused_count=$UNUSED_COUNT" >> $GITHUB_OUTPUT - - if [[ $UNUSED_COUNT -gt 0 ]]; then - echo "" >> /tmp/unused-code-report.md - echo "#### Details:" >> /tmp/unused-code-report.md - echo '```' >> /tmp/unused-code-report.md - head -n 50 /tmp/unused-exports.txt >> /tmp/unused-code-report.md - echo '```' >> /tmp/unused-code-report.md - fi - - - name: Analyze code complexity - id: complexity - run: | - echo "## Code Complexity Analysis" > /tmp/complexity-report.md - echo "" >> /tmp/complexity-report.md - - # Use eslint-plugin-complexity from pinned devDependencies (installed via npm ci) - - # Run complexity analysis - echo "Analyzing cyclomatic complexity..." >> /tmp/complexity-report.md - - # Create temporary eslint config with complexity rules - echo '{' > /tmp/.eslintrc.complexity.json - echo ' "extends": ["./.eslintrc.json"],' >> /tmp/.eslintrc.complexity.json - echo ' "plugins": ["complexity"],' >> /tmp/.eslintrc.complexity.json - echo ' "rules": {' >> /tmp/.eslintrc.complexity.json - echo ' "complexity": ["warn", 10],' >> /tmp/.eslintrc.complexity.json - echo ' "max-depth": ["warn", 4],' >> /tmp/.eslintrc.complexity.json - echo ' "max-lines-per-function": ["warn", 100],' >> /tmp/.eslintrc.complexity.json - echo ' "max-nested-callbacks": ["warn", 3]' >> /tmp/.eslintrc.complexity.json - echo ' }' >> /tmp/.eslintrc.complexity.json - echo '}' >> /tmp/.eslintrc.complexity.json - - # Run analysis - npx eslint 'src/**/*.ts' -c /tmp/.eslintrc.complexity.json --format=json --output-file=/tmp/complexity.json || true - - # Parse and report high complexity functions - node -e " - const fs = require('fs'); - try { - const results = JSON.parse(fs.readFileSync('/tmp/complexity.json', 'utf8')); - const complexIssues = results - .flatMap(r => r.messages - .filter(m => m.ruleId && m.ruleId.includes('complexity')) - .map(m => ({file: r.filePath, line: m.line, message: m.message})) - ); - - if (complexIssues.length > 0) { - console.log('### High Complexity Issues Found:'); - console.log(''); - complexIssues.slice(0, 20).forEach(issue => { - console.log('- ' + issue.file.split('/').pop() + ':' + issue.line + ' - ' + issue.message); - }); - console.log(''); - console.log('Total issues: ' + complexIssues.length); - } else { - console.log('No high complexity issues found.'); - } - } catch (e) { - console.log('Analysis completed.'); - } - " >> /tmp/complexity-report.md - - - name: Check test coverage gaps - id: coverage-gaps - run: | - echo "## Test Coverage Analysis" > /tmp/coverage-report.md - echo "" >> /tmp/coverage-report.md - - # Run tests with coverage - npm test -- --coverage --coverageReporters=json --coverageReporters=text || true - - # Analyze coverage - if [[ -f coverage/coverage-summary.json ]]; then - node -e " - const fs = require('fs'); - const coverage = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8')); - - console.log('### Coverage Summary:'); - console.log(''); - const total = coverage.total; - console.log('- Statements: ' + total.statements.pct + '%'); - console.log('- Branches: ' + total.branches.pct + '%'); - console.log('- Functions: ' + total.functions.pct + '%'); - console.log('- Lines: ' + total.lines.pct + '%'); - console.log(''); - - // Find files with low coverage - const lowCoverage = Object.entries(coverage) - .filter(([file, data]) => file !== 'total' && data.lines.pct < 80) - .sort((a, b) => a[1].lines.pct - b[1].lines.pct); - - if (lowCoverage.length > 0) { - console.log('### Files with Low Coverage (<80%):'); - console.log(''); - lowCoverage.slice(0, 15).forEach(([file, data]) => { - console.log('- ' + file.split('/').slice(-2).join('/') + ': ' + data.lines.pct + '%'); - }); - } - " >> /tmp/coverage-report.md - else - echo "No coverage data available." >> /tmp/coverage-report.md - fi - - - name: Identify risky code patterns - id: risky-code - run: | - echo "## Risky Code Pattern Detection" > /tmp/risky-code-report.md - echo "" >> /tmp/risky-code-report.md - - # Search for risky patterns - echo "### Potential Security Issues:" >> /tmp/risky-code-report.md - echo "" >> /tmp/risky-code-report.md - - RISKY_FOUND="false" - - # Check for eval usage - EVAL_COUNT=$(grep -r "eval(" src/ --include="*.ts" 2>/dev/null | wc -l || echo "0") - if [[ $EVAL_COUNT -gt 0 ]]; then - echo "⚠️ **eval() usage detected ($EVAL_COUNT instances)** - High security risk" >> /tmp/risky-code-report.md - grep -rn "eval(" src/ --include="*.ts" 2>/dev/null | head -n 10 >> /tmp/risky-code-report.md || true - echo "" >> /tmp/risky-code-report.md - RISKY_FOUND="true" - fi - - # Check for any usage - ANY_COUNT=$(grep -r ": any" src/ --include="*.ts" 2>/dev/null | wc -l || echo "0") - if [[ $ANY_COUNT -gt 100 ]]; then - echo "⚠️ **Excessive 'any' type usage ($ANY_COUNT instances)** - Type safety compromised" >> /tmp/risky-code-report.md - echo "" >> /tmp/risky-code-report.md - RISKY_FOUND="true" - fi - - # Check for TODO/FIXME comments - TODO_COUNT=$(grep -r "TODO\|FIXME" src/ --include="*.ts" 2>/dev/null | wc -l || echo "0") - if [[ $TODO_COUNT -gt 0 ]]; then - echo "📝 **Found $TODO_COUNT TODO/FIXME comments** - Technical debt identified" >> /tmp/risky-code-report.md - grep -rn "TODO\|FIXME" src/ --include="*.ts" 2>/dev/null | head -n 20 >> /tmp/risky-code-report.md || true - echo "" >> /tmp/risky-code-report.md - RISKY_FOUND="true" - fi - - # Check for console.log in production code - CONSOLE_COUNT=$(grep -r "console.log" src/ --include="*.ts" --exclude="*logger*" 2>/dev/null | wc -l || echo "0") - if [[ $CONSOLE_COUNT -gt 0 ]]; then - echo "⚠️ **console.log() in production code ($CONSOLE_COUNT instances)** - Should use logger" >> /tmp/risky-code-report.md - echo "" >> /tmp/risky-code-report.md - RISKY_FOUND="true" - fi - - # Check for private key handling - KEY_COUNT=$(grep -r "privateKey\|private_key\|PRIVATE_KEY" src/ --include="*.ts" 2>/dev/null | grep -v "WALLET_PRIVATE_KEY" | wc -l || echo "0") - if [[ $KEY_COUNT -gt 0 ]]; then - echo "🔐 **Private key references detected ($KEY_COUNT)** - Verify secure handling" >> /tmp/risky-code-report.md - echo "" >> /tmp/risky-code-report.md - RISKY_FOUND="true" - fi - - echo "risky_patterns_found=$RISKY_FOUND" >> $GITHUB_OUTPUT - - - name: Report automated fixes status - id: report-fixes - if: steps.eslint-fix.outputs.fixed == 'true' - run: | - echo "## ⚠️ Automated Fixes Required" > /tmp/fix-required.md - echo "" >> /tmp/fix-required.md - echo "This PR has auto-fixable issues. However, automated fixes are NOT pushed to your branch." >> /tmp/fix-required.md - echo "" >> /tmp/fix-required.md - echo "### Manual Steps Required:" >> /tmp/fix-required.md - echo "1. Run \`npm run lint:fix\` locally to apply ESLint fixes" >> /tmp/fix-required.md - echo "2. Run \`cd webapp && npm run lint -- --fix\` for webapp fixes" >> /tmp/fix-required.md - echo "3. Review and commit the changes" >> /tmp/fix-required.md - echo "4. Push to your branch" >> /tmp/fix-required.md - echo "" >> /tmp/fix-required.md - echo "Alternatively, a maintainer can create a fix branch and PR for you." >> /tmp/fix-required.md - - - name: Generate comprehensive PR comment - id: generate-comment - run: | - echo "## 🤖 Self-Optimization Report" > /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "This PR has been analyzed for code quality, security, and optimization opportunities." >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "---" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - - # Add each report section - cat /tmp/eslint-report.md >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "---" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - - cat /tmp/unused-code-report.md >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "---" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - - cat /tmp/complexity-report.md >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "---" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - - cat /tmp/coverage-report.md >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "---" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - - cat /tmp/risky-code-report.md >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "---" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - - # Add fix-required notice if applicable - if [[ -f /tmp/fix-required.md ]]; then - cat /tmp/fix-required.md >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "---" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - fi - - # Add summary - echo "" >> /tmp/pr-comment.md - echo "### 📊 Summary" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "- 📝 Review the reports above for manual attention items" >> /tmp/pr-comment.md - echo "- 🔍 Check inline comments for specific recommendations" >> /tmp/pr-comment.md - echo "- ⚠️ Address any flagged security or complexity issues" >> /tmp/pr-comment.md - echo "- 📦 Full analysis artifacts available in workflow run" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "### Next Steps" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "1. Review automated changes committed by this workflow" >> /tmp/pr-comment.md - echo "2. Address any flagged security or complexity issues" >> /tmp/pr-comment.md - echo "3. Consider refactoring high-complexity functions" >> /tmp/pr-comment.md - echo "4. Add tests for low-coverage areas" >> /tmp/pr-comment.md - echo "5. Remove or document TODO/FIXME items" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "---" >> /tmp/pr-comment.md - echo "" >> /tmp/pr-comment.md - echo "*🤖 Generated by Continuous Self-Optimization Workflow*" >> /tmp/pr-comment.md - - - name: Post PR comment - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const comment = fs.readFileSync('/tmp/pr-comment.md', 'utf8'); - - // Find existing comment from this workflow - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('Self-Optimization Report') - ); - - if (botComment) { - // Update existing comment - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: comment - }); - } else { - // Create new comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); - } - - - name: Create inline PR review comments - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const { execSync } = require('child_process'); - - // Get changed files in this PR - const changedFiles = execSync('git diff --name-only origin/${{ github.base_ref }}...HEAD') - .toString() - .trim() - .split('\n') - .filter(f => f.endsWith('.ts') || f.endsWith('.tsx')); - - const comments = []; - // Use a Map to deduplicate comments by file:line - const commentMap = new Map(); - - // Parse complexity issues - try { - const complexityData = JSON.parse(fs.readFileSync('/tmp/complexity.json', 'utf8')); - - for (const result of complexityData) { - const file = result.filePath.replace(process.cwd() + '/', ''); - - if (!changedFiles.includes(file)) continue; - - for (const message of result.messages) { - if (message.ruleId && (message.ruleId.includes('complexity') || message.ruleId.includes('max-'))) { - const key = `${file}:${message.line}`; - const commentBody = `⚠️ **${message.ruleId}**: ${message.message}\n\n**Suggestion:** Consider refactoring this function to reduce complexity and improve maintainability.`; - - if (!commentMap.has(key)) { - commentMap.set(key, { - path: file, - line: message.line, - body: commentBody - }); - } else { - // Aggregate findings for the same line - const existing = commentMap.get(key); - existing.body += '\n\n---\n\n' + commentBody; - } - } - } - } - } catch (e) { - console.log('No complexity issues to comment on'); - } - - // Add comments for TODO/FIXME - for (const file of changedFiles) { - try { - const content = fs.readFileSync(file, 'utf8'); - const lines = content.split('\n'); - - lines.forEach((line, index) => { - const lineNum = index + 1; - const key = `${file}:${lineNum}`; - - if (line.includes('TODO') || line.includes('FIXME')) { - const commentBody = '📝 **Technical Debt Detected**: This TODO/FIXME should be addressed before merging to production.\n\n**Action Required:** Either resolve the issue or create a tracking issue.'; - - if (!commentMap.has(key)) { - commentMap.set(key, { - path: file, - line: lineNum, - body: commentBody - }); - } else { - const existing = commentMap.get(key); - existing.body += '\n\n---\n\n' + commentBody; - } - } - - if (line.includes('console.log') && !file.includes('logger')) { - const commentBody = '⚠️ **Logging Issue**: Using console.log in production code.\n\n**Recommendation:** Replace with proper logger utility from `src/utils/logger.ts`.'; - - if (!commentMap.has(key)) { - commentMap.set(key, { - path: file, - line: lineNum, - body: commentBody - }); - } else { - const existing = commentMap.get(key); - existing.body += '\n\n---\n\n' + commentBody; - } - } - - if (line.includes('eval(')) { - const commentBody = '🚨 **Security Risk**: eval() is dangerous and should be avoided.\n\n**Action Required:** Refactor to use safer alternatives. This is a critical security issue.'; - - if (!commentMap.has(key)) { - commentMap.set(key, { - path: file, - line: lineNum, - body: commentBody - }); - } else { - const existing = commentMap.get(key); - existing.body += '\n\n---\n\n' + commentBody; - } - } - }); - } catch (e) { - console.log(`Could not analyze file: ${file}`); - } - } - - // Convert map to array (deduplicated comments) - const deduplicatedComments = Array.from(commentMap.values()); - - // Post inline comments (max 50 to avoid rate limits) - const limitedComments = deduplicatedComments.slice(0, 50); - - if (limitedComments.length > 0) { - try { - await github.rest.pulls.createReview({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - event: 'COMMENT', - comments: limitedComments - }); - } catch (error) { - console.log('Could not post inline comments:', error.message); - } - } - - - name: Upload analysis artifacts - uses: actions/upload-artifact@v7 - if: always() - with: - name: self-optimization-reports - path: | - /tmp/*-report.md - /tmp/*.json - /tmp/*.txt - retention-days: 30 - - validate-production-readiness: - name: Validate Production Readiness - runs-on: ubuntu-latest - needs: analyze-and-optimize - timeout-minutes: 20 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - ref: ${{ github.event.pull_request.head.ref }} - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: | - npm ci - cd webapp && npm ci - - - name: Verify no mock implementations - id: verify-no-mocks - run: | - echo "## Production Readiness Check" > /tmp/prod-check.md - echo "" >> /tmp/prod-check.md - - # Check for mock implementations - MOCK_COUNT=$(grep -r "mock\|Mock\|MOCK\|placeholder\|Placeholder\|PLACEHOLDER" src/ --include="*.ts" | grep -v "test" | grep -v "spec" | wc -l || echo "0") - - if [[ $MOCK_COUNT -gt 0 ]]; then - echo "⚠️ **Found $MOCK_COUNT potential mock/placeholder implementations**" >> /tmp/prod-check.md - echo "" >> /tmp/prod-check.md - grep -rn "mock\|Mock\|MOCK\|placeholder\|Placeholder\|PLACEHOLDER" src/ --include="*.ts" | grep -v "test" | grep -v "spec" | head -n 20 >> /tmp/prod-check.md - echo "" >> /tmp/prod-check.md - echo "mock_found=true" >> $GITHUB_OUTPUT - else - echo "✅ No mock implementations detected" >> /tmp/prod-check.md - echo "mock_found=false" >> $GITHUB_OUTPUT - fi - - - name: Run full test suite - run: | - npm test -- --ci --coverage || echo "Some tests failed" - - - name: Verify build succeeds - run: | - npm run build:backend - npm run build:webapp - - - name: Update production readiness comment - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const prodCheck = fs.readFileSync('/tmp/prod-check.md', 'utf8'); - - const comment = '## ✅ Production Readiness Validation\n\n' + - prodCheck + '\n\n' + - '### Build Status\n' + - '- ✅ Backend build: Success\n' + - '- ✅ Webapp build: Success\n' + - '- ✅ All tests: Passed\n\n' + - '### Safety Checks\n' + - '- ✅ No breaking changes detected\n' + - '- ✅ Type safety enforced\n' + - '- ✅ Security scan passed\n\n' + - '---\n\n' + - '*🤖 Generated by Continuous Self-Optimization Workflow*'; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); diff --git a/.github/workflows/sync-railway-secrets.yml b/.github/workflows/sync-railway-secrets.yml deleted file mode 100644 index d64c6c73..00000000 --- a/.github/workflows/sync-railway-secrets.yml +++ /dev/null @@ -1,274 +0,0 @@ -name: Sync Railway Secrets - -on: - workflow_dispatch: - inputs: - environment: - description: 'Target environment to sync secrets' - required: true - default: 'production' - type: choice - options: - - production - - preview - - all - dry_run: - description: 'Dry run (preview changes without applying)' - required: false - default: false - type: boolean - -permissions: - contents: read - -jobs: - validate-secrets: - name: Validate Required Secrets - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - has-secrets: ${{ steps.check.outputs.has-secrets }} - missing-secrets: ${{ steps.check.outputs.missing-secrets }} - - steps: - - name: Check required secrets - id: check - run: | - MISSING_SECRETS="" - ALL_SECRETS_PRESENT=true - - echo "🔍 Validating required secrets..." - - if [ -z "${{ secrets.RAILWAY_TOKEN }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}RAILWAY_TOKEN, " - ALL_SECRETS_PRESENT=false - fi - - if [ -z "${{ secrets.RAILWAY_PROJECT_ID }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}RAILWAY_PROJECT_ID, " - ALL_SECRETS_PRESENT=false - fi - - if [ -z "${{ secrets.SOLANA_RPC_URL }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}SOLANA_RPC_URL, " - ALL_SECRETS_PRESENT=false - fi - - if [ -z "${{ secrets.WALLET_PRIVATE_KEY }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}WALLET_PRIVATE_KEY, " - ALL_SECRETS_PRESENT=false - fi - - if [ -z "${{ secrets.ADMIN_USERNAME }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}ADMIN_USERNAME, " - ALL_SECRETS_PRESENT=false - fi - - if [ -z "${{ secrets.ADMIN_PASSWORD }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}ADMIN_PASSWORD, " - ALL_SECRETS_PRESENT=false - fi - - if [ -z "${{ secrets.JWT_SECRET }}" ]; then - MISSING_SECRETS="${MISSING_SECRETS}JWT_SECRET, " - ALL_SECRETS_PRESENT=false - fi - - if [ "$ALL_SECRETS_PRESENT" = true ]; then - echo "has-secrets=true" >> $GITHUB_OUTPUT - echo "missing-secrets=" >> $GITHUB_OUTPUT - echo "✅ All required secrets are configured" - else - echo "has-secrets=false" >> $GITHUB_OUTPUT - echo "missing-secrets=$MISSING_SECRETS" >> $GITHUB_OUTPUT - echo "⚠️ Missing required secrets: $MISSING_SECRETS" - echo "::warning::Missing required secrets: $MISSING_SECRETS" - exit 1 - fi - - sync-secrets: - name: Sync Secrets to Railway - runs-on: ubuntu-latest - needs: validate-secrets - if: needs.validate-secrets.outputs.has-secrets == 'true' - timeout-minutes: 10 - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Install Railway CLI - run: | - echo "📦 Installing Railway CLI..." - curl -fsSL https://railway.app/install.sh | sh - echo "$HOME/.railway/bin" >> $GITHUB_PATH - - - name: Verify Railway CLI installation - run: | - railway --version - echo "✅ Railway CLI installed successfully" - - - name: Link to Railway project - run: | - echo "🔗 Linking to Railway project..." - railway link ${{ secrets.RAILWAY_PROJECT_ID }} - echo "✅ Project linked successfully" - env: - RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} - - - name: Sync secrets (Dry Run) - if: ${{ inputs.dry_run == true }} - run: | - echo "🔍 DRY RUN MODE - Preview of secrets that would be synced:" - echo "" - echo "Environment: ${{ inputs.environment }}" - echo "" - echo "Secrets to sync:" - echo " ✓ SOLANA_RPC_URL (configured)" - echo " ✓ WALLET_PRIVATE_KEY (configured)" - echo " ✓ ADMIN_USERNAME (configured)" - echo " ✓ ADMIN_PASSWORD (configured)" - echo " ✓ JWT_SECRET (configured)" - echo " ✓ NODE_ENV = production" - echo " ✓ MINIMUM_PROFIT_SOL = 0.01" - echo " ✓ MAX_SLIPPAGE = 0.01" - echo " ✓ DEV_FEE_ENABLED = true" - echo " ✓ DEV_FEE_PERCENTAGE = 0.10" - echo " ✓ LOG_LEVEL = info" - echo "" - echo "ℹ️ No changes were made. Run without dry_run to apply changes." - env: - RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} - - - name: Sync secrets to production - if: ${{ inputs.dry_run == false && (inputs.environment == 'production' || inputs.environment == 'all') }} - run: | - echo "🔄 Syncing secrets to production environment..." - - # Core secrets - railway variables --set SOLANA_RPC_URL="${{ secrets.SOLANA_RPC_URL }}" - railway variables --set WALLET_PRIVATE_KEY="${{ secrets.WALLET_PRIVATE_KEY }}" - railway variables --set ADMIN_USERNAME="${{ secrets.ADMIN_USERNAME }}" - railway variables --set ADMIN_PASSWORD="${{ secrets.ADMIN_PASSWORD }}" - railway variables --set JWT_SECRET="${{ secrets.JWT_SECRET }}" - - # Environment configuration - railway variables --set NODE_ENV="production" - railway variables --set LOG_LEVEL="info" - - # Trading parameters - railway variables --set MINIMUM_PROFIT_SOL="0.01" - railway variables --set MAX_SLIPPAGE="0.01" - railway variables --set DEV_FEE_ENABLED="true" - railway variables --set DEV_FEE_PERCENTAGE="0.10" - - echo "✅ Production secrets synced successfully" - env: - RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} - - - name: Sync secrets to preview - if: ${{ inputs.dry_run == false && (inputs.environment == 'preview' || inputs.environment == 'all') }} - run: | - echo "🔄 Syncing secrets to preview environment..." - - # Core secrets (same as production) - railway variables --set SOLANA_RPC_URL="${{ secrets.SOLANA_RPC_URL }}" - railway variables --set WALLET_PRIVATE_KEY="${{ secrets.WALLET_PRIVATE_KEY }}" - railway variables --set ADMIN_USERNAME="${{ secrets.ADMIN_USERNAME }}" - railway variables --set ADMIN_PASSWORD="${{ secrets.ADMIN_PASSWORD }}" - railway variables --set JWT_SECRET="${{ secrets.JWT_SECRET }}" - - # Environment configuration (preview-specific) - railway variables --set NODE_ENV="preview" - railway variables --set LOG_LEVEL="debug" - - # Trading parameters (more conservative for preview) - railway variables --set MINIMUM_PROFIT_SOL="0.05" - railway variables --set MAX_SLIPPAGE="0.005" - railway variables --set DEV_FEE_ENABLED="false" - - echo "✅ Preview secrets synced successfully" - env: - RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} - - - name: Verify secrets - if: ${{ inputs.dry_run == false }} - run: | - echo "🔍 Verifying secrets were set correctly..." - railway variables | grep -E "NODE_ENV|LOG_LEVEL" || echo "Unable to verify secrets" - echo "✅ Verification complete" - env: - RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} - - - name: Summary - run: | - echo "📊 Secret Sync Summary" - echo "=====================" - echo "Environment: ${{ inputs.environment }}" - echo "Dry Run: ${{ inputs.dry_run }}" - echo "Status: ${{ inputs.dry_run == true && 'Preview Only' || 'Applied' }}" - echo "" - if [ "${{ inputs.dry_run }}" = "true" ]; then - echo "ℹ️ This was a dry run. No changes were made." - echo "Run the workflow again without dry_run to apply changes." - else - echo "✅ Secrets have been synced to Railway." - echo "Railway will automatically reload the application with new environment variables." - fi - - - name: Create summary - run: | - echo "## 🔐 Railway Secret Sync Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Environment:** ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY - echo "**Dry Run:** ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ inputs.dry_run }}" = "true" ]; then - echo "### ℹ️ Dry Run Mode" >> $GITHUB_STEP_SUMMARY - echo "No changes were applied. This was a preview only." >> $GITHUB_STEP_SUMMARY - else - echo "### ✅ Secrets Synced Successfully" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "The following secrets were synced:" >> $GITHUB_STEP_SUMMARY - echo "- SOLANA_RPC_URL" >> $GITHUB_STEP_SUMMARY - echo "- WALLET_PRIVATE_KEY" >> $GITHUB_STEP_SUMMARY - echo "- ADMIN_USERNAME" >> $GITHUB_STEP_SUMMARY - echo "- ADMIN_PASSWORD" >> $GITHUB_STEP_SUMMARY - echo "- JWT_SECRET" >> $GITHUB_STEP_SUMMARY - echo "- NODE_ENV" >> $GITHUB_STEP_SUMMARY - echo "- LOG_LEVEL" >> $GITHUB_STEP_SUMMARY - echo "- MINIMUM_PROFIT_SOL" >> $GITHUB_STEP_SUMMARY - echo "- MAX_SLIPPAGE" >> $GITHUB_STEP_SUMMARY - echo "- DEV_FEE_ENABLED" >> $GITHUB_STEP_SUMMARY - echo "- DEV_FEE_PERCENTAGE" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "🚂 Railway will automatically reload with the new environment variables." >> $GITHUB_STEP_SUMMARY - fi - - notify-on-failure: - name: Notify on Failure - runs-on: ubuntu-latest - needs: [validate-secrets, sync-secrets] - if: failure() - - steps: - - name: Create failure summary - run: | - echo "## ❌ Railway Secret Sync Failed" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Environment:** ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY - echo "**Dry Run:** ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ needs.validate-secrets.outputs.has-secrets }}" = "false" ]; then - echo "### Missing Secrets" >> $GITHUB_STEP_SUMMARY - echo "The following secrets are not configured:" >> $GITHUB_STEP_SUMMARY - echo "${{ needs.validate-secrets.outputs.missing-secrets }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Please configure all required secrets in repository settings." >> $GITHUB_STEP_SUMMARY - else - echo "### Sync Failed" >> $GITHUB_STEP_SUMMARY - echo "An error occurred while syncing secrets to Railway." >> $GITHUB_STEP_SUMMARY - echo "Please check the workflow logs for details." >> $GITHUB_STEP_SUMMARY - fi diff --git a/docs/branch-protection.md b/docs/branch-protection.md new file mode 100644 index 00000000..ca48c6f0 --- /dev/null +++ b/docs/branch-protection.md @@ -0,0 +1,34 @@ +# Branch Protection Recommendations (main) + +## Required checks +Set **only** these required checks: +- `ci / lint` +- `ci / typecheck` +- `ci / test` +- `ci / build` +- `security / audit` + +## Protection settings +- Protect `main` branch. +- Require pull request before merge. +- Require at least 1 approval. +- Dismiss stale approvals when new commits are pushed. +- Require conversation resolution before merge. +- Require branches to be up to date before merging. +- Require linear history. +- Restrict force pushes. +- Restrict branch deletion. + +## Merge policy +- Allow squash merge or rebase merge. +- Disable merge commits if linear history is enforced. +- Do not enable auto-merge for CI convergence branch policy. + +## Release gating +- Production deployment only from `main` and only after successful `ci` workflow. +- Preview deployment must remain non-blocking and must not be required for merge. +- Security audit check remains required. + +## Operational guidance +- Remove legacy required checks from deleted workflows before enabling new required checks. +- Revisit required checks only when workflow names or job names intentionally change. diff --git a/docs/ci-audit.md b/docs/ci-audit.md new file mode 100644 index 00000000..02e64c7b --- /dev/null +++ b/docs/ci-audit.md @@ -0,0 +1,87 @@ +# CI/CD Audit: Deterministic Convergence + +## Scope +Audited all workflow files in `.github/workflows/*` and converged to deterministic production workflows. + +## Final kept workflows +- `.github/workflows/ci.yml` +- `.github/workflows/security.yml` +- `.github/workflows/deploy.yml` +- `.github/workflows/self-heal.yml` + +## Removed workflow inventory +- `auto-label.yml` +- `autonomous-oracle-pipeline.yml` +- `codeql-analysis.yml` +- `complete-production-pipeline.yml` +- `deploy-preview.yml` +- `deploy-railway-preview.yml` +- `deploy-railway.yml` +- `docker-build.yml` +- `failed-job-handler.yml` +- `gxq-master-ci.yml` +- `gxq-pr-check.yml` +- `gxq-scheduled-health.yml` +- `performance-monitoring.yml` +- `powershell-merge-automation.yml` +- `rename-vercel-hosts.yml` +- `self-optimize.yml` +- `sync-railway-secrets.yml` + +## Merge and convergence decisions +- Merged all CI/test/build variants into `ci.yml` with exactly 4 deterministic jobs: `lint`, `typecheck`, `test`, `build`. +- Merged security checks into `security.yml`: `audit`, `dependency-review`, `secret-scan`. +- Merged deployment logic into `deploy.yml`: + - production deploy gated by successful `ci` workflow on `main` via `workflow_run` + - preview deploy retained as optional non-blocking PR job. +- Replaced self-mutating/self-optimizing automation with bounded `self-heal.yml` running deterministic `scripts/converge.sh` only. + +## Risk analysis +### Removed high-risk patterns +- Workflow self-mutation (`git commit`/`git push` in workflows). +- Recursive and unstable retry cascades. +- Duplicate deploy paths (Vercel + Railway + Docker pipelines racing each other). +- Comment/label automation coupled to deployment behavior. +- Multi-pipeline overlap producing non-deterministic branch status outcomes. + +### Current controlled risks +- Preview Vercel deployment can still fail due to external Vercel variability, but is explicitly non-blocking. +- Production Vercel deploy remains strict and only executes after successful CI on `main`. + +## Workflow dependency graph +- `ci` (PR + push main) + - jobs: `lint` -> independent + - jobs: `typecheck` -> independent + - jobs: `test` -> independent + - jobs: `build` -> independent +- `security` (PR + push main + scheduled) + - jobs: `audit`, `dependency-review`, `secret-scan` +- `deploy` + - `production` depends on `workflow_run(conclusion=success, head_branch=main, workflow=ci)` + - `preview` runs on PR and is non-blocking +- `self-heal` + - scheduled/manual bounded converge execution + - no workflow mutation, no commit, no PR merge, no recursive trigger chain + +## Determinism controls added +- Pinned Node version through `.nvmrc` in all workflows. +- `npm ci` used for lockfile-based install determinism. +- Single package manager path (npm) across root and webapp. +- Removed `continue-on-error` from required CI jobs. +- Removed auto-commit and workflow mutation behavior. +- Added bounded failure handling in `scripts/converge.sh`: + - failure -> classify -> known fix recipe -> rerun once -> stop. + +## Migration notes +1. Required checks should be updated to new check names from `ci` and `security` workflows. +2. Legacy required checks from removed workflows must be removed in branch protection. +3. If Railway deploys are still needed, keep them external to required convergence path and not coupled to CI required checks. +4. `scripts/converge.sh` and `scripts/doctor.ts` can be executed locally for deterministic preflight. + +## Rollback notes +1. Revert this PR commit to restore previous workflow set. +2. Reapply old branch protection required checks matching restored workflow names. +3. Re-run CI and deploy pipelines to re-establish prior status checks. + +## Justification summary +This convergence removes workflow entropy, recursion, and self-mutation while preserving core production controls: deterministic CI validation, security audit, gated production deploy from `main`, and bounded non-mutating self-heal routines. diff --git a/package.json b/package.json index 886573d2..1cfaee2d 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,9 @@ "env-sync": "bash scripts/env-sync-check.sh", "db-migrate": "bash scripts/db-migrate.sh", "health": "bash scripts/health-check.sh", - "perf": "bash scripts/performance-report.sh" + "perf": "bash scripts/performance-report.sh", + "converge": "bash scripts/converge.sh", + "doctor": "ts-node-esm scripts/doctor.ts" }, "keywords": [ "solana", @@ -109,5 +111,6 @@ "ts-node": "^10.9.1", "ts-prune": "^0.10.3", "typescript": "^5.3.2" - } + }, + "packageManager": "npm@10" } diff --git a/scripts/converge.sh b/scripts/converge.sh new file mode 100755 index 00000000..810dbcd6 --- /dev/null +++ b/scripts/converge.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +log() { + printf '[converge] %s\n' "$1" +} + +warn() { + printf '[converge][warn] %s\n' "$1" +} + +die() { + printf '[converge][error] %s\n' "$1" >&2 + exit 1 +} + +CLASSIFICATION="none" + +classify_and_fix_once() { + local stage="$1" + local command="$2" + + log "stage=${stage} run=${command}" + if eval "$command"; then + return 0 + fi + + case "$stage" in + install|webapp-install) + CLASSIFICATION="dependency_drift" + warn "classification=${CLASSIFICATION}; applying lockfile + dedupe recipe" + npm install --package-lock-only --ignore-scripts --no-audit --no-fund + npm --prefix webapp install --package-lock-only --ignore-scripts --no-audit --no-fund + npm dedupe || true + npm --prefix webapp dedupe || true + ;; + lint|typecheck|test|build) + CLASSIFICATION="stale_install" + warn "classification=${CLASSIFICATION}; reinstalling dependencies" + npm ci --no-audit --no-fund + npm --prefix webapp ci --no-audit --no-fund + ;; + *) + CLASSIFICATION="unknown" + warn "classification=${CLASSIFICATION}; no fix recipe" + ;; + esac + + log "stage=${stage} rerun=once" + if eval "$command"; then + return 0 + fi + + die "stage=${stage} failed after one bounded retry; classification=${CLASSIFICATION}" +} + +verify_structure() { + log "verify required folder structure" + local missing=0 + local required=(src webapp scripts .github/workflows) + for item in "${required[@]}"; do + if [ ! -e "$item" ]; then + warn "missing: $item" + missing=1 + fi + done + [ "$missing" -eq 0 ] || die "required structure missing" +} + +normalize_lockfiles() { + log "normalize lockfiles" + npm install --package-lock-only --ignore-scripts --no-audit --no-fund + npm --prefix webapp install --package-lock-only --ignore-scripts --no-audit --no-fund +} + +restore_generated_configs() { + log "restore deterministic generated configs" + if [ ! -f "webapp/next-env.d.ts" ]; then + cat > webapp/next-env.d.ts <<'EOF' +/// +/// + +// NOTE: This file should not be edited. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. +EOF + fi +} + +validate_env_templates() { + log "validate environment templates" + [ -f .env.example ] || die ".env.example missing" + [ -f webapp/.env.example ] || die "webapp/.env.example missing" + + grep -q '^SOLANA_RPC_URL=' .env.example || die "SOLANA_RPC_URL missing in .env.example" + grep -q '^WALLET_PRIVATE_KEY=' .env.example || die "WALLET_PRIVATE_KEY missing in .env.example" + grep -q '^NEXT_PUBLIC_RPC_URL=' webapp/.env.example || die "NEXT_PUBLIC_RPC_URL missing in webapp/.env.example" +} + +validate_json_file() { + local file_path="$1" + node -e "JSON.parse(require('fs').readFileSync('${file_path}', 'utf8'));" +} + +validate_nextjs_config() { + log "validate next config" + [ -f webapp/next.config.ts ] || die "webapp/next.config.ts missing" + grep -q 'export default' webapp/next.config.ts || die "webapp/next.config.ts missing default export" +} + +validate_firebase_config() { + log "validate firebase config" + local count + count=$(find . -maxdepth 2 -type f \( -name 'firebase.json' -o -name 'firebase*.json' \) | wc -l | tr -d ' ') + if [ "$count" -gt 0 ]; then + warn "firebase config files detected; ensure environment-specific validation is handled upstream" + fi +} + +validate_vercel_config() { + log "validate vercel config" + [ -f vercel.json ] || die "vercel.json missing" + [ -f webapp/vercel.json ] || die "webapp/vercel.json missing" + validate_json_file vercel.json + validate_json_file webapp/vercel.json +} + +regenerate_deterministic_artifacts() { + log "regenerate deterministic artifacts" + rm -rf dist webapp/.next +} + +install_dependencies() { + classify_and_fix_once "install" "npm ci --no-audit --no-fund" + classify_and_fix_once "webapp-install" "npm --prefix webapp ci --no-audit --no-fund" +} + +run_quality_pipeline() { + classify_and_fix_once "lint" "npm run lint && npm run lint:webapp" + classify_and_fix_once "typecheck" "npm run type-check && npm run type-check:webapp" + classify_and_fix_once "test" "npm test && npm run test:webapp" + classify_and_fix_once "build" "npm run build" +} + +main() { + log "start deterministic convergence" + verify_structure + normalize_lockfiles + restore_generated_configs + validate_env_templates + regenerate_deterministic_artifacts + install_dependencies + validate_nextjs_config + validate_firebase_config + validate_vercel_config + run_quality_pipeline + log "convergence completed successfully" +} + +main "$@" diff --git a/scripts/doctor.ts b/scripts/doctor.ts new file mode 100644 index 00000000..8696b0b3 --- /dev/null +++ b/scripts/doctor.ts @@ -0,0 +1,383 @@ +#!/usr/bin/env ts-node-esm + +import fs from 'node:fs'; +import path from 'node:path'; + +interface Finding { + check: string; + severity: 'error' | 'warning' | 'info'; + message: string; + fix: string; +} + +const findings: Finding[] = []; + +function addFinding( + check: string, + severity: 'error' | 'warning' | 'info', + message: string, + fix: string +): void { + findings.push({ check, severity, message, fix }); +} + +function readJson(filePath: string): unknown { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function scanFiles(root: string, extensions: Set): string[] { + const output: string[] = []; + const stack: string[] = [root]; + + while (stack.length > 0) { + const current = stack.pop(); + if (!current) { + continue; + } + + if (!fs.existsSync(current)) { + continue; + } + + const stats = fs.statSync(current); + if (stats.isDirectory()) { + const base = path.basename(current); + if (base === 'node_modules' || base === '.git' || base === 'dist' || base === '.next') { + continue; + } + + for (const entry of fs.readdirSync(current)) { + stack.push(path.join(current, entry)); + } + continue; + } + + const ext = path.extname(current); + if (extensions.has(ext)) { + output.push(current); + } + } + + return output; +} + +function checkEnvTemplates(repoRoot: string): void { + const files = [ + path.join(repoRoot, '.env.example'), + path.join(repoRoot, 'webapp', '.env.example'), + ]; + + const requiredByFile: Record = { + [files[0]]: ['SOLANA_RPC_URL', 'WALLET_PRIVATE_KEY', 'JWT_SECRET'], + [files[1]]: ['NEXT_PUBLIC_RPC_URL', 'NEXT_PUBLIC_BACKEND_URL'], + }; + + for (const filePath of files) { + if (!fs.existsSync(filePath)) { + addFinding('env-template', 'error', `Missing template: ${path.relative(repoRoot, filePath)}`, 'Restore the missing .env.example file.'); + continue; + } + + const content = fs.readFileSync(filePath, 'utf8'); + for (const variable of requiredByFile[filePath]) { + const pattern = new RegExp(`^${variable}=`, 'm'); + if (!pattern.test(content)) { + addFinding('env-template', 'error', `Missing ${variable} in ${path.relative(repoRoot, filePath)}`, `Add ${variable}=... to ${path.relative(repoRoot, filePath)}.`); + } + } + } +} + +function resolveRelativeImport(fromFile: string, specifier: string): string | null { + const base = path.resolve(path.dirname(fromFile), specifier); + const candidates = [ + base, + `${base}.ts`, + `${base}.tsx`, + `${base}.mts`, + `${base}.js`, + `${base}.mjs`, + path.join(base, 'index.ts'), + path.join(base, 'index.tsx'), + path.join(base, 'index.js'), + ]; + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + + return null; +} + +function parseRelativeImports(filePath: string): string[] { + const content = fs.readFileSync(filePath, 'utf8'); + const matches = content.matchAll(/from\s+['"](\.{1,2}\/[^'"]+)['"]/g); + const imports: string[] = []; + + for (const match of matches) { + const specifier = match[1]; + if (specifier) { + imports.push(specifier); + } + } + + return imports; +} + +function checkInvalidImportsAndCycles(repoRoot: string): void { + const codeFiles = scanFiles(repoRoot, new Set(['.ts', '.tsx', '.mts'])); + const graph = new Map(); + + for (const filePath of codeFiles) { + const rel = path.relative(repoRoot, filePath); + const imports = parseRelativeImports(filePath); + const resolved: string[] = []; + + for (const imp of imports) { + const resolvedPath = resolveRelativeImport(filePath, imp); + if (!resolvedPath) { + addFinding('invalid-import', 'error', `Unresolved import in ${rel}: ${imp}`, 'Fix the import path or create the missing module.'); + continue; + } + resolved.push(path.relative(repoRoot, resolvedPath)); + } + + graph.set(rel, resolved); + } + + const visited = new Set(); + const inStack = new Set(); + + function dfs(node: string): void { + visited.add(node); + inStack.add(node); + + const deps = graph.get(node) ?? []; + for (const dep of deps) { + if (!graph.has(dep)) { + continue; + } + if (!visited.has(dep)) { + dfs(dep); + } else if (inStack.has(dep)) { + addFinding('circular-dependency', 'warning', `Circular dependency detected: ${node} -> ${dep}`, 'Refactor shared code into a lower-level module to break the cycle.'); + } + } + + inStack.delete(node); + } + + for (const node of graph.keys()) { + if (!visited.has(node)) { + dfs(node); + } + } +} + +function checkNextConfig(repoRoot: string): void { + const nextConfigPath = path.join(repoRoot, 'webapp', 'next.config.ts'); + if (!fs.existsSync(nextConfigPath)) { + addFinding('next-config', 'error', 'webapp/next.config.ts is missing', 'Restore Next.js config file.'); + return; + } + + const content = fs.readFileSync(nextConfigPath, 'utf8'); + if (!content.includes('export default')) { + addFinding('next-config', 'error', 'webapp/next.config.ts does not export default config', 'Export a default NextConfig object.'); + } +} + +function checkFirebaseConfig(repoRoot: string): void { + const firebaseFiles = scanFiles(repoRoot, new Set(['.json'])).filter((filePath) => path.basename(filePath).startsWith('firebase')); + if (firebaseFiles.length === 0) { + addFinding('firebase-config', 'info', 'No Firebase config files found', 'No action required unless Firebase is used in this deployment.'); + return; + } + + for (const filePath of firebaseFiles) { + try { + readJson(filePath); + } catch (_error) { + addFinding('firebase-config', 'error', `Invalid JSON in ${path.relative(repoRoot, filePath)}`, 'Fix malformed Firebase JSON.'); + } + } +} + +function checkVercelConfig(repoRoot: string): void { + const configs = [path.join(repoRoot, 'vercel.json'), path.join(repoRoot, 'webapp', 'vercel.json')]; + + for (const filePath of configs) { + if (!fs.existsSync(filePath)) { + addFinding('vercel-config', 'error', `Missing ${path.relative(repoRoot, filePath)}`, 'Restore Vercel config.'); + continue; + } + + try { + const parsed = readJson(filePath) as { framework?: string; buildCommand?: string }; + if (!parsed.framework) { + addFinding('vercel-config', 'warning', `${path.relative(repoRoot, filePath)} missing framework`, 'Set framework (for example: nextjs).'); + } + if (!parsed.buildCommand) { + addFinding('vercel-config', 'warning', `${path.relative(repoRoot, filePath)} missing buildCommand`, 'Set an explicit build command.'); + } + } catch (_error) { + addFinding('vercel-config', 'error', `Invalid JSON in ${path.relative(repoRoot, filePath)}`, 'Fix malformed Vercel JSON.'); + } + } +} + +function checkPackageDuplicationAndDrift(repoRoot: string): void { + const rootPkgPath = path.join(repoRoot, 'package.json'); + const webPkgPath = path.join(repoRoot, 'webapp', 'package.json'); + + if (!fs.existsSync(rootPkgPath) || !fs.existsSync(webPkgPath)) { + addFinding('package-drift', 'error', 'Missing package.json in root or webapp', 'Restore package manifests.'); + return; + } + + const rootPkg = readJson(rootPkgPath) as { dependencies?: Record; engines?: Record }; + const webPkg = readJson(webPkgPath) as { dependencies?: Record; engines?: Record }; + const rootDeps = rootPkg.dependencies ?? {}; + const webDeps = webPkg.dependencies ?? {}; + + for (const dep of Object.keys(rootDeps)) { + if (webDeps[dep] && webDeps[dep] !== rootDeps[dep]) { + addFinding('package-drift', 'warning', `Version drift for ${dep}: root=${rootDeps[dep]} webapp=${webDeps[dep]}`, 'Align shared dependency versions where safe.'); + } + } + + const rootNode = rootPkg.engines?.node; + const webNode = webPkg.engines?.node; + if (rootNode && webNode && rootNode !== webNode) { + addFinding('node-version', 'error', `Node engine mismatch: root=${rootNode} webapp=${webNode}`, 'Use the same Node engine range across manifests.'); + } +} + +function checkOrphanGeneratedArtifacts(repoRoot: string): void { + const distPath = path.join(repoRoot, 'dist'); + const nextPath = path.join(repoRoot, 'webapp', '.next'); + + if (fs.existsSync(distPath)) { + addFinding('orphan-artifacts', 'warning', 'dist/ exists in repository workspace', 'Ensure generated artifacts are not committed and are reproducible in CI.'); + } + + if (fs.existsSync(nextPath)) { + addFinding('orphan-artifacts', 'warning', 'webapp/.next exists in repository workspace', 'Ensure Next build output is not committed.'); + } +} + +function checkWorkflowGraph(repoRoot: string): void { + const workflowDir = path.join(repoRoot, '.github', 'workflows'); + const allowed = new Set(['ci.yml', 'security.yml', 'deploy.yml', 'self-heal.yml']); + + if (!fs.existsSync(workflowDir)) { + addFinding('workflow-graph', 'error', '.github/workflows missing', 'Restore workflows directory.'); + return; + } + + const files = fs.readdirSync(workflowDir).filter((fileName) => fileName.endsWith('.yml') || fileName.endsWith('.yaml')); + + for (const fileName of files) { + if (!allowed.has(fileName)) { + addFinding('workflow-graph', 'error', `Unexpected workflow file: ${fileName}`, 'Remove non-deterministic or obsolete workflow definitions.'); + } + + const fullPath = path.join(workflowDir, fileName); + const content = fs.readFileSync(fullPath, 'utf8'); + + if (/git\s+push/.test(content) || /create-pull-request/.test(content)) { + addFinding('workflow-graph', 'error', `${fileName} contains repository mutation behavior`, 'Remove auto-commit/auto-PR logic from workflows.'); + } + + if (/workflow_run:\s*[\s\S]*workflows:\s*\[[^\]]*self-heal[^\]]*\]/m.test(content)) { + addFinding('workflow-graph', 'error', `${fileName} can recursively trigger self-heal`, 'Break recursive workflow_run chains.'); + } + } +} + +function checkTypeScriptPaths(repoRoot: string): void { + const configs = [path.join(repoRoot, 'tsconfig.json'), path.join(repoRoot, 'webapp', 'tsconfig.json')]; + + for (const filePath of configs) { + if (!fs.existsSync(filePath)) { + addFinding('typescript-paths', 'error', `Missing ${path.relative(repoRoot, filePath)}`, 'Restore tsconfig file.'); + continue; + } + + const parsed = readJson(filePath) as { compilerOptions?: { paths?: Record } }; + const pathsMap = parsed.compilerOptions?.paths ?? {}; + + for (const alias of Object.keys(pathsMap)) { + for (const target of pathsMap[alias]) { + const cleaned = target.replace(/\*.*$/, ''); + const resolved = path.resolve(path.dirname(filePath), cleaned); + if (!fs.existsSync(resolved)) { + addFinding('typescript-paths', 'error', `${path.relative(repoRoot, filePath)} has invalid path mapping ${alias} -> ${target}`, 'Fix or remove invalid TS path aliases.'); + } + } + } + } +} + +function checkSupportedNodeVersion(repoRoot: string): void { + const nvmrcPath = path.join(repoRoot, '.nvmrc'); + if (!fs.existsSync(nvmrcPath)) { + addFinding('node-version', 'error', '.nvmrc missing', 'Add .nvmrc and pin supported Node version.'); + return; + } + + const nvmrc = fs.readFileSync(nvmrcPath, 'utf8').trim(); + if (!/^\d+$/.test(nvmrc)) { + addFinding('node-version', 'error', `.nvmrc is invalid: ${nvmrc}`, 'Set .nvmrc to a major Node version (for example 24).'); + } + + const rootPkg = readJson(path.join(repoRoot, 'package.json')) as { engines?: Record }; + const nodeEngine = rootPkg.engines?.node; + if (nodeEngine && !nodeEngine.includes(nvmrc)) { + addFinding('node-version', 'warning', `Node engine (${nodeEngine}) does not explicitly include .nvmrc (${nvmrc})`, 'Align engines.node with .nvmrc for deterministic toolchain selection.'); + } +} + +function printResults(): void { + const errors = findings.filter((item) => item.severity === 'error'); + const warnings = findings.filter((item) => item.severity === 'warning'); + const info = findings.filter((item) => item.severity === 'info'); + + console.log('Platform Doctor Diagnostics\n'); + + for (const finding of findings) { + const icon = finding.severity === 'error' ? '❌' : finding.severity === 'warning' ? '⚠️' : 'ℹ️'; + console.log(`${icon} [${finding.check}] ${finding.message}`); + console.log(` Fix: ${finding.fix}`); + } + + console.log('\nSummary'); + console.log(`- Errors: ${errors.length}`); + console.log(`- Warnings: ${warnings.length}`); + console.log(`- Info: ${info.length}`); + + if (errors.length > 0) { + process.exit(1); + } +} + +function main(): void { + const repoRoot = path.resolve(path.join(path.dirname(new URL(import.meta.url).pathname), '..')); + + checkEnvTemplates(repoRoot); + checkInvalidImportsAndCycles(repoRoot); + checkNextConfig(repoRoot); + checkFirebaseConfig(repoRoot); + checkVercelConfig(repoRoot); + checkPackageDuplicationAndDrift(repoRoot); + checkOrphanGeneratedArtifacts(repoRoot); + checkWorkflowGraph(repoRoot); + checkTypeScriptPaths(repoRoot); + checkSupportedNodeVersion(repoRoot); + + printResults(); +} + +main(); From 183bfd398f0110744065087450538379a8728603 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 01:07:07 +0000 Subject: [PATCH 02/13] fix: harden converge execution and deploy workflow trust boundaries Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/3c2458ad-668b-49fc-9f9e-5bf4a0fdb2f1 Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- .github/workflows/ci.yml | 16 ++++---- .github/workflows/deploy.yml | 29 +++++++++++--- .github/workflows/security.yml | 8 ++-- .github/workflows/self-heal.yml | 4 +- scripts/converge.sh | 70 +++++++++++++++++++++++++++------ scripts/doctor.ts | 33 ++++++++++------ 6 files changed, 116 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed2c7c7f..f17a2fd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm @@ -38,8 +38,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm @@ -56,8 +56,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm @@ -74,8 +74,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d3e2a4e4..6d6ed2c9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -22,28 +22,45 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 + - name: Check preview deploy secrets + id: preview_secrets + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + run: | + if [ -n "$VERCEL_TOKEN" ] && [ -n "$VERCEL_ORG_ID" ] && [ -n "$VERCEL_PROJECT_ID" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + fi - uses: amondnet/vercel-action@v42 - if: ${{ secrets.VERCEL_TOKEN != '' && secrets.VERCEL_ORG_ID != '' && secrets.VERCEL_PROJECT_ID != '' }} + if: steps.preview_secrets.outputs.enabled == 'true' with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} working-directory: ./webapp + - name: Preview deploy skipped (missing secrets) + if: steps.preview_secrets.outputs.enabled != 'true' + run: echo "Skipping preview deploy because required Vercel secrets are not configured." production: name: production if: >- github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.head_branch == 'main' + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 with: - ref: ${{ github.event.workflow_run.head_sha }} - - uses: actions/setup-node@v6 + ref: refs/heads/main + - uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 2332268a..ca321e20 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -19,8 +19,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm @@ -41,7 +41,7 @@ jobs: contents: read pull-requests: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - uses: actions/dependency-review-action@v5 secret-scan: @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/self-heal.yml b/.github/workflows/self-heal.yml index 2cc46ce0..4a5d66ed 100644 --- a/.github/workflows/self-heal.yml +++ b/.github/workflows/self-heal.yml @@ -18,8 +18,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm diff --git a/scripts/converge.sh b/scripts/converge.sh index 810dbcd6..59aca46b 100755 --- a/scripts/converge.sh +++ b/scripts/converge.sh @@ -3,6 +3,8 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" +CI_MODE=false +SELF_HEAL_MODE=false log() { printf '[converge] %s\n' "$1" @@ -19,12 +21,42 @@ die() { CLASSIFICATION="none" +run_stage_command() { + local stage="$1" + + case "$stage" in + install) + npm ci --no-audit --no-fund + ;; + webapp-install) + npm --prefix webapp ci --no-audit --no-fund + ;; + lint) + npm run lint + npm run lint:webapp + ;; + typecheck) + npm run type-check + npm run type-check:webapp + ;; + test) + npm test + npm run test:webapp + ;; + build) + npm run build + ;; + *) + die "unknown stage command: ${stage}" + ;; + esac +} + classify_and_fix_once() { local stage="$1" - local command="$2" - log "stage=${stage} run=${command}" - if eval "$command"; then + log "stage=${stage} run" + if run_stage_command "$stage"; then return 0 fi @@ -50,7 +82,7 @@ classify_and_fix_once() { esac log "stage=${stage} rerun=once" - if eval "$command"; then + if run_stage_command "$stage"; then return 0 fi @@ -101,7 +133,7 @@ validate_env_templates() { validate_json_file() { local file_path="$1" - node -e "JSON.parse(require('fs').readFileSync('${file_path}', 'utf8'));" + node -e "JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));" "$file_path" } validate_nextjs_config() { @@ -133,19 +165,35 @@ regenerate_deterministic_artifacts() { } install_dependencies() { - classify_and_fix_once "install" "npm ci --no-audit --no-fund" - classify_and_fix_once "webapp-install" "npm --prefix webapp ci --no-audit --no-fund" + classify_and_fix_once "install" + classify_and_fix_once "webapp-install" } run_quality_pipeline() { - classify_and_fix_once "lint" "npm run lint && npm run lint:webapp" - classify_and_fix_once "typecheck" "npm run type-check && npm run type-check:webapp" - classify_and_fix_once "test" "npm test && npm run test:webapp" - classify_and_fix_once "build" "npm run build" + classify_and_fix_once "lint" + classify_and_fix_once "typecheck" + classify_and_fix_once "test" + classify_and_fix_once "build" } main() { + while [ "$#" -gt 0 ]; do + case "$1" in + --ci) + CI_MODE=true + ;; + --self-heal) + SELF_HEAL_MODE=true + ;; + *) + die "unknown argument: $1" + ;; + esac + shift + done + log "start deterministic convergence" + log "mode ci=${CI_MODE} self_heal=${SELF_HEAL_MODE}" verify_structure normalize_lockfiles restore_generated_configs diff --git a/scripts/doctor.ts b/scripts/doctor.ts index 8696b0b3..e0a7e4f2 100644 --- a/scripts/doctor.ts +++ b/scripts/doctor.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; interface Finding { check: string; @@ -62,24 +63,28 @@ function scanFiles(root: string, extensions: Set): string[] { } function checkEnvTemplates(repoRoot: string): void { - const files = [ - path.join(repoRoot, '.env.example'), - path.join(repoRoot, 'webapp', '.env.example'), + const rootEnvPath = path.join(repoRoot, '.env.example'); + const webappEnvPath = path.join(repoRoot, 'webapp', '.env.example'); + const requirements: Array<{ filePath: string; required: string[] }> = [ + { + filePath: rootEnvPath, + required: ['SOLANA_RPC_URL', 'WALLET_PRIVATE_KEY', 'JWT_SECRET'], + }, + { + filePath: webappEnvPath, + required: ['NEXT_PUBLIC_RPC_URL', 'NEXT_PUBLIC_BACKEND_URL'], + }, ]; - const requiredByFile: Record = { - [files[0]]: ['SOLANA_RPC_URL', 'WALLET_PRIVATE_KEY', 'JWT_SECRET'], - [files[1]]: ['NEXT_PUBLIC_RPC_URL', 'NEXT_PUBLIC_BACKEND_URL'], - }; - - for (const filePath of files) { + for (const requirement of requirements) { + const filePath = requirement.filePath; if (!fs.existsSync(filePath)) { addFinding('env-template', 'error', `Missing template: ${path.relative(repoRoot, filePath)}`, 'Restore the missing .env.example file.'); continue; } const content = fs.readFileSync(filePath, 'utf8'); - for (const variable of requiredByFile[filePath]) { + for (const variable of requirement.required) { const pattern = new RegExp(`^${variable}=`, 'm'); if (!pattern.test(content)) { addFinding('env-template', 'error', `Missing ${variable} in ${path.relative(repoRoot, filePath)}`, `Add ${variable}=... to ${path.relative(repoRoot, filePath)}.`); @@ -329,13 +334,15 @@ function checkSupportedNodeVersion(repoRoot: string): void { } const nvmrc = fs.readFileSync(nvmrcPath, 'utf8').trim(); - if (!/^\d+$/.test(nvmrc)) { + if (!/^v?\d+(\.\d+(\.\d+)?)?$/.test(nvmrc)) { addFinding('node-version', 'error', `.nvmrc is invalid: ${nvmrc}`, 'Set .nvmrc to a major Node version (for example 24).'); } + const nvmrcMajorMatch = nvmrc.match(/\d+/); + const nvmrcMajor = nvmrcMajorMatch ? nvmrcMajorMatch[0] : ''; const rootPkg = readJson(path.join(repoRoot, 'package.json')) as { engines?: Record }; const nodeEngine = rootPkg.engines?.node; - if (nodeEngine && !nodeEngine.includes(nvmrc)) { + if (nodeEngine && nvmrcMajor && !nodeEngine.includes(nvmrcMajor)) { addFinding('node-version', 'warning', `Node engine (${nodeEngine}) does not explicitly include .nvmrc (${nvmrc})`, 'Align engines.node with .nvmrc for deterministic toolchain selection.'); } } @@ -364,7 +371,7 @@ function printResults(): void { } function main(): void { - const repoRoot = path.resolve(path.join(path.dirname(new URL(import.meta.url).pathname), '..')); + const repoRoot = path.resolve(path.join(path.dirname(fileURLToPath(import.meta.url)), '..')); checkEnvTemplates(repoRoot); checkInvalidImportsAndCycles(repoRoot); From 93c8861e6f268c9666ac4fb6f6dab99456cffcd9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 01:37:40 +0000 Subject: [PATCH 03/13] refactor: decouple legacy vercel domains and make deploy optional Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/c8c88e83-7824-4ac2-8ca0-78170625a1d0 Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- .env.example | 10 ++- .github/workflows/deploy.yml | 38 ++++++++--- api/middleware/cors.ts | 8 +-- config/deployment.ts | 55 ++++++++++++++++ docs/CANARY_DEPLOYMENT.md | 8 +-- docs/DEPLOYMENT_AUTOMATION.md | 2 +- docs/DEPLOYMENT_GUIDE.md | 8 +-- docs/ENDPOINT_CONFIGURATION.md | 4 +- docs/IMPLEMENTATION_SUMMARY_OLD.md | 4 +- docs/archive/COMPLETE_IMPLEMENTATION.md | 2 +- docs/archive/VERCEL_DEPLOYMENT.md | 2 +- docs/ci-audit.md | 6 +- docs/stuck.md | 4 +- rename-vercel-hosts.yml | 88 ------------------------- scripts/canary-deployment.ts | 6 +- tsconfig.json | 2 +- webapp/.env.example | 4 +- 17 files changed, 120 insertions(+), 131 deletions(-) create mode 100644 config/deployment.ts delete mode 100644 rename-vercel-hosts.yml diff --git a/.env.example b/.env.example index fdbf482d..f66a1089 100644 --- a/.env.example +++ b/.env.example @@ -419,13 +419,17 @@ ANALYSIS_WINDOW=60 MIN_ERROR_THRESHOLD=5 # Canary deployment -STAGING_URL=https://gxq-staging.vercel.app -PRODUCTION_URL=https://gxq.vercel.app +DEPLOYMENT_PROVIDER=none +DEPLOY_PREVIEW_ENABLED=false +DEPLOYMENT_PRIMARY_DOMAIN=https://TradeOS.app +STAGING_URL=https://staging.tradeos.app +PRODUCTION_URL=https://TradeOS.app +ADDITIONAL_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001 # GitHub integration (for auto-ticketing) # GITHUB_TOKEN=your_github_token_here -# Vercel integration (for auto-deployment) +# Optional Vercel integration (only when DEPLOYMENT_PROVIDER=vercel) # VERCEL_TOKEN=your_vercel_token_here # VERCEL_ORG_ID=your_vercel_org_id_here # VERCEL_PROJECT_ID=your_vercel_project_id_here diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6d6ed2c9..e77197f7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,11 +26,12 @@ jobs: - name: Check preview deploy secrets id: preview_secrets env: + DEPLOYMENT_PROVIDER: ${{ secrets.DEPLOYMENT_PROVIDER }} VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} run: | - if [ -n "$VERCEL_TOKEN" ] && [ -n "$VERCEL_ORG_ID" ] && [ -n "$VERCEL_PROJECT_ID" ]; then + if [ "$DEPLOYMENT_PROVIDER" = "vercel" ] && [ -n "$VERCEL_TOKEN" ] && [ -n "$VERCEL_ORG_ID" ] && [ -n "$VERCEL_PROJECT_ID" ]; then echo "enabled=true" >> "$GITHUB_OUTPUT" else echo "enabled=false" >> "$GITHUB_OUTPUT" @@ -44,7 +45,7 @@ jobs: working-directory: ./webapp - name: Preview deploy skipped (missing secrets) if: steps.preview_secrets.outputs.enabled != 'true' - run: echo "Skipping preview deploy because required Vercel secrets are not configured." + run: echo "Skipping preview deploy because deployment provider is not configured for optional preview deployment." production: name: production @@ -55,30 +56,47 @@ jobs: github.event.workflow_run.event == 'push' && github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-latest + continue-on-error: true timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: ref: refs/heads/main + - name: Check production deploy secrets + id: production_secrets + env: + DEPLOYMENT_PROVIDER: ${{ secrets.DEPLOYMENT_PROVIDER }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + run: | + if [ "$DEPLOYMENT_PROVIDER" = "vercel" ] && [ -n "$VERCEL_TOKEN" ] && [ -n "$VERCEL_ORG_ID" ] && [ -n "$VERCEL_PROJECT_ID" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + fi - uses: actions/setup-node@v4 + if: steps.production_secrets.outputs.enabled == 'true' with: node-version-file: .nvmrc cache: npm cache-dependency-path: | package-lock.json webapp/package-lock.json - - run: npm ci --no-audit --no-fund - - run: npm --prefix webapp ci --no-audit --no-fund - - run: npm run build - - name: Validate deploy secrets - run: | - test -n "${{ secrets.VERCEL_TOKEN }}" - test -n "${{ secrets.VERCEL_ORG_ID }}" - test -n "${{ secrets.VERCEL_PROJECT_ID }}" + - if: steps.production_secrets.outputs.enabled == 'true' + run: npm ci --no-audit --no-fund + - if: steps.production_secrets.outputs.enabled == 'true' + run: npm --prefix webapp ci --no-audit --no-fund + - if: steps.production_secrets.outputs.enabled == 'true' + run: npm run build - uses: amondnet/vercel-action@v42 + if: steps.production_secrets.outputs.enabled == 'true' with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} vercel-args: --prod working-directory: ./webapp + - name: Production deploy skipped (provider unavailable) + if: steps.production_secrets.outputs.enabled != 'true' + run: echo "Skipping production deploy because deployment provider infrastructure is not configured." diff --git a/api/middleware/cors.ts b/api/middleware/cors.ts index 79c95762..ffab89b2 100644 --- a/api/middleware/cors.ts +++ b/api/middleware/cors.ts @@ -4,6 +4,7 @@ */ import type { VercelRequest, VercelResponse } from "@vercel/node"; +import { getAllowedOrigins } from "../../config/deployment.js"; export interface CorsOptions { origin?: string | string[] | ((origin: string) => boolean); @@ -137,12 +138,7 @@ export function withCors( */ export const productionCorsOptions: CorsOptions = { origin: (origin: string) => { - const allowedDomains = [ - "https://reimagined-jupiter.vercel.app", - "https://gxq-studio.vercel.app", - "http://localhost:3000", - "http://localhost:3001", - ]; + const allowedDomains = getAllowedOrigins(); // Allow requests with no origin (mobile apps, curl, etc.) if (!origin) return true; diff --git a/config/deployment.ts b/config/deployment.ts new file mode 100644 index 00000000..1c73952a --- /dev/null +++ b/config/deployment.ts @@ -0,0 +1,55 @@ +export type DeploymentProvider = 'none' | 'vercel' | 'railway' | 'custom'; + +export interface DeploymentConfig { + provider: DeploymentProvider; + productionUrl: string; + stagingUrl: string; + previewEnabled: boolean; +} + +const DEFAULT_PRODUCTION_URL = 'https://TradeOS.app'; +const DEFAULT_STAGING_URL = 'https://staging.tradeos.app'; + +function normalizeUrl(url: string): string { + return url.endsWith('/') ? url.slice(0, -1) : url; +} + +function normalizeProvider(value: string | undefined): DeploymentProvider { + const provider = (value ?? 'none').toLowerCase(); + if (provider === 'vercel' || provider === 'railway' || provider === 'custom') { + return provider; + } + return 'none'; +} + +export function getDeploymentConfig(): DeploymentConfig { + const productionUrl = normalizeUrl(process.env.PRODUCTION_URL ?? DEFAULT_PRODUCTION_URL); + const stagingUrl = normalizeUrl(process.env.STAGING_URL ?? DEFAULT_STAGING_URL); + const provider = normalizeProvider(process.env.DEPLOYMENT_PROVIDER); + const previewEnabled = process.env.DEPLOY_PREVIEW_ENABLED === 'true'; + + return { + provider, + productionUrl, + stagingUrl, + previewEnabled, + }; +} + +export function getAllowedOrigins(): string[] { + const config = getDeploymentConfig(); + const additionalOrigins = (process.env.ADDITIONAL_ALLOWED_ORIGINS ?? '') + .split(',') + .map((origin) => origin.trim()) + .filter(Boolean); + + return Array.from( + new Set([ + config.productionUrl, + config.stagingUrl, + 'http://localhost:3000', + 'http://localhost:3001', + ...additionalOrigins, + ]) + ); +} diff --git a/docs/CANARY_DEPLOYMENT.md b/docs/CANARY_DEPLOYMENT.md index 4e00efab..95bd076d 100644 --- a/docs/CANARY_DEPLOYMENT.md +++ b/docs/CANARY_DEPLOYMENT.md @@ -74,10 +74,10 @@ Set environment variables: ```bash # Staging URL -STAGING_URL=https://gxq-staging.vercel.app +STAGING_URL=https://staging.tradeos.app # Production URL -PRODUCTION_URL=https://gxq.vercel.app +PRODUCTION_URL=https://TradeOS.app # Optional: Vercel tokens for deployment VERCEL_TOKEN=your_vercel_token @@ -105,8 +105,8 @@ The script will: ```typescript { - stagingUrl: 'https://gxq-staging.vercel.app', - productionUrl: 'https://gxq.vercel.app', + stagingUrl: 'https://staging.tradeos.app', + productionUrl: 'https://TradeOS.app', canaryPercentage: 10, // Start with 10% traffic testDurationMinutes: 5, // Monitor for 5 minutes rollbackThresholdPercent: 5, // Rollback if error rate exceeds 5% diff --git a/docs/DEPLOYMENT_AUTOMATION.md b/docs/DEPLOYMENT_AUTOMATION.md index 3241a741..f07163e2 100644 --- a/docs/DEPLOYMENT_AUTOMATION.md +++ b/docs/DEPLOYMENT_AUTOMATION.md @@ -158,7 +158,7 @@ railway rollback ## Deployment Environments ### Production (Vercel) -- **URL:** `https://TradeOS.vercel.app` +- **URL:** `https://TradeOS.app` - **Branch:** `main` - **Auto-deploy:** Yes - **Rollback:** Automatic on failure diff --git a/docs/DEPLOYMENT_GUIDE.md b/docs/DEPLOYMENT_GUIDE.md index 89417ad5..206e77e1 100644 --- a/docs/DEPLOYMENT_GUIDE.md +++ b/docs/DEPLOYMENT_GUIDE.md @@ -54,8 +54,8 @@ JWT_SECRET=your_jwt_secret AUTO_FIX_ENABLED=false AUTO_REDEPLOY_ENABLED=false METRICS_PORT=9090 -STAGING_URL=https://gxq-staging.vercel.app -PRODUCTION_URL=https://gxq.vercel.app +STAGING_URL=https://staging.tradeos.app +PRODUCTION_URL=https://TradeOS.app ``` ### 3. Build and Test @@ -232,8 +232,8 @@ vercel link ```bash gh secret set VERCEL_TOKEN --body "YOUR_TOKEN" gh secret set VERCEL_PROJECT_ID --body "YOUR_PROJECT_ID" -gh secret set STAGING_URL --body "https://gxq-staging.vercel.app" -gh secret set PRODUCTION_URL --body "https://gxq.vercel.app" +gh secret set STAGING_URL --body "https://staging.tradeos.app" +gh secret set PRODUCTION_URL --body "https://TradeOS.app" ``` 3. **Run canary**: diff --git a/docs/ENDPOINT_CONFIGURATION.md b/docs/ENDPOINT_CONFIGURATION.md index 41da10aa..dc7585e8 100644 --- a/docs/ENDPOINT_CONFIGURATION.md +++ b/docs/ENDPOINT_CONFIGURATION.md @@ -329,8 +329,8 @@ NEYNAR_API_KEY=your_neynar_key export const productionCorsOptions: CorsOptions = { origin: (origin: string) => { const allowedDomains = [ - 'https://TradeOS.vercel.app', - 'https://gxq-studio.vercel.app', + 'https://TradeOS.app', + 'https://TradeOS.app', ]; return allowedDomains.some(domain => origin.startsWith(domain)); }, diff --git a/docs/IMPLEMENTATION_SUMMARY_OLD.md b/docs/IMPLEMENTATION_SUMMARY_OLD.md index e5faaf2f..6916692c 100644 --- a/docs/IMPLEMENTATION_SUMMARY_OLD.md +++ b/docs/IMPLEMENTATION_SUMMARY_OLD.md @@ -90,8 +90,8 @@ export default withErrorHandler(async (req, res) => { export const productionCorsOptions: CorsOptions = { origin: (origin: string) => { const allowedDomains = [ - 'https://TradeOS.vercel.app', - 'https://gxq-studio.vercel.app', + 'https://TradeOS.app', + 'https://TradeOS.app', 'http://localhost:3000', ]; return allowedDomains.some(domain => origin.startsWith(domain)); diff --git a/docs/archive/COMPLETE_IMPLEMENTATION.md b/docs/archive/COMPLETE_IMPLEMENTATION.md index 22536fca..e2031fd1 100644 --- a/docs/archive/COMPLETE_IMPLEMENTATION.md +++ b/docs/archive/COMPLETE_IMPLEMENTATION.md @@ -141,7 +141,7 @@ webapp/ - Add environment variable: `NEXT_PUBLIC_RPC_URL` - Click Deploy -3. **Done!** Your app is live at `https://gxq.vercel.app/` +3. **Done!** Your app is live at `https://TradeOS.app/` ### Deploy Backend for Automated Trading (Optional) diff --git a/docs/archive/VERCEL_DEPLOYMENT.md b/docs/archive/VERCEL_DEPLOYMENT.md index 9aa80681..31a2e340 100644 --- a/docs/archive/VERCEL_DEPLOYMENT.md +++ b/docs/archive/VERCEL_DEPLOYMENT.md @@ -173,7 +173,7 @@ docker run -d \ ## 🌐 URLs After Deployment -- **Frontend (Vercel)**: `https://gxq.vercel.app/` +- **Frontend (Vercel)**: `https://TradeOS.app/` - **Custom Domain**: `https://gxqstudio.com` (if configured) ## 📊 Post-Deployment Checklist diff --git a/docs/ci-audit.md b/docs/ci-audit.md index 02e64c7b..c2b30890 100644 --- a/docs/ci-audit.md +++ b/docs/ci-audit.md @@ -45,8 +45,8 @@ Audited all workflow files in `.github/workflows/*` and converged to determinist - Multi-pipeline overlap producing non-deterministic branch status outcomes. ### Current controlled risks -- Preview Vercel deployment can still fail due to external Vercel variability, but is explicitly non-blocking. -- Production Vercel deploy remains strict and only executes after successful CI on `main`. +- Preview deployment can still fail due to provider variability, but it is explicitly non-blocking. +- Production deployment is gated by successful CI on `main` and now gracefully skips when provider infrastructure is not configured. ## Workflow dependency graph - `ci` (PR + push main) @@ -58,7 +58,7 @@ Audited all workflow files in `.github/workflows/*` and converged to determinist - jobs: `audit`, `dependency-review`, `secret-scan` - `deploy` - `production` depends on `workflow_run(conclusion=success, head_branch=main, workflow=ci)` - - `preview` runs on PR and is non-blocking + - both `preview` and `production` are optional/non-blocking until deployment provider infrastructure is configured - `self-heal` - scheduled/manual bounded converge execution - no workflow mutation, no commit, no PR merge, no recursive trigger chain diff --git a/docs/stuck.md b/docs/stuck.md index 154be286..889e338d 100644 --- a/docs/stuck.md +++ b/docs/stuck.md @@ -915,7 +915,7 @@ webapp/ - Add environment variable: `NEXT_PUBLIC_RPC_URL` - Click Deploy -3. **Done!** Your app is live at `https://gxq.vercel.app/` +3. **Done!** Your app is live at `https://TradeOS.app/` ### Deploy Backend for Automated Trading (Optional) @@ -2218,7 +2218,7 @@ docker run -d \ ## 🌐 URLs After Deployment -- **Frontend (Vercel)**: `https://gxq.vercel.app/` +- **Frontend (Vercel)**: `https://TradeOS.app/` - **Custom Domain**: `https://gxqstudio.com` (if configured) ## 📊 Post-Deployment Checklist diff --git a/rename-vercel-hosts.yml b/rename-vercel-hosts.yml deleted file mode 100644 index 482af714..00000000 --- a/rename-vercel-hosts.yml +++ /dev/null @@ -1,88 +0,0 @@ -# Replace old Vercel preview hostnames with https://gxq.vercel.app (no trailing slash), -# push changes to branch `gxq`, and create/update a PR to `main`. -# Triggers: manual (workflow_dispatch) and push to main. -on: - workflow_dispatch: {} - push: - branches: - - main - -jobs: - replace-and-pr: - name: Replace Vercel hostnames and open PR - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: true - - - name: Setup Git identity - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Search and replace Vercel hostnames - id: replace - run: | - set -euo pipefail - - OLD1='https://jup-git-copilot-optimize-wallet-data-ui-enhancements-tradeos.vercel.app' - OLD2='https://reimagined-jupiter-git-copilot-optimize-wallet-d-edc02a-tradeos.vercel.app' - NEW='https://gxq.vercel.app' - - # Build list of likely text files to scan - FILES=$(git ls-files | grep -Ei '\.md$|\.markdown$|\.json$|\.toml$|\.yml$|\.yaml$|\.env|\.js$|\.ts$|\.html$|\.css$|manifest|README' || true) - if [ -z "$FILES" ]; then - FILES=$(git ls-files) - fi - - CHANGED=0 - for f in $FILES; do - [ -f "$f" ] || continue - case "$f" in - *.png|*.jpg|*.jpeg|*.gif|*.ico|*.svg|*.woff|*.woff2|*.ttf|*.eot) continue ;; - esac - - perl -0777 -pe "s#\Q${OLD1}\E/?#${NEW}#g; s#\Q${OLD2}\E/?#${NEW}#g" "$f" > "$f.tmp" || true - if ! cmp -s "$f" "$f.tmp"; then - mv "$f.tmp" "$f" - git add "$f" - CHANGED=1 - else - rm -f "$f.tmp" - fi - done - - echo "changed=$CHANGED" >> $GITHUB_OUTPUT - - - name: Commit changes if any - if: steps.replace.outputs.changed == '1' - run: | - git commit -m "chore: replace Vercel preview hostnames with https://gxq.vercel.app" || echo "no new commit" - - - name: Push to branch gxq - if: steps.replace.outputs.changed == '1' - env: - TARGET_BRANCH: gxq - run: | - git push origin HEAD:${TARGET_BRANCH} --force - - - name: Create or update PR to main - if: steps.replace.outputs.changed == '1' - uses: peter-evans/create-pull-request@v5 - with: - token: ${{ secrets.REPO_WRITE_TOKEN || github.token }} - commit-message: chore: replace Vercel preview hostnames with https://gxq.vercel.app - branch: gxq - base: main - title: Rename Vercel hostnames to gxq.vercel.app and update deployment metadata - body: | - This automated PR replaces old Vercel preview hostnames with https://gxq.vercel.app across the repository. - Only URL and deployment metadata updates are included — no UI/design changes. - labels: automated, infra - draft: false diff --git a/scripts/canary-deployment.ts b/scripts/canary-deployment.ts index e2a5f7af..f9d7d51c 100644 --- a/scripts/canary-deployment.ts +++ b/scripts/canary-deployment.ts @@ -3,6 +3,7 @@ */ import axios from 'axios'; +import { getDeploymentConfig } from '../config/deployment.js'; import { logger } from '../lib/logger.js'; interface CanaryConfig { @@ -41,9 +42,10 @@ export class CanaryDeployment { private config: CanaryConfig; constructor(config?: Partial) { + const deploymentConfig = getDeploymentConfig(); this.config = { - stagingUrl: process.env.STAGING_URL || 'https://gxq-staging.vercel.app', - productionUrl: process.env.PRODUCTION_URL || 'https://gxq.vercel.app', + stagingUrl: deploymentConfig.stagingUrl, + productionUrl: deploymentConfig.productionUrl, canaryPercentage: 10, testDurationMinutes: 5, rollbackThresholdPercent: 5, diff --git a/tsconfig.json b/tsconfig.json index 022d57e1..b516d1ce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,6 @@ "declarationMap": true, "sourceMap": true }, - "include": ["src/**/*", "api/**/*", "scripts/**/*", "db/**/*"], + "include": ["src/**/*", "api/**/*", "scripts/**/*", "db/**/*", "config/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"] } diff --git a/webapp/.env.example b/webapp/.env.example index 6dd5536d..11e4b32d 100644 --- a/webapp/.env.example +++ b/webapp/.env.example @@ -33,6 +33,9 @@ NEXT_PUBLIC_BACKEND_URL=http://localhost:3000 # WebSocket URL for real-time updates NEXT_PUBLIC_WS_URL=ws://localhost:3000 +# Public app URL/domain +NEXT_PUBLIC_APP_URL=http://localhost:3000 + # For Docker Compose, use service names: # NEXT_PUBLIC_BACKEND_URL=http://backend:3000 # NEXT_PUBLIC_WS_URL=ws://backend:3000 @@ -150,4 +153,3 @@ NEXT_PUBLIC_PRICE_UPDATE_INTERVAL=5000 NEXT_PUBLIC_ENABLE_LIVE_PRICES=true # ------------------------------------------------------------------------------ - From 79514083f299e14bd34b06f06654122c2b9e2faa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 01:39:08 +0000 Subject: [PATCH 04/13] docs: remove duplicated TradeOS domain examples Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/c8c88e83-7824-4ac2-8ca0-78170625a1d0 Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- docs/ENDPOINT_CONFIGURATION.md | 1 - docs/IMPLEMENTATION_SUMMARY_OLD.md | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/ENDPOINT_CONFIGURATION.md b/docs/ENDPOINT_CONFIGURATION.md index dc7585e8..9bec921d 100644 --- a/docs/ENDPOINT_CONFIGURATION.md +++ b/docs/ENDPOINT_CONFIGURATION.md @@ -330,7 +330,6 @@ export const productionCorsOptions: CorsOptions = { origin: (origin: string) => { const allowedDomains = [ 'https://TradeOS.app', - 'https://TradeOS.app', ]; return allowedDomains.some(domain => origin.startsWith(domain)); }, diff --git a/docs/IMPLEMENTATION_SUMMARY_OLD.md b/docs/IMPLEMENTATION_SUMMARY_OLD.md index 6916692c..9e770e71 100644 --- a/docs/IMPLEMENTATION_SUMMARY_OLD.md +++ b/docs/IMPLEMENTATION_SUMMARY_OLD.md @@ -90,7 +90,6 @@ export default withErrorHandler(async (req, res) => { export const productionCorsOptions: CorsOptions = { origin: (origin: string) => { const allowedDomains = [ - 'https://TradeOS.app', 'https://TradeOS.app', 'http://localhost:3000', ]; From 6d721b6749f6c31c2c77479902b4ff3ae167dc90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 03:08:06 +0000 Subject: [PATCH 05/13] fix: harden deterministic deploy gating and provider-independent CI behavior Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/e347e08a-3e2e-47a3-bb5d-dfcd0d18dbdf Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- .github/workflows/ci.yml | 8 +++++++- .github/workflows/deploy.yml | 3 +-- config/deployment.ts | 3 ++- docs/ci-audit.md | 7 +++++++ tsconfig.json | 2 +- vercel.json | 20 -------------------- 6 files changed, 18 insertions(+), 25 deletions(-) delete mode 100644 vercel.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f17a2fd6..fd407785 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,13 @@ jobs: - run: npm ci --no-audit --no-fund - run: npm --prefix webapp ci --no-audit --no-fund - run: npm test - - run: npm run test:webapp + - name: Run webapp tests when configured + run: | + if npm --prefix webapp run | grep -Eq '^[[:space:]]+test[[:space:]]'; then + npm --prefix webapp run test -- --ci --coverage + else + echo "No webapp test script configured; skipping webapp tests." + fi build: name: build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e77197f7..b8fd5538 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -56,12 +56,11 @@ jobs: github.event.workflow_run.event == 'push' && github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-latest - continue-on-error: true timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: - ref: refs/heads/main + ref: ${{ github.event.workflow_run.head_sha }} - name: Check production deploy secrets id: production_secrets env: diff --git a/config/deployment.ts b/config/deployment.ts index 1c73952a..09f705ad 100644 --- a/config/deployment.ts +++ b/config/deployment.ts @@ -23,7 +23,8 @@ function normalizeProvider(value: string | undefined): DeploymentProvider { } export function getDeploymentConfig(): DeploymentConfig { - const productionUrl = normalizeUrl(process.env.PRODUCTION_URL ?? DEFAULT_PRODUCTION_URL); + const primaryDomain = normalizeUrl(process.env.DEPLOYMENT_PRIMARY_DOMAIN ?? DEFAULT_PRODUCTION_URL); + const productionUrl = normalizeUrl(process.env.PRODUCTION_URL ?? primaryDomain); const stagingUrl = normalizeUrl(process.env.STAGING_URL ?? DEFAULT_STAGING_URL); const provider = normalizeProvider(process.env.DEPLOYMENT_PROVIDER); const previewEnabled = process.env.DEPLOY_PREVIEW_ENABLED === 'true'; diff --git a/docs/ci-audit.md b/docs/ci-audit.md index c2b30890..05aab926 100644 --- a/docs/ci-audit.md +++ b/docs/ci-audit.md @@ -72,6 +72,13 @@ Audited all workflow files in `.github/workflows/*` and converged to determinist - Added bounded failure handling in `scripts/converge.sh`: - failure -> classify -> known fix recipe -> rerun once -> stop. +## Provider-independence verification +- Local build path is independent from deployment credentials: + - `npm install` + - `npm run build` +- Deploy workflow is credential-gated and skips cleanly when provider is not configured (`DEPLOYMENT_PROVIDER != vercel` or missing Vercel secrets). +- Core required CI checks remain provider-agnostic: `lint`, `typecheck`, `test`, `build`. + ## Migration notes 1. Required checks should be updated to new check names from `ci` and `security` workflows. 2. Legacy required checks from removed workflows must be removed in branch protection. diff --git a/tsconfig.json b/tsconfig.json index b516d1ce..fb6e53e8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,6 @@ "declarationMap": true, "sourceMap": true }, - "include": ["src/**/*", "api/**/*", "scripts/**/*", "db/**/*", "config/**/*"], + "include": ["src/**/*", "api/**/*", "scripts/**/*", "db/**/*", "config/deployment.ts"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"] } diff --git a/vercel.json b/vercel.json deleted file mode 100644 index d5a3208f..00000000 --- a/vercel.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://openapi.vercel.sh/vercel.json", - "buildCommand": "cd webapp && npm run build", - "devCommand": "cd webapp && npm run dev", - "installCommand": "cd webapp && npm install", - "framework": "nextjs", - "outputDirectory": "webapp/.next", - "regions": [ - "iad1" - ], - "cleanUrls": true, - "trailingSlash": false, - "functions": { - "api/**/*.ts": { - "runtime": "nodejs22.x", - "maxDuration": 60, - "memory": 1024 - } - } -} From 2a600b766afefb6963c91cf07a73069a33fd11e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 03:10:22 +0000 Subject: [PATCH 06/13] fix: tighten CI test step and deterministic deploy checkout Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/e347e08a-3e2e-47a3-bb5d-dfcd0d18dbdf Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- .github/workflows/ci.yml | 8 +------- .github/workflows/deploy.yml | 3 ++- tsconfig.json | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd407785..293aaf54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,13 +67,7 @@ jobs: - run: npm ci --no-audit --no-fund - run: npm --prefix webapp ci --no-audit --no-fund - run: npm test - - name: Run webapp tests when configured - run: | - if npm --prefix webapp run | grep -Eq '^[[:space:]]+test[[:space:]]'; then - npm --prefix webapp run test -- --ci --coverage - else - echo "No webapp test script configured; skipping webapp tests." - fi + - run: npm --prefix webapp run test --if-present -- --ci --coverage build: name: build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b8fd5538..4a7c981c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -58,7 +58,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v4 + - name: Checkout exact commit that passed CI + uses: actions/checkout@v4 with: ref: ${{ github.event.workflow_run.head_sha }} - name: Check production deploy secrets diff --git a/tsconfig.json b/tsconfig.json index fb6e53e8..022d57e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,6 @@ "declarationMap": true, "sourceMap": true }, - "include": ["src/**/*", "api/**/*", "scripts/**/*", "db/**/*", "config/deployment.ts"], + "include": ["src/**/*", "api/**/*", "scripts/**/*", "db/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"] } From 71863e9cd50ff4790dedd744f2c521d5115591fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 03:11:17 +0000 Subject: [PATCH 07/13] fix: restore config typecheck coverage and harden deploy workflow determinism Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/e347e08a-3e2e-47a3-bb5d-dfcd0d18dbdf Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- .github/workflows/deploy.yml | 1 + config/rpc-endpoints.ts | 4 ++-- tsconfig.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4a7c981c..803f1f8a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -58,6 +58,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: + # Use the exact CI-passed SHA to avoid deploying a newer commit if main advances. - name: Checkout exact commit that passed CI uses: actions/checkout@v4 with: diff --git a/config/rpc-endpoints.ts b/config/rpc-endpoints.ts index bd3dd9cf..c70914d5 100644 --- a/config/rpc-endpoints.ts +++ b/config/rpc-endpoints.ts @@ -58,7 +58,7 @@ export const MAINNET_RPC_ENDPOINTS: RpcEndpoint[] = [ rateLimit: 10, tier: 'free', }, -].filter(endpoint => endpoint.url); // Remove endpoints without URLs +].filter((endpoint): endpoint is RpcEndpoint => Boolean(endpoint.url)); // Remove endpoints without URLs /** * Devnet RPC endpoints for testing @@ -78,7 +78,7 @@ export const DEVNET_RPC_ENDPOINTS: RpcEndpoint[] = [ rateLimit: 100, tier: 'premium', }, -].filter(endpoint => endpoint.url); +].filter((endpoint): endpoint is RpcEndpoint => Boolean(endpoint.url)); /** * Get RPC endpoints for the current environment diff --git a/tsconfig.json b/tsconfig.json index 022d57e1..b516d1ce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,6 @@ "declarationMap": true, "sourceMap": true }, - "include": ["src/**/*", "api/**/*", "scripts/**/*", "db/**/*"], + "include": ["src/**/*", "api/**/*", "scripts/**/*", "db/**/*", "config/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"] } From cafb982a1affd989ad2a9c110adb6966d9d6d5a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 12:34:48 +0000 Subject: [PATCH 08/13] fix: align converge/doctor with provider-agnostic vercel layout Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/931fde24-2e6e-4749-8119-e2f59de5f602 Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- .env.example | 4 ++-- config/deployment.ts | 17 +++++++++++++++-- scripts/converge.sh | 10 +++++++--- scripts/doctor.ts | 29 ++++++++++++++++++----------- 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/.env.example b/.env.example index f66a1089..7232d4b9 100644 --- a/.env.example +++ b/.env.example @@ -421,9 +421,9 @@ MIN_ERROR_THRESHOLD=5 # Canary deployment DEPLOYMENT_PROVIDER=none DEPLOY_PREVIEW_ENABLED=false -DEPLOYMENT_PRIMARY_DOMAIN=https://TradeOS.app +DEPLOYMENT_PRIMARY_DOMAIN=https://tradeos.app STAGING_URL=https://staging.tradeos.app -PRODUCTION_URL=https://TradeOS.app +PRODUCTION_URL=https://tradeos.app ADDITIONAL_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001 # GitHub integration (for auto-ticketing) diff --git a/config/deployment.ts b/config/deployment.ts index 09f705ad..efd7b4ba 100644 --- a/config/deployment.ts +++ b/config/deployment.ts @@ -7,11 +7,24 @@ export interface DeploymentConfig { previewEnabled: boolean; } -const DEFAULT_PRODUCTION_URL = 'https://TradeOS.app'; +const DEFAULT_PRODUCTION_URL = 'https://tradeos.app'; const DEFAULT_STAGING_URL = 'https://staging.tradeos.app'; function normalizeUrl(url: string): string { - return url.endsWith('/') ? url.slice(0, -1) : url; + const trimmed = url.trim(); + if (!trimmed) { + return trimmed; + } + + const withoutTrailingSlash = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed; + + try { + const parsed = new URL(withoutTrailingSlash); + parsed.hostname = parsed.hostname.toLowerCase(); + return parsed.toString().replace(/\/$/, ''); + } catch { + return withoutTrailingSlash; + } } function normalizeProvider(value: string | undefined): DeploymentProvider { diff --git a/scripts/converge.sh b/scripts/converge.sh index 59aca46b..0a813d39 100755 --- a/scripts/converge.sh +++ b/scripts/converge.sh @@ -41,7 +41,7 @@ run_stage_command() { ;; test) npm test - npm run test:webapp + npm --prefix webapp run test --if-present -- --ci --coverage ;; build) npm run build @@ -153,10 +153,14 @@ validate_firebase_config() { validate_vercel_config() { log "validate vercel config" - [ -f vercel.json ] || die "vercel.json missing" [ -f webapp/vercel.json ] || die "webapp/vercel.json missing" - validate_json_file vercel.json validate_json_file webapp/vercel.json + + if [ -f vercel.json ]; then + validate_json_file vercel.json + else + warn "root vercel.json not present; skipping root vercel config validation" + fi } regenerate_deterministic_artifacts() { diff --git a/scripts/doctor.ts b/scripts/doctor.ts index e0a7e4f2..094efbbf 100644 --- a/scripts/doctor.ts +++ b/scripts/doctor.ts @@ -211,24 +211,31 @@ function checkFirebaseConfig(repoRoot: string): void { } function checkVercelConfig(repoRoot: string): void { - const configs = [path.join(repoRoot, 'vercel.json'), path.join(repoRoot, 'webapp', 'vercel.json')]; - - for (const filePath of configs) { - if (!fs.existsSync(filePath)) { - addFinding('vercel-config', 'error', `Missing ${path.relative(repoRoot, filePath)}`, 'Restore Vercel config.'); - continue; - } + const webappConfigPath = path.join(repoRoot, 'webapp', 'vercel.json'); + const rootConfigPath = path.join(repoRoot, 'vercel.json'); + if (!fs.existsSync(webappConfigPath)) { + addFinding('vercel-config', 'error', 'Missing webapp/vercel.json', 'Restore webapp Vercel config.'); + } else { try { - const parsed = readJson(filePath) as { framework?: string; buildCommand?: string }; + const parsed = readJson(webappConfigPath) as { framework?: string; buildCommand?: string }; if (!parsed.framework) { - addFinding('vercel-config', 'warning', `${path.relative(repoRoot, filePath)} missing framework`, 'Set framework (for example: nextjs).'); + addFinding('vercel-config', 'warning', 'webapp/vercel.json missing framework', 'Set framework (for example: nextjs).'); } if (!parsed.buildCommand) { - addFinding('vercel-config', 'warning', `${path.relative(repoRoot, filePath)} missing buildCommand`, 'Set an explicit build command.'); + addFinding('vercel-config', 'warning', 'webapp/vercel.json missing buildCommand', 'Set an explicit build command.'); } } catch (_error) { - addFinding('vercel-config', 'error', `Invalid JSON in ${path.relative(repoRoot, filePath)}`, 'Fix malformed Vercel JSON.'); + addFinding('vercel-config', 'error', 'Invalid JSON in webapp/vercel.json', 'Fix malformed Vercel JSON.'); + } + } + + if (fs.existsSync(rootConfigPath)) { + try { + readJson(rootConfigPath); + addFinding('vercel-config', 'warning', 'Root vercel.json detected', 'Remove root vercel.json unless explicitly required by your deployment provider.'); + } catch (_error) { + addFinding('vercel-config', 'error', 'Invalid JSON in vercel.json', 'Fix malformed root Vercel JSON or remove the file.'); } } } From fb99ee3170da02ae6d36fceb33347474f7e75fb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 12:36:18 +0000 Subject: [PATCH 09/13] fix: ignore empty deployment URL env values Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/931fde24-2e6e-4749-8119-e2f59de5f602 Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- config/deployment.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/config/deployment.ts b/config/deployment.ts index efd7b4ba..c87f844e 100644 --- a/config/deployment.ts +++ b/config/deployment.ts @@ -35,10 +35,15 @@ function normalizeProvider(value: string | undefined): DeploymentProvider { return 'none'; } +function getNonEmptyEnv(name: string): string | undefined { + const value = process.env[name]?.trim(); + return value ? value : undefined; +} + export function getDeploymentConfig(): DeploymentConfig { - const primaryDomain = normalizeUrl(process.env.DEPLOYMENT_PRIMARY_DOMAIN ?? DEFAULT_PRODUCTION_URL); - const productionUrl = normalizeUrl(process.env.PRODUCTION_URL ?? primaryDomain); - const stagingUrl = normalizeUrl(process.env.STAGING_URL ?? DEFAULT_STAGING_URL); + const primaryDomain = normalizeUrl(getNonEmptyEnv('DEPLOYMENT_PRIMARY_DOMAIN') ?? DEFAULT_PRODUCTION_URL); + const productionUrl = normalizeUrl(getNonEmptyEnv('PRODUCTION_URL') ?? primaryDomain); + const stagingUrl = normalizeUrl(getNonEmptyEnv('STAGING_URL') ?? DEFAULT_STAGING_URL); const provider = normalizeProvider(process.env.DEPLOYMENT_PROVIDER); const previewEnabled = process.env.DEPLOY_PREVIEW_ENABLED === 'true'; From f955117c6a8aa4a2c5130ff22321fd97c012eb4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 12:37:38 +0000 Subject: [PATCH 10/13] chore: simplify deployment URL normalization implementation Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/931fde24-2e6e-4749-8119-e2f59de5f602 Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- config/deployment.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/config/deployment.ts b/config/deployment.ts index c87f844e..45e49082 100644 --- a/config/deployment.ts +++ b/config/deployment.ts @@ -16,14 +16,13 @@ function normalizeUrl(url: string): string { return trimmed; } - const withoutTrailingSlash = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed; - try { - const parsed = new URL(withoutTrailingSlash); + const parsed = new URL(trimmed); parsed.hostname = parsed.hostname.toLowerCase(); - return parsed.toString().replace(/\/$/, ''); + const normalized = parsed.toString(); + return normalized.endsWith('/') ? normalized.slice(0, -1) : normalized; } catch { - return withoutTrailingSlash; + return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed; } } @@ -37,7 +36,7 @@ function normalizeProvider(value: string | undefined): DeploymentProvider { function getNonEmptyEnv(name: string): string | undefined { const value = process.env[name]?.trim(); - return value ? value : undefined; + return value || undefined; } export function getDeploymentConfig(): DeploymentConfig { From eab5fb13613a91d2a7825397c717f4ea21982031 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 12:39:00 +0000 Subject: [PATCH 11/13] docs: clarify env normalization and optional webapp test execution Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/931fde24-2e6e-4749-8119-e2f59de5f602 Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- config/deployment.ts | 4 ++++ scripts/converge.sh | 1 + 2 files changed, 5 insertions(+) diff --git a/config/deployment.ts b/config/deployment.ts index 45e49082..02e5703e 100644 --- a/config/deployment.ts +++ b/config/deployment.ts @@ -34,6 +34,10 @@ function normalizeProvider(value: string | undefined): DeploymentProvider { return 'none'; } +/** + * Returns a trimmed env var value, treating blank values as undefined. + * This avoids admitting empty origins into CORS allow-lists. + */ function getNonEmptyEnv(name: string): string | undefined { const value = process.env[name]?.trim(); return value || undefined; diff --git a/scripts/converge.sh b/scripts/converge.sh index 0a813d39..6ae802a2 100755 --- a/scripts/converge.sh +++ b/scripts/converge.sh @@ -41,6 +41,7 @@ run_stage_command() { ;; test) npm test + # Run webapp tests when present, but do not mask real failures when they exist. npm --prefix webapp run test --if-present -- --ci --coverage ;; build) From 58741bfaef30b17e570c44adf8d98b70afa89aeb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 08:47:59 +0000 Subject: [PATCH 12/13] fix: fail converge automation on lockfile drift Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/018cee29-e998-45e7-9d33-6905c96dee01 Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- scripts/converge.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/converge.sh b/scripts/converge.sh index 6ae802a2..b10e0e27 100755 --- a/scripts/converge.sh +++ b/scripts/converge.sh @@ -109,6 +109,21 @@ normalize_lockfiles() { npm --prefix webapp install --package-lock-only --ignore-scripts --no-audit --no-fund } +ensure_clean_git_state_for_automation() { + local phase="$1" + + if [ "$CI_MODE" != "true" ] && [ "$SELF_HEAL_MODE" != "true" ]; then + return 0 + fi + + git update-index -q --refresh || true + local dirty_files + dirty_files="$(git --no-pager status --porcelain)" + if [ -n "$dirty_files" ]; then + die "detected workspace drift after ${phase} in automation mode; commit generated changes (for example lockfile updates) before rerunning."$'\n'"${dirty_files}" + fi +} + restore_generated_configs() { log "restore deterministic generated configs" if [ ! -f "webapp/next-env.d.ts" ]; then @@ -201,6 +216,7 @@ main() { log "mode ci=${CI_MODE} self_heal=${SELF_HEAL_MODE}" verify_structure normalize_lockfiles + ensure_clean_git_state_for_automation "lockfile normalization" restore_generated_configs validate_env_templates regenerate_deterministic_artifacts @@ -209,6 +225,7 @@ main() { validate_firebase_config validate_vercel_config run_quality_pipeline + ensure_clean_git_state_for_automation "quality pipeline" log "convergence completed successfully" } From d6214fe08d21b7bb3217295ddc0411f5824060c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 08:48:28 +0000 Subject: [PATCH 13/13] chore: refine converge drift error wording Agent-Logs-Url: https://github.com/SMSDAO/TradeOS/sessions/018cee29-e998-45e7-9d33-6905c96dee01 Co-authored-by: SMSDAO <144380926+SMSDAO@users.noreply.github.com> --- scripts/converge.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/converge.sh b/scripts/converge.sh index b10e0e27..1a10764e 100755 --- a/scripts/converge.sh +++ b/scripts/converge.sh @@ -120,7 +120,7 @@ ensure_clean_git_state_for_automation() { local dirty_files dirty_files="$(git --no-pager status --porcelain)" if [ -n "$dirty_files" ]; then - die "detected workspace drift after ${phase} in automation mode; commit generated changes (for example lockfile updates) before rerunning."$'\n'"${dirty_files}" + die "detected workspace drift after ${phase} in automation mode; commit generated changes such as lockfile updates before rerunning."$'\n'"${dirty_files}" fi }