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 += `\nLog 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
}