diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 000000000..9b57c62ce --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,186 @@ +:toc: + +# Contributors Guide + +## Local Environment + +.Tools +|=== +| Tool | Purpose + +| https://gradle.org[Gradle] +| Used to run unit tests, package the JPI, and publish the plugin + +| https://github.com/casey/just[Just] +| A task runner. Used here to automate common commands used during development. + +| https://www.docker.com/get-started[Docker] +| Used to build the documentation for local preview + +|=== + + +## Running Tests + +To run all the tests, run: + +[source,bash] +---- +just test +---- + +The gradle test report is published to `build/reports/tests/test/index.html` + +### Execute tests for a specific class + +To run tests for a specific Class, `StepWrapperSpec` for example, run: + +[source,bash] +---- +just test '*.StepWrapperSpec' +---- + +### Code Coverage + +By default, JaCoCo code coverage is enabled when running test. + +Once executed, the JaCoCo coverage report can be found at: `build/reports/jacoco/test/html/index.html` + +To disable this, run: + +[source, bash] +---- +just --set coverage false test +---- + +## Linting + +This project uses https://github.com/diffplug/spotless[Spotless] and https://github.com/CodeNarc/CodeNarc[CodeNarc] to perform linting. The CodeNarc rule sets for `src/main` and `src/test` can be found in `config/codenarc/rules.groovy` and `config/codenarc/rulesTest.groovy`, respectively. + +Once executed, the reports can be found at `build/reports/codenarc/main.html` and `build/reports/codenarc/test.html`. + +To execute linting, run: + +[source,groovy] +---- +just lint +---- + +## Building the JPI + +To build the JPI, run: + +[source, bash] +---- +just jpi +---- + +Once built, the JPI will be located at `build/libs/templating-engine.jpi` + +## Building the Documentation + +This project uses https://antora.org/[Antora] to build the documentation. + +To build the documentation, run: + +[source, bash] +---- +just docs +---- + +Once built, the documentation can be viewed at `docs/html/index.html` + +### Customizing the documentation output directory + +The `docsDir` justfile variable configures the output directory. + +To modify the output directory, run: + +[source, bash] +---- +just --set docsDir some/other/directory docs +---- + +## Publishing JTE + +**If you have the permission**, you can cut a new release of JTE by running `just release `. + +For example: + +[source, bash] +---- +just release 2.0.4 +---- + +This will: + +1. create a `release/2.0.4` branch +2. update the version in the `build.gradle` +3. update the version in the `docs/antora.yml` +4. push those changes +5. create a `2.0.4` tag +6. publish the JPI + +[NOTE] +==== +Don't forget to go to the https://github.com/jenkinsci/templating-engine-plugin/releases[Releases Page] to officially release JTE with the current change log based off the most recent tag. +==== + +## Run a containerized Jenkins + +It is often helpful to run Jenkins in a container locally to test various scenarios with JTE during development. + +[source, bash] +---- +just run +---- + +With the default settings, this will expose jenkins on http://localhost:8080 + +### Change the container name + +[source, bash] +---- +just --set container someName run +---- + +### Change the port forwarding target + +[source, bash] +---- +just --set port 9000 run +---- + +### Pass arbitrary flags to the container + +Parameters passed to `just run` are sent as flags to the `docker run` command. + +[source, bash] +---- +just run -e SOMEVAR="some var" +---- + +### Mounting local libraries for testing + +Local directories can be configured as Git SCM library sources even if they do not have a remote repository. + +For example, if `~/local-libraries` is a directory containing a local git repository then to mount it to the container you would run: + +[source, bash] +---- +just run -v ~/local-libraries:/local-libraries +---- + +You could then configure a library source using the file protocol to specify the repository location at `file:///local-libraries` + +[TIP] +==== +When using this technique, changes to the libraries must be committed to be found. In a separate terminal, run: + +[source, bash] +---- +just watch ~/local-libraries +---- + +to automatically commit changes to the libraries. +==== diff --git a/Justfile b/Justfile new file mode 100644 index 000000000..dce3e5376 --- /dev/null +++ b/Justfile @@ -0,0 +1,91 @@ +# when false, disables code coverage +coverage := "true" +# the output directory for the documentation +docsDir := "docs/html" +# the Antora playbook file to use when building docs +playbook := "docs/antora-playbook-local.yml" +# variables for running containerized jenkins +container := "jenkins" # the name of the container +port := "8080" # the port to forward Jenkins to + +# describes available recipes +help: + just --list --unsorted + +# wipe local caches +clean: + ./gradlew clean + rm -rf {{docsDir}} + +# Run unit tests +test class="*": + #!/usr/bin/env bash + set -euxo pipefail + coverage=$([[ {{coverage}} == "true" ]] && echo "jacocoTestReport" || echo "") + ./gradlew test --tests '{{class}}' $coverage + +# Run spotless & codenarc +lint: + ./gradlew spotlessApply codenarc + +# Build the JPI +jpi: + ./gradlew clean jpi + +# executes the CI checks (test lint jpi) +ci: test lint jpi + +# Build the local Antora documentation +docs: + docker run \ + -it --rm \ + -v ~/.git-credentials:/home/antora/.git-credentials \ + -v $(pwd):/app -w /app \ + docker.pkg.github.com/boozallen/sdp-docs/builder \ + generate --generator booz-allen-site-generator \ + --to-dir {{docsDir}} \ + {{playbook}} + +# publishes the jpi +release version branch=`git branch --show-current`: + #!/usr/bin/env bash + if [[ ! "{{branch}}" == "main" ]]; then + echo "You can only cut a release from the 'main' branch." + echo "Currently on branch '{{branch}}'" + exit 1 + fi + # cut a release branch + git checkout -B release/{{version}} + # bump the version in relevant places + sed -ie "s/^version.*/version = '{{version}}'/g" build.gradle + sed -ie "s/^version:.*/version: '{{version}}'/g" docs/antora.yml + git add build.gradle docs/antora.yml + git commit -m "bump version to {{version}}" + git push --set-upstream origin release/{{version}} + # push a tag for this release + git tag {{version}} + git push origin refs/tags/{{version}} + # publish the JPI + ./gradlew publish + +# run a containerized jenkins instace +run flags='': + docker pull jenkins/jenkins:lts + docker run -d \ + --publish {{port}}:8080 \ + --name {{container}} \ + {{flags}} \ + jenkins/jenkins:lts + +# swap the JPI in a running container and restart +reload: + if [ ! "$(docker ps -qaf name={{container}})" ]; then echo "container '{{container}}' not found'" && exit 1; fi + if [ ! "$(docker ps -qaf name={{container}} -f status=running)" ]; then docker start {{container}}; fi + just jpi + docker exec -it {{container}} /bin/bash -c "rm -rf /var/jenkins_home/plugins/templating-engine{,.*}" + docker cp build/libs/templating-engine.hpi {{container}}:/var/jenkins_home/plugins/templating-engine.hpi + docker restart {{container}} + +# watches the given path to commit all changes as they occur +watch path: + watchexec 'cd {{path}} && git add -A && git commit -m "update"' -w {{path}} diff --git a/Makefile b/Makefile deleted file mode 100644 index a672de89d..000000000 --- a/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -# Minimal makefile to build Antora documentation -BUILDDIR = docs/html -PLAYBOOK = docs/antora-playbook-local.yml - -# Put it first so that "make" without argument is like "make help". -help: ## Show target options - @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' - -clean: ## removes remote documentation and compiled documentation - rm -rf $(BUILDDIR) - -.PHONY: docs -docs: clean ## builds the antora documentation - docker run \ - -t --rm \ - -v ~/.git-credentials:/home/antora/.git-credentials \ - -v $(shell pwd):/app -w /app \ - docker.pkg.github.com/boozallen/sdp-docs/builder \ - generate --generator booz-allen-site-generator \ - --to-dir $(BUILDDIR) \ - $(PLAYBOOK) - -jpi: ## builds the jpi via gradle - ./gradlew clean jpi - -test: ## runs the plugin's test suite - ./gradlew clean test - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - echo "Make command $@ not found" \ No newline at end of file diff --git a/docs/modules/developer/nav.adoc b/docs/modules/developer/nav.adoc index b2c4e5c30..8f4ec729f 100644 --- a/docs/modules/developer/nav.adoc +++ b/docs/modules/developer/nav.adoc @@ -1,2 +1,3 @@ * xref:index.adoc[Developer Documentation] +** xref:CONTRIBUTING.adoc[Contributing] ** xref:initialization.adoc[Initialization Process] \ No newline at end of file diff --git a/docs/modules/developer/pages/CONTRIBUTING.adoc b/docs/modules/developer/pages/CONTRIBUTING.adoc new file mode 120000 index 000000000..b58c1b761 --- /dev/null +++ b/docs/modules/developer/pages/CONTRIBUTING.adoc @@ -0,0 +1 @@ +../../../../CONTRIBUTING.adoc \ No newline at end of file diff --git a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/ReservedVariableName.groovy b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/ReservedVariableName.groovy index f8d4cdeb2..9f7ac8d55 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/ReservedVariableName.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/ReservedVariableName.groovy @@ -39,12 +39,14 @@ abstract class ReservedVariableName implements ExtensionPoint{ abstract String getExceptionMessage() - void throwPreLockException() throws Exception{ - throw new Exception(getExceptionMessage()) + void throwPreLockException(String msg) throws Exception{ + msg += getExceptionMessage() + throw new Exception(msg) } - void throwPostLockException() throws Exception{ - throw new Exception(getExceptionMessage()) + void throwPostLockException(String msg) throws Exception{ + msg += getExceptionMessage() + throw new Exception(msg) } } diff --git a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBinding.groovy b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBinding.groovy index 5218c93ca..83fbf9fd0 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBinding.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBinding.groovy @@ -50,7 +50,8 @@ class TemplateBinding extends Binding implements Serializable{ setVariable(STEPS, new DSL(owner)) /** * for jte namespace, we need to bypass the exception throwing logic - * that would be triggered by "jte" as a ReservedVariableName + * that would be triggered by "jte" as a ReservedVariableName which is + * why we use `variables.put()` instead of `setVariable()` */ variables.put(registry.getVariableName(), registry) } @@ -86,17 +87,23 @@ class TemplateBinding extends Binding implements Serializable{ if (!collisionTarget) { throw new JTEException("Something weird happened. Unable to determine source of binding collision.") } + String preface + if(value in TemplatePrimitive){ + preface = "Failed to create ${value.getDescription()}. " + } else { + preface = "Failed to create variable '${name}' with value ${value}. " + } if (locked) { // during pipeline execution: // always throw exceptions if overriding during pipeline execution // i.e., a template or library inadvertently create a variable // that collides - collisionTarget.throwPostLockException() + collisionTarget.throwPostLockException(preface) } else if (!permissiveInitialization || reservedVar) { // during initialization: // throw an exception if the initialization mode is strict // always throw exception if the collision target is a reserved variable - collisionTarget.throwPreLockException() + collisionTarget.throwPreLockException(preface) } } @@ -116,6 +123,13 @@ class TemplateBinding extends Binding implements Serializable{ throw new MissingPropertyException(name, this.getClass()) } + /** + * The registry tracks TemplatePrimitives that have been + * put into the binding. When `jte.permissive_initialization` is + * set to true, there may be multiple primitives with the same + * name. When that's the case, JTE requires that each primitive + * be accessed via its long-name using primitive namespacing. + */ List primitives = registry.getPrimitivesByName(name) if(primitives.size() >= 2 && locked){ List msg = [ diff --git a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplatePrimitive.groovy b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplatePrimitive.groovy index 88d2fb4d7..009971f67 100644 --- a/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplatePrimitive.groovy +++ b/src/main/groovy/org/boozallen/plugins/jte/init/primitives/TemplatePrimitive.groovy @@ -34,12 +34,12 @@ abstract class TemplatePrimitive implements Serializable{ /** * Invoked if an object with this class were to be overridden in the {@link TemplateBinding} during initialization */ - abstract void throwPreLockException() + abstract void throwPreLockException(String preface) /** * Invoked if an object with this class were to be overridden in the {@link TemplateBinding} after initialization */ - abstract void throwPostLockException() + abstract void throwPostLockException(String preface) /** * Returns the injector that creates the primitive @@ -59,4 +59,14 @@ abstract class TemplatePrimitive implements Serializable{ */ abstract String getName() + /** + * Returns the user-facing description of what this primitive is + *

