Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions .github/actions/setup-sonarqube/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: 'Setup Ephemeral SonarQube'
description: 'Starts a SonarQube Enterprise container with PostgreSQL, restores a DB dump if available, or runs a fresh scan.'

inputs:
sq-version:
description: 'SonarQube version (9.9, 10.0, 10.4, 2025.1)'
required: true
sq-license-key:
description: 'SonarQube Enterprise license key'
required: true
db-dump-artifact:
description: 'Name of the DB dump artifact to restore (empty = fresh scan)'
required: false
default: ''

outputs:
sq-url:
description: 'URL of the running SonarQube instance'
value: ${{ steps.wait-healthy.outputs.sq-url }}
sq-token:
description: 'Admin token for the SonarQube instance'
value: ${{ steps.create-token.outputs.sq-token }}

runs:
using: 'composite'
steps:
- name: Create Docker network
shell: bash
run: docker network create sq-net 2>/dev/null || true

- name: Start PostgreSQL
shell: bash
run: |
docker run -d --name sq-postgres --network sq-net \
-e POSTGRES_USER=sonar \
-e POSTGRES_PASSWORD=sonar \
-e POSTGRES_DB=sonarqube \
postgres:15-alpine
echo "Waiting for PostgreSQL..."
for i in $(seq 1 30); do
docker exec sq-postgres pg_isready -U sonar && break
sleep 1
done

- name: Restore DB dump (if provided)
if: inputs.db-dump-artifact != ''
shell: bash
run: |
echo "Restoring DB dump from artifact..."
docker exec -i sq-postgres psql -U sonar -d sonarqube < /tmp/sq-dump-${{ inputs.sq-version }}.sql
echo "Verifying restore..."
ROW_COUNT=$(docker exec sq-postgres psql -U sonar -d sonarqube -t -c "SELECT count(*) FROM projects;" 2>/dev/null | tr -d ' ' || echo "0")
echo "Projects in DB: $ROW_COUNT"
if [ "$ROW_COUNT" = "0" ]; then
echo "ERROR: DB restore failed (0 projects found)"
exit 1
fi

- name: Start SonarQube Enterprise
shell: bash
run: |
docker run -d --name sonarqube --network sq-net \
-e SONAR_JDBC_URL=jdbc:postgresql://sq-postgres:5432/sonarqube \
-e SONAR_JDBC_USERNAME=sonar \
-e SONAR_JDBC_PASSWORD=sonar \
-e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
-e SONAR_WEB_JAVAADDITIONALOPTS="-Xmx2g" \
-p 9000:9000 \
sonarqube:${{ inputs.sq-version }}-enterprise
echo "SonarQube ${{ inputs.sq-version }} Enterprise starting..."

- name: Wait for healthy
id: wait-healthy
shell: bash
run: |
for i in $(seq 1 120); do
HEALTH=$(curl -s http://localhost:9000/api/system/status 2>/dev/null | grep -o '"status":"[^"]*"' | head -1 || echo "")
if echo "$HEALTH" | grep -q "UP"; then
echo "SonarQube is UP"
echo "sq-url=http://localhost:9000" >> "$GITHUB_OUTPUT"
break
fi
echo " Waiting for UP... ($i/120) status: $HEALTH"
sleep 2
done
if ! echo "$HEALTH" | grep -q "UP"; then
echo "ERROR: SonarQube did not become healthy within 240 seconds"
docker logs sonarqube --tail 50
exit 1
fi

- name: Inject license key
shell: bash
run: |
echo "Injecting Enterprise license..."
RESULT=$(curl -s -u admin:admin -X POST \
"http://localhost:9000/api/editions/set_license" \
-d "license=${{ inputs.sq-license-key }}")
if echo "$RESULT" | grep -q '"errors"'; then
echo "WARNING: License injection may have failed: $RESULT"
else
echo "License injected successfully"
fi

- name: Create admin token
id: create-token
shell: bash
run: |
TOKEN_RESPONSE=$(curl -s -u admin:admin \
-X POST "http://localhost:9000/api/user_tokens/generate" \
-d "name=regression-test-$(date +%s)" \
-d "type=GLOBAL_ANALYSIS_TOKEN")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong token type blocks enrichment script API access

High Severity

The token is created with type=GLOBAL_ANALYSIS_TOKEN, which is scoped exclusively to scanner analysis operations (submitting reports via /api/ce/submit). However, the enrichment scripts and migration tool need full user-level API access — including /api/issues/search, /api/issues/add_comment, /api/issues/do_transition, /api/hotspots/search, and /api/hotspots/change_status. A GLOBAL_ANALYSIS_TOKEN cannot perform these operations, causing 403 errors. The type needs to be USER_TOKEN (or omitted entirely, which defaults to USER_TOKEN).

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d751b0c. Configure here.

TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "$TOKEN" ]; then
echo "ERROR: Failed to create SQ token"
echo "$TOKEN_RESPONSE"
exit 1
fi
echo "sq-token=$TOKEN" >> "$GITHUB_OUTPUT"
echo "SQ admin token created"
131 changes: 131 additions & 0 deletions .github/workflows/regression-bug-fixes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Regression - Bug Fix Scenarios

