Repository: ploigos/ploigos-step-runner (language: python)

Issue:
Add mvn note to development/test section in README
# Purpose

Project should call out that mvn is required in development setup.

Following dev setup instructions and running `tox test` will result in an error for `tests/step_implementers/generate_metadata/test_maven_generate_metadata.py::TestStepImplementerMavenGenerateMetadata::test_run_step_pass` if the `mvn` command isn't available.

```
E       sh.CommandNotFound: mvn

.venv/lib/python3.9/site-packages/sh.py:3457: CommandNotFound
```

Might save somebody time troubleshooting 🤷

# Breaking?

No


Discussion / hints:
# [Codecov](https://codecov.io/gh/ploigos/ploigos-step-runner/pull/289?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ploigos) Report
> Merging [#289](https://codecov.io/gh/ploigos/ploigos-step-runner/pull/289?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ploigos) (e557058) into [main](https://codecov.io/gh/ploigos/ploigos-step-runner/commit/d0a7f54c7895c252f40b9625decb9b69de0147c9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ploigos) (d0a7f54) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff            @@
##              main      #289   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          100       100           
  Lines         4202      4202           
=========================================
  Hits          4202      4202           
```

| Flag | Coverage Δ | |
|---|---|---|
| pytests | `100.00% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ploigos#carryforward-flags-in-the-pull-request-comment) to find out more.


Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ploigos). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ploigos)


Fix patch:
diff --git a/.gitignore b/.gitignore
index a6be164c..c2a968f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ step-runner-results
 step-runner-working
 results.yml/
 .venvs
