Step packages are an evolution in the development of Octopus steps. Step packages address many of the concerns that made it difficult to implement new functionality as Octopus scaled up in terms of internal team size and external surface area. In particular, step packages are:
- Developed outside the main Octopus code base.
- Tested as isolated and independent projects.
- Distributed on their own independent timelines (not quite implemented yet).
- Independently versioned, with each version able to be run side-by-side (not implemented yet).
This sample project provides a starting point for anyone looking to create a new step package. The code in this repository defines a "Hello World" target and step demonstrating a minimal step package implementation.
The directory structure of a step package is shown below:
\
steps
- A directory containing one or more step definitions.<step-name>
- A directory containing the definition of a step. There may be many of these directories defining many steps within a single step package.src
- The parent directory containing the step code and assets.__tests__
- The directory containing step tests.executor.spec.ts
- Tests validating the logic in theexecutor.ts
file.
executor.ts
- The code to be executed when a step is run by Octopus.inputs.ts
- The definition of the inputs required by the step.logo.svg
- The image to be displayed in the Octopus web UI for the step.metadata.json
- The step metadata.ui.ts
- The step UI definition.validation.ts
- The step validation rules.
targets
- A directory containing one or more target definitions.<target-name>
- A directory containing the definition of a target. There may be many of these directories defining many steps within a single step package.src
- The parent directory containing the target code and assets.__tests__
- The directory containing target tests.executor.spec.ts
- Tests validating the logic in theexecutor.ts
file.
executor.ts
- The code to be executed when a target health check is run by Octopus.inputs.ts
- The definition of the inputs required by the target.logo.svg
- The image to be displayed in the Octopus web UI for the target.metadata.json
- The target metadata.ui.ts
- The target UI definition.validation.ts
- The target validation rules.
.eslintignore
- The ESLint ignore file..eslintrc.js
- The ESLint configuration file..gitignore
- The git ignore file..prettierrc
- The prettier configuration file.global.d.ts
- The TypeScript global libraries.package.json
- The Node.js project file.package-lock.json
- The specific package versions to be retrieved by npm.tsconfig.json
- The TypeScript compiler options file.
Creating a new target involves creating the following files under the targets/<target-name>-target/src
directory. In the case of this sample step package, we'll create them under targets/hello-world-target/src
:
metadata.json
inputs.ts
executor.js
ui.ts
validation.ts
The metadata.json
file provides details about the target. A sample is shown below:
{
"schemaVersion": "1.0.0",
"version": "0.0.0",
"type": "deployment-target",
"id": "hello-world-target",
"name": "Hello world target",
"description": "An example target that does nothing useful",
"categories": [
"Linux"
],
"launcher": "node"
}
schemaVersion
is the version of the metadata file.1.0.0
is the only version available.version
is the version of the target. Versioning is covered in detail here.type
defines the type of resource to be created. It must bedeployment-target
for a target.id
is the resource ID.name
is the name of the target displayed by the Octopus web UI.description
is the description of the target displayed by the Octopus UI.categories
is an array containing one or more target categories display by the Octopus UI where the target will be listed.launcher
defines how the step is executed. A value ofnode
means the step is executed by Node.js.
The inputs.ts
file exports an interface defining the input fields required by the target. This interface is consumed by executor.ts
to read the values when performing the target's health check, ui.ts
to build up the form exposed in the Octopus web UI, and validate.ts
to verify new values.
An example is shown below exposing a single string field:
export default interface HelloWorldTargetInputs {
greetingPrefix: string;
}
Targets perform a health check to validate their inputs and check the state of the system they represent. This health check is performed by the function exported by the executor.ts
file.
The example below prints some text to the log during a health check, and will always pass, meaning the target is always healthy:
import { Handler } from "@octopusdeploy/step-api";
import HelloWorldTargetInputs from "./inputs";
const HelloWorldDeploymentTargetHealthCheckExecutor: Handler<HelloWorldTargetInputs> = async (_, context) => {
context.print("Hello world target is healthy");
};
export default HelloWorldDeploymentTargetHealthCheckExecutor;
The form displayed by the Octopus web UI is defined by the function exported by the ui.ts
file. The form is defined as an instance of the DeploymentTargetUI
interface, which has two functions: createInitialInputs
and editInputsForm
.
The createInitialInputs
function allows the initial default field values to be defined.
The editInputsForm
function provides a DSL for building the user interface. The first parameter is the inputs defined in inputs.ts
. The second parameter is an instance of AvailableDeploymentTargetComponents
, which has factory functions for creating various input widgets like text fields, lists, radio buttons etc.
Here we define the initial value of the greetingPrefix
input to be Hello
, and build the form with a single text
input:
import { DeploymentTargetUI } from "@octopusdeploy/step-api";
import HelloWorldTargetInputs from "./inputs";
export const HelloWorldTargetUI: DeploymentTargetUI<HelloWorldTargetInputs> = {
createInitialInputs: () => {
return {
greetingPrefix: "Hello"
};
},
editInputsForm: (inputs, { section, text }) => [
section({
title: "Greeting",
content: [
text({
input: inputs.greetingPrefix,
label: "Greeting Prefix",
helpText: "The beginning of the greeting",
}),
],
}),
],
};
export default HelloWorldTargetUI;
Form validation is performed by the function exported by the validate.ts
file. This function returns an array of ValueValidator
objects, and takes two parameters:
- The step inputs as input paths.
- A validation function that returns a
ValueValidator
and takes two parameters:- An input path.
- A function returning a string containing the error code (or returns nothing if there is no validation error) and takes the input value (retrieved from the input path) as the first parameter.
Here is an example:
import { ValidateInputs } from "@octopusdeploy/step-api";
import HelloWorldTargetInputs from "./inputs";
const validateInputs: ValidateInputs<HelloWorldTargetInputs> = (inputs, validate) => {
return [
validate(inputs.greetingPrefix, (greeting) => {
if (greeting === "") return "Greeting can not be empty";
}),
];
};
export default validateInputs;
Creating a new step involves creating the following files under the steps/<step-name>/src
directory. In the case of this sample step package, we'll create them under steps/hello-world/src
:
metadata.json
inputs.ts
executor.js
ui.ts
validation.ts
The metadata.json
provides details about the step. A sample is shown below:
{
"schemaVersion": "1.0.0",
"version": "0.0.0",
"type": "step",
"id": "hello-world-upload",
"name": "Hello World",
"description": "An example step",
"categories": [
"BuiltInStep"
],
"canRunOnDeploymentTarget": false,
"launcher": "node"
}
The following fields make up this file:
schemaVersion
is the version of the metadata file.1.0.0
is the only version available.version
is the version of the step. Versioning is covered in detail here.type
defines the type of resource tobe created. Valid values arestep
anddeployment-target
.id
is the resource ID.name
is the name of the step displayed by the Octopus web UI.description
is the description of the step displayed by the Octopus UI.categories
is an array containing one or more step categories display by the Octopus UI where the step will be listed.canRunOnDeploymentTarget
can be set totrue
to run the step on a target accessible via a tentacle, orfalse
to be run on a worker.launcher
defines how the step is executed. A value ofnode
means the step is executed by Node.js.
This file performs the same function as it does when defining a new target.
An example is shown below exposing a single string field:
export default interface HelloWorldStepInputs {
name: string;
}
The executor.ts
file contains the logic to be executed when the step is run as part of a deployment process.
Unlike the function returned by the target executor.ts
function, a step executor.ts
function can receive inputs from both the step and the target. The combination of the inputs defined by these two resources allows a step executor to perform a common action on multiple targets.
For this example we print the greeting defined in the target and the name defined on the step to the output log:
import HelloWorldStepInputs from "./inputs";
import {ExecutionInputs, Handler, OctopusContext, TargetInputs} from "@octopusdeploy/step-api";
import HelloWorldTargetInputs from "../../targets/hello-world-target/inputs";
const HelloWorldStepExecutor: Handler<HelloWorldStepInputs, HelloWorldTargetInputs> = async (
inputs: ExecutionInputs<HelloWorldStepInputs>,
target: TargetInputs<HelloWorldTargetInputs>,
context: OctopusContext
) => {
context.print(target.greetingPrefix + " " + inputs.name);
};
export default HelloWorldStepExecutor;
The step form displayed by the Octopus web UI is defined much the same as it was with the target. There are some subtle differences though:
- It implements the
StepUI
type. - The first parameter to the
createInitialInputs
function can be an instance ofInitialInputFactories
, which provides the ability to create blank package references. - The second parameter to the
editInputsForm
function is an instance ofAvailableStepComponents
, which exposes a different widget set.
Here we define the initial value of the name
input to be a blank string, and build the form with a single text
input:
import { StepUI } from "@octopusdeploy/step-api";
import HelloWorldStepInputs from "./inputs";
export const HelloWorldStepUI: StepUI<HelloWorldStepInputs> = {
createInitialInputs: () => {
return {
name: ""
};
},
editInputsForm: (inputs, { text }) => {
return [
text({
input: inputs.name,
label: "Greeting Name",
helpText: `The name of the person to greet.`,
}),
];
},
};
export default HelloWorldStepUI;
Step form validation is identical to target form validation:
import { ValidateInputs } from "@octopusdeploy/step-api";
import HelloWorldStepInputs from "./inputs";
const validateInputs: ValidateInputs<HelloWorldStepInputs> = (inputs, validate) => {
return [
validate(inputs.name, (name) => {
if (name === "") return "Name can not be empty";
}),
];
};
export default validateInputs;
To build step packages, run the following commands:
npm install
npm run build
npm run test
The step package files are saved under the dist/steps
folder. Add the contents of this folder to a ZIP file and copy it to the steps
folder in Octopus.
If you are testing with a locally built copy of Octopus, place the ZIP file in \source\Octopus.Server\bin\net5.0\steps
.
This repository contains a GitHub workflow that builds and tests the code, packages the resulting files, and creates a release with the step package archive.
Use git tags to indicate releases, while commits to the main
branch create alpha
releases.
With the step package copied to the steps
directory, target can now be selected when creating a new deployment target:
The step is available when building a deployment project:
Here is the result of a deployment with the new step package: