Skip to content

deps(dev)(deps-dev): bump the eslint-ecosystem group across 1 directory with 2 updates #366

deps(dev)(deps-dev): bump the eslint-ecosystem group across 1 directory with 2 updates

deps(dev)(deps-dev): bump the eslint-ecosystem group across 1 directory with 2 updates #366

name: Continuous integration
permissions:
contents: read
actions: write
pull-requests: write
issues: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on:
pull_request:
branches: [main, master, develop]
paths-ignore:
- '**.md'
- 'issue/**'
push:
branches: [main, master, develop]
paths-ignore:
- '**.md'
- 'issue/**'
merge_group:
types: [checks_requested]
workflow_dispatch:
env:
NODE_VERSION: '24'
NEXT_PUBLIC_SITE_URL: https://www.sortvision.com
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
defaults:
run:
working-directory: ./SortVision
jobs:
format:
name: Format
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 1
- name: Setup SortVision
uses: ./.github/actions/setup-sortvision
with:
node-version: ${{ env.NODE_VERSION }}
- name: Prettier check
run: pnpm run format:check
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 1
- name: Setup SortVision
uses: ./.github/actions/setup-sortvision
with:
node-version: ${{ env.NODE_VERSION }}
- name: ESLint
run: pnpm run lint
- name: Unit tests
run: pnpm run test:unit
hygiene:
name: Hygiene
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 1
- name: Lockfile policy (pnpm only)
run: |
set -euo pipefail
ROOT="${GITHUB_WORKSPACE}/SortVision"
test -f "${ROOT}/pnpm-lock.yaml"
if [ -f "${ROOT}/package-lock.json" ] || [ -f "${ROOT}/yarn.lock" ]; then
echo "::error::SortVision must use pnpm only β€” remove package-lock.json / yarn.lock under SortVision/."
exit 1
fi
- name: Setup SortVision
uses: ./.github/actions/setup-sortvision
with:
node-version: ${{ env.NODE_VERSION }}
- name: pnpm audit (production high/critical)
run: pnpm audit --production --audit-level=high
build:
name: Build
needs: [format, lint, hygiene]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 1
- name: Cache Next.js build
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
with:
path: SortVision/.next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('SortVision/pnpm-lock.yaml', 'SortVision/package.json') }}
restore-keys: nextjs-${{ runner.os }}-
- name: Setup SortVision
uses: ./.github/actions/setup-sortvision
with:
node-version: ${{ env.NODE_VERSION }}
- name: Build application
run: pnpm run build
env:
NODE_ENV: production
- name: Analyze bundle size
run: |
echo "=== Bundle Size Analysis ==="
du -sh .next/static/chunks/* 2>/dev/null | sort -h | tail -10 || echo "No chunks found"
TOTAL_SIZE=$(du -sb .next 2>/dev/null | awk '{print $1}')
TOTAL_SIZE_MB=$((TOTAL_SIZE / 1024 / 1024))
echo "Total build size: ${TOTAL_SIZE_MB}MB"
if [ "$TOTAL_SIZE_MB" -gt 50 ]; then
echo "WARNING: Build size exceeds 50MB"
fi
- name: Validate dynamic sitemap configuration
run: |
pnpm run generate-sitemap
test -f src/app/sitemap.xml/route.ts
test -f src/app/sitemap-index.xml/route.ts
echo "Dynamic sitemap routes detected:"
echo "- src/app/sitemap.xml/route.ts"
echo "- src/app/sitemap-index.xml/route.ts"
- name: Pack Next.js build for CI artifact
working-directory: ${{ github.workspace }}
run: |
set -euo pipefail
test -f SortVision/.next/BUILD_ID
tar czf "${RUNNER_TEMP}/next-build.tar.gz" -C SortVision .next
- name: Upload Next.js build output
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: next-build
path: ${{ runner.temp }}/next-build.tar.gz
if-no-files-found: error
retention-days: 7
test:
name: Test
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
- name: Setup SortVision
uses: ./.github/actions/setup-sortvision
with:
node-version: ${{ env.NODE_VERSION }}
- name: Restore Next.js build from artifact
uses: ./.github/actions/restore-next-build
- name: Log scan context
run: |
echo "Commit: ${{ github.sha }}"
echo "Branch: ${{ github.ref_name }}"
- name: Start production server
run: |
pnpm run start &
echo $! > .server-pid
pnpm exec wait-on http://localhost:3000 --timeout 120000
sleep 2
- name: Run complete test suite
id: qa_tests
run: pnpm run test:ci
env:
CI: 'true'
GITHUB_ACTIONS: 'true'
QA_COMMENT_DIR: ${{ github.event.pull_request && format('{0}/SortVision/.qa-pr-comment', github.workspace) || '' }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
QA_METRICS_FILE: ${{ (github.event_name == 'push' || github.event_name == 'pull_request') && format('{0}/SortVision/qa-metrics.json', github.workspace) || '' }}
- name: Prepare QA PR comment artifact
if: github.event_name == 'pull_request' && always()
run: |
DIR="${GITHUB_WORKSPACE}/SortVision/.qa-pr-comment"
mkdir -p "$DIR"
if [ ! -s "$DIR/comment.md" ]; then
echo "No QA report on disk; writing minimal placeholder for PR comment workflow."
{
echo '<!-- sortvision-qa-report -->'
echo '### QA suite report'
echo ''
echo '**Result:** No report file was written (early failure or env issue). See **Test** job logs.'
echo ''
echo "[View run](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
} > "$DIR/comment.md"
fi
if [ ! -s "$DIR/pr_number.txt" ]; then
echo '${{ github.event.pull_request.number }}' > "$DIR/pr_number.txt"
fi
- name: Download base branch QA metrics (PR)
if: github.event_name == 'pull_request' && always()
env:
GH_TOKEN: ${{ github.token }}
working-directory: ${{ github.workspace }}
run: |
set +e
BASE_REF="${{ github.event.pull_request.base.ref }}"
PREV_RUN_ID=$(gh run list --repo "${GITHUB_REPOSITORY}" \
--workflow="continuous-integration.yml" \
--branch="${BASE_REF}" \
--status=success \
--json databaseId \
--limit 25 | jq -r --argjson rid "${GITHUB_RUN_ID}" '[.[] | select(.databaseId != $rid) | .databaseId] | first // empty')
set -e
if [ -z "${PREV_RUN_ID}" ]; then
echo "No successful CI run on ${BASE_REF} with downloadable metrics yet."
exit 0
fi
mkdir -p "${GITHUB_WORKSPACE}/qa-base-dl"
if gh run download "${PREV_RUN_ID}" -n qa-metrics -D "${GITHUB_WORKSPACE}/qa-base-dl"; then
FOUND=$(find "${GITHUB_WORKSPACE}/qa-base-dl" -name qa-metrics.json -print -quit)
if [ -n "${FOUND}" ]; then
cp "${FOUND}" "${GITHUB_WORKSPACE}/SortVision/qa-metrics-base.json"
echo "Base metrics from workflow run ${PREV_RUN_ID} (${BASE_REF})"
fi
else
echo "Could not download qa-metrics from run ${PREV_RUN_ID}."
fi
- name: Append QA vs base branch to PR comment
if: github.event_name == 'pull_request' && always()
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
QA_COMMENT_FILE: ${{ github.workspace }}/SortVision/.qa-pr-comment/comment.md
QA_METRICS_CURRENT: ${{ github.workspace }}/SortVision/qa-metrics.json
QA_METRICS_BASE: ${{ github.workspace }}/SortVision/qa-metrics-base.json
run: node scripts/ci/ci-qa-pr-vs-base.cjs
- name: Upload QA metrics artifact
if: (github.event_name == 'push' || github.event_name == 'pull_request') && always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: qa-metrics
path: SortVision/qa-metrics.json
if-no-files-found: ignore
retention-days: 30
- name: Find previous QA comment
id: find_qa_comment
if: github.event_name == 'pull_request' && always()
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- sortvision-qa-report -->'
- name: Post QA report to pull request
if: github.event_name == 'pull_request' && always()
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
with:
comment-id: ${{ steps.find_qa_comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: SortVision/.qa-pr-comment/comment.md
edit-mode: replace
- name: Upload QA PR comment artifact
if: github.event_name == 'pull_request' && always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: qa-pr-comment
path: SortVision/.qa-pr-comment/
include-hidden-files: true
doctor:
name: Doctor
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
# fetch-depth: 0 required for react-doctor diff comparison
fetch-depth: 0
- name: Setup SortVision
uses: ./.github/actions/setup-sortvision
with:
node-version: ${{ env.NODE_VERSION }}
- name: Log scan context
run: |
echo "Scanning commit: ${{ github.sha }}"
echo "Branch: ${{ github.ref_name }}"
- name: Run React Doctor
run: |
npx -y react-doctor@latest . \
--diff main \
--fail-on none \
--score 2>&1 | tee doctor-output.txt
continue-on-error: true
- name: Extract score
id: score
run: |
SCORE=$(cat doctor-output.txt | grep -oP '\d+\s*/\s*100' | head -1 | grep -oP '\d+' || echo "0")
echo "score=$SCORE" >> $GITHUB_OUTPUT
- name: Write metrics
run: |
echo "{\"score\": ${{ steps.score.outputs.score }}}" > doctor-metrics.json
- name: Download base Doctor metrics (PR)
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
working-directory: ${{ github.workspace }}
run: |
set +e
BASE_REF="${{ github.event.pull_request.base.ref }}"
PREV_RUN_ID=$(gh run list --repo "${GITHUB_REPOSITORY}" \
--workflow="continuous-integration.yml" \
--branch="${BASE_REF}" \
--status=success \
--json databaseId \
--limit 25 | jq -r --argjson rid "${GITHUB_RUN_ID}" '[.[] | select(.databaseId != $rid) | .databaseId] | first // empty')
set -e
if [ -z "${PREV_RUN_ID}" ]; then
echo "No previous successful CI on ${BASE_REF} for Doctor baseline."
exit 0
fi
mkdir -p "${GITHUB_WORKSPACE}/doctor-base-dl"
if gh run download "${PREV_RUN_ID}" -n doctor-metrics -D "${GITHUB_WORKSPACE}/doctor-base-dl"; then
FOUND=$(find "${GITHUB_WORKSPACE}/doctor-base-dl" -name doctor-metrics.json -print -quit)
if [ -n "${FOUND}" ]; then
cp "${FOUND}" "${GITHUB_WORKSPACE}/doctor-metrics-base.json"
echo "Base Doctor metrics from run ${PREV_RUN_ID}"
fi
fi
- name: Update PR comment
env:
DOCTOR_COMMENT_DIR: ${{ github.workspace }}/.doctor-pr-comment
DOCTOR_METRICS_CURRENT: ${{ github.workspace }}/doctor-metrics.json
DOCTOR_METRICS_BASE: ${{ github.workspace }}/doctor-metrics-base.json
run: node scripts/ci/ci-doctor-pr-vs-base.mjs
- name: Upload Doctor metrics artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: doctor-metrics
path: doctor-metrics.json
if-no-files-found: ignore
retention-days: 30
- name: Upload Doctor PR comment artifact
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: doctor-pr-comment
path: .doctor-pr-comment/
include-hidden-files: true
lighthouse:
name: Lighthouse (${{ matrix.variant }})
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- variant: mobile
config: lighthouserc.json
artifact: lighthouse-results-mobile
- variant: desktop
config: lighthouserc.desktop.json
artifact: lighthouse-results-desktop
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 1
- name: Setup SortVision
uses: ./.github/actions/setup-sortvision
with:
node-version: ${{ env.NODE_VERSION }}
- name: Restore Next.js build from artifact
uses: ./.github/actions/restore-next-build
- name: Log scan context
run: |
echo "Commit: ${{ github.sha }}"
echo "Branch: ${{ github.ref_name }}"
- name: Start production server
run: |
pnpm run start &
echo $! > .server-pid
pnpm exec wait-on http://localhost:3000 --timeout 120000
sleep 2
- name: Lighthouse (${{ matrix.variant }})
uses: treosh/lighthouse-ci-action@3e7e23fb74242897f95c0ba9cabad3d0227b9b18
with:
configPath: ./SortVision/${{ matrix.config }}
runs: 1
urls: |
http://localhost:3000
http://localhost:3000/algorithms/config/bubble
http://localhost:3000/es
http://localhost:3000/contributions/overview
uploadArtifacts: true
artifactName: ${{ matrix.artifact }}
temporaryPublicStorage: true
- name: Upload Lighthouse manifest (for summary)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: lighthouse-manifest-${{ matrix.variant }}
path: .lighthouseci/manifest.json
if-no-files-found: error
retention-days: 7
summary:
name: Summary
runs-on: ubuntu-latest
timeout-minutes: 10
if: ${{ !cancelled() }}
defaults:
run:
working-directory: ${{ github.workspace }}
steps:
- name: Log CI status
run: |
echo "## CI Status Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Format | \${{ needs.format.result }} |" 2>/dev/null || true
echo "| Lint | \${{ needs.lint.result }} |" 2>/dev/null || true
echo "| Hygiene | \${{ needs.hygiene.result }} |" 2>/dev/null || true
echo "| Build | \${{ needs.build.result }} |" 2>/dev/null || true
echo "| Test | \${{ needs.test.result }} |" 2>/dev/null || true
echo "| Doctor | \${{ needs.doctor.result }} |" 2>/dev/null || true
echo "| Lighthouse | \${{ needs.lighthouse.result }} |" 2>/dev/null || true
echo "" >> $GITHUB_STEP_SUMMARY
- name: Fail on critical job failures
run: |
FAILED=""
for job in test doctor lighthouse; do
result="\${needs.$job.result:-skipped}"
if [ "$result" = "failure" ]; then
FAILED="$FAILED $job"
fi
done
if [ -n "$FAILED" ]; then
echo "::error::Critical jobs failed:$FAILED"
exit 1
fi
validate:
name: Validate
needs: [summary]
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
defaults:
run:
working-directory: ./SortVision
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: ${{ env.NODE_VERSION }}
- name: Test production site
run: node tests/qa/quality-assurance.ts --production
- name: Validate production performance
run: |
echo "=== Production Site Health Check ==="
RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}' https://www.sortvision.com)
HTTP_CODE=$(curl -o /dev/null -s -w '%{http_code}' https://www.sortvision.com)
echo "HTTP Status: $HTTP_CODE"
echo "Response Time: ${RESPONSE_TIME}s"
if [ "$HTTP_CODE" != "200" ]; then
echo "::error::Production site returned $HTTP_CODE"
exit 1
fi
RESPONSE_MS=$(echo "$RESPONSE_TIME * 1000" | bc)
if [ "$(echo "$RESPONSE_MS > 3000" | bc)" -eq 1 ]; then
echo "WARNING: Response time exceeds 3 seconds"
fi
- name: Generate production report
if: always()
run: |
echo "## Production Validation Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Production site: https://www.sortvision.com" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Check | Result |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Site Accessibility | Passed |" >> $GITHUB_STEP_SUMMARY
echo "| Production Tests | Passed |" >> $GITHUB_STEP_SUMMARY
echo "| Response Time | Under 3s |" >> $GITHUB_STEP_SUMMARY