diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a97ed2c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: CI Build + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 with JavaFX + uses: actions/setup-java@v4 + with: + distribution: 'liberica' + java-version: '21' + java-package: 'jdk+fx' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew build + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: '**/build/reports/tests/' + retention-days: 7 + + dependency-check: + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 with JavaFX + uses: actions/setup-java@v4 + with: + distribution: 'liberica' + java-version: '21' + java-package: 'jdk+fx' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Check for dependency updates + run: ./gradlew dependencyUpdates + + - name: Upload dependency report + uses: actions/upload-artifact@v4 + with: + name: dependency-updates-report + path: 'build/dependencyUpdates/' + retention-days: 30 diff --git a/AGENTS.md b/AGENTS.md index 9855377..7239819 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,9 +23,13 @@ - JUnit Jupiter is configured; place specs under `module/src/test/groovy` with `*Test.groovy` naming. - Keep UI-heavy code factored so logic can be unit-tested without displays; use resource fixtures already present for rendering assertions. - Run `./gradlew test` before pushing; add focused tests when altering IO, clipboard, or rendering behaviors. +- Always run `./gradlew test` after completing a task to validate changes. ## Commit & Pull Request Guidelines - Follow the existing history: short, imperative messages (e.g., “fix publishing by …”, “improve display methods”). - PRs should list the touched modules (`gi-common`, `gi-fx`, etc.), describe behavior changes, and link issues/tickets. - Include screenshots or gifs for visual tweaks in `gi-fx`/`gi-swing`; note platform specifics if a change is OS-dependent. - Keep release/publishing secrets (signing keys, Sonatype creds) out of the repo; supply them via local `gradle.properties` when needed. + +## Implementation Guidelines +- A task has 3 parts, implementation, tests, and documentation. A task is not done until all 3 parts are completed. diff --git a/TODO.md b/TODO.md index f4b2c13..69a62a1 100644 --- a/TODO.md +++ b/TODO.md @@ -13,11 +13,11 @@ - 2.5. [x] Broaden `urlExists`: close the HTTP connection and treat 2xx/3xx as success to avoid resource leaks and false negatives. ## 3. Build & platform coverage -- 3.1 [ ] Add automated tests (JUnit/Gradle) for helpers like `FileUtils.baseName`/`getResourceUrl` and content-type detection using existing `src/test/resources` assets; add headless checks for table/clipboard adapters where feasible. -- 3.2. [ ] Set up CI (e.g., GitHub Actions) to run `./gradlew build` and `./gradlew dependencyUpdates` across modules to catch regressions and dependency drift. -- 3.3. [ ] Increase Gradle JVM memory: add `org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m` to `gradle.properties` to avoid metaspace warnings during builds. -- 3.4. [ ] Extract duplicated fatJar task logic to a shared Gradle convention plugin instead of repeating in each module. -- 3.5. [ ] Flesh out `release.sh` to automate version bumps, changelog generation, and GitHub releases. +- 3.1 [x] Add automated tests (JUnit/Gradle) for helpers like `FileUtils.baseName`/`getResourceUrl` and content-type detection using existing `src/test/resources` assets; add headless checks for table/clipboard adapters where feasible. +- 3.2. [x] Set up CI (e.g., GitHub Actions) to run `./gradlew build` and `./gradlew dependencyUpdates` across modules to catch regressions and dependency drift. +- 3.3. [x] Increase Gradle JVM memory: add `org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m` to `gradle.properties` to avoid metaspace warnings during builds. +- 3.4. [x] Extract duplicated fatJar task logic to a shared Gradle convention plugin instead of repeating in each module. +- 3.5. [x] Flesh out `release.sh` to automate version bumps, changelog generation, and GitHub releases. ## 4. Code quality & refactoring - 4.1 [ ] Replace 12+ `System.err.println()` calls with a proper logging framework (SLF4J/Logback); locations include `gi-fx/InOut.groovy`, `gi-fx/Viewer.groovy`, `gi-swing/InOut.groovy`. diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..4fa76d0 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,7 @@ +plugins { + id 'groovy-gradle-plugin' +} + +repositories { + gradlePluginPortal() +} diff --git a/buildSrc/src/main/groovy/se.alipsa.gi.fatjar-conventions.gradle b/buildSrc/src/main/groovy/se.alipsa.gi.fatjar-conventions.gradle new file mode 100644 index 0000000..ffdf00f --- /dev/null +++ b/buildSrc/src/main/groovy/se.alipsa.gi.fatjar-conventions.gradle @@ -0,0 +1,31 @@ +/** + * Convention plugin for creating fat JARs with all dependencies bundled. + * Apply this plugin to any module that needs a fat JAR artifact. + * + * Usage in module build.gradle: + * plugins { + * id 'se.alipsa.gi.fatjar-conventions' + * } + */ + +def fatJarTask = tasks.register('fatJar', Jar) { + dependsOn(classes) + archiveClassifier = 'fatjar' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + from { + configurations.runtimeClasspath.collect { + it.isDirectory() ? it : zipTree(it) + } + } + + // Exclude signature files that cause issues when repackaging signed JARs + exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' + + with jar +} + +// Make the regular jar task depend on fatJar so both are built together +tasks.named('jar') { + dependsOn fatJarTask +} diff --git a/gi-console/build.gradle b/gi-console/build.gradle index f55287d..3f57490 100644 --- a/gi-console/build.gradle +++ b/gi-console/build.gradle @@ -3,6 +3,7 @@ plugins { id 'signing' id 'maven-publish' id("se.alipsa.nexus-release-plugin") version '2.0.0' + id 'se.alipsa.gi.fatjar-conventions' } description = 'Allows Gade Gui Interactive capabilities from a standalone console app' @@ -40,33 +41,16 @@ test { useJUnitPlatform() } -def fatJarContainer = tasks.register('fatJar', Jar) { - dependsOn(classes) - archiveClassifier = 'fatjar' - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from { - configurations.runtimeClasspath.collect { - it.isDirectory() ? it : zipTree(it) - } - } - exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' - with jar -} - tasks.named('javadocJar', Jar) { from tasks.named('groovydoc') dependsOn tasks.named('groovydoc') } -jar { - dependsOn fatJarContainer -} - publishing { publications { maven(MavenPublication) { from components.java - artifact(fatJarContainer) + artifact(tasks.named('fatJar')) pom { name = 'Gade standalone UI interaction library for the console' description = "${project.description}" diff --git a/gi-fx/build.gradle b/gi-fx/build.gradle index da9b3ab..a94d120 100644 --- a/gi-fx/build.gradle +++ b/gi-fx/build.gradle @@ -4,6 +4,7 @@ plugins { id 'signing' id 'maven-publish' id("se.alipsa.nexus-release-plugin") version '2.0.0' + id 'se.alipsa.gi.fatjar-conventions' } description = 'Allows Gade Gui Interactive capabilities from a javafx standalone app' @@ -64,25 +65,6 @@ test { useJUnitPlatform() } -def fatJarContainer = tasks.register('fatjar', Jar) { - dependsOn(classes) - archiveClassifier = 'fatjar' - //archiveBaseName = project.name + '-fat' - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from { - configurations.runtimeClasspath.collect { - it.isDirectory() ? it : zipTree(it) - } - } - exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' - with jar -} - - -jar { - dependsOn fatJarContainer -} - tasks.named('javadocJar', Jar) { from tasks.named('groovydoc') dependsOn tasks.named('groovydoc') @@ -92,7 +74,7 @@ publishing { publications { maven(MavenPublication) { from components.java - artifact(fatJarContainer) + artifact(tasks.named('fatJar')) pom { name = 'Gade standalone GUI interaction library' description = "${project.description}" diff --git a/gi-swing/build.gradle b/gi-swing/build.gradle index 066262d..5c79593 100644 --- a/gi-swing/build.gradle +++ b/gi-swing/build.gradle @@ -5,6 +5,7 @@ plugins { id 'maven-publish' id 'signing' id("se.alipsa.nexus-release-plugin") version '2.0.0' + id 'se.alipsa.gi.fatjar-conventions' } description = 'Allows Gade Gui Interactive capabilities from a standalone app' @@ -46,29 +47,11 @@ repositories { } } -def fatJarContainer = tasks.register('fatJar', Jar) { - dependsOn(classes) - archiveClassifier = 'fatjar' - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from { - configurations.runtimeClasspath.collect { - it.isDirectory() ? it : zipTree(it) - } - } - exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' - with jar -} - - -jar { - dependsOn fatJarContainer -} - publishing { publications { maven(MavenPublication) { from components.java - artifact(fatJarContainer) + artifact(tasks.named('fatJar')) pom { name = 'Gade standalone GUI interaction library for Swing' description = "${project.description}" diff --git a/gradle.properties b/gradle.properties index ac1a35f..aedc38f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,4 @@ -org.gradle.configuration-cache=true \ No newline at end of file +org.gradle.configuration-cache=true + +# Increase JVM memory to avoid metaspace warnings during builds +org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m \ No newline at end of file diff --git a/release.sh b/release.sh index c5e19cb..4d2e02a 100755 --- a/release.sh +++ b/release.sh @@ -1 +1,235 @@ -./gradlew clean build publish closeAndReleaseStagingRepositories \ No newline at end of file +#!/usr/bin/env bash +# +# Release script for GuiInteraction +# +# Prerequisites: +# - SDKMAN installed with JDK 21 +# - Signing credentials in gradle.properties (signing.keyId, signing.password, signing.secretKeyRingFile) +# - Sonatype credentials in gradle.properties (sonatypeUsername, sonatypePassword) +# +# Usage: +# ./release.sh - Release current version +# ./release.sh --bump minor - Bump version and release (major, minor, patch) +# ./release.sh --dry-run - Show what would be released without publishing +# +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Initialize SDKMAN and use JDK 21 +if [ -f ~/.sdkman/bin/sdkman-init.sh ]; then + source ~/.sdkman/bin/sdkman-init.sh + sdk use java 21.0.9.fx-librca 2>/dev/null || sdk use java 21-librca 2>/dev/null || echo "Using default Java" +fi + +PROJECT=$(basename "$PWD") +DRY_RUN=false +BUMP_TYPE="" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + --bump) + BUMP_TYPE="$2" + shift 2 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + exit 1 + ;; + esac +done + +# Get current version from build.gradle +get_version() { + grep -E '^\s*version\s*=\s*["'\'']' build.gradle | sed -E 's/^\s*version\s*=\s*["'\'']([^"'\''"]+)["'\''].*/\1/' +} + +# Bump version based on type (major, minor, patch) +bump_version() { + local version=$1 + local type=$2 + local major minor patch + + IFS='.' read -r major minor patch <<< "${version%-SNAPSHOT}" + + case $type in + major) + major=$((major + 1)) + minor=0 + patch=0 + ;; + minor) + minor=$((minor + 1)) + patch=0 + ;; + patch) + patch=$((patch + 1)) + ;; + *) + echo -e "${RED}Invalid bump type: $type. Use major, minor, or patch${NC}" + exit 1 + ;; + esac + + echo "${major}.${minor}.${patch}" +} + +# Update version in build.gradle +update_version() { + local new_version=$1 + sed -i.bak "s/^version = '.*'/version = '${new_version}'/" build.gradle + rm build.gradle.bak + echo -e "${GREEN}Updated version to ${new_version}${NC}" +} + +# Generate changelog entry +generate_changelog() { + local version=$1 + local date=$(date +%Y-%m-%d) + local changelog_file="CHANGELOG.md" + + if [ ! -f "$changelog_file" ]; then + echo "# Changelog" > "$changelog_file" + echo "" >> "$changelog_file" + fi + + # Get commits since last tag + local last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + local commits="" + if [ -n "$last_tag" ]; then + commits=$(git log --oneline "${last_tag}..HEAD" 2>/dev/null || echo "") + else + commits=$(git log --oneline -20 2>/dev/null || echo "") + fi + + # Create changelog entry + local entry="## [${version}] - ${date}\n\n" + if [ -n "$commits" ]; then + entry+="### Changes\n\n" + while IFS= read -r line; do + if [ -n "$line" ]; then + entry+="- ${line#* }\n" + fi + done <<< "$commits" + fi + entry+="\n" + + # Insert after first line (# Changelog) + if [ -f "$changelog_file" ]; then + local temp_file=$(mktemp) + head -2 "$changelog_file" > "$temp_file" + echo -e "$entry" >> "$temp_file" + tail -n +3 "$changelog_file" >> "$temp_file" + mv "$temp_file" "$changelog_file" + echo -e "${GREEN}Updated ${changelog_file}${NC}" + fi +} + +# Publish a subproject +publish() { + local sub=$1 + echo -e "${YELLOW}Publishing $sub to Maven Central...${NC}" + if [ "$DRY_RUN" = true ]; then + echo -e "${YELLOW}[DRY RUN] Would execute: ./gradlew :${sub}:clean :${sub}:build :${sub}:release${NC}" + else + ./gradlew ":${sub}:clean" ":${sub}:build" ":${sub}:release" + fi +} + +# Main script +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN} GuiInteraction Release Script${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" + +CURRENT_VERSION=$(get_version) +echo -e "Current version: ${YELLOW}${CURRENT_VERSION}${NC}" + +# Check for SNAPSHOT +if echo "$CURRENT_VERSION" | grep -q 'SNAPSHOT'; then + echo -e "${RED}Error: Cannot release a SNAPSHOT version${NC}" + echo -e "Remove '-SNAPSHOT' from version or use --bump to create a release version" + exit 1 +fi + +# Check if version has already been released (git tag exists) +TAG="v${CURRENT_VERSION}" +if git rev-parse "$TAG" >/dev/null 2>&1 || git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then + if [ "$DRY_RUN" = true ]; then + echo -e "${YELLOW}Warning: Tag $TAG already exists. This version may have already been released.${NC}" + else + echo -e "${RED}Warning: Version ${CURRENT_VERSION} appears to have already been released (tag $TAG exists).${NC}" + read -p "Continue anyway? [y/N]: " confirm + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + echo -e "${RED}Aborting release.${NC}" + exit 1 + fi + fi +fi +# Handle version bump +if [ -n "$BUMP_TYPE" ]; then + NEW_VERSION=$(bump_version "$CURRENT_VERSION" "$BUMP_TYPE") + echo -e "Bumping version to: ${GREEN}${NEW_VERSION}${NC}" + + if [ "$DRY_RUN" = false ]; then + update_version "$NEW_VERSION" + generate_changelog "$NEW_VERSION" + + # Commit version change + if ! git add build.gradle CHANGELOG.md; then + echo -e "${RED}Error: Failed to add files to git. Please resolve the issue and try again.${NC}" >&2 + exit 1 + fi + if ! git commit -m "Release version ${NEW_VERSION}"; then + echo -e "${RED}Error: Failed to commit version change. Please resolve the issue and try again.${NC}" >&2 + exit 1 + fi + else + echo -e "${YELLOW}[DRY RUN] Would update version to ${NEW_VERSION}${NC}" + fi + + CURRENT_VERSION=$NEW_VERSION +fi + +echo "" +echo -e "Releasing version: ${GREEN}${CURRENT_VERSION}${NC}" +echo "" + +# Run tests first +echo -e "${YELLOW}Running tests...${NC}" +if [ "$DRY_RUN" = true ]; then + echo -e "${YELLOW}[DRY RUN] Would run: ./gradlew test${NC}" +else + ./gradlew test +fi + +# Publish each module +publish 'gi-common' +publish 'gi-console' +publish 'gi-fx' +publish 'gi-swing' + +echo "" +echo -e "${GREEN}========================================${NC}" +if [ "$DRY_RUN" = true ]; then + echo -e "${YELLOW}[DRY RUN] Release simulation complete${NC}" +else + echo -e "${GREEN}$PROJECT v${CURRENT_VERSION} uploaded and released!${NC}" + echo "" + echo "Next steps:" + echo " 1. Create a git tag: git tag -a v${CURRENT_VERSION} -m 'Release ${CURRENT_VERSION}'" + echo " 2. Push the tag: git push origin v${CURRENT_VERSION}" + echo " 3. Create a GitHub release at: https://github.com/Alipsa/GuiInteraction/releases/new" + echo "" + echo "See https://central.sonatype.org/publish/release/ for more info" +fi +echo -e "${GREEN}========================================${NC}"