From 9173ae18f18ed01e0f80ca6233a6e0f84a4f84db Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 23 May 2026 05:41:53 +0000 Subject: [PATCH] refactor(plugin): install via marketplace; skill bundle fetched lazily from Maven Central The plugin is now installed from git via Claude Code's / GitHub Copilot CLI's marketplace, not as a Maven-built zip. Its source lives under /plugin/ in the repo and ships only what is small and immutable per release: the manifest, two named agent variants, and a thin sonar-predictor skill whose launcher fetches the heavy analyzer bundle (~150 MB) from Maven Central on first invocation. Install /plugin marketplace add RandomCodeSpace/sonar-predict /plugin install sonar-predictor@sonar-predict Layout /.claude-plugin/marketplace.json marketplace manifest at repo root /plugin/.claude-plugin/plugin.json plugin manifest /plugin/agents/ two named scanner variants /plugin/skills/sonar-predictor/SKILL.md /plugin/skills/sonar-predictor/bin/sonar bootstrap (bash) /plugin/skills/sonar-predictor/bin/sonar.bat bootstrap (Windows) The bootstrap downloads sonar-predictor-dist-.zip from Maven Central into ${XDG_CACHE_HOME:-~/.cache}/sonar-predictor//, verifies its SHA-1 sidecar, unpacks it, and exec's the cached launcher. Every subsequent call runs from cache with no network. Air-gapped or pre-staged installs override the cache location with SONAR_PREDICTOR_HOME. The pinned VERSION is bumped in lockstep with each plugin release. Removed: the dist module's plugin assembly (dist/src/assembly/plugin*.xml and the build-plugin-{dir,zip} executions) plus the publish workflow's plugin-bundle attach step. The skill bundle assembly stays; it is what Maven Central serves to the bootstrap. Currently the bootstrap pins VERSION=0.1.1, the last bundle on Maven Central that was actually shaped as a skill. v0.1.2's dist artifact on Central is the (now-removed) plugin-bundle shape because the assembly's appendAssemblyId=false collision made the plugin-zip become the attached artifact in that release. v0.1.3 onward will be skill-shaped again. Co-Authored-By: Claude Opus 4.7 --- .claude-plugin/marketplace.json | 24 ++++ .github/workflows/publish.yml | 28 ++--- dist/pom.xml | 31 ----- dist/src/assembly/plugin-zip.xml | 85 -------------- dist/src/assembly/plugin.xml | 107 ------------------ .../.claude-plugin/plugin.json | 4 +- .../agents/sonar-scanner-claude.md | 0 .../agents/sonar-scanner-copilot.md | 0 plugin/skills/sonar-predictor/SKILL.md | 20 ++++ plugin/skills/sonar-predictor/bin/sonar | 88 ++++++++++++++ plugin/skills/sonar-predictor/bin/sonar.bat | 79 +++++++++++++ 11 files changed, 220 insertions(+), 246 deletions(-) create mode 100644 .claude-plugin/marketplace.json delete mode 100644 dist/src/assembly/plugin-zip.xml delete mode 100644 dist/src/assembly/plugin.xml rename {dist/src/main/plugin => plugin}/.claude-plugin/plugin.json (66%) rename {.claude => plugin}/agents/sonar-scanner-claude.md (100%) rename {dist/src/main/plugin => plugin}/agents/sonar-scanner-copilot.md (100%) create mode 100644 plugin/skills/sonar-predictor/SKILL.md create mode 100755 plugin/skills/sonar-predictor/bin/sonar create mode 100644 plugin/skills/sonar-predictor/bin/sonar.bat diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..2fecfdd --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,24 @@ +{ + "name": "sonar-predict", + "owner": { + "name": "Amit Kumar", + "url": "https://github.com/RandomCodeSpace" + }, + "plugins": [ + { + "name": "sonar-predictor", + "source": "./plugin", + "description": "Offline SonarSource quality gate — runs the genuine SonarSource analyzers locally (no network, no server) to catch bugs, code smells, vulnerabilities and security hotspots. The plugin itself is lightweight (kilobytes); the heavy analyzer bundle is fetched from Maven Central on first invocation and cached locally for every subsequent run.", + "homepage": "https://github.com/RandomCodeSpace/sonar-predict", + "license": "Apache-2.0", + "keywords": [ + "code-quality", + "static-analysis", + "sonarsource", + "linter", + "quality-gate", + "offline" + ] + } + ] +} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dcdc66b..6b2ea3e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,10 +9,12 @@ name: Publish to Maven Central + GitHub Release # 2. builds, tests, GPG-signs and deploys protocol/daemon/cli to Maven # Central via the Sonatype Central Portal (the dist module sets # maven.deploy.skip=true so it is built but never staged), -# 3. creates a GitHub Release carrying three bundles: a whole-repo source -# zip (git archive of HEAD), the assembled skill bundle zip, and the -# assembled plugin bundle zip (the .claude-plugin format installable -# on Claude Code and GitHub Copilot CLI). +# 3. creates a GitHub Release carrying two bundles: a whole-repo source +# zip (git archive of HEAD) and the assembled skill bundle zip. The +# plugin itself is installed via the in-repo /plugin/ directory through +# Claude Code's / Copilot CLI's marketplace (see /.claude-plugin/ +# marketplace.json); its skill launcher downloads the analyzer bundle +# on first run from the Maven Central artifact this workflow publishes. # # Required repo secrets: # OSS_NEXUS_USER - Sonatype Central Portal token username @@ -126,20 +128,6 @@ jobs: echo "skill_zip=${SKILL_ZIP}" >> "$GITHUB_OUTPUT" echo "Skill bundle: ${SKILL_ZIP}" - - name: Locate the plugin bundle zip - id: plugin - run: | - set -euo pipefail - VERSION="${{ steps.version.outputs.version }}" - PLUGIN_ZIP="dist/target/sonar-predict-plugin-${VERSION}.zip" - if [ ! -f "${PLUGIN_ZIP}" ]; then - echo "::error::plugin bundle not found at ${PLUGIN_ZIP}" - ls -la dist/target || true - exit 1 - fi - echo "plugin_zip=${PLUGIN_ZIP}" >> "$GITHUB_OUTPUT" - echo "Plugin bundle: ${PLUGIN_ZIP}" - - name: Create the GitHub Release env: GH_TOKEN: ${{ github.token }} @@ -148,7 +136,6 @@ jobs: VERSION="${{ steps.version.outputs.version }}" SRC_ZIP="sonar-predict-${VERSION}-src.zip" SKILL_ZIP="${{ steps.bundle.outputs.skill_zip }}" - PLUGIN_ZIP="${{ steps.plugin.outputs.plugin_zip }}" if [ -n "${GITHUB_REF_NAME:-}" ] && [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then TAG="${GITHUB_REF_NAME}" else @@ -158,5 +145,4 @@ jobs: --title "sonar-predict ${VERSION}" \ --generate-notes \ "${SRC_ZIP}" \ - "${SKILL_ZIP}" \ - "${PLUGIN_ZIP}" + "${SKILL_ZIP}" diff --git a/dist/pom.xml b/dist/pom.xml index 11c8550..c5bc100 100644 --- a/dist/pom.xml +++ b/dist/pom.xml @@ -171,37 +171,6 @@ - - - build-plugin-dir - package - single - - false - plugin - - src/assembly/plugin.xml - - - - - - build-plugin-zip - package - single - - false - sonar-predict-plugin-${project.version} - - src/assembly/plugin-zip.xml - - - diff --git a/dist/src/assembly/plugin-zip.xml b/dist/src/assembly/plugin-zip.xml deleted file mode 100644 index e672ff8..0000000 --- a/dist/src/assembly/plugin-zip.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - plugin-zip - - - - zip - - - sonar-predictor - true - - - - ${project.basedir}/src/main/plugin/.claude-plugin - .claude-plugin - - plugin.json - - - - ${project.basedir}/../.claude/agents - agents - - sonar-scanner-claude.md - - - - ${project.basedir}/src/main/plugin/agents - agents - - sonar-scanner-copilot.md - - - - ${project.basedir}/src/main/skill - skills/sonar-predictor - - SKILL.md - - - - ${project.basedir}/src/main/scripts - skills/sonar-predictor/bin - - sonar - - 0755 - - - ${project.basedir}/src/main/scripts - skills/sonar-predictor/bin - - sonar.bat - - 0644 - - - ${project.build.directory}/plugins - skills/sonar-predictor/plugins - - *.jar - - - - - - - skills/sonar-predictor/lib - false - false - ${artifact.artifactId}.jar - - io.github.randomcodespace.sonarpredict:sonar-predictor-cli - io.github.randomcodespace.sonarpredict:sonar-predictor-daemon - - - - diff --git a/dist/src/assembly/plugin.xml b/dist/src/assembly/plugin.xml deleted file mode 100644 index 7be9977..0000000 --- a/dist/src/assembly/plugin.xml +++ /dev/null @@ -1,107 +0,0 @@ - - - plugin-dir - - - - dir - - - sonar-predictor - true - - - - - ${project.basedir}/src/main/plugin/.claude-plugin - .claude-plugin - - plugin.json - - - - - - ${project.basedir}/../.claude/agents - agents - - sonar-scanner-claude.md - - - - ${project.basedir}/src/main/plugin/agents - agents - - sonar-scanner-copilot.md - - - - - - ${project.basedir}/src/main/skill - skills/sonar-predictor - - SKILL.md - - - - ${project.basedir}/src/main/scripts - skills/sonar-predictor/bin - - sonar - - 0755 - - - ${project.basedir}/src/main/scripts - skills/sonar-predictor/bin - - sonar.bat - - 0644 - - - ${project.build.directory}/plugins - skills/sonar-predictor/plugins - - *.jar - - - - - - - - skills/sonar-predictor/lib - false - false - ${artifact.artifactId}.jar - - io.github.randomcodespace.sonarpredict:sonar-predictor-cli - io.github.randomcodespace.sonarpredict:sonar-predictor-daemon - - - - diff --git a/dist/src/main/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json similarity index 66% rename from dist/src/main/plugin/.claude-plugin/plugin.json rename to plugin/.claude-plugin/plugin.json index ddde8fb..74286f7 100644 --- a/dist/src/main/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "sonar-predictor", - "version": "0.1.1", - "description": "Offline SonarSource quality gate — runs the genuine SonarSource analyzers locally (no network, no server) to catch bugs, code smells, vulnerabilities and security hotspots. Bundles the sonar-predictor scanner skill and two named agent variants — invoke `sonar-scanner-claude` on Claude Code (model: haiku) and `sonar-scanner-copilot` on GitHub Copilot CLI (model: gpt-5-mini). Selection is by agent name; neither is implicitly default.", + "version": "0.1.3", + "description": "Offline SonarSource quality gate — runs the genuine SonarSource analyzers locally (no network, no server) to catch bugs, code smells, vulnerabilities and security hotspots. Ships two named scanner agent variants (sonar-scanner-claude on Claude Code, sonar-scanner-copilot on GitHub Copilot CLI) and a thin sonar-predictor skill whose launcher fetches the analyzer bundle (~150 MB) from Maven Central on first invocation and caches it for every subsequent run.", "author": { "name": "Amit Kumar", "url": "https://github.com/RandomCodeSpace" diff --git a/.claude/agents/sonar-scanner-claude.md b/plugin/agents/sonar-scanner-claude.md similarity index 100% rename from .claude/agents/sonar-scanner-claude.md rename to plugin/agents/sonar-scanner-claude.md diff --git a/dist/src/main/plugin/agents/sonar-scanner-copilot.md b/plugin/agents/sonar-scanner-copilot.md similarity index 100% rename from dist/src/main/plugin/agents/sonar-scanner-copilot.md rename to plugin/agents/sonar-scanner-copilot.md diff --git a/plugin/skills/sonar-predictor/SKILL.md b/plugin/skills/sonar-predictor/SKILL.md new file mode 100644 index 0000000..ecfd2ea --- /dev/null +++ b/plugin/skills/sonar-predictor/SKILL.md @@ -0,0 +1,20 @@ +--- +name: sonar-predictor +description: Use after writing or modifying source code, before committing or pushing — runs genuine SonarSource analyzers offline as a fast local quality gate to catch bugs, code smells, vulnerabilities and security hotspots. Also use when the user asks to check code quality, run sonar, or analyze a file or diff. +--- + +# sonar-predictor + +An offline SonarSource pre-push quality gate — runs the genuine analyzers locally, no network, no server. + +**Scan-only.** This skill only *scans and reports* — it reads source and emits findings, and never modifies any file. Applying fixes is the calling agent's job, not this tool's; running it is a safe, read-only operation. + +Run `./bin/sonar` from this skill's base directory (the folder with this `SKILL.md`), or by its absolute path — it is not on `PATH`. The first invocation downloads the analyzer bundle (~150 MB) from Maven Central into a user cache; every subsequent call runs from that cache with no network. **Read the tool's own help before invoking it:** `./bin/sonar --help` lists the commands and the global options, and `./bin/sonar --help` gives a command's own options and exact argument order. The skill scans a git changeset or explicit files and directories and reports in a chosen format — the help states the precise flags and where each one goes. That generated help is the single source of truth; this `SKILL.md` deliberately does not restate command syntax, which would drift. Do not guess flag names or their placement — read the help. + +Exit codes: `0` clean, `1` issues found, `2` tool error. + +Acting on findings: fix `BUG`/`VULNERABILITY`/`SECURITY_HOTSPOT` and `CRITICAL`/`MAJOR` first. This is a fast first-pass gate, not the release gate — fix the real issues and move on. + +**Air-gapped / pre-staged installs.** Set `SONAR_PREDICTOR_HOME=/path/to/extracted/sonar-predictor` to point the launcher at a pre-downloaded bundle and skip the first-run download. + +**Plugin-bundled agent variants.** Two named scanner subagents ship with this plugin: invoke `sonar-scanner-claude` on Claude Code (model: haiku) or `sonar-scanner-copilot` on GitHub Copilot CLI (model: gpt-5-mini). Selection is by agent name — pick the one matching your platform. diff --git a/plugin/skills/sonar-predictor/bin/sonar b/plugin/skills/sonar-predictor/bin/sonar new file mode 100755 index 0000000..077e7b7 --- /dev/null +++ b/plugin/skills/sonar-predictor/bin/sonar @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Bootstrap wrapper for the sonar-predictor skill bundle. +# +# On first invocation, downloads the analyzer bundle (~150 MB) from Maven +# Central into the user cache, verifies its SHA-1, and unpacks it. Every +# subsequent call exec's the cached launcher directly — no network. +# +# Override the bundle location for air-gapped or pre-staged installs: +# SONAR_PREDICTOR_HOME=/path/to/extracted/sonar-predictor + +set -euo pipefail + +# Pinned to the bundle version published to Maven Central. Bumped in lockstep +# with the plugin's release: each tagged plugin release publishes a matching +# sonar-predictor-dist-.zip and updates this pin. +# +# Note: v0.1.2's dist artifact on Central is the (now-removed) plugin-bundle +# shape, not the skill-bundle this wrapper expects. v0.1.1 is the last good +# skill bundle; v0.1.3 onwards will be skill-shaped again. +VERSION="0.1.1" + +GROUP_PATH="io/github/randomcodespace/sonarpredict" +ARTIFACT="sonar-predictor-dist" +BASE_URL="https://repo1.maven.org/maven2/${GROUP_PATH}/${ARTIFACT}/${VERSION}" + +if [ -n "${SONAR_PREDICTOR_HOME:-}" ]; then + SKILL_DIR="$SONAR_PREDICTOR_HOME" +else + CACHE_ROOT="${XDG_CACHE_HOME:-$HOME/.cache}/sonar-predictor" + SKILL_DIR="$CACHE_ROOT/$VERSION" +fi + +REAL_SONAR="$SKILL_DIR/bin/sonar" + +sha1_of() { + if command -v sha1sum >/dev/null 2>&1; then + sha1sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 1 "$1" | awk '{print $1}' + else + echo "sonar-predictor: no sha1sum or shasum found — cannot verify bundle" >&2 + exit 2 + fi +} + +if [ ! -x "$REAL_SONAR" ]; then + for cmd in curl unzip; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "sonar-predictor: required command '$cmd' not found on PATH" >&2 + exit 2 + fi + done + + echo "sonar-predictor: first run — downloading $VERSION bundle from Maven Central..." >&2 + mkdir -p "$(dirname "$SKILL_DIR")" + TMP="$(mktemp -d)" + trap 'rm -rf "$TMP"' EXIT + + ZIP="$TMP/bundle.zip" + SHA="$TMP/bundle.zip.sha1" + + curl -fsSL --retry 3 -o "$ZIP" "$BASE_URL/$ARTIFACT-$VERSION.zip" + curl -fsSL --retry 3 -o "$SHA" "$BASE_URL/$ARTIFACT-$VERSION.zip.sha1" + + EXPECTED="$(awk '{print $1}' "$SHA")" + ACTUAL="$(sha1_of "$ZIP")" + if [ "$EXPECTED" != "$ACTUAL" ]; then + echo "sonar-predictor: SHA-1 verification failed (expected $EXPECTED, got $ACTUAL)" >&2 + exit 2 + fi + + unzip -q "$ZIP" -d "$TMP/extracted" + if [ ! -d "$TMP/extracted/sonar-predictor" ]; then + echo "sonar-predictor: unexpected bundle layout (no sonar-predictor/ dir at zip root)" >&2 + exit 2 + fi + + # If another invocation populated the cache while we were downloading, + # respect its work — atomically rename, ignoring failure on race. + mv "$TMP/extracted/sonar-predictor" "$SKILL_DIR" 2>/dev/null || true + + if [ ! -x "$REAL_SONAR" ]; then + echo "sonar-predictor: bootstrap completed but $REAL_SONAR is not executable" >&2 + exit 2 + fi +fi + +exec "$REAL_SONAR" "$@" diff --git a/plugin/skills/sonar-predictor/bin/sonar.bat b/plugin/skills/sonar-predictor/bin/sonar.bat new file mode 100644 index 0000000..c60592d --- /dev/null +++ b/plugin/skills/sonar-predictor/bin/sonar.bat @@ -0,0 +1,79 @@ +@echo off +rem Bootstrap wrapper for the sonar-predictor skill bundle (Windows). +rem +rem On first invocation, downloads the analyzer bundle (~150 MB) from Maven +rem Central into the user cache, verifies its SHA-1, and unpacks it. Every +rem subsequent call dispatches to the cached launcher directly — no network. +rem +rem Override the bundle location for air-gapped or pre-staged installs: +rem set SONAR_PREDICTOR_HOME=C:\path\to\extracted\sonar-predictor + +setlocal enabledelayedexpansion + +rem v0.1.2's dist artifact on Central is the (now-removed) plugin-bundle shape, +rem not the skill-bundle this wrapper expects. v0.1.1 is the last good skill +rem bundle; v0.1.3 onwards will be skill-shaped again. +set "VERSION=0.1.1" +set "BASE_URL=https://repo1.maven.org/maven2/io/github/randomcodespace/sonarpredict/sonar-predictor-dist/%VERSION%" + +if defined SONAR_PREDICTOR_HOME ( + set "SKILL_DIR=%SONAR_PREDICTOR_HOME%" +) else ( + if defined LOCALAPPDATA ( + set "SKILL_DIR=%LOCALAPPDATA%\sonar-predictor\%VERSION%" + ) else ( + set "SKILL_DIR=%USERPROFILE%\.cache\sonar-predictor\%VERSION%" + ) +) + +set "REAL_SONAR=%SKILL_DIR%\bin\sonar.bat" + +if exist "%REAL_SONAR%" goto :exec + +echo sonar-predictor: first run -- downloading %VERSION% bundle from Maven Central... 1>&2 + +set "TMP=%TEMP%\sonar-predictor-bootstrap-%RANDOM%-%RANDOM%" +mkdir "%TMP%" || (echo sonar-predictor: failed to create temp dir 1>&2 & exit /b 2) + +set "ZIP=%TMP%\bundle.zip" +set "SHA=%TMP%\bundle.zip.sha1" + +powershell -NoProfile -ExecutionPolicy Bypass -Command ^ + "$ProgressPreference='SilentlyContinue'; try { Invoke-WebRequest -Uri '%BASE_URL%/sonar-predictor-dist-%VERSION%.zip' -OutFile '%ZIP%' } catch { exit 2 }" +if errorlevel 1 (echo sonar-predictor: bundle download failed 1>&2 & rmdir /S /Q "%TMP%" & exit /b 2) + +powershell -NoProfile -ExecutionPolicy Bypass -Command ^ + "$ProgressPreference='SilentlyContinue'; try { Invoke-WebRequest -Uri '%BASE_URL%/sonar-predictor-dist-%VERSION%.zip.sha1' -OutFile '%SHA%' } catch { exit 2 }" +if errorlevel 1 (echo sonar-predictor: SHA-1 download failed 1>&2 & rmdir /S /Q "%TMP%" & exit /b 2) + +for /f "tokens=1" %%i in (%SHA%) do set "EXPECTED=%%i" +for /f "tokens=1" %%i in ('powershell -NoProfile -Command "(Get-FileHash -Algorithm SHA1 '%ZIP%').Hash.ToLower()"') do set "ACTUAL=%%i" +if /i not "!EXPECTED!"=="!ACTUAL!" ( + echo sonar-predictor: SHA-1 verification failed ^(expected !EXPECTED!, got !ACTUAL!^) 1>&2 + rmdir /S /Q "%TMP%" + exit /b 2 +) + +powershell -NoProfile -ExecutionPolicy Bypass -Command ^ + "$ProgressPreference='SilentlyContinue'; try { Expand-Archive -Force -Path '%ZIP%' -DestinationPath '%TMP%\extracted' } catch { exit 2 }" +if errorlevel 1 (echo sonar-predictor: extraction failed 1>&2 & rmdir /S /Q "%TMP%" & exit /b 2) + +if not exist "%TMP%\extracted\sonar-predictor" ( + echo sonar-predictor: unexpected bundle layout ^(no sonar-predictor\ at zip root^) 1>&2 + rmdir /S /Q "%TMP%" + exit /b 2 +) + +for %%I in ("%SKILL_DIR%") do set "PARENT=%%~dpI" +if not exist "%PARENT%" mkdir "%PARENT%" +move /Y "%TMP%\extracted\sonar-predictor" "%SKILL_DIR%" >nul +rmdir /S /Q "%TMP%" + +if not exist "%REAL_SONAR%" ( + echo sonar-predictor: bootstrap completed but %REAL_SONAR% is missing 1>&2 + exit /b 2 +) + +:exec +call "%REAL_SONAR%" %* +exit /b %ERRORLEVEL%