on:
workflow_call:

concurrency:
group: regression-bug-fixes-${{ github.ref }}
cancel-in-progress: false

jobs:
bug-fix-scenarios:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 4
matrix:
scenario:
- name: large-project-10k-plus
issues: '#53, #94, #98'
command: migrate
- name: github-actions-language
issues: '#88'
command: migrate
- name: external-analyzers
issues: '#56, #82'
command: migrate
- name: issue-sync-first-migration
issues: '#91'
command: migrate
- name: kill-and-continue
issues: '#15, #57'
command: migrate
sq_version:
- id: '9.9'
secret_key: '9_9'
- id: '10.0'
secret_key: '10_0'
- id: '10.4'
secret_key: '10_4'
- id: '2025.1'
secret_key: '2025_1'

env:
SCENARIO: ${{ matrix.scenario.name }}
SQ_VERSION: ${{ matrix.sq_version.id }}
SQC_TARGET_KEY: cv-regression-${{ matrix.scenario.name }}-sq${{ matrix.sq_version.secret_key }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Restore dependencies
uses: ./.github/actions/restore-deps

- name: Setup ephemeral SonarQube
id: sq-setup
uses: ./.github/actions/setup-sonarqube
with:
sq-version: ${{ matrix.sq_version.id }}
sq-license-key: ${{ secrets[format('SONARQUBE_{0}_LICENSE_KEY', matrix.sq_version.secret_key)] }}

- name: Generate config
uses: ./.github/actions/generate-config
env:
SONARQUBE_URL: ${{ steps.sq-setup.outputs.sq-url }}
SONARQUBE_TOKEN: ${{ steps.sq-setup.outputs.sq-token }}
SONARCLOUD_URL: ${{ secrets.SONARCLOUD_URL }}
SONARCLOUD_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }}
SONARCLOUD_ORG_KEY: ${{ secrets.SONARCLOUD_ORG_KEY }}
SONARCLOUD_ENTERPRISE_KEY: ${{ secrets.SONARCLOUD_ENTERPRISE_KEY }}

- name: Patch config with target key
shell: bash
run: node test/regression/helpers/patch-config.js "$SQC_TARGET_KEY" "${{ steps.sq-setup.outputs.sq-url }}" "${{ steps.sq-setup.outputs.sq-token }}"

- name: Cleanup SQC target project
shell: bash
run: node test/regression/helpers/cleanup-project.js "$SQC_TARGET_KEY"

- name: Enrich test data - Add issue comments
if: success()
run: node test/regression/enrichment/add-issue-comments.js
env:
SQ_PROJECT_KEY: angular

- name: Enrich test data - Change issue statuses
if: success()
run: node test/regression/enrichment/change-issue-status.js
env:
SQ_PROJECT_KEY: angular

- name: Enrich test data - Add hotspot data
if: success()
run: node test/regression/enrichment/add-hotspot-data.js
env:
SQ_PROJECT_KEY: angular

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workflow enrichment fails on empty SonarQube instance

High Severity

