This is an automated email from the ASF dual-hosted git repository.

bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 369c499bb7 Issue #4881 : Add environment configuration files from a 
folder and wildcard (#8694)
369c499bb7 is described below

commit 369c499bb7eb9bd537a854d48659d025d356fbf5
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 12:03:05 2026 +0200

    Issue #4881 : Add environment configuration files from a folder and 
wildcard (#8694)
---
 .../hop-tools/hop-conf/hop-conf-projects.adoc      |  25 ++
 .../ROOT/pages/hop-tools/hop-conf/hop-conf.adoc    |  20 ++
 .../ROOT/pages/projects/projects-environments.adoc |  11 +-
 .../environment/EnvironmentConfigFileSelector.java | 182 +++++++++++++++
 .../environment/LifecycleEnvironmentDialog.java    | 259 ++++++++++++++++++++-
 .../ManageEnvironmentsOptionPlugin.java            |  66 +++++-
 .../environment/messages/messages_en_US.properties |  19 ++
 .../EnvironmentConfigFileSelectorTest.java         | 185 +++++++++++++++
 .../ManageEnvironmentsOptionPluginTest.java        | 125 ++++++++++
 9 files changed, 876 insertions(+), 16 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf-projects.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf-projects.adoc
index 363dec2f51..78f082e1d6 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf-projects.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf-projects.adoc
@@ -99,6 +99,31 @@ As you can see from the log, an empty file was created to 
set variables in:
 { }
 ----
 
+==== Adding configuration files from a directory
+
+`--environment-config-files` still lists files one by one.
+To add every matching file in a folder, pass the folder and a regular 
expression.
+The expression is matched against the file name, the same way as in the JSON 
Input and Get File Names transforms.
+`*.json` is not a valid expression; use `.*\.json`.
+
+An exclusion expression and `--environment-config-include-subfolders` are 
optional.
+Files found in the folder are stored as individual paths.
+They can be combined with `--environment-config-files`.
+When either option is set, that selection replaces the environment's 
configuration file list.
+
+[source,bash]
+----
+$ sh hop-conf.sh \
+     --environment-create \
+     --environment test-file \
+     --environment-project test-file \
+     --environment-purpose=Testing \
+     --environment-config-file-directory="<YOUR_ENV_CONFIG_PATH>/config" \
+     --environment-config-file-wildcard=".*\.json" \
+     --environment-config-file-exclude-wildcard=".*secret.*" \
+     --environment-config-include-subfolders
+----
+
 ==== Setting variables in an environment
 
 This command adds a variable to the environment configuration file:
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc
index 03264ed6b2..16c4df2272 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc
@@ -93,6 +93,22 @@ Usage: <main class> [-h] [-ec] [-ed] [-el] [-em] [-ey] [-pc] 
[-pd] [-pl] [-pm]
         <environmentConfigFiles>...]
                             A list of configuration files for this lifecycle
                               environment, comma separated
+          --environment-config-file-directory=<environmentConfigFileDirectory>
+                            Folder of configuration files for this lifecycle
+                              environment. Matching files replace the 
configuration
+                              file list, together with 
--environment-config-files
+                              when that option is also set.
+          
--environment-config-file-exclude-wildcard=<environmentConfigFileExcludeWildcard>
+                            Regular expression for file names to ignore in
+                              --environment-config-file-directory. Example:
+                              .*secret.*
+          --environment-config-file-wildcard=<environmentConfigFileWildcard>
+                            Regular expression matched against file names in
+                              --environment-config-file-directory. Example:
+                              .*\.json
+          --environment-config-include-subfolders
+                            Also include matching files from subfolders of
+                              --environment-config-file-directory
       -el, --environments-list
                             List the defined lifecycle environments
       -em, --environment-modify
@@ -223,6 +239,10 @@ TIP: `-sv` can be used to unset a variable by specifying a 
variable without a va
 
|-cfd|--config-file-describe-variables=<configDescribeVariables>[,<configDescribeVariables>...]|A
 list of variable=description combinations separated by a comma
 |-cfv|--config-file-set-variables=<configSetVariables> 
,<configSetVariables>...]|A list of variable=value combinations separated by a 
comma
 |-eg|--environment-config-files=<environmentConfigFiles>[, 
<environmentConfigFiles>...]|A list of configuration files for this lifecycle 
environment, comma separated
+||--environment-config-file-directory=<environmentConfigFileDirectory>|Folder 
of configuration files. Matching files replace the configuration file list, 
together with --environment-config-files when that option is also set. The 
wildcard is a regular expression, for example `.*\.json`, not a shell glob
+||--environment-config-file-wildcard=<environmentConfigFileWildcard>|Regular 
expression matched against file names in --environment-config-file-directory
+||--environment-config-file-exclude-wildcard=<environmentConfigFileExcludeWildcard>|Regular
 expression for file names to ignore in that folder
+||--environment-config-include-subfolders|Also include matching files from 
subfolders of --environment-config-file-directory
 |-pv|--project-variables=<projectVariables>[,<projectVariables>...]| A list of 
