Sanitize project update HTML to prevent stored XSS #143
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: run tests on pull request | |
| on: | |
| pull_request: | |
| # Performance: Cancel outdated workflow runs | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Security: Set minimal default permissions for all jobs | |
| permissions: | |
| contents: read # All jobs can read code, but can't write by default | |
| jobs: | |
| lint-and-build: | |
| name: Lint and Build | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # Read code | |
| pull-requests: write # Post lint comments | |
| checks: write # Create check runs | |
| statuses: write # Update check status | |
| steps: | |
| - name: Check out Git repository | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 1 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22.18.0 | |
| cache: 'npm' | |
| # Performance: Cache node_modules (shared with test jobs) | |
| - name: Cache node_modules | |
| id: node-cache | |
| uses: actions/cache@v4.3.0 | |
| with: | |
| path: node_modules | |
| key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }} | |
| restore-keys: | | |
| node-modules-${{ runner.os }}- | |
| - name: Install Node.js dependencies | |
| if: steps.node-cache.outputs.cache-hit != 'true' | |
| run: npm ci | |
| - name: Run linters | |
| uses: wearerequired/lint-action@v2.3.0 | |
| with: | |
| eslint: true | |
| #prettier: true | |
| continue_on_error: true | |
| # Build entire project INCLUDING tests (compile once, use 6 times!) | |
| - name: Build project and tests | |
| run: | | |
| echo "π¨ Building TypeScript (src + tests)..." | |
| npm run build:ci | |
| echo "β Build complete" | |
| # Upload compiled code for test jobs to use | |
| - name: Upload compiled build | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: compiled-build | |
| path: build/ | |
| retention-days: 1 | |
| # Separate job: Verify migrations work (optional, can fail independently) | |
| migration-check: | |
| name: Migration Verification | |
| runs-on: ubuntu-latest | |
| needs: lint-and-build | |
| # Security: Explicit minimal permissions | |
| permissions: | |
| contents: read | |
| services: | |
| postgres: | |
| image: ghcr.io/giveth/postgres-givethio:latest | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: givethio | |
| PGDATA: /var/lib/postgresql/data/pgdata | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5443:5432 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 1 | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v5.1.0 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_S3_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_S3_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ secrets.AWS_S3_REGION }} | |
| mask-aws-account-id: true # Security: Hide account ID in logs | |
| # Performance: Get current week for cache key (caches DB backup for the week) | |
| - name: Get current week | |
| id: date | |
| run: | | |
| # Get ISO week number (e.g., 2025-W44) | |
| WEEK=$(date +'%Y-W%V') | |
| echo "week=$WEEK" >> $GITHUB_OUTPUT | |
| echo "Cache key will be: db-backup-staging-$WEEK" | |
| # Performance: Cache database backup to avoid repeated downloads | |
| # Cache is shared across all runs in the same week (7 days) | |
| - name: Cache database backup | |
| id: cache-db | |
| uses: actions/cache@v4.3.0 | |
| with: | |
| path: /tmp/db_backup.zip | |
| # Cache key: week-based so all runs same week share cache | |
| # Format: db-backup-staging-2025-W44 (changes every Monday) | |
| key: db-backup-staging-${{ steps.date.outputs.week }} | |
| restore-keys: | | |
| db-backup-staging- | |
| # Only download if cache miss | |
| - name: Download latest DB backup from S3 | |
| if: steps.cache-db.outputs.cache-hit != 'true' | |
| run: | | |
| set -e | |
| echo "π₯ Cache miss - downloading fresh backup from S3..." | |
| FILENAME=$(aws s3 ls ${{ secrets.AWS_S3_BUCKET_PATH_STAGING }}/ | sort | tail -n 1 | awk '{print $4}') | |
| if [ -z "$FILENAME" ]; then | |
| echo "Error: No backup file found in S3" | |
| exit 1 | |
| fi | |
| echo "Downloading backup: $FILENAME" | |
| aws s3 cp ${{ secrets.AWS_S3_BUCKET_PATH_STAGING }}/$FILENAME /tmp/db_backup.zip | |
| echo "β Download complete: $(du -h /tmp/db_backup.zip | cut -f1)" | |
| - name: Verify cached backup exists | |
| if: steps.cache-db.outputs.cache-hit == 'true' | |
| run: | | |
| echo "β Cache hit! Using cached database backup" | |
| echo "Cached file size: $(du -h /tmp/db_backup.zip | cut -f1)" | |
| - name: Unzip and validate DB backup | |
| run: | | |
| set -e | |
| echo "π¦ Extracting database backup..." | |
| unzip -q /tmp/db_backup.zip -d /tmp | |
| # Security: Validate the backup file exists and is readable | |
| SQL_FILE=$(find /tmp/backups/givethio-staging/*.sql -type f -print -quit 2>/dev/null) | |
| if [ ! -f "$SQL_FILE" ]; then | |
| echo "Error: SQL backup file not found" | |
| exit 1 | |
| fi | |
| mv "$SQL_FILE" /tmp/backups/givethio-staging/db_backup.sql | |
| echo "β Database backup prepared: $(du -h /tmp/backups/givethio-staging/db_backup.sql | cut -f1)" | |
| - name: Wait for PostgreSQL to become ready | |
| run: | | |
| for i in {1..10}; do | |
| if pg_isready -h localhost -p 5443 -U postgres; then | |
| echo "β PostgreSQL is ready" | |
| break | |
| fi | |
| echo -n . | |
| sleep 1 | |
| done | |
| # Verify database is actually ready | |
| if ! pg_isready -h localhost -p 5443 -U postgres; then | |
| echo "β PostgreSQL failed to become ready after 10 attempts" | |
| exit 1 | |
| fi | |
| - name: Use Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22.18.0 | |
| cache: 'npm' | |
| # Performance: Cache node_modules (shared with all jobs) | |
| - name: Cache node_modules | |
| id: node-cache | |
| uses: actions/cache@v4.3.0 | |
| with: | |
| path: node_modules | |
| key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }} | |
| restore-keys: | | |
| node-modules-${{ runner.os }}- | |
| - name: Install dependencies | |
| if: steps.node-cache.outputs.cache-hit != 'true' | |
| run: npm ci | |
| # Performance: Check if we have a cached post-migration database state FIRST | |
| - name: Get migration checksum | |
| id: migration-checksum | |
| run: | | |
| # Create checksum of all migration files to detect changes | |
| CHECKSUM=$(find migration -name "*.ts" -type f -exec sha256sum {} \; | sort | sha256sum | cut -d' ' -f1) | |
| echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT | |
| echo "Migration checksum: $CHECKSUM" | |
| - name: Cache post-migration database state | |
| id: cache-migrated-db | |
| uses: actions/cache@v4.3.0 | |
| with: | |
| path: /tmp/migrated_db_dump.sql | |
| key: migrated-db-${{ steps.date.outputs.week }}-${{ steps.migration-checksum.outputs.checksum }} | |
| restore-keys: | | |
| migrated-db-${{ steps.date.outputs.week }}- | |
| # Only restore initial backup if we DON'T have cached migrated DB | |
| - name: Restore initial DB backup | |
| if: steps.cache-migrated-db.outputs.cache-hit != 'true' | |
| run: | | |
| set -e | |
| echo "π¦ Restoring initial database backup..." | |
| PGPASSWORD=postgres psql -h localhost -p 5443 -U postgres -d givethio \ | |
| < /tmp/backups/givethio-staging/db_backup.sql | |
| echo "β Initial database restored" | |
| # Restore from cached migrated database if available (SKIP initial restore) | |
| - name: Restore cached post-migration database | |
| if: steps.cache-migrated-db.outputs.cache-hit == 'true' | |
| run: | | |
| set -e | |
| echo "β‘ Restoring from cached post-migration database..." | |
| PGPASSWORD=postgres psql -h localhost -p 5443 -U postgres -d givethio \ | |
| --set ON_ERROR_STOP=on < /tmp/migrated_db_dump.sql | |
| echo "β Cached database restored (skipped migrations!)" | |
| # Only run migrations if no cached migrated DB | |
| - name: Run migrations | |
| if: steps.cache-migrated-db.outputs.cache-hit != 'true' | |
| run: | | |
| echo "π Running migrations (no cache)..." | |
| npm run db:migrate:run:test | |
| echo "β Migrations complete" | |
| # Create dump of migrated database for caching | |
| - name: Create post-migration database dump | |
| if: steps.cache-migrated-db.outputs.cache-hit != 'true' | |
| run: | | |
| echo "πΎ Creating post-migration database dump for caching..." | |
| PGPASSWORD=postgres pg_dump -h localhost -p 5443 -U postgres -d givethio \ | |
| --no-owner --no-acl > /tmp/migrated_db_dump.sql | |
| # Verify dump was created and show size | |
| if [ ! -f /tmp/migrated_db_dump.sql ]; then | |
| echo "β ERROR: Dump file was not created!" | |
| exit 1 | |
| fi | |
| DUMP_SIZE=$(du -h /tmp/migrated_db_dump.sql | cut -f1) | |
| DUMP_SIZE_MB=$(du -m /tmp/migrated_db_dump.sql | cut -f1) | |
| echo "β Dump created: $DUMP_SIZE ($DUMP_SIZE_MB MB)" | |
| # Warn if dump is very large | |
| if [ $DUMP_SIZE_MB -gt 2000 ]; then | |
| echo "β οΈ Warning: Dump is large (${DUMP_SIZE_MB}MB), caching may be slow" | |
| fi | |
| - name: Verify migrated database dump exists | |
| run: | | |
| if [ -f /tmp/migrated_db_dump.sql ]; then | |
| echo "β Migrated database dump ready for test jobs" | |
| ls -lh /tmp/migrated_db_dump.sql | |
| else | |
| echo "β οΈ Warning: Dump file not found, test jobs will need to migrate" | |
| fi | |
| test: | |
| name: Integration Tests - ${{ matrix.test-group-name }} | |
| runs-on: ubuntu-latest | |
| needs: lint-and-build # Tests don't need migration-check to complete! | |
| # Performance: Run test groups in parallel | |
| strategy: | |
| fail-fast: false # Don't cancel other groups if one fails | |
| matrix: | |
| include: | |
| - test-group-name: "Resolvers" | |
| test-pattern: "src/resolvers/**/*.test.ts" | |
| - test-group-name: "Repositories" | |
| test-pattern: "src/repositories/**/*.test.ts" | |
| - test-group-name: "Services" | |
| test-pattern: "src/services/**/*.test.ts" | |
| - test-group-name: "Server & Routes" | |
| test-pattern: "src/server/**/*.test.ts src/routers/**/*.test.ts src/user/**/*.test.ts" | |
| - test-group-name: "Entities & Utils" | |
| test-pattern: "src/entities/**/*.test.ts src/utils/**/*.test.ts src/adapters/**/*.test.ts" | |
| - test-group-name: "Workers & Migrations" | |
| test-pattern: "src/workers/**/*.test.ts migration/tests/*.test.ts" | |
| # Security: Explicit minimal permissions | |
| permissions: | |
| contents: read # Read code | |
| pull-requests: write # Comment test results | |
| checks: write # Update check runs | |
| services: | |
| # Label used to access the service container | |
| redis: | |
| # Docker Hub image - Security: Consider using specific version tag | |
| image: redis:7.4-alpine # Performance: Alpine is lighter, Security: specific version | |
| # Set health checks to wait until redis has started | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 6379:6379 | |
| postgres: | |
| image: ghcr.io/giveth/postgres-givethio:latest | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: givethio | |
| PGDATA: /var/lib/postgresql/data/pgdata | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5443:5432 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 1 | |
| - name: Use Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22.18.0 | |
| cache: 'npm' | |
| # Performance: Cache node_modules (shared from lint-and-build) | |
| - name: Cache node_modules | |
| id: node-cache | |
| uses: actions/cache@v4.3.0 | |
| with: | |
| path: node_modules | |
| key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }} | |
| restore-keys: | | |
| node-modules-${{ runner.os }}- | |
| - name: Install dependencies | |
| if: steps.node-cache.outputs.cache-hit != 'true' | |
| run: npm ci | |
| # Performance: Download pre-compiled build from lint-and-build job | |
| - name: Download compiled build | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: compiled-build | |
| path: build/ | |
| - name: Wait for services to become ready | |
| run: | | |
| echo "β³ Waiting for PostgreSQL and Redis..." | |
| for i in {1..30}; do | |
| if pg_isready -h localhost -p 5443 -U postgres > /dev/null 2>&1; then | |
| echo "β PostgreSQL is ready!" | |
| break | |
| fi | |
| echo -n "." | |
| sleep 1 | |
| done | |
| # Redis should be ready almost immediately | |
| if redis-cli -h localhost ping > /dev/null 2>&1; then | |
| echo "β Redis is ready!" | |
| fi | |
| # Performance: Run pre-compiled JavaScript tests (much faster!) | |
| - name: Run tests - ${{ matrix.test-group-name }} | |
| run: | | |
| echo "π§ͺ Running test group: ${{ matrix.test-group-name }}" | |
| echo "π Test pattern: ${{ matrix.test-pattern }}" | |
| # Convert TypeScript patterns to JavaScript patterns | |
| TEST_PATTERN=$(echo "${{ matrix.test-pattern }}" | sed 's/\.ts/.js/g') | |
| echo "π¦ Compiled test pattern: $TEST_PATTERN" | |
| # Run compiled JavaScript tests (no ts-node overhead!) | |
| NODE_ENV=test npx mocha \ | |
| --exit \ | |
| --retries 2 \ | |
| ./build/test/pre-test-scripts.js \ | |
| $(echo "$TEST_PATTERN" | sed 's|src/|build/src/|g') | |
| env: | |
| # Performance: Optimize Node.js memory for testing | |
| NODE_OPTIONS: "--max-old-space-size=4096" | |
| ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} | |
| XDAI_NODE_HTTP_URL: ${{ secrets.XDAI_NODE_HTTP_URL }} | |
| INFURA_API_KEY: ${{ secrets.INFURA_API_KEY }} | |
| INFURA_ID: ${{ secrets.INFURA_ID }} | |
| POLYGON_SCAN_API_KEY: ${{ secrets.POLYGON_SCAN_API_KEY }} | |
| OPTIMISTIC_SCAN_API_KEY: ${{ secrets.OPTIMISTIC_SCAN_API_KEY }} | |
| CELO_SCAN_API_KEY: ${{ secrets.CELO_SCAN_API_KEY }} | |
| CELO_ALFAJORES_SCAN_API_KEY: ${{ secrets.CELO_ALFAJORES_SCAN_API_KEY }} | |
| ARBITRUM_SCAN_API_KEY: ${{ secrets.ARBITRUM_SCAN_API_KEY }} | |
| ARBITRUM_SEPOLIA_SCAN_API_KEY: ${{ secrets.ARBITRUM_SEPOLIA_SCAN_API_KEY }} | |
| BASE_SCAN_API_KEY: ${{ secrets.BASE_SCAN_API_KEY }} | |
| BASE_SEPOLIA_SCAN_API_KEY: ${{ secrets.BASE_SEPOLIA_SCAN_API_KEY }} | |
| ZKEVM_MAINNET_SCAN_API_KEY: ${{ secrets.ZKEVM_MAINNET_SCAN_API_KEY }} | |
| ZKEVM_CARDONA_SCAN_API_KEY: ${{ secrets.ZKEVM_CARDONA_SCAN_API_KEY }} | |
| MORDOR_ETC_TESTNET: ${{ secrets.MORDOR_ETC_TESTNET }} | |
| ETC_NODE_HTTP_URL: ${{ secrets.ETC_NODE_HTTP_URL }} | |
| DROP_DATABASE: ${{ secrets.DROP_DATABASE_DURING_TEST_STAGING }} | |
| SOLANA_TEST_NODE_RPC_URL: ${{ secrets.SOLANA_TEST_NODE_RPC_URL }} | |
| SOLANA_DEVNET_NODE_RPC_URL: ${{ secrets.SOLANA_DEVNET_NODE_RPC_URL }} | |
| SOLANA_MAINNET_NODE_RPC_URL: ${{ secrets.SOLANA_MAINNET_NODE_RPC_URL }} | |
| MPETH_GRAPHQL_PRICES_URL: ${{ secrets.MPETH_GRAPHQL_PRICES_URL }} | |
| GIV_POWER_SUBGRAPH_URL: ${{ secrets.GIV_POWER_SUBGRAPH_URL }} | |
| VERIFY_RIGHT_URL: ${{ secrets.VERIFY_RIGHT_URL }} | |
| VERIFY_RIGHT_TOKEN: ${{ secrets.VERIFY_RIGHT_TOKEN }} |