The workflow starts a fresh, empty SonarQube instance (no db-dump-artifact is provided and no sonar-scanner step exists), yet the enrichment steps immediately query for issues in project angular which doesn't exist. Both add-issue-comments.js and change-issue-status.js unconditionally throw when zero issues are found, failing the step. Since all subsequent steps (migration, assertions) use the default if: success() condition, they're skipped — making all 20 matrix jobs non-functional.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d751b0c. Configure here.


- name: Run migration
if: matrix.scenario.name != 'kill-and-continue'
run: npm run ${{ matrix.scenario.command }} -- -c migrate-config.json --verbose

- name: Run migration (kill-and-continue)
if: matrix.scenario.name == 'kill-and-continue'
shell: bash
run: |
timeout --signal=SIGTERM 60s npm run migrate -- -c migrate-config.json --verbose || true
echo "Resuming after SIGTERM..."
npm run migrate -- -c migrate-config.json --verbose

- name: Assert results
run: node test/regression/assert-${{ matrix.scenario.name }}.js --config migrate-config.json

- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: regression-logs-${{ matrix.scenario.name }}-sq${{ matrix.sq_version.secret_key }}
path: migration-output/logs/
retention-days: 7

- name: Teardown containers
if: always()
shell: bash
run: |
docker stop sonarqube sq-postgres 2>/dev/null || true
docker rm sonarqube sq-postgres 2>/dev/null || true
7 changes: 6 additions & 1 deletion .github/workflows/regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ jobs:
uses: ./.github/workflows/regression-verify.yml
secrets: inherit

bug-fixes:
needs: [setup, lint]
uses: ./.github/workflows/regression-bug-fixes.yml
secrets: inherit

summary:
needs: [migrate, sync-metadata, verify]
needs: [migrate, sync-metadata, verify, bug-fixes]
uses: ./.github/workflows/regression-summary.yml
38 changes: 21 additions & 17 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🏗️ Architecture

<!-- Last updated: Apr 21, 2026 -->
<!-- Last updated: Apr 23, 2026 -->

<!-- Updated: Apr 21, 2026 -->
## 📁 Project Structure
Expand Down Expand Up @@ -74,13 +74,14 @@ src/
│ │ │ ├── build-windows.js # Initial window partitioning
│ │ │ ├── fetch-window.js # Fetch issues within a single window
│ │ │ └── merge-results.js # Deduplicate and merge sliced results
│ ├── batch-distributor/ # Issue batching for upload (5K per date bucket)
│ │ ├── index.js # shouldBatch + createBatchExtractedData orchestrator
│ ├── batch-distributor/ # SCM date-bucket distribution (5K per date bucket)
│ │ ├── index.js # Re-exports all batch-distributor helpers
│ │ ├── helpers/
│ │ │ ├── should-batch.js # Predicate: issues.length > 5000
│ │ │ ├── compute-batch-plan.js # Returns batch descriptors with start/end indices
│ │ │ ├── compute-batch-date.js # Computes backdated ISO date per batch
│ │ │ └── create-batch-extracted-data.js # Shallow-clones extracted data with sliced issues
│ │ │ ├── backdate-changesets.js # Core: modifies SCM blame dates to spread issue creation dates
│ │ │ ├── should-batch.js # ISSUE_BATCH_SIZE constant; shouldBatch() always returns false
│ │ │ ├── compute-batch-plan.js # Returns batch descriptors (legacy, multi-analysis path)
│ │ │ ├── compute-batch-date.js # Computes backdated ISO date per batch (30-day spacing)
│ │ │ └── create-batch-extracted-data.js # Shallow-clones extracted data (legacy)
│ ├── issue-sync/ # Shared issue sync utilities
│ │ ├── has-manual-changes.js # Detects human-authored changes on an SQ issue
│ │ ├── fetch-sq-changelogs.js # Batch-fetches SQ changelogs concurrently
Expand Down Expand Up @@ -130,7 +131,7 @@ sq-{version}/
│ └── fetch-and-sync-hotspots.js # Fetches SQ hotspots, syncs to SC
├── transfer-branch.js # Re-export → transfer-branch/index.js
├── transfer-branch/
│ ├── index.js # Orchestrates per-branch transfer; gates to batched path when issues > 5K
│ ├── index.js # Orchestrates per-branch transfer; calls backdateChangesets() before protobuf build
│ └── helpers/ # build-and-encode-report, upload-report, compute-branch-stats, transfer-branch-batched, ...
├── migrate-pipeline.js # Re-export → migrate-pipeline/index.js
├── migrate-pipeline/
Expand Down Expand Up @@ -248,21 +249,24 @@ sq-{version}/

