diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..564be13 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,74 @@ +name: Coverage + +on: + push: + branches: ["main", "release/**"] + pull_request: + branches: ["main", "release/**"] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + coverage: + runs-on: ubuntu-latest + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Java 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: gradle + + - name: Grant execute permission for Gradle wrapper + run: chmod +x ./gradlew + + - name: Run tests + JaCoCo coverage + run: ./gradlew clean coverage --no-daemon + + - name: Build coverage summary + id: jacoco + uses: madrapps/jacoco-report@v1.7.2 + with: + paths: build/reports/jacoco/test/jacocoTestReport.xml + token: ${{ secrets.GITHUB_TOKEN }} + title: Code Coverage + min-coverage-overall: 0 + min-coverage-changed-files: 0 + update-comment: false + + - name: Add coverage summary to job output + run: | + echo "### Code Coverage" >> "$GITHUB_STEP_SUMMARY" + echo "- Overall: ${{ steps.jacoco.outputs.coverage-overall }}%" >> "$GITHUB_STEP_SUMMARY" + echo "- Changed files: ${{ steps.jacoco.outputs.coverage-changed-files }}%" >> "$GITHUB_STEP_SUMMARY" + + - name: Upload coverage HTML report + uses: actions/upload-artifact@v4 + with: + name: jacoco-html-report + path: build/reports/jacoco/test/html/ + + - name: Upload coverage to Codecov + if: ${{ env.CODECOV_TOKEN != '' }} + uses: codecov/codecov-action@v5 + with: + token: ${{ env.CODECOV_TOKEN }} + files: build/reports/jacoco/test/jacocoTestReport.xml + fail_ci_if_error: false + + - name: Codecov token missing notice + if: ${{ env.CODECOV_TOKEN == '' }} + run: | + echo "### Codecov upload skipped" >> "$GITHUB_STEP_SUMMARY" + echo "- Missing repository secret: CODECOV_TOKEN" >> "$GITHUB_STEP_SUMMARY" + echo "- Add it in GitHub Settings > Secrets and variables > Actions" >> "$GITHUB_STEP_SUMMARY" + diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml new file mode 100644 index 0000000..c61b6af --- /dev/null +++ b/.github/workflows/qodana_code_quality.yml @@ -0,0 +1,36 @@ +#-------------------------------------------------------------------------------# +# Discover all capabilities of Qodana in our documentation # +# https://www.jetbrains.com/help/qodana/about-qodana.html # +#-------------------------------------------------------------------------------# + +name: Qodana + +on: + push: + branches: ["main", "release/**"] + workflow_dispatch: + pull_request: + branches: + - main + - release/** + +permissions: + contents: read + +jobs: + qodana: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2026.1 + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} + with: + pr-mode: false + use-caches: true + post-pr-comment: false + use-annotations: false + upload-result: false + push-fixes: 'none' + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d0043a8..a7db1c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,11 +2,9 @@ name: Test on: push: - branches: - - '**' + branches: ["main", "release/**"] pull_request: - branches: - - '**' + branches: ["main", "release/**"] jobs: test: diff --git a/README.md b/README.md index 8132270..a638313 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ ![Conformance](https://img.shields.io/badge/Conformance-Check--All%20Passing-brightgreen) [![Test](https://github.com/jurgenei/gradle-python-plugin/actions/workflows/test.yml/badge.svg)](https://github.com/jurgenei/gradle-python-plugin/actions/workflows/test.yml) +[![Coverage CI](https://github.com/jurgenei/gradle-python-plugin/actions/workflows/coverage.yml/badge.svg)](https://github.com/jurgenei/gradle-python-plugin/actions/workflows/coverage.yml) +[![Coverage](https://codecov.io/gh/jurgenei/gradle-python-plugin/branch/main/graph/badge.svg)](https://codecov.io/gh/jurgenei/gradle-python-plugin) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) Run Python scripts from Gradle with isolated virtual environments and optional dependency installation. diff --git a/RELEASE_NOTES_NEXT_TAG.md b/RELEASE_NOTES_NEXT_TAG.md new file mode 100644 index 0000000..55cca47 --- /dev/null +++ b/RELEASE_NOTES_NEXT_TAG.md @@ -0,0 +1,22 @@ + +# Release Notes (Next Tag) + +Date: 2026-05-29 +Scope: `gradle-python-plugin` release notes sync for `0.1.1` + +## Highlights + +- Version line remains on `0.1.1` for the release branch. +- Repository metadata and quality/security configuration remain aligned. + +## Notes + +- This release-note update is a repository-level sync entry for the release branch. +- No additional release-note-only behavioral deltas are introduced in this file. + +## Quality Automation + +- Added baseline `qodana.yaml` for JVM community linting on JDK 21. +- Added `.github/workflows/qodana_code_quality.yml` with `main`/`release/**` trigger parity. +- Qodana workflow uses read-only permissions and publishes scan results without auto-fixes. + diff --git a/build.gradle b/build.gradle index f7490dd..d795ca5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java-gradle-plugin' + id 'jacoco' id 'maven-publish' id 'signing' id 'org.owasp.dependencycheck' version '10.0.3' @@ -8,7 +9,7 @@ plugins { } group = 'name.jurgenei.gradle' -version = '0.1.0' +version = '0.1.1' repositories { mavenCentral() @@ -127,5 +128,47 @@ tasks.register('allSecurityChecks') { tasks.withType(Test).configureEach { useJUnitPlatform() + finalizedBy tasks.named('jacocoTestReport') +} + +jacoco { + toolVersion = '0.8.12' +} + +tasks.named('jacocoTestReport') { + dependsOn tasks.named('test') + classDirectories.setFrom(sourceSets.main.output.classesDirs) + sourceDirectories.setFrom(sourceSets.main.allSource.srcDirs) + reports { + xml.required = true + html.required = true + csv.required = false + } +} + +tasks.named('jacocoTestCoverageVerification') { + dependsOn tasks.named('jacocoTestReport') + classDirectories.setFrom(sourceSets.main.output.classesDirs) + sourceDirectories.setFrom(sourceSets.main.allSource.srcDirs) + violationRules { + rule { + element = 'BUNDLE' + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 0.0 + } + } + } +} + +tasks.register('coverage') { + group = 'verification' + description = 'Runs tests, generates JaCoCo report, and verifies minimum coverage threshold.' + dependsOn tasks.named('jacocoTestCoverageVerification') +} + +tasks.named('check') { + dependsOn tasks.named('jacocoTestCoverageVerification') } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 61285a6..b1b8ef5 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 19a6bde..b52fb7e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index adff685..b9bb139 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index e509b2d..aa5f10b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -23,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..629055d --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,50 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# + +################################################################################# +# WARNING: Do not store sensitive information in this file, # +# as its contents will be included in the Qodana report. # +################################################################################# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +projectJDK: "21" #(Applied in CI/CD pipeline) + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +# Quality gate. Will fail the CI/CD pipeline if any condition is not met +# severityThresholds - configures maximum thresholds for different problem severities +# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code +# Code Coverage is available in Ultimate and Ultimate Plus plans +#failureConditions: +# severityThresholds: +# any: 15 +# critical: 5 +# testCoverageThresholds: +# fresh: 70 +# total: 50 + +#Qodana supports other languages, for example, Python, JavaScript, TypeScript, Go, C#, PHP +#For all supported languages see https://www.jetbrains.com/help/qodana/linters.html +linter: jetbrains/qodana-jvm-community:2026.1 + diff --git a/src/main/java/name/jurgenei/gradle/python/PythonRunnerTask.java b/src/main/java/name/jurgenei/gradle/python/PythonRunnerTask.java index 22be617..ebd70af 100644 --- a/src/main/java/name/jurgenei/gradle/python/PythonRunnerTask.java +++ b/src/main/java/name/jurgenei/gradle/python/PythonRunnerTask.java @@ -2,11 +2,7 @@ import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputFile; -import org.gradle.api.tasks.Internal; -import org.gradle.api.tasks.Optional; -import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.*; import java.io.BufferedReader; import java.io.File; @@ -36,11 +32,17 @@ *
  • Runs the configured script and fails the task on non-zero exit code.
  • * */ +@CacheableTask public class PythonRunnerTask extends DefaultTask { private File workDir; + + @PathSensitive(PathSensitivity.RELATIVE) private File script; + + @PathSensitive(PathSensitivity.RELATIVE) private File requirements; + private List args = new ArrayList<>(); private String pythonExecutable = "/usr/bin/python3"; @@ -154,8 +156,7 @@ private void runCommand(File effectiveWorkDir, List cmd) throws Exceptio } private ProcessOutput readProcessOutput(Process process) throws Exception { - ExecutorService executor = Executors.newFixedThreadPool(2); - try { + try (ExecutorService executor = Executors.newFixedThreadPool(2)) { Future stdoutFuture = executor.submit(streamReader(process.getInputStream())); Future stderrFuture = executor.submit(streamReader(process.getErrorStream())); @@ -164,8 +165,6 @@ private ProcessOutput readProcessOutput(Process process) throws Exception { return new ProcessOutput(stdout, stderr); } catch (ExecutionException e) { throw new GradleException("Failed to read process output", e.getCause()); - } finally { - executor.shutdownNow(); } } @@ -231,15 +230,6 @@ public void setRequirements(File requirements) { this.requirements = requirements; } - /** - * Returns the working directory used for command execution and venv storage. - * - * @return working directory or {@code null} when default resolution is used - */ - @Internal - public File getWorkDir() { - return workDir; - } /** * Returns the configured working directory path for incremental input tracking.