diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java index 08257f822..b0292eb7e 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java @@ -51,6 +51,7 @@ import io.flamingock.internal.core.plugin.Plugin; import io.flamingock.internal.core.plugin.PluginManager; import io.flamingock.internal.core.builder.args.FlamingockArguments; +import io.flamingock.internal.core.builder.runner.DisabledRunner; import io.flamingock.internal.core.builder.runner.Runner; import io.flamingock.internal.core.builder.runner.RunnerBuilder; import io.flamingock.internal.core.builder.runner.RunnerFactory; @@ -191,6 +192,15 @@ public HOLDER setApplicationArguments(String[] args) { */ @Override public final Runner build() { + FlamingockArguments flamingockArgs = FlamingockArguments.parse(applicationArgs); + + // Kill switch for the auto-execution path: when `flamingock.enabled=false` and we're + // not in CLI mode, return a no-op runner before any side-effectful setup runs (no + // template scan, no plugin init, no audit-store connection, no lock attempt, no + // pipeline load). Explicitly-invoked CLI commands ignore the flag — user intent wins. + if (!flamingockArgs.isCliMode() && !coreConfiguration.isEnabled()) { + return new DisabledRunner(); + } ChangeTemplateManager.loadTemplates(); pluginManager.initialize(context); @@ -221,8 +231,6 @@ public final Runner build() { pipeline.validate(); pipeline.contributeToContext(hierarchicalContext); - FlamingockArguments flamingockArgs = FlamingockArguments.parse(applicationArgs); - OperationResolver operationResolver = new OperationResolver( runnerId, flamingockArgs, diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/DisabledRunner.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/DisabledRunner.java new file mode 100644 index 000000000..9f26d2384 --- /dev/null +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/DisabledRunner.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed 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 io.flamingock.internal.core.builder.runner; + +import io.flamingock.internal.util.log.FlamingockLoggerFactory; +import org.slf4j.Logger; + +/** + * No-op {@link Runner} returned by {@code AbstractChangeRunnerBuilder.build()} when + * {@code flamingock.enabled=false} and the build is not in CLI mode. Emits a single + * INFO line and exits. + * + *
By the time the builder returns this runner, none of the change-application + * machinery has been initialised — no template scan, no plugin initialisation, no + * audit-store connection, no lock attempt, no pipeline load. Hence no finalizer is + * needed: nothing was opened, nothing to close. + */ +public final class DisabledRunner implements Runner { + private static final Logger logger = FlamingockLoggerFactory.getLogger("flamingock.runner"); + + @Override + public void run() { + logger.info("Flamingock disabled (flamingock.enabled=false); skipping execution."); + } +} diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/runner/DisabledRunnerTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/runner/DisabledRunnerTest.java new file mode 100644 index 000000000..8073992f4 --- /dev/null +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/builder/runner/DisabledRunnerTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed 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 io.flamingock.internal.core.builder.runner; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for {@link DisabledRunner}. The runner is returned by + * {@code AbstractChangeRunnerBuilder.build()} when {@code flamingock.enabled=false} and the + * build is not in CLI mode; its sole contract is to log a single line and exit without side + * effects. + */ +class DisabledRunnerTest { + + @Test + @DisplayName("run() returns cleanly without throwing") + void runDoesNotThrow() { + DisabledRunner runner = new DisabledRunner(); + assertDoesNotThrow((org.junit.jupiter.api.function.Executable) runner::run); + } + + @Test + @DisplayName("run() is repeatable (idempotent no-op)") + void runIsIdempotent() { + DisabledRunner runner = new DisabledRunner(); + assertDoesNotThrow((org.junit.jupiter.api.function.Executable) runner::run); + assertDoesNotThrow((org.junit.jupiter.api.function.Executable) runner::run); + } + + @Test + @DisplayName("DisabledRunner is a Runner") + void isARunner() { + assertTrue(new DisabledRunner() instanceof Runner, + "DisabledRunner must implement the Runner contract so AbstractChangeRunnerBuilder.build() can return it"); + } +}