From 33bcc8c008fe2cee4ed67a11935d2008d2e45afc Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Tue, 14 Apr 2026 15:23:57 -0300 Subject: [PATCH 1/5] fix(frontend-pr-analysis): detect vitest version for coverage reporter flag compatibility Vitest 3 removed --coverageReporters CLI flag. Detect the installed Vitest major version at runtime and use --coverage.reporter (dot notation) for v3+, keeping --coverageReporters for Jest and Vitest 2. Closes #215 --- .github/workflows/frontend-pr-analysis.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/frontend-pr-analysis.yml b/.github/workflows/frontend-pr-analysis.yml index e6fae6ce..b4be922d 100644 --- a/.github/workflows/frontend-pr-analysis.yml +++ b/.github/workflows/frontend-pr-analysis.yml @@ -364,10 +364,20 @@ jobs: - name: Run tests with coverage working-directory: ${{ matrix.app.working_dir }} run: | + # Vitest 3+ removed --coverageReporters CLI flag (use --coverage.reporter instead) + # Jest and Vitest 2 still support --coverageReporters + VITEST_MAJOR=$(node -e "try{console.log(require('vitest/package.json').version.split('.')[0])}catch{console.log('0')}" 2>/dev/null) + + if [[ "$VITEST_MAJOR" -ge 3 ]]; then + COVERAGE_FLAGS="--coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=lcov" + else + COVERAGE_FLAGS="--coverageReporters=text --coverageReporters=json-summary --coverageReporters=lcov" + fi + case "${{ inputs.package_manager }}" in - yarn) yarn test --coverage --coverageReporters=text --coverageReporters=json-summary --coverageReporters=lcov ;; - pnpm) pnpm test --coverage --coverageReporters=text --coverageReporters=json-summary --coverageReporters=lcov ;; - *) npm test -- --coverage --coverageReporters=text --coverageReporters=json-summary --coverageReporters=lcov ;; + yarn) yarn test --coverage $COVERAGE_FLAGS ;; + pnpm) pnpm test --coverage $COVERAGE_FLAGS ;; + *) npm test -- --coverage $COVERAGE_FLAGS ;; esac - name: Upload coverage artifact From 89fe756fba40549c6ac79a1b6d73fa1fd870613b Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Tue, 14 Apr 2026 15:25:58 -0300 Subject: [PATCH 2/5] fix(frontend-pr-analysis): use bash array for coverage flags to satisfy shellcheck --- .github/workflows/frontend-pr-analysis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/frontend-pr-analysis.yml b/.github/workflows/frontend-pr-analysis.yml index b4be922d..ae01733b 100644 --- a/.github/workflows/frontend-pr-analysis.yml +++ b/.github/workflows/frontend-pr-analysis.yml @@ -369,15 +369,15 @@ jobs: VITEST_MAJOR=$(node -e "try{console.log(require('vitest/package.json').version.split('.')[0])}catch{console.log('0')}" 2>/dev/null) if [[ "$VITEST_MAJOR" -ge 3 ]]; then - COVERAGE_FLAGS="--coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=lcov" + COVERAGE_FLAGS=(--coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=lcov) else - COVERAGE_FLAGS="--coverageReporters=text --coverageReporters=json-summary --coverageReporters=lcov" + COVERAGE_FLAGS=(--coverageReporters=text --coverageReporters=json-summary --coverageReporters=lcov) fi case "${{ inputs.package_manager }}" in - yarn) yarn test --coverage $COVERAGE_FLAGS ;; - pnpm) pnpm test --coverage $COVERAGE_FLAGS ;; - *) npm test -- --coverage $COVERAGE_FLAGS ;; + yarn) yarn test --coverage "${COVERAGE_FLAGS[@]}" ;; + pnpm) pnpm test --coverage "${COVERAGE_FLAGS[@]}" ;; + *) npm test -- --coverage "${COVERAGE_FLAGS[@]}" ;; esac - name: Upload coverage artifact From f7cfa90cc412ae84b5e4147c1b4afc1a6e6617ae Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Tue, 14 Apr 2026 15:40:03 -0300 Subject: [PATCH 3/5] fix(frontend-pr-analysis): resolve all lint warnings - Pin all external actions by SHA (checkout, setup-node, upload-artifact, download-artifact, github-script) - Quote $GITHUB_OUTPUT to satisfy SC2086 - Replace $? check with direct exit code test to satisfy SC2181 - Quote $BASE_SHA, $HEAD_SHA, $PREV_COMMIT variables --- .github/workflows/frontend-pr-analysis.yml | 57 +++++++++++----------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/.github/workflows/frontend-pr-analysis.yml b/.github/workflows/frontend-pr-analysis.yml index ae01733b..35adf098 100644 --- a/.github/workflows/frontend-pr-analysis.yml +++ b/.github/workflows/frontend-pr-analysis.yml @@ -101,7 +101,7 @@ jobs: has_changes: ${{ steps.set-matrix.outputs.has_changes }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 @@ -112,12 +112,11 @@ jobs: if [[ "${{ github.event_name }}" == "pull_request" ]]; then BASE_SHA="${{ github.event.pull_request.base.sha }}" HEAD_SHA="${{ github.event.pull_request.head.sha }}" - git fetch origin $BASE_SHA --depth=1 2>/dev/null || true - FILES=$(git diff --name-only $BASE_SHA $HEAD_SHA 2>/dev/null || git diff --name-only origin/${{ github.base_ref }}...HEAD) + git fetch origin "$BASE_SHA" --depth=1 2>/dev/null || true + FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" 2>/dev/null || git diff --name-only origin/${{ github.base_ref }}...HEAD) elif [[ "${{ github.event.before }}" == "0000000000000000000000000000000000000000" ]] || [[ -z "${{ github.event.before }}" ]]; then - PREV_COMMIT=$(git rev-parse HEAD^) - if [[ $? -eq 0 ]]; then - FILES=$(git diff --name-only $PREV_COMMIT HEAD) + if PREV_COMMIT=$(git rev-parse HEAD^); then + FILES=$(git diff --name-only "$PREV_COMMIT" HEAD) else FILES=$(git ls-tree -r --name-only HEAD) fi @@ -225,10 +224,10 @@ jobs: app: ${{ fromJson(needs.detect-changes.outputs.matrix) }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: ${{ inputs.node_version }} cache: ${{ inputs.package_manager }} @@ -266,10 +265,10 @@ jobs: app: ${{ fromJson(needs.detect-changes.outputs.matrix) }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: ${{ inputs.node_version }} cache: ${{ inputs.package_manager }} @@ -302,10 +301,10 @@ jobs: app: ${{ fromJson(needs.detect-changes.outputs.matrix) }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: ${{ inputs.node_version }} cache: ${{ inputs.package_manager }} @@ -343,10 +342,10 @@ jobs: app: ${{ fromJson(needs.detect-changes.outputs.matrix) }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: ${{ inputs.node_version }} cache: ${{ inputs.package_manager }} @@ -381,7 +380,7 @@ jobs: esac - name: Upload coverage artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: coverage-${{ matrix.app.name }} path: | @@ -402,10 +401,10 @@ jobs: app: ${{ fromJson(needs.detect-changes.outputs.matrix) }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Download coverage artifact - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: coverage-${{ matrix.app.name }} path: ${{ matrix.app.working_dir }}/coverage @@ -419,23 +418,23 @@ jobs: STATEMENTS=$(jq '.total.statements.pct' coverage/coverage-summary.json) BRANCHES=$(jq '.total.branches.pct' coverage/coverage-summary.json) FUNCTIONS=$(jq '.total.functions.pct' coverage/coverage-summary.json) - - echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT - echo "statements=$STATEMENTS" >> $GITHUB_OUTPUT - echo "branches=$BRANCHES" >> $GITHUB_OUTPUT - echo "functions=$FUNCTIONS" >> $GITHUB_OUTPUT + + echo "coverage=$COVERAGE" >> "$GITHUB_OUTPUT" + echo "statements=$STATEMENTS" >> "$GITHUB_OUTPUT" + echo "branches=$BRANCHES" >> "$GITHUB_OUTPUT" + echo "functions=$FUNCTIONS" >> "$GITHUB_OUTPUT" echo "Total line coverage: $COVERAGE%" else - echo "coverage=0" >> $GITHUB_OUTPUT - echo "statements=0" >> $GITHUB_OUTPUT - echo "branches=0" >> $GITHUB_OUTPUT - echo "functions=0" >> $GITHUB_OUTPUT + echo "coverage=0" >> "$GITHUB_OUTPUT" + echo "statements=0" >> "$GITHUB_OUTPUT" + echo "branches=0" >> "$GITHUB_OUTPUT" + echo "functions=0" >> "$GITHUB_OUTPUT" echo "No coverage file found" fi - name: Post coverage comment if: github.event_name == 'pull_request' - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: github-token: ${{ secrets.MANAGE_TOKEN || github.token }} script: | @@ -522,10 +521,10 @@ jobs: app: ${{ fromJson(needs.detect-changes.outputs.matrix) }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: ${{ inputs.node_version }} cache: ${{ inputs.package_manager }} From 7abd6466ddcb1a191dc085afbb54e83f7a533530 Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Tue, 14 Apr 2026 15:45:42 -0300 Subject: [PATCH 4/5] fix(frontend-pr-analysis): group redirects and quote GITHUB_STEP_SUMMARY - Group GITHUB_OUTPUT writes with { } >> to satisfy SC2129 - Quote $GITHUB_STEP_SUMMARY to satisfy SC2086 --- .github/workflows/frontend-pr-analysis.yml | 40 +++++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/.github/workflows/frontend-pr-analysis.yml b/.github/workflows/frontend-pr-analysis.yml index 35adf098..236230d6 100644 --- a/.github/workflows/frontend-pr-analysis.yml +++ b/.github/workflows/frontend-pr-analysis.yml @@ -419,16 +419,20 @@ jobs: BRANCHES=$(jq '.total.branches.pct' coverage/coverage-summary.json) FUNCTIONS=$(jq '.total.functions.pct' coverage/coverage-summary.json) - echo "coverage=$COVERAGE" >> "$GITHUB_OUTPUT" - echo "statements=$STATEMENTS" >> "$GITHUB_OUTPUT" - echo "branches=$BRANCHES" >> "$GITHUB_OUTPUT" - echo "functions=$FUNCTIONS" >> "$GITHUB_OUTPUT" + { + echo "coverage=$COVERAGE" + echo "statements=$STATEMENTS" + echo "branches=$BRANCHES" + echo "functions=$FUNCTIONS" + } >> "$GITHUB_OUTPUT" echo "Total line coverage: $COVERAGE%" else - echo "coverage=0" >> "$GITHUB_OUTPUT" - echo "statements=0" >> "$GITHUB_OUTPUT" - echo "branches=0" >> "$GITHUB_OUTPUT" - echo "functions=0" >> "$GITHUB_OUTPUT" + { + echo "coverage=0" + echo "statements=0" + echo "branches=0" + echo "functions=0" + } >> "$GITHUB_OUTPUT" echo "No coverage file found" fi @@ -497,15 +501,17 @@ jobs: - name: Coverage summary run: | - echo "## Coverage Summary: ${{ matrix.app.name }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY - echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Lines | ${{ steps.coverage.outputs.coverage }}% |" >> $GITHUB_STEP_SUMMARY - echo "| Statements | ${{ steps.coverage.outputs.statements }}% |" >> $GITHUB_STEP_SUMMARY - echo "| Branches | ${{ steps.coverage.outputs.branches }}% |" >> $GITHUB_STEP_SUMMARY - echo "| Functions | ${{ steps.coverage.outputs.functions }}% |" >> $GITHUB_STEP_SUMMARY - echo "| Threshold | ${{ inputs.coverage_threshold }}% |" >> $GITHUB_STEP_SUMMARY + { + echo "## Coverage Summary: ${{ matrix.app.name }}" + echo "" + echo "| Metric | Value |" + echo "|--------|-------|" + echo "| Lines | ${{ steps.coverage.outputs.coverage }}% |" + echo "| Statements | ${{ steps.coverage.outputs.statements }}% |" + echo "| Branches | ${{ steps.coverage.outputs.branches }}% |" + echo "| Functions | ${{ steps.coverage.outputs.functions }}% |" + echo "| Threshold | ${{ inputs.coverage_threshold }}% |" + } >> "$GITHUB_STEP_SUMMARY" # ============================================ # BUILD VERIFICATION From 21a8667946622f83aae97d42acd84f7eb8e66cc6 Mon Sep 17 00:00:00 2001 From: Lucas Bedatty Date: Tue, 14 Apr 2026 15:51:10 -0300 Subject: [PATCH 5/5] fix(frontend-pr-analysis): mitigate code injection by mapping expressions to env vars Move ${{ }} expressions out of run: blocks into env: mappings to prevent potential code injection via user-controlled inputs (github.base_ref, matrix.app.name, coverage outputs, inputs.coverage_threshold). Resolves all 7 CodeQL code-injection/medium findings. --- .github/workflows/frontend-pr-analysis.yml | 45 ++++++++++++++-------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/.github/workflows/frontend-pr-analysis.yml b/.github/workflows/frontend-pr-analysis.yml index 236230d6..e8281867 100644 --- a/.github/workflows/frontend-pr-analysis.yml +++ b/.github/workflows/frontend-pr-analysis.yml @@ -108,20 +108,25 @@ jobs: - name: Get changed files id: changed shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BASE_REF: ${{ github.base_ref }} + EVENT_BEFORE: ${{ github.event.before }} + CURRENT_SHA: ${{ github.sha }} run: | - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - BASE_SHA="${{ github.event.pull_request.base.sha }}" - HEAD_SHA="${{ github.event.pull_request.head.sha }}" - git fetch origin "$BASE_SHA" --depth=1 2>/dev/null || true - FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" 2>/dev/null || git diff --name-only origin/${{ github.base_ref }}...HEAD) - elif [[ "${{ github.event.before }}" == "0000000000000000000000000000000000000000" ]] || [[ -z "${{ github.event.before }}" ]]; then + if [[ "$EVENT_NAME" == "pull_request" ]]; then + git fetch origin "$PR_BASE_SHA" --depth=1 2>/dev/null || true + FILES=$(git diff --name-only "$PR_BASE_SHA" "$PR_HEAD_SHA" 2>/dev/null || git diff --name-only "origin/${BASE_REF}...HEAD") + elif [[ "$EVENT_BEFORE" == "0000000000000000000000000000000000000000" ]] || [[ -z "$EVENT_BEFORE" ]]; then if PREV_COMMIT=$(git rev-parse HEAD^); then FILES=$(git diff --name-only "$PREV_COMMIT" HEAD) else FILES=$(git ls-tree -r --name-only HEAD) fi else - FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }}) + FILES=$(git diff --name-only "$EVENT_BEFORE" "$CURRENT_SHA") fi printf "files<> "$GITHUB_OUTPUT" @@ -491,26 +496,34 @@ jobs: - name: Check coverage threshold if: inputs.fail_on_coverage_threshold + env: + COVERAGE: ${{ steps.coverage.outputs.coverage }} + THRESHOLD: ${{ inputs.coverage_threshold }} run: | - COVERAGE=${{ steps.coverage.outputs.coverage }} - THRESHOLD=${{ inputs.coverage_threshold }} if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then - echo "::error::Coverage $COVERAGE% is below threshold $THRESHOLD%" + echo "::error::Coverage ${COVERAGE}% is below threshold ${THRESHOLD}%" exit 1 fi - name: Coverage summary + env: + APP_NAME: ${{ matrix.app.name }} + COV_LINES: ${{ steps.coverage.outputs.coverage }} + COV_STATEMENTS: ${{ steps.coverage.outputs.statements }} + COV_BRANCHES: ${{ steps.coverage.outputs.branches }} + COV_FUNCTIONS: ${{ steps.coverage.outputs.functions }} + COV_THRESHOLD: ${{ inputs.coverage_threshold }} run: | { - echo "## Coverage Summary: ${{ matrix.app.name }}" + echo "## Coverage Summary: ${APP_NAME}" echo "" echo "| Metric | Value |" echo "|--------|-------|" - echo "| Lines | ${{ steps.coverage.outputs.coverage }}% |" - echo "| Statements | ${{ steps.coverage.outputs.statements }}% |" - echo "| Branches | ${{ steps.coverage.outputs.branches }}% |" - echo "| Functions | ${{ steps.coverage.outputs.functions }}% |" - echo "| Threshold | ${{ inputs.coverage_threshold }}% |" + echo "| Lines | ${COV_LINES}% |" + echo "| Statements | ${COV_STATEMENTS}% |" + echo "| Branches | ${COV_BRANCHES}% |" + echo "| Functions | ${COV_FUNCTIONS}% |" + echo "| Threshold | ${COV_THRESHOLD}% |" } >> "$GITHUB_STEP_SUMMARY" # ============================================