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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 40 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,21 @@ on:
- cron: '0 4 * * 1'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 45

steps:
- name: Checkout code
Expand All @@ -27,33 +39,37 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build with Gradle
- name: Build with Gradle (Linux)
if: runner.os == 'Linux'
run: ./gradlew build

- name: Verify with Spotless and SpotBugs
run: ./gradlew check --no-configuration-cache
- name: Build with Gradle (Windows)
if: runner.os == 'Windows'
run: .\gradlew.bat build

- name: Check release helpers
if: runner.os == 'Linux'
run: ./release-lib-test.sh

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
name: test-results-${{ matrix.os }}
path: '**/build/reports/tests/'
retention-days: 7

- name: Upload static analysis reports
uses: actions/upload-artifact@v4
if: always()
with:
name: static-analysis-reports
name: static-analysis-reports-${{ matrix.os }}
path: '**/build/reports/spotbugs/'
retention-days: 7

dependency-check:
runs-on: ubuntu-latest
timeout-minutes: 90
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'

steps:
Expand All @@ -70,11 +86,26 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Scan dependencies for known vulnerabilities
env:
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
run: ./gradlew dependencyCheckAggregate --no-configuration-cache

- name: Check for dependency updates
if: always()
run: ./gradlew dependencyUpdates

- name: Upload dependency-check report
uses: actions/upload-artifact@v4
if: always()
with:
name: dependency-check-report
path: 'build/reports/dependency-check/'
retention-days: 30

- name: Upload dependency report
uses: actions/upload-artifact@v4
if: always()
with:
name: dependency-updates-report
path: 'build/dependencyUpdates/'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ build
/.idea/
/.codex
**/.DS_Store
/release.md.??????
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
- Gradle wrappers (`./gradlew`, per-module wrappers) and `gradle.properties` are the expected entry points; avoid invoking system Gradle directly.

## Build, Test, and Development Commands
- `./gradlew build` — compile all modules, run tests where present.
- `./gradlew build` — compile all modules, run tests, Spotless, and SpotBugs (both static checks are part of `check`).
- `./check.sh` — test the release helpers, reformat with Spotless, then run the full `check`; use this before pushing.
- `./gradlew test` or `./gradlew :gi-fx:test` — run the full suite or a single module; tests execute on JUnit Platform.
- `./gradlew :gi-swing:shadowJar` (likewise `:gi-fx:shadowJar`, `:gi-console:shadowJar`) — produce self-contained artifacts (`*-fatjar.jar`). Note these bundle the runtime classpath but not Groovy, which is a `compileOnly` dependency the consumer supplies.
- `./gradlew dependencyUpdates` — check for newer dependencies with the versions plugin.
- `./checkCVE.sh` — run a fresh aggregate CVE scan after exporting `NVD_API_KEY`.
- Use a JDK with JavaFX when building/running `gi-fx` (e.g., Bellsoft Full JDK); `gi-swing` and `gi-console` work on any Java 21 JDK.

