diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 2b280a851..1f42662bc 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -35,6 +35,8 @@ components: - novalisdenahi providers/statsig: - liran2000 + providers/multiprovider: + - liran2000 ignored-authors: - renovate-bot diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f818fd120..69acedd13 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -9,5 +9,6 @@ "providers/flipt": "0.1.1", "providers/configcat": "0.1.0", "providers/statsig": "0.1.0", + "providers/multiprovider": "0.0.1", "tools/junit-openfeature": "0.1.1" } diff --git a/pom.xml b/pom.xml index f481c1964..10e28c18e 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ providers/flipt providers/configcat providers/statsig + providers/multiprovider diff --git a/providers/multiprovider/README.md b/providers/multiprovider/README.md new file mode 100644 index 000000000..d7af9fc26 --- /dev/null +++ b/providers/multiprovider/README.md @@ -0,0 +1,86 @@ +# OpenFeature Multi-Provider for Java + +The OpenFeature Multi-Provider wraps multiple underlying providers in a unified interface, allowing the SDK client to transparently interact with all those providers at once. +This allows use cases where a single client and evaluation interface is desired, but where the flag data should come from more than one source. + +Some examples: + +- A migration from one feature flagging provider to another. + During that process, you may have some flags that have been ported to the new system and others that haven’t. + Therefore, you’d want the Multi-Provider to return the result of the “new” system if available otherwise, return the "old" system’s result. +- Long-term use of multiple sources for flags. + For example, someone might want to be able to combine environment variables, database entries, and vendor feature flag results together in a single interface, and define the precedence order in which those sources should be consulted. +- Setting a fallback for cloud providers. + You can use the Multi-Provider to automatically fall back to a local configuration if an external vendor provider goes down, rather than using the default values. + By using the FirstSuccessfulStrategy, the Multi-Provider will move on to the next provider in the list if an error is thrown. + +## Strategies + +The Multi-Provider supports multiple ways of deciding how to evaluate the set of providers it is managing, and how to deal with any errors that are thrown. + +Strategies must be adaptable to the various requirements that might be faced in a multi-provider situation. +In some cases, the strategy may want to ignore errors from individual providers as long as one of them successfully responds. +In other cases, it may want to evaluate providers in order and skip the rest if a successful result is obtained. +In still other scenarios, it may be required to always call every provider and decide what to do with the set of results. + +The strategy to use is passed in to the Multi-Provider. + +By default, the Multi-Provider uses the “FirstMatchStrategy”. + +Here are some standard strategies that come with the Multi-Provider: + +### First Match + +Return the first result returned by a provider. +Skip providers that indicate they had no value due to `FLAG_NOT_FOUND`. +In all other cases, use the value returned by the provider. +If any provider returns an error result other than `FLAG_NOT_FOUND`, the whole evaluation should error and “bubble up” the individual provider’s error in the result. + +As soon as a value is returned by a provider, the rest of the operation should short-circuit and not call the rest of the providers. + +### First Successful + +Similar to “First Match”, except that errors from evaluated providers do not halt execution. +Instead, it will return the first successful result from a provider. If no provider successfully responds, it will throw an error result. + +### User Defined + +Rather than making assumptions about when to use a provider’s result and when not to (which may not hold across all providers) there is also a way for the user to define their own strategy that determines whether to use a result or fall through to the next one. + +## Installation + + + +```xml + + + dev.openfeature.contrib.providers + multi-provider + 0.0.1 + +``` + + + +## Usage + +Usage example: + +``` +... +List providers = new ArrayList<>(2); +providers.add(provider1); +providers.add(provider2); + +// initialize using default strategy (first match) +MultiProvider multiProvider = new MultiProvider(providers); +OpenFeatureAPI.getInstance().setProviderAndWait(multiProvider); + +// initialize using a different strategy +multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy()); +... +``` + +See [MultiProviderTest](./src/test/java/dev/openfeature/contrib/providers/multiprovider/MultiProviderTest.java) +for more information. + diff --git a/providers/multiprovider/lombok.config b/providers/multiprovider/lombok.config new file mode 100644 index 000000000..bcd1afdae --- /dev/null +++ b/providers/multiprovider/lombok.config @@ -0,0 +1,5 @@ +# This file is needed to avoid errors throw by findbugs when working with lombok. +lombok.addSuppressWarnings = true +lombok.addLombokGeneratedAnnotation = true +config.stopBubbling = true +lombok.extern.findbugs.addSuppressFBWarnings = true diff --git a/providers/multiprovider/pom.xml b/providers/multiprovider/pom.xml new file mode 100644 index 000000000..1ef8c52a2 --- /dev/null +++ b/providers/multiprovider/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + dev.openfeature.contrib + parent + 0.1.0 + ../../pom.xml + + dev.openfeature.contrib.providers + multiprovider + 0.0.1 + + multiprovider + OpenFeature Multi-Provider + https://github.com/open-feature/java-sdk-contrib/tree/main/providers/multiprovider + + + + org.json + json + 20240303 + + + org.apache.logging.log4j + log4j-slf4j2-impl + 2.24.1 + test + + + diff --git a/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/FirstMatchStrategy.java b/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/FirstMatchStrategy.java new file mode 100644 index 000000000..3c90b3154 --- /dev/null +++ b/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/FirstMatchStrategy.java @@ -0,0 +1,53 @@ +package dev.openfeature.contrib.providers.multiprovider; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.function.Function; + +import static dev.openfeature.sdk.ErrorCode.FLAG_NOT_FOUND; + +/** + * First match strategy. + * Return the first result returned by a provider. Skip providers that indicate they had no value due to + * FLAG_NOT_FOUND. + * In all other cases, use the value returned by the provider. + * If any provider returns an error result other than FLAG_NOT_FOUND, the whole evaluation should error and + * “bubble up” the individual provider’s error in the result. + * As soon as a value is returned by a provider, the rest of the operation should short-circuit and not call + * the rest of the providers. + */ +@Slf4j +@NoArgsConstructor +public class FirstMatchStrategy implements Strategy { + + /** + * Represents a strategy that evaluates providers based on a first-match approach. + * Provides a method to evaluate providers using a specified function and return the evaluation result. + * + * @param providerFunction provider function + * @param ProviderEvaluation type + * @return the provider evaluation + */ + @Override + public ProviderEvaluation evaluate(Map providers, String key, T defaultValue, + EvaluationContext ctx, Function> providerFunction) { + for (FeatureProvider provider: providers.values()) { + try { + ProviderEvaluation res = providerFunction.apply(provider); + if (!FLAG_NOT_FOUND.equals(res.getErrorCode())) { + return res; + } + } catch (FlagNotFoundError e) { + log.debug("flag not found {}", e.getMessage()); + } + } + + throw new FlagNotFoundError("flag not found"); + } +} diff --git a/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/FirstSuccessfulStrategy.java b/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/FirstSuccessfulStrategy.java new file mode 100644 index 000000000..ec23746fb --- /dev/null +++ b/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/FirstSuccessfulStrategy.java @@ -0,0 +1,39 @@ +package dev.openfeature.contrib.providers.multiprovider; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.exceptions.GeneralError; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.function.Function; + +/** + * First Successful Strategy. + * Similar to “First Match”, except that errors from evaluated providers do not halt execution. + * Instead, it will return the first successful result from a provider. + * If no provider successfully responds, it will throw an error result. + */ +@Slf4j +@NoArgsConstructor +public class FirstSuccessfulStrategy implements Strategy { + + @Override + public ProviderEvaluation evaluate(Map providers, String key, T defaultValue, + EvaluationContext ctx, Function> providerFunction) { + for (FeatureProvider provider: providers.values()) { + try { + ProviderEvaluation res = providerFunction.apply(provider); + if (res.getErrorCode() == null) { + return res; + } + } catch (Exception e) { + log.debug("evaluation exception {}", e.getMessage()); + } + } + + throw new GeneralError("evaluation error"); + } +} diff --git a/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/MultiProvider.java b/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/MultiProvider.java new file mode 100644 index 000000000..65d24cb41 --- /dev/null +++ b/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/MultiProvider.java @@ -0,0 +1,152 @@ +package dev.openfeature.contrib.providers.multiprovider; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.EventProvider; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.GeneralError; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * Provider implementation for Multi-provider. + */ +@Slf4j +public class MultiProvider extends EventProvider { + + @Getter + private static final String NAME = "multiprovider"; + public static final int INIT_THREADS_COUNT = 8; + private final Map providers; + private final Strategy strategy; + private String metadataName; + + /** + * Constructs a MultiProvider with the given list of FeatureProviders, using a default strategy. + * + * @param providers the list of FeatureProviders to initialize the MultiProvider with + */ + public MultiProvider(List providers) { + this(providers, null); + } + + /** + * Constructs a MultiProvider with the given list of FeatureProviders and a strategy. + * + * @param providers the list of FeatureProviders to initialize the MultiProvider with + * @param strategy the strategy + */ + public MultiProvider(List providers, Strategy strategy) { + this.providers = buildProviders(providers); + if (strategy != null) { + this.strategy = strategy; + } else { + this.strategy = new FirstMatchStrategy(); + } + } + + protected static Map buildProviders(List providers) { + Map providersMap = new LinkedHashMap<>(providers.size()); + for (FeatureProvider provider: providers) { + FeatureProvider prevProvider = providersMap.put(provider.getMetadata().getName(), provider); + if (prevProvider != null) { + log.warn("duplicated provider name: {}", provider.getMetadata().getName()); + } + } + return Collections.unmodifiableMap(providersMap); + } + + /** + * Initialize the provider. + * @param evaluationContext evaluation context + * @throws Exception on error + */ + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + JSONObject json = new JSONObject(); + json.put("name", NAME); + JSONObject providersMetadata = new JSONObject(); + json.put("originalMetadata", providersMetadata); + ExecutorService initPool = Executors.newFixedThreadPool(INIT_THREADS_COUNT); + Collection> tasks = new ArrayList<>(providers.size()); + for (FeatureProvider provider: providers.values()) { + tasks.add(() -> { + provider.initialize(evaluationContext); + return true; + }); + JSONObject providerMetadata = new JSONObject(); + providerMetadata.put("name", provider.getMetadata().getName()); + providersMetadata.put(provider.getMetadata().getName(), providerMetadata); + } + List> results = initPool.invokeAll(tasks); + for (Future result: results) { + if (!result.get()) { + throw new GeneralError("init failed"); + } + } + metadataName = json.toString(); + } + + @Override + public Metadata getMetadata() { + return () -> metadataName; + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + return strategy.evaluate(providers, key, defaultValue, ctx, + p -> p.getBooleanEvaluation(key, defaultValue, ctx)); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + return strategy.evaluate(providers, key, defaultValue, ctx, + p -> p.getStringEvaluation(key, defaultValue, ctx)); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + return strategy.evaluate(providers, key, defaultValue, ctx, + p -> p.getIntegerEvaluation(key, defaultValue, ctx)); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + return strategy.evaluate(providers, key, defaultValue, ctx, + p -> p.getDoubleEvaluation(key, defaultValue, ctx)); + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + return strategy.evaluate(providers, key, defaultValue, ctx, + p -> p.getObjectEvaluation(key, defaultValue, ctx)); + } + + @Override + public void shutdown() { + log.debug("shutdown begin"); + for (FeatureProvider provider: providers.values()) { + try { + provider.shutdown(); + } catch (Exception e) { + log.error("error shutdown provider {}", provider.getMetadata().getName(), e); + } + } + log.debug("shutdown end"); + } + +} diff --git a/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/Strategy.java b/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/Strategy.java new file mode 100644 index 000000000..85fc06747 --- /dev/null +++ b/providers/multiprovider/src/main/java/dev/openfeature/contrib/providers/multiprovider/Strategy.java @@ -0,0 +1,16 @@ +package dev.openfeature.contrib.providers.multiprovider; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.ProviderEvaluation; + +import java.util.Map; +import java.util.function.Function; + +/** + * strategy. + */ +public interface Strategy { + ProviderEvaluation evaluate(Map providers, String key, T defaultValue, + EvaluationContext ctx, Function> providerFunction); +} diff --git a/providers/multiprovider/src/test/java/dev/openfeature/contrib/providers/multiprovider/MultiProviderTest.java b/providers/multiprovider/src/test/java/dev/openfeature/contrib/providers/multiprovider/MultiProviderTest.java new file mode 100644 index 000000000..943728c6a --- /dev/null +++ b/providers/multiprovider/src/test/java/dev/openfeature/contrib/providers/multiprovider/MultiProviderTest.java @@ -0,0 +1,202 @@ +package dev.openfeature.contrib.providers.multiprovider; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.MutableContext; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.providers.memory.Flag; +import dev.openfeature.sdk.providers.memory.InMemoryProvider; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class MultiProviderTest { + + @SneakyThrows + @Test + public void testInit() { + FeatureProvider provider1 = mock(FeatureProvider.class); + FeatureProvider provider2 = mock(FeatureProvider.class); + when(provider1.getMetadata()).thenReturn(() -> "provider1"); + when(provider2.getMetadata()).thenReturn(() -> "provider2"); + + List providers = new ArrayList<>(2); + providers.add(provider1); + providers.add(provider2); + Strategy strategy = mock(Strategy.class); + MultiProvider multiProvider = new MultiProvider(providers, strategy); + multiProvider.initialize(null); + + assertNotNull(multiProvider); + assertEquals("{\"originalMetadata\":{\"provider1\":{\"name\":\"provider1\"}," + + "\"provider2\":{\"name\":\"provider2\"}},\"name\":\"multiprovider\"}", + multiProvider.getMetadata().getName()); + } + + @SneakyThrows + @Test + public void testInitOneFails() { + FeatureProvider provider1 = mock(FeatureProvider.class); + FeatureProvider provider2 = mock(FeatureProvider.class); + when(provider1.getMetadata()).thenReturn(() -> "provider1"); + when(provider2.getMetadata()).thenReturn(() -> "provider2"); + doThrow(new GeneralError()).when(provider1).initialize(any()); + doThrow(new GeneralError()).when(provider1).shutdown(); + + List providers = new ArrayList<>(2); + providers.add(provider1); + providers.add(provider2); + Strategy strategy = mock(Strategy.class); + MultiProvider multiProvider = new MultiProvider(providers, strategy); + assertThrows(ExecutionException.class, () -> multiProvider.initialize(null)); + assertDoesNotThrow(() -> multiProvider.shutdown()); + } + + @Test + public void testDuplicateProviderNames() { + FeatureProvider provider1 = mock(FeatureProvider.class); + FeatureProvider provider2 = mock(FeatureProvider.class); + when(provider1.getMetadata()).thenReturn(() -> "provider"); + when(provider2.getMetadata()).thenReturn(() -> "provider"); + + List providers = new ArrayList<>(2); + providers.add(provider1); + providers.add(provider2); + + assertDoesNotThrow(() -> new MultiProvider(providers, null).initialize(null)); + } + + @SneakyThrows + @Test + public void testRetrieveMetadataName() { + List providers = new ArrayList<>(); + FeatureProvider mockProvider = mock(FeatureProvider.class); + when(mockProvider.getMetadata()).thenReturn(() -> "MockProvider"); + providers.add(mockProvider); + Strategy mockStrategy = mock(Strategy.class); + MultiProvider multiProvider = new MultiProvider(providers, mockStrategy); + multiProvider.initialize(null); + + assertEquals("{\"originalMetadata\":{\"MockProvider\":{\"name\":\"MockProvider\"}}," + + "\"name\":\"multiprovider\"}", multiProvider.getMetadata().getName()); + } + + @SneakyThrows + @Test + public void testEvaluations() { + Map> flags1 = new HashMap<>(); + flags1.put("b1", Flag.builder().variant("true", true) + .variant("false", false).defaultVariant("true").build()); + flags1.put("i1", Flag.builder().variant("v", 1).defaultVariant("v").build()); + flags1.put("d1", Flag.builder().variant("v", 1.0).defaultVariant("v").build()); + flags1.put("s1", Flag.builder().variant("v", "str1").defaultVariant("v").build()); + flags1.put("o1", Flag.builder().variant("v", new Value("v1")) + .defaultVariant("v").build()); + InMemoryProvider provider1 = new InMemoryProvider(flags1) { + public Metadata getMetadata() { + return () -> "old-provider"; + } + }; + Map> flags2 = new HashMap<>(); + flags2.put("b1", Flag.builder().variant("true", true) + .variant("false", false).defaultVariant("false").build()); + flags2.put("i1", Flag.builder().variant("v", 2).defaultVariant("v").build()); + flags2.put("d1", Flag.builder().variant("v", 2.0).defaultVariant("v").build()); + flags2.put("s1", Flag.builder().variant("v", "str2").defaultVariant("v").build()); + flags2.put("o1", Flag.builder().variant("v", new Value("v2")) + .defaultVariant("v").build()); + + flags2.put("s2", Flag.builder().variant("v", "s2str2").defaultVariant("v").build()); + InMemoryProvider provider2 = new InMemoryProvider(flags2) { + public Metadata getMetadata() { + return () -> "new-provider"; + } + }; + List providers = new ArrayList<>(2); + providers.add(provider1); + providers.add(provider2); + MultiProvider multiProvider = new MultiProvider(providers); + multiProvider.initialize(null); + + assertEquals(true, multiProvider.getBooleanEvaluation("b1", false, null) + .getValue()); + assertEquals(1, multiProvider.getIntegerEvaluation("i1", 0, null) + .getValue()); + assertEquals(1.0, multiProvider.getDoubleEvaluation("d1", 0.0, null) + .getValue()); + assertEquals("str1", multiProvider.getStringEvaluation("s1", "", null) + .getValue()); + assertEquals("v1", multiProvider.getObjectEvaluation("o1", null, null) + .getValue().asString()); + + assertEquals("s2str2", multiProvider.getStringEvaluation("s2", "", null) + .getValue()); + MultiProvider finalMultiProvider1 = multiProvider; + assertThrows(FlagNotFoundError.class, () -> + finalMultiProvider1.getStringEvaluation("non-existing", "", null)); + + multiProvider.shutdown(); + multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy()); + multiProvider.initialize(null); + + assertEquals(true, multiProvider.getBooleanEvaluation("b1", false, null) + .getValue()); + assertEquals(1, multiProvider.getIntegerEvaluation("i1", 0, null) + .getValue()); + assertEquals(1.0, multiProvider.getDoubleEvaluation("d1", 0.0, null) + .getValue()); + assertEquals("str1", multiProvider.getStringEvaluation("s1", "", null) + .getValue()); + assertEquals("v1", multiProvider.getObjectEvaluation("o1", null, null) + .getValue().asString()); + + assertEquals("s2str2", multiProvider.getStringEvaluation("s2", "", null) + .getValue()); + MultiProvider finalMultiProvider2 = multiProvider; + assertThrows(GeneralError.class, () -> + finalMultiProvider2.getStringEvaluation("non-existing", "", null)); + + multiProvider.shutdown(); + Strategy customStrategy = new Strategy() { + final FirstMatchStrategy fallbackStrategy = new FirstMatchStrategy(); + @Override + public ProviderEvaluation evaluate(Map providers, String key, T defaultValue, EvaluationContext ctx, Function> providerFunction) { + Value contextProvider = null; + if (ctx != null) { + contextProvider = ctx.getValue("provider"); + } + if (contextProvider != null && "new-provider".equals(contextProvider.asString())) { + return providerFunction.apply(providers.get("new-provider")); + } + return fallbackStrategy.evaluate(providers, key, defaultValue, ctx, providerFunction); + } + }; + multiProvider = new MultiProvider(providers, customStrategy); + multiProvider.initialize(null); + + EvaluationContext context = new MutableContext().add("provider", "new-provider"); + assertEquals(false, multiProvider.getBooleanEvaluation("b1", true, context) + .getValue()); + assertEquals(true, multiProvider.getBooleanEvaluation("b1", true, null) + .getValue()); + } +} \ No newline at end of file diff --git a/providers/multiprovider/src/test/resources/log4j2-test.xml b/providers/multiprovider/src/test/resources/log4j2-test.xml new file mode 100644 index 000000000..aced30f8a --- /dev/null +++ b/providers/multiprovider/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/providers/multiprovider/version.txt b/providers/multiprovider/version.txt new file mode 100644 index 000000000..8acdd82b7 --- /dev/null +++ b/providers/multiprovider/version.txt @@ -0,0 +1 @@ +0.0.1 diff --git a/release-please-config.json b/release-please-config.json index 964a4ee8b..b8a017625 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -100,6 +100,17 @@ "README.md" ] }, + "providers/multiprovider": { + "package-name": "dev.openfeature.contrib.providers.multiprovider", + "release-type": "simple", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [ + "pom.xml", + "README.md" + ] + }, "hooks/open-telemetry": { "package-name": "dev.openfeature.contrib.hooks.otel", "release-type": "simple",