+ * examples: + * - Library Step 'build' from the 'maven' library + * - Stage 'continuous_integration' + * @return the user-facing description of the primitive + */ + abstract String getDescription() + } diff --git a/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/ApplicationEnvironment.groovy b/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/ApplicationEnvironment.groovy index 117b4d48b..24299a8fc 100644 --- a/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/ApplicationEnvironment.groovy +++ b/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/ApplicationEnvironment.groovy @@ -66,6 +66,7 @@ class ApplicationEnvironment extends TemplatePrimitive implements Serializable{ this.config = config.asImmutable() } + @NonCPS @Override String getDescription(){ return "Application Environment '${name}'" } @NonCPS @Override String getName(){ return name } @NonCPS @Override Class getInjector(){ return ApplicationEnvironmentInjector } @@ -80,12 +81,14 @@ class ApplicationEnvironment extends TemplatePrimitive implements Serializable{ } @NonCPS - void throwPreLockException(){ - throw new TemplateException ("Application Environment ${name} already defined.") + void throwPreLockException(String msg){ + msg += "Application Environment ${name} already defined." + throw new TemplateException(msg) } - void throwPostLockException(){ - throw new TemplateException ("Variable ${name} is reserved as an Application Environment.") + void throwPostLockException(String msg){ + msg += "Variable ${name} is reserved as an Application Environment." + throw new TemplateException(msg) } } diff --git a/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/Keyword.groovy b/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/Keyword.groovy index e564ea49b..d58f8b231 100644 --- a/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/Keyword.groovy +++ b/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/Keyword.groovy @@ -32,6 +32,7 @@ class Keyword extends TemplatePrimitive implements Serializable{ String preLockException = "Variable ${name} already exists as a Keyword." String postLockException = "Variable ${name} is reserved as a template Keyword." + @NonCPS @Override String getDescription(){ return "Keyword '${name}'" } @NonCPS @Override String getName(){ return name } @NonCPS @Override Class getInjector(){ return injector } @@ -40,12 +41,14 @@ class Keyword extends TemplatePrimitive implements Serializable{ } @NonCPS - void throwPreLockException(){ - throw new TemplateException(preLockException) + void throwPreLockException(String msg){ + msg += preLockException + throw new TemplateException(msg) } - void throwPostLockException(){ - throw new TemplateException(postLockException) + void throwPostLockException(String msg){ + msg += postLockException + throw new TemplateException(msg) } } diff --git a/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/Stage.groovy b/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/Stage.groovy index b48e4c5a4..06bb762dc 100644 --- a/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/Stage.groovy +++ b/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/Stage.groovy @@ -42,6 +42,7 @@ class Stage extends TemplatePrimitive implements Serializable{ this.steps = steps } + @NonCPS @Override String getDescription(){ return "Stage '${name}'" } @NonCPS @Override String getName(){ return name } @NonCPS @Override Class getInjector(){ return injector } @@ -64,12 +65,14 @@ class Stage extends TemplatePrimitive implements Serializable{ } @NonCPS - void throwPreLockException(){ - throw new TemplateException ("The Stage ${name} is already defined.") + void throwPreLockException(String msg){ + msg += "The Stage ${name} is already defined." + throw new TemplateException(msg) } - void throwPostLockException(){ - throw new TemplateException ("The variable ${name} is reserved as a template Stage.") + void throwPostLockException(String msg){ + msg += "The variable ${name} is reserved as a template Stage." + throw new TemplateException(msg) } } diff --git a/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/StepWrapper.groovy b/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/StepWrapper.groovy index e8e357614..12f1e299d 100644 --- a/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/StepWrapper.groovy +++ b/src/main/resources/org/boozallen/plugins/jte/init/primitives/injectors/StepWrapper.groovy @@ -91,6 +91,7 @@ class StepWrapper extends TemplatePrimitive implements Serializable, Cloneable{ */ private HookContext hookContext + @NonCPS @Override String getDescription(){ return "Library Step '${name}' from the '${library}' library" } @NonCPS @Override String getName(){ return name } @NonCPS String getLibrary(){ return library } @NonCPS @Override Class getInjector(){ return injector } @@ -179,12 +180,14 @@ class StepWrapper extends TemplatePrimitive implements Serializable, Cloneable{ } @NonCPS - void throwPreLockException(){ - throw new TemplateException ("Library Step Collision. The step ${name} already defined via the ${library} library.") + void throwPreLockException(String msg){ + msg += "The step '${name}' already defined via the '${library}' library." + throw new TemplateException(msg) } - void throwPostLockException(){ - throw new TemplateException ("Library Step Collision. The variable ${name} is reserved as a library step via the ${library} library.") + void throwPostLockException(String msg){ + msg += "The variable '${name}' is reserved as a library step via the '${library}' library." + throw new TemplateException(msg) } /** diff --git a/src/test/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBindingSpec.groovy b/src/test/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBindingSpec.groovy index b481f3bec..349009b3f 100644 --- a/src/test/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBindingSpec.groovy +++ b/src/test/groovy/org/boozallen/plugins/jte/init/primitives/TemplateBindingSpec.groovy @@ -43,14 +43,17 @@ class TemplateBindingSpec extends Specification{ String name Class injector + @NonCPS @Override String getDescription(){ return "Test Primitive ${name}" } @NonCPS @Override String getName(){ return name } @NonCPS @Override Class getInjector(){ return injector } - void throwPreLockException(){ + @SuppressWarnings("UnusedMethodParameter") + void throwPreLockException(String msg){ throw new TemplateException ("pre-lock exception") } - void throwPostLockException(){ + @SuppressWarnings("UnusedMethodParameter") + void throwPostLockException(String msg){ throw new TemplateException ("post-lock exception") } @@ -304,33 +307,33 @@ class TemplateBindingSpec extends Specification{ def "application env as argument for stage context"(){ given: String template = """ -broadway dev -""" + broadway dev + """ String config = """ -jte{ - permissive_initialization = true -} + jte{ + permissive_initialization = true + } -application_environments { - dev{ - long_name = "development" - } -} + application_environments { + dev{ + long_name = "development" + } + } -stages{ - broadway{ - temp_meth1 - } -} + stages{ + broadway{ + temp_meth1 + } + } -template_methods{ - temp_meth1 -} -""" + template_methods{ + temp_meth1 + } + """ WorkflowJob job = TestUtil.createAdHoc(jenkins, - template: template, - config: config + template: template, + config: config ) expect: @@ -339,32 +342,30 @@ template_methods{ def "permissive mode binding collision with ReservedVariable (stageContext) pre-lock throws pre-lock exception"(){ given: - String template = """ -broadway -""" + String template = "broadway" String config = """ -jte{ - permissive_initialization = true -} + jte{ + permissive_initialization = true + } -stages{ - broadway{ - temp_meth1 - } -} + stages{ + broadway{ + temp_meth1 + } + } -keywords{ - stageContext = "x" -} + keywords{ + stageContext = "x" + } -template_methods{ - temp_meth1 -} -""" + template_methods{ + temp_meth1 + } + """ WorkflowJob job = TestUtil.createAdHoc(jenkins, - template: template, - config: config + template: template, + config: config ) expect: