diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..e400910f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,58 @@ +# Overthere Project Guidelines + +## Architecture + +Overthere is a Java library that abstracts remote file manipulation and process execution behind a protocol-agnostic API (think `java.io.File` and `java.lang.Process` as interfaces). Key layers: + +- **Public API** (`src/main/java/com/xebialabs/overthere/`): `Overthere` (factory), `OverthereConnection`, `OverthereFile`, `OverthereProcess`, `ConnectionOptions`, `CmdLine`/`CmdLineArgument` +- **SPI** (`spi/`): `OverthereConnectionBuilder`, `BaseOverthereConnection`, `BaseOverthereFile`, `Protocol` annotation — extend these when adding a new protocol +- **Protocol implementations**: `local/`, `ssh/`, `cifs/`, `smb/`, `winrm/`, `winrs/`, `gcp/`, `telnet/`, `proxy/` +- **Utilities** (`util/`): `OverthereUtils`, `OverthereFileTransmitter`, `OverthereFileTranscoder` + +**SPI plugin mechanism**: Connection builders are annotated with `@Protocol(name = "...")` and discovered at runtime via classpath scanning (scannit library). `OverthereConnector` instantiates the builder reflectively. See [SshConnectionBuilder.java](../src/main/java/com/xebialabs/overthere/ssh/SshConnectionBuilder.java) and [CifsConnectionBuilder.java](../src/main/java/com/xebialabs/overthere/cifs/CifsConnectionBuilder.java) as reference implementations. + +## Build and Test + +```bash +# Build (Windows) +gradlew clean build + +# Build (Unix) +./gradlew clean build + +# Run only unit tests (no cloud infra needed) +gradlew test + +# Publish to Maven Central (requires PGP key + Central Publisher Portal tokens) +gradle clean build signArchives uploadArchives closeAndPromoteRepository +``` + +- **Java 21** required (`sourceCompatibility = JavaVersion.VERSION_21`) +- **Integration tests** (`itest/`) require live cloud hosts configured via [Overcast](https://github.com/xebialabs/overcast) (`src/test/resources/overcast.conf`) +- `LocalConnectionTest` is the only itest that runs without cloud infra + +## Code Style + +- **Testing framework**: TestNG (not JUnit) — use `org.testng.annotations.@Test`, `@BeforeMethod`, `@AfterMethod` +- **Conditional tests**: Use `@Assumption` from AssumEng (e.g., `@Assumption(methods = "onWindows")`) — see [ItestsBase6Windows.java](../src/test/java/com/xebialabs/overthere/itest/ItestsBase6Windows.java) +- **Assertions**: Hamcrest (`assertThat(..., is(...))` / `equalTo(...)`) preferred over TestNG `assert*` +- **Error handling**: Throw `RuntimeIOException` (not checked exceptions) for I/O failures in `OverthereFile`/`OverthereConnection` implementations +- All source files must carry the GPLv2 copyright header — use the template in [HEADER](../HEADER) + +## Project Conventions + +- **ConnectionOptions**: All protocol-specific options are declared as `public static final String` constants in the corresponding `*ConnectionBuilder`. Boolean/numeric defaults follow the `*_DEFAULT` naming convention (e.g., `CIFS_PORT_DEFAULT`). +- **Integration test hierarchy**: New connection itests extend the chain `ItestsBase1Utils` → `ItestsBase2Basics` → `ItestsBase3Copy` → `ItestsBase4Size` → `ItestsBase5Unix` → `ItestsBase6Windows`; override `getProtocol()`, `getOptions()`, and `getExpectedConnectionClassName()` +- **Examples**: Standalone Maven project under `examples/` demonstrating library usage; kept intentionally simple + +## Integration Points + +- **SSH**: [sshj](https://github.com/hierynomus/sshj) library (`com.hierynomus:sshj`) +- **CIFS/SMB**: jcifs (`jcifs:jcifs`) and smbj for SMB 2.x +- **GCP**: Google Cloud SDK — two flavours: OsLogin ([GcpOsLoginSshConnection.java](../examples/src/main/java/com/xebialabs/overthere/GcpOsLoginSshConnection.java)) and Metadata ([GcpMetaSshConnection.java](../examples/src/main/java/com/xebialabs/overthere/GcpMetaSshConnection.java)) +- **WinRM**: Internal HTTP implementation (`winrm/`) and native `winrs` (`winrs/`) + +## Security + +- GCP credentials are configured via `GCP_CREDENTIALS_TYPE` option; supported types in `GcpCredentialsType` enum (ServiceAccountJsonFile, ServiceAccountToken, OsLogin, etc.) — test key files are in `src/test/resources/gcp/` and must **not** contain real credentials +- Kerberos config for WinRM domain auth lives in `src/test/resources/winrm/conf/krb5.conf` — treat as sensitive diff --git a/.github/skills/create-pr/SKILL.md b/.github/skills/create-pr/SKILL.md new file mode 100644 index 00000000..ed23d77b --- /dev/null +++ b/.github/skills/create-pr/SKILL.md @@ -0,0 +1,190 @@ +--- +name: create-pr +description: "Use this skill to create a pull request after dependency upgrades (or any successful, committed changes). Validates repo state, generates a structured PR description, requires explicit approval before pushing." +argument-hint: "create-pr [--base master]" +--- + +# create-pr Skill + +Generic PR creation after successful changes. Works for any repo — not specific to dependency upgrades. + +## Prerequisites + +### GitHub CLI (`gh`) must be installed and authenticated + +**Install GitHub CLI** (if not already installed): + +```bash +# macOS (Homebrew) +brew install gh + +# Linux (apt) +sudo apt install gh + +# Windows — choose one: +winget install GitHub.cli # Windows 11 / winget (recommended) +choco install gh # Chocolatey +scoop install gh # Scoop +# or: download the *.msi installer from https://github.com/cli/cli/releases +``` + +**Authenticate:** +```bash +gh auth status # check current auth +gh auth login # if not authenticated +``` + +**Requirements before invoking this skill:** +- Changes must be committed (or staged for commit) +- Build must be green (`./gradlew clean build`) +- You must **not** be on `master` or `main` (or be prepared to create a new branch) + +--- + +## Workflow + +### Step 1 — Validate repo state + +```bash +git status +git branch --show-current +git log master..HEAD --oneline +``` + +Check: +- Not on `master`/`main` → if so, go to Step 2 branch handling +- Has at least one commit ahead of base branch +- No unrelated staged changes (warn user if mix of changes detected) + +### Step 2 — Branch handling + +If already on a feature branch: proceed. + +If on `master`/`main`: +> "You are on `master`. A new branch is needed. Suggest: `chore/deps-update-YYYY-MM-DD`. Use this name or provide your own?" + +Create branch only after explicit confirmation: +```bash +git checkout -b chore/deps-update-2026-03-27 +``` + +### Step 3 — Build validation + +Check the [Jenkinsfile](../../Jenkinsfile) for the canonical build steps. For overthere: + +```bash +# Standard build (always run) +./gradlew clean build +# Windows: +gradlew.bat clean build + +# Integration tests (run if SSH/SMB/WinRM/CIFS deps changed) +# Requires Overcast cloud infra — skip if not available: +# ./gradlew itest +``` + +**STOP if the build fails.** Do not create a PR with a broken build. + +### Step 4 — Collect change context + +```bash +git diff --name-only master...HEAD +git diff master...HEAD +``` + +Also read (if available): +- `dependency-upgrade-report.md` — summary of applied upgrades +- `dependency-upgrade-report.json` — structured upgrade data + +### Step 5 — Generate PR title + +Conventional commit format: + +``` +chore(deps): upgrade {primary-libraries} +``` + +Examples: +- `chore(deps): upgrade slf4j 2.0.13→2.0.17, bouncycastle 1.80→1.83` +- `chore(deps): upgrade sshj 0.38→0.40, smbj 0.13→0.14` +- `chore(deps): upgrade 8 patch/minor dependencies` + +### Step 6 — Generate PR description + +Use this template (remove irrelevant lines; keep checkboxes **unchecked**): + +```markdown +**Summary** +- Dependency upgrades applied via dependency-scan skill. +- {Briefly describe which libraries were upgraded and why (e.g. security patch, new features needed)}. + +**What Changed** +- [ ] Dependency version bumps in `build.gradle` +- [ ] Build/configuration updates (if any) +- [ ] Tests/docs updates (if any) + +**Validation** +- [ ] `./gradlew clean build` passes +- [ ] Integration tests considered (SSH/SMB/WinRM paths affected? If yes, run `./gradlew itest`) + +**Risk Notes** +- [ ] Backward compatibility considered +- [ ] Security-category updates reviewed against changelogs +- [ ] Major version bumps (if any) reviewed for API changes + +**Optional Dependency Notes** _(include only when relevant)_ +- [ ] Reviewed `dependency-upgrade-report.md` +- [ ] Related versions kept in sync (e.g. bcprov + bcpkix, slf4j-api + jcl-over-slf4j) + +**Checklist** +- [ ] Branch pushed to origin +- [ ] Build is green +- [ ] PR labels added as needed +- [ ] Ready for review +``` + +**CI labels to consider** (add with `--label`): +- `ci-skip-integration-tests` — when only build plugin or test-scope deps changed +- `ci-run-platform-build` — when runtime deps changed and full verification is needed +- `ci-skip-sonar-analysis` — if sonar quota is low and changes are mechanical + +### Step 7 — Show to user and ask for approval + +Display the generated title and body in full, then: +> "Create PR with the above title and description? (yes / edit / no)" + +If `edit`: open editor or accept freeform corrections before proceeding. + +### Step 8 — Push and create PR + +```bash +git push --set-upstream origin + +gh pr create \ + --base master \ + --title "" \ + --body "<BODY>" + +# With labels: +gh pr create \ + --base master \ + --title "<TITLE>" \ + --body "<BODY>" \ + --label "ci-skip-integration-tests" +``` + +### Step 9 — Return PR URL + +Report back: +> "PR created: https://github.com/xebialabs/overthere/pull/NNN" + +--- + +## Guardrails + +- ❌ Never create PR without explicit user `yes` +- ❌ Never skip build validation +- ❌ Never run destructive git commands (`--force`, `reset --hard`, `push --force`) +- ❌ Never invent versions or change summaries — use actual `git diff` and report data +- ✅ Always check `gh auth status` before attempting PR creation +- ✅ Always warn if there are uncommitted changes before pushing diff --git a/.github/skills/dependency-scan/SKILL.md b/.github/skills/dependency-scan/SKILL.md new file mode 100644 index 00000000..fefa6689 --- /dev/null +++ b/.github/skills/dependency-scan/SKILL.md @@ -0,0 +1,83 @@ +--- +name: dependency-scan +description: "Use this skill to scan overthere's dependencies for available updates. Runs 3 phases: xl-platform BOM scan (optional), repo hardcoded dep scan, and alignment check." +argument-hint: "dependency-scan [--phase all|xl-platform-scan|scan|alignment] [--xl-platform-dir ../xl-platform]" +--- + +# dependency-scan Skill — overthere + +Orchestrates a full dependency scan across all three phases and produces upgrade reports. + +## Phases + +### Phase 1 — xl-platform scan (delegated to `xl-platform-scan` skill) + +Scans the xl-platform BOM for upstream dependency updates. See [`xl-platform-scan` skill](../xl-platform-scan/SKILL.md) for full details. + +> **Note**: `overthere` does not consume the xl-platform BOM directly. Run Phase 1 only if you are also maintaining `xl-platform` and want to propagate upstream updates. + +Parses: +- `xl-reference/xl-reference.conf` (~100 unique vars) +- `xl-jakartaee-bom/xl-jakartaee-bom.conf` (~30 vars) +- xl-platform's `gradle.properties` (~5 build plugin vars) +- **~135 total unique version variables** + +Resolves via `XL_PLATFORM_COORDINATES` (114 entries) + `SKIP_VARIABLES` (24 entries); dynamic inference (46 patterns) handles unmapped vars. + +### Phase 2 — repo scan (overthere) + +Scans `overthere`'s own dependency files for hardcoded versions not managed by any BOM. + +| Source file | What it contains | +|---|---| +| `build.gradle` (buildscript block) | Gradle build plugin versions: nexus-staging, nebula-release, gradle-pom, grgit, license | +| `build.gradle` (dependencies block) | All library versions: sshj, smbj, jcifs, bouncycastle, httpcomponents, dom4j, jaxen, grpc, GCP, testng, mockito, guava, logback, etc. | +| `gradle.properties` | JVM args only — no library versions defined here | + +Uses: +- `REPO_COORDINATES` (9 entries) — this repo's Gradle plugin → Maven coordinate mappings +- `CODE_ANALYSIS_COORDINATES` (0 entries) — no code-analysis tool config in overthere + +### Phase 3 — alignment check + +Verifies that any version overrides in `build.gradle` are consistent with each other (e.g. Bouncy Castle `bcprov` and `bcpkix` should match, `httpclient` and `httpcore` should be compatible). Flags mismatches. + +## Interactive workflow + +1. Ask user: "Scan xl-platform too? (yes/no)" (defaults to **no** for standalone repos) +2. Run Phase 2 (always) ± Phase 1 (if yes) +3. Run Phase 3 (alignment check) +4. Output reports +5. Offer handoff: "Run dependency-upgrade to apply selected upgrades?" + +## Run command + +```bash +# All phases (including xl-platform scan) +python3 .github/skills/dependency-scan/scripts/scan-dependencies.py \ + --phase all \ + --xl-platform-dir ../xl-platform + +# Repo scan + alignment only (no xl-platform) +python3 .github/skills/dependency-scan/scripts/scan-dependencies.py \ + --phase scan + +# Alignment check only +python3 .github/skills/dependency-scan/scripts/scan-dependencies.py \ + --phase alignment +``` + +## Output + +Both files are written (or overwritten) in the repo root: + +| File | Purpose | +|---|---| +| `dependency-upgrade-report.md` | Human-readable grouped upgrade table | +| `dependency-upgrade-report.json` | Machine-readable; consumed by `execute-upgrade.py` | + +## Configuration & credentials + +- **Maven Central**: no credentials required +- **Nexus**: optional; set `NEXUS_USER` + `NEXUS_PASSWORD` env vars, or `nexusUserName`/`nexusPassword` in `~/.gradle/gradle.properties` +- Nexus base URL: `https://nexus.xebialabs.com/nexus/content` (searches `releases`, `central`, `public`) diff --git a/.github/skills/dependency-scan/references/architecture.md b/.github/skills/dependency-scan/references/architecture.md new file mode 100644 index 00000000..061af2e5 --- /dev/null +++ b/.github/skills/dependency-scan/references/architecture.md @@ -0,0 +1,99 @@ +# Dependency Scan — Architecture + +## How `overthere` manages dependencies + +`overthere` is a **standalone Java library** that does **not** consume the xl-platform BOM. All dependency versions are hardcoded directly in `build.gradle`. + +``` +build.gradle +├── buildscript { dependencies { ... } } ← Gradle plugin versions (5 entries) +└── dependencies { ... } ← Library versions (~20 runtime + ~7 test entries) +``` + +## Dependency source files + +| File | Format | Contains | +|---|---|---| +| `build.gradle` (buildscript block) | Groovy string literals `'group:artifact:version'` | Gradle plugin versions | +| `build.gradle` (dependencies block) | Groovy string literals | All runtime and test library versions | +| `gradle.properties` | `key=value` | Only JVM/SSL args — **no version variables** | + +## Coordinate maps + +### `REPO_COORDINATES` (9 entries) + +Maps the artifact identifier (as it appears in `build.gradle`) to its Maven coordinate for version lookup: + +| Artifact string | Maven coordinate | +|---|---| +| `io.codearte.gradle.nexus:gradle-nexus-staging-plugin` | `io.codearte.gradle.nexus:gradle-nexus-staging-plugin` | +| `com.netflix.nebula:nebula-release-plugin` | `com.netflix.nebula:nebula-release-plugin` | +| `ru.vyarus:gradle-pom-plugin` | `ru.vyarus:gradle-pom-plugin` | +| `org.ajoberstar.grgit:grgit-gradle` | `org.ajoberstar.grgit:grgit-gradle` | +| `com.github.hierynomus.license` (plugin id) | `gradle.plugin.com.hierynomus.license:licensegradle` | +| `com.hierynomus:sshj` | `com.hierynomus:sshj` | +| `com.hierynomus:smbj` | `com.hierynomus:smbj` | +| `com.google.auth:google-auth-library-oauth2-http` | `com.google.auth:google-auth-library-oauth2-http` | +| `com.google.cloud:google-cloud-os-login` | `com.google.cloud:google-cloud-os-login` | + +### `CODE_ANALYSIS_COORDINATES` (0 entries) + +`overthere` has no `codeAnalysis.gradle` or equivalent — no code-analysis tool versions to track here. + +### `XL_PLATFORM_COORDINATES` (114 entries) + +Shared with all xl-platform consumers. See `scan-dependencies.py` for full table. + +## Two-tier resolution + +``` +Variable/artifact + │ + ├─► XL_PLATFORM_COORDINATES (114 entries) ─► Maven Central → Nexus + ├─► REPO_COORDINATES (9 entries) ─► Maven Central → Nexus + ├─► CODE_ANALYSIS_COORDINATES (0 entries) + ├─► SKIP_VARIABLES (24 entries) ─► skip + └─► DYNAMIC_PATTERNS (46 patterns) ─► infer groupId:artifactId + → Maven Central search API + → Nexus search fallback +``` + +## Dependency categories (with risk ratings) + +| Category | Risk | Examples in overthere | +|---|---|---| +| Security | 🔴 High | `org.bouncycastle:bcprov-jdk18on`, `org.bouncycastle:bcpkix-jdk18on` | +| Core Framework | 🔴 High | `nl.javadude.scannit:scannit`, `org.slf4j:slf4j-api` | +| Logging | 🔴 High | `org.slf4j:jcl-over-slf4j`, `ch.qos.logback:logback-classic` | +| SSH / Network | 🟠 Medium | `com.hierynomus:sshj`, `com.hierynomus:smbj`, `jcifs:jcifs` | +| HTTP | 🟠 Medium | `org.apache.httpcomponents:httpclient`, `org.apache.httpcomponents:httpcore` | +| Serialization | 🟠 Medium | `org.dom4j:dom4j`, `jaxen:jaxen`, `commons-codec:commons-codec` | +| GCP | 🟠 Medium | `com.google.apis:google-api-services-compute`, `com.google.cloud:google-cloud-os-login` | +| Testing | 🟡 Low | `org.testng:testng`, `org.mockito:mockito-core`, `org.hamcrest:*`, `com.google.guava:guava` | +| Build | 🟢 Info | Gradle buildscript plugins (nexus-staging, nebula-release, grgit, etc.) | + +## Alignment checks (Phase 3) + +| Check | Rule | +|---|---| +| Bouncy Castle | `bcprov-jdk18on` version must equal `bcpkix-jdk18on` version | +| Apache HttpComponents | `httpclient` 4.x is compatible with `httpcore` 4.x; verify minor alignment | +| SLF4J | `slf4j-api` and `jcl-over-slf4j` must share the same version | +| Guava (test) | Track separately from runtime Guava to avoid accidental runtime promotion | + +## Pre-release filtering rules + +Versions matching any of the following patterns are excluded from "latest stable": + +- `SNAPSHOT` (case-insensitive) +- `alpha`, `beta`, `rc`, `cr` (case-insensitive) +- `M1`–`M9` milestone suffixes +- `milestone`, `dev`, `incubating` (case-insensitive) +- `-pr`, `-preview` suffixes + +## Internal group prefixes (skip Maven Central lookup) + +- `com.xebialabs.*` +- `ai.digital.*` + +These are internal artifacts not published to Maven Central. diff --git a/.github/skills/dependency-scan/scripts/scan-dependencies.py b/.github/skills/dependency-scan/scripts/scan-dependencies.py new file mode 100644 index 00000000..649b77e3 --- /dev/null +++ b/.github/skills/dependency-scan/scripts/scan-dependencies.py @@ -0,0 +1,1099 @@ +#!/usr/bin/env python3 +""" +scan-dependencies.py — Dependency scan for overthere +Phases: + xl-platform-scan : Scan xl-platform BOM for upstream updates + scan : Scan overthere's own build.gradle for hardcoded dep updates + alignment : Check version consistency in build.gradle + all : Run xl-platform-scan + scan + alignment + +Usage: + python3 scan-dependencies.py --phase scan + python3 scan-dependencies.py --phase all --xl-platform-dir ../xl-platform +""" + +import argparse +import base64 +import json +import os +import re +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +# Windows: ensure stdout/stderr use UTF-8 so emoji and non-ASCII chars print correctly +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") +if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +MAVEN_CENTRAL_METADATA = "https://repo1.maven.org/maven2/{group}/{artifact}/maven-metadata.xml" +MAVEN_CENTRAL_SEARCH = "https://search.maven.org/solrsearch/select" +NEXUS_BASE_URL = "https://nexus.xebialabs.com/nexus/content" +NEXUS_REPOS = ["releases", "central", "public"] + +REQUEST_TIMEOUT = 15 +CACHE: dict[str, Optional[str]] = {} +FAILED: set[str] = set() + +# --------------------------------------------------------------------------- +# XL_PLATFORM_COORDINATES — 114 entries +# Maps xl-platform variable name → "groupId:artifactId" +# --------------------------------------------------------------------------- +XL_PLATFORM_COORDINATES: dict[str, str] = { + # Spring ecosystem + "springBootVersion": "org.springframework.boot:spring-boot", + "springVersion": "org.springframework:spring-core", + "springSecurityVersion": "org.springframework.security:spring-security-core", + "springDataVersion": "org.springframework.data:spring-data-commons", + "springIntegrationVersion": "org.springframework.integration:spring-integration-core", + "springCloudVersion": "org.springframework.cloud:spring-cloud-core", + "springBatchVersion": "org.springframework.batch:spring-batch-core", + "springRetryVersion": "org.springframework.retry:spring-retry", + "springWebservicesVersion": "org.springframework.ws:spring-ws-core", + "springSessionVersion": "org.springframework.session:spring-session-core", + "springShellVersion": "org.springframework.shell:spring-shell-core", + "springKafkaVersion": "org.springframework.kafka:spring-kafka", + "springRabbitVersion": "org.springframework.amqp:spring-rabbit", + # Hibernate / JPA + "hibernateVersion": "org.hibernate.orm:hibernate-core", + "hibernateValidatorVersion": "org.hibernate.validator:hibernate-validator", + "hibernateSearchVersion": "org.hibernate.search:hibernate-search-engine", + "javaxPersistenceVersion": "jakarta.persistence:jakarta.persistence-api", + # Jackson + "jacksonVersion": "com.fasterxml.jackson.core:jackson-databind", + "jacksonModuleScalaVersion": "com.fasterxml.jackson.module:jackson-module-scala_2.13", + "jacksonDataformatYamlVersion": "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", + "jacksonDataformatXmlVersion": "com.fasterxml.jackson.dataformat:jackson-dataformat-xml", + "jacksonDatatypeJsr310Version": "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", + # Logging + "slf4jVersion": "org.slf4j:slf4j-api", + "logbackVersion": "ch.qos.logback:logback-classic", + "log4j2Version": "org.apache.logging.log4j:log4j-core", + "logstashLogbackEncoderVersion": "net.logstash.logback:logstash-logback-encoder", + # Database + "flywayVersion": "org.flywaydb:flyway-core", + "liquibaseVersion": "org.liquibase:liquibase-core", + "h2Version": "com.h2database:h2", + "postgresVersion": "org.postgresql:postgresql", + "mysqlVersion": "com.mysql:mysql-connector-j", + "mssqlVersion": "com.microsoft.sqlserver:mssql-jdbc", + "hikariVersion": "com.zaxxer:HikariCP", + "c3p0Version": "com.mchange:c3p0", + # Apache Commons + "commonsLangVersion": "org.apache.commons:commons-lang3", + "commonsCollectionsVersion": "org.apache.commons:commons-collections4", + "commonsIoVersion": "commons-io:commons-io", + "commonsCodecVersion": "commons-codec:commons-codec", + "commonsTextVersion": "org.apache.commons:commons-text", + "commonsMathVersion": "org.apache.commons:commons-math3", + "commonsNetVersion": "commons-net:commons-net", + "commonsCompressVersion": "org.apache.commons:commons-compress", + "commonsCsvVersion": "org.apache.commons:commons-csv", + # HTTP / Networking + "httpClientVersion": "org.apache.httpcomponents:httpclient", + "httpCoreVersion": "org.apache.httpcomponents:httpcore", + "httpClient5Version": "org.apache.httpcomponents.client5:httpclient5", + "okHttpVersion": "com.squareup.okhttp3:okhttp", + "retrofitVersion": "com.squareup.retrofit2:retrofit", + "nettyVersion": "io.netty:netty-all", + "grpcVersion": "io.grpc:grpc-core", + "grpcNettyVersion": "io.grpc:grpc-netty-shaded", + # Serialization + "protobufVersion": "com.google.protobuf:protobuf-java", + "dom4jVersion": "org.dom4j:dom4j", + "jaxenVersion": "jaxen:jaxen", + "snakeyamlVersion": "org.yaml:snakeyaml", + "gsonVersion": "com.google.gson:gson", + "kryoVersion": "com.esotericsoftware:kryo", + # Testing + "junitVersion": "junit:junit", + "junit5Version": "org.junit.jupiter:junit-jupiter", + "testngVersion": "org.testng:testng", + "mockitoVersion": "org.mockito:mockito-core", + "hamcrestVersion": "org.hamcrest:hamcrest", + "assertjVersion": "org.assertj:assertj-core", + "spockVersion": "org.spockframework:spock-core", + "awaitilityVersion": "org.awaitility:awaitility", + "wireMockVersion": "com.github.tomakehurst:wiremock", + "restAssuredVersion": "io.rest-assured:rest-assured", + "testContainersVersion": "org.testcontainers:testcontainers", + # Security + "bouncycastleVersion": "org.bouncycastle:bcprov-jdk18on", + "nimbusJoseVersion": "com.nimbusds:nimbus-jose-jwt", + "keycloakVersion": "org.keycloak:keycloak-core", + "oauthClientVersion": "com.google.auth:google-auth-library-oauth2-http", + # Build tools + "gradleVersion": "dev.gradleplugins:gradle-api", + "lombokVersion": "org.projectlombok:lombok", + "mapstructVersion": "org.mapstruct:mapstruct", + "immutablesVersion": "org.immutables:value", + # Guava / Google + "guavaVersion": "com.google.guava:guava", + "googleApiClientVersion": "com.google.api-client:google-api-client", + "googleCloudVersion": "com.google.cloud:google-cloud-core", + "googleHttpClientVersion": "com.google.http-client:google-http-client", + "googleAuthVersion": "com.google.auth:google-auth-library-credentials", + "observabilityVersion": "io.micrometer:micrometer-core", + "micrometerVersion": "io.micrometer:micrometer-core", + "opentelemetryVersion": "io.opentelemetry:opentelemetry-api", + "prometheusVersion": "io.prometheus:simpleclient", + # Akka/Pekko + "akkaVersion": "com.typesafe.akka:akka-actor_2.13", + "pekkoVersion": "org.apache.pekko:pekko-actor_2.13", + "akkaHttpVersion": "com.typesafe.akka:akka-http_2.13", + "pekkoHttpVersion": "org.apache.pekko:pekko-http_2.13", + # Scala + "scalatestVersion": "org.scalatest:scalatest_2.13", + # Cache + "caffeineVersion": "com.github.ben-manes.caffeine:caffeine", + "ehcacheVersion": "org.ehcache:ehcache", + "hazelcastVersion": "com.hazelcast:hazelcast", + # Message queues + "kafkaVersion": "org.apache.kafka:kafka-clients", + "rabbitMQVersion": "com.rabbitmq:amqp-client", + # Build plugins (xl-platform gradle.properties) + "gradleNexusStagingVersion": "io.codearte.gradle.nexus:gradle-nexus-staging-plugin", + "nebulaReleaseVersion": "com.netflix.nebula:nebula-release-plugin", + "gradlePomPluginVersion": "ru.vyarus:gradle-pom-plugin", + "grgitGradleVersion": "org.ajoberstar.grgit:grgit-gradle", + "licensePluginVersion": "com.github.hierynomus:license-gradle-plugin", + "sonarQubePluginVersion": "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin", + # Jakarta EE (xl-jakartaee-bom) + "jakartaActivationVersion": "jakarta.activation:jakarta.activation-api", + "jakartaAnnotationVersion": "jakarta.annotation:jakarta.annotation-api", + "jakartaBatchVersion": "jakarta.batch:jakarta.batch-api", + "jakartaCdiVersion": "jakarta.enterprise:jakarta.enterprise.cdi-api", + "jakartaEjbVersion": "jakarta.ejb:jakarta.ejb-api", + "jakartaElVersion": "jakarta.el:jakarta.el-api", + "jakartaFacesVersion": "jakarta.faces:jakarta.faces-api", + "jakartaInjectVersion": "jakarta.inject:jakarta.inject-api", + "jakartaInterceptorVersion": "jakarta.interceptor:jakarta.interceptor-api", + "jakartaJsonVersion": "jakarta.json:jakarta.json-api", + "jakartaJsonBindVersion": "jakarta.json.bind:jakarta.json.bind-api", + "jakartaMailVersion": "jakarta.mail:jakarta.mail-api", + "jakartaMessagingVersion": "jakarta.jms:jakarta.jms-api", + "jakartaPersistenceVersion": "jakarta.persistence:jakarta.persistence-api", + "jakartaSecurityVersion": "jakarta.security.enterprise:jakarta.security.enterprise-api", + "jakartaServletVersion": "jakarta.servlet:jakarta.servlet-api", + "jakartaTransactionVersion": "jakarta.transaction:jakarta.transaction-api", + "jakartaValidationVersion": "jakarta.validation:jakarta.validation-api", + "jakartaWebsocketVersion": "jakarta.websocket:jakarta.websocket-api", + "jakartaWsRsVersion": "jakarta.ws.rs:jakarta.ws.rs-api", + "jakartaXmlBindVersion": "jakarta.xml.bind:jakarta.xml.bind-api", + "jakartaXmlSoapVersion": "jakarta.xml.soap:jakarta.xml.soap-api", + "jakartaXmlWsVersion": "jakarta.xml.ws:jakarta.xml.ws-api", + "jakartaResourceVersion": "jakarta.resource:jakarta.resource-api", + "jakartaSecurityJaspicVersion": "jakarta.security.auth.message:jakarta.security.auth.message-api", + "jakartaDeployVersion": "jakarta.enterprise.deploy:jakarta.enterprise.deploy-api", + "jakartaManagementVersion": "jakarta.management.j2ee:jakarta.management.j2ee-api", + "jakartaXmlRegistryVersion": "jakarta.xml.registry:jakarta.xml.registry-api", + "jakartaConnectorVersion": "jakarta.resource:jakarta.resource-api", +} + +# --------------------------------------------------------------------------- +# REPO_COORDINATES — 9 entries specific to overthere +# Maps artifact coordinate (group:artifact) as used in build.gradle to its +# canonical Maven coordinate for latest-version lookup +# --------------------------------------------------------------------------- +REPO_COORDINATES: dict[str, str] = { + "io.codearte.gradle.nexus:gradle-nexus-staging-plugin": "io.codearte.gradle.nexus:gradle-nexus-staging-plugin", + "com.netflix.nebula:nebula-release-plugin": "com.netflix.nebula:nebula-release-plugin", + "ru.vyarus:gradle-pom-plugin": "ru.vyarus:gradle-pom-plugin", + "org.ajoberstar.grgit:grgit-gradle": "org.ajoberstar.grgit:grgit-gradle", + "com.github.hierynomus:license-gradle-plugin": "com.github.hierynomus:license-gradle-plugin", + "com.hierynomus:sshj": "com.hierynomus:sshj", + "com.hierynomus:smbj": "com.hierynomus:smbj", + "com.google.auth:google-auth-library-oauth2-http": "com.google.auth:google-auth-library-oauth2-http", + "com.google.cloud:google-cloud-os-login": "com.google.cloud:google-cloud-os-login", +} + +# --------------------------------------------------------------------------- +# CODE_ANALYSIS_COORDINATES — 0 entries for overthere +# overthere has no codeAnalysis.gradle +# --------------------------------------------------------------------------- +CODE_ANALYSIS_COORDINATES: dict[str, str] = {} + +# --------------------------------------------------------------------------- +# SKIP_VARIABLES — 24 entries: variables that should not be looked up +# --------------------------------------------------------------------------- +SKIP_VARIABLES: set[str] = { + # Meta / non-version + "scalaVersion", + "scalaFullVersion", + "pekkoMajorVersion", + # Python plugin deps (13 total) + "pyTestVersion", + "pyMockVersion", + "pyConanVersion", + "pyJinja2Version", + "pyParamikoVersion", + "pyRequestsVersion", + "pySixVersion", + "pyYamlVersion", + "pyLxmlVersion", + "pySetuptoolsVersion", + "pyWheelVersion", + "pyTwineVersion", + "pyVirtualenvVersion", + # Internal artifacts + "crashVersion", + "scannitVersion", + "docBaseStyleVersion", + "overcastVersion", + "jythonStandaloneVersion", + # Derived versions (managed transitively) + "jacksonAnnotationsVersion", + # Gradle settings + "languageLevel", + "release.stage", + # Internal group artifacts + "overcastLibVersion", + "xlPlatformVersion", +} + +# --------------------------------------------------------------------------- +# DYNAMIC_PATTERNS — 46 regex patterns for unmapped variable inference +# Strip "Version" suffix, then try these transformations +# --------------------------------------------------------------------------- +DYNAMIC_PATTERNS: list[tuple[str, str]] = [ + (r"^spring(.+)$", "org.springframework:spring-{lower}"), + (r"^springBoot(.+)$", "org.springframework.boot:spring-boot-{lower}"), + (r"^springCloud(.+)$", "org.springframework.cloud:spring-cloud-{lower}"), + (r"^springSecurity(.+)$", "org.springframework.security:spring-security-{lower}"), + (r"^springData(.+)$", "org.springframework.data:spring-data-{lower}"), + (r"^hibernate(.+)$", "org.hibernate.orm:hibernate-{lower}"), + (r"^jackson(.+)$", "com.fasterxml.jackson.core:jackson-{lower}"), + (r"^jacksonModule(.+)$", "com.fasterxml.jackson.module:jackson-module-{lower}"), + (r"^jacksonDataformat(.+)$", "com.fasterxml.jackson.dataformat:jackson-dataformat-{lower}"), + (r"^jacksonDatatype(.+)$", "com.fasterxml.jackson.datatype:jackson-datatype-{lower}"), + (r"^slf4j(.*)$", "org.slf4j:slf4j-{lower}"), + (r"^logback(.+)$", "ch.qos.logback:logback-{lower}"), + (r"^log4j(.*)$", "org.apache.logging.log4j:log4j-{lower}"), + (r"^commons(.+)$", "org.apache.commons:commons-{lower}"), + (r"^apacheCommons(.+)$", "org.apache.commons:commons-{lower}"), + (r"^netty(.*)$", "io.netty:netty-{lower}"), + (r"^grpc(.*)$", "io.grpc:grpc-{lower}"), + (r"^protobuf(.*)$", "com.google.protobuf:protobuf-{lower}"), + (r"^guava$", "com.google.guava:guava"), + (r"^gson$", "com.google.gson:gson"), + (r"^okhttp(.*)$", "com.squareup.okhttp3:okhttp"), + (r"^retrofit(.*)$", "com.squareup.retrofit2:retrofit"), + (r"^kafka(.*)$", "org.apache.kafka:kafka-{lower}"), + (r"^akka(.*)$", "com.typesafe.akka:akka-{lower}_2.13"), + (r"^pekko(.*)$", "org.apache.pekko:pekko-{lower}_2.13"), + (r"^junit(.*)$", "org.junit.jupiter:junit-jupiter"), + (r"^testng$", "org.testng:testng"), + (r"^mockito(.*)$", "org.mockito:mockito-{lower}"), + (r"^assertj(.*)$", "org.assertj:assertj-{lower}"), + (r"^hamcrest(.*)$", "org.hamcrest:hamcrest-{lower}"), + (r"^spock(.*)$", "org.spockframework:spock-{lower}"), + (r"^testContainers(.*)$", "org.testcontainers:testcontainers"), + (r"^flyway(.*)$", "org.flywaydb:flyway-{lower}"), + (r"^liquibase(.*)$", "org.liquibase:liquibase-{lower}"), + (r"^h2(.*)$", "com.h2database:h2"), + (r"^postgres(.*)$", "org.postgresql:postgresql"), + (r"^hikari(.*)$", "com.zaxxer:HikariCP"), + (r"^bouncycastle(.*)$", "org.bouncycastle:bcprov-jdk18on"), + (r"^micrometer(.*)$", "io.micrometer:micrometer-{lower}"), + (r"^opentelemetry(.*)$", "io.opentelemetry:opentelemetry-{lower}"), + (r"^lombok$", "org.projectlombok:lombok"), + (r"^mapstruct(.*)$", "org.mapstruct:mapstruct"), + (r"^caffeine$", "com.github.ben-manes.caffeine:caffeine"), + (r"^ehcache(.*)$", "org.ehcache:ehcache"), + (r"^hazelcast(.*)$", "com.hazelcast:hazelcast"), + (r"^dom4j(.*)$", "org.dom4j:dom4j"), +] + +# --------------------------------------------------------------------------- +# Risk classification +# --------------------------------------------------------------------------- +CATEGORY_MAP: dict[str, str] = { + "org.springframework": "Core Framework", + "org.springframework.boot": "Core Framework", + "org.springframework.security": "Security", + "org.hibernate": "Core Framework", + "com.fasterxml.jackson": "Serialization", + "org.slf4j": "Logging", + "ch.qos.logback": "Logging", + "org.apache.logging.log4j": "Logging", + "org.bouncycastle": "Security", + "com.nimbusds": "Security", + "org.apache.httpcomponents": "HTTP", + "io.netty": "HTTP", + "io.grpc": "HTTP", + "com.squareup.okhttp3": "HTTP", + "org.dom4j": "Serialization", + "jaxen": "Serialization", + "org.yaml": "Serialization", + "com.google.protobuf": "Serialization", + "commons-codec": "Serialization", + "org.testng": "Testing", + "org.mockito": "Testing", + "org.hamcrest": "Testing", + "org.assertj": "Testing", + "com.google.guava": "Testing", + "nl.javadude.assumeng": "Testing", + "org.spockframework": "Testing", + "org.testcontainers": "Testing", + "com.google.auth": "Security", + "com.google.cloud": "GCP", + "com.google.apis": "GCP", + "io.grpc:grpc-netty": "GCP", + "com.hierynomus:sshj": "SSH", + "com.hierynomus:smbj": "SMB", + "jcifs": "SMB", + "commons-net": "Network", + "com.jcraft": "SSH", + "net.engio": "Core Framework", + "nl.javadude.scannit": "Core Framework", + "org.flywaydb": "Data", + "com.h2database": "Data", + "org.postgresql": "Data", + "com.zaxxer": "Data", + "io.micrometer": "Observability", + "io.opentelemetry": "Observability", + "io.codearte.gradle.nexus": "Build", + "com.netflix.nebula": "Build", + "ru.vyarus": "Build", + "org.ajoberstar.grgit": "Build", + "com.github.hierynomus": "Build", + "org.sonarsource": "Build", + "jakarta": "Jakarta EE", +} + +RISK_EMOJI: dict[str, str] = { + "Security": "🔴", + "Core Framework": "🔴", + "Logging": "🔴", + "SSH": "🟠", + "SMB": "🟠", + "Data": "🟠", + "Serialization": "🟠", + "HTTP": "🟠", + "GCP": "🟠", + "Network": "🟠", + "Testing": "🟡", + "Observability": "🟡", + "Jakarta EE": "🟡", + "Build": "🟢", + "Unknown": "⚪", +} + +# --------------------------------------------------------------------------- +# Spring compatibility matrix +# --------------------------------------------------------------------------- +SPRING_COMPAT: dict[str, dict] = { + "hibernate": { + "constraint": "Must be compatible with Spring Boot version", + "spring_boot_6x": ">=6.0.0", + }, + "spring-security": { + "constraint": "Major version must match Spring Boot major version", + }, + "jackson-databind": { + "constraint": "Managed by Spring Boot BOM — do not upgrade independently", + }, +} + +# --------------------------------------------------------------------------- +# Internal group prefixes — skip Maven Central lookup +# --------------------------------------------------------------------------- +INTERNAL_GROUP_PREFIXES: tuple[str, ...] = ( + "com.xebialabs.", + "ai.digital.", +) + +# --------------------------------------------------------------------------- +# Pre-release version filter patterns +# --------------------------------------------------------------------------- +PRE_RELEASE_PATTERNS: list[re.Pattern] = [ + re.compile(r"(?i)snapshot"), + re.compile(r"(?i)(^|[.\-])alpha"), + re.compile(r"(?i)(^|[.\-])beta"), + re.compile(r"(?i)(^|[.\-])(rc|cr)\d*($|[.\-])"), + re.compile(r"(?i)(^|[.\-])m\d+($|[.\-])"), + re.compile(r"(?i)milestone"), + re.compile(r"(?i)dev"), + re.compile(r"(?i)incubating"), + re.compile(r"(?i)-pr[\-\.]"), + re.compile(r"(?i)-preview"), +] + +# Google API services use non-semver versions like "v1-rev20251110-2.0.0" +NON_SEMVER_PATTERNS: list[re.Pattern] = [ + re.compile(r"^v\d+-rev\d+-"), # e.g. v1-rev20251110-2.0.0 +] + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _nexus_credentials() -> Optional[tuple[str, str]]: + """Return (user, password) from env or ~/.gradle/gradle.properties, or None.""" + user = os.environ.get("NEXUS_USER") + pwd = os.environ.get("NEXUS_PASSWORD") + if user and pwd: + return (user, pwd) + gradle_props = Path.home() / ".gradle" / "gradle.properties" + if gradle_props.exists(): + text = gradle_props.read_text(encoding="utf-8", errors="replace") + u_match = re.search(r"^nexusUserName\s*=\s*(.+)$", text, re.MULTILINE) + p_match = re.search(r"^nexusPassword\s*=\s*(.+)$", text, re.MULTILINE) + if u_match and p_match: + return (u_match.group(1).strip(), p_match.group(1).strip()) + return None + + +_NEXUS_CREDS: Optional[tuple[str, str]] = _nexus_credentials() + + +def fetch_url(url: str, auth: Optional[tuple[str, str]] = None) -> Optional[str]: + """Fetch URL content as text, returns None on error.""" + req = urllib.request.Request(url) + req.add_header("User-Agent", "overthere-dep-scan/1.0") + if auth: + token = base64.b64encode(f"{auth[0]}:{auth[1]}".encode()).decode() + req.add_header("Authorization", f"Basic {token}") + try: + with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp: + return resp.read().decode("utf-8", errors="replace") + except Exception: + return None + + +def is_pre_release(version: str) -> bool: + for pat in PRE_RELEASE_PATTERNS: + if pat.search(version): + return True + return False + + +def is_non_semver(version: str) -> bool: + for pat in NON_SEMVER_PATTERNS: + if pat.search(version): + return True + return False + + +def parse_semver(version: str) -> Optional[tuple[int, int, int]]: + """Parse a version string into (major, minor, patch) tuple, or None.""" + # Strip leading 'v' + v = version.lstrip("v") + m = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", v) + if m: + return (int(m.group(1)), int(m.group(2)), int(m.group(3) or 0)) + return None + + +def classify_bump(current: str, latest: str) -> str: + """Return 'major', 'minor', 'patch', or 'unknown'.""" + c = parse_semver(current) + n = parse_semver(latest) + if not c or not n: + return "unknown" + if n[0] > c[0]: + return "major" + if n[1] > c[1]: + return "minor" + if n[2] > c[2]: + return "patch" + return "up-to-date" + + +def latest_from_maven_central(group: str, artifact: str) -> Optional[str]: + """Fetch the latest stable version from Maven Central maven-metadata.xml.""" + cache_key = f"{group}:{artifact}" + if cache_key in CACHE: + return CACHE[cache_key] + if cache_key in FAILED: + return None + + url = MAVEN_CENTRAL_METADATA.format( + group=group.replace(".", "/"), artifact=artifact + ) + body = fetch_url(url) + if not body: + # Try Maven Central search API as fallback + body = _maven_central_search(group, artifact) + + result = _extract_latest_stable(body) if body else None + if result: + CACHE[cache_key] = result + else: + FAILED.add(cache_key) + return result + + +def _maven_central_search(group: str, artifact: str) -> Optional[str]: + """Query Maven Central search API, return pseudo-metadata XML or None.""" + url = f"{MAVEN_CENTRAL_SEARCH}?q={urllib.parse.quote(f'g:{group} AND a:{artifact}')}&rows=5&wt=json&core=gav" + body = fetch_url(url) + if not body: + return None + try: + data = json.loads(body) + versions = [doc["v"] for doc in data.get("response", {}).get("docs", [])] + return "<versions>" + "".join(f"<version>{v}</version>" for v in versions) + "</versions>" + except Exception: + return None + + +def latest_from_nexus(group: str, artifact: str) -> Optional[str]: + """Try each Nexus repo for the artifact's latest stable version.""" + if not _NEXUS_CREDS: + return None + for repo in NEXUS_REPOS: + url = ( + f"{NEXUS_BASE_URL}/repositories/{repo}/" + f"{group.replace('.', '/')}/{artifact}/maven-metadata.xml" + ) + body = fetch_url(url, auth=_NEXUS_CREDS) + if body: + result = _extract_latest_stable(body) + if result: + return result + return None + + +def _extract_latest_stable(xml_body: str) -> Optional[str]: + """Extract the highest non-pre-release version from maven-metadata.xml content.""" + versions = re.findall(r"<version>([^<]+)</version>", xml_body) + stable = [v for v in versions if not is_pre_release(v)] + if not stable: + return None + # Sort by semver, keep string for non-semver + def sort_key(v: str): + parsed = parse_semver(v) + return parsed if parsed else (0, 0, 0) + stable.sort(key=sort_key) + return stable[-1] + + +def get_latest_version(group: str, artifact: str) -> Optional[str]: + """Full version lookup: Maven Central → Nexus fallback.""" + # Skip internal artifacts + coord = f"{group}:{artifact}" + for prefix in INTERNAL_GROUP_PREFIXES: + if group.startswith(prefix.rstrip(".")): + return None + result = latest_from_maven_central(group, artifact) + if not result: + result = latest_from_nexus(group, artifact) + return result + + +def get_category(group: str, artifact: str) -> str: + coord = f"{group}:{artifact}" + for prefix, cat in CATEGORY_MAP.items(): + if coord.startswith(prefix) or group.startswith(prefix): + return cat + return "Unknown" + + +def infer_coordinate(var_name: str) -> Optional[str]: + """ + Try to infer a Maven coordinate from a variable name using DYNAMIC_PATTERNS. + Returns "group:artifact" or None. + """ + # Strip "Version" suffix + base = re.sub(r"Version$", "", var_name, flags=re.IGNORECASE) + if not base: + return None + for pattern_str, template in DYNAMIC_PATTERNS: + m = re.match(pattern_str, base, re.IGNORECASE) + if m: + suffix = m.group(1).lower() if m.lastindex and m.lastindex >= 1 else "" + coord = template.replace("{lower}", suffix) + # Clean up double hyphens or trailing hyphens + coord = re.sub(r"-+", "-", coord) + coord = re.sub(r"-$", "", coord) + return coord + return None + + +# --------------------------------------------------------------------------- +# Conf file parser (HOCON-style: key: "value" or key = "value") +# --------------------------------------------------------------------------- +def parse_conf_file(path: Path) -> dict[str, str]: + """Parse a .conf file and return {variableName: version} for version-like values.""" + result = {} + if not path.exists(): + return result + text = path.read_text(encoding="utf-8", errors="replace") + # Match: key: "1.2.3" or key = "1.2.3" or key: 1.2.3 + pattern = re.compile( + r'^[ \t]*([a-zA-Z][a-zA-Z0-9_.-]*)[ \t]*[:=][ \t]*"?([0-9][^"\s,}]+)"?', + re.MULTILINE, + ) + for m in pattern.finditer(text): + key, val = m.group(1).strip(), m.group(2).strip() + val = val.strip('"\'') + if re.match(r"^\d", val): + result[key] = val + return result + + +# --------------------------------------------------------------------------- +# build.gradle parser +# --------------------------------------------------------------------------- +def parse_build_gradle(path: Path) -> list[dict]: + """ + Parse a build.gradle file and return a list of dependency dicts: + {group, artifact, version, source_file, line_num, scope} + """ + if not path.exists(): + print(f" [WARN] File not found: {path}") + return [] + + text = path.read_text(encoding="utf-8", errors="replace") + deps = [] + + # Match strings like 'group:artifact:version' or "group:artifact:version" + dep_pattern = re.compile( + r"""['"]([\w.\-]+):([\w.\-]+):([\w.\-+]+)['"]""", + re.MULTILINE, + ) + # Also match: id "plugin.id" version "x.y.z" + plugin_pattern = re.compile( + r"""id\s+['"]([^'"]+)['"]\s+version\s+['"]([^'"]+)['"]""", + re.MULTILINE, + ) + + lines = text.splitlines() + + for i, line in enumerate(lines, 1): + m = dep_pattern.search(line) + if m: + group, artifact, version = m.group(1), m.group(2), m.group(3) + # Determine scope + scope = "implementation" + if "testImplementation" in line or "testCompile" in line: + scope = "test" + elif "classpath" in line: + scope = "buildscript" + elif "api " in line or "api(" in line: + scope = "api" + deps.append({ + "group": group, + "artifact": artifact, + "version": version, + "source_file": str(path.name), + "line_num": i, + "scope": scope, + }) + continue + m2 = plugin_pattern.search(line) + if m2: + plugin_id, version = m2.group(1), m2.group(2) + deps.append({ + "group": plugin_id, + "artifact": plugin_id.split(".")[-1], + "version": version, + "source_file": str(path.name), + "line_num": i, + "scope": "plugin", + }) + + return deps + + +# --------------------------------------------------------------------------- +# Phase 1 — xl-platform scan +# --------------------------------------------------------------------------- +def scan_xl_platform(xl_platform_dir: Path) -> list[dict]: + """ + Scan the xl-platform BOM for upstream dependency updates. + Returns list of update dicts. + """ + print("\n=== Phase 1: xl-platform scan ===") + updates = [] + + # Source files in xl-platform + sources = { + "xl-reference.conf": xl_platform_dir / "xl-reference" / "xl-reference.conf", + "xl-jakartaee-bom.conf": xl_platform_dir / "xl-jakartaee-bom" / "xl-jakartaee-bom.conf", + "gradle.properties": xl_platform_dir / "gradle.properties", + } + + all_vars: dict[str, tuple[str, str]] = {} # varName -> (version, source_file) + for label, fpath in sources.items(): + if not fpath.exists(): + print(f" [SKIP] Not found: {fpath}") + continue + if fpath.suffix == ".properties": + # Parse gradle.properties: key=value + text = fpath.read_text(encoding="utf-8", errors="replace") + for m in re.finditer(r"^([a-zA-Z][a-zA-Z0-9._-]*)\s*=\s*(\S+)$", text, re.MULTILINE): + key, val = m.group(1), m.group(2) + if re.match(r"^\d", val): + all_vars[key] = (val, label) + else: + for key, val in parse_conf_file(fpath).items(): + all_vars[key] = (val, label) + + print(f" Found {len(all_vars)} version variables across xl-platform sources") + + for var_name, (current_ver, source) in sorted(all_vars.items()): + if var_name in SKIP_VARIABLES: + continue + + # Try explicit mapping first + coord = XL_PLATFORM_COORDINATES.get(var_name) + method = "mapped" + + # Fall back to dynamic inference + if not coord: + coord = infer_coordinate(var_name) + method = "inferred" + + if not coord: + print(f" [SKIP] No coordinate for {var_name}") + continue + + group, _, artifact = coord.partition(":") + # Skip internal artifacts + skip = False + for prefix in INTERNAL_GROUP_PREFIXES: + if group.startswith(prefix.rstrip(".")): + skip = True + break + if skip: + continue + + if is_non_semver(current_ver): + print(f" [SKIP] Non-semver version for {var_name}={current_ver}") + continue + + print(f" Checking {var_name} ({group}:{artifact}) current={current_ver}...", end="", flush=True) + latest = get_latest_version(group, artifact) + if not latest: + print(" no data") + continue + + bump = classify_bump(current_ver, latest) + print(f" latest={latest} [{bump}]") + + if bump in ("major", "minor", "patch"): + category = get_category(group, artifact) + updates.append({ + "type": "xl-platform", + "variable": var_name, + "source_file": source, + "group": group, + "artifact": artifact, + "current": current_ver, + "latest": latest, + "bump": bump, + "category": category, + "risk": RISK_EMOJI.get(category, "⚪"), + "method": method, + }) + + print(f" => {len(updates)} updates available in xl-platform") + return updates + + +# --------------------------------------------------------------------------- +# Phase 2 — repo scan (overthere) +# --------------------------------------------------------------------------- +def scan_repo(repo_dir: Path) -> list[dict]: + """ + Scan overthere's build.gradle for dependency updates. + Returns list of update dicts. + """ + print("\n=== Phase 2: overthere repo scan ===") + updates = [] + + build_gradle = repo_dir / "build.gradle" + raw_deps = parse_build_gradle(build_gradle) + print(f" Found {len(raw_deps)} dependency entries in build.gradle") + + seen: set[str] = set() + + for dep in raw_deps: + group = dep["group"] + artifact = dep["artifact"] + current_ver = dep["version"] + coord = f"{group}:{artifact}" + + # Skip internal artifacts + skip_internal = any(group.startswith(p.rstrip(".")) for p in INTERNAL_GROUP_PREFIXES) + if skip_internal: + continue + + # Skip non-semver versions (like google-api-services-compute v1-rev...) + if is_non_semver(current_ver): + print(f" [SKIP] Non-semver: {coord}:{current_ver}") + continue + + # Skip duplicates + key = coord + if key in seen: + continue + seen.add(key) + + # Skip variables in SKIP_VARIABLES (by group:artifact pattern) + if coord.startswith("com.xebialabs") or coord.startswith("ai.digital"): + continue + + print(f" Checking {coord} current={current_ver}...", end="", flush=True) + latest = get_latest_version(group, artifact) + if not latest: + print(" no data") + continue + + bump = classify_bump(current_ver, latest) + print(f" latest={latest} [{bump}]") + + if bump in ("major", "minor", "patch"): + category = get_category(group, artifact) + updates.append({ + "type": "repo", + "source_file": dep["source_file"], + "line_num": dep["line_num"], + "group": group, + "artifact": artifact, + "current": current_ver, + "latest": latest, + "bump": bump, + "scope": dep["scope"], + "category": category, + "risk": RISK_EMOJI.get(category, "⚪"), + }) + + print(f" => {len(updates)} updates available in overthere") + return updates + + +# --------------------------------------------------------------------------- +# Phase 3 — alignment check +# --------------------------------------------------------------------------- +def check_alignment(repo_dir: Path, all_deps: list[dict]) -> list[dict]: + """ + Verify version consistency rules within build.gradle. + Returns list of alignment issue dicts. + """ + print("\n=== Phase 3: alignment check ===") + issues = [] + + # Build a map of group:artifact -> version from parsed deps + version_map: dict[str, str] = {} + build_gradle = repo_dir / "build.gradle" + raw_deps = parse_build_gradle(build_gradle) + for d in raw_deps: + version_map[f"{d['group']}:{d['artifact']}"] = d["version"] + + # Rule 1: Bouncy Castle bcprov and bcpkix should match + bcprov = version_map.get("org.bouncycastle:bcprov-jdk18on") + bcpkix = version_map.get("org.bouncycastle:bcpkix-jdk18on") + if bcprov and bcpkix and bcprov != bcpkix: + issues.append({ + "type": "alignment", + "rule": "Bouncy Castle version mismatch", + "details": f"bcprov-jdk18on={bcprov} vs bcpkix-jdk18on={bcpkix}", + "fix": f"Set both to the same version (suggest: {bcprov})", + }) + + # Rule 2: SLF4J slf4j-api and jcl-over-slf4j should match + slf4j = version_map.get("org.slf4j:slf4j-api") + jcl = version_map.get("org.slf4j:jcl-over-slf4j") + if slf4j and jcl and slf4j != jcl: + issues.append({ + "type": "alignment", + "rule": "SLF4J version mismatch", + "details": f"slf4j-api={slf4j} vs jcl-over-slf4j={jcl}", + "fix": f"Set both to the same version (suggest: {slf4j})", + }) + + # Rule 3: Apache HttpComponents httpclient and httpcore minor alignment (4.x) + httpclient = version_map.get("org.apache.httpcomponents:httpclient") + httpcore = version_map.get("org.apache.httpcomponents:httpcore") + if httpclient and httpcore: + hc = parse_semver(httpclient) + hcore = parse_semver(httpcore) + if hc and hcore and hc[0] != hcore[0]: + issues.append({ + "type": "alignment", + "rule": "HttpComponents major version mismatch", + "details": f"httpclient={httpclient} vs httpcore={httpcore}", + "fix": "Ensure httpclient and httpcore are from the same major release line", + }) + + if issues: + print(f" => {len(issues)} alignment issue(s) found") + for iss in issues: + print(f" [WARN] {iss['rule']}: {iss['details']}") + else: + print(" => No alignment issues found") + + return issues + + +# --------------------------------------------------------------------------- +# Report generation +# --------------------------------------------------------------------------- +def generate_reports( + repo_dir: Path, + xl_updates: list[dict], + repo_updates: list[dict], + alignment_issues: list[dict], +): + """Write dependency-upgrade-report.md and dependency-upgrade-report.json.""" + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + + # --- Markdown report --- + md_lines = [ + f"# Dependency Upgrade Report", + f"", + f"Generated: {timestamp}", + f"", + ] + + def add_section(title: str, updates: list[dict], key_col: str = "artifact"): + if not updates: + return + md_lines.append(f"## {title}") + md_lines.append("") + # Group by bump type + for bump_type in ("major", "minor", "patch"): + group_items = [u for u in updates if u.get("bump") == bump_type] + if not group_items: + continue + md_lines.append(f"### {bump_type.capitalize()} updates") + md_lines.append("") + md_lines.append("| Risk | Dependency | Current | Latest | Bump | Category |") + md_lines.append("|------|------------|---------|--------|------|----------|") + for u in sorted(group_items, key=lambda x: x.get("category", "")): + risk = u.get("risk", "⚪") + coord = f"{u['group']}:{u['artifact']}" + current = u["current"] + latest = u["latest"] + bump = u["bump"] + cat = u.get("category", "Unknown") + md_lines.append(f"| {risk} | `{coord}` | `{current}` | `{latest}` | {bump} | {cat} |") + md_lines.append("") + + if xl_updates: + add_section("xl-platform BOM Updates (Phase 1)", xl_updates) + if repo_updates: + add_section("overthere Direct Dependency Updates (Phase 2)", repo_updates) + if alignment_issues: + md_lines.append("## Alignment Issues (Phase 3)") + md_lines.append("") + for iss in alignment_issues: + md_lines.append(f"- **{iss['rule']}**: {iss['details']}") + md_lines.append(f" - Fix: {iss['fix']}") + md_lines.append("") + + if not xl_updates and not repo_updates and not alignment_issues: + md_lines.append("All dependencies are up to date. ✅") + md_lines.append("") + + total = len(xl_updates) + len(repo_updates) + md_lines.append(f"---") + md_lines.append( + f"**Summary**: {total} update(s) available " + f"({len([u for u in xl_updates + repo_updates if u['bump']=='major'])} major, " + f"{len([u for u in xl_updates + repo_updates if u['bump']=='minor'])} minor, " + f"{len([u for u in xl_updates + repo_updates if u['bump']=='patch'])} patch)" + ) + + md_path = repo_dir / "dependency-upgrade-report.md" + with open(md_path, "w", encoding="utf-8") as f: + f.write("\n".join(md_lines)) + print(f"\n Report written: {md_path}") + + # --- JSON report --- + json_data = { + "generated": timestamp, + "summary": { + "total_updates": total, + "major": len([u for u in xl_updates + repo_updates if u["bump"] == "major"]), + "minor": len([u for u in xl_updates + repo_updates if u["bump"] == "minor"]), + "patch": len([u for u in xl_updates + repo_updates if u["bump"] == "patch"]), + "alignment_issues": len(alignment_issues), + }, + "xl_platform_updates": xl_updates, + "repo_updates": repo_updates, + "alignment_issues": alignment_issues, + } + + json_path = repo_dir / "dependency-upgrade-report.json" + with open(json_path, "w", encoding="utf-8") as f: + json.dump(json_data, f, indent=2, ensure_ascii=False) + print(f" Report written: {json_path}") + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +def main(): + parser = argparse.ArgumentParser(description="Dependency scanner for overthere") + parser.add_argument( + "--phase", + choices=["all", "xl-platform-scan", "scan", "alignment"], + default="scan", + help="Which phase(s) to run", + ) + parser.add_argument( + "--xl-platform-dir", + default="../xl-platform", + help="Path to the xl-platform repository root", + ) + args = parser.parse_args() + + # Locate repo root: assume this script lives at .github/skills/dependency-scan/scripts/ + script_dir = Path(__file__).resolve().parent + repo_dir = script_dir.parent.parent.parent.parent # up 4 levels + # Sanity check + if not (repo_dir / "build.gradle").exists(): + # Try current working directory + repo_dir = Path.cwd() + if not (repo_dir / "build.gradle").exists(): + print("[ERROR] Cannot locate repo root (build.gradle not found).") + print(f" Tried: {repo_dir}") + sys.exit(1) + + xl_platform_dir = Path(args.xl_platform_dir).resolve() + phase = args.phase + + print(f"Overthere dependency scan") + print(f" repo_dir : {repo_dir}") + print(f" xl_platform_dir: {xl_platform_dir}") + print(f" phase : {phase}") + print(f" nexus creds : {'found' if _NEXUS_CREDS else 'not found (Maven Central only)'}") + + xl_updates: list[dict] = [] + repo_updates: list[dict] = [] + alignment_issues: list[dict] = [] + + if phase in ("all", "xl-platform-scan"): + if not xl_platform_dir.exists(): + print(f"\n[WARN] xl-platform directory not found: {xl_platform_dir}") + print(" Skipping Phase 1. Use --xl-platform-dir to specify the correct path.") + else: + xl_updates = scan_xl_platform(xl_platform_dir) + + if phase in ("all", "scan"): + repo_updates = scan_repo(repo_dir) + + if phase in ("all", "alignment", "scan"): + alignment_issues = check_alignment(repo_dir, repo_updates) + + generate_reports(repo_dir, xl_updates, repo_updates, alignment_issues) + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/.github/skills/dependency-upgrade/SKILL.md b/.github/skills/dependency-upgrade/SKILL.md new file mode 100644 index 00000000..bdd8edbd --- /dev/null +++ b/.github/skills/dependency-upgrade/SKILL.md @@ -0,0 +1,148 @@ +--- +name: dependency-upgrade +description: "Use this skill to interactively select and apply dependency upgrades to overthere. Always reads or re-runs the scan report first, then lets you pick which updates to apply." +argument-hint: "dependency-upgrade [--report path/to/report.json] [--skip-build]" +--- + +# dependency-upgrade Skill — overthere + +Interactive upgrade workflow: scan → select → confirm → apply → verify → (optionally) PR. + +## Step 1 — Check scan report & ask user + +Always ask the user one of these: + +**If `dependency-upgrade-report.json` already exists in the repo root:** +> "A scan report already exists (generated on {timestamp}). Use existing report or re-scan?" +> - [1] Use existing report +> - [2] Re-scan now (all phases) + +**If no report exists:** +> "No scan report found. Running dependency scan now..." +→ Automatically run: +```bash +python3 .github/skills/dependency-scan/scripts/scan-dependencies.py --phase scan +``` +> Add `--phase all --xl-platform-dir ../xl-platform` if xl-platform scan is also requested. + +> ⚠️ The scan script lives **only** in `dependency-scan/scripts/` — do NOT duplicate it here. + +## Step 2 — Present numbered checkbox list + +Group by bump type with defaults pre-applied: + +``` +Select updates to apply (defaults: patch ✅ minor ✅ major ☐): + +Patch updates + [x] 1. org.slf4j:slf4j-api 2.0.13 → 2.0.17 🔴 Logging + [x] 2. org.slf4j:jcl-over-slf4j 2.0.13 → 2.0.17 🔴 Logging + [x] 3. org.bouncycastle:bcprov-jdk18on 1.80 → 1.83 🔴 Security + ... + +Minor updates + [x] 7. com.hierynomus:sshj 0.38.0 → 0.40.0 🟠 SSH + ... + +Major updates + [ ] 12. org.apache.httpcomponents:httpclient 4.5.14 → 5.3.0 🟠 HTTP + ... + +Type toggle commands (e.g. +1, -3, all patch, ok) or see selection-protocol.md +``` + +## Step 3 — Toggle commands + +See [selection-protocol.md](references/selection-protocol.md) for full command reference. + +## Step 4 — Confirmation + +After `ok`: +1. Show final summary table (checked items only) +2. Require explicit `yes` before any file is modified + +## Step 5 — Apply xl-platform upgrades (if Phase 1 was run) + +1. Edit `xl-reference.conf` / `xl-jakartaee-bom.conf` in xl-platform +2. Keep related versions in sync (e.g. `jacksonVersion` and `jacksonModuleScalaVersion`) +3. Build & publish xl-platform: + ```bash + cd ../xl-platform && ./gradlew clean build publishToMavenLocal -x test + ``` +4. If an artifact fails Nexus resolution → revert that dep, inform user, retry build +5. Update `xlPlatformVersion` in `gradle/dependencies.conf` to the SNAPSHOT version (if applicable) +6. Verify overthere: `cd {REPO} && ./gradlew clean build` + +> **Note**: overthere does not currently consume the xl-platform BOM. Skip xl-platform steps unless you have added BOM consumption. + +## Step 6 — Apply repo upgrades (build.gradle) + +For each selected update: + +1. **Backup** `build.gradle` with timestamp: `build.gradle.backup.YYYYMMDD_HHMMSS` +2. **Edit** in place: + - Buildscript plugin: replace `'group:artifact:OLD'` → `'group:artifact:NEW'` in the `buildscript` block + - Library dep: replace `'group:artifact:OLD'` → `'group:artifact:NEW'` in the `dependencies` block + - Plugin block: replace `version "OLD"` → `version "NEW"` on the line with that plugin id +3. **Build verify**: + ```bash + ./gradlew clean build + ``` + (on Windows: `gradlew.bat clean build`) + +## Step 7 — Rollback on failure + +If the build fails after any edit: +1. Restore all `.backup.*` files +2. Report which update caused the failure +3. Stop execution (do not attempt remaining updates) + +## Step 8 — Offer PR creation + +After a successful build: +> "All selected upgrades applied successfully. Create a PR? (yes/no)" + +If yes, hand off to `create-pr` skill. + +--- + +## Quick Scan + Upgrade (combined) + +When user says "quick scan and upgrade" or similar: + +1. Run scan silently (`--phase scan`) +2. Present **combined checkbox list** in one shot +3. Wait for user selection + `ok` +4. Apply in listed order +5. One build verify at the end + +--- + +## Execute script + +```bash +python3 .github/skills/dependency-upgrade/scripts/execute-upgrade.py \ + --report dependency-upgrade-report.json \ + --project-dir . + +# Skip build verification (dry run) +python3 .github/skills/dependency-upgrade/scripts/execute-upgrade.py \ + --report dependency-upgrade-report.json \ + --project-dir . \ + --skip-build + +# Auto-select specific items (non-interactive) +python3 .github/skills/dependency-upgrade/scripts/execute-upgrade.py \ + --report dependency-upgrade-report.json \ + --auto-select 1,3,5 +``` + +--- + +## Rules + +- ❌ Never auto-select updates without user confirmation +- ❌ Never apply upgrades without an explicit `yes` +- ❌ Never skip build verification after edits +- ✅ Always roll back on build failure +- ✅ Always invoke `create-pr` only after successful build and explicit user consent diff --git a/.github/skills/dependency-upgrade/references/selection-protocol.md b/.github/skills/dependency-upgrade/references/selection-protocol.md new file mode 100644 index 00000000..7e0b3ee4 --- /dev/null +++ b/.github/skills/dependency-upgrade/references/selection-protocol.md @@ -0,0 +1,71 @@ +# Selection Protocol + +Reference for toggle commands when choosing which dependency upgrades to apply. + +## Toggle commands + +| Command | Effect | +|---------|--------| +| `+N` or `check N` | Check item N | +| `-N` or `uncheck N` | Uncheck item N | +| `+N, M` or `check N, M` | Check multiple items | +| `-N-M` or `uncheck N-M` | Uncheck a range (e.g. `-3-7` unchecks 3,4,5,6,7) | +| `except N M` | Keep all currently checked except items N and M | +| `only N M` | Check only items N and M (uncheck everything else) | +| `all patch` | Check only patch-level updates | +| `all patch+minor` | Check all patch and minor updates | +| `all minor` | Check only minor-level updates | +| `all major` | Check only major-level updates | +| `all` | Check every item | +| `none` | Uncheck every item | +| `ok` / `proceed` | Confirm the current selection and move to summary | +| `reset` | Reset to defaults (patch ✅ minor ✅ major ☐) | + +## Confirmation flow + +After typing `ok`: + +1. A final summary table is shown (checked items only): + ``` + Will apply 5 update(s): + ┌──────────────────────────────────────────────┬─────────┬─────────┬───────┐ + │ Dependency │ Current │ Latest │ Bump │ + ├──────────────────────────────────────────────┼─────────┼─────────┼───────┤ + │ org.slf4j:slf4j-api │ 2.0.13 │ 2.0.17 │ patch │ + │ org.bouncycastle:bcprov-jdk18on │ 1.80 │ 1.83 │ patch │ + │ com.hierynomus:sshj │ 0.38.0 │ 0.40.0 │ minor │ + └──────────────────────────────────────────────┴─────────┴─────────┴───────┘ + ``` +2. You must type **`yes`** (not just Enter) to proceed. +3. Typing `no`, `cancel`, or `reset` returns to the selection list. + +## Risk guidance + +| Emoji | Category | Guidance | +|-------|----------|----------| +| 🔴 | Security, Core Framework, Logging | Review changelogs before applying; test carefully | +| 🟠 | SSH, SMB, HTTP, Serialization, GCP, Data | Check for API changes | +| 🟡 | Testing, Observability, Jakarta EE | Low risk; usually safe to batch | +| 🟢 | Build plugins | Lowest risk; check Gradle compatibility | +| ⚪ | Unknown | Evaluate manually | + +## Examples + +``` +# Check all patch + minor, plus major item 12: +all patch+minor ++12 +ok + +# Check only items 1, 2, 3: +only 1 2 3 +ok + +# Check all, then uncheck the two major HTTP updates: +all +-12-13 +ok + +# Start fresh: +reset +``` diff --git a/.github/skills/dependency-upgrade/scripts/execute-upgrade.py b/.github/skills/dependency-upgrade/scripts/execute-upgrade.py new file mode 100644 index 00000000..413290fb --- /dev/null +++ b/.github/skills/dependency-upgrade/scripts/execute-upgrade.py @@ -0,0 +1,502 @@ +#!/usr/bin/env python3 +""" +execute-upgrade.py — Apply dependency upgrades to overthere +Reads dependency-upgrade-report.json, presents a selection UI, and patches +build.gradle in place. + +Usage: + python3 execute-upgrade.py --report dependency-upgrade-report.json --project-dir . + python3 execute-upgrade.py --report dependency-upgrade-report.json --skip-build + python3 execute-upgrade.py --report dependency-upgrade-report.json --auto-select all + python3 execute-upgrade.py --report dependency-upgrade-report.json --auto-select 1,3,5 +""" + +import argparse +import json +import re +import shutil +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +# Windows: ensure stdout/stderr use UTF-8 so emoji and non-ASCII chars print correctly +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") +if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + +IS_WINDOWS = sys.platform == "win32" + +# Cross-platform gradlew: use gradlew.bat on Windows +BUILD_COMMAND = ( + ["gradlew.bat", "clean", "build"] if IS_WINDOWS else ["./gradlew", "clean", "build"] +) + +BUILD_TIMEOUT = 600 # 10 minutes + +# --------------------------------------------------------------------------- +# Colour helpers (ANSI — disabled on Windows if not supported) +# --------------------------------------------------------------------------- +_USE_COLOR = not IS_WINDOWS or (IS_WINDOWS and sys.stdout.isatty()) + + +def _c(code: str, text: str) -> str: + if _USE_COLOR: + return f"\033[{code}m{text}\033[0m" + return text + + +def red(t): return _c("31", t) +def green(t): return _c("32", t) +def yellow(t): return _c("33", t) +def cyan(t): return _c("36", t) +def bold(t): return _c("1", t) + + +# --------------------------------------------------------------------------- +# Backup helpers +# --------------------------------------------------------------------------- +def make_backup(path: Path) -> Path: + ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") + backup = path.parent / f"{path.name}.backup.{ts}" + shutil.copy2(str(path), str(backup)) + return backup + + +def restore_backup(backup: Path, original: Path): + shutil.copy2(str(backup), str(original)) + print(f" Restored: {original.name} from {backup.name}") + + +# --------------------------------------------------------------------------- +# File patchers +# --------------------------------------------------------------------------- +def patch_gradle_dep( + path: Path, group: str, artifact: str, old_ver: str, new_ver: str +) -> bool: + """Replace group:artifact:OLD → group:artifact:NEW in a Gradle file.""" + text = path.read_text(encoding="utf-8", errors="replace") + old_coord = f"{group}:{artifact}:{old_ver}" + new_coord = f"{group}:{artifact}:{new_ver}" + if old_coord not in text: + # Try with quotes + for q in ("'", '"'): + if f"{q}{old_coord}{q}" in text: + text = text.replace(f"{q}{old_coord}{q}", f"{q}{new_coord}{q}") + path.write_text(text, encoding="utf-8") + return True + return False + text = text.replace(old_coord, new_coord) + path.write_text(text, encoding="utf-8") + return True + + +def patch_plugin_version(path: Path, plugin_id: str, old_ver: str, new_ver: str) -> bool: + """Replace version on the line containing a plugin id declaration.""" + text = path.read_text(encoding="utf-8", errors="replace") + # Pattern: id "some.plugin.id" version "OLD" + pattern = re.compile( + r"""(id\s+['"]""" + re.escape(plugin_id) + r"""['"]\s+version\s+['"])""" + + re.escape(old_ver) + + r"""(['"])""", + re.MULTILINE, + ) + new_text, count = pattern.subn(lambda m: m.group(1) + new_ver + m.group(2), text) + if count == 0: + return False + path.write_text(new_text, encoding="utf-8") + return True + + +def patch_properties_key(path: Path, key: str, old_ver: str, new_ver: str) -> bool: + """Replace key=OLD → key=NEW in a .properties file.""" + text = path.read_text(encoding="utf-8", errors="replace") + pattern = re.compile( + r"^(" + re.escape(key) + r"\s*=\s*)" + re.escape(old_ver) + r"$", + re.MULTILINE, + ) + new_text, count = pattern.subn(lambda m: m.group(1) + new_ver, text) + if count == 0: + return False + path.write_text(new_text, encoding="utf-8") + return True + + +def patch_conf_key(path: Path, key: str, old_ver: str, new_ver: str) -> bool: + """Replace key: "OLD" or key = "OLD" in a HOCON-style .conf file.""" + text = path.read_text(encoding="utf-8", errors="replace") + pattern = re.compile( + r"""(^[ \t]*""" + re.escape(key) + r"""[ \t]*[:=][ \t]*["']?)""" + + re.escape(old_ver) + + r"""(["']?)""", + re.MULTILINE, + ) + new_text, count = pattern.subn(lambda m: m.group(1) + new_ver + m.group(2), text) + if count == 0: + return False + path.write_text(new_text, encoding="utf-8") + return True + + +# --------------------------------------------------------------------------- +# Build runner +# --------------------------------------------------------------------------- +def run_build(project_dir: Path) -> bool: + """Run ./gradlew clean build in project_dir. Returns True on success.""" + print(f"\n Running: {' '.join(BUILD_COMMAND)}") + try: + result = subprocess.run( + BUILD_COMMAND, + cwd=project_dir, + capture_output=True, + text=True, + timeout=BUILD_TIMEOUT, + shell=IS_WINDOWS, # .bat files require shell=True on Windows + ) + if result.returncode == 0: + print(green(" BUILD SUCCESS")) + return True + else: + print(red(" BUILD FAILED")) + print(result.stdout[-3000:] if result.stdout else "") + print(result.stderr[-2000:] if result.stderr else "") + return False + except subprocess.TimeoutExpired: + print(red(f" BUILD TIMED OUT after {BUILD_TIMEOUT}s")) + return False + except FileNotFoundError: + print(red(f" BUILD COMMAND NOT FOUND: {BUILD_COMMAND[0]}")) + return False + + +# --------------------------------------------------------------------------- +# Interactive selection +# --------------------------------------------------------------------------- +RISK_ORDER = {"🔴": 0, "🟠": 1, "🟡": 2, "🟢": 3, "⚪": 4} +BUMP_ORDER = {"major": 0, "minor": 1, "patch": 2, "unknown": 3} + + +def build_item_list(updates: list[dict]) -> list[dict]: + """Flatten updates into a numbered list sorted by bump type, then risk.""" + items = sorted( + updates, + key=lambda u: ( + BUMP_ORDER.get(u.get("bump", "unknown"), 9), + RISK_ORDER.get(u.get("risk", "⚪"), 9), + u.get("group", ""), + ), + ) + # Assign numbers + for i, item in enumerate(items, 1): + item["_num"] = i + return items + + +def default_checked(item: dict) -> bool: + return item.get("bump") in ("patch", "minor") + + +def print_selection(items: list[dict], checked: set[int]): + print() + current_bump = None + for item in items: + bump = item.get("bump", "unknown") + if bump != current_bump: + current_bump = bump + print(f"\n {bold(bump.capitalize() + ' updates')}") + num = item["_num"] + mark = "[x]" if num in checked else "[ ]" + coord = f"{item['group']}:{item['artifact']}" + cur = item.get("current", "?") + lat = item.get("latest", "?") + risk = item.get("risk", "⚪") + cat = item.get("category", "Unknown") + print(f" {mark} {num:>3}. {coord:<50} {cur:>12} → {lat:<12} {risk} {cat}") + print() + + +def parse_toggle(cmd: str, items: list[dict], checked: set[int]) -> Optional[set[int]]: + """Parse a toggle command and return updated checked set, or None if unrecognised.""" + cmd = cmd.strip().lower() + all_nums = {i["_num"] for i in items} + patch_nums = {i["_num"] for i in items if i.get("bump") == "patch"} + minor_nums = {i["_num"] for i in items if i.get("bump") == "minor"} + major_nums = {i["_num"] for i in items if i.get("bump") == "major"} + + if cmd in ("all",): + return set(all_nums) + if cmd == "none": + return set() + if cmd == "reset": + return {i["_num"] for i in items if default_checked(i)} + if cmd in ("all patch",): + return set(patch_nums) + if cmd in ("all minor",): + return set(minor_nums) + if cmd in ("all major",): + return set(major_nums) + if cmd in ("all patch+minor", "all minor+patch"): + return patch_nums | minor_nums + + # +N or check N [, M ...] + m = re.match(r"^(\+|check\s+)(.+)$", cmd) + if m: + for part in re.split(r"[,\s]+", m.group(2)): + part = part.strip() + if part.isdigit(): + checked.add(int(part)) + return checked + + # -N or uncheck N [, M ...] or N-M range + m = re.match(r"^(-|uncheck\s+)(.+)$", cmd) + if m: + spec = m.group(2).strip() + # Range: N-M + r = re.match(r"^(\d+)-(\d+)$", spec) + if r: + for n in range(int(r.group(1)), int(r.group(2)) + 1): + checked.discard(n) + else: + for part in re.split(r"[,\s]+", spec): + if part.isdigit(): + checked.discard(int(part)) + return checked + + # only N M ... + m = re.match(r"^only\s+(.+)$", cmd) + if m: + new = set() + for part in re.split(r"[,\s]+", m.group(1)): + if part.isdigit(): + new.add(int(part)) + return new + + # except N M ... + m = re.match(r"^except\s+(.+)$", cmd) + if m: + for part in re.split(r"[,\s]+", m.group(1)): + if part.isdigit(): + checked.discard(int(part)) + return checked + + return None + + +def interactive_select(items: list[dict]) -> Optional[list[dict]]: + """Interactive selection UI. Returns selected items, or None if user aborted.""" + checked = {i["_num"] for i in items if default_checked(i)} + + print(bold("\nSelect updates to apply (defaults: patch ✅ minor ✅ major ☐)")) + print("Commands: +N/-N, all, none, all patch, all patch+minor, ok, reset, quit") + print("See .github/skills/dependency-upgrade/references/selection-protocol.md for full reference") + print_selection(items, checked) + + while True: + try: + cmd = input(" > ").strip() + except (EOFError, KeyboardInterrupt): + print() + return None + + if not cmd: + continue + if cmd.lower() in ("ok", "proceed"): + selected = [i for i in items if i["_num"] in checked] + if not selected: + print(yellow(" Nothing selected. Type 'all' to select all, or 'quit' to exit.")) + continue + # Show confirmation table + print() + print(bold(f"Will apply {len(selected)} update(s):")) + print(f" {'Dependency':<52} {'Current':>12} {'Latest':<12} {'Bump'}") + print(" " + "-" * 90) + for sel in selected: + coord = f"{sel['group']}:{sel['artifact']}" + print( + f" {coord:<52} {sel.get('current','?'):>12} {sel.get('latest','?'):<12} {sel.get('bump','?')}" + ) + print() + confirm = input(bold(" Type 'yes' to confirm, or anything else to go back: ")).strip().lower() + if confirm == "yes": + return selected + else: + print(" Returning to selection.") + print_selection(items, checked) + continue + if cmd.lower() in ("quit", "exit", "abort"): + return None + + result = parse_toggle(cmd, items, set(checked)) + if result is not None: + checked = result + print_selection(items, checked) + else: + print(yellow(f" Unrecognised command: '{cmd}'")) + + +# --------------------------------------------------------------------------- +# Apply upgrades +# --------------------------------------------------------------------------- +def apply_update(update: dict, project_dir: Path) -> tuple[bool, str]: + """ + Apply a single update to the relevant file. + Returns (success, message). + """ + group = update.get("group", "") + artifact = update.get("artifact", "") + old_ver = update.get("current", "") + new_ver = update.get("latest", "") + src = update.get("source_file", "build.gradle") + scope = update.get("scope", "") + + file_path = project_dir / src + if not file_path.exists(): + # Fallback to build.gradle + file_path = project_dir / "build.gradle" + + if scope == "plugin": + # Plugin block: id "group.artifact" version "OLD" + ok = patch_plugin_version(file_path, group, old_ver, new_ver) + if not ok: + # Fall back to dep coordinate replacement + ok = patch_gradle_dep(file_path, group, artifact, old_ver, new_ver) + elif src.endswith(".properties"): + # Try: treat as plugin/variable name key + key = f"{group}:{artifact}" + ok = patch_properties_key(file_path, key, old_ver, new_ver) + if not ok: + ok = patch_gradle_dep(file_path, group, artifact, old_ver, new_ver) + elif src.endswith(".conf"): + var_name = update.get("variable", "") + ok = patch_conf_key(file_path, var_name, old_ver, new_ver) if var_name else False + else: + ok = patch_gradle_dep(file_path, group, artifact, old_ver, new_ver) + + if ok: + return True, f"Updated {group}:{artifact} {old_ver} → {new_ver} in {file_path.name}" + return False, f"Could not find {group}:{artifact}:{old_ver} in {file_path.name}" + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +def main(): + parser = argparse.ArgumentParser(description="Apply dependency upgrades to overthere") + parser.add_argument("--report", default="dependency-upgrade-report.json", + help="Path to dependency-upgrade-report.json") + parser.add_argument("--project-dir", default=".", + help="Root directory of the overthere project") + parser.add_argument("--skip-build", action="store_true", + help="Skip build verification (dry run)") + parser.add_argument("--auto-select", + help="Non-interactive auto-select: 'all' or comma-separated item numbers (e.g. '1,3,5')") + args = parser.parse_args() + + project_dir = Path(args.project_dir).resolve() + report_path = Path(args.report) + if not report_path.is_absolute(): + report_path = project_dir / report_path + + if not report_path.exists(): + print(red(f"Report not found: {report_path}")) + print("Run scan-dependencies.py first.") + sys.exit(1) + + with open(report_path, encoding="utf-8") as f: + report = json.load(f) + + generated = report.get("generated", "unknown") + print(bold(f"Dependency upgrade report: {report_path}")) + print(f"Generated: {generated}") + + # Collect all updates + all_updates: list[dict] = ( + report.get("xl_platform_updates", []) + report.get("repo_updates", []) + ) + + if not all_updates: + print(green("\nNo updates to apply. All dependencies are up to date.")) + sys.exit(0) + + items = build_item_list(all_updates) + + # Selection + if args.auto_select: + if args.auto_select.lower() == "all": + selected = items + else: + nums = {int(n.strip()) for n in args.auto_select.split(",") if n.strip().isdigit()} + selected = [i for i in items if i["_num"] in nums] + if selected: + print(f"\nAuto-selected {len(selected)} update(s).") + else: + print(yellow("No items matched auto-select criteria.")) + sys.exit(0) + else: + selected = interactive_select(items) + if selected is None: + print("\nUpgrade cancelled.") + sys.exit(0) + + # Collect unique files that will be modified → create backups + files_to_modify: set[Path] = set() + for upd in selected: + src = upd.get("source_file", "build.gradle") + p = project_dir / src + if not p.exists(): + p = project_dir / "build.gradle" + files_to_modify.add(p) + + backups: dict[Path, Path] = {} + for fpath in sorted(files_to_modify): + if fpath.exists(): + bp = make_backup(fpath) + backups[fpath] = bp + print(f" Backup: {fpath.name} → {bp.name}") + + # Apply all selected updates + applied = [] + failed = [] + print(f"\nApplying {len(selected)} update(s)...") + for upd in selected: + ok, msg = apply_update(upd, project_dir) + if ok: + print(f" {green('✓')} {msg}") + applied.append(upd) + else: + print(f" {yellow('?')} {msg}") + failed.append(upd) + + if failed: + print(yellow(f"\n {len(failed)} update(s) could not be applied automatically:")) + for upd in failed: + print(f" - {upd['group']}:{upd['artifact']} ({upd['source_file']})") + print(" These may need manual editing.") + + if not applied: + print(red("\nNo updates were applied.")) + sys.exit(1) + + # Build verification + if args.skip_build: + print(yellow("\n Skipping build verification (--skip-build).")) + print(green(f"\n{len(applied)} update(s) applied successfully (build not verified).")) + sys.exit(0) + + build_ok = run_build(project_dir) + if build_ok: + print(green(f"\n✅ {len(applied)} update(s) applied and build verified.")) + if failed: + print(yellow(f" {len(failed)} update(s) could not be applied — manual review needed.")) + else: + print(red("\n❌ Build failed. Rolling back all changes...")) + for fpath, bp in backups.items(): + restore_backup(bp, fpath) + print(red("Rollback complete. No changes were kept.")) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.github/skills/xl-platform-scan/SKILL.md b/.github/skills/xl-platform-scan/SKILL.md new file mode 100644 index 00000000..7369d8af --- /dev/null +++ b/.github/skills/xl-platform-scan/SKILL.md @@ -0,0 +1,74 @@ +--- +name: xl-platform-scan +description: "Use this skill to scan the xl-platform BOM for available dependency updates. Standalone — reusable for any repo that consumes the xl-platform BOM." +argument-hint: "xl-platform-scan [--xl-platform-dir ../xl-platform]" +--- + +# xl-platform-scan Skill + +Standalone scan of the xl-platform BOM for available dependency updates. **Reusable across any repo** that consumes the xl-platform BOM — no dependency on any specific repo's configuration. + +## What it scans + +| Source file | Variables | Notes | +|---|---|---| +| `xl-reference/xl-reference.conf` | ~100 unique version vars | Core platform dependency versions | +| `xl-jakartaee-bom/xl-jakartaee-bom.conf` | ~30 vars | Jakarta EE stack | +| `gradle.properties` (xl-platform root) | ~5 build plugin vars | Gradle plugin versions | +| **Total** | **~135 unique version variables** | | + +## Resolution strategy + +1. **`XL_PLATFORM_COORDINATES`** — 114 explicit `variableName → group:artifact` mappings (78 from xl-reference.conf + 30 Jakarta BOM + 6 build plugins) +2. **`SKIP_VARIABLES`** — 24 variables excluded from lookup (meta vars, Python plugin deps, internal forks, Gradle settings); see below +3. **Dynamic inference** — 46 regex patterns strip `Version` suffix and infer Maven coordinates via Maven Central search API → Nexus search fallback + +Coverage: 114 mapped + 24 skipped = full coverage of all ~135 source variables. + +## Skipped variables (24 total, `SKIP_VARIABLES`) + +| Category | Variables | +|---|---| +| Meta / non-version | `scalaVersion`, `scalaFullVersion`, `pekkoMajorVersion` | +| Python plugin deps | `pyTestVersion`, `pyMockVersion`, `pyConanVersion`, `pyJinja2Version`, `pyParamikoVersion`, `pyRequestsVersion`, `pySixVersion`, `pyYamlVersion`, `pyLxmlVersion`, `pySetuptoolsVersion`, `pyWheelVersion`, `pyTwineVersion`, `pyVirtualenvVersion` | +| Internal artifacts | `crashVersion`, `scannitVersion`, `docBaseStyleVersion`, `overcastVersion`, `jythonStandaloneVersion` | +| Derived versions | `jacksonAnnotationsVersion` (follows `jacksonVersion`) | +| Gradle settings | `languageLevel`, `release.stage` | + +## Risk categories + +| Category | Risk | Examples | +|---|---|---| +| Security | 🔴 High | Bouncy Castle, TLS libs | +| Core Framework | 🔴 High | Spring Boot, Hibernate | +| Logging | 🔴 High | SLF4J, Logback | +| Data | 🟠 Medium | JDBC drivers, connection pools | +| Serialization | 🟠 Medium | Jackson, Kryo | +| HTTP | 🟠 Medium | Apache HttpClient, Netty | +| Testing | 🟡 Low | TestNG, Mockito, Hamcrest | +| Build | 🟢 Info | Gradle plugins | + +## Spring compatibility matrix + +| Dependency | Constraint | +|---|---| +| Hibernate | Must be compatible with Spring Boot version | +| Spring Security | Must match Spring Boot major version | +| Jackson | Managed by Spring Boot BOM — do not independently upgrade | + +## Run command + +```bash +python3 .github/skills/dependency-scan/scripts/scan-dependencies.py \ + --phase xl-platform-scan \ + --xl-platform-dir ../xl-platform +``` + +> **Note**: This skill has no `scripts/` or `references/` of its own. +> Script lives at: `dependency-scan/scripts/scan-dependencies.py` +> Architecture details at: `dependency-scan/references/architecture.md` + +## Output + +- `.github/skills/dependency-scan/dependency-upgrade-report.md` — human-readable report +- `.github/skills/dependency-scan/dependency-upgrade-report.json` — machine-readable JSON for upgrade skill diff --git a/build.gradle b/build.gradle index 42afe6cb..4ffdb56d 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ buildscript { } dependencies { classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.30.0" - classpath "com.netflix.nebula:nebula-release-plugin:20.2.0" + classpath "com.netflix.nebula:nebula-release-plugin:21.0.0" classpath "ru.vyarus:gradle-pom-plugin:3.0.0" classpath "org.ajoberstar.grgit:grgit-gradle:5.3.3" } @@ -156,10 +156,10 @@ dependencies { api "org.bouncycastle:bcpkix-jdk18on:1.83" // Telnet - api 'commons-net:commons-net:3.12.0' + api 'commons-net:commons-net:3.13.0' // WinRM - api('org.dom4j:dom4j:2.1.4') { + api('org.dom4j:dom4j:2.2.0') { exclude group: 'xml-apis', module: 'xml-apis' } api('jaxen:jaxen:2.0.0') { @@ -172,16 +172,16 @@ dependencies { exclude group: 'commons-logging', module: 'commons-logging' } api 'org.apache.httpcomponents:httpcore:4.4.16' - api 'commons-codec:commons-codec:1.19.0' + api 'commons-codec:commons-codec:1.21.0' // GCP api 'com.jcraft:jsch:0.1.55' api 'com.google.apis:google-api-services-compute:v1-rev20251110-2.0.0' - api 'com.google.auth:google-auth-library-oauth2-http:1.41.0' - api('com.google.cloud:google-cloud-os-login:2.80.0') { + api 'com.google.auth:google-auth-library-oauth2-http:1.43.0' + api('com.google.cloud:google-cloud-os-login:2.87.0') { exclude group: 'io.grpc', module: 'grpc-netty-shaded' } - implementation 'io.grpc:grpc-netty-shaded:1.78.0' + implementation 'io.grpc:grpc-netty-shaded:1.80.0' // Test dependencies testImplementation('com.xebialabs.cloud:overcast:2.5.1') { @@ -190,17 +190,17 @@ dependencies { } testImplementation 'org.hamcrest:hamcrest-core:3.0' testImplementation 'org.hamcrest:hamcrest-library:3.0' - testImplementation 'org.mockito:mockito-core:5.21.0' + testImplementation 'org.mockito:mockito-core:5.23.0' - testImplementation('org.testng:testng:7.10.2') { + testImplementation('org.testng:testng:7.12.0') { exclude group: "junit", module: "junit" } testImplementation('nl.javadude.assumeng:assumeng:1.2.4') { exclude group: "junit", module: "junit" } - testImplementation 'com.google.guava:guava:33.4.6-jre' + testImplementation 'com.google.guava:guava:33.5.0-jre' - testImplementation 'ch.qos.logback:logback-classic:1.5.24' + testImplementation 'ch.qos.logback:logback-classic:1.5.32' } tasks.withType(Test).configureEach { diff --git a/dependency-upgrade-report.json b/dependency-upgrade-report.json new file mode 100644 index 00000000..aa30e9e7 --- /dev/null +++ b/dependency-upgrade-report.json @@ -0,0 +1,157 @@ +{ + "generated": "2026-03-27 11:38 UTC", + "summary": { + "total_updates": 11, + "major": 1, + "minor": 9, + "patch": 1, + "alignment_issues": 0 + }, + "xl_platform_updates": [], + "repo_updates": [ + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 35, + "group": "com.netflix.nebula", + "artifact": "nebula-release-plugin", + "current": "20.2.0", + "latest": "21.0.0", + "bump": "major", + "scope": "buildscript", + "category": "Build", + "risk": "🟢" + }, + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 159, + "group": "commons-net", + "artifact": "commons-net", + "current": "3.12.0", + "latest": "3.13.0", + "bump": "minor", + "scope": "api", + "category": "Network", + "risk": "🟠" + }, + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 162, + "group": "org.dom4j", + "artifact": "dom4j", + "current": "2.1.4", + "latest": "2.2.0", + "bump": "minor", + "scope": "api", + "category": "Serialization", + "risk": "🟠" + }, + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 175, + "group": "commons-codec", + "artifact": "commons-codec", + "current": "1.19.0", + "latest": "1.21.0", + "bump": "minor", + "scope": "api", + "category": "Serialization", + "risk": "🟠" + }, + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 180, + "group": "com.google.auth", + "artifact": "google-auth-library-oauth2-http", + "current": "1.41.0", + "latest": "1.43.0", + "bump": "minor", + "scope": "api", + "category": "Security", + "risk": "🔴" + }, + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 181, + "group": "com.google.cloud", + "artifact": "google-cloud-os-login", + "current": "2.80.0", + "latest": "2.87.0", + "bump": "minor", + "scope": "api", + "category": "GCP", + "risk": "🟠" + }, + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 184, + "group": "io.grpc", + "artifact": "grpc-netty-shaded", + "current": "1.78.0", + "latest": "1.80.0", + "bump": "minor", + "scope": "implementation", + "category": "HTTP", + "risk": "🟠" + }, + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 193, + "group": "org.mockito", + "artifact": "mockito-core", + "current": "5.21.0", + "latest": "5.23.0", + "bump": "minor", + "scope": "test", + "category": "Testing", + "risk": "🟡" + }, + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 195, + "group": "org.testng", + "artifact": "testng", + "current": "7.10.2", + "latest": "7.12.0", + "bump": "minor", + "scope": "test", + "category": "Testing", + "risk": "🟡" + }, + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 201, + "group": "com.google.guava", + "artifact": "guava", + "current": "33.4.6-jre", + "latest": "33.5.0-jre", + "bump": "minor", + "scope": "test", + "category": "Testing", + "risk": "🟡" + }, + { + "type": "repo", + "source_file": "build.gradle", + "line_num": 203, + "group": "ch.qos.logback", + "artifact": "logback-classic", + "current": "1.5.24", + "latest": "1.5.32", + "bump": "patch", + "scope": "test", + "category": "Logging", + "risk": "🔴" + } + ], + "alignment_issues": [] +} \ No newline at end of file diff --git a/dependency-upgrade-report.md b/dependency-upgrade-report.md new file mode 100644 index 00000000..d2f3a1bd --- /dev/null +++ b/dependency-upgrade-report.md @@ -0,0 +1,34 @@ +# Dependency Upgrade Report + +Generated: 2026-03-27 11:38 UTC + +## overthere Direct Dependency Updates (Phase 2) + +### Major updates + +| Risk | Dependency | Current | Latest | Bump | Category | +|------|------------|---------|--------|------|----------| +| 🟢 | `com.netflix.nebula:nebula-release-plugin` | `20.2.0` | `21.0.0` | major | Build | + +### Minor updates + +| Risk | Dependency | Current | Latest | Bump | Category | +|------|------------|---------|--------|------|----------| +| 🟠 | `com.google.cloud:google-cloud-os-login` | `2.80.0` | `2.87.0` | minor | GCP | +| 🟠 | `io.grpc:grpc-netty-shaded` | `1.78.0` | `1.80.0` | minor | HTTP | +| 🟠 | `commons-net:commons-net` | `3.12.0` | `3.13.0` | minor | Network | +| 🔴 | `com.google.auth:google-auth-library-oauth2-http` | `1.41.0` | `1.43.0` | minor | Security | +| 🟠 | `org.dom4j:dom4j` | `2.1.4` | `2.2.0` | minor | Serialization | +| 🟠 | `commons-codec:commons-codec` | `1.19.0` | `1.21.0` | minor | Serialization | +| 🟡 | `org.mockito:mockito-core` | `5.21.0` | `5.23.0` | minor | Testing | +| 🟡 | `org.testng:testng` | `7.10.2` | `7.12.0` | minor | Testing | +| 🟡 | `com.google.guava:guava` | `33.4.6-jre` | `33.5.0-jre` | minor | Testing | + +### Patch updates + +| Risk | Dependency | Current | Latest | Bump | Category | +|------|------------|---------|--------|------|----------| +| 🔴 | `ch.qos.logback:logback-classic` | `1.5.24` | `1.5.32` | patch | Logging | + +--- +**Summary**: 11 update(s) available (1 major, 9 minor, 1 patch) \ No newline at end of file