diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dabccd..65a14df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - jre: [17] + jre: [21] os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4371aac..69f9980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - `webtools.Env.isHerokuBuild()` and `isGitHubAction()` - `com.diffplug.webtools.jte` plugin ([#10](https://github.com/diffplug/webtools/pull/10)) +- `com.diffplug.webtools.flywayjooq` plugin ([#11](https://github.com/diffplug/webtools/pull/11)) ## [1.2.6] - 2025-08-22 ### Fixed diff --git a/README.md b/README.md index 1597b6e..837d78f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ - [node](#node) - hassle-free `npm install` and `npm run blah` - [static server](#static-server) - a simple static file server - [jte](#jte) - creates idiomatic Kotlin model classes for `jte` templates (strict nullability & idiomatic collections and generics) +- [flywayjooq](#flywayjooq) - coordinates docker, flyway, and jOOQ for fast testing ## Node @@ -66,3 +67,24 @@ class header( ``` We also translate Java collections and generics to their Kotlin equivalents. See `JteRenderer.convertJavaToKotlin` for details. + +### flywayjooq + +Compile tasks just need to depend on the `jooq` task. It will keep a live database running to test against. + +```gradle +flywayJooq { + // starts this docker container which needs to have postgres + setup.dockerComposeFile = file('src/test/resources/docker-compose.yml') + // writes out connection data to this file + setup.dockerConnectionParams = file('build/pgConnection.properties') + // migrates a template database to this + setup.flywayMigrations = file('src/main/resources/db/migration') + // dumps the final schema out to this + setup.flywaySchemaDump = file('src/test/resources/schema.sql') + // sets up jOOQ + configuration { + // jOOQ setup same as the official jOOQ plugin + } +} +``` diff --git a/build.gradle b/build.gradle index a7bb9b3..b1b2f1e 100644 --- a/build.gradle +++ b/build.gradle @@ -47,4 +47,22 @@ dependencies { jteCompileOnly gradleApi() jteCompileOnly "gg.jte:jte-runtime:${VER_JTE}" jteCompileOnly "gg.jte:jte:${VER_JTE}" + // flyway and jooq + String VER_FLYWAY='11.11.1' + String VER_JOOQ='3.20.6' + String VER_PALANTIR_DOCKER_COMPOSE='2.3.0' + String VER_POSTGRESQL_DRIVER='42.7.7' + // https://github.com/palantir/docker-compose-rule + api "com.palantir.docker.compose:docker-compose-rule-core:${VER_PALANTIR_DOCKER_COMPOSE}" + api "com.palantir.docker.compose:docker-compose-rule-junit4:${VER_PALANTIR_DOCKER_COMPOSE}" + // https://jdbc.postgresql.org/documentation/changelog.html + implementation "org.postgresql:postgresql:${VER_POSTGRESQL_DRIVER}" + // jooq codegen + api "org.jooq:jooq-codegen:${VER_JOOQ}" + api "org.jooq.jooq-codegen-gradle:org.jooq.jooq-codegen-gradle.gradle.plugin:${VER_JOOQ}" + // db migration + api "org.flywaydb:flyway-core:${VER_FLYWAY}" + api "org.flywaydb:flyway-database-postgresql:${VER_FLYWAY}" + // java8 utilities + implementation 'com.diffplug.durian:durian-core:1.2.0' } diff --git a/gradle.properties b/gradle.properties index 7693bdf..adb4267 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,9 +5,9 @@ org=diffplug license=apache git_url=github.com/diffplug/webtools plugin_tags=node -plugin_list=node jte +plugin_list=node jte flywayjooq -ver_java=17 +ver_java=21 javadoc_links= @@ -20,3 +20,8 @@ plugin_jte_id=com.diffplug.webtools.jte plugin_jte_impl=com.diffplug.webtools.jte.JtePlugin plugin_jte_name=DiffPlug JTE plugin_jte_desc=Runs the JTE plugin and adds typesafe model classes + +plugin_flywayjooq_id=com.diffplug.webtools.flywayjooq +plugin_flywayjooq_impl=com.diffplug.webtools.flywayjooq.FlywayJooqPlugin +plugin_flywayjooq_name=DiffPlug Flyway jOOQ +plugin_flywayjooq_desc=Coordinates Docker Compose, Flyway, and jOOQ for fast testing and easy deployment. diff --git a/src/main/java/com/diffplug/webtools/node/SetupCleanup.java b/src/main/java/com/diffplug/webtools/SetupCleanup.java similarity index 96% rename from src/main/java/com/diffplug/webtools/node/SetupCleanup.java rename to src/main/java/com/diffplug/webtools/SetupCleanup.java index 7a5b34c..d2eeb9f 100644 --- a/src/main/java/com/diffplug/webtools/node/SetupCleanup.java +++ b/src/main/java/com/diffplug/webtools/SetupCleanup.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.webtools.node; +package com.diffplug.webtools; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -24,7 +24,7 @@ import java.nio.file.Files; import java.util.Arrays; -abstract class SetupCleanup { +public abstract class SetupCleanup { public void start(File keyFile, K key) throws Exception { synchronized (key.getClass()) { byte[] required = toBytes(key); diff --git a/src/main/java/com/diffplug/webtools/flywayjooq/FlywayJooqPlugin.java b/src/main/java/com/diffplug/webtools/flywayjooq/FlywayJooqPlugin.java new file mode 100644 index 0000000..79ee478 --- /dev/null +++ b/src/main/java/com/diffplug/webtools/flywayjooq/FlywayJooqPlugin.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2025 DiffPlug + * + * 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 + * + * https://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 com.diffplug.webtools.flywayjooq; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.inject.Inject; +import org.gradle.api.DefaultTask; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.TaskProvider; +import org.jooq.codegen.gradle.CodegenPluginExtension; + +/** + * This plugin spools up a fresh postgres session, + * runs flyway on it, then runs jooq on the result. + * The postgres stays up so that the flyway result + * can be used as a template database for testing. + */ +public class FlywayJooqPlugin implements Plugin { + private static final String EXTENSION_NAME = "flywayJooq"; + + public static class Extension extends CodegenPluginExtension { + @Inject + public Extension( + ObjectFactory objects, + ProjectLayout layout) { + super(objects, layout); + } + + public final SetupCleanupDockerFlyway setup = new SetupCleanupDockerFlyway(); + + /** Ensures a database with a template prepared by Flyway is available. */ + public void neededBy(TaskProvider taskProvider) { + taskProvider.configure(this::neededBy); + } + + /** Ensures a database with a template prepared by Flyway is available. */ + public void neededBy(Task task) { + task.dependsOn(DockerUp.TASK_NAME); + task.getInputs().file(setup.dockerComposeFile).withPathSensitivity(PathSensitivity.RELATIVE); + task.getInputs().dir(setup.flywayMigrations).withPathSensitivity(PathSensitivity.RELATIVE); + } + } + + @Override + public void apply(Project project) { + // FlywayPlugin needs to be applied first + project.getPlugins().apply(JavaBasePlugin.class); + + Extension extension = project.getExtensions().create(EXTENSION_NAME, Extension.class); + extension.configuration(a -> {}); + extension.setup.dockerPullOnStartup = !project.getGradle().getStartParameter().isOffline(); + + // force all jooq versions to match + String jooqVersion = detectJooqVersion(); + project.getConfigurations().all(config -> { + config.resolutionStrategy(strategy -> { + strategy.eachDependency(details -> { + String group = details.getRequested().getGroup(); + String name = details.getRequested().getName(); + if (group.equals("org.jooq") && name.startsWith("jooq")) { + details.useTarget(group + ":" + name + ":" + jooqVersion); + } + }); + }); + }); + + // create a jooq task, which will be needed by all compilation tasks + TaskProvider jooqTask = project.getTasks().register("jooq", JooqTask.class, task -> { + task.setup = extension.setup; + var generator = extension.getExecutions().maybeCreate("").getConfiguration().getGenerator(); + task.generatorConfig = generator; + task.getGeneratedSource().set(project.file(generator.getTarget().getDirectory())); + }); + extension.neededBy(jooqTask); + project.getTasks().named(JavaPlugin.COMPILE_JAVA_TASK_NAME).configure(task -> { + task.dependsOn(jooqTask); + }); + + project.getTasks().register(DockerDown.TASK_NAME, DockerDown.class, task -> { + task.getSetupCleanup().set(extension.setup); + task.getProjectDir().set(project.getProjectDir()); + }); + project.getTasks().register(DockerUp.TASK_NAME, DockerUp.class, task -> { + task.getSetupCleanup().set(extension.setup); + task.getProjectDir().set(project.getProjectDir()); + task.mustRunAfter(DockerDown.TASK_NAME); + }); + } + + public abstract static class DockerUp extends DefaultTask { + private static final String TASK_NAME = "dockerUp"; + + @Internal + public abstract DirectoryProperty getProjectDir(); + + @Internal + public abstract Property getSetupCleanup(); + + @TaskAction + public void dockerUp() throws Exception { + getSetupCleanup().get().start(getProjectDir().get().getAsFile()); + } + } + + public abstract static class DockerDown extends DefaultTask { + private static final String TASK_NAME = "dockerDown"; + + @Internal + public abstract DirectoryProperty getProjectDir(); + + @Internal + public abstract Property getSetupCleanup(); + + @TaskAction + public void dockerDown() throws Exception { + getSetupCleanup().get().forceStop(getProjectDir().get().getAsFile()); + } + } + + /** Detects the jooq version. */ + private static String detectJooqVersion() { + URLClassLoader loader = (URLClassLoader) FlywayJooqPlugin.class.getClassLoader(); + for (URL url : loader.getURLs()) { + Pattern pattern = Pattern.compile("(.*)/jooq-([0-9,.]*?).jar$"); + Matcher matcher = pattern.matcher(url.getPath()); + if (matcher.matches()) { + return matcher.group(2); + } + } + throw new IllegalStateException("Unable to detect jooq version."); + } +} diff --git a/src/main/java/com/diffplug/webtools/flywayjooq/JooqTask.java b/src/main/java/com/diffplug/webtools/flywayjooq/JooqTask.java new file mode 100644 index 0000000..8c28457 --- /dev/null +++ b/src/main/java/com/diffplug/webtools/flywayjooq/JooqTask.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 DiffPlug + * + * 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 + * + * https://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 com.diffplug.webtools.flywayjooq; + +import com.diffplug.common.base.Preconditions; +import com.diffplug.common.base.Throwables; +import java.io.File; +import java.net.ConnectException; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.tasks.*; +import org.jooq.codegen.GenerationTool; +import org.jooq.meta.jaxb.Configuration; +import org.jooq.meta.jaxb.Generator; +import org.jooq.meta.jaxb.Logging; + +@CacheableTask +public abstract class JooqTask extends DefaultTask { + SetupCleanupDockerFlyway setup; + Generator generatorConfig; + + @Internal + public SetupCleanupDockerFlyway getSetup() { + return setup; + } + + @OutputDirectory + public abstract DirectoryProperty getGeneratedSource(); + + @Input + public Generator getGeneratorConfig() { + return generatorConfig; + } + + @TaskAction + public void generate() throws Exception { + String targetDir = generatorConfig.getTarget().getDirectory(); + Preconditions.checkArgument(!(new File(targetDir).isAbsolute()), "`generator.target.directory` must not be absolute, was `%s`", targetDir); + // configure jooq to run against the db + try { + generatorConfig.getTarget().setDirectory(getGeneratedSource().get().getAsFile().getAbsolutePath()); + Configuration jooqConfig = new Configuration(); + jooqConfig.setGenerator(generatorConfig); + jooqConfig.setLogging(Logging.TRACE); + + // write the config out to file + GenerationTool tool = new GenerationTool(); + tool.setDataSource(setup.getConnection()); + tool.run(jooqConfig); + } catch (Exception e) { + var rootCause = Throwables.getRootCause(e); + if (rootCause instanceof ConnectException) { + throw new GradleException("Unable to connect to the database. Is the docker container running?", e); + } else { + throw e; + } + } finally { + generatorConfig.getTarget().setDirectory(targetDir); + } + } +} diff --git a/src/main/java/com/diffplug/webtools/flywayjooq/SetupCleanupDockerFlyway.java b/src/main/java/com/diffplug/webtools/flywayjooq/SetupCleanupDockerFlyway.java new file mode 100644 index 0000000..44a70f1 --- /dev/null +++ b/src/main/java/com/diffplug/webtools/flywayjooq/SetupCleanupDockerFlyway.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2025 DiffPlug + * + * 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 + * + * https://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 com.diffplug.webtools.flywayjooq; + +import com.diffplug.common.base.Either; +import com.diffplug.common.base.Errors; +import com.diffplug.common.base.Throwables; +import com.diffplug.common.base.Throwing; +import com.diffplug.webtools.SetupCleanup; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import com.palantir.docker.compose.DockerComposeRule; +import com.palantir.docker.compose.configuration.ProjectName; +import com.palantir.docker.compose.configuration.ShutdownStrategy; +import com.palantir.docker.compose.connection.DockerPort; +import com.palantir.docker.compose.connection.waiting.HealthChecks; +import com.palantir.docker.compose.execution.DockerCompose; +import com.palantir.docker.compose.execution.DockerComposeExecArgument; +import com.palantir.docker.compose.execution.DockerComposeExecOption; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import java.util.concurrent.TimeUnit; +import org.flywaydb.core.Flyway; +import org.gradle.api.GradleException; +import org.postgresql.ds.PGSimpleDataSource; +import webtools.Env; + +public class SetupCleanupDockerFlyway implements Serializable { + private static final long serialVersionUID = -8606504827780656288L; + + private static final String GITHUB_IP = "localhost"; + private static final int GITHUB_PORT = 5432; + + public File dockerComposeFile; + public File dockerConnectionParams; + public boolean dockerPullOnStartup = true; + + public File flywayMigrations; + public File flywaySchemaDump; + private TreeMap flywaySnapshot; + private File buildDir; + + /** Saves the flywayMigrations, then starts docker (if necessary) and runs flyway. */ + void start(File projectDir) throws Exception { + try { + buildDir = new File(projectDir, "build"); + flywaySnapshot = new TreeMap<>(); + Path root = flywayMigrations.toPath(); + java.nio.file.Files.walkFileTree(root, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String path = root.relativize(file).toString(); + flywaySnapshot.put(path, java.nio.file.Files.readAllBytes(file)); + return FileVisitResult.CONTINUE; + } + }); + new Impl().start(keyFile(projectDir), this); + } catch (Exception e) { + var rootCause = Throwables.getRootCause(e); + if (rootCause != null && rootCause.getMessage() != null) { + if (rootCause.getMessage().contains("Connection refused")) { + throw new GradleException("Unable to connect to docker. Is it running?", e); + } + } + throw e; + } + } + + void forceStop(File projectDir) throws Exception { + try { + new Impl().doStop(this); + } catch (Exception e) { + if (Throwables.getStackTraceAsString(e).contains("Connection refused")) { + // if we can't connect to docker, then we can't stop it + // so we'll just ignore the error + } else { + e.printStackTrace(); + } + } + File kf = keyFile(projectDir); + if (kf.exists()) { + java.nio.file.Files.delete(kf.toPath()); + } + } + + PGSimpleDataSource getConnection() throws IOException { + String ip; + int port; + if (Env.isGitHubAction()) { + ip = GITHUB_IP; + port = GITHUB_PORT; + } else { + // read the connection properties + Properties connectionProps = new Properties(); + try (Reader reader = Files.asCharSource(dockerConnectionParams, StandardCharsets.UTF_8).openBufferedStream()) { + connectionProps.load(reader); + } catch (IOException e) { + throw Errors.asRuntime(e); + } + ip = connectionProps.getProperty("host"); + port = Integer.parseInt(connectionProps.getProperty("port")); + } + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setServerNames(new String[]{ip}); + dataSource.setPortNumbers(new int[]{port}); + dataSource.setUser("root"); + dataSource.setPassword("password"); + dataSource.setDatabaseName("template1"); + dataSource.setConnectTimeout(20); + return dataSource; + } + + private static File keyFile(File projectDir) { + return new File(projectDir, "build/docker"); + } + + private static final int TRY_SILENTLY_FOR = 10_000; + private static final int TRY_LOUDLY_UNTIL = 12_000; + private static final int WAIT_BETWEEN_TRIES = 100; + + public static void keepTrying(Throwing.Runnable toAttempt) { + long start = System.currentTimeMillis(); + while (true) { + try { + toAttempt.run(); + return; + } catch (Throwable e) { + long elapsed = System.currentTimeMillis() - start; + if (elapsed < TRY_SILENTLY_FOR) { + Errors.rethrow().run(() -> Thread.sleep(WAIT_BETWEEN_TRIES)); + } else if (elapsed < TRY_LOUDLY_UNTIL) { + e.printStackTrace(); + Errors.rethrow().run(() -> Thread.sleep(WAIT_BETWEEN_TRIES)); + } else { + throw Errors.asRuntime(e); + } + } + } + } + + DockerComposeRule rule() { + return DockerComposeRule.builder() + .file(dockerComposeFile.getAbsolutePath()) + .projectName(ProjectName.fromString(Integer.toString(Math.abs(dockerComposeFile.getAbsolutePath().hashCode())))) + .waitingForService("postgres", HealthChecks.toHaveAllPortsOpen()) + .pullOnStartup(dockerPullOnStartup) + .removeConflictingContainersOnStartup(true) + .saveLogsTo(new File(buildDir, "tmp/docker").getAbsolutePath()) + .shutdownStrategy(ShutdownStrategy.SKIP) + .build(); + } + + private static class Impl extends SetupCleanup { + @Override + protected void doStart(SetupCleanupDockerFlyway key) throws IOException, InterruptedException { + DockerComposeRule rule; + String ip; + int port; + if (Env.isGitHubAction()) { + // circle provides the container for us + rule = null; + ip = GITHUB_IP; + port = GITHUB_PORT; + } else { + // start docker-compose and get postgres from that + rule = key.rule(); + rule.before(); + Files.createParentDirs(key.dockerConnectionParams); + + DockerPort dockerPort = rule.containers() + .container("postgres") + .port(5432); + ip = dockerPort.getIp(); + port = dockerPort.getExternalPort(); + } + Files.createParentDirs(key.dockerConnectionParams); + Files.asCharSink(key.dockerConnectionParams, StandardCharsets.UTF_8).write("host=" + ip + "\nport=" + port); + + // run flyway + PGSimpleDataSource postgres = key.getConnection(); + keepTrying(() -> { + Flyway.configure() + .dataSource(postgres) + .locations("filesystem:" + key.flywayMigrations.getAbsolutePath()) + .schemas("public") + .load() + .migrate(); + }); + + // write out the schema to disk + String schema; + List pg_dump_args = Arrays.asList("-d", "template1", "-U", postgres.getUser(), "--schema-only", "--restrict-key=reproduciblediff"); + if (rule == null) { + Process process = Runtime.getRuntime().exec(ImmutableList. builder().add( + "pg_dump", + "-h", GITHUB_IP, "-p", Integer.toString(GITHUB_PORT)) + .addAll(pg_dump_args).build().toArray(new String[0])); + // swallow errors (not great...) + new InputStreamCollector(process.getErrorStream()); + InputStreamCollector output = new InputStreamCollector(process.getInputStream()); + process.waitFor(10, TimeUnit.SECONDS); + output.join(1_000); + schema = new String(output.result().getLeft(), StandardCharsets.UTF_8); + } else { + schema = rule.dockerCompose().exec(DockerComposeExecOption.noOptions(), + "postgres", DockerComposeExecArgument.arguments(ImmutableList.builder().add("pg_dump") + .addAll(pg_dump_args) + .build().toArray(new String[0]))); + } + Files.createParentDirs(key.flywaySchemaDump); + Files.write(schema, key.flywaySchemaDump, StandardCharsets.UTF_8); + } + + @Override + protected void doStop(SetupCleanupDockerFlyway key) throws IOException, InterruptedException { + if (!Env.isGitHubAction()) { + DockerCompose compose = key.rule().dockerCompose(); + compose.kill(); + compose.rm(); + } + } + + static class InputStreamCollector extends Thread { + private final InputStream iStream; + private Either result; + + public InputStreamCollector(InputStream is) { + this.iStream = Objects.requireNonNull(is); + start(); + } + + @Override + public synchronized void run() { + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ByteStreams.copy(iStream, output); + result = Either.createLeft(output.toByteArray()); + } catch (IOException ex) { + result = Either.createRight(ex); + } + } + + public synchronized Either result() { + return result; + } + } + } +} diff --git a/src/main/java/com/diffplug/webtools/node/NodePlugin.java b/src/main/java/com/diffplug/webtools/node/NodePlugin.java index 2cce670..68ee05d 100644 --- a/src/main/java/com/diffplug/webtools/node/NodePlugin.java +++ b/src/main/java/com/diffplug/webtools/node/NodePlugin.java @@ -32,6 +32,7 @@ import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskProvider; import org.gradle.work.DisableCachingByDefault; +import webtools.Env; /** * Installs a specific version of node.js and npm, @@ -49,6 +50,14 @@ public Extension(Project project) { this.project = Objects.requireNonNull(project); } + public boolean envIsHerokuBuild() { + return Env.isHerokuBuild(); + } + + public boolean envIsGitHubAction() { + return Env.isGitHubAction(); + } + public TaskProvider npm_run(String name, Action taskConfig) { return project.getTasks().register("npm_run_" + name.replace(':', '-'), NpmRunTask.class, task -> { task.npmTaskName = name; diff --git a/src/main/java/com/diffplug/webtools/node/SetupCleanupNode.java b/src/main/java/com/diffplug/webtools/node/SetupCleanupNode.java index 95e29f1..9592d88 100644 --- a/src/main/java/com/diffplug/webtools/node/SetupCleanupNode.java +++ b/src/main/java/com/diffplug/webtools/node/SetupCleanupNode.java @@ -16,6 +16,7 @@ package com.diffplug.webtools.node; import com.diffplug.common.swt.os.OS; +import com.diffplug.webtools.SetupCleanup; import com.github.eirslett.maven.plugins.frontend.lib.FrontendPluginFactory; import com.github.eirslett.maven.plugins.frontend.lib.InstallationException; import com.github.eirslett.maven.plugins.frontend.lib.ProxyConfig; diff --git a/src/jte/java/webtools/Env.java b/src/main/java/webtools/Env.java similarity index 100% rename from src/jte/java/webtools/Env.java rename to src/main/java/webtools/Env.java