+.venv
 
 # Eclipse
 .project
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..9c6f5642
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,185 @@
+# Contributing
+
+## Fork and Clone Repo
+
+Fork this repo under your GitHub account. After forking, clone the fork to your
+local machine: (replace *your-username*)
+
+```bash
+git clone https://github.com/your-username/ploigos-step-runner.git
+```
+
+## Set Up Development Environment
+
+> :warning: **If you are running RHEL7 or older versions of Python**: This project will need Python 3.3 or better to run. If you are running on RHEL7, you can invoke `python3` in place of `python` in the following commands.
+
+```bash
+cd ploigos-step-runner
+python -m venv .venvs/psr-dev
+source .venvs/psr-dev/bin/activate
+python -m pip install --upgrade pip
+python -m pip install -e '.[tests]'
+```
+
+You will need to run `source .venvs/psr-dev/bin/activate` to activate the
+development environment each time you start working in a new terminal session.
+
+## Run Tests
+
+> :notebook: Some tests require `mvn` command in your PATH. Install with brew/yum/dnf prior to running tests.
+
+```bash
+tox -e test
+```
+
+Or to run for just a particular implementer, and include the sections of code that you didn't cover
+
+```bash
+python3 -m pytest --cov --cov-report term-missing tests/step_implementers/package/test_maven_package.py
+```
+
+## Run linter
+
+```bash
+tox -e lint
+```
+
+## Run linter and all tests (a good idea before a commit)
+
+```bash
+tox
+```
+
+## Generate the Documentation Locally
+
+If you are updating the python documentation and want to generate locally this is how you do it.
+
+```bash
+tox -e docs
+```
+
+## Adding a New Step Implementer
+
+The easiest way to add a new step implementer is to copy an existing step
+implementer. The "example" step and it's implementers were created as examples
+for developers to copy and create their own implementers.
+
+The source code for step implementers is under
+`./src/ploigos_step_runner/step_implementers/`. Each sub-directory represents a different step.
+Each python files under a sub-directory is a step implementer for that step.
+For example, `./src/ploigos_step_runner/step_implementers/unit-test/npm_test.py` is a step implementer
+for the unit-test step that knows how to use the npm command to run unit tests.
+
+Step implementers under the `./src/ploigos_step_runner/step_implementers/examples/` directory
+are associated with the special "examples" step, which is not part of any workflow but is used
+to organize and run example code. You can copy these examples as the basis for your own
+step implementers.
+
+
+**To add a new step implementer:**
+
+1. Find example code you want to start with. `hello_shell.py` in the `examples` directory is a good
+starting point if your step implementer will run an external command. You could also copy a real
+step implementer as a starting point or start from scratch.
+
+2. Copy the example into a new python file under the appropriate sub directory. For example, if you
+are writing a step implementer for the package step that will compile intercal code, you should put it
+under the `package` directory and name it something like `intercal_package.py`.
+
+3. Create a file for your unit tests. You can copy the unit tests for the example. Put it in the
+appropriate sub directory of `./tests/`. The directory structure matches `./src/`. Start the file name
+with 'test_', for example 'test_intercal_package.py`. If you like Test Driven Development, you can copy
+the structure of the test file and delete all the of old tests, writing new ones as you code.
+
+4. Modify the python code to implement the logic of your step. Continuing the intercal example, you
+would have to change the example code that runs the "echo" shell command and make it run the intercal
+compiler instead. 
+
+5. Be sure to run the unit tests and linter. **PRs are not merged until the unit tests and lint checks pass
+and unit test coverage is 100%**. If you are having trouble writing the unit tests, feel free to open a
+draft PR and ask for help!
+
+6. During development, it is often useful to test the step by running the psr command directly. This saves
+time because you do not have to run a full pipeline to see if your python code behaves as expected outside
+of the unit tests. The next section explains how to do that.
+
+7. Once the new step implementer is ready, [create a Pull Request](https://github.com/ploigos/ploigos-step-runner/compare)
+to contribute your code to the upstream project!
+
+
+## Testing a Step Implementer by Running the psr Command
+
+To run the HelloShell example step implementer:
+
+1. Create a *psr.yaml* file:
+```bash
+cat > psr.yaml << EOF
+step-runner-config:
+
+  examples:
+    - implementer: HelloShell
+      config:
+        greeting-name: Folks
+EOF
+```
+
+2. Run PSR:
+The "examples" step is a special step used to execute example StepImplementers. It takes the place
+of a real step like "unit-test" or "create-container-image".
+```bash
+psr -s examples -c psr.yaml
+```
+
+3. Validate the output, both stdout and artifacts sections.
+
+4. Delete the PSR working directory:
+
+```bash
+rm -r ./step-runner-working
+```
+
+> :warning: This step is important. If you need to re-run hello-world (or any
+> step that has been run), you must remove the directory
+> `./step-runner-working`. See [Troubleshooting](#troubleshooting).
+
+5. To execute a different implementer for the examples step, replace *psr.yaml*:
+```bash
+cat > psr.yaml << EOF
+step-runner-config:
+
+  examples:
+    - implementer: HelloWorld
+      config:
+        greeting-name: Folks
+EOF
+```
+
+This will use the simpler HelloWorld implementer instead of HelloShell. Re-run PSR
+and validate the output.
+
+
+## Sample Applications
+
+When developing a step implementer, it is often useful to have a real application
+that you can run the psr against. We maintain several specifically for that purpose:
+* [spring-petclinic](https://github.com/ploigos/spring-petclinic/) is a Java application that uses Maven.
+* [reference-nodejs-npm](https://github.com/ploigos-reference-apps/reference-nodejs-npm) is a NodeJS application that uses NPM.
+* [dotnet-app](https://github.com/ploigos/dotnet-app/) is a .NET application.
+
+If you are testing another language or framework, use an application written for that language.
+
+> :notebook: If you're testing using a different repo, make sure to create a
+psr.yaml file in that root of that repo for PSR configurations.
+
+## Troubleshooting
+
+> Fatal error calling step (generate-metadata): Can not add duplicate StepResult for step (generate-metadata), sub step (Maven), and environment (None)
+
+This error (and similar duplicate step errors) occurs when a step that was previously run is re-run. PSR expects a given step and sub-step to run only once. In an actual pipeline, this indicates an error. When developing step implementers, it is common to trigger this error when re-running steps to test changes.
+
+To fix this, remove the ./step-runner-working directory:
+
+```bash
+rm -r ./step-runner-working
+```
+
diff --git a/README.md b/README.md
index e665a489..e5d4fbbe 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,34 @@
 [![License](https://img.shields.io/github/license/ploigos/ploigos-step-runner?color=informational)](LICENSE)
 
 # ploigos-step-runner
-Ploigos Step Runner (PSR) implemented as a Python library.
+
+Ploigos Step Runner (PSR) is a middleware tool to execute Continuous
+Integration (CI) pipeline steps.  **PSR is not a pipeline!** It is a CLI tool
+called in each stage of a pipeline as a command: `psr -s package -c psr.yaml`.
+
+PSR is tech stack agnostic and runs on workflow runners like Jenkins, Tekton,
+GitHub Actions, GitLab CI, or any other workflow runner that can execute
+commands.
+
+PSR abstracts the implementation of a *step* from a specific product or
+solution. A few common steps most pipelines implement are *unit testing*,
+*packaging*, and *deploying*. The implementation of these steps differs between
+a Java and JavaScript application. PSR abstracts implementation from the
+pipeline definition, allowing the same pipeline definition to be reused for all
+applications, regardless of the application language and framework.
+
+Each PSR step has one or more *Step Implementers* that are executed by PSR from
+a pipeline. A step implementer does two things:
+
+1. Integrates with a given product or solution
+2. Produces output in a standard format that can be used or validated by other
+pipeline steps
+
+Available PSR steps can be found
+[here](https://ploigos.github.io/ploigos-step-runner/#step-configuration).
+
+An end-to-end example pipeline that uses PSR is documented
+[here](docs/end-to-end.md).
 
 ## Documentation
 
@@ -18,52 +45,34 @@ Ploigos Step Runner (PSR) implemented as a Python library.
 ## Install
 
 Latest Release
+
 ```bash
 pip install ploigos-step-runner
 ```
 
 Latest Development Release
+
 ```bash
 pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple ploigos-step-runner
 ```
 
-## Development
+## FAQ
 
-> :warning: **If you are running RHEL7 or older versions of Python**: This project will need Python 3.3 or better to run. If you are running on RHEL7, you can invoke `python3` in place of `python` in the following commands.
+> What is a Workflow Runner?
 
-### Set Up Development Environment
-```bash
-cd ploigos-step-runner
-python -m venv .venvs/psr-dev
-source .venvs/psr-dev/bin/activate
-python -m pip install --upgrade pip
-python -m pip install -e '.[tests]'
-```
+A workflow runner is a Continuous Integration (CI) tool that executes pipelines. Common workflow runners are Jenkins, Tekton, GitHub Actions, and GitLab CI.
 
-### Run Tests
-```bash
-tox -e test
-```
+> What if I want to use a tool that PSR doesn't have an step implementer for?
 
-Or to run for just a particular implementer, and include the sections of code that you didn't cover
+Each PSR step has a corresponding *step_implementer* Python module under
+[src/ploigos_step_runner/step_implementers](src/ploigos_step_runner/step_implementers).
+If you need to use a tool that PSR does not have a step implementer for, you
+can create one or open a GitHub Issue to request an enhancement. See [CONTRIBUTING.md](CONTRIBUTING.md) for
+instructions on creating step implementers.
 
-```bash
-python3 -m pytest --cov --cov-report term-missing tests/step_implementers/package/test_maven_package.py
-```
+An AdHoc step implementer is proposed in #131 that would allow for a command or
+script to be specified in `psr.yaml`.
 
-### Run linter
-```bash
-tox -e lint
-```
+## Contributing
 
-### Run linter and all tests (a good idea before a commit)
-```bash
-tox
-```
-
-### Generate the Documentation Locally
-If you are updating the python documentation and want to generate locally this is how you do it.
-
-```bash
-tox -e docs
-```
+See [CONTRIBUTING.md](CONTRIBUTING.md)
diff --git a/docs/end-to-end.md b/docs/end-to-end.md
new file mode 100644
index 00000000..529d0270
--- /dev/null
+++ b/docs/end-to-end.md
@@ -0,0 +1,238 @@
+# End-to-End Ploigos Step Runner (PSR) Example
+
+Below is an end-to-end example pipeline that leverages PSR.
+
+## Example Scenario
+
+- We have two applications
+    - First is a Java Spring backend API built with Maven
+    - Second is a JavaScript React frontend built with NPM
+- We want to build a pipeline that has 5 stages
+    - Generate Metadata
+    - Unit test
+    - Package
+    - Create Container Image
+    - Push Container Image
+- We want to use Jenkins as the workflow runner
+
+## Step 1: Configure *psr.yaml* for Maven
+
+Create a *psr.yaml* file in the source code repo for the Spring application:
+
+```yaml
+step-runner-config:
+
+  global-defaults:
+    organization: ploigos
+    service-name: java-app
+    application-name: java-app
+
+  generate-metadata:
+  - implementer: Maven
+  - implementer: Git
+  - implementer: SemanticVersion
+
+  unit-test:
+    - implementer: MavenTest
+
+  package:
+  - implementer: MavenPackage
+
+  create-container-image:
+  - implementer: Buildah
+    config:
+      containers-config-auth-file: /var/jenkins_home/container-auth.json
+
+  push-container-image:
+  - implementer: Skopeo
+    config:
+      destination-url: docker-nexus.apps.mycluster.io
+      container-image-push-repository: ploigos/java-app
+```
+
+> :notebook: The order in psr.yaml does not matter. Additionally, even if a
+> step appears in psr.yaml, the step must be explicitly called by the workflow
+> runner. psr.yaml contains configurations for your steps when they are
+> executed by the pipeline. The pipeline definition for your workflow runner
+> will dictate what steps are called and in what order.
+
+## Step 2: Configure *psr.yaml* for NPM
+
+Create a *psr.yaml* file in the source code repo for the NPM application:
+
+```yaml
+step-runner-config:
+
+  global-defaults:
+    organization: ploigos
+    service-name: javascript-app
+    application-name: javascript-app
+
+  generate-metadata:
+  - implementer: Npm
+  - implementer: Git
+  - implementer: SemanticVersion
+
+  unit-test:
+    - implementer: NpmTest
+
+  package:
+  - implementer: NpmPackage
+
+  create-container-image:
+  - implementer: Buildah
+    config:
+      containers-config-auth-file: /var/jenkins_home/container-auth.json
+
+  push-container-image:
+  - implementer: Skopeo
+    config:
+      destination-url: docker-nexus.apps.mycluster.io
+      container-image-push-repository: ploigos/javascript-app
+
+```
+
+## Step 3: Create a Pipeline using any CI/CD Workflow Runner
+
+PSR runs inside of a CI/CD pipeline, but it does not orchestrate the pipeline.
+You need another tool for that, and PSR does not care which one you choose.
+Portability between CI/CD workflow runners is one benefit of the PSR. For this
+example we will create a simple Jenkins pipeline.
+
+> :notebook: This is a simple example. Real-world pipelines are usually a bit more
+> complex. For examples of production-quality pipelines that you can copy, see our
+> tool-specific libraries:
+> * [GitHub Actions](https://github.com/ploigos/ploigos-github-workflows/)
+> * [GitLab](https://github.com/ploigos/ploigos-gitlab-library/)
+> * [Jenkins](https://github.com/ploigos/ploigos-jenkins-library/)
+> * [Tekton](https://github.com/ploigos/ploigos-charts/)
+
+Jenkins will call the PSR command with two options:
+
+- `-s <step>` is the name of the step that will be executed
+- `-c psr.yaml` is the path to the PSR configuration file for the application
+
+Create a Jenkinsfile in both source code repos that contains the same pipeline
+definition:
+
+```Jenkinsfile
+pipeline {
+   agent {
+        kubernetes {
+            cloud 'openshift'
+            defaultContainer 'default'
+            yamlFile 'kubernetes-pod.yaml'
+        }
+    }
+    stages {
+        stage('Generate Metadata') {
+            steps {
+                sh "psr -s generate-metadata -c psr.yaml"
+            }
+        }
+        stage('Unit Test') {
+            steps {
+                sh "psr -s unit-test -c psr.yaml"
+            }
+        }
+        stage('Package') {
+            steps {
+                sh "psr -s package -c psr.yaml"
+            }
+        }
+        stage('Create Container Image') {
+            steps {
+                sh "psr -s create-container-image -c psr.yaml"
+            }
+        }
+        stage('Push Container Image') {
+            steps {
+                sh "psr -s push-container-image -c psr.yaml"
+            }
+        }
+    }
+}
+```
+
+This Jenkinsfile will be used for both the Maven and NPM applications. PSR
+provides each step and one or many implementers for that step. The PSR commands
+remain the same between applications because the Java (Maven) and JavaScript
+(NPM) applications reference different implementers in their psr.yaml files.
+
+## Appendix A: Pipeline Flow for Maven App
+
+```mermaid
+sequenceDiagram
+    participant wr as Workflow Runner
+    participant psr as PSR
+    participant tool as External Tool
+
+    Note over wr,tool: Generate Metadata Step
+    wr->>psr: `psr -s generate-metadata -c psr.yaml`
+    psr->>tool: Gathers Git and Maven Metadata
+    tool->>psr: Metadata
+    psr->>wr: Artifacts Object
+
+    Note over wr,tool: Unit Test Step
+    wr->>psr: `psr -s unit-test -c psr.yaml`
+    psr->>tool: Tests with JUnit
+    tool->>psr: Test Results + Logs
+    psr->>wr: Artifacts Object
+
+    Note over wr,tool: Package Step
+    wr->>psr: `psr -s package -c psr.yaml`
+    psr->>tool: Builds with Maven
+    tool->>psr: JAR File + Logs
+    psr->>wr: Artifacts Object
+
+    Note over wr,tool: Create Container Image Step
+    wr->>psr: `psr -s create-container-image -c psr.yaml`
+    psr->>tool: Creates Container Image with Buildah
+    tool->>psr: Container Image + Logs
+    psr->>wr: Artifacts Object
+
+    Note over wr,tool: Push Container Image Step
+    wr->>psr: `psr -s push-container-image -c psr.yaml`
+    psr->>tool: Pushes Container Image with Skopeo
+    tool->>psr: Container Image + Logs
+    psr->>wr: Artifacts Object
+```
+
+## Appendix B: Pipeline Flow for NPM App
+
+```mermaid
+sequenceDiagram
+    participant wr as Workflow Runner
+    participant psr as PSR
+    participant tool as External Tool
+
+    Note over wr,tool: Generate Metadata Step
+    wr->>psr: `psr -s generate-metadata -c psr.yaml`
+    psr->>tool: Gathers Git and NPM Metadata
+    tool->>psr: Metadata
+    psr->>wr: Artifacts Object
+
+    Note over wr,tool: Unit Test Step
+    wr->>psr: `psr -s unit-test -c psr.yaml`
+    psr->>tool: Tests with NpmXunitTest
+    tool->>psr: Test Results + Logs
+    psr->>wr: Artifacts Object
+
+    Note over wr,tool: Package Step
+    wr->>psr: `psr -s package -c psr.yaml`
+    psr->>tool: Builds with NPM
+    tool->>psr: Build Outputs + Logs
+    psr->>wr: Artifacts Object
+
+    Note over wr,tool: Create Container Image Step
+    wr->>psr: `psr -s create-container-image -c psr.yaml`
+    psr->>tool: Creates Container Image with Buildah
+    tool->>psr: Container Image + Logs
+    psr->>wr: Artifacts Object
+
+    Note over wr,tool: Push Container Image Step
+    wr->>psr: `psr -s push-container-image -c psr.yaml`
+    psr->>tool: Pushes Container Image with Skopeo
+    tool->>psr: Container Image + Logs
+    psr->>wr: Artifacts Object
+```
diff --git a/src/ploigos_step_runner/step_implementers/examples/__init__.py b/src/ploigos_step_runner/step_implementers/examples/__init__.py
new file mode 100644
index 00000000..9d9a246f
--- /dev/null
+++ b/src/ploigos_step_runner/step_implementers/examples/__init__.py
@@ -0,0 +1,5 @@
+"""`StepImplementers` for the `hello-world` step.
+"""
+
+from ploigos_step_runner.step_implementers.examples.hello_world import HelloWorld
+from ploigos_step_runner.step_implementers.examples.hello_shell import HelloShell
diff --git a/src/ploigos_step_runner/step_implementers/examples/hello_shell.py b/src/ploigos_step_runner/step_implementers/examples/hello_shell.py
new file mode 100644
index 00000000..29e9d715
--- /dev/null
+++ b/src/ploigos_step_runner/step_implementers/examples/hello_shell.py
@@ -0,0 +1,98 @@
+"""Example StepImplementer that executes a shell command. This is a good starting point if you are writing
+a step implementer that is meant to run an external command line tool.
+
+You can run this example from the command line by creating a file named psr.yaml with these contents:
+
+```
+step-runner-config:
+
+  examples:
+    - implementer: HelloShell
+      config:
+        greeting-name: Folks
+```
+
+And then running the command `psr -s examples -c psr.yaml`
+
+Step Configuration
+------------------
+Step configuration expected as input to this step.
+Could come from:
+
+  * static configuration
+  * runtime configuration
+  * previous step results
+
+Configuration Key             | Required? | Default                  | Description
+------------------------------|-----------|--------------------------|-----------
+`greeting-name`               | No        | `World`                  | Name to use in greeting message.
+
+Result Artifacts
+----------------
+Results artifacts output by this step.
+
+Result Artifact Key    | Description
+-----------------------|------------
+`echo-output`              | Message that was printed
+"""  # pylint: disable=line-too-long
+
+from ploigos_step_runner.exceptions import StepRunnerException
+from ploigos_step_runner.results import StepResult
+from ploigos_step_runner.step_implementer import StepImplementer
+from ploigos_step_runner.utils.shell import Shell
+
+
+class HelloShell(StepImplementer):
+    """
+    Example StepImplementer that uses the echo shell command to print a message.
+    """
+
+    # Overridden to specify default values for this StepImplementer's configuration.
+    @staticmethod
+    def step_implementer_config_defaults():
+        return {
+            'greeting-name': 'World'
+        }
+
+    # Overridden to specify required configuration values. These can be specified in
+    # the configuration file or calculated by previous steps in the workflow.
+    @staticmethod
+    def _required_config_or_result_keys():
+        return []  # No required values without defaults
+
+    # Overridden to implement the behavior of this StepImplementer.
+    def _run_step(self):
+        """Runs the step implemented by this StepImplementer.
+
+        Returns
+        -------
+        StepResult
+            Object containing the dictionary results of this step.
+        """
+        step_result = StepResult.from_step_implementer(self)
+
+        greeting = self.get_value('greeting-name')  # Read from configuration
+        message = f"Hello {greeting}!"
+
+        try:
+            # Log STDOUT and STDERR to this file
+            output_file_path = self.write_working_file('greeting-output.txt')
+
+            # Run the command
+            Shell().run(
+                'echo',
+                args=[message],
+                output_file_path=output_file_path
+            )
+
+        except StepRunnerException as error:
+            step_result.success = False
+            step_result.message = str(error)
+            return step_result
+        finally:
+            step_result.add_artifact(
+                description="Standard out and standard error from the echo command",
+                name='greeting-output',
+                value=output_file_path
+            )
+        return step_result
diff --git a/src/ploigos_step_runner/step_implementers/examples/hello_world.py b/src/ploigos_step_runner/step_implementers/examples/hello_world.py
new file mode 100644
index 00000000..73868a5d
--- /dev/null
+++ b/src/ploigos_step_runner/step_implementers/examples/hello_world.py
@@ -0,0 +1,85 @@
+"""A simple StepImplementer that prints out a greeting. See the HelloShell example for an upgraded version
+that runs a shell command to print the greeting.
+
+You can run this example from the command line by creating a file named psr.yaml with these contents:
+
+```
+step-runner-config:
+
+  examples:
+    - implementer: HelloWorld
+      config:
+        greeting-name: Folks
+```
+
+And then running the command `psr -s examples -c psr.yaml`
+
+Step Configuration
+------------------
+Step configuration expected as input to this step.
+Could come from:
+
+  * static configuration
+  * runtime configuration
+  * previous step results
+
+Configuration Key             | Required? | Default                  | Description
+------------------------------|-----------|--------------------------|-----------
+`greeting-name`               | No        | `World`                  | Name to use in greeting message.
+
+Result Artifacts
+----------------
+Results artifacts output by this step.
+
+Result Artifact Key    | Description
+-----------------------|------------
+`greeting`             | Message that was printed
+"""  # pylint: disable=line-too-long
+
+from ploigos_step_runner.results import StepResult
+from ploigos_step_runner.step_implementer import StepImplementer
+
+
+class HelloWorld(StepImplementer):
+    """
+    Example StepImplementer that prints a message and does nothing else.
+    """
+
+    # Overridden to specify default values for this StepImplementer's configuration.
+    @staticmethod
+    def step_implementer_config_defaults():
+        return {
+            'greeting-name': 'World'
+        }
+
+    # Overridden to specify required configuration values. These can be specified in
+    # the configuration file or calculated by previous steps in the workflow.
+    @staticmethod
+    def _required_config_or_result_keys():
+        return []  # No required values without defaults
+
+    # Overridden to implement the behavior of this StepImplementer.
+    def _run_step(self):
+        """Runs the step implemented by this StepImplementer.
+
+        Returns
+        -------
+        StepResult
+            Object containing the dictionary results of this step.
+        """
+
+        # Read the configuration, usually from `psr.yaml`.
+        step_result = StepResult.from_step_implementer(self)
+        greeting = self.get_value('greeting-name')  # Read from configuration
+
+        # Do the actual work of the step.
+        message = f"Hello {greeting}!"
+        print(message)
+
+        # Save the result. Future steps like the report step can access artifacts.
+        step_result.add_artifact(
+            name='greeting-output',
+            value=message
+        )
+
+        return step_result


Test patch:
diff --git a/tests/step_implementers/examples/test_hello_shell.py b/tests/step_implementers/examples/test_hello_shell.py
new file mode 100644
index 00000000..0bc50c95
--- /dev/null
+++ b/tests/step_implementers/examples/test_hello_shell.py
@@ -0,0 +1,89 @@
+import os
+from unittest.mock import patch
+from ploigos_step_runner.results import WorkflowResult
+from ploigos_step_runner.step_implementers.examples import HelloShell
+from testfixtures import TempDirectory
+from tests.helpers.base_step_implementer_test_case import \
+    BaseStepImplementerTestCase
+from ploigos_step_runner.exceptions import StepRunnerException
+
+
+@patch('ploigos_step_runner.step_implementers.shared.npm_generic.Shell.run')  # Given a shell
+class TestHelloShell(BaseStepImplementerTestCase):
+
+    def test_run_step_executes_echo_command(self, mock_shell):
+        with TempDirectory() as test_dir:
+
+            # GIVEN a step implementer configured like:
+            step_implementer = self.create_step_implementer(test_dir, {})
+
+            # WHEN I run the step
+            step_result = step_implementer._run_step()
+
+            # THEN it should run a shell command like `echo "Hello World!"`
+            expected_output_file_path = os.path.join(test_dir.path, 'working', 'examples', 'greeting-output.txt')
+            mock_shell.assert_any_call(
+                'echo',
+                args=['Hello World!'],
+                output_file_path=expected_output_file_path
+            )
+
+    def test_run_step_configure_greeting(self, mock_shell):
+        with TempDirectory() as test_dir:
+
+            # GIVEN a step implementer configured with a non-default message
+            step_implementer = self.create_step_implementer(test_dir, {
+                'greeting-name': 'Everyone'
+            })
+
+            # WHEN I run the step
+            step_result = step_implementer._run_step()
+
+            # THEN it should run a shell command like 'echo "Hello Everyone!"'
+            expected_output_file_path = os.path.join(test_dir.path, 'working', 'examples', 'greeting-output.txt')
+            mock_shell.assert_any_call(
+                'echo',
+                args=['Hello Everyone!'],
+                output_file_path=expected_output_file_path
+            )
+
+    def test_run_step_result(self, mock_shell):
+        with TempDirectory() as test_dir:
+
+            # GIVEN a step implementer configured like:
+            step_implementer = self.create_step_implementer(test_dir, {})
+
+            # WHEN I run the step
+            step_result = step_implementer._run_step()
+
+            # THEN it should return a StepResult
+            self.assertIsNotNone(step_result)
+
+            # AND the StepResult should say that the step was successful
+            self.assertEqual(step_result.success, True)
+
+    def test_run_step_success_false_when_shell_command_fails(self, mock_shell):
+        with TempDirectory() as test_dir:
+
+            # GIVEN a step implementer
+            step_implementer = self.create_step_implementer(test_dir, {})
+
+            # Given that every shell command exits with an error code
+            mock_shell.side_effect = StepRunnerException()
+
+            # When I run the step
+            step_result = step_implementer.run_step()
+
+            # AND the StepResult should say that the step was not successful
+            self.assertEqual(step_result.success, False)
+
+    def create_step_implementer(self, test_dir, step_config):
+        parent_work_dir_path = os.path.join(test_dir.path, 'working')
+        return self.create_given_step_implementer(
+            step_implementer=HelloShell,
+            step_config=step_config,
+            step_name='examples',
+            implementer='HelloWorld',
+            workflow_result=WorkflowResult(),
+            parent_work_dir_path=parent_work_dir_path
+        )
diff --git a/tests/step_implementers/examples/test_hello_world.py b/tests/step_implementers/examples/test_hello_world.py
new file mode 100644
index 00000000..a55d6a54
--- /dev/null
+++ b/tests/step_implementers/examples/test_hello_world.py
@@ -0,0 +1,57 @@
+import os
+
+from ploigos_step_runner.results import WorkflowResult
+from ploigos_step_runner.step_implementers.examples import HelloWorld
+from testfixtures import TempDirectory
+from tests.helpers.base_step_implementer_test_case import \
+    BaseStepImplementerTestCase
+
+
+class TestHelloWorld(BaseStepImplementerTestCase):
+
+    def test_step_run_step_result(self):
+        with TempDirectory() as test_dir:
+
+            # GIVEN a step implementer configured like:
+            step_implementer = self.create_step_implementer(test_dir, {})
+
+            # WHEN I run the step
+            step_result = step_implementer._run_step()
+
+            # THEN it should return a StepResult
+            self.assertIsNotNone(step_result)
+
+            # AND the StepResult should have an artifact with the default message
+            self.assertEqual(step_result.get_artifact('greeting-output').value, 'Hello World!')
+
+    def test_run_step_configuration(self):
+        with TempDirectory() as test_dir:
+
+            # GIVEN a step implementer configured with a non-default message
+            step_implementer = self.create_step_implementer(test_dir, {
+                'greeting-name': 'Everyone'
+            })
+
+            # WHEN I run the step
+            step_result = step_implementer._run_step()
+
+            # THEN it should return a StepResult
+            self.assertIsNotNone(step_result)
+
+            # AND the StepResult should have an artifact with the configured message
+            self.assertEqual(step_result.get_artifact('greeting-output').value, 'Hello Everyone!')
+
+    def test__required_config_or_result_keys(self):
+        required_keys = HelloWorld._required_config_or_result_keys()
+        self.assertEqual(required_keys, [])
+
+    def create_step_implementer(self, test_dir, step_config):
+        parent_work_dir_path = os.path.join(test_dir.path, 'working')
+        return self.create_given_step_implementer(
+            step_implementer=HelloWorld,
+            step_config=step_config,
+            step_name='examples',
+            implementer='HelloWorld',
+            workflow_result=WorkflowResult(),
+            parent_work_dir_path=parent_work_dir_path
+        )