From e226c01698813524a26229923f9310d4c56f2050 Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Sun, 19 Apr 2026 16:24:49 +0300 Subject: [PATCH 1/6] feat(batch): add batch workspace analysis action and settings UI Add a new "Batch Workspace Analysis Report" action that triggers batch stack analysis on all packages in a JS/TS monorepo or Cargo workspace from the project root. The action validates workspace layout before calling the API and shows a warning dialog for unsupported projects. Includes batch-specific settings (concurrency, continueOnError, metadata) in the settings UI, a dedicated setBatchRequestProperties() method in ApiService, and tests for both the API delegation and settings defaults. Upgrades trustify-da-java-client to 0.0.16-SNAPSHOT for the batch API, Java target to 21 for bytecode compatibility, and Mockito to 5.x for Java 21 support. Implements TC-3865 Assisted-by: Claude Code --- build.gradle.kts | 4 +- gradle/libs.versions.toml | 4 +- .../tools/intellij/exhort/ApiService.java | 91 ++++++++++ .../settings/ApiSettingsComponent.java | 45 +++++ .../settings/ApiSettingsConfigurable.java | 9 + .../intellij/settings/ApiSettingsState.java | 4 + .../intellij/stackanalysis/SaBatchAction.java | 157 ++++++++++++++++ src/main/resources/META-INF/plugin.xml | 11 ++ .../intellij/exhort/ApiServiceBatchTest.java | 171 ++++++++++++++++++ .../stackanalysis/SaBatchActionTest.java | 78 ++++++++ 10 files changed, 570 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/jboss/tools/intellij/stackanalysis/SaBatchAction.java create mode 100644 src/test/java/org/jboss/tools/intellij/exhort/ApiServiceBatchTest.java create mode 100644 src/test/java/org/jboss/tools/intellij/stackanalysis/SaBatchActionTest.java diff --git a/build.gradle.kts b/build.gradle.kts index ce264462..6f4a0d0b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,8 +14,8 @@ version = providers.gradleProperty("projectVersion").get() // Plugin version val platformVersion = providers.gradleProperty("ideaVersion").get() // Set the JVM language level used to build the project. java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 withSourcesJar() withJavadocJar() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9788b40..92a17320 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,10 +4,10 @@ caffeine = "3.1.8" commons-compress = "1.21" commons-io = "2.16.1" trustify-da-api-spec = "2.0.2" -trustify-da-java-client = "0.0.15" +trustify-da-java-client = "0.0.16-SNAPSHOT" github-api = "1.314" junit = "4.13.2" -mockito = "4.11.0" +mockito = "5.14.2" packageurl-java = "1.4.1" # plugins diff --git a/src/main/java/org/jboss/tools/intellij/exhort/ApiService.java b/src/main/java/org/jboss/tools/intellij/exhort/ApiService.java index e449c386..a6f39195 100644 --- a/src/main/java/org/jboss/tools/intellij/exhort/ApiService.java +++ b/src/main/java/org/jboss/tools/intellij/exhort/ApiService.java @@ -29,7 +29,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; @@ -88,6 +91,36 @@ public Path getStackAnalysis(final String packageManager, final String manifestN } } + /** + * Performs batch stack analysis on all workspace members in the given directory, + * returning the path to a temporary HTML report file. + * + * @param workspacePath the workspace root directory path + * @param ignorePatterns glob patterns for paths to exclude from workspace discovery + * @return the path to the generated HTML report file + */ + public Path getBatchStackAnalysis(final String workspacePath, final List ignorePatterns) { + var telemetryMsg = TelemetryService.instance().action("batch-stack-analysis"); + telemetryMsg.property(TelemetryKeys.PLATFORM.toString(), System.getProperty("os.name")); + telemetryMsg.property(TelemetryKeys.TRUST_DA_TOKEN.toString(), ApiSettingsState.getInstance().rhdaToken); + + try { + setBatchRequestProperties(); + Set ignorePatternsSet = new HashSet<>(ignorePatterns); + var htmlContent = exhortApi.stackAnalysisBatchHtml(Path.of(workspacePath), ignorePatternsSet); + var tmpFile = Files.createTempFile("exhort_batch_", ".html"); + Files.write(tmpFile, htmlContent.get()); + + telemetryMsg.send(); + return tmpFile; + + } catch (IOException | InterruptedException | ExecutionException exc) { + telemetryMsg.error(exc); + telemetryMsg.send(); + throw new RuntimeException(exc); + } + } + public AnalysisReport getComponentAnalysis(final String packageManager, final String manifestName, final String manifestPath) { var telemetryMsg = TelemetryService.instance().action("component-analysis"); telemetryMsg.property(TelemetryKeys.ECOSYSTEM.toString(), packageManager); @@ -274,6 +307,64 @@ private void setRequestProperties(final String manifestName) { } } + void setBatchRequestProperties() { + String ideName = ApplicationInfo.getInstance().getFullApplicationName(); + PluginDescriptor pluginDescriptor = PluginManagerCore.getPlugin(PluginId.getId("org.jboss.tools.intellij.analytics")); + if (pluginDescriptor != null) { + String pluginName = pluginDescriptor.getName() + " " + pluginDescriptor.getVersion(); + System.setProperty("TRUST_DA_SOURCE", ideName + " / " + pluginName); + } else { + System.setProperty("TRUST_DA_SOURCE", ideName); + } + + ApiSettingsState settings = ApiSettingsState.getInstance(); + System.setProperty("TRUST_DA_TOKEN", settings.rhdaToken); + + setBackendUrl(); + + if (settings.batchConcurrency != null && !settings.batchConcurrency.isBlank()) { + System.setProperty("TRUSTIFY_DA_BATCH_CONCURRENCY", settings.batchConcurrency); + } else { + System.setProperty("TRUSTIFY_DA_BATCH_CONCURRENCY", "10"); + } + System.setProperty("TRUSTIFY_DA_CONTINUE_ON_ERROR", String.valueOf(settings.batchContinueOnError)); + System.setProperty("TRUSTIFY_DA_BATCH_METADATA", String.valueOf(settings.batchMetadata)); + + // Set tool paths needed for batch analysis + if (settings.npmPath != null && !settings.npmPath.isBlank()) { + System.setProperty("TRUSTIFY_DA_NPM_PATH", settings.npmPath); + } else { + System.clearProperty("TRUSTIFY_DA_NPM_PATH"); + } + if (settings.yarnPath != null && !settings.yarnPath.isBlank()) { + System.setProperty("TRUSTIFY_DA_YARN_PATH", settings.yarnPath); + } else { + System.clearProperty("TRUSTIFY_DA_YARN_PATH"); + } + if (settings.nodePath != null && !settings.nodePath.isBlank()) { + System.setProperty("NODE_HOME", settings.nodePath); + } else { + System.clearProperty("NODE_HOME"); + } + if (settings.pnpmPath != null && !settings.pnpmPath.isBlank()) { + System.setProperty("TRUSTIFY_DA_PNPM_PATH", settings.pnpmPath); + } else { + System.clearProperty("TRUSTIFY_DA_PNPM_PATH"); + } + if (settings.cargoPath != null && !settings.cargoPath.isBlank()) { + System.setProperty("TRUSTIFY_DA_CARGO_PATH", settings.cargoPath); + } else { + System.clearProperty("TRUSTIFY_DA_CARGO_PATH"); + } + + Optional proxyUrlOpt = getProxyUrl(); + if (proxyUrlOpt.isPresent()) { + System.setProperty("TRUSTIFY_DA_PROXY_URL", proxyUrlOpt.get()); + } else { + System.clearProperty("TRUSTIFY_DA_PROXY_URL"); + } + } + public static void setBackendUrl() { String backendUrl = System.getenv(TRUSTIFY_DA_BACKEND_URL_PROPERTY); if (backendUrl == null || backendUrl.isBlank()) { diff --git a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsComponent.java b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsComponent.java index 8ab64e4c..0eeb4096 100644 --- a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsComponent.java +++ b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsComponent.java @@ -81,6 +81,12 @@ public class ApiSettingsComponent { private final static String manifestExclusionPatternsLabel = "Component Analysis > Exclusion Patterns" + "
Specifies glob patterns for manifest files to exclude from component analysis." + "
One pattern per line. Examples: **/node_modules/**/package.json, test/**/pom.xml"; + private final static String batchConcurrencyLabel = "Batch Analysis > Concurrency" + + "
Maximum number of concurrent analyses during batch workspace analysis."; + private final static String batchContinueOnErrorLabel = "Batch Analysis > Continue on Error" + + "
Continue analyzing remaining packages when one package analysis fails."; + private final static String batchMetadataLabel = "Batch Analysis > Include Metadata" + + "
Include additional metadata in batch analysis results."; private final static String reportFilePathLabel = "Reports > Save Directory: Path" + "
Specifies directory where stack analytics reports will be saved permanently." + "
Leave empty to use temporary files only."; @@ -113,6 +119,9 @@ public class ApiSettingsComponent { private final TextFieldWithBrowseButton dockerPathText; private final TextFieldWithBrowseButton podmanPathText; private final JBTextField imagePlatformText; + private final JBTextField batchConcurrencyText; + private final JBCheckBox batchContinueOnErrorCheck; + private final JBCheckBox batchMetadataCheck; private final JBTextArea manifestExclusionPatternsText; private final JBScrollPane manifestExclusionPatternsScrollPane; private final TextFieldWithBrowseButton reportFilePathText; @@ -252,6 +261,10 @@ public ApiSettingsComponent() { imagePlatformText = new JBTextField(); + batchConcurrencyText = new JBTextField(); + batchContinueOnErrorCheck = new JBCheckBox("Continue analyzing when a package fails"); + batchMetadataCheck = new JBCheckBox("Include metadata in batch results"); + manifestExclusionPatternsText = new JBTextArea(); manifestExclusionPatternsText.setRows(5); manifestExclusionPatternsText.setColumns(50); @@ -319,6 +332,13 @@ public ApiSettingsComponent() { .addLabeledComponent(new JBLabel(imagePlatformLabel), imagePlatformText, 1, true) .addSeparator(10) .addVerticalGap(10) + .addLabeledComponent(new JBLabel(batchConcurrencyLabel), batchConcurrencyText, 1, true) + .addVerticalGap(10) + .addLabeledComponent(new JBLabel(batchContinueOnErrorLabel), batchContinueOnErrorCheck, 1, true) + .addVerticalGap(10) + .addLabeledComponent(new JBLabel(batchMetadataLabel), batchMetadataCheck, 1, true) + .addSeparator(10) + .addVerticalGap(10) .addLabeledComponent(new JBLabel(manifestExclusionPatternsLabel), manifestExclusionPatternsScrollPane, 1, true) .addVerticalGap(10) .addLabeledComponent(new JBLabel(reportFilePathLabel), reportFilePathText, 1, true) @@ -544,6 +564,31 @@ public void setCargoPathText(@NotNull String text) { cargoPathText.setText(text); } + @NotNull + public String getBatchConcurrencyText() { + return batchConcurrencyText.getText(); + } + + public void setBatchConcurrencyText(@NotNull String text) { + batchConcurrencyText.setText(text); + } + + public boolean getBatchContinueOnErrorCheck() { + return batchContinueOnErrorCheck.isSelected(); + } + + public void setBatchContinueOnErrorCheck(boolean selected) { + batchContinueOnErrorCheck.setSelected(selected); + } + + public boolean getBatchMetadataCheck() { + return batchMetadataCheck.isSelected(); + } + + public void setBatchMetadataCheck(boolean selected) { + batchMetadataCheck.setSelected(selected); + } + @NotNull public String getManifestExclusionPatternsText() { return manifestExclusionPatternsText.getText(); diff --git a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsConfigurable.java b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsConfigurable.java index adeb5a73..ffb86efd 100644 --- a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsConfigurable.java +++ b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsConfigurable.java @@ -69,6 +69,9 @@ public boolean isModified() { modified |= !settingsComponent.getImagePlatformText().equals(settings.imagePlatform); modified |= !settingsComponent.getGradlePathText().equals(settings.gradlePath); modified |= !Objects.equals(settingsComponent.getCargoPathText(), settings.cargoPath); + modified |= !Objects.equals(settingsComponent.getBatchConcurrencyText(), settings.batchConcurrency); + modified |= settingsComponent.getBatchContinueOnErrorCheck() != settings.batchContinueOnError; + modified |= settingsComponent.getBatchMetadataCheck() != settings.batchMetadata; modified |= !settingsComponent.getManifestExclusionPatternsText().equals(settings.manifestExclusionPatterns); modified |= !settingsComponent.getReportFilePathText().equals(settings.reportFilePath); return modified; @@ -103,6 +106,9 @@ public void apply() { settings.gradlePath = settingsComponent.getGradlePathText(); settings.reportFilePath = settingsComponent.getReportFilePathText(); settings.cargoPath = settingsComponent.getCargoPathText(); + settings.batchConcurrency = settingsComponent.getBatchConcurrencyText(); + settings.batchContinueOnError = settingsComponent.getBatchContinueOnErrorCheck(); + settings.batchMetadata = settingsComponent.getBatchMetadataCheck(); // Check if exclusion patterns changed String oldPatterns = settings.manifestExclusionPatterns; @@ -158,6 +164,9 @@ public void reset() { settingsComponent.setImagePlatformText(settings.imagePlatform != null ? settings.imagePlatform : ""); settingsComponent.setGradlePathText(settings.gradlePath != null ? settings.gradlePath : ""); settingsComponent.setCargoPathText(settings.cargoPath != null ? settings.cargoPath : ""); + settingsComponent.setBatchConcurrencyText(settings.batchConcurrency != null ? settings.batchConcurrency : "10"); + settingsComponent.setBatchContinueOnErrorCheck(settings.batchContinueOnError); + settingsComponent.setBatchMetadataCheck(settings.batchMetadata); settingsComponent.setManifestExclusionPatternsText(settings.manifestExclusionPatterns != null ? settings.manifestExclusionPatterns : ""); settingsComponent.setReportFilePathText(settings.reportFilePath != null ? settings.reportFilePath : ""); } diff --git a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsState.java b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsState.java index 4c45764b..e6f9b44b 100644 --- a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsState.java +++ b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsState.java @@ -65,6 +65,10 @@ public final class ApiSettingsState implements PersistentStateComponent ignorePatterns = ManifestExclusionManager.getExclusionPatterns(); + + indicator.setText("Generating batch analysis report..."); + + Path reportPath = apiService.getBatchStackAnalysis(basePath, ignorePatterns); + String reportLink = reportPath.toUri().toString(); + + JsonObject manifestDetails = new JsonObject(); + manifestDetails.addProperty("showParent", false); + manifestDetails.addProperty("manifestName", "batch-workspace-analysis.html"); + manifestDetails.addProperty("manifestPath", basePath); + manifestDetails.addProperty("manifestFileParent", project.getName()); + manifestDetails.addProperty("report_link", reportLink); + manifestDetails.addProperty("manifestNameWithoutExtension", "batch-workspace-analysis"); + + ApplicationManager.getApplication().invokeLater(() -> { + if (project.isDisposed()) { + return; + } + try { + AnalyticsReportUtils analyticsReportUtils = new AnalyticsReportUtils(); + analyticsReportUtils.openCustomEditor( + FileEditorManager.getInstance(project), manifestDetails, project); + } catch (AlreadyDisposedException e) { + // Project was disposed — ignore silently. + } catch (Exception e) { + logger.error(e); + Messages.showErrorDialog(project, + "Can't open report: " + e.getLocalizedMessage(), + "Error"); + } + }); + } catch (RuntimeException ex) { + logger.error(ex); + ApplicationManager.getApplication().invokeLater(() -> + Messages.showErrorDialog(project, + "Batch analysis failed: " + ex.getLocalizedMessage(), + "Error")); + } + } + }); + } + + /** + * Checks whether the project root contains a supported workspace layout + * (JS/TS with a lock file, or Cargo with Cargo.lock). + */ + static boolean isSupportedWorkspace(Path rootDir) { + // Cargo workspace + if (Files.isRegularFile(rootDir.resolve("Cargo.toml")) + && Files.isRegularFile(rootDir.resolve("Cargo.lock"))) { + return true; + } + // JS/TS workspace + if (Files.isRegularFile(rootDir.resolve("package.json"))) { + return Files.isRegularFile(rootDir.resolve("pnpm-lock.yaml")) + || Files.isRegularFile(rootDir.resolve("yarn.lock")) + || Files.isRegularFile(rootDir.resolve("package-lock.json")); + } + return false; + } + + /** + * Enables the action only when a project is open. + * + * @param event the action event + */ + @Override + public void update(AnActionEvent event) { + event.getPresentation().setEnabledAndVisible(event.getProject() != null); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index ebfae6fb..944bee46 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -597,6 +597,17 @@ + + + + + + + + + telemetryServiceMock; + private MockedStatic apiSettingsStateMock; + + private static final List BATCH_PROPERTIES = Arrays.asList( + "TRUSTIFY_DA_BATCH_CONCURRENCY", + "TRUSTIFY_DA_CONTINUE_ON_ERROR", + "TRUSTIFY_DA_BATCH_METADATA" + ); + private final Map originalProperties = new HashMap<>(); + + @Before + public void setUp() { + mockApi = mock(Api.class); + apiService = spy(new ApiService(mockApi)); + doNothing().when(apiService).setBatchRequestProperties(); + + // Mock TelemetryService + TelemetryMessageBuilder mockBuilder = mock(TelemetryMessageBuilder.class); + TelemetryMessageBuilder.ActionMessage mockAction = mock(TelemetryMessageBuilder.ActionMessage.class); + when(mockBuilder.action(anyString())).thenReturn(mockAction); + when(mockAction.property(anyString(), anyString())).thenReturn(mockAction); + telemetryServiceMock = mockStatic(TelemetryService.class); + telemetryServiceMock.when(TelemetryService::instance).thenReturn(mockBuilder); + + // Mock ApiSettingsState + ApiSettingsState mockSettings = new ApiSettingsState(); + mockSettings.rhdaToken = "test-token"; + apiSettingsStateMock = mockStatic(ApiSettingsState.class); + apiSettingsStateMock.when(ApiSettingsState::getInstance).thenReturn(mockSettings); + + for (String key : BATCH_PROPERTIES) { + originalProperties.put(key, System.getProperty(key)); + } + } + + @After + public void tearDown() { + telemetryServiceMock.close(); + apiSettingsStateMock.close(); + for (String key : BATCH_PROPERTIES) { + String original = originalProperties.get(key); + if (original != null) { + System.setProperty(key, original); + } else { + System.clearProperty(key); + } + } + } + + /** Verifies that getBatchStackAnalysis delegates to exhortApi and returns a valid HTML file. */ + @Test + public void testGetBatchStackAnalysis_returnsHtmlReport() throws Exception { + // Given + byte[] htmlContent = "Batch Report".getBytes(); + when(mockApi.stackAnalysisBatchHtml(any(Path.class), any())) + .thenReturn(CompletableFuture.completedFuture(htmlContent)); + + // When + Path result = apiService.getBatchStackAnalysis("/workspace", Collections.emptyList()); + + // Then + assertNotNull("Report path should not be null", result); + assertTrue("Report file should exist", Files.exists(result)); + String content = Files.readString(result); + assertEquals("Batch Report", content); + assertTrue("Report filename should have .html extension", result.toString().endsWith(".html")); + + Files.deleteIfExists(result); + } + + /** Verifies that exclusion patterns are correctly passed to the API as a Set. */ + @Test + public void testGetBatchStackAnalysis_passesIgnorePatterns() throws Exception { + // Given + byte[] htmlContent = "".getBytes(); + when(mockApi.stackAnalysisBatchHtml(any(Path.class), any())) + .thenReturn(CompletableFuture.completedFuture(htmlContent)); + List patterns = Arrays.asList("**/node_modules/**", "**/dist/**"); + + // When + Path result = apiService.getBatchStackAnalysis("/workspace", patterns); + + // Then + Set expectedPatterns = new HashSet<>(Arrays.asList("**/node_modules/**", "**/dist/**")); + verify(mockApi).stackAnalysisBatchHtml(eq(Path.of("/workspace")), eq(expectedPatterns)); + + Files.deleteIfExists(result); + } + + /** Verifies that an empty ignore patterns list results in an empty set passed to the API. */ + @Test + public void testGetBatchStackAnalysis_emptyIgnorePatterns() throws Exception { + // Given + byte[] htmlContent = "".getBytes(); + when(mockApi.stackAnalysisBatchHtml(any(Path.class), any())) + .thenReturn(CompletableFuture.completedFuture(htmlContent)); + + // When + Path result = apiService.getBatchStackAnalysis("/workspace", Collections.emptyList()); + + // Then + verify(mockApi).stackAnalysisBatchHtml(eq(Path.of("/workspace")), eq(Collections.emptySet())); + + Files.deleteIfExists(result); + } + + /** Verifies that IOException from the API is wrapped in RuntimeException. */ + @Test(expected = RuntimeException.class) + public void testGetBatchStackAnalysis_wrapsIOException() throws Exception { + // Given + when(mockApi.stackAnalysisBatchHtml(any(Path.class), any())) + .thenThrow(new IOException("workspace not found")); + + // When + apiService.getBatchStackAnalysis("/nonexistent", Collections.emptyList()); + } +} diff --git a/src/test/java/org/jboss/tools/intellij/stackanalysis/SaBatchActionTest.java b/src/test/java/org/jboss/tools/intellij/stackanalysis/SaBatchActionTest.java new file mode 100644 index 00000000..b699e532 --- /dev/null +++ b/src/test/java/org/jboss/tools/intellij/stackanalysis/SaBatchActionTest.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ + +package org.jboss.tools.intellij.stackanalysis; + +import org.jboss.tools.intellij.settings.ApiSettingsState; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests for batch analysis settings persistence and default values + * in ApiSettingsState. + */ +public class SaBatchActionTest { + + /** Verifies that batch settings fields have correct default values. */ + @Test + public void testBatchSettingsDefaults() { + // Given a fresh ApiSettingsState + ApiSettingsState settings = new ApiSettingsState(); + + // Then defaults should be set correctly + assertEquals("Default batch concurrency should be 10", "10", settings.batchConcurrency); + assertTrue("Default continueOnError should be true", settings.batchContinueOnError); + assertTrue("Default batchMetadata should be true", settings.batchMetadata); + } + + /** Verifies that batch settings fields can be modified and retain their values. */ + @Test + public void testBatchSettingsPersistence() { + // Given + ApiSettingsState settings = new ApiSettingsState(); + + // When modifying settings + settings.batchConcurrency = "5"; + settings.batchContinueOnError = false; + settings.batchMetadata = false; + + // Then values should be retained + assertEquals("5", settings.batchConcurrency); + assertFalse(settings.batchContinueOnError); + assertFalse(settings.batchMetadata); + } + + /** Verifies that exclusion patterns from ManifestExclusionManager are properly parseable. */ + @Test + public void testExclusionPatternConversion() { + // Given patterns in the settings format (newline-separated) + String patterns = "**/node_modules/**\n**/dist/**\n# comment\n \n**/build/**"; + + // When splitting and filtering (same logic as ManifestExclusionManager.parsePatterns) + String[] lines = patterns.split("[\\n\\r]+"); + java.util.List parsed = new java.util.ArrayList<>(); + for (String line : lines) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("#")) { + parsed.add(trimmed); + } + } + + // Then comments and blank lines should be filtered out + assertEquals(3, parsed.size()); + assertEquals("**/node_modules/**", parsed.get(0)); + assertEquals("**/dist/**", parsed.get(1)); + assertEquals("**/build/**", parsed.get(2)); + } +} From 3cdb51965ccbeed1ca57c1fea2cc3c12851cba39 Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Sun, 19 Apr 2026 16:54:07 +0300 Subject: [PATCH 2/6] fix(batch): use IDE notifications instead of modal dialogs for errors Replace Messages.showErrorDialog and Messages.showWarningDialog with NotificationGroupManager notifications using the existing "Red Hat Dependency Analytics" notification group, matching the pattern used in ReportFileManager. Implements TC-3865 Assisted-by: Claude Code --- .../intellij/stackanalysis/SaBatchAction.java | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/jboss/tools/intellij/stackanalysis/SaBatchAction.java b/src/main/java/org/jboss/tools/intellij/stackanalysis/SaBatchAction.java index d6b79dee..d10f9cd0 100644 --- a/src/main/java/org/jboss/tools/intellij/stackanalysis/SaBatchAction.java +++ b/src/main/java/org/jboss/tools/intellij/stackanalysis/SaBatchAction.java @@ -21,8 +21,9 @@ import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; +import com.intellij.notification.NotificationGroupManager; +import com.intellij.notification.NotificationType; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; import com.intellij.serviceContainer.AlreadyDisposedException; import org.jboss.tools.intellij.componentanalysis.ManifestExclusionManager; import org.jboss.tools.intellij.exhort.ApiService; @@ -39,6 +40,7 @@ */ public class SaBatchAction extends AnAction { private static final Logger logger = Logger.getInstance(SaBatchAction.class); + private static final String NOTIFICATION_GROUP_ID = "Red Hat Dependency Analytics"; @Override public @NotNull ActionUpdateThread getActionUpdateThread() { @@ -60,18 +62,25 @@ public void actionPerformed(@NotNull AnActionEvent event) { String basePath = project.getBasePath(); if (basePath == null) { - Messages.showErrorDialog(project, "Cannot determine project base path.", "Error"); + NotificationGroupManager.getInstance() + .getNotificationGroup(NOTIFICATION_GROUP_ID) + .createNotification( + "Batch Workspace Analysis", + "Cannot determine project base path.", + NotificationType.ERROR) + .notify(project); return; } Path baseDirPath = Path.of(basePath); if (!isSupportedWorkspace(baseDirPath)) { - Messages.showWarningDialog(project, - "No supported workspace detected in the project root.\n\n" - + "Batch analysis requires one of:\n" - + " • JS/TS workspace (package.json + lock file)\n" - + " • Cargo workspace (Cargo.toml + Cargo.lock)", - "Batch Analysis Not Available"); + NotificationGroupManager.getInstance() + .getNotificationGroup(NOTIFICATION_GROUP_ID) + .createNotification( + "Batch Workspace Analysis", + "No supported workspace detected. Batch analysis requires a JS/TS workspace (package.json + lock file) or a Cargo workspace (Cargo.toml + Cargo.lock).", + NotificationType.WARNING) + .notify(project); return; } @@ -110,17 +119,25 @@ public void run(@NotNull ProgressIndicator indicator) { // Project was disposed — ignore silently. } catch (Exception e) { logger.error(e); - Messages.showErrorDialog(project, - "Can't open report: " + e.getLocalizedMessage(), - "Error"); + NotificationGroupManager.getInstance() + .getNotificationGroup(NOTIFICATION_GROUP_ID) + .createNotification( + "Batch Workspace Analysis", + "Can't open report: " + e.getLocalizedMessage(), + NotificationType.ERROR) + .notify(project); } }); } catch (RuntimeException ex) { logger.error(ex); ApplicationManager.getApplication().invokeLater(() -> - Messages.showErrorDialog(project, - "Batch analysis failed: " + ex.getLocalizedMessage(), - "Error")); + NotificationGroupManager.getInstance() + .getNotificationGroup(NOTIFICATION_GROUP_ID) + .createNotification( + "Batch Workspace Analysis", + "Batch analysis failed: " + ex.getLocalizedMessage(), + NotificationType.ERROR) + .notify(project)); } } }); From 0536e5fd3fdabf0496a424d14684c3578e86ba52 Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Sun, 19 Apr 2026 17:44:41 +0300 Subject: [PATCH 3/6] docs: add batch workspace analysis documentation Add batch analysis feature and settings documentation to README.md, batch telemetry events to USAGE_DATA.md, and batch analysis quick start entry to plugin.xml description. Implements TC-3865 Assisted-by: Claude Code --- README.md | 16 ++++++++++++++++ USAGE_DATA.md | 2 ++ src/main/resources/META-INF/plugin.xml | 3 +++ 3 files changed, 21 insertions(+) diff --git a/README.md b/README.md index 66bffd1d..788a1c19 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,12 @@ according to your preferences. - **Proxy Configuration** :
From IntelliJ IDEA Appearance & Behavior > System Settings > HTTP Proxy, you can configure a static proxy for all HTTP requests made by the plugin. This is useful when your environment requires going through a proxy to access external services. For example:`http://proxy.example.com:8080` +- **Batch Analysis** : +
Configure settings for batch workspace analysis, which analyzes all packages in a JS/TS monorepo or Cargo workspace at once. +
**Concurrency**: Maximum number of concurrent analyses during batch workspace analysis (default: 10). +
**Continue on Error**: Continue analyzing remaining packages when one package analysis fails (default: enabled). +
**Include Metadata**: Include additional metadata in batch analysis results (default: enabled). + - **Manifest Exclusion Patterns** :
You can exclude manifest files from component analysis using glob patterns. This is useful for excluding third-party dependencies, test files, or other manifests that should not be analyzed.
Enter one pattern per line. Examples: `**/node_modules/**/package.json` to exclude all package.json files in node_modules directories, or `test/**/pom.xml` to exclude all Maven files in test directories. @@ -377,6 +383,16 @@ When modifying the grammar or lexer files, you need to regenerate the parser cla
Right-click on any manifest file and select **Exclude from Component Analysis** to quickly add an exclusion pattern for that specific file. +- **Batch Workspace Analysis** +
For JS/TS monorepos (npm, pnpm, yarn workspaces) and Cargo workspaces, you can run a batch analysis that scans + all packages in the workspace at once and generates a combined vulnerability report. +
To run a batch analysis, right-click in the **Project** window and click **Batch Workspace Analysis Report**. +
The workspace root must contain a supported workspace layout: + - JS/TS workspace: `package.json` with a lock file (`package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock`) + - Cargo workspace: `Cargo.toml` with `Cargo.lock` +
Exclusion patterns configured in **Manifest Exclusion Patterns** are applied during workspace discovery to skip + specific packages from the batch analysis. + - **Red Hat Dependency Analytics report**
The Red Hat Dependency Analytics report is a temporary HTML file that exist if the **Red Hat Dependency Analytics Report** tab remains open. diff --git a/USAGE_DATA.md b/USAGE_DATA.md index d9ac0d17..10c9d6ff 100644 --- a/USAGE_DATA.md +++ b/USAGE_DATA.md @@ -5,4 +5,6 @@ * when plugin is started * when plugin analyse dependency file for vulnerability(s) and file name * when plugin analyse dependency file fails, error message and file name +* when plugin runs batch workspace analysis on a workspace root +* when plugin batch workspace analysis fails, error message * when plugin is shutdown diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 944bee46..986301f6 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -61,6 +61,9 @@
  • Right click on a manifest file in the Project window, and click Dependency Analytics Report.
  • +
  • For JS/TS monorepos or Cargo workspaces, right click in the Project window and click + Batch Workspace Analysis Report to analyze all packages at once. +
  • From cec323935605c1691a84c2aa5d5c12a62bf251de Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Thu, 23 Apr 2026 10:48:59 +0300 Subject: [PATCH 4/6] refactor: extract common request properties into shared method Consolidate duplicated property setup logic from setRequestProperties, setBatchRequestProperties, and image.ApiService.setServiceEnvironment into a single setCommonRequestProperties method. Each caller now invokes the common method and only adds its own specific properties on top. Co-Authored-By: Claude Opus 4.6 --- .../tools/intellij/exhort/ApiService.java | 94 ++++++------------- .../tools/intellij/image/ApiService.java | 27 +----- 2 files changed, 33 insertions(+), 88 deletions(-) diff --git a/src/main/java/org/jboss/tools/intellij/exhort/ApiService.java b/src/main/java/org/jboss/tools/intellij/exhort/ApiService.java index 512d7289..6b7dc3b4 100644 --- a/src/main/java/org/jboss/tools/intellij/exhort/ApiService.java +++ b/src/main/java/org/jboss/tools/intellij/exhort/ApiService.java @@ -151,7 +151,15 @@ public ComponentAnalysisResult getComponentAnalysis(final String packageManager, return null; } - private void setRequestProperties(final String manifestName) { + /** + * Sets up common request properties shared across all analysis types (stack, batch, image). + * Configures the plugin descriptor, backend connection, all tool paths, and proxy settings. + * Not all analysis types use every property configured here (e.g., batch analysis does not use + * Maven/Gradle/Python-specific properties, and image analysis does not use any of the package + * manager tool paths), but setting them unconditionally is harmless as unused properties are + * simply ignored by the backend. + */ + public static void setCommonRequestProperties() { String ideName = ApplicationInfo.getInstance().getFullApplicationName(); PluginDescriptor pluginDescriptor = PluginManagerCore.getPlugin(PluginId.getId("org.jboss.tools.intellij.analytics")); if (pluginDescriptor != null) { @@ -239,13 +247,6 @@ private void setRequestProperties(final String manifestName) { } else { System.clearProperty("TRUSTIFY_DA_CARGO_PATH"); } - if ("go.mod".equals(manifestName)) { - if (settings.goMatchManifestVersions) { - System.setProperty("MATCH_MANIFEST_VERSIONS", "true"); - } else { - System.clearProperty("MATCH_MANIFEST_VERSIONS"); - } - } if (settings.usePython2) { if (settings.pythonPath != null && !settings.pythonPath.isBlank()) { System.setProperty("TRUSTIFY_DA_PYTHON_PATH", settings.pythonPath); @@ -284,16 +285,6 @@ private void setRequestProperties(final String manifestName) { System.clearProperty("TRUSTIFY_DA_PYTHON_VIRTUAL_ENV"); System.clearProperty("TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS"); } - if ("requirements.txt".equals(manifestName)) { - if (settings.pythonMatchManifestVersions) { - System.setProperty("MATCH_MANIFEST_VERSIONS", "true"); - } else { - System.setProperty("MATCH_MANIFEST_VERSIONS","false"); - } - } - if (!"go.mod".equals(manifestName) && !"requirements.txt".equals(manifestName)) { - System.clearProperty("MATCH_MANIFEST_VERSIONS"); - } if (settings.licenseCheckEnabled) { System.setProperty("TRUSTIFY_DA_LICENSE_CHECK", "true"); } else { @@ -308,21 +299,32 @@ private void setRequestProperties(final String manifestName) { } } - void setBatchRequestProperties() { - String ideName = ApplicationInfo.getInstance().getFullApplicationName(); - PluginDescriptor pluginDescriptor = PluginManagerCore.getPlugin(PluginId.getId("org.jboss.tools.intellij.analytics")); - if (pluginDescriptor != null) { - String pluginName = pluginDescriptor.getName() + " " + pluginDescriptor.getVersion(); - System.setProperty("TRUST_DA_SOURCE", ideName + " / " + pluginName); + private void setRequestProperties(final String manifestName) { + setCommonRequestProperties(); + + if ("go.mod".equals(manifestName)) { + ApiSettingsState settings = ApiSettingsState.getInstance(); + if (settings.goMatchManifestVersions) { + System.setProperty("MATCH_MANIFEST_VERSIONS", "true"); + } else { + System.clearProperty("MATCH_MANIFEST_VERSIONS"); + } + } else if ("requirements.txt".equals(manifestName)) { + ApiSettingsState settings = ApiSettingsState.getInstance(); + if (settings.pythonMatchManifestVersions) { + System.setProperty("MATCH_MANIFEST_VERSIONS", "true"); + } else { + System.setProperty("MATCH_MANIFEST_VERSIONS", "false"); + } } else { - System.setProperty("TRUST_DA_SOURCE", ideName); + System.clearProperty("MATCH_MANIFEST_VERSIONS"); } + } - ApiSettingsState settings = ApiSettingsState.getInstance(); - System.setProperty("TRUST_DA_TOKEN", settings.rhdaToken); - - setBackendUrl(); + void setBatchRequestProperties() { + setCommonRequestProperties(); + ApiSettingsState settings = ApiSettingsState.getInstance(); if (settings.batchConcurrency != null && !settings.batchConcurrency.isBlank()) { System.setProperty("TRUSTIFY_DA_BATCH_CONCURRENCY", settings.batchConcurrency); } else { @@ -330,40 +332,6 @@ void setBatchRequestProperties() { } System.setProperty("TRUSTIFY_DA_CONTINUE_ON_ERROR", String.valueOf(settings.batchContinueOnError)); System.setProperty("TRUSTIFY_DA_BATCH_METADATA", String.valueOf(settings.batchMetadata)); - - // Set tool paths needed for batch analysis - if (settings.npmPath != null && !settings.npmPath.isBlank()) { - System.setProperty("TRUSTIFY_DA_NPM_PATH", settings.npmPath); - } else { - System.clearProperty("TRUSTIFY_DA_NPM_PATH"); - } - if (settings.yarnPath != null && !settings.yarnPath.isBlank()) { - System.setProperty("TRUSTIFY_DA_YARN_PATH", settings.yarnPath); - } else { - System.clearProperty("TRUSTIFY_DA_YARN_PATH"); - } - if (settings.nodePath != null && !settings.nodePath.isBlank()) { - System.setProperty("NODE_HOME", settings.nodePath); - } else { - System.clearProperty("NODE_HOME"); - } - if (settings.pnpmPath != null && !settings.pnpmPath.isBlank()) { - System.setProperty("TRUSTIFY_DA_PNPM_PATH", settings.pnpmPath); - } else { - System.clearProperty("TRUSTIFY_DA_PNPM_PATH"); - } - if (settings.cargoPath != null && !settings.cargoPath.isBlank()) { - System.setProperty("TRUSTIFY_DA_CARGO_PATH", settings.cargoPath); - } else { - System.clearProperty("TRUSTIFY_DA_CARGO_PATH"); - } - - Optional proxyUrlOpt = getProxyUrl(); - if (proxyUrlOpt.isPresent()) { - System.setProperty("TRUSTIFY_DA_PROXY_URL", proxyUrlOpt.get()); - } else { - System.clearProperty("TRUSTIFY_DA_PROXY_URL"); - } } public static void setBackendUrl() { diff --git a/src/main/java/org/jboss/tools/intellij/image/ApiService.java b/src/main/java/org/jboss/tools/intellij/image/ApiService.java index c387fcf0..d27207a2 100644 --- a/src/main/java/org/jboss/tools/intellij/image/ApiService.java +++ b/src/main/java/org/jboss/tools/intellij/image/ApiService.java @@ -11,13 +11,9 @@ package org.jboss.tools.intellij.image; -import com.intellij.ide.plugins.PluginManagerCore; -import com.intellij.openapi.application.ApplicationInfo; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.Service; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.extensions.PluginDescriptor; -import com.intellij.openapi.extensions.PluginId; import io.github.guacsec.trustifyda.Api; import io.github.guacsec.trustifyda.api.v5.AnalysisReport; import io.github.guacsec.trustifyda.image.ImageRef; @@ -29,7 +25,6 @@ import java.nio.file.Path; import java.util.Collection; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; @@ -37,8 +32,7 @@ import java.util.stream.Collectors; import static org.jboss.tools.intellij.exhort.ApiService.createExhortApiWithBackendUrl; -import static org.jboss.tools.intellij.exhort.ApiService.getProxyUrl; -import static org.jboss.tools.intellij.exhort.ApiService.setBackendUrl; +import static org.jboss.tools.intellij.exhort.ApiService.setCommonRequestProperties; @Service(Service.Level.APP) public final class ApiService { @@ -114,19 +108,9 @@ Path getImageAnalysisReport(final Set imageRefs) { } private void setServiceEnvironment() { - var ideName = ApplicationInfo.getInstance().getFullApplicationName(); - PluginDescriptor pluginDescriptor = PluginManagerCore.getPlugin(PluginId.getId("org.jboss.tools.intellij.analytics")); - if (pluginDescriptor != null) { - var pluginName = pluginDescriptor.getName() + " " + pluginDescriptor.getVersion(); - System.setProperty("TRUST_DA_SOURCE", ideName + " / " + pluginName); - } else { - System.setProperty("TRUST_DA_SOURCE", ideName); - } + setCommonRequestProperties(); var settings = ApiSettingsState.getInstance(); - System.setProperty("TRUST_DA_TOKEN", settings.rhdaToken); - - setBackendUrl(); if (settings.syftPath != null && !settings.syftPath.isBlank()) { System.setProperty("TRUSTIFY_DA_SYFT_PATH", settings.syftPath); @@ -169,12 +153,5 @@ private void setServiceEnvironment() { } else { System.clearProperty("TRUSTIFY_DA_IMAGE_PLATFORM"); } - - Optional proxyUrlOpt = getProxyUrl(); - if (proxyUrlOpt.isPresent()) { - System.setProperty("TRUSTIFY_DA_PROXY_URL", proxyUrlOpt.get()); - } else { - System.clearProperty("TRUSTIFY_DA_PROXY_URL"); - } } } From 043a0deffcf56d257c02bcd3152ec0cf585d1acb Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Sun, 26 Apr 2026 09:39:38 +0300 Subject: [PATCH 5/6] build: upgrade trustify-da-java-client to 0.0.16 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92a17320..07a4627b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ caffeine = "3.1.8" commons-compress = "1.21" commons-io = "2.16.1" trustify-da-api-spec = "2.0.2" -trustify-da-java-client = "0.0.16-SNAPSHOT" +trustify-da-java-client = "0.0.16" github-api = "1.314" junit = "4.13.2" mockito = "5.14.2" From 9a1f4747ad8c90c1dca6024035b36c6b95cb8633 Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Mon, 27 Apr 2026 11:36:25 +0300 Subject: [PATCH 6/6] build: update CI workflows to use Java 21 The project source/target compatibility was changed to Java 21 but CI workflows still used Java 17, causing compilation failures. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/IJ-latest.yml | 4 ++-- .github/workflows/IJ.yml | 4 ++-- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/publish-marketplace.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/stage.yml | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/IJ-latest.yml b/.github/workflows/IJ-latest.yml index c4d7b4f8..90091332 100644 --- a/.github/workflows/IJ-latest.yml +++ b/.github/workflows/IJ-latest.yml @@ -15,10 +15,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'temurin' cache: 'gradle' - name: Grant execute permission for gradlew diff --git a/.github/workflows/IJ.yml b/.github/workflows/IJ.yml index 4fc75aee..92964646 100644 --- a/.github/workflows/IJ.yml +++ b/.github/workflows/IJ.yml @@ -16,10 +16,10 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'temurin' cache: 'gradle' - name: Grant execute permission for gradlew diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9fdfed6..19b4de9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'temurin' cache: 'gradle' - name: Grant execute permission for gradlew @@ -34,10 +34,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'temurin' cache: 'gradle' - name: Grant execute permission for gradlew @@ -53,10 +53,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'temurin' cache: 'gradle' - name: Grant execute permission for gradlew diff --git a/.github/workflows/publish-marketplace.yml b/.github/workflows/publish-marketplace.yml index e205f6b8..a6e9e825 100644 --- a/.github/workflows/publish-marketplace.yml +++ b/.github/workflows/publish-marketplace.yml @@ -24,10 +24,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: 'temurin' cache: 'gradle' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f7a1906..ca743e08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,10 +8,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'temurin' cache: 'gradle' - name: Grant execute permission for gradlew diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 6c5e7fab..1754842f 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -17,10 +17,10 @@ jobs: - name: Checkout sources uses: actions/checkout@v4 - - name: Install Java 17 + - name: Install Java 21 uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'oracle' cache: 'gradle'