diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 000000000..a80f0f04b --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,10 @@ +--- +engines: + eslint-8: + exclude_paths: + - '.github/workflows/sonar.yml' + - 'scripts/sonar-monorepo.ts' + opengrep: + exclude_paths: + - '.github/workflows/sonar.yml' + - 'scripts/sonar-monorepo.ts' diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 000000000..f0ddd55e6 --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,75 @@ +name: Sonar + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + validate: + name: Validate Sonar token + runs-on: ubuntu-latest + outputs: + valid: ${{ steps.check.outputs.valid }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - id: check + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + if [ -z "$SONAR_TOKEN" ]; then + echo '::error::SONAR_TOKEN is not set' + exit 1 + fi + code=$(curl -s -o /dev/null -u "$SONAR_TOKEN": -w '%{http_code}' 'https://api.sonarcloud.io/analysis/jres?os=linux&arch=x86_64') + if [ "$code" = '200' ]; then + echo 'valid=true' >> "$GITHUB_OUTPUT" + else + echo "::error::SonarCloud token validation failed with HTTP $code" + exit 1 + fi + + matrix: + name: Generate Sonar matrix + needs: validate + if: needs.validate.outputs.valid == 'true' + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - id: matrix + run: | + printf 'matrix=%s\n' "$(jq -c . sonar-matrix.json)" >> "$GITHUB_OUTPUT" + + sonar: + name: Sonar (${{ matrix.projectName }}) + needs: [validate, matrix] + if: needs.validate.outputs.valid == 'true' && needs.matrix.outputs.matrix != '' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8 # nosemgrep: generic.secrets.security.detected-sonarqube-docs-api-key + with: + args: > + -Dsonar.projectKey=${{ matrix.projectKey }} + -Dsonar.projectName=${{ matrix.projectName }} + -Dsonar.sources=${{ matrix.sources }} + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/scripts/sonar-monorepo.ts b/scripts/sonar-monorepo.ts new file mode 100644 index 000000000..b5a31d2c7 --- /dev/null +++ b/scripts/sonar-monorepo.ts @@ -0,0 +1,203 @@ +#!/usr/bin/env bun +/** + * Generate SonarQube Cloud monorepo artifacts for the abapify Nx workspace: + * - sonar-monorepo.json -> bulk import into SonarQube Cloud + * - sonar-matrix.json -> matrix used by .github/workflows/sonar.yml + * + * Run with: bunx tsx scripts/sonar-monorepo.ts + */ +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; +import { + existsSync, + readdirSync, + readFileSync, + statSync, + writeFileSync, +} from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const execAsync = promisify(exec); + +const ROOT = dirname(dirname(fileURLToPath(import.meta.url))); + +const ORG = 'abapify'; +const MONOREPO = 'adt-cli'; +const EXISTING_ADT_CLI_KEY = `${ORG}_${MONOREPO}`; + +interface NxProject { + name: string; + root: string; + sourceRoot: string | null; +} + +interface SonarProject { + projectKey: string; + projectName: string; + sources: string; +} + +async function run(cmd: string): Promise { + const { stdout } = await execAsync(cmd, { + cwd: ROOT, + maxBuffer: 1024 * 1024, + }); + return stdout.trim(); +} + +function hasSourceFiles(dir: string): boolean { + if (!existsSync(dir)) return false; + const entries = readdirSync(dir); + return entries.some((entry) => { + const full = join(dir, entry); + const stat = statSync(full); + if (stat.isDirectory()) return false; + return /\.(ts|tsx|js|mjs|cjs|jsx|css|scss|yml|yaml|sh)$/.test(entry); + }); +} + +function hasDirFiles(dir: string): boolean { + return existsSync(dir) && readdirSync(dir).length > 0; +} + +function readPackageName(root: string): string | undefined { + const pkgPath = join(ROOT, root, 'package.json'); + if (!existsSync(pkgPath)) return undefined; + try { + return (JSON.parse(readFileSync(pkgPath, 'utf-8')) as { name?: string }) + .name; + } catch { + return undefined; + } +} + +function sanitizeKeyPart(part: string): string { + return part.replace(/[^A-Za-z0-9._:-]/g, '_'); +} + +function computeKey({ + root, + packageName, + nxName, +}: { + root: string; + packageName: string | undefined; + nxName: string; +}): string { + if (root === '.') { + return `${ORG}_${MONOREPO}_root`; + } + + const base = root.split('/').pop() ?? nxName; + const suffixSource = packageName ?? base; + const suffix = suffixSource.replace(/^@abapify\//, ''); + const cleanSuffix = sanitizeKeyPart(suffix); + + if (root === 'packages/adt-cli' && cleanSuffix === 'adt-cli') { + // Preserve the existing single-project key for the main CLI package. + return EXISTING_ADT_CLI_KEY; + } + + return `${ORG}_${MONOREPO}_${cleanSuffix}`; +} + +function computeName({ + nxName, + packageName, +}: { + nxName: string; + packageName: string | undefined; +}): string { + return packageName ?? nxName; +} + +function determineSources({ + root, + sourceRoot, +}: { + root: string; + sourceRoot: string | null; +}): string | null { + if (root === '.') { + const parts: string[] = []; + if (hasDirFiles(join(ROOT, 'src'))) parts.push('src'); + if (hasDirFiles(join(ROOT, '.github'))) parts.push('.github'); + return parts.length > 0 ? parts.join(',') : null; + } + + const srcDir = join(ROOT, root, 'src'); + if (hasDirFiles(srcDir)) { + return `${root}/src`; + } + + if (sourceRoot && hasDirFiles(join(ROOT, sourceRoot))) { + return sourceRoot; + } + + if (hasSourceFiles(join(ROOT, root))) { + return root; + } + + return null; +} + +async function main(): Promise { + const projectsRaw = await run('bunx nx show projects --json'); + const projectNames: string[] = JSON.parse(projectsRaw) as string[]; + + const details = await Promise.all( + projectNames.map(async (name) => { + const raw = await run(`bunx nx show project ${name} --json`); + return JSON.parse(raw) as NxProject; + }), + ); + + const projects: SonarProject[] = []; + + for (const p of details) { + const sources = determineSources(p); + if (!sources) { + // eslint-disable-next-line no-console + console.log(`Skipping ${p.name}: no analyzable source directory`); + continue; + } + + const packageName = readPackageName(p.root); + const projectName = computeName({ nxName: p.name, packageName }); + const projectKey = computeKey({ + root: p.root, + packageName, + nxName: p.name, + }); + + projects.push({ projectKey, projectName, sources }); + } + + const importFile = projects.map(({ projectKey, projectName }) => ({ + projectKey, + projectName, + })); + + const matrixFile = { include: projects }; + + writeFileSync( + join(ROOT, 'sonar-monorepo.json'), + JSON.stringify(importFile, null, 2) + '\n', + ); + writeFileSync( + join(ROOT, 'sonar-matrix.json'), + JSON.stringify(matrixFile, null, 2) + '\n', + ); + + // eslint-disable-next-line no-console + console.log(`Generated ${projects.length} Sonar projects`); +} + +try { + await main(); +} catch (err) { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); +} diff --git a/sonar-matrix.json b/sonar-matrix.json new file mode 100644 index 000000000..e7eead9de --- /dev/null +++ b/sonar-matrix.json @@ -0,0 +1,234 @@ +{ + "include": [ + { + "projectKey": "abapify_adt-cli_adt-plugin-gcts-cli", + "projectName": "@abapify/adt-plugin-gcts-cli", + "sources": "packages/adt-plugin-gcts-cli/src" + }, + { + "projectKey": "abapify_adt-cli_adt-plugin-abapgit", + "projectName": "@abapify/adt-plugin-abapgit", + "sources": "packages/adt-plugin-abapgit/src" + }, + { + "projectKey": "abapify_adt-cli_adt-server-client", + "projectName": "@abapify/adt-server-client", + "sources": "packages/adt-server-client/src" + }, + { + "projectKey": "abapify_adt-cli_adt-plugin-gcts", + "projectName": "@abapify/adt-plugin-gcts", + "sources": "packages/adt-plugin-gcts/src" + }, + { + "projectKey": "abapify_adt-cli_adt-playwright", + "projectName": "@abapify/adt-playwright", + "sources": "packages/adt-playwright/src" + }, + { + "projectKey": "abapify_adt-cli_openai-codegen", + "projectName": "@abapify/openai-codegen", + "sources": "packages/openai-codegen/src" + }, + { + "projectKey": "abapify_adt-cli_adt-puppeteer", + "projectName": "@abapify/adt-puppeteer", + "sources": "packages/adt-puppeteer/src" + }, + { + "projectKey": "abapify_adt-cli_adt-contracts", + "projectName": "@abapify/adt-contracts", + "sources": "packages/adt-contracts/src" + }, + { + "projectKey": "abapify_adt-cli_asjson-parser", + "projectName": "@abapify/asjson-parser", + "sources": "packages/asjson-parser/src" + }, + { + "projectKey": "abapify_adt-cli_adt-fixtures", + "projectName": "@abapify/adt-fixtures", + "sources": "packages/adt-fixtures/src" + }, + { + "projectKey": "abapify_adt-cli_browser-auth", + "projectName": "@abapify/browser-auth", + "sources": "packages/browser-auth/src" + }, + { + "projectKey": "abapify_adt-cli_sample-tsdown", + "projectName": "@abapify/sample-tsdown", + "sources": "samples/sample-tsdown/src" + }, + { + "projectKey": "abapify_adt-cli_adt-codegen", + "projectName": "@abapify/adt-codegen", + "sources": "packages/adt-codegen/src" + }, + { + "projectKey": "abapify_adt-cli_adt-schemas", + "projectName": "@abapify/adt-schemas", + "sources": "packages/adt-schemas/src" + }, + { + "projectKey": "abapify_adt-cli_adt-client", + "projectName": "@abapify/adt-client", + "sources": "packages/adt-client/src" + }, + { + "projectKey": "abapify_adt-cli_adt-config", + "projectName": "@abapify/adt-config", + "sources": "packages/adt-config/src" + }, + { + "projectKey": "abapify_adt-cli_adt-export", + "projectName": "@abapify/adt-export", + "sources": "packages/adt-export/src" + }, + { + "projectKey": "abapify_adt-cli_adt-plugin", + "projectName": "@abapify/adt-plugin", + "sources": "packages/adt-plugin/src" + }, + { + "projectKey": "abapify_adt-cli_adt-server", + "projectName": "@abapify/adt-server", + "sources": "packages/adt-server/src" + }, + { + "projectKey": "abapify_adt-cli_adt-aunit", + "projectName": "@abapify/adt-aunit", + "sources": "packages/adt-aunit/src" + }, + { + "projectKey": "abapify_adt-cli_adt-locks", + "projectName": "@abapify/adt-locks", + "sources": "packages/adt-locks/src" + }, + { + "projectKey": "abapify_adt-cli_adt-pilot", + "projectName": "@abapify/adt-pilot", + "sources": "packages/adt-pilot/src" + }, + { + "projectKey": "abapify_adt-cli_adt-proxy", + "projectName": "@abapify/adt-proxy", + "sources": "packages/adt-proxy/src" + }, + { + "projectKey": "abapify_adt-cli_nx-npm-trust", + "projectName": "@abapify/nx-npm-trust", + "sources": "tools/nx-npm-trust/src" + }, + { + "projectKey": "abapify_adt-cli_nx-typecheck", + "projectName": "@abapify/nx-typecheck", + "sources": "tools/nx-typecheck/src" + }, + { + "projectKey": "abapify_adt-cli_abap-ast", + "projectName": "@abapify/abap-ast", + "sources": "packages/abap-ast/src" + }, + { + "projectKey": "abapify_adt-cli_adt-lint", + "projectName": "@abapify/adt-lint", + "sources": "packages/adt-lint/src" + }, + { + "projectKey": "abapify_adt-cli_adt-auth", + "projectName": "@abapify/adt-auth", + "sources": "packages/adt-auth/src" + }, + { + "projectKey": "abapify_adt-cli_adt-diff", + "projectName": "@abapify/adt-diff", + "sources": "packages/adt-diff/src" + }, + { + "projectKey": "abapify_adt-cli_adt-atc", + "projectName": "@abapify/adt-atc", + "sources": "packages/adt-atc/src" + }, + { + "projectKey": "abapify_adt-cli", + "projectName": "@abapify/adt-cli", + "sources": "packages/adt-cli/src" + }, + { + "projectKey": "abapify_adt-cli_adt-mcp", + "projectName": "@abapify/adt-mcp", + "sources": "packages/adt-mcp/src" + }, + { + "projectKey": "abapify_adt-cli_adt-rfc", + "projectName": "@abapify/adt-rfc", + "sources": "packages/adt-rfc/src" + }, + { + "projectKey": "abapify_adt-cli_adt-tui", + "projectName": "@abapify/adt-tui", + "sources": "packages/adt-tui/src" + }, + { + "projectKey": "abapify_adt-cli_aclass", + "projectName": "@abapify/aclass", + "sources": "packages/aclass/src" + }, + { + "projectKey": "abapify_adt-cli_logger", + "projectName": "@abapify/logger", + "sources": "packages/logger/src" + }, + { + "projectKey": "abapify_adt-cli_nx-vitest", + "projectName": "@abapify/nx-vitest", + "sources": "tools/nx-vitest/src" + }, + { + "projectKey": "abapify_adt-cli_nx-tsdown", + "projectName": "@abapify/nx-tsdown", + "sources": "tools/nx-tsdown/src" + }, + { + "projectKey": "abapify_adt-cli_ts-xsd", + "projectName": "@abapify/ts-xsd", + "sources": "packages/ts-xsd/src" + }, + { + "projectKey": "abapify_adt-cli_speci", + "projectName": "@abapify/speci", + "sources": "packages/speci/src" + }, + { + "projectKey": "abapify_adt-cli_acds", + "projectName": "@abapify/acds", + "sources": "packages/acds/src" + }, + { + "projectKey": "abapify_adt-cli_nx-sync", + "projectName": "@abapify/nx-sync", + "sources": "tools/nx-sync/src" + }, + { + "projectKey": "abapify_adt-cli_adk", + "projectName": "@abapify/adk", + "sources": "packages/adk/src" + }, + { + "projectKey": "abapify_adt-cli_p2-cli", + "projectName": "@abapify/p2-cli", + "sources": "tools/p2-cli/src" + }, + { + "projectKey": "abapify_adt-cli_adt-cli-docs", + "projectName": "adt-cli-docs", + "sources": "website/src" + }, + { + "projectKey": "abapify_adt-cli_root", + "projectName": "abapify", + "sources": "src,.github" + } + ] +} diff --git a/sonar-monorepo.json b/sonar-monorepo.json new file mode 100644 index 000000000..0e47063e2 --- /dev/null +++ b/sonar-monorepo.json @@ -0,0 +1,186 @@ +[ + { + "projectKey": "abapify_adt-cli_adt-plugin-gcts-cli", + "projectName": "@abapify/adt-plugin-gcts-cli" + }, + { + "projectKey": "abapify_adt-cli_adt-plugin-abapgit", + "projectName": "@abapify/adt-plugin-abapgit" + }, + { + "projectKey": "abapify_adt-cli_adt-server-client", + "projectName": "@abapify/adt-server-client" + }, + { + "projectKey": "abapify_adt-cli_adt-plugin-gcts", + "projectName": "@abapify/adt-plugin-gcts" + }, + { + "projectKey": "abapify_adt-cli_adt-playwright", + "projectName": "@abapify/adt-playwright" + }, + { + "projectKey": "abapify_adt-cli_openai-codegen", + "projectName": "@abapify/openai-codegen" + }, + { + "projectKey": "abapify_adt-cli_adt-puppeteer", + "projectName": "@abapify/adt-puppeteer" + }, + { + "projectKey": "abapify_adt-cli_adt-contracts", + "projectName": "@abapify/adt-contracts" + }, + { + "projectKey": "abapify_adt-cli_asjson-parser", + "projectName": "@abapify/asjson-parser" + }, + { + "projectKey": "abapify_adt-cli_adt-fixtures", + "projectName": "@abapify/adt-fixtures" + }, + { + "projectKey": "abapify_adt-cli_browser-auth", + "projectName": "@abapify/browser-auth" + }, + { + "projectKey": "abapify_adt-cli_sample-tsdown", + "projectName": "@abapify/sample-tsdown" + }, + { + "projectKey": "abapify_adt-cli_adt-codegen", + "projectName": "@abapify/adt-codegen" + }, + { + "projectKey": "abapify_adt-cli_adt-schemas", + "projectName": "@abapify/adt-schemas" + }, + { + "projectKey": "abapify_adt-cli_adt-client", + "projectName": "@abapify/adt-client" + }, + { + "projectKey": "abapify_adt-cli_adt-config", + "projectName": "@abapify/adt-config" + }, + { + "projectKey": "abapify_adt-cli_adt-export", + "projectName": "@abapify/adt-export" + }, + { + "projectKey": "abapify_adt-cli_adt-plugin", + "projectName": "@abapify/adt-plugin" + }, + { + "projectKey": "abapify_adt-cli_adt-server", + "projectName": "@abapify/adt-server" + }, + { + "projectKey": "abapify_adt-cli_adt-aunit", + "projectName": "@abapify/adt-aunit" + }, + { + "projectKey": "abapify_adt-cli_adt-locks", + "projectName": "@abapify/adt-locks" + }, + { + "projectKey": "abapify_adt-cli_adt-pilot", + "projectName": "@abapify/adt-pilot" + }, + { + "projectKey": "abapify_adt-cli_adt-proxy", + "projectName": "@abapify/adt-proxy" + }, + { + "projectKey": "abapify_adt-cli_nx-npm-trust", + "projectName": "@abapify/nx-npm-trust" + }, + { + "projectKey": "abapify_adt-cli_nx-typecheck", + "projectName": "@abapify/nx-typecheck" + }, + { + "projectKey": "abapify_adt-cli_abap-ast", + "projectName": "@abapify/abap-ast" + }, + { + "projectKey": "abapify_adt-cli_adt-lint", + "projectName": "@abapify/adt-lint" + }, + { + "projectKey": "abapify_adt-cli_adt-auth", + "projectName": "@abapify/adt-auth" + }, + { + "projectKey": "abapify_adt-cli_adt-diff", + "projectName": "@abapify/adt-diff" + }, + { + "projectKey": "abapify_adt-cli_adt-atc", + "projectName": "@abapify/adt-atc" + }, + { + "projectKey": "abapify_adt-cli", + "projectName": "@abapify/adt-cli" + }, + { + "projectKey": "abapify_adt-cli_adt-mcp", + "projectName": "@abapify/adt-mcp" + }, + { + "projectKey": "abapify_adt-cli_adt-rfc", + "projectName": "@abapify/adt-rfc" + }, + { + "projectKey": "abapify_adt-cli_adt-tui", + "projectName": "@abapify/adt-tui" + }, + { + "projectKey": "abapify_adt-cli_aclass", + "projectName": "@abapify/aclass" + }, + { + "projectKey": "abapify_adt-cli_logger", + "projectName": "@abapify/logger" + }, + { + "projectKey": "abapify_adt-cli_nx-vitest", + "projectName": "@abapify/nx-vitest" + }, + { + "projectKey": "abapify_adt-cli_nx-tsdown", + "projectName": "@abapify/nx-tsdown" + }, + { + "projectKey": "abapify_adt-cli_ts-xsd", + "projectName": "@abapify/ts-xsd" + }, + { + "projectKey": "abapify_adt-cli_speci", + "projectName": "@abapify/speci" + }, + { + "projectKey": "abapify_adt-cli_acds", + "projectName": "@abapify/acds" + }, + { + "projectKey": "abapify_adt-cli_nx-sync", + "projectName": "@abapify/nx-sync" + }, + { + "projectKey": "abapify_adt-cli_adk", + "projectName": "@abapify/adk" + }, + { + "projectKey": "abapify_adt-cli_p2-cli", + "projectName": "@abapify/p2-cli" + }, + { + "projectKey": "abapify_adt-cli_adt-cli-docs", + "projectName": "adt-cli-docs" + }, + { + "projectKey": "abapify_adt-cli_root", + "projectName": "abapify" + } +] diff --git a/sonar-project.properties b/sonar-project.properties index f5217bfc1..e4a1a42e4 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -84,6 +84,10 @@ sonar.issue.ignore.multicriteria.h13.resourceKey=.github/workflows/** sonar.organization=abapify sonar.projectKey=abapify_adt-cli +# Default sources for the main @abapify/adt-cli project. Monorepo scans override +# this per-project via -Dsonar.sources in the GitHub Actions workflow. +sonar.sources=packages/adt-cli/src + # ── Test coverage ────────────────────────────────────────────────────── # TypeScript coverage for this monorepo (generated by `bunx nx test` # when a reporter writes out lcov / jacoco). Adjust the path if you