## Coding Style & Naming Conventions
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,27 @@ println("File chosen was $file")
./gradlew build
```

When `org.gradle.configuration-cache=true` is enabled, `build` skips applying Spotless and SpotBugs because their current Gradle tasks are not configuration-cache compatible on Gradle 9. CI therefore runs both commands, and so should you before pushing:
`build` runs tests, Spotless, and SpotBugs, including when Gradle's configuration
cache is enabled. Before pushing, you can run the formatting-and-check wrapper:

```bash
./gradlew build
./gradlew check --no-configuration-cache
./check.sh
```

`check.sh` runs the second command with `spotlessApply` so formatting is fixed rather than merely reported.
`check.sh` tests the release helpers, then runs `spotlessApply` and `check`.

`urlExists` reports unsupported URL schemes and broken redirect chains as
warnings. Connection and HTTP failures are logged at debug level. To see those
details, set the matrix logger to DEBUG and enable `FINE` for the underlying
Java Util Logging configuration.

Table views use `Table` as the window or tab title when neither a nonblank
explicit title nor a matrix name is available.

CI builds all four modules on Linux and Windows. The full dependency CVE scan
runs on the weekly schedule or by manual dispatch because its NVD database
refresh needs an API key and can be rate limited; it is not a PR gate. Run
`./checkCVE.sh` with `NVD_API_KEY` set for a fresh scan before release.

## Requirements

Expand Down
13 changes: 12 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,20 @@ See `req/2026-09-25-fixes.md` for the plan and `release.md` for the changelog.
- [x] Release: `release.sh` version read-back and partial-publish reporting
- [x] Docs: sample scripts updated to 0.4.0 and current Matrix package names

## Review fixes (2026-09-26)
See `req/v0.4.1-fixes.md` for the plan and `release.md` for the changelog.
- [x] Apply Java 21 toolchains to every module
- [x] Include Spotless and SpotBugs in default `check`
- [x] Scan module dependencies with `dependencyCheckAggregate`
- [x] Resolve JavaFX natives per OS and architecture
- [x] Guard null file names, password EOF, and null matrices
- [x] Merge duplicate fat-JAR metadata
- [x] Promote Unreleased notes and validate release versions
- [x] Diagnose `urlExists` failures and cover both shell platforms
- [x] Define the intended null-handling behavior for console `display(String)` — a null name reports `File name is null` without throwing.

## Deferred

- [ ] Define the intended null-handling behavior for console `display(String)`.
- [ ] Tidy the cosmetic Swing dialog title inconsistency.
- [ ] Finish the root `test.groovy` sample update.
- [ ] Review whether JavaFX `TableData` should remain publicly visible.
Expand Down
94 changes: 48 additions & 46 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,80 +10,81 @@ plugins {
id('io.github.gradle-nexus.publish-plugin') version '2.0.0'
id "io.github.ben-manes.versions" version "0.64.0"
id "org.owasp.dependencycheck" version "13.0.0"
id "com.diffplug.spotless" version "8.10.2" apply false
id "com.github.spotbugs" version "6.5.11" apply false
id "com.diffplug.spotless" version "8.10.3"
id "com.github.spotbugs" version "6.5.12" apply false
}

group = 'se.alipsa.gi'
version = '0.4.1-SNAPSHOT'
description = 'Allows Gade Gui Interactive capabilities from a standalone app'

def requestedTaskNames = gradle.startParameter.taskNames.collect { it.toLowerCase(Locale.ROOT) }
def configurationCacheRequested = gradle.startParameter.configurationCacheRequested
def enableSpotless = !configurationCacheRequested || requestedTaskNames.any { it.contains('spotless') }
def enableSpotbugs = !configurationCacheRequested || requestedTaskNames.any { it.contains('spotbugs') }

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}

spotless {
groovyGradle {
target 'build.gradle'
leadingTabsToSpaces(2)
trimTrailingWhitespace()
endWithNewline()
}
}

subprojects {
group = rootProject.group
version = rootProject.version
if (it.plugins.hasPlugin('java')) {
// Configure after each module applies Java; this block runs before module scripts.
pluginManager.withPlugin('java') {
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
}

if (enableSpotless) {
apply plugin: 'com.diffplug.spotless'
spotless {
java {
googleJavaFormat('1.34.0')
}
groovy {
excludeJava()
leadingTabsToSpaces(2)
trimTrailingWhitespace()
endWithNewline()
}
groovyGradle {
leadingTabsToSpaces(2)
trimTrailingWhitespace()
endWithNewline()
}
apply plugin: 'com.diffplug.spotless'
spotless {
java {
googleJavaFormat('1.34.0')
}
groovy {
excludeJava()
leadingTabsToSpaces(2)
trimTrailingWhitespace()
endWithNewline()
}
tasks.matching { it.name.startsWith('spotless') }.configureEach {
notCompatibleWithConfigurationCache("Spotless does not support configuration cache yet")
groovyGradle {
leadingTabsToSpaces(2)
trimTrailingWhitespace()
endWithNewline()
}
}

if (enableSpotbugs) {
apply plugin: 'com.github.spotbugs'
spotbugs {
toolVersion = '4.10.4'
effort = 'max'
reportLevel = 'medium'
excludeFilter = rootProject.file('config/spotbugs-exclude.xml')
apply plugin: 'com.github.spotbugs'
spotbugs {
toolVersion = '4.10.4'
effort = 'max'
reportLevel = 'medium'
excludeFilter = rootProject.file('config/spotbugs-exclude.xml')
}
tasks.withType(com.github.spotbugs.snom.SpotBugsTask).configureEach {
reports {
html.required = true
xml.required = false
}
}
// The SpotBugs plugin assigns auxClassPaths during project evaluation.
// Register this after it has applied so the full compile and runtime paths win.
afterEvaluate {
tasks.withType(com.github.spotbugs.snom.SpotBugsTask).configureEach { spotbugsTask ->
notCompatibleWithConfigurationCache("SpotBugs does not support configuration cache yet")
String suffix = spotbugsTask.name.startsWith('spotbugs') ?
spotbugsTask.name.substring('spotbugs'.length()) : ''
String sourceSetName = suffix.isEmpty() ? 'main' :
suffix.substring(0, 1).toLowerCase(Locale.ROOT) + suffix.substring(1)
String suffix = spotbugsTask.name.substring('spotbugs'.length())
String sourceSetName = suffix.substring(0, 1).toLowerCase(Locale.ROOT) + suffix.substring(1)
def sourceSet = project.sourceSets.findByName(sourceSetName)
if (sourceSet != null) {
auxClassPaths.from(sourceSet.compileClasspath, sourceSet.runtimeClasspath)
}
reports {
html.required = true
xml.required = false
spotbugsTask.auxClassPaths.setFrom(sourceSet.compileClasspath, sourceSet.runtimeClasspath)
}
}
}
Expand Down Expand Up @@ -131,13 +132,14 @@ tasks.named("dependencyUpdates").configure {
}

// OWASP Dependency Check configuration
// Run with: ./gradlew dependencyCheckAnalyze --rerun-tasks --no-configuration-cache --console=plain
// Run with: ./gradlew dependencyCheckAggregate --rerun-tasks --no-configuration-cache --console=plain
// The root has no dependencies; the single-project Analyze task scans nothing.
// Reports are generated in build/reports/dependency-check/
dependencyCheck {
// Read the NVD API key from the environment; never commit it to the repository.
nvd.apiKey = providers.environmentVariable('NVD_API_KEY').orNull
// Scan all configurations including test dependencies
scanConfigurations = ['runtimeClasspath', 'testRuntimeClasspath']
scanConfigurations = ['compileClasspath', 'runtimeClasspath', 'testRuntimeClasspath']
// Fail the build if a CVSS score of 7 or higher is found (HIGH/CRITICAL)
failBuildOnCVSS = 7.0f
// Output formats: HTML, JSON, CSV, XML, SARIF, JUNIT, ALL
Expand Down
6 changes: 6 additions & 0 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ plugins {
id 'groovy-gradle-plugin'
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

repositories {
gradlePluginPortal()
}
Expand Down
25 changes: 24 additions & 1 deletion buildSrc/src/main/groovy/se.alipsa.gi.fatjar-conventions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,33 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow
mergeGroovyExtensionModules()

// Exclude signature files that cause issues when repackaging signed JARs
exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/INDEX.LIST'

// INCLUDE writes non-merged duplicates once per source jar. Module
// descriptors are the only ones that matter here: a fat JAR is not a module,
// and duplicate descriptors make jar tooling warn.
exclude 'module-info.class', 'META-INF/versions/*/module-info.class'

// Preserve bundled licence and notice texts without duplicate archive paths.
transform(com.github.jengelman.gradle.plugins.shadow.transformers.MergeLicenseResourceTransformer) {
artifactLicense = rootProject.file('LICENSE')
artifactLicenseSpdxId = 'MIT'
caseSensitive = false
include 'META-INF/LICENSE*', 'LICENSE*'
}
transform(com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer) {
resource = 'META-INF/NOTICE'
}
// Some bundled artifacts use these distinct paths. Merge each at its own
// path so their attribution survives without duplicate ZIP entries.
transform(com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer) {
resource = 'META-INF/NOTICE.txt'
}
transform(com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer) {
resource = 'NOTICE'
}
transform(com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer) {
resource = 'META-INF/DEPENDENCIES'
}
exclude 'META-INF/buildinfo.xml'
}
Loading
Loading