From c1a811b5a76613627a64f66ad9238852d1c31668 Mon Sep 17 00:00:00 2001 From: Nick Josevski Date: Mon, 10 Aug 2026 15:16:09 +1000 Subject: [PATCH] Document the AddTimeSpan date filter Spells out the {days}.{hours}:{minutes}:{seconds} format, links to the .NET TimeSpan.Parse docs, and calls out that 48:00:00 is 48 days rather than 48 hours because the leading field stops meaning hours above 23. Also fixes pre-existing markdownlint violations in this file, which CI surfaces because it lints only changed files. Table content is unchanged, only cell padding and separator widths. Co-Authored-By: Claude Opus 5 (1M context) --- .../projects/variables/variable-filters.md | 160 +++++++++++++----- .../variables/variable-substitutions.mdx | 1 + 2 files changed, 118 insertions(+), 43 deletions(-) diff --git a/src/pages/docs/projects/variables/variable-filters.md b/src/pages/docs/projects/variables/variable-filters.md index 830451da31..00c33e98ee 100644 --- a/src/pages/docs/projects/variables/variable-filters.md +++ b/src/pages/docs/projects/variables/variable-filters.md @@ -15,7 +15,7 @@ Octopus variable substitutions support *filters* to correctly encode values for Given the variable: | Name | Value | Scope | -| ------------- | --------- | ----- | +|---------------|-----------|-------| | `ProjectName` | `You & I` | | And the template: @@ -67,7 +67,7 @@ These core filters perform common string operations. The *Format* filter allows for converting of input based on an additionally provided argument that is passed to the *`.ToString()`* method. | MyVar Value | Filter Expression | Output | -| ------------------------------- | ------------------------------------- | ----------------- | +|---------------------------------|---------------------------------------|-------------------| | `4.3` | `#{MyVar \| Format C}` | $4.30 | | `2030/05/22 09:05:00` | `#{MyVar \| Format yyyy}` | 2030 | | | `#{ \| NowDate \| Format Date MMM}` | Nov | @@ -76,10 +76,10 @@ The *Format* filter allows for converting of input based on an additionally prov ### Replace -The *Replace* filter performs a regular expression replace function on the variable. The regular expression should be provided in the [.NET format](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference). Double quotes need to be used around any expressions that contain whitespace or special characters. Expressions containing double quotes can not be expressed inline, but can be done via nested variables. If both the search and replace expressions are variables, ensure there is no space between the expressions. For using Replace on special characters, you should escape the first parameter which will be the regex but the second parameter can be left as a string - see last example below. +The *Replace* filter performs a regular expression replace function on the variable. The regular expression should be provided in the [.NET format](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference). Double quotes need to be used around any expressions that contain whitespace or special characters. Expressions containing double quotes can not be expressed inline, but can be done via nested variables. If both the search and replace expressions are variables, ensure there is no space between the expressions. For using Replace on special characters, you should escape the first parameter which will be the regex but the second parameter can be left as a string - see last example below. | MyVar Value | Filter Expression | Output | -| ----------- | ----------------------------------------- | ------------------------------------------ | +|-------------|-------------------------------------------|--------------------------------------------| | `abc` | `#{MyVar \| Replace b}` | `ac` | | `abc` | `#{MyVar \| Replace b X}` | `aXc` | | `a b c` | `#{MyVar \| Replace "a b" X}` | `X c` | @@ -89,24 +89,22 @@ The *Replace* filter performs a regular expression replace function on the varia | `abc` | `#{MyVar \| Replace #{match} _}` | `a_c` (when `match`=`b`) | | `a\b` | `#{MyVar \| Replace "\\" "\\\\"}` | `a\\b` | - ### Substring The *Substring* filter extracts a range of characters from the input and outputs them. If two arguments are supplied, they are interpreted as start and end offsets of the range. If only one argument is supplied, it is interpreted as the end offset of a range starting at 0. | MyVar Value | Filter Expression | Output | -| ---------------- | --------------------------- | --------- | +|------------------|-----------------------------|-----------| | `Octopus Deploy` | `#{MyVar \| Substring 8 6}` | `Deploy` | | `Octopus Deploy` | `#{MyVar \| Substring 7}` | `Octopus` | | `Octopus Deploy` | `#{MyVar \| Substring 2 3}` | `top` | - ### Trim The *Trim* filter removes any whitespace from the ends of the input. Both ends are trimmed unless an optional argument of `start` or `end` is provided. | MyVar Value | Filter Expression | Output | -| ----------- | ------------------------ | -------- | +|-------------|--------------------------|----------| | `···Bar···` | `#{MyVar \| Trim}` | `Bar` | | `···Bar···` | `#{MyVar \| Trim start}` | `Bar···` | | `···Bar···` | `#{MyVar \| Trim end}` | `···Bar` | @@ -116,7 +114,7 @@ The *Trim* filter removes any whitespace from the ends of the input. Both ends a The *Truncate* filter limits the length of the input. If the input is longer than the length specified by the argument, the rest is replaced with an ellipsis. | MyVar Value | Filter Expression | Output | -| ---------------- | ------------------------ | ------------ | +|------------------|--------------------------|--------------| | `Octopus Deploy` | `#{MyVar \| Truncate 7}` | `Octopus...` | | `abc` | `#{MyVar \| Truncate 7}` | `abc` | @@ -124,11 +122,11 @@ The *Truncate* filter limits the length of the input. If the input is longer tha These filters return `true` or `false` depending on the result of a comparison. They are typically useful for specifying the condition in an `#{if}` block. -| Name | Purpose | Example input | Example output | -|----------------------------------------------------------------------|-------------------------------------------------------------------------|------------------|----------------| -| [`Contains`](#startswith-endswith-and-contains) | Determines whether a string contains a given string | `Octopus Dep` | `true` | -| [`EndsWith`](#startswith-endswith-and-contains) | Determines whether the end of a string matches a given string | `Deploy` | `true` | -| [`Match`](#match) | Determines whether a string contains a given regular expression pattern | `"Octo.*Deploy"` | `true` | +| Name | Purpose | Example input | Example output | +|---------------------------------------------------|-------------------------------------------------------------------------|------------------|----------------| +| [`Contains`](#startswith-endswith-and-contains) | Determines whether a string contains a given string | `Octopus Dep` | `true` | +| [`EndsWith`](#startswith-endswith-and-contains) | Determines whether the end of a string matches a given string | `Deploy` | `true` | +| [`Match`](#match) | Determines whether a string contains a given regular expression pattern | `"Octo.*Deploy"` | `true` | | [`StartsWith`](#startswith-endswith-and-contains) | Determines whether the beginning of a string matches a given string | `Octo` | `true` | ### Match @@ -136,7 +134,7 @@ These filters return `true` or `false` depending on the result of a comparison. The *Match* filter searches the input for an occurrence of a given regular expression pattern. It returns `true` if an occurrence is found, and `false` otherwise. The regular expression should be provided in the [.NET format](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference). Double quotes need to be used around any expressions that contain whitespace or special characters. Expressions containing double quotes can not be expressed inline, but can be done via nested variables. | MyVar Value | Filter Expression | Output | -| ----------- | ------------------------------ | --------------------------- | +|-------------|--------------------------------|-----------------------------| | `abc` | `#{MyVar \| Match abc}` | `true` | | `abc` | `#{MyVar \| Match def}` | `false` | | `a b c` | `#{MyVar \| Match "a b"}` | `true` | @@ -148,7 +146,7 @@ The *Match* filter searches the input for an occurrence of a given regular expre The *StartsWith*, *EndsWith* and *Contains* filters compare the input to a given string argument. They return `true` if the argument matches, and `false` otherwise. The comparison is case-sensitive. Strings are compared as [Ordinals](https://docs.microsoft.com/en-us/dotnet/api/system.stringcomparison). Double quotes need to be used around any expressions that contain whitespace or special characters. Expressions containing double quotes can not be expressed inline, but can be done via nested variables. | MyVar Value | Filter Expression | Output | -| ----------- | ----------------------------- | ------------------------- | +|-------------|-------------------------------|---------------------------| | `abc` | `#{MyVar \| StartsWith ab}` | `true` | | `abc` | `#{MyVar \| StartsWith bc}` | `false` | | `abc` | `#{MyVar \| StartsWith Ab}` | `false` | @@ -175,17 +173,93 @@ These filters provide a mechanism to convert a value from one form to another. These filters are used to work with dates. -| Name | Purpose | Example input | Example output | -|-----------------------------------------|---------------------------------|---------------|--------------------------------| -| [`NowDate`](#nowdate-and-nowdateutc) | Outputs the current date | | `2016-11-03T08:53:11.0946448` | -| [`NowDateUtc`](#nowdate-and-nowdateutc) | Outputs the current date in UTC | | `2016-11-02T23:01:46.9441479Z` | +| Name | Purpose | Example input | Example output | +|-----------------------------------------|---------------------------------|-----------------------|--------------------------------| +| [`AddTimeSpan`](#addtimespan) | Shifts a date by a time span | `2016-11-03T08:53:11` | `2016-11-03T10:53:11.0000000` | +| [`NowDate`](#nowdate-and-nowdateutc) | Outputs the current date | | `2016-11-03T08:53:11.0946448` | +| [`NowDateUtc`](#nowdate-and-nowdateutc) | Outputs the current date in UTC | | `2016-11-02T23:01:46.9441479Z` | + +### AddTimeSpan + +The *AddTimeSpan* filter shifts a date by a time span and outputs the result in ISO-8601 [Round-trip format](https://msdn.microsoft.com/en-us/library/az4se3k1#Roundtrip), ready to be chained into [`Format`](#format). + +The time span is written as: + +```text +{days}.{hours}:{minutes}:{seconds} +``` + +The `{days}.` part is optional. When it is left off, the remaining fields are read as `{hours}:{minutes}:{seconds}`. The full set of accepted forms is described in the .NET [TimeSpan.Parse](https://learn.microsoft.com/en-us/dotnet/api/system.timespan.parse#remarks) documentation. + +:::div{.warning} +**`48:00:00` is 48 days, not 48 hours.** + +The `{hours}` field only holds values from 0 to 23. Once the leading field goes above 23 it is no longer read as hours — it becomes `{days}`, and every other field shifts along with it: + +| Time span | Reading | Actual shift | +|------------|-------------------------------|-------------------------------------------------------------------| +| `23:00:00` | `{hours}:{minutes}:{seconds}` | 23 hours | +| `24:00:00` | `{days}:{hours}:{minutes}` | **24 days** | +| `48:00:00` | `{days}:{hours}:{minutes}` | **48 days** | +| `24:01:02` | `{days}:{hours}:{minutes}` | **24 days, 1 hour, 2 minutes** — the `02` is minutes, not seconds | + +This is silent. Nothing fails, and the deployment proceeds with the wrong date. + +To express more than 23 hours, use the day form instead: 48 hours is `"2.00:00:00"`. +::: + +:::div{.hint} +The day form contains a `.`, which is not valid in an unquoted filter argument, so it has to be quoted: `#{MyVar | AddTimeSpan "2.00:00:00"}`. Forms without a `.` can be written either way. +::: + +| MyVar Value | Filter Expression | Shift | Output | +|-----------------------|--------------------------------------------------------------------|-----------------------|-------------------------------| +| `2016-11-03T08:53:11` | `#{MyVar \| AddTimeSpan 02:00:00}` | 2 hours | `2016-11-03T10:53:11.0000000` | +| `2016-11-03T08:53:11` | `#{MyVar \| AddTimeSpan -02:00:00}` | 2 hours back | `2016-11-03T06:53:11.0000000` | +| `2016-11-03T08:53:11` | `#{MyVar \| AddTimeSpan 00:02:00}` | 2 minutes | `2016-11-03T08:55:11.0000000` | +| `2016-11-03T08:53:11` | `#{MyVar \| AddTimeSpan "2.00:00:00"}` | 2 days | `2016-11-05T08:53:11.0000000` | +| `2016-11-03T08:53:11` | `#{MyVar \| AddTimeSpan "2.12:00:00"}` | 2.5 days, ie 60 hours | `2016-11-05T20:53:11.0000000` | +| `2016-11-03T08:53:11` | `#{MyVar \| AddTimeSpan 02:00:00 \| Format "yyyy-MM-dd HH:mm:ss"}` | 2 hours | `2016-11-03 10:53:11` | +| | `#{ \| NowDate \| AddTimeSpan 02:00:00}` | 2 hours | `2016-11-03T10:53:11.0946448` | + +A bare number with no colons is read as days, so `AddTimeSpan 2` is the same as `AddTimeSpan "2.00:00:00"`. + +The filter preserves how the input expressed its time zone: a value with no offset stays without one, a UTC value stays in UTC, and a value with an explicit offset keeps that offset rather than being converted to the server's local time. + +If the input isn't a date, or the time span can't be parsed, the expression is left unevaluated and appears in the output as written. An unreplaced `#{...}` means the filter couldn't be applied, not that the value was empty. + +#### Deriving a change window from a deployment's start time + +A common use is deriving an end time from a start time. For example, to give a ServiceNow change request a two hour implementation window: + +| Name | Value | +|----------------------------------------|-------------------------------------------------------------------------------------| +| `Octopus.ServiceNow.Field[start_date]` | `#{Octopus.Task.QueueTime \| Format "yyyy-MM-dd HH:mm:ss"}` | +| `Octopus.ServiceNow.Field[end_date]` | `#{Octopus.Task.QueueTime \| AddTimeSpan 02:00:00 \| Format "yyyy-MM-dd HH:mm:ss"}` | + +:::div{.warning} +`Octopus.Task.QueueTime` is the time the task was **queued**, not the time it started executing. The window above therefore starts counting down the moment the deployment joins the queue. + +For a deployment that starts straight away this is effectively the current time, so the two are interchangeable. But if the task waits — behind another deployment, on a busy worker pool, or because it was scheduled for later — the window will already be partly or entirely used up by the time the deployment actually runs, and a change request can fall outside its own implementation window before any steps execute. + +Where that matters, size the window against the worst-case queue wait rather than the expected run time. To vary it per environment, scope the whole expression rather than just the time span: + +| Name | Value | Scope | +|-------------------|-------------------------------------------------------------------------------------|------------| +| `ChangeWindowEnd` | `#{Octopus.Task.QueueTime \| AddTimeSpan 02:00:00 \| Format "yyyy-MM-dd HH:mm:ss"}` | | +| `ChangeWindowEnd` | `#{Octopus.Task.QueueTime \| AddTimeSpan 08:00:00 \| Format "yyyy-MM-dd HH:mm:ss"}` | Production | + +then set `Octopus.ServiceNow.Field[end_date]` to `#{ChangeWindowEnd}`. + +The time span has to be written into each scoped value rather than supplied as `AddTimeSpan #{ChangeWindow}`, because a filter argument that is itself a variable can't be followed by another filter in the same chain. That restriction applies to all filters that take arguments, not just `AddTimeSpan`. +::: ### NowDate and NowDateUtc The *NowDate* and *NowDateUtc* filters take no variable input but can take an additional optional right-hand side argument to define the string format (Defaults to ISO-8601 [Round-trip format](https://msdn.microsoft.com/en-us/library/az4se3k1#Roundtrip)). | MyFormat Variable | Filter Expression | Output | -| ----------------- | --------------------------------- | ------------------------------ | +|-------------------|-----------------------------------|--------------------------------| | | `#{ \| NowDate }` | `2016-11-03T08:53:11.0946448` | | | `#{ \| NowDateUtc}` | `2016-11-02T23:01:46.9441479Z` | | | `#{ \| NowDate "HH dd-MMM-yyyy"}` | `09 03-Nov-2016` | @@ -200,36 +274,36 @@ These filters apply format-specific escaping rules. |------------------------------------------------------------------------------------------------------------------|----------------------------------------------------|--------------------|------------------------| | `HtmlEscape` | Escapes entities for use in HTML content | `1 < 2` | `1 \< 2` | | `JsonEscape` | Escapes data for use in JSON strings | `He said "Hello!"` | `He said \\"Hello!\\"` | -| `PropertiesKeyEscape` | Escapes data for use in .properties keys | `Hey: x=y` | `Hey\:\ x\=y` | -| `PropertiesValueEscape` | Escapes data for use in .properties values | `a\b=c` | `a\\b=c` | +| `PropertiesKeyEscape` | Escapes data for use in .properties keys | `Hey: x=y` | `Hey\:\ x\=y` | +| `PropertiesValueEscape` | Escapes data for use in .properties values | `a\b=c` | `a\\b=c` | | [`UriEscape`](https://docs.microsoft.com/en-us/dotnet/api/system.uri.escapeuristring?view=netframework-4.0) | Escape a URI string | `A b:c+d/e` | `A%20b:c+d/e` | | [`UriDataEscape`](https://docs.microsoft.com/en-us/dotnet/api/system.uri.escapedatastring?view=netframework-4.0) | Escape a URI data string | `A b:c+d/e` | `A%20b%3Ac%2Bd%2Fe` | | `XmlEscape` | Escapes entities for use in XML content | `1 < 2` | `1 \< 2` | -| `YamlDoubleQuoteEscape` | Escapes data for use in YAML double quoted strings | `"Hello"\Goodbye` | `\"Hello\"\\Goodbye` | -| `YamlSingleQuoteEscape` | Escapes data for use in YAML single quoted strings | `The bee's knees` | `The bee''s knees` | +| `YamlDoubleQuoteEscape` | Escapes data for use in YAML double quoted strings | `"Hello"\Goodbye` | `\"Hello\"\\Goodbye` | +| `YamlSingleQuoteEscape` | Escapes data for use in YAML single quoted strings | `The bee's knees` | `The bee''s knees` | ## Extraction filters {#extraction-filters} These filters extract a part of value. -| Name | Purpose | Example input | Example output | -|-----------------------------------------------|----------------------------------------------------------------------|--------------------------------|----------------| -| [`UriPart`](#uripart) | Extracts a specified part of a URI string | `https://octopus.com/docs` | `/docs` | -| `VersionMajor` | Extracts the major version field from a version string | `1.2.3.4-my-branch.1.2+build10` | `1` | -| `VersionMinor` | Extracts the minor version field from a version string | `1.2.3.4-my-branch.1.2+build10` | `2` | -| `VersionPatch` | Extracts the patch version field from a version string | `1.2.3.4-my-branch.1.2+build10` | `3` | -| `VersionRevision` | Extracts the revision version field from a version string | `1.2.3.4-my-branch.1.2+build10` | `4` | -| `VersionPreRelease` | Extracts the prerelease field from a version string | `1.2.3.4-my-branch.1.2+build10` | `my-branch.1.2` | -| `VersionPreReleasePrefix` | Extracts the prefix from the prerelease field from a version string | `1.2.3.4-my-branch.1.2+build10` | `my-branch` | -| `VersionPreReleaseCounter` | Extracts the counter from the prerelease field from a version string | `1.2.3.4-my-branch.1.2+build10` | `1.2` | -| `VersionMetadata` | Extracts the metadata field from a version string | `1.2.3.4-my-branch.1.2+build10` | `build10` | +| Name | Purpose | Example input | Example output | +|----------------------------|----------------------------------------------------------------------|---------------------------------|-----------------| +| [`UriPart`](#uripart) | Extracts a specified part of a URI string | `https://octopus.com/docs` | `/docs` | +| `VersionMajor` | Extracts the major version field from a version string | `1.2.3.4-my-branch.1.2+build10` | `1` | +| `VersionMinor` | Extracts the minor version field from a version string | `1.2.3.4-my-branch.1.2+build10` | `2` | +| `VersionPatch` | Extracts the patch version field from a version string | `1.2.3.4-my-branch.1.2+build10` | `3` | +| `VersionRevision` | Extracts the revision version field from a version string | `1.2.3.4-my-branch.1.2+build10` | `4` | +| `VersionPreRelease` | Extracts the prerelease field from a version string | `1.2.3.4-my-branch.1.2+build10` | `my-branch.1.2` | +| `VersionPreReleasePrefix` | Extracts the prefix from the prerelease field from a version string | `1.2.3.4-my-branch.1.2+build10` | `my-branch` | +| `VersionPreReleaseCounter` | Extracts the counter from the prerelease field from a version string | `1.2.3.4-my-branch.1.2+build10` | `1.2` | +| `VersionMetadata` | Extracts the metadata field from a version string | `1.2.3.4-my-branch.1.2+build10` | `build10` | ### UriPart The *UriPart* filter parses the input as a URI and extracts a specified part of it. A helpful error will be written to the output if there is an error in the input or the filter expression. | MyVar Value | Filter Expression | Output | -| --------------------------------------- | ------------------------------------- | -------------------------- | +|-----------------------------------------|---------------------------------------|----------------------------| | `https://octopus.com/docs` | `#{MyVar \| UriPart AbsolutePath}` | `/docs` | | `https://octopus.com/docs` | `#{MyVar \| UriPart AbsoluteUri}` | `https://octopus.com/docs` | | `https://octopus.com/docs` | `#{MyVar \| UriPart Authority}` | `octopus.com` | @@ -266,7 +340,7 @@ Octostache 2.x includes an update to support parsing JSON formatted variables na Given the variable: | Name | Value | Scope | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----- | +|-----------------------------|-------------------------------------------------------------------------------------------------------------------------|-------| | `Custom.MyJson` | `{Name: "t-shirt", Description: "I am a shirt", Sizes: [{size: "small", price: 15.00}, {size: "large", price: 20.00}]}` | | | `Custom.MyJson.Description` | `Shirts are not shorts.` | | @@ -300,7 +374,7 @@ There are a few things to note here: Given the variables: | Name | Value | -| --------- | ------------------------------------------------------------------------------------ | +|-----------|--------------------------------------------------------------------------------------| | MyNumbers | `[5,2,4]` | | MyObjects | `{Cat: {Price: 11.5, Description: "Meow"}, Dog: {Price: 17.5, Description: "Woof"}}` | @@ -314,7 +388,7 @@ Numbers: Objects: #{each item in MyObjects} - #{item.Key}: #{item.Value.Price} + #{item.Key}: #{item.Value.Price} #{/each} ``` @@ -332,10 +406,10 @@ Dog: 17.5 ``` ## Older versions -* Comparison filters are available from Octopus Deploy **2021.2** onwards. -* `VersionMajor`, `VersionMinor`, `VersionPatch`, `VersionRevision`, `VersionPreRelease`, `VersionPreReleasePrefix`, `VersionPreReleaseCounter` and `VersionMetadata` extraction filters are available from Octopus Deploy **2020.5** onwards. -* `PropertiesKeyEscape`, `PropertiesValueEscape`, `YamlDoubleQuoteEscape` and `YamlSingleQuoteEscape` escape filters are available from Octopus Deploy **2020.4** onwards. +- Comparison filters are available from Octopus Deploy **2021.2** onwards. +- `VersionMajor`, `VersionMinor`, `VersionPatch`, `VersionRevision`, `VersionPreRelease`, `VersionPreReleasePrefix`, `VersionPreReleaseCounter` and `VersionMetadata` extraction filters are available from Octopus Deploy **2020.5** onwards. +- `PropertiesKeyEscape`, `PropertiesValueEscape`, `YamlDoubleQuoteEscape` and `YamlSingleQuoteEscape` escape filters are available from Octopus Deploy **2020.4** onwards. ## Learn more diff --git a/src/pages/docs/projects/variables/variable-substitutions.mdx b/src/pages/docs/projects/variables/variable-substitutions.mdx index 2864be5b38..d7580c038e 100644 --- a/src/pages/docs/projects/variables/variable-substitutions.mdx +++ b/src/pages/docs/projects/variables/variable-substitutions.mdx @@ -413,6 +413,7 @@ The following filters are available: - Markdown - NowDate - NowDateUtc +- AddTimeSpan - Format - Replace - Trim