Templates
Templates can be used in several places to make configuration dynamic or parametrize text outputs, be them strings, messages or even whole text files.
Nyx uses Handlebars templates passing the engine the internal state for the template scope (the template input value) so it’s easy to figure out which values are available. Moreover a few functions (lambdas) are available for common needs.
Handlebars is also compatible with Mustache templates so you can reuse them.
Tip: when writing templates you may find useful to serialize the state to a file for reference or troubleshooting.
Reference
Render a static output
At times you just need to output a simple hardcoded string instead of rendering even the simplest template. Don’t even bother with {{ }}
delimiters in this case and simply enter the value.
This means that wherever templates are allowed, you can still use static text, with no templates.
Let’s assume you want to set the Hello World
value for a configuration option
, then your template is just the plain Hello World
:
option = "Hello World"
Render a simple string
Often times you just need to use a simple value from the Nyx state to be the result of the template.
This is fairly simple as you just need to wrap the name of the state attribute with double curly braces, like {{attribute}}
. This is the simplest form of a Handlebars expression (and yes, the double curly brances are the handlebars, or mustaches).
Please note that only simple attributes from the state can be rendered this way, while complex objects like changelog, configuration and releaseScope cannot be rendered as a whole. Instead, their children can be rendered as simple attributes.
So if we want to set our configuration option
to the current value of the version
attribute from the state our definition will look like:
option = "{{version}}"
When the value you need to render is nested into another (i.e. section/attribute
or section.attribute
, depending on the notation), the template looks like:
option = "{{#section}}{{attribute}}{{/section}}"
As you can see this is a little cumbersome for a simple value but you will soon understand how powerful it can be. What is important to note here is that the {{attribute}}
expression is now enclosed between a {{#section}}
opening tag and a {{/section}}
closing tag (denoted by the #
and /
characters).
Alternatively you can simply use the dotted notation, like:
option = "{{section.attribute}}"
Type conversions
All templates return text values but ofter times the template output needs to be converted to other types. This may happen for configuration options, for example, when a dynamically computed value needs a boolean or a number. You can always tell if some conversion is to be performed by the type of the option.
This table gives you the overall rules used to convert text values to types other than strings:
Expected type | Translation criteria |
---|---|
boolean | If the expression returns an empty or blank string translates to false , otherwise returns the boolean evaluation of the string value |
number | Translates to the number representation of the string when it contains a valid number, 0 in all other cases, including when the string does not contain a valid number. Different numeric types (i.e. integers and floats) require specific constraints to be met in order for the conversion to succeed, as per the standard number representation rules |
Functions
Wherever templates are allowed you can also use functions to produce outputs or transform an input value. These functions are provided by lambdas and the syntax is like the one we’ve seen for nested values, like in this example:
option = "{{#upper}}{{ attribute }}{{/upper}}"
Here upper
is a function accepting one parameter (attribute
) and returning the same output, with upper case. Below you can find the list of available lambdas.
The underlying Handlebars template engine also provides an powerful set of built-in functions to handle conditionals, loops, lookups and log messages. Those functions (a.k.a. helpers) can be freely used in templates but they are not officially supported by Nyx. For more on those helpers see Built-in Helpers.
Functions can be nested for combined output. This example is valid and shows how to take the first 20 characters from the given `` and transform it to upper case:
option = "{{#upper}}{{#cutRight length="20"}}{{ attribute }}{{/cutRight}}{{/upper}}"
The functions library
replace
Replaces all occurrences of a given character sequence with another character sequence in the input string. The from
option defines the sequence to be replaced while the to
option defines the sequence to use for replacement.
Example:
output = "{{#replace from="X" to="Y"}}{{input}}{{/replace}}"
Example inputs and corresponding outputs:
Input | Options | Output |
---|---|---|
01234567890 |
from="0" |
123456789 |
01234567890 |
from="0" to="" |
123456789 |
01234567890 |
from="0" to="X" |
X123456789X |
01234567890 |
from="45" to="_" |
0123_67890 |
lower
Transforms the input characters to lower case. Example:
output = "{{#lower}}{{input}}{{/lower}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
camelCase |
camelcase |
feature |
feature |
FEATURE |
feature |
feature/XX-12345 |
feature/xx-12345 |
upper
Transforms the input characters to upper case. Example:
output = "{{#upper}}{{input}}{{/upper}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
camelCase |
CAMELCASE |
feature |
FEATURE |
FEATURE |
FEATURE |
feature/XX-12345 |
FEATURE/XX-12345 |
trim
Removes the leading and trailing spaces from the input. Example:
output = "{{#trim}}{{input}}{{/trim}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
camelCase |
CAMELCASE |
` feature ` | feature |
` FEATURE ` | FEATURE |
` feature/XX-12345 ` | feature/XX-12345 |
first
Discards everything from the first occurrence of a character other than letters and positive digits. Example:
output = "{{#first}}{{input}}{{/first}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
feature |
feature |
12345 |
12345 |
feature/XX-12345 |
feature |
firstLower
Discards everything from the first occurrence of a character other than letters and positive digits and transforms the remaining characters to lower case. Example:
output = "{{#firstLower}}{{input}}{{/firstLower}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
feature |
feature |
12345 |
12345 |
feature/XX-12345 |
feature |
FEATURE/XX-12345 |
feature |
firstUpper
Discards everything from the first occurrence of a character other than letters and positive digits and transforms the remaining characters to upper case. Example:
output = "{{#firstUpper}}{{input}}{{/firstUpper}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
feature |
FEATURE |
12345 |
12345 |
feature/XX-12345 |
FEATURE |
FEATURE/XX-12345 |
FEATURE |
last
Discards everything before the last occurrence of a character other than letters and positive digits. Example:
output = "{{#last}}{{input}}{{/last}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
feature |
feature |
12345 |
12345 |
feature/XX-12345 |
12345 |
lastLower
Discards everything before the last occurrence of a character other than letters and positive digits and transforms the remaining characters to lower case. Example:
output = "{{#lastLower}}{{input}}{{/lastLower}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
feature |
feature |
FEATURE |
feature |
12345 |
12345 |
feature/XX-12345 |
12345 |
lastUpper
Discards everything before the last occurrence of a character other than letters and positive digits and transforms the remaining characters to upper case. Example:
output = "{{#lastUpper}}{{input}}{{/lastUpper}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
feature |
FEATURE |
FEATURE |
FEATURE |
12345 |
12345 |
feature/XX-12345 |
12345 |
sanitize
Removes all characters other than letters and positive digits from the input string, leaving all other characters untouched. Example:
output = "{{#sanitize}}{{input}}{{/sanitize}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
feature |
feature |
12345 |
12345 |
feature/XX-12345 |
featureXX12345 |
sanitizeLower
Removes all characters other than letters and positive digits from the input string, and transforms all others to lower case. Example:
output = "{{#sanitizeLower}}{{input}}{{/sanitizeLower}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
feature |
feature |
12345 |
12345 |
feature/XX-12345 |
featurexx12345 |
sanitizeUpper
Removes all characters other than letters and positive digits from the input string, and transforms all others to upper case. Example:
output = "{{#sanitizeUpper}}{{input}}{{/sanitizeUpper}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
feature |
FEATURE |
12345 |
12345 |
feature/XX-12345 |
FEATUREXX12345 |
capture
Matches a regular expression against the input value and extracts a portion of it. The portion to be extracted must be a capturing group (identified by its index or named) defined in the regular expression. Remember that when using indexes, 0
is the group that returns the whole string.
The expression
option defines the regular expression, while the group
option can be either a positive integer (in which case the group will be extracted by its index) or a string (to extract the capturing group by its name).
Use of named capturing group is recommended in place of using groups by their index as it’s consistent between the Java (Gradle) and Go (command line) implementations. Retrieving groups by index may lead to different outcomes as the two underlying frameworks use a different numbering for groups.
Example:
output = "{{#capture expression="(?<type>[a-zA-Z0-9_]+)(\((?<scope>[a-z ]+)\))?:( (?<title>.+))" group="type"}}{{input}}{{/capture}}"
Example inputs and corresponding outputs:
Input | Options | Output |
---|---|---|
mytype(myscope): mytitle |
expression="(?<type>[a-zA-Z0-9_]+)(\((?<scope>[a-z ]+)\))?:( (?<title>.+))" group="1" |
mytype |
mytype(myscope): mytitle |
expression="(?<type>[a-zA-Z0-9_]+)(\((?<scope>[a-z ]+)\))?:( (?<title>.+))" group="type" |
mytype |
Use tools like regular expressions 101 to write and test your regular expressions.
cutLeft
Returns only the last N characters of the input, where N is an arbitrary positive integer represented by the length
option. If the input is shorter than N characters it’s returned untouched. This is often useful to shorten SHAs. Example:
output = "{{#cutLeft length="3"}}{{input}}{{/cutLeft}}"
Example inputs and corresponding outputs:
Input | Options | Output |
---|---|---|
7b9da5286d4724dd7385bb80639a08841fa26606 |
length=3 |
606 |
7b9da |
length=3 |
9da |
7b |
length=3 |
7b |
cutRight
Returns only the first N characters of the input, where N is an arbitrary positive integer represented by the length
option. If the input is shorter than N characters it’s returned untouched. This is often useful to shorten SHAs. Example:
output = "{{#cutRight length="3"}}{{input}}{{/cutRight}}"
Example inputs and corresponding outputs:
Input | Options | Output |
---|---|---|
7b9da5286d4724dd7385bb80639a08841fa26606 |
length=3 |
7b9 |
7b9da |
length=3 |
7b9 |
7b |
length=3 |
7b |
short5
Returns only the first 5 characters of the input. If the input is shorter than 5 characters it’s returned untouched. This is often useful to shorten SHAs. Example:
output = "{{#short5}}{{input}}{{/short5}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
7b9da5286d4724dd7385bb80639a08841fa26606 |
7b9da |
7b9da |
7b9da |
7b |
7b |
For arbitrary length strings see cutLeft
and cutRight
.
short6
Returns only the first 6 characters of the input. If the input is shorter than 5 characters it’s returned untouched. This is often useful to shorten SHAs. Example:
output = "{{#short6}}{{input}}{{/short6}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
7b9da5286d4724dd7385bb80639a08841fa26606 |
7b9da5 |
7b9da5 |
7b9da5 |
7b |
7b |
For arbitrary length strings see cutLeft
and cutRight
.
short7
Returns only the first 7 characters of the input. If the input is shorter than 5 characters it’s returned untouched. This is often useful to shorten SHAs. Example:
output = "{{#short7}}{{input}}{{/short7}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
7b9da5286d4724dd7385bb80639a08841fa26606 |
7b9da52 |
7b9da52 |
7b9da52 |
7b |
7b |
For arbitrary length strings see cutLeft
and cutRight
.
timeFormat
Returns a timestamp formatted according to the given format string.
The value for the timestamp is the current system time in milliseconds since January 1, 1970, 00:00:00 GMT unless a value is provided.
The returned string is the timestamp as an integer value unless the format
option is passed with a valid format string.
Since the underlying frameworks have different behaviors, the output from this function may not be consistent between the command line version (written in Go) and the Gradle version (written in Java). The format string passd in the format
option also depends on the implementation. Please see here for the available patterns available in Java and here and here for those available in Go. Moreover, when invalid patterns are passed in the format
option, the behavior is different between the two implementations: the Java implementation returns and empty string (and logs an error), while the Go implementation just returns the plain value passed in the format
option. For consistent behavior functions see timestampISO8601
and timestampYYYYMMDDHHMMSS
.
Example inputs and corresponding outputs:
output = "{{#timeFormat}}{{/timeFormat}}"
output = "{{#timeFormat}}{{timestamp}}{{/timeFormat}}"
output = "{{#timeFormat format="yyyyMMdd"}}{{/timeFormat}}"
output = "{{#timeFormat format="yyyyMMdd"}}{{timestamp}}{{/timeFormat}}"
output = "{{#timeFormat format="20060102"}}{{/timeFormat}}"
output = "{{#timeFormat format="20060102"}}{{timestamp}}{{/timeFormat}}"
Example inputs and corresponding outputs:
Input | Options | Output |
---|---|---|
1577880000000 |
||
format=yyyyMMdd or format=yyyyMMdd |
20200101 |
|
1577880000000 |
1577880000000 |
|
1577880000000 |
format=yyyyMMdd or format=yyyyMMdd |
20200101 |
timestampISO8601
Provided a timestamp in the unix format returns it formatted as ISO 8601 UTC. If the input is not a Unix timestamp returns an empty string. Example:
output = "{{#timestampISO8601}}{{timestamp}}{{/timestampISO8601}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
1608210396 |
2020-12-17T13:06:36 |
For arbitrary timestamp formats see timeFormat
.
timestampYYYYMMDDHHMMSS
Provided a timestamp in the unix format returns it formatted as YYYYMMDDHHMMSS
UTC. If the input is not a Unix timestamp returns an empty string. Example:
output = "{{#timestampYYYYMMDDHHMMSS}}{{timestamp}}{{/timestampYYYYMMDDHHMMSS}}"
Example inputs and corresponding outputs:
Input | Output |
---|---|
1608210396 |
20201217130636 |
For arbitrary timestamp formats see timeFormat
.
environmentUser
Returns the current system user name. Example:
user = "{{environmentUser}}"
or, if you prefer the open/close tags:
user = "{{#environmentUser}}{{/environmentUser}}"
In case you pass a value to this function it is ignored.
environmentVariable
Returns the value of the environment variable used as parameter, if any. Example:
os = "{{#environmentVariable}}OS{{/environmentVariable}}"
returns the value of the OS
environment variable, if present, or an empty string.
fileContent
Returns content of the given file, if it exists, or an empty string. Example:
filecontent = "{{#fileContent}}example.txt{{/fileContent}}"
returns returns the entire content of the example.txt
file (if it exists).
The file name can be a relative or an absolute path. Please note that when a relative path is used, it’s always resolved to the current working directory and other configured directories are ignored.
Be careful with the file content type, which must be text, and its size.
fileExists
Returns the string representation of a boolean, depending on whether the the given file exists. Example:
fileexists = "{{#fileExists}}example.txt{{/fileExists}}"
returns true
if the example.txt
file exists, false
otherwise.
The file name can be a relative or an absolute path. Please note that when a relative path is used, it’s always resolved to the current working directory and other configured directories are ignored.
Example
Here is a more complex example where we combine several state attributes to produce a multi-line text content. This example is only useful to show the use of templates and is not meant to be used anywhere.
Consider this template:
Version: {{version}} (bumping '{{bump}}' on {{configuration.initialVersion}} using lenient ({{configuration.releaseLenient}}))
Scheme: {{scheme}}
Timestamp: {{timestamp}}
OS: {{#environmentVariable}}OS{{/environmentVariable}}
User: {{environmentUser}}
Previous Version: {{releaseScope.previousVersion}} at {{#short5}}{{releaseScope.previousVersionCommit}}{{/short5}}
Commits:
{{#releaseScope.commits}}
{{.}}
{{/releaseScope.commits}}
When rendered, it yields to an output like:
Version: 9.8.6 (bumping 'theta' on 1.2.3 using lenient (true))
Scheme: SEMVER
Timestamp: 9223372036854775807
OS: Linux
Users: jdoe
Previous Version: 4.5.6 at 05cbf
Commits:
d40fcded9e516158a2901f5657794931528af106
9bed70fac8a27a4b14b6b12307d034bc59da85c3
ef6a6481adb2df26bc7eebfde465e5c2f3e93539
As you can see there are state attributes like version
, bump
, scheme
and timestamp
used in the template and their usage should already be clear.
But since the resolved configuration is also available as a nested state object, a couple of attributes are fetched from there, as you can see by configuration.initialVersion
and configuration.releaseLenient
.
Some values are retrieved from the current environment: the OS
environment variable is fetched by the {{#environmentVariable}}OS{{/environmentVariable}}
block (where the function parameter OS
is the name of the variable to retrieve) and also the system user name is retrieved by {{{environmentUser}}
.
Moreover, the release scope is also used to get the releaseScope.previousVersion
and releaseScope.previousVersionCommit
. By pay attention here: the previousVersionCommit
is not used as is but only the shortened SHA-1 is used, thanks to the short5
function.
Finally, all the SHA-1 IDs of the release scope commits are printed, as you can see by the final block enclosed within #releaseScope.commits
and /releaseScope.commits
.