**404 JS files** across the sq-10.4 pipeline, all ≤50 lines. Classes converted to factory functions (`createSonarQubeClient`, `createSonarCloudClient`, `createProtobufBuilder`, `createDataExtractor`) with thin class wrappers for backward compatibility.

<!-- Updated: 2026-04-22_14:30:00 -->
### Shared Utilities — Batch Distributor
<!-- Updated: 2026-04-23_14:46:00 -->
### Shared Utilities — SCM Date-Bucket Distribution

The **batch-distributor** (`src/shared/utils/batch-distributor/`) is a shared utility that splits large issue sets across multiple scanner report uploads to work around SonarCloud's Elasticsearch 10K-per-date-bucket visualization limit. Without batching, branches with more than 10,000 issues on a single analysis date would have issues hidden in the SonarCloud UI.
The **batch-distributor** (`src/shared/utils/batch-distributor/`) works around SonarCloud's Elasticsearch 10K-per-date-bucket visualization limit. When a branch has more than 5K issues, `backdateChangesets()` modifies SCM changeset blame dates within a **single analysis** so the CE assigns different creation dates to different groups of issues, spreading them across multiple date buckets.

It exposes four pure-function helpers:
> **Note:** Multi-analysis batching (separate scanner report uploads) was abandoned because SonarCloud's CE issue tracker treats each analysis as a complete snapshot — issues from prior analyses not in the current one are closed.

Key helpers:

| Function | Purpose |
|----------|---------|
| `shouldBatch(extractedData)` | Predicate — returns `true` when `issues.length > 5000` |
| `computeBatchPlan(totalIssues)` | Returns an array of batch descriptors, each with `startIndex`, `endIndex`, `batchIndex`, and `isLast` |
| `computeBatchDate(baseDateISO, batchIndex, totalBatches)` | Computes a backdated ISO date string per batch, stepping one day back per batch from the base date |
| `createBatchExtractedData(originalData, batchDescriptor, batchDate, batchScmRevisionId)` | Shallow-clones the extracted data with a sliced issues array and overridden metadata; non-final batches strip sources, changesets, and duplications to reduce upload size |
| `backdateChangesets(extractedData)` | Core: sorts issues by file, groups files into ≤5K batches, sets ALL lines of each file to the batch date. No-op when issues ≤ 5K. |
| `shouldBatch(extractedData)` | Always returns `false` (multi-analysis batching disabled). Exports `ISSUE_BATCH_SIZE` constant. |
| `computeBatchDate(baseDateISO, batchIndex, totalBatches)` | Computes a backdated ISO date per batch with 30-day spacing |
| `computeBatchPlan(totalIssues)` | Legacy: returns batch descriptors (used by disabled multi-analysis path) |
| `createBatchExtractedData(...)` | Legacy: shallow-clones extracted data (used by disabled multi-analysis path) |

**Integration:** All four pipeline versions (`sq-9.9`, `sq-10.0`, `sq-10.4`, `sq-2025`) integrate via a `shouldBatch` gate in `transferBranch`. When a branch has more than 5,000 issues, `transferBranch` computes a batch plan and uploads each batch as a separate scanner report with a unique backdated analysis date and `scmRevisionId`. Only the final batch includes sources, changesets, and duplications.
**Integration:** All 6 pipeline `transfer-branch` entry points call `backdateChangesets(extractedData)` before `buildProtobufMessages()`. The function mutates `extractedData.changesets` in place.

<!-- Updated: Mar 25, 2026 -->
## 🔄 Version Routing
Expand Down
Loading