Skip to content

Sanitize project update HTML to prevent stored XSS #143

Sanitize project update HTML to prevent stored XSS

Sanitize project update HTML to prevent stored XSS #143

Workflow file for this run

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 }}