variable=value combinations separated by a comma
 |===
 
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
index 0894a872e5..7d5b61fbcc 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
@@ -147,14 +147,23 @@ 
image:hop-gui/environment/environment-dialog-configuration-files-tab.png[Environ
 |Property|Description|Variables Supported|Mandatory|Default
 |Configuration files|One or more files that define environment-specific 
variables. Each row is a filename and a description|Yes|No|
 |Description|Short note about that file. It is stored in the JSON file, not on 
the environment|No|No|
+|File or directory|A single configuration file, or a folder to scan. *Add* 
puts the matching files into the list below|Yes|No|
+|Inclusion wildcard (RegExp)|Regular expression matched against the file name, 
for example `.*\.json`. Leave empty to include every file. Not a shell 
glob|Yes|No|
+|Exclusion wildcard (RegExp)|Regular expression for file names to skip, for 
example `.*secret.*`|Yes|No|
+|Include subfolders|Also add matching files from subfolders|No|No|
+|Add|Resolve the file or folder and append each match to the list. The 
environment still stores one path per file|No|No|
 |Import variables|Scan the project for `+${VARIABLE}+` expressions and add 
missing names to a configuration file|No|No|
-|Select...|Add an existing configuration file|No|No|
+|Select...|Add one or more existing configuration files. Several files can be 
selected at once|No|No|
 |New...|Create a new configuration file and add it to the environment|No|No|
 |Edit...|Open the selected configuration file to set its description and the 
variable names, values and descriptions|No|No|
 |===
 
 Each environment contains one or more configuration files where you manage 
environment-specific variables.
 
+*Add* expands a file or a folder into that list immediately, using the same 
regular-expression rules as the JSON Input and Get File Names transforms.
+A folder is not stored as a wildcard.
+After the files are added, each row is still one file, so *Edit...* opens that 
file.
+
 Store secrets (database passwords, API keys, tokens) in these files as 
variables, not in project metadata or pipelines.
 Keep the files *outside* the project folder so they are not checked into the 
project repository.
 Better still, store the value in a keystore or vault and put a 
xref:metadata-types/variable-resolver/index.adoc[variable resolver] expression 
in the configuration file.
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelector.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelector.java
new file mode 100644
index 0000000000..b75fef7abe
--- /dev/null
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelector.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.environment;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.fileinput.FileInputList;
+import org.apache.hop.core.fileinput.FileTypeFilter;
+import org.apache.hop.core.fileinput.InputFile;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.vfs.HopVfs;
+
+/**
+ * Resolves environment configuration files from one file or from a directory.
+ *
+ * <p>Wildcards are regular expressions matched against the file name, using 
the same rules as
+ * {@link FileInputList}. They are not shell globs: {@code .*\.json} matches 
JSON files and {@code
+ * *.json} is rejected.
+ */
+public final class EnvironmentConfigFileSelector {
+
+  private EnvironmentConfigFileSelector() {}
+
+  /**
+   * Files selected by an explicit list and, when a directory is set, by a 
directory scan.
+   *
+   * <p>Explicit paths stay in the given order. Directory matches are appended 
and a file already
+   * present is not added again. A wildcard, an exclude wildcard, or 
subfolders without a directory
+   * is an error. An empty directory argument with no extra options returns 
the explicit list, which
+   * may be empty.
+   */
+  public static List<String> combine(
+      IVariables variables,
+      String[] explicitFiles,
+      String directory,
+      String wildcard,
+      String excludeWildcard,
+      boolean includeSubFolders)
+      throws HopException {
+    boolean directorySet = 
StringUtils.isNotEmpty(StringUtils.trimToNull(directory));
+    boolean wildcardSet = 
StringUtils.isNotEmpty(StringUtils.trimToNull(wildcard));
+    boolean excludeSet = 
StringUtils.isNotEmpty(StringUtils.trimToNull(excludeWildcard));
+    if (!directorySet && (wildcardSet || excludeSet || includeSubFolders)) {
+      throw new HopException(
+          "Specify --environment-config-file-directory when using a 
configuration file wildcard or --environment-config-include-subfolders");
+    }
+
+    List<String> files = new ArrayList<>();
+    if (explicitFiles != null) {
+      for (String explicitFile : explicitFiles) {
+        if (StringUtils.isEmpty(StringUtils.trimToNull(explicitFile))) {
+          continue;
+        }
+        String trimmed = explicitFile.trim();
+        if (!containsSameFile(variables, files, trimmed)) {
+          files.add(trimmed);
+        }
+      }
+    }
+    if (!directorySet) {
+      return files;
+    }
+
+    for (String matchedFile :
+        resolve(variables, directory.trim(), wildcard, excludeWildcard, 
includeSubFolders)) {
+      if (!containsSameFile(variables, files, matchedFile)) {
+        files.add(matchedFile);
+      }
+    }
+    return files;
+  }
+
+  /**
+   * @param fileOrDirectory one configuration file, or a folder to scan
+   * @param wildcard regular expression matched against the file name, or 
empty for every file
+   * @param excludeWildcard regular expression for file names to skip, or empty
+   * @param includeSubFolders also return matches from subfolders
+   * @return existing file paths, in {@link FileInputList} order
+   * @throws HopException when the location is missing, the expression is 
invalid, or nothing
+   *     matches
+   */
+  public static List<String> resolve(
+      IVariables variables,
+      String fileOrDirectory,
+      String wildcard,
+      String excludeWildcard,
+      boolean includeSubFolders)
+      throws HopException {
+    if (StringUtils.isEmpty(StringUtils.trimToNull(fileOrDirectory))) {
+      return List.of();
+    }
+
+    IVariables space = variables == null ? new Variables() : variables;
+    String location = fileOrDirectory.trim();
+    String mask = StringUtils.trimToEmpty(wildcard);
+    String exclude = StringUtils.trimToEmpty(excludeWildcard);
+    validatePattern(space.resolve(mask), "wildcard");
+    validatePattern(space.resolve(exclude), "exclude wildcard");
+
+    InputFile inputFile = new InputFile();
+    inputFile.setFileName(location);
+    inputFile.setFileMask(mask);
+    inputFile.setExcludeFileMask(exclude);
+    inputFile.setIncludeSubFolders(includeSubFolders);
+    inputFile.setFileRequired(true);
+    inputFile.setFileTypeFilter(FileTypeFilter.ONLY_FILES);
+
+    FileInputList fileInputList = FileInputList.createFileList(space, 
List.of(inputFile));
+    if (fileInputList.nrOfFiles() == 0) {
+      throw new HopException(
+          "No configuration files matched '"
+              + location
+              + "'. The file or folder must exist. A wildcard is a regular 
expression matched against the file name, for example .*\\.json");
+    }
+    List<String> paths = new ArrayList<>();
+    for (String path : fileInputList.getFileStrings()) {
+      paths.add(path);
+    }
+    return paths;
+  }
+
+  private static void validatePattern(String pattern, String label) throws 
HopException {
+    if (StringUtils.isEmpty(pattern)) {
+      return;
+    }
+    try {
+      Pattern.compile(pattern);
+    } catch (PatternSyntaxException e) {
+      throw new HopException(
+          "Invalid "
+              + label
+              + " regular expression '"
+              + pattern
+              + "'. Use a regular expression such as .*\\.json, not a shell 
glob such as *.json",
+          e);
+    }
+  }
+
+  private static boolean containsSameFile(
+      IVariables variables, List<String> files, String candidate) {
+    for (String file : files) {
+      if (sameFile(variables, file, candidate)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static boolean sameFile(IVariables variables, String left, String 
right) {
+    if (StringUtils.equals(left, right)) {
+      return true;
+    }
+    try {
+      IVariables space = variables == null ? new Variables() : variables;
+      String leftName = 
HopVfs.getFilename(HopVfs.getFileObject(space.resolve(left), space));
+      String rightName = 
HopVfs.getFilename(HopVfs.getFileObject(space.resolve(right), space));
+      return leftName.equals(rightName);
+    } catch (Exception e) {
+      return false;
+    }
+  }
+}
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
index a120bac114..ed6c69c8a2 100644
--- 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
@@ -38,6 +38,7 @@ import org.apache.hop.projects.config.ProjectsConfig;
 import org.apache.hop.projects.config.ProjectsConfigSingleton;
 import org.apache.hop.projects.project.ProjectConfig;
 import org.apache.hop.projects.util.Defaults;
+import org.apache.hop.projects.util.PathVariableReplacer;
 import org.apache.hop.ui.core.ConstUi;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.dialog.AttributesDialogExtension;
@@ -68,6 +69,7 @@ import org.eclipse.swt.widgets.Combo;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Dialog;
 import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Group;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.TableItem;
@@ -96,6 +98,11 @@ public class LifecycleEnvironmentDialog extends Dialog {
   private Text wCanvasText;
   private TableView wConfigFiles;
 
+  private TextVar wAddFilename;
+  private TextVar wAddWildcard;
+  private TextVar wAddExcludeWildcard;
+  private Button wAddIncludeSubfolders;
+
   private IVariables variables;
   private Button wbEdit;
   private Button wbImportVariables;
@@ -331,6 +338,8 @@ public class LifecycleEnvironmentDialog extends Dialog {
     comp.setLayout(layout);
     tab.setControl(comp);
 
+    Group wAddGroup = createAddFilesGroup(comp, margin);
+
     Label wlConfigFiles = new Label(comp, SWT.LEFT);
     PropsUi.setLook(wlConfigFiles);
     wlConfigFiles.setText(
@@ -338,7 +347,7 @@ public class LifecycleEnvironmentDialog extends Dialog {
     FormData fdlConfigFiles = new FormData();
     fdlConfigFiles.left = new FormAttachment(0, 0);
     fdlConfigFiles.right = new FormAttachment(100, 0);
-    fdlConfigFiles.top = new FormAttachment(0, 0);
+    fdlConfigFiles.top = new FormAttachment(wAddGroup, margin);
     wlConfigFiles.setLayoutData(fdlConfigFiles);
 
     wbImportVariables = new Button(comp, SWT.PUSH);
@@ -354,6 +363,8 @@ public class LifecycleEnvironmentDialog extends Dialog {
     Button wbSelect = new Button(comp, SWT.PUSH);
     PropsUi.setLook(wbSelect);
     wbSelect.setText(BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.Button.Select"));
+    wbSelect.setToolTipText(
+        BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.Button.Select.Tooltip"));
     FormData fdAdd = new FormData();
     fdAdd.left = new FormAttachment(wbImportVariables, 0, SWT.LEFT);
     fdAdd.right = new FormAttachment(100, 0);
@@ -467,25 +478,257 @@ public class LifecycleEnvironmentDialog extends Dialog {
     }
   }
 
+  private Group createAddFilesGroup(Composite parent, int margin) {
+    int middle = props.getMiddlePct();
+
+    Group group = new Group(parent, SWT.SHADOW_NONE);
+    PropsUi.setLook(group);
+    group.setText(BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.Group"));
+    FormLayout groupLayout = new FormLayout();
+    groupLayout.marginWidth = PropsUi.getFormMargin();
+    groupLayout.marginHeight = PropsUi.getFormMargin();
+    group.setLayout(groupLayout);
+    FormData fdGroup = new FormData();
+    fdGroup.left = new FormAttachment(0, 0);
+    fdGroup.right = new FormAttachment(100, 0);
+    fdGroup.top = new FormAttachment(0, 0);
+    group.setLayoutData(fdGroup);
+
+    Button wBrowse = new Button(group, SWT.PUSH);
+    PropsUi.setLook(wBrowse);
+    wBrowse.setText(BaseMessages.getString(PKG, "System.Button.Browse"));
+    wBrowse.setToolTipText(
+        BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.Browse.Tooltip"));
+    FormData fdBrowse = new FormData();
+    fdBrowse.right = new FormAttachment(100, 0);
+    fdBrowse.top = new FormAttachment(0, margin);
+    wBrowse.setLayoutData(fdBrowse);
+    wBrowse.addListener(SWT.Selection, this::browseConfigFileOrDirectory);
+
+    Button wAdd = new Button(group, SWT.PUSH);
+    PropsUi.setLook(wAdd);
+    wAdd.setText(BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.Button.Add"));
+    wAdd.setToolTipText(
+        BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.Button.Add.Tooltip"));
+    FormData fdAdd = new FormData();
+    fdAdd.right = new FormAttachment(wBrowse, -margin);
+    fdAdd.top = new FormAttachment(wBrowse, 0, SWT.CENTER);
+    wAdd.setLayoutData(fdAdd);
+    wAdd.addListener(SWT.Selection, this::addMatchingConfigFiles);
+
+    Label wlFilename = new Label(group, SWT.RIGHT);
+    PropsUi.setLook(wlFilename);
+    wlFilename.setText(
+        BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.Filename.Label"));
+    FormData fdlFilename = new FormData();
+    fdlFilename.left = new FormAttachment(0, 0);
+    fdlFilename.right = new FormAttachment(middle, -margin);
+    fdlFilename.top = new FormAttachment(wBrowse, 0, SWT.CENTER);
+    wlFilename.setLayoutData(fdlFilename);
+
+    wAddFilename =
+        new TextVar(variables, group, SWT.SINGLE | SWT.LEFT | SWT.BORDER)
+            .enableNamingSchemes(NamingSchemeTypes.FILE);
+    PropsUi.setLook(wAddFilename);
+    wAddFilename.setToolTipText(
+        BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.Filename.Tooltip"));
+    FormData fdFilename = new FormData();
+    fdFilename.left = new FormAttachment(middle, 0);
+    fdFilename.right = new FormAttachment(wAdd, -margin);
+    fdFilename.top = new FormAttachment(wlFilename, 0, SWT.CENTER);
+    wAddFilename.setLayoutData(fdFilename);
+
+    Label wlWildcard = new Label(group, SWT.RIGHT);
+    PropsUi.setLook(wlWildcard);
+    wlWildcard.setText(
+        BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.Wildcard.Label"));
+    FormData fdlWildcard = new FormData();
+    fdlWildcard.left = new FormAttachment(0, 0);
+    fdlWildcard.right = new FormAttachment(middle, -margin);
+    fdlWildcard.top = new FormAttachment(wAddFilename, margin);
+    wlWildcard.setLayoutData(fdlWildcard);
+
+    wAddWildcard = new TextVar(variables, group, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wAddWildcard);
+    wAddWildcard.setToolTipText(
+        BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.Wildcard.Tooltip"));
+    FormData fdWildcard = new FormData();
+    fdWildcard.left = new FormAttachment(middle, 0);
+    fdWildcard.right = new FormAttachment(wAddFilename, 0, SWT.RIGHT);
+    fdWildcard.top = new FormAttachment(wlWildcard, 0, SWT.CENTER);
+    wAddWildcard.setLayoutData(fdWildcard);
+
+    Label wlExcludeWildcard = new Label(group, SWT.RIGHT);
+    PropsUi.setLook(wlExcludeWildcard);
+    wlExcludeWildcard.setText(
+        BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.ExcludeWildcard.Label"));
+    FormData fdlExcludeWildcard = new FormData();
+    fdlExcludeWildcard.left = new FormAttachment(0, 0);
+    fdlExcludeWildcard.right = new FormAttachment(middle, -margin);
+    fdlExcludeWildcard.top = new FormAttachment(wAddWildcard, margin);
+    wlExcludeWildcard.setLayoutData(fdlExcludeWildcard);
+
+    wAddExcludeWildcard = new TextVar(variables, group, SWT.SINGLE | SWT.LEFT 
| SWT.BORDER);
+    PropsUi.setLook(wAddExcludeWildcard);
+    wAddExcludeWildcard.setToolTipText(
+        BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.ExcludeWildcard.Tooltip"));
+    FormData fdExcludeWildcard = new FormData();
+    fdExcludeWildcard.left = new FormAttachment(middle, 0);
+    fdExcludeWildcard.right = new FormAttachment(wAddFilename, 0, SWT.RIGHT);
+    fdExcludeWildcard.top = new FormAttachment(wlExcludeWildcard, 0, 
SWT.CENTER);
+    wAddExcludeWildcard.setLayoutData(fdExcludeWildcard);
+
+    wAddIncludeSubfolders = new Button(group, SWT.CHECK);
+    PropsUi.setLook(wAddIncludeSubfolders);
+    wAddIncludeSubfolders.setText(
+        BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.IncludeSubfolders.Label"));
+    wAddIncludeSubfolders.setToolTipText(
+        BaseMessages.getString(
+            PKG, 
"LifecycleEnvironmentDialog.AddFiles.IncludeSubfolders.Tooltip"));
+    FormData fdIncludeSubfolders = new FormData();
+    fdIncludeSubfolders.left = new FormAttachment(middle, 0);
+    fdIncludeSubfolders.top = new FormAttachment(wAddExcludeWildcard, margin);
+    wAddIncludeSubfolders.setLayoutData(fdIncludeSubfolders);
+
+    return group;
+  }
+
+  private void browseConfigFileOrDirectory(Event event) {
+    boolean directory =
+        StringUtils.isNotEmpty(wAddWildcard.getText())
+            || StringUtils.isNotEmpty(wAddExcludeWildcard.getText())
+            || wAddIncludeSubfolders.getSelection();
+    if (directory) {
+      BaseDialog.presentDirectoryDialog(shell, wAddFilename, variables);
+      return;
+    }
+    BaseDialog.presentFileDialog(
+        shell,
+        wAddFilename,
+        variables,
+        new String[] {"*.json", "*"},
+        new String[] {
+          BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.ImportVariables.FileFilter.Json"),
+          BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.ImportVariables.FileFilter.All")
+        },
+        true);
+  }
+
+  private void addMatchingConfigFiles(Event event) {
+    try {
+      String location = wAddFilename.getText();
+      if (StringUtils.isEmpty(location)) {
+        showInformation(
+            "LifecycleEnvironmentDialog.AddFiles.MissingLocation.Title",
+            "LifecycleEnvironmentDialog.AddFiles.MissingLocation.Message");
+        return;
+      }
+      List<String> matches =
+          EnvironmentConfigFileSelector.resolve(
+              variables,
+              location,
+              wAddWildcard.getText(),
+              wAddExcludeWildcard.getText(),
+              wAddIncludeSubfolders.getSelection());
+      List<String> stored = new ArrayList<>();
+      for (String match : matches) {
+        stored.add(PathVariableReplacer.replacePathWithVariable(variables, 
match));
+      }
+      if (appendConfigFiles(stored) == 0) {
+        showInformation(
+            "LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Title",
+            "LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Message");
+        return;
+      }
+      wAddFilename.setText("");
+      wAddWildcard.setText("");
+      wAddExcludeWildcard.setText("");
+    } catch (Exception e) {
+      new ErrorDialog(
+          shell,
+          BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.Error.Title"),
+          BaseMessages.getString(PKG, 
"LifecycleEnvironmentDialog.AddFiles.Error.Message"),
+          e);
+    }
+  }
+
   private void addConfigFile(Event event) {
-    String configFile =
-        BaseDialog.presentFileDialog(
+    String[] configFiles =
+        BaseDialog.presentMultiFileDialog(
             shell,
-            null,
             variables,
             new String[] {"*.json", "*"},
-            new String[] {"Config JSON files", "All files"},
+            new String[] {
+              BaseMessages.getString(
+                  PKG, 
"LifecycleEnvironmentDialog.ImportVariables.FileFilter.Json"),
+              BaseMessages.getString(
+                  PKG, 
"LifecycleEnvironmentDialog.ImportVariables.FileFilter.All")
+            },
             true);
-    if (configFile != null) {
+    if (configFiles == null || configFiles.length == 0) {
+      return;
+    }
+    if (appendConfigFiles(List.of(configFiles)) == 0) {
+      showInformation(
+          "LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Title",
+          "LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Message");
+    }
+  }
+
+  /** Adds paths that are not already listed and returns how many were added. 
*/
+  private int appendConfigFiles(List<String> filenames) {
+    int added = 0;
+    TableItem last = null;
+    for (String filename : filenames) {
+      if (StringUtils.isEmpty(filename) || isConfigFileListed(filename)) {
+        continue;
+      }
       TableItem item = new TableItem(wConfigFiles.table, SWT.NONE);
-      item.setText(COL_FILENAME, configFile);
+      item.setText(COL_FILENAME, filename);
       loadConfigFileIntoRow(item);
+      last = item;
+      added++;
+    }
+    if (added > 0) {
       wConfigFiles.removeEmptyRows();
       wConfigFiles.setRowNums();
       wConfigFiles.optWidth(true);
-      wConfigFiles.table.setSelection(item);
+      if (last != null) {
+        wConfigFiles.table.setSelection(last);
+      }
+      setButtonStates(null);
       needingEnvironmentRefresh = true;
     }
+    return added;
+  }
+
+  private boolean isConfigFileListed(String filename) {
+    String resolved = resolvedConfigFilename(filename);
+    for (TableItem item : wConfigFiles.getNonEmptyItems()) {
+      String existing = item.getText(COL_FILENAME);
+      if (filename.equals(existing) || 
resolved.equals(resolvedConfigFilename(existing))) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private String resolvedConfigFilename(String filename) {
+    if (StringUtils.isEmpty(filename) || variables == null) {
+      return Const.NVL(filename, "");
+    }
+    try {
+      return 
HopVfs.getFilename(HopVfs.getFileObject(variables.resolve(filename), 
variables));
+    } catch (Exception e) {
+      return variables.resolve(filename);
+    }
+  }
+
+  private void showInformation(String titleKey, String messageKey) {
+    MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
+    box.setText(BaseMessages.getString(PKG, titleKey));
+    box.setMessage(BaseMessages.getString(PKG, messageKey));
+    box.open();
   }
 
   private void newConfigFile(Event event) {
diff --git 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPlugin.java
 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPlugin.java
index 94a652e92d..c32fa4d6d4 100644
--- 
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPlugin.java
+++ 
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPlugin.java
@@ -17,8 +17,9 @@
 
 package org.apache.hop.projects.environment;
 
-import java.util.Arrays;
 import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hop.core.config.DescribedVariablesConfigFile;
 import org.apache.hop.core.config.HopConfig;
@@ -72,6 +73,39 @@ public class ManageEnvironmentsOptionPlugin implements 
IConfigOptions {
       split = ",")
   private String[] environmentConfigFiles;
 
+  @Getter
+  @Setter
+  @CommandLine.Option(
+      names = {"--environment-config-file-directory"},
+      description =
+          "Folder of configuration files for this lifecycle environment. "
+              + "Matching files replace the configuration file list, together 
with --environment-config-files when that option is also set.")
+  private String environmentConfigFileDirectory;
+
+  @Getter
+  @Setter
+  @CommandLine.Option(
+      names = {"--environment-config-file-wildcard"},
+      description =
+          "Regular expression matched against file names in 
--environment-config-file-directory. Example: .*\\.json")
+  private String environmentConfigFileWildcard;
+
+  @Getter
+  @Setter
+  @CommandLine.Option(
+      names = {"--environment-config-file-exclude-wildcard"},
+      description =
+          "Regular expression for file names to ignore in 
--environment-config-file-directory. Example: .*secret.*")
+  private String environmentConfigFileExcludeWildcard;
+
+  @Getter
+  @Setter
+  @CommandLine.Option(
+      names = {"--environment-config-include-subfolders"},
+      description =
+          "Also include matching files from subfolders of 
--environment-config-file-directory")
+  private boolean environmentConfigIncludeSubfolders;
+
   @CommandLine.Option(
       names = {"-em", "--environment-modify"},
       description = "Modify a lifecycle environment")
@@ -164,7 +198,7 @@ public class ManageEnvironmentsOptionPlugin implements 
IConfigOptions {
           CONST_ENVIRONMENT + environmentName + "' doesn't exist, it can't be 
modified");
     }
 
-    if (updateEnvironmentDetails(environment)) {
+    if (updateEnvironmentDetails(environment, variables)) {
       config.addEnvironment(environment);
       log.logBasic(
           CONST_LIFECYCLE_ENVIRONMENT
@@ -197,7 +231,7 @@ public class ManageEnvironmentsOptionPlugin implements 
IConfigOptions {
     environment = new LifecycleEnvironment();
     environment.setName(environmentName);
 
-    updateEnvironmentDetails(environment);
+    updateEnvironmentDetails(environment, variables);
 
     config.addEnvironment(environment);
     ProjectsConfigSingleton.saveConfig();
@@ -243,7 +277,7 @@ public class ManageEnvironmentsOptionPlugin implements 
IConfigOptions {
 
   private void validateConfigFiles(
       ILogChannel log, IVariables variables, LifecycleEnvironment environment) 
throws Exception {
-    if (environment == null || environmentConfigFiles == null) {
+    if (environment == null || !configurationFilesSpecified()) {
       return;
     }
     for (String environmentConfigFilename : 
environment.getConfigurationFiles()) {
@@ -260,7 +294,8 @@ public class ManageEnvironmentsOptionPlugin implements 
IConfigOptions {
     }
   }
 
-  private boolean updateEnvironmentDetails(LifecycleEnvironment environment) {
+  private boolean updateEnvironmentDetails(LifecycleEnvironment environment, 
IVariables variables)
+      throws HopException {
     boolean changed = false;
     if (StringUtils.isNotEmpty(environmentPurpose)) {
       environment.setPurpose(environmentPurpose);
@@ -270,14 +305,31 @@ public class ManageEnvironmentsOptionPlugin implements 
IConfigOptions {
       environment.setProjectName(environmentProject);
       changed = true;
     }
-    if (environmentConfigFiles != null && environmentConfigFiles.length > 0) {
+    if (configurationFilesSpecified()) {
+      List<String> configurationFiles =
+          EnvironmentConfigFileSelector.combine(
+              variables,
+              environmentConfigFiles,
+              environmentConfigFileDirectory,
+              environmentConfigFileWildcard,
+              environmentConfigFileExcludeWildcard,
+              environmentConfigIncludeSubfolders);
       environment.getConfigurationFiles().clear();
-      
environment.getConfigurationFiles().addAll(Arrays.asList(environmentConfigFiles));
+      environment.getConfigurationFiles().addAll(configurationFiles);
       changed = true;
     }
     return changed;
   }
 
+  /** True when create/modify was asked to replace the configuration file 
list. */
+  private boolean configurationFilesSpecified() {
+    return (environmentConfigFiles != null && environmentConfigFiles.length > 
0)
+        || StringUtils.isNotEmpty(environmentConfigFileDirectory)
+        || StringUtils.isNotEmpty(environmentConfigFileWildcard)
+        || StringUtils.isNotEmpty(environmentConfigFileExcludeWildcard)
+        || environmentConfigIncludeSubfolders;
+  }
+
   private void validateEnvironmentNameSpecified() throws Exception {
     if (StringUtil.isEmpty(environmentName)) {
       throw new HopException("Please specify the name of the environment to 
create");
diff --git 
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties
 
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties
index 250577c415..8c307e74e0 100644
--- 
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties
+++ 
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties
@@ -19,6 +19,25 @@ LifecycleEnvironmentDialog.Button.Edit=Edit...
 LifecycleEnvironmentDialog.Button.ImportVariables=Import variables
 LifecycleEnvironmentDialog.Button.New=New...
 LifecycleEnvironmentDialog.Button.Select=Select...
+LifecycleEnvironmentDialog.Button.Select.Tooltip=Select one or more existing 
configuration files
+LifecycleEnvironmentDialog.AddFiles.Group=Add files
+LifecycleEnvironmentDialog.AddFiles.Filename.Label=File or directory
+LifecycleEnvironmentDialog.AddFiles.Filename.Tooltip=A configuration file, or 
a folder to scan. Variables are supported, for example '${PROJECT_HOME}'.
+LifecycleEnvironmentDialog.AddFiles.Wildcard.Label=Inclusion wildcard (RegExp)
+LifecycleEnvironmentDialog.AddFiles.Wildcard.Tooltip=Regular expression 
matched against each file name, for example '.*\\.json'. Leave empty to include 
every file. This is not a shell glob.
+LifecycleEnvironmentDialog.AddFiles.ExcludeWildcard.Label=Exclusion wildcard 
(RegExp)
+LifecycleEnvironmentDialog.AddFiles.ExcludeWildcard.Tooltip=Regular expression 
for file names to skip, for example '.*secret.*'.
+LifecycleEnvironmentDialog.AddFiles.IncludeSubfolders.Label=Include subfolders
+LifecycleEnvironmentDialog.AddFiles.IncludeSubfolders.Tooltip=Also add 
matching files from subfolders
+LifecycleEnvironmentDialog.AddFiles.Button.Add=Add
+LifecycleEnvironmentDialog.AddFiles.Button.Add.Tooltip=Add the file, or every 
matching file in the folder, to the list below
+LifecycleEnvironmentDialog.AddFiles.Browse.Tooltip=Browse for a configuration 
file, or for a folder when a wildcard or subfolders is set
+LifecycleEnvironmentDialog.AddFiles.MissingLocation.Title=File or directory 
required
+LifecycleEnvironmentDialog.AddFiles.MissingLocation.Message=Specify a 
configuration file or a directory before adding files.
+LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Title=Already listed
+LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Message=Every matching 
configuration file is already in the list.
+LifecycleEnvironmentDialog.AddFiles.Error.Title=Error
+LifecycleEnvironmentDialog.AddFiles.Error.Message=Error adding configuration 
files
 LifecycleEnvironmentDialog.DetailTable.Label.Filename=Filename
 LifecycleEnvironmentDialog.DetailTable.Label.Description=Description
 LifecycleEnvironmentDialog.DetailTable.ToolTip.Description=Short note stored 
in the configuration file, next to its variables
diff --git 
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelectorTest.java
 
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelectorTest.java
new file mode 100644
index 0000000000..7b1d98fc67
--- /dev/null
+++ 
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelectorTest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.environment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class EnvironmentConfigFileSelectorTest {
+
+  @TempDir Path tempDir;
+
+  @Test
+  void directoryWildcardKeepsJsonFilesOnly() throws Exception {
+    writeTree();
+
+    List<String> names =
+        baseNames(
+            EnvironmentConfigFileSelector.resolve(
+                new Variables(), tempDir.toString(), ".*\\.json", "", false));
+
+    assertEquals(List.of("a.json", "notes.json", "secret.json"), names);
+  }
+
+  @Test
+  void excludeWildcardAndSubfolders() throws Exception {
+    writeTree();
+
+    List<String> withoutSubfolders =
+        baseNames(
+            EnvironmentConfigFileSelector.resolve(
+                new Variables(), tempDir.toString(), ".*\\.json", "secret.*", 
false));
+    assertEquals(List.of("a.json", "notes.json"), withoutSubfolders);
+
+    List<String> withSubfolders =
+        baseNames(
+            EnvironmentConfigFileSelector.resolve(
+                new Variables(), tempDir.toString(), ".*\\.json", "secret.*", 
true));
+    assertEquals(List.of("a.json", "nested.json", "notes.json"), 
withSubfolders);
+  }
+
+  @Test
+  void singleFileAndVariableDirectory() throws Exception {
+    writeTree();
+    Path single = tempDir.resolve("a.json");
+    List<String> oneFile =
+        EnvironmentConfigFileSelector.resolve(new Variables(), 
single.toString(), "", "", false);
+    assertEquals(1, oneFile.size());
+    assertTrue(oneFile.get(0).endsWith("a.json"));
+
+    IVariables variables = new Variables();
+    variables.setVariable("CONFIG_DIR", tempDir.toString());
+    List<String> fromVariable =
+        baseNames(
+            EnvironmentConfigFileSelector.resolve(
+                variables, "${CONFIG_DIR}", ".*\\.json", "", false));
+    assertEquals(List.of("a.json", "notes.json", "secret.json"), fromVariable);
+  }
+
+  @Test
+  void missingFolderAndShellGlobFail() throws Exception {
+    writeTree();
+
+    HopException missing =
+        assertThrows(
+            HopException.class,
+            () ->
+                EnvironmentConfigFileSelector.resolve(
+                    new Variables(), tempDir.resolve("missing").toString(), 
"", "", false));
+    assertTrue(missing.getMessage().contains("missing"));
+
+    HopException glob =
+        assertThrows(
+            HopException.class,
+            () ->
+                EnvironmentConfigFileSelector.resolve(
+                    new Variables(), tempDir.toString(), "*.json", "", false));
+    assertTrue(glob.getMessage().contains("regular expression"));
+  }
+
+  @Test
+  void emptyLocationReturnsNothing() throws Exception {
+    assertTrue(
+        EnvironmentConfigFileSelector.resolve(new Variables(), "  ", 
".*\\.json", "", true)
+            .isEmpty());
+  }
+
+  @Test
+  void combineKeepsExplicitFilesAndSkipsDuplicates() throws Exception {
+    writeTree();
+    Path extra = tempDir.resolve("sibling").resolve("extra.json");
+    Files.createDirectories(extra.getParent());
+    Files.writeString(extra, "{}");
+
+    List<String> directoryFiles =
+        EnvironmentConfigFileSelector.resolve(
+            new Variables(), tempDir.toString(), ".*\\.json", "secret.*", 
false);
+    assertEquals(2, directoryFiles.size());
+
+    List<String> combined =
+        EnvironmentConfigFileSelector.combine(
+            new Variables(),
+            new String[] {"  ", extra.toString(), directoryFiles.get(0), 
extra.toString()},
+            tempDir.toString(),
+            ".*\\.json",
+            "secret.*",
+            false);
+
+    assertEquals(3, combined.size());
+    assertEquals(extra.toString(), combined.get(0));
+    assertEquals(directoryFiles.get(0), combined.get(1));
+    assertEquals(directoryFiles.get(1), combined.get(2));
+  }
+
+  @Test
+  void wildcardWithoutDirectoryIsRejected() {
+    HopException exception =
+        assertThrows(
+            HopException.class,
+            () ->
+                EnvironmentConfigFileSelector.combine(
+                    new Variables(), new String[] {"/tmp/a.json"}, null, 
"*.json", null, false));
+    
assertTrue(exception.getMessage().contains("--environment-config-file-directory"));
+
+    assertThrows(
+        HopException.class,
+        () -> EnvironmentConfigFileSelector.combine(new Variables(), null, " 
", null, null, true));
+  }
+
+  @Test
+  void explicitFilesAloneAreNotScanned() throws Exception {
+    List<String> files =
+        EnvironmentConfigFileSelector.combine(
+            new Variables(),
+            new String[] {"config/a.json", " ", "config/b.json"},
+            null,
+            null,
+            null,
+            false);
+    assertEquals(List.of("config/a.json", "config/b.json"), files);
+  }
+
+  private void writeTree() throws Exception {
+    Files.writeString(tempDir.resolve("a.json"), "{}");
+    Files.writeString(tempDir.resolve("notes.json"), "{}");
+    Files.writeString(tempDir.resolve("secret.json"), "{}");
+    Files.writeString(tempDir.resolve("readme.txt"), "text");
+    Files.createDirectories(tempDir.resolve("nested"));
+    Files.writeString(tempDir.resolve("nested").resolve("nested.json"), "{}");
+  }
+
+  private static List<String> baseNames(List<String> paths) {
+    List<String> names = new ArrayList<>();
+    for (String path : paths) {
+      int slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
+      names.add(slash < 0 ? path : path.substring(slash + 1));
+    }
+    return names;
+  }
+}
diff --git 
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPluginTest.java
 
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPluginTest.java
new file mode 100644
index 0000000000..2995ac7ac3
--- /dev/null
+++ 
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPluginTest.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.environment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import org.apache.hop.core.config.HopConfig;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.projects.config.ProjectsConfigSingleton;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import picocli.CommandLine;
+
+class ManageEnvironmentsOptionPluginTest {
+
+  @TempDir Path tempDir;
+
+  @BeforeAll
+  static void beforeAll() {
+    HopLogStore.init();
+  }
+
+  @Test
+  void directoryOptionsAreParsed() {
+    ManageEnvironmentsOptionPlugin plugin = new 
ManageEnvironmentsOptionPlugin();
+    new CommandLine(plugin)
+        .parseArgs(
+            "--environment-config-file-directory=/data/config",
+            "--environment-config-file-wildcard=.*\\.json",
+            "--environment-config-file-exclude-wildcard=.*secret.*",
+            "--environment-config-include-subfolders");
+
+    assertEquals("/data/config", plugin.getEnvironmentConfigFileDirectory());
+    assertEquals(".*\\.json", plugin.getEnvironmentConfigFileWildcard());
+    assertEquals(".*secret.*", 
plugin.getEnvironmentConfigFileExcludeWildcard());
+    assertTrue(plugin.isEnvironmentConfigIncludeSubfolders());
+  }
+
+  @Test
+  void createEnvironmentStoresFilesFromADirectory() throws Exception {
+    Files.writeString(tempDir.resolve("a.json"), "{}");
+    Files.writeString(tempDir.resolve("notes.json"), "{}");
+    Files.writeString(tempDir.resolve("readme.txt"), "text");
+    Files.createDirectories(tempDir.resolve("nested"));
+    Files.writeString(tempDir.resolve("nested").resolve("nested.json"), "{}");
+
+    String name = "issue-4881-" + System.nanoTime();
+    HopConfig.setInMemoryMode(true);
+    try {
+      ManageEnvironmentsOptionPlugin plugin = new 
ManageEnvironmentsOptionPlugin();
+      new CommandLine(plugin)
+          .parseArgs(
+              "--environment-create",
+              "--environment=" + name,
+              "--environment-project=missing-project",
+              "--environment-purpose=Testing",
+              "--environment-config-files=" + tempDir.resolve("notes.json"),
+              "--environment-config-file-directory=" + tempDir,
+              "--environment-config-file-wildcard=.*\\.json");
+
+      assertTrue(plugin.handleOption(LogChannel.GENERAL, null, new 
Variables()));
+
+      LifecycleEnvironment environment = 
ProjectsConfigSingleton.getConfig().findEnvironment(name);
+      List<String> files = environment.getConfigurationFiles();
+      assertEquals(2, files.size());
+      assertTrue(files.get(0).endsWith("notes.json"));
+      assertTrue(files.get(1).endsWith("a.json"));
+    } finally {
+      ProjectsConfigSingleton.getConfig().removeEnvironment(name);
+      HopConfig.setInMemoryMode(false);
+    }
+  }
+
+  @Test
+  void shellGlobDoesNotCreateTheEnvironment() throws Exception {
+    Files.writeString(tempDir.resolve("a.json"), "{}");
+    String name = "issue-4881-glob-" + System.nanoTime();
+    HopConfig.setInMemoryMode(true);
+    try {
+      ManageEnvironmentsOptionPlugin plugin = new 
ManageEnvironmentsOptionPlugin();
+      new CommandLine(plugin)
+          .parseArgs(
+              "--environment-create",
+              "--environment=" + name,
+              "--environment-project=missing-project",
+              "--environment-config-file-directory=" + tempDir,
+              "--environment-config-file-wildcard=*.json");
+
+      HopException exception =
+          assertThrows(
+              HopException.class,
+              () -> plugin.handleOption(LogChannel.GENERAL, null, new 
Variables()));
+      assertTrue(exception.getCause().getMessage().contains("regular 
expression"));
+      assertNull(ProjectsConfigSingleton.getConfig().findEnvironment(name));
+    } finally {
+      ProjectsConfigSingleton.getConfig().removeEnvironment(name);
+      HopConfig.setInMemoryMode(false);
+    }
+  }
+}

Reply via email to