From e6734bfa9d008754036a3cd6e8328acb29e7d189 Mon Sep 17 00:00:00 2001 From: Davide Ghiotto Date: Wed, 29 Oct 2025 23:53:07 +0100 Subject: [PATCH 1/8] Update README and CLI functionality for Sonar Autofixer - Revised README to clarify the tool's capabilities, including local SonarQube scans and AI editor integration. - Added a new interactive setup command to initialize project configuration. - Updated CLI commands to reflect changes in the fetching process and removed the deprecated `fetch-sonar-issues-github.js` file. - Enhanced the command structure to support local scanning and improved configuration management. --- README.md | 121 ++++++-- src/cli.js | 2 +- src/fetch-sonar-issues-github.js | 443 ------------------------------ src/fetch-sonar-issues.mjs | 241 ---------------- src/sonar-issue-extractor.js | 454 +++++++++++++++++++++++++++++++ src/versioning/index.js | 159 +++++++++++ 6 files changed, 713 insertions(+), 707 deletions(-) delete mode 100644 src/fetch-sonar-issues-github.js delete mode 100755 src/fetch-sonar-issues.mjs create mode 100644 src/sonar-issue-extractor.js create mode 100644 src/versioning/index.js diff --git a/README.md b/README.md index a0c6804..7ddb100 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sonar Autofixer -CLI utility for fetching SonarQube issues and integrating with Bitbucket/GitHub PR workflows. Automatically detects PR IDs from branches and fetches SonarQube issues for code quality analysis. +CLI utility for fetching SonarQube issues and running local SonarQube scans. Automatically detects PR IDs from branches and fetches SonarQube issues for code quality analysis. Includes AI editor integration for automated issue fixing. ## Installation @@ -40,65 +40,142 @@ Or install globally: npm install -g @davide97g/sonar-autofixer ``` +## Quick Start + +### 1. Initialize Configuration + +Run the interactive setup to configure your project: + +```bash +npx @davide97g/sonar-autofixer init +``` + +This will: + +- Create `.sonar/autofixer.config.json` with your project settings +- Add npm scripts to your `package.json` +- Create AI editor rules for automated issue fixing (Cursor, VSCode, Windsurf) + +### 2. Set Up Environment Variables + +Create a `.env` file in your project root: + +```env +# GitHub Configuration +GITHUB_TOKEN=your-github-token +GITHUB_OWNER=your-username +GITHUB_REPO=your-repo-name + +# SonarQube/SonarCloud Configuration +SONAR_TOKEN=your-sonar-token +SONAR_ORGANIZATION=your-organization +SONAR_COMPONENT_KEYS=your-project-key +SONAR_BASE_URL=https://sonarcloud.io/api/issues/search +``` + ## Usage -### As a CLI tool +### Commands -After installation, you can use the `sonar-fetch` command: +#### Fetch SonarQube Issues ```bash # Fetch issues for current branch (auto-detects PR) -sonar-fetch +npx @davide97g/sonar-autofixer fetch # Fetch issues for a specific branch -sonar-fetch my-branch +npx @davide97g/sonar-autofixer fetch my-branch # Fetch issues from a SonarQube PR link -sonar-fetch my-branch https://sonarqube.example.com/project/issues?id=project&pullRequest=PR_KEY +npx @davide97g/sonar-autofixer fetch my-branch https://sonarcloud.io/project/issues?id=project&pullRequest=PR_KEY ``` -### As an npm script +#### Run Local SonarQube Scan ```bash -npm run sonar:fetch [branch] [sonar-pr-link] +# Run local SonarQube scan +npx @davide97g/sonar-autofixer scan ``` -## Configuration +#### Initialize Configuration -Create a `.env` file in your project root with the following variables: +```bash +# Interactive setup +npx @davide97g/sonar-autofixer init +``` -```env -BITBUCKET_EMAIL=your-email@example.com -BITBUCKET_API_TOKEN=your-api-token -SONAR_BASE_URL=https://your-sonarqube.com/project/issues -BITBUCKET_BASE_URL=https://api.bitbucket.org/2.0/repositories/your-org/your-repo +### As npm Scripts + +After initialization, you can use the added npm scripts: + +```bash +# Fetch issues +npm run sonar:fetch + +# Run local scan +npm run sonar:scan ``` ## Features -- **Automatic PR Detection**: Automatically detects PR IDs from your current git branch using Bitbucket/GitHub API +- **Automatic PR Detection**: Automatically detects PR IDs from your current git branch using GitHub API - **Fallback Support**: Falls back to branch-based fetching if PR detection fails - **PR Link Support**: Directly fetch issues using a SonarQube PR link +- **Local Scanning**: Run SonarQube scans locally and save results +- **AI Editor Integration**: Creates rules for Cursor, VSCode, Windsurf for automated issue fixing - **Issue Summary**: Displays a summary of issues by severity after fetching +- **Configuration Management**: Interactive setup for easy configuration ## How It Works +### Fetch Command + 1. Detects the current git branch or uses provided branch name -2. Attempts to find associated PR using Bitbucket API or branch name pattern matching +2. Attempts to find associated PR using GitHub API or branch name pattern matching 3. Fetches SonarQube issues for the PR or branch 4. Saves issues to `.sonar/issues.json` 5. Displays a summary of fetched issues -## Output +### Scan Command + +1. Validates SonarQube token and configuration +2. Runs local SonarQube scanner +3. Saves results to `.sonar/scanner-report.json` +4. Provides detailed scan output + +### Init Command + +1. Prompts for project configuration (repo name, git provider, etc.) +2. Creates configuration file +3. Updates package.json with npm scripts +4. Creates AI editor rules based on your editor choice + +## Output Files + +- `.sonar/issues.json` - Fetched SonarQube issues in JSON format +- `.sonar/scanner-report.json` - Local scan results +- `.sonar/autofixer.config.json` - Project configuration +- `.cursor/rules/sonar-issue-fix.mdc` - Cursor AI rules (if selected) +- `.vscode/sonar-issue-fix.md` - VSCode rules (if selected) +- `.windsurf/rules/sonar-issue-fix.mdc` - Windsurf rules (if selected) + +## AI Editor Integration + +The tool creates specific rules for your chosen AI editor to help with automated SonarQube issue fixing: + +- **Cursor**: Creates `.cursor/rules/sonar-issue-fix.mdc` +- **VSCode with Copilot**: Creates `.vscode/sonar-issue-fix.md` +- **Windsurf**: Creates `.windsurf/rules/sonar-issue-fix.mdc` -The script saves fetched issues to `.sonar/issues.json` in your project root. This file contains all SonarQube issues in JSON format, ready for further processing or analysis. +These rules provide patterns and priorities for fixing common SonarQube issues. ## Requirements -- Node.js (v14 or higher) +- Node.js (v18 or higher) - Git repository -- Bitbucket/GitHub API token with appropriate permissions -- SonarQube access +- GitHub API token with appropriate permissions +- SonarQube/SonarCloud access +- SonarQube Scanner (for local scans): `npm install -g @sonar/scan` ## License diff --git a/src/cli.js b/src/cli.js index 3c515ca..55ad43b 100755 --- a/src/cli.js +++ b/src/cli.js @@ -41,7 +41,7 @@ program .description("Fetch Sonar issues and save to .sonar/issues.json") .allowExcessArguments(true) .action(() => { - runNodeScript("./fetch-sonar-issues-github.js", process.argv.slice(3)); + runNodeScript("./versioning/index.js", process.argv.slice(3)); }); program diff --git a/src/fetch-sonar-issues-github.js b/src/fetch-sonar-issues-github.js deleted file mode 100644 index 10d83dd..0000000 --- a/src/fetch-sonar-issues-github.js +++ /dev/null @@ -1,443 +0,0 @@ -#!/usr/bin/env node - -import dotenv from "dotenv"; -import { execSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -dotenv.config(); - -const githubToken = process.env.GITHUB_TOKEN; -const sonarToken = process.env.SONAR_TOKEN; -const sonarBaseUrlRaw = - process.env.SONAR_BASE_URL || "https://sonarcloud.io/api/issues/search"; -// Ensure we're using the API endpoint, not the web UI endpoint -const sonarBaseUrl = sonarBaseUrlRaw - .replace(/\/project\/issues\/search$/, "/api/issues/search") - .replace(/\/project\/issues$/, "/api/issues/search"); -const sonarComponentKeys = process.env.SONAR_COMPONENT_KEYS; -const sonarOrganization = process.env.SONAR_ORGANIZATION; -const githubOwner = process.env.GITHUB_OWNER; -const githubRepo = process.env.GITHUB_REPO; -const githubBaseUrl = process.env.GITHUB_API_URL || "https://api.github.com"; - -// SonarCloud/SonarQube API authentication -// According to SonarQube API docs: https://next.sonarqube.com/sonarqube/web_api/api/issues/search -// Authentication can be done via Basic Auth (token:empty) or token parameter -const getSonarAuthHeaders = (token) => { - if (!token) return {}; - - // SonarCloud/SonarQube typically uses Basic Auth: token as username, empty password - // Format: Authorization: Basic base64(token:) - const tokenString = `${token}:`; - const basicAuth = `Basic ${Buffer.from(tokenString).toString("base64")}`; - - return { - Authorization: basicAuth, - }; -}; - -/** - * Validates required configuration environment variables - * @throws {Error} If any required configuration is missing - */ -const validateConfiguration = () => { - if (!githubToken) { - throw new Error("GITHUB_TOKEN environment variable is required"); - } - if (!githubOwner || !githubRepo) { - throw new Error( - "GITHUB_OWNER and GITHUB_REPO environment variables are required" - ); - } - if (!sonarComponentKeys) { - throw new Error("SONAR_COMPONENT_KEYS environment variable is required"); - } - if (!sonarOrganization) { - throw new Error("SONAR_ORGANIZATION environment variable is required"); - } - if (!sonarToken) { - throw new Error( - "SONAR_TOKEN environment variable is required for SonarCloud API authentication" - ); - } -}; - -/** - * Automatically detects PR number from current branch using GitHub API - * @param {string} branch - Current git branch name - * @returns {Promise} PR number if found, null otherwise - */ -const detectPrId = async (branch) => { - try { - // Try to get PR number from GitHub API using the branch name - const githubApiUrl = `${githubBaseUrl}/repos/${githubOwner}/${githubRepo}/pulls?head=${githubOwner}:${branch}&state=open`; - console.log(`šŸ” Checking for PR associated with branch: ${branch}`); - const response = await fetch(githubApiUrl, { - headers: { - Authorization: `token ${githubToken}`, - Accept: "application/vnd.github.v3+json", - }, - }); - - if (response.ok) { - const data = await response.json(); - if (Array.isArray(data) && data.length > 0) { - const prNumber = data[0].number; - console.log(`āœ… Found PR #${prNumber} for branch: ${branch}`); - return prNumber.toString(); - } - } - - // Also check closed PRs in case the branch is merged - const closedPrUrl = `${githubBaseUrl}/repos/${githubOwner}/${githubRepo}/pulls?head=${githubOwner}:${branch}&state=all`; - const closedResponse = await fetch(closedPrUrl, { - headers: { - Authorization: `token ${githubToken}`, - Accept: "application/vnd.github.v3+json", - }, - }); - - if (closedResponse.ok) { - const closedData = await closedResponse.json(); - if (Array.isArray(closedData) && closedData.length > 0) { - const prNumber = closedData[0].number; - console.log( - `āœ… Found PR #${prNumber} for branch: ${branch} (closed/merged)` - ); - return prNumber.toString(); - } - } - - // Fallback: try to extract PR number from branch name if it follows a pattern like "feat/BAT-1234" or "pr/1234" - const prNumberRegex = - /(?:pr\/|PR\/|pull\/|PULL\/)(\d+)|(?:feat\/|feature\/|fix\/|bugfix\/).*?(\d+)/i; - const prNumberMatch = prNumberRegex.exec(branch); - if (prNumberMatch) { - const prNumber = prNumberMatch[1] || prNumberMatch[2]; - console.log(`āœ… Extracted PR #${prNumber} from branch name: ${branch}`); - return prNumber; - } - - console.log(`āš ļø No PR found for branch: ${branch}`); - return null; - } catch (error) { - console.log(`āš ļø Could not detect PR ID: ${error.message}`); - return null; - } -}; - -/** - * Ensures .sonar directory exists and saves issues to file - * @param {Object} issues - Issues data to save - * @param {string} usedSource - Source description for logging - */ -const saveIssuesToFile = (issues, usedSource) => { - const sonarDir = path.join(process.cwd(), ".sonar"); - if (!fs.existsSync(sonarDir)) { - fs.mkdirSync(sonarDir, { recursive: true }); - } - - const issuesPath = path.join(sonarDir, "issues.json"); - fs.writeFileSync(issuesPath, JSON.stringify(issues, null, 2)); - - console.log( - `āœ… Successfully fetched ${ - issues.issues?.length || 0 - } issues (source: ${usedSource})` - ); - console.log(`šŸ“ Saved to: ${issuesPath}`); -}; - -/** - * Displays summary of issues by severity - * @param {Object} issues - Issues data - */ -const displayIssuesSummary = (issues) => { - if (!issues.issues || issues.issues.length === 0) { - return; - } - - const severityCounts = {}; - for (const issue of issues.issues) { - const severity = issue.severity || "UNKNOWN"; - severityCounts[severity] = (severityCounts[severity] || 0) + 1; - } - - console.log("\nšŸ“Š Issues by severity:"); - const sortedEntries = Object.entries(severityCounts).sort( - ([, a], [, b]) => b - a - ); - for (const [severity, count] of sortedEntries) { - console.log(` ${severity}: ${count}`); - } -}; - -/** - * Builds URL for fetching issues by branch - * @param {string} branch - Branch name - * @returns {string} URL for fetching issues - */ -const buildUrlForBranch = (branch) => { - const params = new URLSearchParams({ - s: "FILE_LINE", - issueStatuses: "OPEN,CONFIRMED", - ps: "100", - facets: "impactSoftwareQualities,impactSeverities", - componentKeys: sonarComponentKeys, - organization: sonarOrganization, - branch: branch, - additionalFields: "_all", - }); - - let url = sonarBaseUrl; - if (!url.includes("/api/issues/search")) { - url = url.replace(/\/project\/issues[^/]*$/, "").replace(/\/$/, ""); - url = url.replace(/\/api\/issues$/, ""); - url = `${url}/api/issues/search`; - } - - return `${url}?${params.toString()}`; -}; - -/** - * Builds URL for fetching issues by PR link - * @param {string} prLink - SonarCloud PR link - * @returns {string} URL for fetching issues - */ -const buildUrlForPr = (prLink) => { - const prKeyMatch = /pullRequest=([^&]+)/.exec(prLink); - if (!prKeyMatch) { - throw new Error( - "Invalid SonarCloud/SonarQube PR link format. Expected format: https://sonarcloud.io/project/issues?id=project&pullRequest=PR_KEY" - ); - } - - const prKey = prKeyMatch[1]; - const params = new URLSearchParams({ - s: "FILE_LINE", - issueStatuses: "OPEN,CONFIRMED", - ps: "100", - facets: "impactSoftwareQualities,impactSeverities", - componentKeys: sonarComponentKeys, - organization: sonarOrganization, - pullRequest: prKey, - additionalFields: "_all", - }); - - let url = sonarBaseUrl; - if (!url.includes("/api/issues/search")) { - url = url.replace(/\/project\/issues[^/]*$/, "").replace(/\/$/, ""); - url = url.replace(/\/api\/issues$/, ""); - url = `${url}/api/issues/search`; - } - - return `${url}?${params.toString()}`; -}; - -/** - * Builds URL for fetching issues by PR ID - * @param {string} prId - PR ID - * @returns {string} URL for fetching issues - */ -const buildUrlForPrId = (prId) => { - const params = new URLSearchParams({ - s: "FILE_LINE", - issueStatuses: "OPEN,CONFIRMED", - ps: "100", - facets: "impactSoftwareQualities,impactSeverities", - componentKeys: sonarComponentKeys, - organization: sonarOrganization, - pullRequest: prId, - additionalFields: "_all", - }); - - let url = sonarBaseUrl; - if (!url.includes("/api/issues/search")) { - url = url.replace(/\/project\/issues[^/]*$/, "").replace(/\/$/, ""); - url = url.replace(/\/api\/issues$/, ""); - url = `${url}/api/issues/search`; - } - - return `${url}?${params.toString()}`; -}; - -/** - * Handles SonarQube API response - * @param {Response} response - Fetch response - * @returns {Promise} Parsed JSON response - */ -const handleSonarResponse = async (response) => { - const contentType = response.headers.get("content-type"); - if (!contentType?.includes("application/json")) { - const errorText = await response.text(); - console.error(`Unexpected content type: ${contentType}`); - console.error(`Response preview: ${errorText.substring(0, 500)}`); - throw new Error( - `Expected JSON but got ${contentType}. Check authentication and API endpoint. Status: ${response.status}` - ); - } - - if (!response.ok) { - const errorText = await response.text(); - throw new Error( - `HTTP error! status: ${response.status}, body: ${errorText.substring( - 0, - 200 - )}` - ); - } - - return await response.json(); -}; - -/** - * Fetches issues for a branch - * @param {string} branch - Branch name - * @returns {Promise} Issues data - */ -const fetchIssuesForBranch = async (branch) => { - const url = buildUrlForBranch(branch); - console.log( - `Fetching issues from: ${url.replace(/sonarToken=[^&]+/, "sonarToken=***")}` - ); - - const authHeaders = getSonarAuthHeaders(sonarToken); - const response = await fetch(url, { - headers: { - ...authHeaders, - "Content-Type": "application/json", - Accept: "application/json", - }, - }); - - return await handleSonarResponse(response); -}; - -/** - * Fetches issues for a PR link - * @param {string} prLink - SonarCloud PR link - * @returns {Promise} Issues data - */ -const fetchIssuesForPr = async (prLink) => { - const url = buildUrlForPr(prLink); - console.log( - `Fetching issues from PR: ${url.replace( - /sonarToken=[^&]+/, - "sonarToken=***" - )}` - ); - - const authHeaders = getSonarAuthHeaders(sonarToken); - const response = await fetch(url, { - headers: { - ...authHeaders, - "Content-Type": "application/json", - Accept: "application/json", - }, - }); - - return await handleSonarResponse(response); -}; - -/** - * Fetches issues for a PR ID - * @param {string} prId - PR ID - * @returns {Promise} Issues data - */ -const fetchIssuesForPrId = async (prId) => { - const url = buildUrlForPrId(prId); - console.log(`Fetching issues from PR ID: ${prId}`); - console.log(`URL: ${url.replace(/sonarToken=[^&]+/, "sonarToken=***")}`); - - const authHeaders = getSonarAuthHeaders(sonarToken); - const response = await fetch(url, { - headers: { - ...authHeaders, - "Content-Type": "application/json", - Accept: "application/json", - }, - }); - - return await handleSonarResponse(response); -}; - -/** - * Fetches issues based on the provided source (PR link, detected PR ID, or branch) - * @param {string|null} sonarPrLink - Optional SonarCloud PR link - * @param {string} currentBranch - Current git branch name - * @returns {Promise<{issues: Object, usedSource: string}>} Issues data and source description - */ -const fetchIssuesFromSource = async (sonarPrLink, currentBranch) => { - if (sonarPrLink) { - console.log(`Using provided SonarQube PR link: ${sonarPrLink}`); - const issues = await fetchIssuesForPr(sonarPrLink); - return { issues, usedSource: `PR: ${sonarPrLink}` }; - } - - const detectedPrId = await detectPrId(currentBranch); - if (detectedPrId) { - console.log(`šŸš€ Using automatically detected PR ID: ${detectedPrId}`); - const issues = await fetchIssuesForPrId(detectedPrId); - return { - issues, - usedSource: `PR #${detectedPrId} (auto-detected from branch: ${currentBranch})`, - }; - } - - console.log("šŸ“‹ No PR detected, falling back to branch-based approach"); - let issues = await fetchIssuesForBranch(currentBranch); - let usedSource = currentBranch; - - if (!issues.issues || issues.issues.length === 0) { - console.log( - "No issues found for current branch. Falling back to branch: develop" - ); - issues = await fetchIssuesForBranch("develop"); - usedSource = "develop"; - } - - return { issues, usedSource }; -}; - -/** - * Fetches SonarCloud issues for the current git branch and saves them to .sonar/issues.json. - * If the current branch has 0 issues, it falls back to fetching from "develop". - * Can automatically detect PR ID from current branch using GitHub API or use provided SonarCloud PR link. - * - * Usage: node fetch-sonar-issues-github.js [branch-name] [sonar-pr-link] - * - * @param {string} sonarPrLink - Optional SonarCloud PR link to fetch issues from - * @param {string} branchName - Optional branch name to use instead of current branch - */ -async function fetchSonarIssues(sonarPrLink = null, branchName = null) { - try { - // Get current git branch - const currentBranch = - branchName || - execSync("git branch --show-current", { - encoding: "utf8", - }).trim(); - console.log(`Current branch: ${currentBranch}`); - - validateConfiguration(); - - const { issues, usedSource } = await fetchIssuesFromSource( - sonarPrLink, - currentBranch - ); - - saveIssuesToFile(issues, usedSource); - displayIssuesSummary(issues); - } catch (error) { - console.error("āŒ Error fetching SonarQube issues:", error.message); - process.exit(1); - } -} - -// Parse command line arguments -const args = process.argv.slice(2); -const branchName = args[0] || null; -const sonarPrLink = args[1] || null; - -// Use top-level await -await fetchSonarIssues(sonarPrLink, branchName); diff --git a/src/fetch-sonar-issues.mjs b/src/fetch-sonar-issues.mjs deleted file mode 100755 index f7663c6..0000000 --- a/src/fetch-sonar-issues.mjs +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env node - -import { execSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -import dotenv from "dotenv"; -dotenv.config(); - -const email = process.env.BITBUCKET_EMAIL; -const apiToken = process.env.BITBUCKET_API_TOKEN; -const sonarBaseUrl = process.env.SONAR_BASE_URL; -const bitbucketBaseUrl = process.env.BITBUCKET_BASE_URL; - -const auth = Buffer.from(`${email}:${apiToken}`).toString("base64"); - -/** - * Fetches SonarQube issues for the current git branch and saves them to .sonar/issues.json. - * If the current branch has 0 issues, it falls back to fetching from "develop". - * Can automatically detect PR ID from current branch using Bitbucket API or use provided SonarQube PR link. - * - * Usage: node fetch-sonar-issues.js [sonar-pr-link] - */ -try { - // Parse command line arguments - const args = process.argv.slice(2); - const branchName = args[0] || null; - const sonarPrLink = args[1] || null; - - // Get current git branch - const currentBranch = - branchName || - execSync("git branch --show-current", { - encoding: "utf8", - }).trim(); - console.log(`Current branch: ${currentBranch}`); - - /** - * Automatically detects PR ID from current branch using Bitbucket API - * @param {string} branch - Current git branch name - * @returns {Promise} PR ID if found, null otherwise - */ - const detectPrId = async (branch) => { - try { - // Try to get PR number from Bitbucket API using the branch name - const bitbucketApiUrl = `${bitbucketBaseUrl}/pullrequests?q=source.branch.name="${branch}"`; - console.log(`šŸ” Checking for PR associated with branch: ${branch}`); - const response = await fetch(bitbucketApiUrl, { - headers: { - Authorization: `Basic ${auth}`, - }, - }); - if (response.ok) { - const data = await response.json(); - if (data.values && data.values.length > 0) { - const prNumber = data.values[0].id; - console.log(`āœ… Found PR #${prNumber} for branch: ${branch}`); - return prNumber.toString(); - } - } - - // Fallback: try to extract PR number from branch name if it follows a pattern like "feat/BAT-1234" or "pr/1234" - const prNumberRegex = /(?:pr\/|PR\/|pull\/|PULL\/)(\d+)|(?:feat\/|feature\/|fix\/|bugfix\/).*?(\d+)/i; - const prNumberMatch = prNumberRegex.exec(branch); - if (prNumberMatch) { - const prNumber = prNumberMatch[1] || prNumberMatch[2]; - console.log( - `āœ… Extracted PR #${prNumber} from branch name: ${branch}` - ); - return prNumber; - } - - console.log(`āš ļø No PR found for branch: ${branch}`); - return null; - } catch (error) { - console.log(`āš ļø Could not detect PR ID: ${error.message}`); - return null; - } - }; - - const buildUrlForBranch = (branch) => { - const params = new URLSearchParams({ - branch, - components: "bat", - s: "FILE_LINE", - inNewCodePeriod: "true", - issueStatuses: "CONFIRMED,OPEN", - ps: "100", - facets: - "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", - additionalFields: "_all", - timeZone: "Europe/Rome", - }); - return `${sonarBaseUrl}?${params.toString()}`; - }; - - const buildUrlForPr = (prLink) => { - // Extract PR key from the SonarQube PR link - // Expected format: https://eyecare-sonarqube.luxgroup.net/project/issues?id=bat&pullRequest=PR_KEY - const prKeyMatch = prLink.match(/pullRequest=([^&]+)/); - if (!prKeyMatch) { - throw new Error( - "Invalid SonarQube PR link format. Expected format: https://eyecare-sonarqube.luxgroup.net/project/issues?id=bat&pullRequest=PR_KEY" - ); - } - - const prKey = prKeyMatch[1]; - const params = new URLSearchParams({ - pullRequest: prKey, - components: "bat", - s: "FILE_LINE", - inNewCodePeriod: "true", - issueStatuses: "CONFIRMED,OPEN", - ps: "100", - facets: - "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", - additionalFields: "_all", - timeZone: "Europe/Rome", - }); - return `${sonarBaseUrl}?${params.toString()}`; - }; - - const buildUrlForPrId = (prId) => { - const params = new URLSearchParams({ - pullRequest: prId, - components: "bat", - s: "FILE_LINE", - inNewCodePeriod: "true", - issueStatuses: "CONFIRMED,OPEN", - ps: "100", - facets: - "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", - additionalFields: "_all", - timeZone: "Europe/Rome", - }); - return `${sonarBaseUrl}?${params.toString()}`; - }; - - const fetchIssuesForBranch = async (branch) => { - const url = buildUrlForBranch(branch); - console.log(`Fetching issues from: ${url}`); - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const json = await response.json(); - return json; - }; - - const fetchIssuesForPr = async (prLink) => { - const url = buildUrlForPr(prLink); - console.log(`Fetching issues from PR: ${url}`); - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const json = await response.json(); - return json; - }; - - const fetchIssuesForPrId = async (prId) => { - const url = buildUrlForPrId(prId); - console.log(`Fetching issues from PR ID: ${prId}`); - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const json = await response.json(); - return json; - }; - - let issues; - let usedSource; - - if (sonarPrLink) { - // If PR link is provided, fetch issues from that PR - console.log(`Using provided SonarQube PR link: ${sonarPrLink}`); - issues = await fetchIssuesForPr(sonarPrLink); - usedSource = `PR: ${sonarPrLink}`; - } else { - // Try to automatically detect PR ID from current branch - const detectedPrId = await detectPrId(currentBranch); - - if (detectedPrId) { - // Use detected PR ID - console.log(`šŸš€ Using automatically detected PR ID: ${detectedPrId}`); - issues = await fetchIssuesForPrId(detectedPrId); - usedSource = `PR #${detectedPrId} (auto-detected from branch: ${currentBranch})`; - } else { - // Fallback to branch-based approach - console.log("šŸ“‹ No PR detected, falling back to branch-based approach"); - issues = await fetchIssuesForBranch(currentBranch); - usedSource = currentBranch; - - // Fallback to develop if no issues found - if (!issues.issues || issues.issues.length === 0) { - console.log( - "No issues found for current branch. Falling back to branch: develop" - ); - issues = await fetchIssuesForBranch("develop"); - usedSource = "develop"; - } - } - } - - // Ensure .sonar directory exists - const sonarDir = path.join(process.cwd(), ".sonar"); - if (!fs.existsSync(sonarDir)) { - fs.mkdirSync(sonarDir, { recursive: true }); - } - - // Save issues to file - const issuesPath = path.join(sonarDir, "issues.json"); - fs.writeFileSync(issuesPath, JSON.stringify(issues, null, 2)); - - console.log( - `āœ… Successfully fetched ${ - issues.issues?.length || 0 - } issues (source: ${usedSource})` - ); - console.log(`šŸ“ Saved to: ${issuesPath}`); - - // Display summary - if (issues.issues && issues.issues.length > 0) { - const severityCounts = {}; - for (const issue of issues.issues) { - const severity = issue.severity || "UNKNOWN"; - severityCounts[severity] = (severityCounts[severity] || 0) + 1; - } - - console.log("\nšŸ“Š Issues by severity:"); - const sortedEntries = Object.entries(severityCounts).sort( - ([, a], [, b]) => b - a - ); - for (const [severity, count] of sortedEntries) { - console.log(` ${severity}: ${count}`); - } - } -} catch (error) { - console.error("āŒ Error fetching SonarQube issues:", error.message); - process.exit(1); -} diff --git a/src/sonar-issue-extractor.js b/src/sonar-issue-extractor.js new file mode 100644 index 0000000..2d470f6 --- /dev/null +++ b/src/sonar-issue-extractor.js @@ -0,0 +1,454 @@ +import dotenv from "dotenv"; + +dotenv.config(); + +/** + * SonarIssueExtractor - Handles all SonarQube API interactions and PR detection + */ +export class SonarIssueExtractor { + constructor() { + // GitHub configuration + this.githubToken = process.env.GITHUB_TOKEN; + this.githubOwner = process.env.GITHUB_OWNER; + this.githubRepo = process.env.GITHUB_REPO; + this.githubBaseUrl = process.env.GITHUB_API_URL || "https://api.github.com"; + + // Bitbucket configuration + this.bitbucketEmail = process.env.BITBUCKET_EMAIL; + this.bitbucketApiToken = process.env.BITBUCKET_API_TOKEN; + this.bitbucketBaseUrl = process.env.BITBUCKET_BASE_URL; + + // SonarQube configuration + this.sonarToken = process.env.SONAR_TOKEN; + this.sonarBaseUrlRaw = + process.env.SONAR_BASE_URL || "https://sonarcloud.io/api/issues/search"; + this.sonarComponentKeys = process.env.SONAR_COMPONENT_KEYS; + this.sonarOrganization = process.env.SONAR_ORGANIZATION; + } + + /** + * Gets SonarQube authentication headers + * @param {string} token - SonarQube token + * @returns {Object} Authentication headers + */ + getSonarAuthHeaders(token) { + if (!token) return {}; + + // SonarCloud/SonarQube typically uses Basic Auth: token as username, empty password + const tokenString = `${token}:`; + const basicAuth = `Basic ${Buffer.from(tokenString).toString("base64")}`; + + return { + Authorization: basicAuth, + }; + } + + /** + * Gets Bitbucket authentication headers + * @returns {Object} Authentication headers + */ + getBitbucketAuthHeaders() { + if (!this.bitbucketEmail || !this.bitbucketApiToken) { + throw new Error("BITBUCKET_EMAIL and BITBUCKET_API_TOKEN are required"); + } + + const auth = Buffer.from( + `${this.bitbucketEmail}:${this.bitbucketApiToken}` + ).toString("base64"); + return { + Authorization: `Basic ${auth}`, + }; + } + + /** + * Normalizes SonarQube base URL to API endpoint + * @param {string} baseUrl - Raw base URL + * @returns {string} Normalized API URL + */ + normalizeSonarUrl(baseUrl) { + let url = baseUrl; + if (!url.includes("/api/issues/search")) { + url = url.replace(/\/project\/issues[^/]*$/, "").replace(/\/$/, ""); + url = url.replace(/\/api\/issues$/, ""); + url = `${url}/api/issues/search`; + } + return url; + } + + /** + * Detects GitHub PR ID from branch name + * @param {string} branch - Branch name + * @returns {Promise} PR ID if found, null otherwise + */ + async detectGitHubPrId(branch) { + try { + if (!this.githubToken || !this.githubOwner || !this.githubRepo) { + console.log("āš ļø GitHub configuration missing, skipping PR detection"); + return null; + } + + // Try to get PR number from GitHub API using the branch name + const githubApiUrl = `${this.githubBaseUrl}/repos/${this.githubOwner}/${this.githubRepo}/pulls?head=${this.githubOwner}:${branch}&state=open`; + console.log(`šŸ” Checking for PR associated with branch: ${branch}`); + + const response = await fetch(githubApiUrl, { + headers: { + Authorization: `token ${this.githubToken}`, + Accept: "application/vnd.github.v3+json", + }, + }); + + if (response.ok) { + const data = await response.json(); + if (Array.isArray(data) && data.length > 0) { + const prNumber = data[0].number; + console.log(`āœ… Found PR #${prNumber} for branch: ${branch}`); + return prNumber.toString(); + } + } + + // Also check closed PRs in case the branch is merged + const closedPrUrl = `${this.githubBaseUrl}/repos/${this.githubOwner}/${this.githubRepo}/pulls?head=${this.githubOwner}:${branch}&state=all`; + const closedResponse = await fetch(closedPrUrl, { + headers: { + Authorization: `token ${this.githubToken}`, + Accept: "application/vnd.github.v3+json", + }, + }); + + if (closedResponse.ok) { + const closedData = await closedResponse.json(); + if (Array.isArray(closedData) && closedData.length > 0) { + const prNumber = closedData[0].number; + console.log( + `āœ… Found PR #${prNumber} for branch: ${branch} (closed/merged)` + ); + return prNumber.toString(); + } + } + + // Fallback: try to extract PR number from branch name + const prNumberRegex = + /(?:pr\/|PR\/|pull\/|PULL\/)(\d+)|(?:feat\/|feature\/|fix\/|bugfix\/).*?(\d+)/i; + const prNumberMatch = prNumberRegex.exec(branch); + if (prNumberMatch) { + const prNumber = prNumberMatch[1] || prNumberMatch[2]; + console.log(`āœ… Extracted PR #${prNumber} from branch name: ${branch}`); + return prNumber; + } + + console.log(`āš ļø No PR found for branch: ${branch}`); + return null; + } catch (error) { + console.log(`āš ļø Could not detect GitHub PR ID: ${error.message}`); + return null; + } + } + + /** + * Detects Bitbucket PR ID from branch name + * @param {string} branch - Branch name + * @returns {Promise} PR ID if found, null otherwise + */ + async detectBitbucketPrId(branch) { + try { + if ( + !this.bitbucketEmail || + !this.bitbucketApiToken || + !this.bitbucketBaseUrl + ) { + console.log( + "āš ļø Bitbucket configuration missing, skipping PR detection" + ); + return null; + } + + // Try to get PR number from Bitbucket API using the branch name + const bitbucketApiUrl = `${this.bitbucketBaseUrl}/pullrequests?q=source.branch.name="${branch}"`; + console.log(`šŸ” Checking for PR associated with branch: ${branch}`); + + const response = await fetch(bitbucketApiUrl, { + headers: this.getBitbucketAuthHeaders(), + }); + + if (response.ok) { + const data = await response.json(); + if (data.values && data.values.length > 0) { + const prNumber = data.values[0].id; + console.log(`āœ… Found PR #${prNumber} for branch: ${branch}`); + return prNumber.toString(); + } + } + + // Fallback: try to extract PR number from branch name + const prNumberRegex = + /(?:pr\/|PR\/|pull\/|PULL\/)(\d+)|(?:feat\/|feature\/|fix\/|bugfix\/).*?(\d+)/i; + const prNumberMatch = prNumberRegex.exec(branch); + if (prNumberMatch) { + const prNumber = prNumberMatch[1] || prNumberMatch[2]; + console.log(`āœ… Extracted PR #${prNumber} from branch name: ${branch}`); + return prNumber; + } + + console.log(`āš ļø No PR found for branch: ${branch}`); + return null; + } catch (error) { + console.log(`āš ļø Could not detect Bitbucket PR ID: ${error.message}`); + return null; + } + } + + /** + * Handles SonarQube API response + * @param {Response} response - Fetch response + * @returns {Promise} Parsed JSON response + */ + async handleSonarResponse(response) { + const contentType = response.headers.get("content-type"); + if (!contentType?.includes("application/json")) { + const errorText = await response.text(); + console.error(`Unexpected content type: ${contentType}`); + console.error(`Response preview: ${errorText.substring(0, 500)}`); + throw new Error( + `Expected JSON but got ${contentType}. Check authentication and API endpoint. Status: ${response.status}` + ); + } + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `HTTP error! status: ${response.status}, body: ${errorText.substring( + 0, + 200 + )}` + ); + } + + return await response.json(); + } + + /** + * Builds URL for fetching issues by branch + * @param {string} branch - Branch name + * @param {Object} config - Configuration object + * @returns {string} URL for fetching issues + */ + buildUrlForBranch(branch, config) { + const sonarBaseUrl = this.normalizeSonarUrl( + config.sonarBaseUrl || this.sonarBaseUrlRaw + ); + + // Check if this is a SonarCloud setup (has organization and componentKeys) + if ( + config.publicSonar && + this.sonarComponentKeys && + this.sonarOrganization + ) { + const params = new URLSearchParams({ + s: "FILE_LINE", + issueStatuses: "OPEN,CONFIRMED", + ps: "100", + facets: "impactSoftwareQualities,impactSeverities", + componentKeys: this.sonarComponentKeys, + organization: this.sonarOrganization, + branch: branch, + additionalFields: "_all", + }); + return `${sonarBaseUrl}?${params.toString()}`; + } else { + // SonarQube setup (private instance) + const params = new URLSearchParams({ + branch, + components: "bat", + s: "FILE_LINE", + inNewCodePeriod: "true", + issueStatuses: "CONFIRMED,OPEN", + ps: "100", + facets: + "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", + additionalFields: "_all", + timeZone: "Europe/Rome", + }); + return `${sonarBaseUrl}?${params.toString()}`; + } + } + + /** + * Builds URL for fetching issues by PR link + * @param {string} prLink - SonarQube PR link + * @param {Object} config - Configuration object + * @returns {string} URL for fetching issues + */ + buildUrlForPr(prLink, config) { + const sonarBaseUrl = this.normalizeSonarUrl( + config.sonarBaseUrl || this.sonarBaseUrlRaw + ); + + // Extract PR key from the SonarQube PR link + const prKeyMatch = prLink.match(/pullRequest=([^&]+)/); + if (!prKeyMatch) { + throw new Error( + "Invalid SonarQube PR link format. Expected format: https://sonarcloud.io/project/issues?id=project&pullRequest=PR_KEY" + ); + } + + const prKey = prKeyMatch[1]; + + // Check if this is a SonarCloud setup + if ( + config.publicSonar && + this.sonarComponentKeys && + this.sonarOrganization + ) { + const params = new URLSearchParams({ + s: "FILE_LINE", + issueStatuses: "OPEN,CONFIRMED", + ps: "100", + facets: "impactSoftwareQualities,impactSeverities", + componentKeys: this.sonarComponentKeys, + organization: this.sonarOrganization, + pullRequest: prKey, + additionalFields: "_all", + }); + return `${sonarBaseUrl}?${params.toString()}`; + } else { + // SonarQube setup (private instance) + const params = new URLSearchParams({ + pullRequest: prKey, + components: "bat", + s: "FILE_LINE", + inNewCodePeriod: "true", + issueStatuses: "CONFIRMED,OPEN", + ps: "100", + facets: + "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", + additionalFields: "_all", + timeZone: "Europe/Rome", + }); + return `${sonarBaseUrl}?${params.toString()}`; + } + } + + /** + * Builds URL for fetching issues by PR ID + * @param {string} prId - PR ID + * @param {Object} config - Configuration object + * @returns {string} URL for fetching issues + */ + buildUrlForPrId(prId, config) { + const sonarBaseUrl = this.normalizeSonarUrl( + config.sonarBaseUrl || this.sonarBaseUrlRaw + ); + + // Check if this is a SonarCloud setup + if ( + config.publicSonar && + this.sonarComponentKeys && + this.sonarOrganization + ) { + const params = new URLSearchParams({ + s: "FILE_LINE", + issueStatuses: "OPEN,CONFIRMED", + ps: "100", + facets: "impactSoftwareQualities,impactSeverities", + componentKeys: this.sonarComponentKeys, + organization: this.sonarOrganization, + pullRequest: prId, + additionalFields: "_all", + }); + return `${sonarBaseUrl}?${params.toString()}`; + } else { + // SonarQube setup (private instance) + const params = new URLSearchParams({ + pullRequest: prId, + components: "bat", + s: "FILE_LINE", + inNewCodePeriod: "true", + issueStatuses: "CONFIRMED,OPEN", + ps: "100", + facets: + "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", + additionalFields: "_all", + timeZone: "Europe/Rome", + }); + return `${sonarBaseUrl}?${params.toString()}`; + } + } + + /** + * Fetches issues for a branch + * @param {string} branch - Branch name + * @param {Object} config - Configuration object + * @returns {Promise} Issues data + */ + async fetchIssuesForBranch(branch, config) { + const url = this.buildUrlForBranch(branch, config); + console.log( + `Fetching issues from: ${url.replace( + /sonarToken=[^&]+/, + "sonarToken=***" + )}` + ); + + const authHeaders = this.getSonarAuthHeaders(this.sonarToken); + const response = await fetch(url, { + headers: { + ...authHeaders, + "Content-Type": "application/json", + Accept: "application/json", + }, + }); + + return await this.handleSonarResponse(response); + } + + /** + * Fetches issues for a PR link + * @param {string} prLink - SonarQube PR link + * @param {Object} config - Configuration object + * @returns {Promise} Issues data + */ + async fetchIssuesForPr(prLink, config) { + const url = this.buildUrlForPr(prLink, config); + console.log( + `Fetching issues from PR: ${url.replace( + /sonarToken=[^&]+/, + "sonarToken=***" + )}` + ); + + const authHeaders = this.getSonarAuthHeaders(this.sonarToken); + const response = await fetch(url, { + headers: { + ...authHeaders, + "Content-Type": "application/json", + Accept: "application/json", + }, + }); + + return await this.handleSonarResponse(response); + } + + /** + * Fetches issues for a PR ID + * @param {string} prId - PR ID + * @param {Object} config - Configuration object + * @returns {Promise} Issues data + */ + async fetchIssuesForPrId(prId, config) { + const url = this.buildUrlForPrId(prId, config); + console.log(`Fetching issues from PR ID: ${prId}`); + console.log(`URL: ${url.replace(/sonarToken=[^&]+/, "sonarToken=***")}`); + + const authHeaders = this.getSonarAuthHeaders(this.sonarToken); + const response = await fetch(url, { + headers: { + ...authHeaders, + "Content-Type": "application/json", + Accept: "application/json", + }, + }); + + return await this.handleSonarResponse(response); + } +} diff --git a/src/versioning/index.js b/src/versioning/index.js new file mode 100644 index 0000000..f0b0323 --- /dev/null +++ b/src/versioning/index.js @@ -0,0 +1,159 @@ +#!/usr/bin/env node + +import dotenv from "dotenv"; +import { execSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { SonarIssueExtractor } from "../sonar-issue-extractor.js"; + +dotenv.config(); + +/** + * Loads configuration from .sonar/autofixer.config.json + * @returns {Object} Configuration object + */ +const loadConfiguration = () => { + const configPath = path.join( + process.cwd(), + ".sonar", + "autofixer.config.json" + ); + + if (!fs.existsSync(configPath)) { + throw new Error( + "Configuration file not found: .sonar/autofixer.config.json" + ); + } + + const config = JSON.parse(fs.readFileSync(configPath, "utf8")); + + // Validate required configuration + if (!config.gitProvider) { + throw new Error("gitProvider is required in configuration"); + } + + if (!["github", "bitbucket"].includes(config.gitProvider)) { + throw new Error("gitProvider must be either 'github' or 'bitbucket'"); + } + + return config; +}; + +/** + * Detects PR ID based on the configured git provider + * @param {string} branch - Current git branch name + * @param {string} gitProvider - Git provider (github or bitbucket) + * @returns {Promise} PR ID if found, null otherwise + */ +const detectPrId = async (branch, gitProvider) => { + const extractor = new SonarIssueExtractor(); + + if (gitProvider === "github") { + return await extractor.detectGitHubPrId(branch); + } else if (gitProvider === "bitbucket") { + return await extractor.detectBitbucketPrId(branch); + } + + return null; +}; + +/** + * Fetches SonarQube issues based on configuration and command line arguments + * @param {string|null} branchName - Optional branch name + * @param {string|null} sonarPrLink - Optional SonarQube PR link + */ +const fetchSonarIssues = async (branchName = null, sonarPrLink = null) => { + try { + // Load configuration + const config = loadConfiguration(); + console.log(`šŸ”§ Using configuration: ${JSON.stringify(config, null, 2)}`); + + // Get current git branch + const currentBranch = + branchName || + execSync("git branch --show-current", { encoding: "utf8" }).trim(); + console.log(`Current branch: ${currentBranch}`); + + // Initialize SonarQube extractor + const extractor = new SonarIssueExtractor(); + + let issues; + let usedSource; + + if (sonarPrLink) { + // If PR link is provided, fetch issues from that PR + console.log(`Using provided SonarQube PR link: ${sonarPrLink}`); + issues = await extractor.fetchIssuesForPr(sonarPrLink, config); + usedSource = `PR: ${sonarPrLink}`; + } else { + // Try to automatically detect PR ID from current branch + const detectedPrId = await detectPrId(currentBranch, config.gitProvider); + + if (detectedPrId) { + // Use detected PR ID + console.log(`šŸš€ Using automatically detected PR ID: ${detectedPrId}`); + issues = await extractor.fetchIssuesForPrId(detectedPrId, config); + usedSource = `PR #${detectedPrId} (auto-detected from branch: ${currentBranch})`; + } else { + // Fallback to branch-based approach + console.log("šŸ“‹ No PR detected, falling back to branch-based approach"); + issues = await extractor.fetchIssuesForBranch(currentBranch, config); + usedSource = currentBranch; + + // Fallback to develop if no issues found + if (!issues.issues || issues.issues.length === 0) { + console.log( + "No issues found for current branch. Falling back to branch: develop" + ); + issues = await extractor.fetchIssuesForBranch("develop", config); + usedSource = "develop"; + } + } + } + + // Save issues to file + const outputPath = config.outputPath || ".sonar/"; + const sonarDir = path.join(process.cwd(), outputPath); + if (!fs.existsSync(sonarDir)) { + fs.mkdirSync(sonarDir, { recursive: true }); + } + + const issuesPath = path.join(sonarDir, "issues.json"); + fs.writeFileSync(issuesPath, JSON.stringify(issues, null, 2)); + + console.log( + `āœ… Successfully fetched ${ + issues.issues?.length || 0 + } issues (source: ${usedSource})` + ); + console.log(`šŸ“ Saved to: ${issuesPath}`); + + // Display summary + if (issues.issues && issues.issues.length > 0) { + const severityCounts = {}; + for (const issue of issues.issues) { + const severity = issue.severity || "UNKNOWN"; + severityCounts[severity] = (severityCounts[severity] || 0) + 1; + } + + console.log("\nšŸ“Š Issues by severity:"); + const sortedEntries = Object.entries(severityCounts).sort( + ([, a], [, b]) => b - a + ); + for (const [severity, count] of sortedEntries) { + console.log(` ${severity}: ${count}`); + } + } + } catch (error) { + console.error("āŒ Error fetching SonarQube issues:", error.message); + process.exit(1); + } +}; + +// Parse command line arguments +const args = process.argv.slice(2); +const branchName = args[0] || null; +const sonarPrLink = args[1] || null; + +// Execute the main function +await fetchSonarIssues(branchName, sonarPrLink); From 0e092e70b90f96da1db45ed401bb827b2f6b06e0 Mon Sep 17 00:00:00 2001 From: Davide Ghiotto Date: Thu, 30 Oct 2025 00:00:24 +0100 Subject: [PATCH 2/8] Refactor CLI structure and update dependencies - Changed the main entry point in `package.json` from `src/cli.js` to `dist/cli.js` to align with the build output. - Removed deprecated source files including `cli.js`, `init.js`, `scanner.js`, `sonar-issue-extractor.js`, and `versioning/index.js`. - Updated `package.json` and `bun.lock` to include new development dependencies for improved functionality. - Enhanced script commands for building, linting, and formatting the project. --- biome.json | 55 +++++ bun.lock | 43 ++++ package.json | 20 +- src/{cli.js => cli.ts} | 9 +- src/{init.js => init.ts} | 43 +++- src/{scanner.js => scanner.ts} | 26 +- ...-extractor.js => sonar-issue-extractor.ts} | 231 +++++++++++------- src/versioning/{index.js => index.ts} | 51 ++-- tsconfig.json | 34 +++ 9 files changed, 380 insertions(+), 132 deletions(-) create mode 100644 biome.json rename src/{cli.js => cli.ts} (88%) mode change 100755 => 100644 rename src/{init.js => init.ts} (83%) rename src/{scanner.js => scanner.ts} (80%) rename src/{sonar-issue-extractor.js => sonar-issue-extractor.ts} (69%) rename src/versioning/{index.js => index.ts} (79%) create mode 100644 tsconfig.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..86d38e7 --- /dev/null +++ b/biome.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": ["node_modules", "dist"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedVariables": "error" + }, + "style": { + "noParameterAssign": "error" + }, + "suspicious": { + "noExplicitAny": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "trailingCommas": "es5", + "semicolons": "always" + } + }, + "overrides": [ + { + "include": ["*.ts", "*.tsx"], + "linter": { + "rules": { + "style": { + "useImportType": "error" + } + } + } + } + ] +} diff --git a/bun.lock b/bun.lock index 8bd3b96..2072329 100644 --- a/bun.lock +++ b/bun.lock @@ -11,9 +11,32 @@ "inquirer": "^12.10.0", "ora": "^9.0.0", }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/node": "^22.10.7", + "@typescript/native-preview": "^7.0.0-dev.20251029.1", + }, }, }, "packages": { + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@inquirer/ansi": ["@inquirer/ansi@1.0.1", "", {}, "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw=="], "@inquirer/checkbox": ["@inquirer/checkbox@4.3.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/figures": "^1.0.14", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5+Q3PKH35YsnoPTh75LucALdAxom6xh5D1oeY561x4cqBuH24ZFVyFREPe14xgnrtmGu3EEt1dIi60wRVSnGCw=="], @@ -46,6 +69,24 @@ "@inquirer/type": ["@inquirer/type@3.0.9", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w=="], + "@types/node": ["@types/node@22.18.13", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A=="], + + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251029.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251029.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251029.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251029.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251029.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251029.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251029.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251029.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-IRmYCDgwZQEfjy2GNJnQbqoRUrvdCbzLE0sLhwc6TP4I0Hx5TnHv3sJGKAgdmcbHmKHtwJeppXjgTRGtFTWRHQ=="], + + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251029.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DBJ3jFP6/MaQj/43LN1TC7tjR4SXZUNDnREiVjtFzpOG4Q71D1LB6QryskkRZsNtxLaTuVV57l2ubCE8tNmz0w=="], + + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251029.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-fnxZZtlXeud6f3bev3q50QMR+FrnuTyVr5akp5G2/o4jfkqLV6cKzseGnY6so+ftwfwP/PX3GOkfL6Ag8NzR0Q=="], + + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251029.1", "", { "os": "linux", "cpu": "arm" }, "sha512-1ok8pxcIlwMTMggySPIVt926lymLWNhCgPTzO751zKFTDTJcmpzmpmSWbiFQQ3fcPzO8LocsLXRfBwYDd/uqQA=="], + + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251029.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WK/N4Tk9nxI+k6AwJ7d80Gnd4+8kbBwmryIgOGPQNNvNJticYg6QiQsFGgC+HnCqvWDQ0fAyW+wdcPG6fwn/EA=="], + + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251029.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GvTl9BeItX0Ox0wXiMIHkktl9sCTkTPBe6f6hEs4XfJlAKm+JHbYtB9UEs62QyPYBFMx2phCytVNejpaUZRJmQ=="], + + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251029.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-BUEC+M6gViaa/zDzOjAOEqpOZeUJxuwrjwOokqxXyUavX+mC6zb6ALqx4r7GAWrfY9sSvGUacW4ZbqDTXe8KAg=="], + + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251029.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ODcXFgM62KpXxHqG5NMG+ipBqTbQ1pGkrzSByBwgRx0c/gTUhgML8UT7iK3nTrTtp9OBgPYPLLDNwiSLyzaIxA=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -116,6 +157,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], diff --git a/package.json b/package.json index 5e8b181..d4c7cd0 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,24 @@ "name": "@davide97g/sonar-autofixer", "version": "1.1.0", "description": "CLI utility for fetching SonarQube issues and integrating with Bitbucket/GitHub PR workflows", - "main": "src/cli.js", + "main": "dist/cli.js", "bin": { - "sonar-autofixer": "./src/cli.js" + "sonar-autofixer": "./dist/cli.js" }, "scripts": { - "prepublishOnly": "echo 'Publishing @davide97g/sonar-autofixer...'", + "build": "tsgo", + "lint": "biome lint src", + "lint:fix": "biome lint --write src", + "format": "biome format --write src", + "check": "biome check src", + "check:fix": "biome check --write src", + "prepublishOnly": "echo 'Publishing @davide97g/sonar-autofixer...' && npm run build", "sonar:scan": "npx davide97g:sonar-autofixer scan", "sonar:fetch": "npx davide97g:sonar-autofixer fetch" }, "files": [ - "src", + "dist", + "src/templates", "README.md", "package.json" ], @@ -50,6 +57,11 @@ "inquirer": "^12.10.0", "ora": "^9.0.0" }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/node": "^22.10.7", + "@typescript/native-preview": "^7.0.0-dev.20251029.1" + }, "type": "module", "engines": { "node": ">=18.0.0" diff --git a/src/cli.js b/src/cli.ts old mode 100755 new mode 100644 similarity index 88% rename from src/cli.js rename to src/cli.ts index 55ad43b..d2d5dac --- a/src/cli.js +++ b/src/cli.ts @@ -1,9 +1,9 @@ #!/usr/bin/env node import { Command } from "commander"; -import path from "path"; -import { fileURLToPath } from "url"; import { spawnSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; // ESM-compatible __dirname/__filename const __filename = fileURLToPath(import.meta.url); @@ -16,7 +16,10 @@ program .description("CLI for sonar-autofixer") .version("1.0.0"); -const runNodeScript = (relativeScriptPath, args = []) => { +const runNodeScript = ( + relativeScriptPath: string, + args: string[] = [] +): void => { const scriptPath = path.join(__dirname, relativeScriptPath); const result = spawnSync(process.execPath, [scriptPath, ...args], { stdio: "inherit", diff --git a/src/init.js b/src/init.ts similarity index 83% rename from src/init.js rename to src/init.ts index 9b358a2..46937c4 100644 --- a/src/init.js +++ b/src/init.ts @@ -3,23 +3,48 @@ import chalk from "chalk"; import fs from "fs-extra"; import inquirer from "inquirer"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import ora from "ora"; -import path from "path"; -import { fileURLToPath } from "url"; // ESM-compatible __dirname/__filename const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const runInit = async () => { +interface PackageJson { + name?: string; + private?: boolean; + scripts?: Record; + [key: string]: unknown; +} + +interface InitAnswers { + repoName: string; + gitProvider: "github" | "bitbucket"; + repositoryVisibility: "private" | "public"; + publicSonar: boolean; + outputPath: string; + aiEditor: "cursor" | "copilot (vscode)" | "windsurf" | "other"; +} + +interface Config { + repoName: string; + gitProvider: "github" | "bitbucket"; + repositoryVisibility: "private" | "public"; + publicSonar: boolean; + outputPath: string; + aiEditor: "cursor" | "copilot (vscode)" | "windsurf" | "other"; +} + +const runInit = async (): Promise => { console.log(chalk.blue("Welcome to sonar-autofixer setup!")); // Load package.json to derive sensible defaults const pkgPath = path.join(process.cwd(), "package.json"); - let pkg = {}; + let pkg: PackageJson = {}; try { if (await fs.pathExists(pkgPath)) { - pkg = await fs.readJson(pkgPath); + pkg = (await fs.readJson(pkgPath)) as PackageJson; } } catch (error) { console.warn( @@ -37,7 +62,7 @@ const runInit = async () => { : path.basename(process.cwd()); const defaultVisibility = pkg.private === true ? "private" : "public"; - const answers = await inquirer.prompt([ + const answers = await inquirer.prompt([ { type: "input", name: "repoName", @@ -80,7 +105,7 @@ const runInit = async () => { ]); // 1) Write configuration file ./sonar/autofixer.config.json - const config = { + const config: Config = { repoName: answers.repoName, gitProvider: answers.gitProvider, repositoryVisibility: answers.repositoryVisibility, @@ -116,7 +141,7 @@ const runInit = async () => { }).start(); try { const existingPkg = (await fs.pathExists(pkgPath)) - ? await fs.readJson(pkgPath) + ? ((await fs.readJson(pkgPath)) as PackageJson) : {}; if (!existingPkg.scripts) existingPkg.scripts = {}; existingPkg.scripts["sonar:scan"] = "npx davide97g:sonar-autofixer scan"; @@ -144,7 +169,7 @@ const runInit = async () => { const ruleContent = await fs.readFile(templateRulePath, "utf8"); const editor = answers.aiEditor; - let targetRulePath; + let targetRulePath: string; if (editor === "cursor") { targetRulePath = path.join( process.cwd(), diff --git a/src/scanner.js b/src/scanner.ts similarity index 80% rename from src/scanner.js rename to src/scanner.ts index 6850bda..ff4c056 100644 --- a/src/scanner.js +++ b/src/scanner.ts @@ -17,7 +17,7 @@ const __dirname = dirname(__filename); * Token is loaded from .env file via dotenv * Results are dumped to .sonar/scanner-report.json for local analysis */ -const runSonarScan = () => { +const runSonarScan = (): void => { try { // Get configuration from environment variables const sonarToken = process.env.SONAR_TOKEN; @@ -41,7 +41,11 @@ const runSonarScan = () => { execSync("which sonar", { encoding: "utf8", stdio: "ignore" }); } catch (error) { console.error("āŒ Error: Sonar scanner (@sonar/scan) is not installed."); - console.error(`Scanner check failed: ${error.message}`); + console.error( + `Scanner check failed: ${ + error instanceof Error ? error.message : String(error) + }` + ); console.error("Please install it with: npm install -g @sonar/scan"); process.exit(1); } @@ -78,19 +82,27 @@ const runSonarScan = () => { "šŸ’” Note: This is a local scan. Results are saved to file for local analysis." ); } catch (error) { - console.error("āŒ Error running Sonar scan:", error.message); + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("āŒ Error running Sonar scan:", errorMessage); // Provide helpful error messages - if (error.status === 127 || error.message.includes("command not found")) { + if ( + (error as { status?: number }).status === 127 || + errorMessage.includes("command not found") + ) { console.error( "\nšŸ’” Tip: Install Sonar Scanner with: npm install -g @sonar/scan" ); } - if (error.stdout || error.stderr) { + const execError = error as { + stdout?: Buffer | string; + stderr?: Buffer | string; + }; + if (execError.stdout || execError.stderr) { console.error("\nScanner output:"); - if (error.stdout) console.error(error.stdout.toString()); - if (error.stderr) console.error(error.stderr.toString()); + if (execError.stdout) console.error(execError.stdout.toString()); + if (execError.stderr) console.error(execError.stderr.toString()); } process.exit(1); diff --git a/src/sonar-issue-extractor.js b/src/sonar-issue-extractor.ts similarity index 69% rename from src/sonar-issue-extractor.js rename to src/sonar-issue-extractor.ts index 2d470f6..17af105 100644 --- a/src/sonar-issue-extractor.js +++ b/src/sonar-issue-extractor.ts @@ -2,10 +2,39 @@ import dotenv from "dotenv"; dotenv.config(); +interface SonarIssue { + severity?: string; + [key: string]: unknown; +} + +interface SonarResponse { + issues?: SonarIssue[]; + [key: string]: unknown; +} + +interface Config { + sonarBaseUrl?: string; + publicSonar?: boolean; + gitProvider?: string; + [key: string]: unknown; +} + /** * SonarIssueExtractor - Handles all SonarQube API interactions and PR detection */ export class SonarIssueExtractor { + private readonly githubToken: string | undefined; + private readonly githubOwner: string | undefined; + private readonly githubRepo: string | undefined; + private readonly githubBaseUrl: string; + private readonly bitbucketEmail: string | undefined; + private readonly bitbucketApiToken: string | undefined; + private readonly bitbucketBaseUrl: string | undefined; + private readonly sonarToken: string | undefined; + private readonly sonarBaseUrlRaw: string; + private readonly sonarComponentKeys: string | undefined; + private readonly sonarOrganization: string | undefined; + constructor() { // GitHub configuration this.githubToken = process.env.GITHUB_TOKEN; @@ -28,10 +57,10 @@ export class SonarIssueExtractor { /** * Gets SonarQube authentication headers - * @param {string} token - SonarQube token - * @returns {Object} Authentication headers + * @param token - SonarQube token + * @returns Authentication headers */ - getSonarAuthHeaders(token) { + getSonarAuthHeaders(token?: string): Record { if (!token) return {}; // SonarCloud/SonarQube typically uses Basic Auth: token as username, empty password @@ -45,9 +74,9 @@ export class SonarIssueExtractor { /** * Gets Bitbucket authentication headers - * @returns {Object} Authentication headers + * @returns Authentication headers */ - getBitbucketAuthHeaders() { + getBitbucketAuthHeaders(): Record { if (!this.bitbucketEmail || !this.bitbucketApiToken) { throw new Error("BITBUCKET_EMAIL and BITBUCKET_API_TOKEN are required"); } @@ -62,10 +91,10 @@ export class SonarIssueExtractor { /** * Normalizes SonarQube base URL to API endpoint - * @param {string} baseUrl - Raw base URL - * @returns {string} Normalized API URL + * @param baseUrl - Raw base URL + * @returns Normalized API URL */ - normalizeSonarUrl(baseUrl) { + normalizeSonarUrl(baseUrl: string): string { let url = baseUrl; if (!url.includes("/api/issues/search")) { url = url.replace(/\/project\/issues[^/]*$/, "").replace(/\/$/, ""); @@ -77,10 +106,10 @@ export class SonarIssueExtractor { /** * Detects GitHub PR ID from branch name - * @param {string} branch - Branch name - * @returns {Promise} PR ID if found, null otherwise + * @param branch - Branch name + * @returns PR ID if found, null otherwise */ - async detectGitHubPrId(branch) { + async detectGitHubPrId(branch: string): Promise { try { if (!this.githubToken || !this.githubOwner || !this.githubRepo) { console.log("āš ļø GitHub configuration missing, skipping PR detection"); @@ -99,7 +128,7 @@ export class SonarIssueExtractor { }); if (response.ok) { - const data = await response.json(); + const data = (await response.json()) as Array<{ number: number }>; if (Array.isArray(data) && data.length > 0) { const prNumber = data[0].number; console.log(`āœ… Found PR #${prNumber} for branch: ${branch}`); @@ -117,7 +146,9 @@ export class SonarIssueExtractor { }); if (closedResponse.ok) { - const closedData = await closedResponse.json(); + const closedData = (await closedResponse.json()) as Array<{ + number: number; + }>; if (Array.isArray(closedData) && closedData.length > 0) { const prNumber = closedData[0].number; console.log( @@ -134,23 +165,25 @@ export class SonarIssueExtractor { if (prNumberMatch) { const prNumber = prNumberMatch[1] || prNumberMatch[2]; console.log(`āœ… Extracted PR #${prNumber} from branch name: ${branch}`); - return prNumber; + return prNumber || null; } console.log(`āš ļø No PR found for branch: ${branch}`); return null; } catch (error) { - console.log(`āš ļø Could not detect GitHub PR ID: ${error.message}`); + const errorMessage = + error instanceof Error ? error.message : String(error); + console.log(`āš ļø Could not detect GitHub PR ID: ${errorMessage}`); return null; } } /** * Detects Bitbucket PR ID from branch name - * @param {string} branch - Branch name - * @returns {Promise} PR ID if found, null otherwise + * @param branch - Branch name + * @returns PR ID if found, null otherwise */ - async detectBitbucketPrId(branch) { + async detectBitbucketPrId(branch: string): Promise { try { if ( !this.bitbucketEmail || @@ -172,7 +205,9 @@ export class SonarIssueExtractor { }); if (response.ok) { - const data = await response.json(); + const data = (await response.json()) as { + values?: Array<{ id: number }>; + }; if (data.values && data.values.length > 0) { const prNumber = data.values[0].id; console.log(`āœ… Found PR #${prNumber} for branch: ${branch}`); @@ -187,23 +222,25 @@ export class SonarIssueExtractor { if (prNumberMatch) { const prNumber = prNumberMatch[1] || prNumberMatch[2]; console.log(`āœ… Extracted PR #${prNumber} from branch name: ${branch}`); - return prNumber; + return prNumber || null; } console.log(`āš ļø No PR found for branch: ${branch}`); return null; } catch (error) { - console.log(`āš ļø Could not detect Bitbucket PR ID: ${error.message}`); + const errorMessage = + error instanceof Error ? error.message : String(error); + console.log(`āš ļø Could not detect Bitbucket PR ID: ${errorMessage}`); return null; } } /** * Handles SonarQube API response - * @param {Response} response - Fetch response - * @returns {Promise} Parsed JSON response + * @param response - Fetch response + * @returns Parsed JSON response */ - async handleSonarResponse(response) { + async handleSonarResponse(response: Response): Promise { const contentType = response.headers.get("content-type"); if (!contentType?.includes("application/json")) { const errorText = await response.text(); @@ -224,16 +261,16 @@ export class SonarIssueExtractor { ); } - return await response.json(); + return (await response.json()) as SonarResponse; } /** * Builds URL for fetching issues by branch - * @param {string} branch - Branch name - * @param {Object} config - Configuration object - * @returns {string} URL for fetching issues + * @param branch - Branch name + * @param config - Configuration object + * @returns URL for fetching issues */ - buildUrlForBranch(branch, config) { + buildUrlForBranch(branch: string, config: Config): string { const sonarBaseUrl = this.normalizeSonarUrl( config.sonarBaseUrl || this.sonarBaseUrlRaw ); @@ -255,31 +292,30 @@ export class SonarIssueExtractor { additionalFields: "_all", }); return `${sonarBaseUrl}?${params.toString()}`; - } else { - // SonarQube setup (private instance) - const params = new URLSearchParams({ - branch, - components: "bat", - s: "FILE_LINE", - inNewCodePeriod: "true", - issueStatuses: "CONFIRMED,OPEN", - ps: "100", - facets: - "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", - additionalFields: "_all", - timeZone: "Europe/Rome", - }); - return `${sonarBaseUrl}?${params.toString()}`; } + // SonarQube setup (private instance) + const params = new URLSearchParams({ + branch, + components: "bat", + s: "FILE_LINE", + inNewCodePeriod: "true", + issueStatuses: "CONFIRMED,OPEN", + ps: "100", + facets: + "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", + additionalFields: "_all", + timeZone: "Europe/Rome", + }); + return `${sonarBaseUrl}?${params.toString()}`; } /** * Builds URL for fetching issues by PR link - * @param {string} prLink - SonarQube PR link - * @param {Object} config - Configuration object - * @returns {string} URL for fetching issues + * @param prLink - SonarQube PR link + * @param config - Configuration object + * @returns URL for fetching issues */ - buildUrlForPr(prLink, config) { + buildUrlForPr(prLink: string, config: Config): string { const sonarBaseUrl = this.normalizeSonarUrl( config.sonarBaseUrl || this.sonarBaseUrlRaw ); @@ -311,31 +347,30 @@ export class SonarIssueExtractor { additionalFields: "_all", }); return `${sonarBaseUrl}?${params.toString()}`; - } else { - // SonarQube setup (private instance) - const params = new URLSearchParams({ - pullRequest: prKey, - components: "bat", - s: "FILE_LINE", - inNewCodePeriod: "true", - issueStatuses: "CONFIRMED,OPEN", - ps: "100", - facets: - "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", - additionalFields: "_all", - timeZone: "Europe/Rome", - }); - return `${sonarBaseUrl}?${params.toString()}`; } + // SonarQube setup (private instance) + const params = new URLSearchParams({ + pullRequest: prKey, + components: "bat", + s: "FILE_LINE", + inNewCodePeriod: "true", + issueStatuses: "CONFIRMED,OPEN", + ps: "100", + facets: + "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", + additionalFields: "_all", + timeZone: "Europe/Rome", + }); + return `${sonarBaseUrl}?${params.toString()}`; } /** * Builds URL for fetching issues by PR ID - * @param {string} prId - PR ID - * @param {Object} config - Configuration object - * @returns {string} URL for fetching issues + * @param prId - PR ID + * @param config - Configuration object + * @returns URL for fetching issues */ - buildUrlForPrId(prId, config) { + buildUrlForPrId(prId: string, config: Config): string { const sonarBaseUrl = this.normalizeSonarUrl( config.sonarBaseUrl || this.sonarBaseUrlRaw ); @@ -357,31 +392,33 @@ export class SonarIssueExtractor { additionalFields: "_all", }); return `${sonarBaseUrl}?${params.toString()}`; - } else { - // SonarQube setup (private instance) - const params = new URLSearchParams({ - pullRequest: prId, - components: "bat", - s: "FILE_LINE", - inNewCodePeriod: "true", - issueStatuses: "CONFIRMED,OPEN", - ps: "100", - facets: - "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", - additionalFields: "_all", - timeZone: "Europe/Rome", - }); - return `${sonarBaseUrl}?${params.toString()}`; } + // SonarQube setup (private instance) + const params = new URLSearchParams({ + pullRequest: prId, + components: "bat", + s: "FILE_LINE", + inNewCodePeriod: "true", + issueStatuses: "CONFIRMED,OPEN", + ps: "100", + facets: + "cleanCodeAttributeCategories,impactSoftwareQualities,severities,types,impactSeverities,codeVariants", + additionalFields: "_all", + timeZone: "Europe/Rome", + }); + return `${sonarBaseUrl}?${params.toString()}`; } /** * Fetches issues for a branch - * @param {string} branch - Branch name - * @param {Object} config - Configuration object - * @returns {Promise} Issues data + * @param branch - Branch name + * @param config - Configuration object + * @returns Issues data */ - async fetchIssuesForBranch(branch, config) { + async fetchIssuesForBranch( + branch: string, + config: Config + ): Promise { const url = this.buildUrlForBranch(branch, config); console.log( `Fetching issues from: ${url.replace( @@ -404,11 +441,14 @@ export class SonarIssueExtractor { /** * Fetches issues for a PR link - * @param {string} prLink - SonarQube PR link - * @param {Object} config - Configuration object - * @returns {Promise} Issues data + * @param prLink - SonarQube PR link + * @param config - Configuration object + * @returns Issues data */ - async fetchIssuesForPr(prLink, config) { + async fetchIssuesForPr( + prLink: string, + config: Config + ): Promise { const url = this.buildUrlForPr(prLink, config); console.log( `Fetching issues from PR: ${url.replace( @@ -431,11 +471,14 @@ export class SonarIssueExtractor { /** * Fetches issues for a PR ID - * @param {string} prId - PR ID - * @param {Object} config - Configuration object - * @returns {Promise} Issues data + * @param prId - PR ID + * @param config - Configuration object + * @returns Issues data */ - async fetchIssuesForPrId(prId, config) { + async fetchIssuesForPrId( + prId: string, + config: Config + ): Promise { const url = this.buildUrlForPrId(prId, config); console.log(`Fetching issues from PR ID: ${prId}`); console.log(`URL: ${url.replace(/sonarToken=[^&]+/, "sonarToken=***")}`); diff --git a/src/versioning/index.js b/src/versioning/index.ts similarity index 79% rename from src/versioning/index.js rename to src/versioning/index.ts index f0b0323..df630dc 100644 --- a/src/versioning/index.js +++ b/src/versioning/index.ts @@ -8,11 +8,24 @@ import { SonarIssueExtractor } from "../sonar-issue-extractor.js"; dotenv.config(); +interface Config { + gitProvider: "github" | "bitbucket"; + outputPath?: string; + sonarBaseUrl?: string; + publicSonar?: boolean; + [key: string]: unknown; +} + +interface SonarIssuesResponse { + issues?: Array<{ severity?: string; [key: string]: unknown }>; + [key: string]: unknown; +} + /** * Loads configuration from .sonar/autofixer.config.json - * @returns {Object} Configuration object + * @returns Configuration object */ -const loadConfiguration = () => { +const loadConfiguration = (): Config => { const configPath = path.join( process.cwd(), ".sonar", @@ -25,7 +38,7 @@ const loadConfiguration = () => { ); } - const config = JSON.parse(fs.readFileSync(configPath, "utf8")); + const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Config; // Validate required configuration if (!config.gitProvider) { @@ -41,16 +54,20 @@ const loadConfiguration = () => { /** * Detects PR ID based on the configured git provider - * @param {string} branch - Current git branch name - * @param {string} gitProvider - Git provider (github or bitbucket) - * @returns {Promise} PR ID if found, null otherwise + * @param branch - Current git branch name + * @param gitProvider - Git provider (github or bitbucket) + * @returns PR ID if found, null otherwise */ -const detectPrId = async (branch, gitProvider) => { +const detectPrId = async ( + branch: string, + gitProvider: "github" | "bitbucket" +): Promise => { const extractor = new SonarIssueExtractor(); if (gitProvider === "github") { return await extractor.detectGitHubPrId(branch); - } else if (gitProvider === "bitbucket") { + } + if (gitProvider === "bitbucket") { return await extractor.detectBitbucketPrId(branch); } @@ -59,10 +76,13 @@ const detectPrId = async (branch, gitProvider) => { /** * Fetches SonarQube issues based on configuration and command line arguments - * @param {string|null} branchName - Optional branch name - * @param {string|null} sonarPrLink - Optional SonarQube PR link + * @param branchName - Optional branch name + * @param sonarPrLink - Optional SonarQube PR link */ -const fetchSonarIssues = async (branchName = null, sonarPrLink = null) => { +const fetchSonarIssues = async ( + branchName: string | null = null, + sonarPrLink: string | null = null +): Promise => { try { // Load configuration const config = loadConfiguration(); @@ -77,8 +97,8 @@ const fetchSonarIssues = async (branchName = null, sonarPrLink = null) => { // Initialize SonarQube extractor const extractor = new SonarIssueExtractor(); - let issues; - let usedSource; + let issues: SonarIssuesResponse; + let usedSource: string; if (sonarPrLink) { // If PR link is provided, fetch issues from that PR @@ -130,7 +150,7 @@ const fetchSonarIssues = async (branchName = null, sonarPrLink = null) => { // Display summary if (issues.issues && issues.issues.length > 0) { - const severityCounts = {}; + const severityCounts: Record = {}; for (const issue of issues.issues) { const severity = issue.severity || "UNKNOWN"; severityCounts[severity] = (severityCounts[severity] || 0) + 1; @@ -145,7 +165,8 @@ const fetchSonarIssues = async (branchName = null, sonarPrLink = null) => { } } } catch (error) { - console.error("āŒ Error fetching SonarQube issues:", error.message); + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("āŒ Error fetching SonarQube issues:", errorMessage); process.exit(1); } }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..869922c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + From 0a33ae94b869767b98ec09fb68ba55868ff0f1c8 Mon Sep 17 00:00:00 2001 From: Davide Ghiotto Date: Thu, 30 Oct 2025 00:02:15 +0100 Subject: [PATCH 3/8] Refactor CLI and restructure Sonar components - Updated the CLI to point to the new scanner location in `./sonar/scanner.js`. - Removed deprecated files `scanner.ts` and `sonar-issue-extractor.ts` to streamline the codebase. - Adjusted import paths in `versioning/index.ts` to reflect the new structure of the Sonar components. --- src/cli.ts | 2 +- src/{ => sonar}/scanner.ts | 0 src/{ => sonar}/sonar-issue-extractor.ts | 0 src/versioning/index.ts | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename src/{ => sonar}/scanner.ts (100%) rename src/{ => sonar}/sonar-issue-extractor.ts (100%) diff --git a/src/cli.ts b/src/cli.ts index d2d5dac..26f76dd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -52,7 +52,7 @@ program .description("Run local Sonar scanner and save report") .allowExcessArguments(true) .action(() => { - runNodeScript("./scanner.js", process.argv.slice(3)); + runNodeScript("./sonar/scanner.js", process.argv.slice(3)); }); program.parse(process.argv); diff --git a/src/scanner.ts b/src/sonar/scanner.ts similarity index 100% rename from src/scanner.ts rename to src/sonar/scanner.ts diff --git a/src/sonar-issue-extractor.ts b/src/sonar/sonar-issue-extractor.ts similarity index 100% rename from src/sonar-issue-extractor.ts rename to src/sonar/sonar-issue-extractor.ts diff --git a/src/versioning/index.ts b/src/versioning/index.ts index df630dc..52279f1 100644 --- a/src/versioning/index.ts +++ b/src/versioning/index.ts @@ -4,7 +4,7 @@ import dotenv from "dotenv"; import { execSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import { SonarIssueExtractor } from "../sonar-issue-extractor.js"; +import { SonarIssueExtractor } from "../sonar/sonar-issue-extractor.js"; dotenv.config(); From 3d315976d1b077c3ca218d7a12c9389bb268b7d1 Mon Sep 17 00:00:00 2001 From: Davide Ghiotto Date: Thu, 30 Oct 2025 00:40:13 +0100 Subject: [PATCH 4/8] Enhance CLI setup and update dependencies - Added new dependencies `figlet` and `gradient-string` for improved CLI output. - Updated the `init` command to include dynamic gradient effects and a banner using `figlet`. - Refactored the `runInit` function to handle graceful exits and improved error handling. - Introduced VSCode settings and extension recommendations for better development experience. --- .vscode/extensions.json | 3 + .vscode/settings.json | 34 +++++++ bun.lock | 12 +++ package.json | 10 +- src/cli.ts | 12 +-- src/init.ts | 217 +++++++++++++++++++++++++++------------- 6 files changed, 203 insertions(+), 85 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..699ed73 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2efb85a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,34 @@ +{ + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.formatOnType": false, + "editor.codeActionsOnSave": { + "source.removeUnusedImports": "always", + "source.fixAll.biome": "explicit", + "source.organizeImports": "none", + "source.organizeImports.biome": "explicit" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "sonarlint.connectedMode.project": { + "connectionId": "https-eyecare-sonarqube-luxgroup-net-", + "projectKey": "bat" + } +} diff --git a/bun.lock b/bun.lock index 2072329..d7adb86 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,9 @@ "chalk": "^5.6.2", "commander": "^14.0.2", "dotenv": "^17.2.3", + "figlet": "^1.9.3", "fs-extra": "^11.3.2", + "gradient-string": "^3.0.0", "inquirer": "^12.10.0", "ora": "^9.0.0", }, @@ -71,6 +73,8 @@ "@types/node": ["@types/node@22.18.13", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A=="], + "@types/tinycolor2": ["@types/tinycolor2@1.4.6", "", {}, "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251029.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251029.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251029.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251029.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251029.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251029.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251029.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251029.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-IRmYCDgwZQEfjy2GNJnQbqoRUrvdCbzLE0sLhwc6TP4I0Hx5TnHv3sJGKAgdmcbHmKHtwJeppXjgTRGtFTWRHQ=="], "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251029.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DBJ3jFP6/MaQj/43LN1TC7tjR4SXZUNDnREiVjtFzpOG4Q71D1LB6QryskkRZsNtxLaTuVV57l2ubCE8tNmz0w=="], @@ -111,12 +115,16 @@ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "figlet": ["figlet@1.9.3", "", { "dependencies": { "commander": "^14.0.0" }, "bin": { "figlet": "bin/index.js" } }, "sha512-majPgOpVtrZN1iyNGbsUP6bOtZ6eaJgg5HHh0vFvm5DJhh8dc+FJpOC4GABvMZ/A7XHAJUuJujhgUY/2jPWgMA=="], + "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "gradient-string": ["gradient-string@3.0.0", "", { "dependencies": { "chalk": "^5.3.0", "tinygradient": "^1.1.5" } }, "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg=="], + "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], "inquirer": ["inquirer@12.10.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/prompts": "^7.9.0", "@inquirer/type": "^3.0.9", "mute-stream": "^2.0.0", "run-async": "^4.0.5", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-K/epfEnDBZj2Q3NMDcgXWZye3nhSPeoJnOh8lcKWrldw54UEZfS4EmAMsAsmVbl7qKi+vjAsy39Sz4fbgRMewg=="], @@ -155,6 +163,10 @@ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + + "tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], diff --git a/package.json b/package.json index d4c7cd0..0604883 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,11 @@ "check": "biome check src", "check:fix": "biome check --write src", "prepublishOnly": "echo 'Publishing @davide97g/sonar-autofixer...' && npm run build", + "init": "node dist/cli.js init", "sonar:scan": "npx davide97g:sonar-autofixer scan", "sonar:fetch": "npx davide97g:sonar-autofixer fetch" }, - "files": [ - "dist", - "src/templates", - "README.md", - "package.json" - ], + "files": ["dist", "src/templates", "README.md", "package.json"], "repository": { "type": "git", "url": "git+https://github.com/davide97g/sonar-autofixer.git" @@ -53,7 +49,9 @@ "chalk": "^5.6.2", "commander": "^14.0.2", "dotenv": "^17.2.3", + "figlet": "^1.9.3", "fs-extra": "^11.3.2", + "gradient-string": "^3.0.0", "inquirer": "^12.10.0", "ora": "^9.0.0" }, diff --git a/src/cli.ts b/src/cli.ts index 26f76dd..f2f16d6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,15 +11,9 @@ const __dirname = path.dirname(__filename); const program = new Command(); -program - .name("sonar-autofixer") - .description("CLI for sonar-autofixer") - .version("1.0.0"); - -const runNodeScript = ( - relativeScriptPath: string, - args: string[] = [] -): void => { +program.name("sonar-autofixer").description("CLI for sonar-autofixer").version("1.0.0"); + +const runNodeScript = (relativeScriptPath: string, args: string[] = []): void => { const scriptPath = path.join(__dirname, relativeScriptPath); const result = spawnSync(process.execPath, [scriptPath, ...args], { stdio: "inherit", diff --git a/src/init.ts b/src/init.ts index 46937c4..7b94aa6 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,12 +1,16 @@ #!/usr/bin/env node +import path from "node:path"; +import { fileURLToPath } from "node:url"; import chalk from "chalk"; import fs from "fs-extra"; import inquirer from "inquirer"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; import ora from "ora"; +import figlet from "figlet"; +import gradient from "gradient-string"; +const colors = ["#A4A5A7", "#C74600", "#EB640A", "#F2A65D"]; +const dynamicGradient = gradient(colors); // ESM-compatible __dirname/__filename const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -37,7 +41,7 @@ interface Config { } const runInit = async (): Promise => { - console.log(chalk.blue("Welcome to sonar-autofixer setup!")); + console.log(dynamicGradient(chalk.bold("Welcome to sonar-autofixer setup!"))); // Load package.json to derive sensible defaults const pkgPath = path.join(process.cwd(), "package.json"); @@ -62,47 +66,71 @@ const runInit = async (): Promise => { : path.basename(process.cwd()); const defaultVisibility = pkg.private === true ? "private" : "public"; - const answers = await inquirer.prompt([ - { - type: "input", - name: "repoName", - message: "Repo name?", - default: defaultRepoName, - }, - { - type: "list", - name: "gitProvider", - message: "Git provider:", - choices: ["github", "bitbucket"], - default: "github", - }, - { - type: "list", - name: "repositoryVisibility", - message: "Repository visibility:", - choices: ["private", "public"], - default: defaultVisibility, - }, - { - type: "confirm", - name: "publicSonar", - message: "Public sonar?", - default: true, // Y/n - }, - { - type: "input", - name: "outputPath", - message: "Output path:", - default: ".sonar/", - }, - { - type: "list", - name: "aiEditor", - message: "AI editor:", - choices: ["cursor", "copilot (vscode)", "windsurf", "other"], - default: "cursor", - }, - ]); + let answers: InitAnswers; + try { + answers = await inquirer.prompt([ + { + type: "input", + name: "repoName", + message: "Repo name?", + default: defaultRepoName, + }, + { + type: "list", + name: "gitProvider", + message: "Git provider:", + choices: ["github", "bitbucket"], + default: "github", + }, + { + type: "list", + name: "repositoryVisibility", + message: "Repository visibility:", + choices: ["private", "public"], + default: defaultVisibility, + }, + { + type: "confirm", + name: "publicSonar", + message: "Public sonar?", + default: true, // Y/n + }, + { + type: "input", + name: "outputPath", + message: "Output path:", + default: ".sonar/", + }, + { + type: "list", + name: "aiEditor", + message: "AI editor:", + choices: ["cursor", "copilot (vscode)", "windsurf", "other"], + default: "cursor", + }, + ]); + } catch (error) { + // Handle graceful exit on SIGINT (Ctrl+C) + if ( + error && + typeof error === "object" && + (("name" in error && error.name === "ExitPromptError") || + ("message" in error && + typeof error.message === "string" && + error.message.includes("SIGINT"))) + ) { + console.log("\n"); + const exitGradient = dynamicGradient; + console.log( + exitGradient.multiline( + "šŸ‘‹ Setup cancelled\nThanks for trying sonar-autofixer!\nSee you next time ✨" + ) + ); + process.exit(0); + } + // Re-throw other errors + throw error; + } // 1) Write configuration file ./sonar/autofixer.config.json const config: Config = { @@ -123,14 +151,10 @@ const runInit = async (): Promise => { await fs.ensureDir(sonarDir); const configPath = path.join(sonarDir, "autofixer.config.json"); await fs.writeJson(configPath, config, { spaces: 2 }); - configSpinner.succeed( - `Configuration saved to ${path.relative(process.cwd(), configPath)}` - ); + configSpinner.succeed(`Configuration saved to ${path.relative(process.cwd(), configPath)}`); } catch (error) { configSpinner.fail("Failed to write configuration"); - console.error( - chalk.red(error instanceof Error ? error.message : String(error)) - ); + console.error(chalk.red(error instanceof Error ? error.message : String(error))); process.exit(1); } @@ -150,9 +174,7 @@ const runInit = async (): Promise => { scriptsSpinner.succeed("package.json scripts updated"); } catch (error) { scriptsSpinner.fail("Failed to update package.json"); - console.error( - chalk.red(error instanceof Error ? error.message : String(error)) - ); + console.error(chalk.red(error instanceof Error ? error.message : String(error))); process.exit(1); } @@ -162,7 +184,8 @@ const runInit = async (): Promise => { color: "yellow", }).start(); try { - const templateRulePath = path.join(__dirname, "./templates/rule.md"); + // Go up one level from dist to src, then to templates + const templateRulePath = path.join(__dirname, "../src/templates/rule.md"); if (!(await fs.pathExists(templateRulePath))) { throw new Error(`Template rule not found at ${templateRulePath}`); } @@ -171,35 +194,89 @@ const runInit = async (): Promise => { const editor = answers.aiEditor; let targetRulePath: string; if (editor === "cursor") { - targetRulePath = path.join( - process.cwd(), - ".cursor/rules/sonar-issue-fix.mdc" - ); + targetRulePath = path.join(process.cwd(), ".cursor/rules/sonar-issue-fix.mdc"); } else if (editor === "copilot (vscode)") { targetRulePath = path.join(process.cwd(), ".vscode/sonar-issue-fix.md"); } else if (editor === "windsurf") { - targetRulePath = path.join( - process.cwd(), - ".windsurf/rules/sonar-issue-fix.mdc" - ); + targetRulePath = path.join(process.cwd(), ".windsurf/rules/sonar-issue-fix.mdc"); } else { targetRulePath = path.join(process.cwd(), "rules/sonar-issue-fix.md"); } await fs.ensureDir(path.dirname(targetRulePath)); await fs.writeFile(targetRulePath, ruleContent, "utf8"); - ruleSpinner.succeed( - `Rule created at ${path.relative(process.cwd(), targetRulePath)}` - ); + ruleSpinner.succeed(`Rule created at ${path.relative(process.cwd(), targetRulePath)}`); } catch (error) { ruleSpinner.fail("Failed to create rule file"); - console.error( - chalk.red(error instanceof Error ? error.message : String(error)) - ); + console.error(chalk.red(error instanceof Error ? error.message : String(error))); process.exit(1); } - console.log(chalk.green("āœ… Setup complete.")); + console.log(dynamicGradient("āœ… Setup complete.")); +}; + +const runBanner = async (): Promise => { + return new Promise((resolve) => { + figlet.text( + "Bitrock", + { + font: "ANSI Shadow", + horizontalLayout: "default", + verticalLayout: "default", + }, + (err, data) => { + if (err) { + console.error("āŒ Figlet error:", err); + return; + } + + const lines = data?.split("\n") ?? []; + + let i = 0; + const interval = setInterval(() => { + // Rotate gradient colors over time for smooth animation + const shifted = [...colors.slice(i), ...colors.slice(0, i)]; + const dynamicGradient = gradient(shifted); + + console.clear(); + console.log(chalk.bold(dynamicGradient.multiline(lines.join("\n")))); + console.log(dynamicGradient("⚔ Empowering modern engineering ⚔")); + + i = (i + 1) % colors.length; + }, 150); // Adjust speed here (lower = faster) + + setTimeout(() => { + clearInterval(interval); + console.clear(); + resolve(); + }, 4000); + } + ); + }); }; -await runInit(); +await runBanner(); + +await runInit().catch((error) => { + // Handle any unexpected errors + if ( + error && + typeof error === "object" && + (("name" in error && error.name === "ExitPromptError") || + ("message" in error && typeof error.message === "string" && error.message.includes("SIGINT"))) + ) { + // Already handled in runInit, but just in case + console.log("\n"); + const exitGradient = dynamicGradient; + console.log( + exitGradient.multiline( + "šŸ‘‹ Setup cancelled\nThanks for trying sonar-autofixer!\nSee you next time ✨" + ) + ); + process.exit(0); + } + // For other errors, exit with error code + console.error(chalk.red("\nāŒ An unexpected error occurred:")); + console.error(chalk.red(error instanceof Error ? error.message : String(error))); + process.exit(1); +}); From 4f1fd9757167dbbe0bd802b47e4df5b3c9746652 Mon Sep 17 00:00:00 2001 From: Davide Ghiotto Date: Thu, 30 Oct 2025 00:40:25 +0100 Subject: [PATCH 5/8] Update version in package.json from 1.1.0 to 1.2.0 for new release. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0604883..766b892 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@davide97g/sonar-autofixer", - "version": "1.1.0", + "version": "1.2.0", "description": "CLI utility for fetching SonarQube issues and integrating with Bitbucket/GitHub PR workflows", "main": "dist/cli.js", "bin": { From a38b084e0d620719030a3d677519f04a01629515 Mon Sep 17 00:00:00 2001 From: Davide Ghiotto Date: Thu, 30 Oct 2025 10:25:15 +0100 Subject: [PATCH 6/8] Update version to 1.2.1 and add update checking functionality - Bumped version in package.json from 1.2.0 to 1.2.1. - Introduced a new command to check for updates and display instructions for getting the latest version. - Enhanced CLI output to remind users to use the latest version when running commands. --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/cli.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7ddb100..8eaa958 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,13 @@ npx @davide97g/sonar-autofixer scan npx @davide97g/sonar-autofixer init ``` +#### Check for Updates + +```bash +# Check for updates and get latest version info +npx @davide97g/sonar-autofixer update +``` + ### As npm Scripts After initialization, you can use the added npm scripts: @@ -125,6 +132,38 @@ npm run sonar:scan - **AI Editor Integration**: Creates rules for Cursor, VSCode, Windsurf for automated issue fixing - **Issue Summary**: Displays a summary of issues by severity after fetching - **Configuration Management**: Interactive setup for easy configuration +- **Update Checking**: Built-in command to check for updates and get latest version info + +## Updating the CLI + +Since this CLI is designed to be used with `npx`, updating is simple: + +### Always Get the Latest Version + +```bash +# Use @latest to always get the most recent version +npx @davide97g/sonar-autofixer@latest +``` + +### Check for Updates + +```bash +# Check current version and get update instructions +npx @davide97g/sonar-autofixer update +``` + +### Update Your npm Scripts + +If you've set up npm scripts in your `package.json`, update them to use `@latest`: + +```json +{ + "scripts": { + "sonar:fetch": "npx @davide97g/sonar-autofixer@latest fetch", + "sonar:scan": "npx @davide97g/sonar-autofixer@latest scan" + } +} +``` ## How It Works diff --git a/package.json b/package.json index 766b892..48a5bd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@davide97g/sonar-autofixer", - "version": "1.2.0", + "version": "1.2.1", "description": "CLI utility for fetching SonarQube issues and integrating with Bitbucket/GitHub PR workflows", "main": "dist/cli.js", "bin": { diff --git a/src/cli.ts b/src/cli.ts index f2f16d6..4db4b6f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,6 +4,7 @@ import { Command } from "commander"; import { spawnSync } from "node:child_process"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { readFileSync } from "node:fs"; // ESM-compatible __dirname/__filename const __filename = fileURLToPath(import.meta.url); @@ -11,7 +12,12 @@ const __dirname = path.dirname(__filename); const program = new Command(); -program.name("sonar-autofixer").description("CLI for sonar-autofixer").version("1.0.0"); +// Get current version from package.json +const packageJsonPath = path.join(__dirname, "..", "package.json"); +const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); +const currentVersion = packageJson.version; + +program.name("sonar-autofixer").description("CLI for sonar-autofixer").version(currentVersion); const runNodeScript = (relativeScriptPath: string, args: string[] = []): void => { const scriptPath = path.join(__dirname, relativeScriptPath); @@ -25,12 +31,47 @@ const runNodeScript = (relativeScriptPath: string, args: string[] = []): void => } }; +/** + * Check for available updates + */ +const checkForUpdates = async (): Promise => { + try { + console.log("šŸ” Checking for updates..."); + console.log(`Current version: ${currentVersion}`); + + const packageName = packageJson.name; + + console.log("\nšŸ”„ To get the latest version, use:"); + console.log(`npx ${packageName}@latest `); + + console.log("\nšŸ“ Current commands:"); + console.log(`npx ${packageName}@latest init`); + console.log(`npx ${packageName}@latest fetch`); + console.log(`npx ${packageName}@latest scan`); + console.log(`npx ${packageName}@latest update`); + } catch (error) { + console.error("āŒ Error checking for updates:", error); + } +}; + +/** + * Show update reminder (non-blocking) + */ +const showUpdateReminder = (): void => { + const packageName = packageJson.name; + console.log( + `\nšŸ’” Tip: Use 'npx ${packageName}@latest ' to always get the latest version` + ); + console.log(` Run 'npx ${packageName} update' to check for updates\n`); +}; + program .command("init") .description("Initialize configuration for sonar-autofixer") .allowExcessArguments(true) .action(() => { runNodeScript("./init.js", process.argv.slice(3)); + showUpdateReminder(); }); program @@ -39,6 +80,7 @@ program .allowExcessArguments(true) .action(() => { runNodeScript("./versioning/index.js", process.argv.slice(3)); + showUpdateReminder(); }); program @@ -47,6 +89,14 @@ program .allowExcessArguments(true) .action(() => { runNodeScript("./sonar/scanner.js", process.argv.slice(3)); + showUpdateReminder(); + }); + +program + .command("update") + .description("Check for updates and show how to get the latest version") + .action(async () => { + await checkForUpdates(); }); program.parse(process.argv); From 78d467dcca3baeedf8f4cc3d899ab55784fc2c25 Mon Sep 17 00:00:00 2001 From: Davide Ghiotto Date: Thu, 30 Oct 2025 10:59:20 +0100 Subject: [PATCH 7/8] Refactor package.json and init.ts for improved output and structure - Reformatted the "files" array in package.json for better readability. - Simplified console output in runInit function by removing unnecessary formatting. - Updated target rule path in runInit to use a leading dot for better organization. - Adjusted console clearing behavior in runBanner for a cleaner display. --- package.json | 7 ++++++- src/init.ts | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 48a5bd0..cf34e1e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,12 @@ "sonar:scan": "npx davide97g:sonar-autofixer scan", "sonar:fetch": "npx davide97g:sonar-autofixer fetch" }, - "files": ["dist", "src/templates", "README.md", "package.json"], + "files": [ + "dist", + "src/templates", + "README.md", + "package.json" + ], "repository": { "type": "git", "url": "git+https://github.com/davide97g/sonar-autofixer.git" diff --git a/src/init.ts b/src/init.ts index 7b94aa6..bf8d4be 100644 --- a/src/init.ts +++ b/src/init.ts @@ -41,7 +41,7 @@ interface Config { } const runInit = async (): Promise => { - console.log(dynamicGradient(chalk.bold("Welcome to sonar-autofixer setup!"))); + console.log(dynamicGradient("Welcome to sonar-autofixer setup!")); // Load package.json to derive sensible defaults const pkgPath = path.join(process.cwd(), "package.json"); @@ -200,7 +200,7 @@ const runInit = async (): Promise => { } else if (editor === "windsurf") { targetRulePath = path.join(process.cwd(), ".windsurf/rules/sonar-issue-fix.mdc"); } else { - targetRulePath = path.join(process.cwd(), "rules/sonar-issue-fix.md"); + targetRulePath = path.join(process.cwd(), ".rules/sonar-issue-fix.md"); } await fs.ensureDir(path.dirname(targetRulePath)); @@ -247,7 +247,7 @@ const runBanner = async (): Promise => { setTimeout(() => { clearInterval(interval); - console.clear(); + console.log("\n"); resolve(); }, 4000); } From 5094de05eb1010d5bae1cff6e21824fff4b33e5e Mon Sep 17 00:00:00 2001 From: Davide Ghiotto Date: Thu, 30 Oct 2025 11:10:24 +0100 Subject: [PATCH 8/8] Update version in package.json from 1.2.1 to 0.1.0 and streamline "files" array formatting --- package.json | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index cf34e1e..d86fd06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@davide97g/sonar-autofixer", - "version": "1.2.1", + "version": "0.1.0", "description": "CLI utility for fetching SonarQube issues and integrating with Bitbucket/GitHub PR workflows", "main": "dist/cli.js", "bin": { @@ -18,12 +18,7 @@ "sonar:scan": "npx davide97g:sonar-autofixer scan", "sonar:fetch": "npx davide97g:sonar-autofixer fetch" }, - "files": [ - "dist", - "src/templates", - "README.md", - "package.json" - ], + "files": ["dist", "src/templates", "README.md", "package.json"], "repository": { "type": "git", "url": "git+https://github.com/davide97g/sonar-autofixer.git" @@ -47,9 +42,6 @@ "url": "https://github.com/davide97g/sonar-autofixer/issues" }, "homepage": "https://github.com/davide97g/sonar-autofixer#readme", - "publishConfig": { - "registry": "https://npm.pkg.github.com" - }, "dependencies": { "chalk": "^5.6.2", "commander": "^14.0.2",