diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index f2034d59d1..06c05221cf 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -12,6 +12,9 @@ on: - v* workflow_dispatch: +env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + jobs: analyze: name: Analyze @@ -19,6 +22,7 @@ jobs: permissions: actions: read contents: write + packages: read security-events: write strategy: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a2df13e1c2..18cb2ddfec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,7 @@ env: SCHEDULER_PASSWORD: "${{secrets.SCHEDULER_PASSWORD}}" SEQUENCING_USERNAME: "${{secrets.SEQUENCING_USERNAME}}" SEQUENCING_PASSWORD: "${{secrets.SEQUENCING_PASSWORD}}" + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" jobs: unit-test: diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle index d7d5c9c359..9a8952fa17 100644 --- a/e2e-tests/build.gradle +++ b/e2e-tests/build.gradle @@ -1,9 +1,7 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - plugins { id 'java-library' id 'jacoco' - id 'com.gradleup.shadow' version '8.3.0' + id 'gov.nasa.ammos.aerie.procedural.plugin' version '0.1.0' } java { @@ -57,14 +55,6 @@ task e2eTest(type: Test) { } dependencies { - annotationProcessor project(':procedural:processor') - - implementation project(":procedural:scheduling") - implementation project(":procedural:timeline") - implementation project(':merlin-sdk') - implementation project(':type-utils') - implementation project(':contrib') - testImplementation project(":procedural:remote") testImplementation "com.zaxxer:HikariCP:5.1.0" testImplementation("org.postgresql:postgresql:42.6.0") @@ -77,65 +67,3 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' } - -tasks.register('buildAllSchedulingProcedureJars') { - group = 'SchedulingProcedureJars' - - dependsOn "generateSchedulingProcedureJarTasks" - dependsOn { - tasks.findAll { task -> task.name.startsWith('buildSchedulingProcedureJar_') } - } -} - -tasks.create("generateSchedulingProcedureJarTasks") { - group = 'SchedulingProcedureJars' - - final proceduresDir = findFirstMatchingBuildDir("generated/procedures") - - if (proceduresDir == null) { - println "No procedures folder found" - return - } - println "Generating jar tasks for the following procedures directory: ${proceduresDir}" - - final files = file(proceduresDir).listFiles() - if (files.length == 0) { - println "No procedures available within folder ${proceduresDir}" - return - } - - files.toList().each { file -> - final nameWithoutExtension = file.name.replace(".java", "") - final taskName = "buildSchedulingProcedureJar_${nameWithoutExtension}" - - println "Generating ${taskName} task, which will build ${nameWithoutExtension}.jar" - - tasks.create(taskName, ShadowJar) { - group = 'SchedulingProcedureJars' - configurations = [project.configurations.compileClasspath] - from sourceSets.main.output - archiveBaseName = "" // clear - archiveClassifier.set(nameWithoutExtension) // set output jar name - manifest { - attributes 'Main-Class': getMainClassFromGeneratedFile(file) - } - minimize() - } - } -} - -private String findFirstMatchingBuildDir(String pattern) { - String found = null - final generatedDir = file("build/generated/sources") - generatedDir.mkdirs() - generatedDir.eachDirRecurse { dir -> if (dir.path.contains(pattern)) found = dir.path } - return found -} - -private static String getMainClassFromGeneratedFile(File file) { - final fileString = file.toString() - final prefix = "build/generated/sources/annotationProcessor/java/main/" - final index = fileString.indexOf(prefix) + prefix.length() - final trimmed = fileString.substring(index).replace(".java", "") - return trimmed.replace("/", ".") -} diff --git a/procedural/examples/foo-procedures/build.gradle b/procedural/examples/foo-procedures/build.gradle index 721e290833..840bff9372 100644 --- a/procedural/examples/foo-procedures/build.gradle +++ b/procedural/examples/foo-procedures/build.gradle @@ -1,8 +1,6 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - plugins { id 'java' - id 'com.gradleup.shadow' version '8.3.0' + id 'gov.nasa.ammos.aerie.procedural.plugin' version '0.1.0' } java { @@ -16,79 +14,9 @@ repositories { } dependencies { - annotationProcessor project(':procedural:processor') - implementation project(':procedural:constraints') - implementation project(':procedural:scheduling') - implementation project(':procedural:timeline') - implementation project(':merlin-driver') - implementation project(':type-utils') - implementation project(':contrib') - testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' } -tasks.register('buildAllSchedulingProcedureJars') { - group = 'SchedulingProcedureJars' - - dependsOn "generateSchedulingProcedureJarTasks" - dependsOn { - tasks.findAll { task -> task.name.startsWith('buildSchedulingProcedureJar_') } - } -} - -tasks.create("generateSchedulingProcedureJarTasks") { - group = 'SchedulingProcedureJars' - - final proceduresDir = findFirstMatchingBuildDir("generated/procedures") - - if (proceduresDir == null) { - println "No procedures folder found" - return - } - println "Generating jar tasks for the following procedures directory: ${proceduresDir}" - - final files = file(proceduresDir).listFiles() - if (files.length == 0) { - println "No procedures available within folder ${proceduresDir}" - return - } - - files.toList().each { file -> - final nameWithoutExtension = file.name.replace(".java", "") - final taskName = "buildSchedulingProcedureJar_${nameWithoutExtension}" - - println "Generating ${taskName} task, which will build ${nameWithoutExtension}.jar" - - tasks.create(taskName, ShadowJar) { - group = 'SchedulingProcedureJars' - configurations = [project.configurations.compileClasspath] - from sourceSets.main.output - archiveBaseName = "" // clear - archiveClassifier.set(nameWithoutExtension) // set output jar name - manifest { - attributes 'Main-Class': getMainClassFromGeneratedFile(file) - } - minimize() - } - } -} - -private String findFirstMatchingBuildDir(String pattern) { - String found = null - final generatedDir = file("build/generated/sources") - generatedDir.mkdirs() - generatedDir.eachDirRecurse { dir -> if (dir.path.contains(pattern)) found = dir.path } - return found -} - -private static String getMainClassFromGeneratedFile(File file) { - final fileString = file.toString() - final prefix = "build/generated/sources/annotationProcessor/java/main/" - final index = fileString.indexOf(prefix) + prefix.length() - final trimmed = fileString.substring(index).replace(".java", "") - return trimmed.replace("/", ".") -} - test { useJUnitPlatform() } diff --git a/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/Helper.java b/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/Helper.java deleted file mode 100644 index 0308d22dd5..0000000000 --- a/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/Helper.java +++ /dev/null @@ -1,7 +0,0 @@ -package gov.nasa.ammos.aerie.procedural.examples.fooprocedures; - -public class Helper { - public static String greeting() { - return "hello from util"; - } -} diff --git a/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/constraints/ConstFruit.java b/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/constraints/ConstFruit.java deleted file mode 100644 index 762cc9f1fd..0000000000 --- a/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/constraints/ConstFruit.java +++ /dev/null @@ -1,20 +0,0 @@ -package gov.nasa.ammos.aerie.procedural.examples.fooprocedures.constraints; - -import gov.nasa.ammos.aerie.procedural.constraints.GeneratorConstraint; -import gov.nasa.ammos.aerie.procedural.constraints.Violations; -import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.Real; -import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan; -import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults; -import org.jetbrains.annotations.NotNull; - -public class ConstFruit extends GeneratorConstraint { - @Override - public void generate(@NotNull Plan plan, @NotNull SimulationResults simResults) { - final var fruit = simResults.resource("/fruit", Real.deserializer()); - - violate(Violations.on( - fruit.equalTo(4), - false - )); - } -} diff --git a/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/constraints/Hello.java b/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/constraints/Hello.java deleted file mode 100644 index 1e36a1d2c1..0000000000 --- a/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/constraints/Hello.java +++ /dev/null @@ -1,9 +0,0 @@ -package gov.nasa.ammos.aerie.procedural.examples.fooprocedures.constraints; - -import gov.nasa.ammos.aerie.procedural.examples.fooprocedures.Helper; - -class Hello { - public static void main(String[] args) { - System.out.println(Helper.greeting()); - } -} diff --git a/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/procedures/SampleProcedure.java b/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/procedures/SampleProcedure.java deleted file mode 100644 index 9a2f1cd290..0000000000 --- a/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/procedures/SampleProcedure.java +++ /dev/null @@ -1,35 +0,0 @@ -package gov.nasa.ammos.aerie.procedural.examples.fooprocedures.procedures; - -import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; -import gov.nasa.ammos.aerie.procedural.scheduling.Goal; -import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; -import gov.nasa.ammos.aerie.procedural.scheduling.plan.NewDirective; -import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; -import org.jetbrains.annotations.NotNull; - -import java.util.Map; - -@SchedulingProcedure -public record SampleProcedure(int quantity) implements Goal { - @Override - public void run(@NotNull final EditablePlan plan) { - final var firstTime = Duration.hours(24); - final var step = Duration.hours(6); - - var currentTime = firstTime; - for (var i = 0; i < quantity; i++) { - plan.create( - new NewDirective( - new AnyDirective(Map.of()), - "It's a bite banana activity", - "BiteBanana", - new DirectiveStart.Absolute(currentTime) - ) - ); - currentTime = currentTime.plus(step); - } - plan.commit(); - } -} diff --git a/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/procedures/SimulationDemo.java b/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/procedures/SimulationDemo.java deleted file mode 100644 index 5ca804d72a..0000000000 --- a/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/procedures/SimulationDemo.java +++ /dev/null @@ -1,46 +0,0 @@ -package gov.nasa.ammos.aerie.procedural.examples.fooprocedures.procedures; - -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; -import gov.nasa.ammos.aerie.procedural.scheduling.Goal; -import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; -import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; -import gov.nasa.ammos.aerie.procedural.timeline.collections.profiles.Real; -import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; -import org.jetbrains.annotations.NotNull; - -import java.util.Map; - -@SchedulingProcedure -public record SimulationDemo(int quantity) implements Goal { - @Override - public void run(@NotNull final EditablePlan plan) { - - var simResults = plan.latestResults(); - if (simResults == null) simResults = plan.simulate(); - - final var lowFruit = simResults.resource("/fruit", Real.deserializer()).lessThan(3.5).isolateTrue(); - final var bites = simResults.instances("BiteBanana"); - - final var connections = lowFruit.starts().shift(Duration.MINUTE.negate()) - .connectTo(bites.ends(), false); - - for (final var connection: connections) { - assert connection.to != null; - plan.create( - "GrowBanana", - new DirectiveStart.Anchor( - connection.to.directiveId, - Duration.minutes(30), - DirectiveStart.Anchor.AnchorPoint.End - ), - Map.of( - "quantity", SerializedValue.of(1), - "growingDuration", SerializedValue.of(Duration.HOUR.dividedBy(Duration.MICROSECOND)) - ) - ); - } - - plan.commit(); - } -} diff --git a/procedural/examples/foo-procedures/src/test/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/constraints/ProcedureLoader.java b/procedural/examples/foo-procedures/src/test/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/constraints/ProcedureLoader.java deleted file mode 100644 index 4a60866c2b..0000000000 --- a/procedural/examples/foo-procedures/src/test/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/constraints/ProcedureLoader.java +++ /dev/null @@ -1,68 +0,0 @@ -package gov.nasa.ammos.aerie.procedural.examples.fooprocedures.constraints; - -import gov.nasa.jpl.aerie.merlin.protocol.model.MerlinPlugin; -import gov.nasa.ammos.aerie.procedural.constraints.Constraint; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Path; -import java.util.Objects; -import java.util.jar.JarFile; - -public final class ProcedureLoader { - public static Constraint loadProcedure(final Path path, final String name, final String version) - throws ProcedureLoadException - { - final var className = getImplementingClassName(path, name, version); - final var classLoader = new URLClassLoader(new URL[] {pathToUrl(path)}); - - try { - final var pluginClass$ = classLoader.loadClass(className); - if (!Constraint.class.isAssignableFrom(pluginClass$)) { - throw new ProcedureLoadException(path, name, version); - } - - return (Constraint) pluginClass$.getConstructor().newInstance(); - } catch (final ReflectiveOperationException ex) { - throw new ProcedureLoadException(path, name, version, ex); - } - } - - private static String getImplementingClassName(final Path jarPath, final String name, final String version) - throws ProcedureLoadException { - try (final var jarFile = new JarFile(jarPath.toFile())) { - return Objects.requireNonNull(jarFile.getManifest().getMainAttributes().getValue("Main-Class")); - } catch (final IOException ex) { - throw new ProcedureLoadException(jarPath, name, version, ex); - } - } - - private static URL pathToUrl(final Path path) { - try { - return path.toUri().toURL(); - } catch (final MalformedURLException ex) { - // This exception only happens if there is no URL protocol handler available to represent a Path. - // This is highly unexpected, and indicates a fundamental problem with the system environment. - throw new Error(ex); - } - } - - public static class ProcedureLoadException extends Exception { - private ProcedureLoadException(final Path path, final String name, final String version) { - this(path, name, version, null); - } - - private ProcedureLoadException(final Path path, final String name, final String version, final Throwable cause) { - super( - String.format( - "No implementation found for `%s` at path `%s` wih name \"%s\" and version \"%s\"", - MerlinPlugin.class.getSimpleName(), - path, - name, - version), - cause); - } - } -} diff --git a/procedural/plugin/build.gradle b/procedural/plugin/build.gradle new file mode 100644 index 0000000000..c9248541aa --- /dev/null +++ b/procedural/plugin/build.gradle @@ -0,0 +1,73 @@ +plugins { + id 'groovy-gradle-plugin' + id "maven-publish" +} + +import groovy.json.JsonSlurper + +def getLatestMavenPackageVersion(String owner, String packageName, String token) { + def apiUrl = "https://api.github.com/orgs/${owner}/packages/maven/${packageName}/versions" + def conn = apiUrl.toURL().openConnection() + + conn.setRequestProperty("Authorization", "Bearer ${token}") + conn.setRequestProperty("Accept", "application/vnd.github+json") + + def response = conn.inputStream.text + def versions = new JsonSlurper().parseText(response) + + if (versions.isEmpty()) { + return "No versions found" + } + + // Sort versions and get the latest one + def latestVersion = versions.sort { a, b -> + def versionA = a.name.tokenize('.') + def versionB = b.name.tokenize('.') + return versionB <=> versionA + }[0] + + return latestVersion.name +} + +// adding this as an implementation dependency here allows usage in our convention plugin +dependencies { + implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.3") +} + +tasks.publish { + dependsOn("check") +} + +version = "0.1.0" + +def shouldPublish = { + def latest = getLatestMavenPackageVersion("nasa-ammos", "gov.nasa.ammos.aerie.procedural.plugin", System.getenv("GITHUB_TOKEN")) + println "latest published procedural plugin is ${latest}" + println "current procedural plugin is ${version}" + def should = latest != version + if (should) { + println "publishing..." + } else { + println "not publishing..." + } + return should +} + +tasks.withType(PublishToMavenRepository).configureEach { it.enabled = shouldPublish() } + +publishing { + publications { + pluginMaven(MavenPublication) + } + repositories { + maven { + name="GitHubPackages" + url="https://maven.pkg.github.com/nasa-ammos/aerie" + version=version + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } +} diff --git a/procedural/plugin/src/main/groovy/gov.nasa.ammos.aerie.procedural.plugin.gradle b/procedural/plugin/src/main/groovy/gov.nasa.ammos.aerie.procedural.plugin.gradle new file mode 100644 index 0000000000..d317f79237 --- /dev/null +++ b/procedural/plugin/src/main/groovy/gov.nasa.ammos.aerie.procedural.plugin.gradle @@ -0,0 +1,75 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +// afterEvaluate ensures these dependencies are passed to projects applying this plugin, not this plugin itself +project.afterEvaluate { + dependencies { + annotationProcessor findProject(':procedural:processor') ?: "gov.nasa.ammos.aerie.procedural:processor:${findProperty("aerieVersion")}" + implementation findProject(':procedural:constraints') ?: "gov.nasa.ammos.aerie.procedural:constraints:${findProperty("aerieVersion")}" + implementation findProject(':procedural:scheduling') ?: "gov.nasa.ammos.aerie.procedural:scheduling:${findProperty("aerieVersion")}" + implementation findProject(':procedural:timeline') ?: "gov.nasa.ammos.aerie.procedural:timeline:${findProperty("aerieVersion")}" + implementation findProject(':merlin-driver') ?: "gov.nasa.jpl.aerie:merlin-driver:${findProperty("aerieVersion")}" + implementation findProject(':type-utils') ?: "gov.nasa.jpl.aerie:type-utils:${findProperty("aerieVersion")}" + implementation findProject(':contrib') ?: "gov.nasa.jpl.aerie:contrib:${findProperty("aerieVersion")}" + } +} + +tasks.register('buildAllSchedulingProcedureJars') { + group = 'SchedulingProcedureJars' + + dependsOn "generateSchedulingProcedureJarTasks" + dependsOn { + tasks.findAll { task -> task.name.startsWith('buildSchedulingProcedureJar_') } + } +} + +tasks.create("generateSchedulingProcedureJarTasks") { + group = 'SchedulingProcedureJars' + + final proceduresDir = findFirstMatchingBuildDir("generated/procedures") + + if (proceduresDir == null) { + println "No generated procedures folder found" + return + } + println "Generating jar tasks for the following procedures directory: ${proceduresDir}" + + final files = file(proceduresDir).listFiles() + if (files.length == 0) { + println "No procedures available within folder ${proceduresDir}" + return + } + + files.toList().each { file -> + final nameWithoutExtension = file.name.replace(".java", "") + final jarPath = nameWithoutExtension + ".jar" + final taskName = "buildSchedulingProcedureJar_${nameWithoutExtension}" + + println "Generating ${taskName} task, which will build ${jarPath}" + + tasks.create(taskName, ShadowJar) { + group = 'SchedulingProcedureJars' + configurations = [project.configurations.runtimeClasspath] + from sourceSets.main.output + archiveFileName = jarPath + manifest { + attributes 'Main-Class': getMainClassFromGeneratedFile(file) + } + minimize() + } } +} + +private String findFirstMatchingBuildDir(String pattern) { + String found = null + final generatedDir = file("build/generated/sources") + generatedDir.mkdirs() + generatedDir.eachDirRecurse { dir -> if (dir.path.contains(pattern)) found = dir.path } + return found +} + +private static String getMainClassFromGeneratedFile(File file) { + final fileString = file.toString() + final prefix = "build/generated/sources/annotationProcessor/java/main/" + final index = fileString.indexOf(prefix) + prefix.length() + final trimmed = fileString.substring(index).replace(".java", "") + return trimmed.replace("/", ".") +} diff --git a/settings.gradle b/settings.gradle index 1240589c6b..f6fd960385 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,16 @@ +pluginManagement { + repositories { + maven { + name = "GitHubPackages" + url = "https://maven.pkg.github.com/NASA-AMMOS/aerie" + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + gradlePluginPortal() + } +} rootProject.name = 'aerie' // Mission Model support @@ -19,6 +32,9 @@ include 'procedural:constraints' include 'procedural:scheduling' include 'procedural:remote' +// Procedural gradle plugins +include 'procedural:plugin' + // Procedural examples include 'procedural:examples:foo-procedures'