# File<no value>
// <!-- Required for asciidoctor -->
:toc:
// Set toclevels to be at least your hugo [markup.tableOfContents.endLevel] config key
:toclevels: 4

[cols="1^,1^,1^",options=header]
|===
| source | condition | target
| &#10004; | &#10004; | &#10004;
|===

== Description

**source**

The File "source" retrieves the content of a file (from the local filesystem or from a remote URL).

**condition**

The File "condition" tests if the content of a file (either local or remote URL) matches the specified constraints.

**target**

The File "target" updates a the content of a file (only local filesystem) from the input source value and specified constraints.

== Parameters

The following parameters can be specified through the `spec` attributes map:

{{< resourceparameters "sources" "file" >}}

Links :

* https://pkg.go.dev/regexp[Documentation: Golang regexp syntax]

=== File Location

The location of the file (attribute `file`) can be specified as an URL (prefixed with `https://` or `http://`) or as a local filesystem path (optional prefix `file://` or any relative or absolute path of the current operating system: Unix or Windows)

=== Searching For Files By Pattern

`searchpattern: true` turns `file` and `files` from exact names into glob patterns, matched against the whole name rather than a substring:

[source,yaml]
----
spec:
  files:
    - "charts/*/values.yaml"
  searchpattern: true
  matchpattern: 'tag: .*'
  replacepattern: 'tag: {{ source "default" }}'
----

The supported terms are `*` (any run of non-separator characters), `?` (a single one), and `[...]` character classes; `\` escapes the next character.

Unlike the link:/docs/plugins/resource/yaml/["Yaml" resource], which rejects `searchpattern` in a source, the `file` resource accepts it everywhere, and that is a trap:

[WARNING]
====
A **source** whose pattern matches more than one file picks one of them *arbitrarily*. The validation that limits a source to a single file only counts the entries written in `files`, not the files a glob expands to, and the matches are held in a map, so the winner varies between runs of the same manifest:

[source,shell]
----
$ for i in $(seq 6); do updatecli diff --config source-glob.yaml; done   # file: "ch/*/values.yaml"
✔ content: found from file "ch/b/values.yaml"                            # 5 runs
✔ content: found from file "ch/a/values.yaml"                            # 1 run
----

Use `searchpattern` for conditions and targets, where every match is processed, and name the exact file in a source.
====

=== Mutually Exclusive Attributes

The combinations below are rejected before the pipeline runs, with a message listing every problem at once:

[cols="2,3",options=header]
|===
| Combination | Reason
| `file` + `files` | Pick one.
| duplicate entries in `files` | Rejected outright.
| `files` (more than one) + `line` | A line number is meaningless across several files.
| `line` + `forcecreate` | Writing a line into a file that does not exist makes no sense.
| `line` + `matchpattern` | Two different ways of locating the content.
| `line` + `replacepattern` | As above.
| `content` + `replacepattern` | Two different sources for the new value.
| `template` + `content` / `matchpattern` / `replacepattern` / `line` | The template owns the whole output.
| `templateData` without `template` | Nothing to pass the data to.
| negative `line` | "Line cannot be negative for a file resource."
|===

=== Changelogs From Capture Groups

For a target, a `matchpattern` containing a capture group also feeds the changelog: the group captures the value being replaced, so the report shows a real transition instead of `unknown`.

[source,yaml]
----
matchpattern: '"version":\s*"([\d\.]+)"'    # reports 1.24.5 → 1.25.1
matchpattern: '"version":\s*"[\d\.]+"'      # reports unknown → 1.25.1
----

== File Source

It retrieves the content of the file at the location `file` and use it as the source value.

:bulb: Please note that the source value might be a multiline string with endline characters.

⚠️ A file source rejects the following attributes with a validation error:

* `content` - "only supported for conditions and targets"
* `forcecreate` - "only supported for targets"
* `replacepattern` - "only supported for targets"
* `files` holding more than one entry - "can't contain more than one element for sources"

NOTE: `template` is a target-only attribute, but a source neither uses nor rejects it (it is silently ignored, and the source still returns the raw file content).

=== Filtering File Source Content

You may filter the content with one of the options below:

* If the attribute `spec.line` is specified, then the source value is set to the content of this line only
* If the attribute `spec.matchpattern` is specified, then the source value is set to the matched content in the file
** The specified pattern must be a valid https://pkg.go.dev/regexp[Golang regexp]
** When there are no matched content (e.g. the matched content is the empty string), then the source fails


=== Source Examples

* The following example should result in a source value set to the whole content of the file `./README.md`:
+
[source,yaml]
.updateCli.yaml:
--
sources:
  ContentFromLocalFile:
    kind: file
    spec:
      file: README.md
--
+
[source,markdown]
.README.md:
--
# Read Me
Hello world
--
+
[source,shell]
--
$ updatecli apply
# ...

SOURCES
=======

ContentFromLocalFile
--------------------
✔ Content: found from file "README.md":
# Read Me
Hello world

=============================
# ...
--

* The following example should result in a source value set to the whole content of the file located at the URL `https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS`:
+
[source,yaml]
.updateCli.yaml:
--
sources:
  ContentFromURL:
    kind: file
    spec:
      file: https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS
--
+
[source,shell]
--
$ updatecli apply
# ...

SOURCES
=======

ContentFromURL
--------------
✔ Content: found from file "https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS":
363d0e0c5c4cb4e69f5f2c7f64f9bf01ab73af0801665d577441521a24313a07  terraform_0.14.5_darwin_amd64.zip
5a3e0c7873faa048f59d563a2a98caf7f04045967cbb5ad6cf05f5991e20b8d1  terraform_0.14.5_freebsd_386.zip
4b7f2b878a9854652493b2c94ac586586f2ab53f93e3baa55fc2199ccd5a042d  terraform_0.14.5_freebsd_amd64.zip
03c201a9a3e1d2776d0cfc0163e52484f3dbbbd73eb08d5bac491ca87a9aa3b7  terraform_0.14.5_freebsd_arm.zip
b262998c85a7cad1c24b90f3d309d592bd349d411167a2939eb482dc2b99702d  terraform_0.14.5_linux_386.zip
2899f47860b7752e31872e4d57b1c03c99de154f12f0fc84965e231bc50f312f  terraform_0.14.5_linux_amd64.zip
a971a5f5da82ea896a2e91fd828c90ea9c28e3de575d03a7ce25a5840ed7ae2b  terraform_0.14.5_linux_arm.zip
d3cab7d777eec230b67eb9723f3b271cd43e29c688439e4c67e3398cdaf6406b  terraform_0.14.5_linux_arm64.zip
67b153c8c754ca03e3f8954b201cf27ec31387c8d3c77d302d647417bc4a23f4  terraform_0.14.5_openbsd_386.zip
062fbc3f596490e33e6493a8e186ae50e7b6077ac2a842392991d918189187fc  terraform_0.14.5_openbsd_amd64.zip
f66920ffedd7e81cd116d185ada479ba466f5514f8b20194cc180d3c6184e060  terraform_0.14.5_solaris_amd64.zip
f8bf1fca0ef11a33955d225198d1211e15827d43488cc9174dcda14d1a7a1d19  terraform_0.14.5_windows_386.zip
5d25f9afc71fc49d5f3e8c7ccc3ccd83a840c56e7a015f55f321fc970a73050b  terraform_0.14.5_windows_amd64.zip

=============================
# ...
--

* The following example should result in a source value set to `Hello World` (e.g. the 2nd line of the file `./README.md`):
+
[source,yaml]
.source-file.yaml:
--
sources:
  ContentFromLocalFile:
    kind: file
    spec:
      file: README.md
      line: 2
--
+
[source,markdown]
.README.md:
--
# Read Me
Hello world
--
+
[source,shell]
--
$ updatecli apply
# ...

SOURCES
=======

ContentFromLocalFile
--------------------
✔ Content: found from file "README.md":
Hello world

=============================
# ...
--

* The following example should result in a source value set to `2899f47860b7752e31872e4d57b1c03c99de154f12f0fc84965e231bc50f312f  terraform_0.14.5_linux_amd64.zip` (e.g. the only line matching the pattern `'.*terraform_.*_linux_amd64.*'` at the URL `https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS`):
+
[source,yaml]
.updateCli.yaml:
--
sources:
  ContentFromURL:
    kind: file
    spec:
      file: https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS
      matchpattern: '.*terraform_.*_linux_amd64.*'
--
+
[source,shell]
--
$ updatecli apply
# ...

SOURCES
=======

ContentFromURL
--------------
✔ Content: found from file "https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS":
2899f47860b7752e31872e4d57b1c03c99de154f12f0fc84965e231bc50f312f  terraform_0.14.5_linux_amd64.zip

=============================
# ...
--

== File Condition

It checks that the content of the file matches the specified content and continue the pipeline execution,
or fails the pipeline (and never run the pipeline's targets).


⚠️ A file condition rejects the following attributes with a validation error:

* `forcecreate` - "only supported for targets"
* `replacepattern` - "only supported for targets"

NOTE: As with a source, `template` is silently ignored in a condition rather than rejected.

=== Condition Input Value

The "Input Value" is the string to compare with the specified file content.

By default, the input value is set to the input source value associated to the condition
(e.g. either the source specified with the `sourceid` attribute or the only source if the pipeline only have one).

Alternatively you can disable the source input value with `disablesourceinput: true` and specify a custom content  with the `spec.content` attribute (see examples below).

⚠️ If both a source input value and a `spec.content` are detected, then the condition fails with an error.

=== Filtering File Condition Content

You may filter the content of the file to be compared to the <<Input Value>> with one of the options below:

* If the attribute `spec.line` is specified, then the input value is only compared to the content of this line
* If the attribute `spec.matchpattern` is specified, then the input value is only compared to the matched content in the file
** The specified pattern must be a valid https://pkg.go.dev/regexp[Golang regexp]

=== Condition Examples

* The following example returns "true" if the content of the file `./LICENSE` is the same as
the value of the source named `ContentFromURL`:
+
[source,yaml]
--
conditions:
  LocalFileHasSameContentAsSource:
    kind: file
    sourceid: ContentFromURL
    spec:
      file: LICENSE
--
+
[source,shell]
--
$ updatecli apply
# ...

CONDITIONS:
===========

LocalFileHasSameContentAsSource
-------------------------------
✔ Content of the file "LICENSE" is the same as the input source value.

=============================
--

* The following example returns "true" if the content of the URL `https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS` is the same as
the value of the source named `checksums`:
+
[source,yaml]
.updateCli.yaml
--
# ...
conditions:
  URLHasSameContentAsSource:
    kind: file
    sourceid: checksums
    spec:
      file: https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS
--
+
[source,shell]
--
$ updatecli apply
# ...

CONDITIONS:
===========

URLHasSameContentAsSource
-------------------------------
✔ Content of the file "https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS" is the same as the input source value.

=============================
--

* The following example returns "true" if the line n°2 of the local file `README.md` is equal to the specified content `Hello world` (input value defers to `spec.content` as the input source is disabled):
+
[source,yaml]
.updateCli.yaml
--
conditions:
  LocalFileHasLineMatchingCustomContent:
    kind: file
    disablesourceinput: true
    spec:
      file: README.md
      line: 2
      content: 'Hello world'
--
+
[source,shell]
--
$ updatecli apply
# ...

CONDITIONS:
===========

LocalFileHasLineMatchingCustomContent
-------------------------------------
✔ Content of the file "README.md" (line 2) is the same as the specified content.

=============================
--

* The following example returns "true" if the line n°5 of the local file `README.md` exists (e.g. is not empty, because no source input value and no `spec.content` are specified):
+
[source,yaml]
.updateCli.yaml
--
conditions:
  LocalFileHasLine2NonEmpty:
    kind: file
    disablesourceinput: true
    spec:
      file: README.md
      line: 2
--
+
[source,shell]
--
$ updatecli apply
# ...

CONDITIONS:
===========

LocalFileHasLine2NonEmpty
-------------------------
✔ Content of the file "README.md" (line 2) is not empty and the file exists.

=============================
--

* The following example returns "true" if the content from the URL `https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS` matches the pattern `'.*terraform_.*_linux.*'` (there are 4 lines matching this pattern in this example):
+
[source,yaml]
.updateCli.yaml
--
conditions:
  UrlContentMatchesPattern:
    kind: file
    disablesourceinput: true
    spec:
      file: https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS
      matchpattern: '.*terraform_.*_linux.*'
--
+
[source,shell]
--
$ updatecli apply
# ...

CONDITIONS:
===========

UrlContentMatchesPattern
------------------------
✔ Content of the file "https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS" matched the pattern ".*terraform_.*_linux.*"

=============================
--

== File Target

It writes the input value into the specified file.
The content update can be restricted (see the section <<Restricting File Content Update>> below) and the file can be created if it does not exist.

⚠️ A file target only supports local files but does not support URLs (remote files).

=== Create File When Absent

By default, a file target errors when the specified file does not exist.

If you want to force the creation of the file prior to the file target execution,
you can specify the `spec.forcecreate` attribute to `true`.

⚠️ If the attribute `spec.line` is defined along with `spec.forcecreate`, then the target is expected to fail, as it makes no sense to write a line in a file which does not exist.

=== Target Input Value

The "Input Value" is the string to write to the specified file.

* By default, the input value is set to the input source value associated to the target
(e.g. either the source specified with the `sourceid` attribute or the only source if the pipeline only have one).

* You can also specify a custom content with the `spec.content` attribute instead of using the input source value.
Using the `spec.content` is useful when you need to templatize with the source input value (see example below).

* Alternatively, you can define a Go template file path with the `spec.template` attribute to render dynamic content based on the source value.
The template is rendered using all https://masterminds.github.io/sprig/[Sprig template functions], with the source value accessible as `.source` in the template context.

* Finally, you can define a https://pkg.go.dev/regexp[Golang regexp] in the attribute `spec.ReplacePattern`,
if and only if you also specified a `spec.matchpattern` (see <<Restricting File Content Update>> and <<Target Examples>> for more details).
** Regexp's capturing group are supported (this is the recommended use case)

⚠️ The `spec.template` attribute is mutually exclusive with `spec.content`, `spec.matchpattern`, `spec.replacepattern`, and `spec.line`.

=== Restricting File Content Update

You may restrict which part of the specified file to be updated with the input value with the following options:

* If the attribute `spec.line` is specified, then the input value is only written to the specified line.
** When the input value is a multi-line string, then additional lines are inserted (the 1st line of the input value is written to the specified line, and subsequent input value's lines are inserted)

* If the attribute `spec.matchpattern` is specified, then all the matching patterns are replaced by the input value.
** The specified pattern must be a valid https://pkg.go.dev/regexp[Golang regexp]
** As described in <<Target Input Value>>, the input value can be the input source, a content string or a regexp "replace pattern"
** Please note that the matched content can be a line but also a substring!

=== Target Examples

* The following target writes the result of the input source `generatedReadMeContent` to the file `README.md` (overriding the existing content) and creates the file if does not exist:
+
[source,yaml]
.updateCli.yaml:
--
# ..
targets:
  setFileContent:
    kind: file
    sourceid: generatedReadMeContent
    spec:
      file: README.md
      forcecreate: true
--
+
[source,shell]
--
$ updatecli diff

#...
TARGETS
========

setFileContent
--------------

**Dry Run enabled**

Creating a new file at "README.md"
✔ File content for "README.md", updated.

<
---
> # ReadMe
> Hello world
--

* The following target only overrides the 3rd line of the file `versions.txt` with the templatized value specified with the `spec.content` attribute.
In this example, is defines a new version from the input source named `getMavenVersion`:
+
[source,yaml]
.updateCli.yaml:
--
# ..
targets:
  updateCopyrightYear:
    kind: file
    sourceid: getMavenVersion # Source Value is "3.8.3"
    spec:
      file: versions.txt
      line: 3
      content: maven_version = "{{ source `getMavenVersion` }}"
--
+
[source,shell]
--
$ updatecli diff

#...
TARGETS
========

setLineOfFileWithContent
------------------------

**Dry Run enabled**

✔ The line 3 of the file "versions.txt" was updated:

< maven_version = "3.6.2"
---
> maven_version = "3.8.3"
--


* The following target replaces, in the file `LICENSE`, the string occurrences matched by the pattern `Copyright \(c\) (\d*) (.\*)` by
the string `Copyright (c) 2021 $2` where `$2` is the content right-matched by `(.*)`
(e.g this example updates the year on the "Copyright" substrings to 2021 while keeping the existing content such as contributors).:
+
[source,yaml]
.updateCli.yaml:
--
# ..
targets:
  updateCopyrightYear:
    kind: file
    sourceid: whateverSource # Will be ignored as `replacepattern` is specified
    spec:
      file: LICENSE
      matchpattern: 'Copyright \(c\) (\d*) (.*)'
      replacepattern: 'Copyright (c) 2021 $2'
--
+
[source,shell]
--
$ updatecli diff

TARGETS
========

updateCopyrightYear
--------------

**Dry Run enabled**

✔ File content for "LICENSE", updated.

< Copyright (c) 2020 Olblak
---
> Copyright (c) 2021 Olblak
--


* The following target uses a Go template file to generate dynamic content based on the source value.
The template has access to all https://masterminds.github.io/sprig/[Sprig functions] and the source value via `.source`:
+
[source,yaml]
.updateCli.yaml:
--
# ..
sources:
  getLatestVersion:
    kind: shell
    spec:
      command: "echo \"1.25.1\""
targets:
  useTemplate:
    kind: file
    sourceid: getLatestVersion # Source Value is "1.25.1"
    spec:
      file: version-info.txt
      template: templates/version.tmpl
      forcecreate: true
--
+
[source,go-template]
.templates/version.tmpl:
--
Application Version Information
================================

Current Version: {{ .source }}
Updated: {{ now | date "2006-01-02" }}
Release Notes: https://example.com/releases/{{ .source }}
--
+
[source,shell]
--
$ updatecli apply


source: getLatestVersion
------------------------

The shell 🐚 command "/bin/sh /tmp/updatecli/bin/ab7e342dff922f8ba849dbf7d0c4ac2adc5855b83fe02401b781e7267f7233c6.sh" ran successfully with the following output:
----
1.25.1
----
✔ shell command executed successfully

target: useTemplate
-------------------

Creating a new file at "version-info.txt"
"version-info.txt" updated with content "Application Version Information\n================================\n\nCurrent Version: 1.25.1\nUpdated: 2026-01-21\nRelease Notes: https://example.com/releases/1.25.1\n"

--- version-info.txt
+++ version-info.txt
@@ -1 +1,6 @@
+Application Version Information
+================================
+
+Current Version: 1.25.1
+Updated: 2026-01-21
+Release Notes: https://example.com/releases/1.25.1
--

== Reference


[source,yaml]
----
---
title: Show a set of file resources as a generic example
sources:
  ContentFromLocalFile:
    kind: file
    spec:
      file: LICENSE
  ContentFromURL:
    kind: file
    spec:
      file: https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS
  LineFromLocalFile:
    kind: file
    spec:
      file: LICENSE
      line: 3
  LineFromURL:
    kind: file
    spec:
      file: https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS
      line: 3
  SingleLineFromFileWithPattern:
    kind: file
    spec:
      file: LICENSE
      matchpattern: 'Copyright.*' # Returns a single line
  MultipleLinesFromURLWithPattern:
    kind: file
    spec:
      file: https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS
      matchpattern: '.*terraform_.*_linux.*' # Returns a multi-line content as multiple lines are matched
conditions:
  LocalFileHasSameContentAsSource:
    kind: file
    sourceid: ContentFromLocalFile
    spec:
      file: LICENSE
  URLFileMatchesSpecifiedContent:
    kind: file
    disablesourceinput: true
    spec:
      file: https://get.helm.sh/helm-v3.5.0-darwin-amd64.tar.gz.sha256sum
      content: |
        53378d8de087395ece3876795111a8077f2451f080ec6150d777cc3105214520  helm-v3.5.0-darwin-amd64.tar.gz
  LocalFileHasLine:
    kind: file
    disablesourceinput: true
    spec:
      file: LICENSE
      line: 5 # The file './LICENSE' has a 5th line which is NOT empty
  URLFileHasLineMatchingSource:
    kind: file
    sourceid: LineFromURL
    spec:
      file: https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS
      line: 3 # The line 3 of the file matches the source LineFromURL
  LocalFileHasLineMatchingSource:
    kind: file
    sourceid: LineFromLocalFile
    spec:
      file: LICENSE
      line: 3 # The file './LICENSE' has a 3rd line which is NOT empty and matches the source LineFromLocalFile
  LocalFileHasLineMatchingCustomContent:
    kind: file
    disablesourceinput: true
    spec:
      file: LICENSE
      line: 3
      content: '{{ source `LineFromLocalFile` }}'
  LocalFileMatchesPattern:
    kind: file
    disablesourceinput: true
    spec:
      file: "LICENSE"
      matchpattern: 'Copyright \(c\) (\d*) Olblak'
  ######## Expected Failures
  ## Should fail condition if uncommented
  # LocalFileHasDifferentContentAsSource:
  #   kind: file
  #   sourceid: ContentFromURL
  #   spec:
  #     file: LICENSE
  # URLFileDifferentThanSpecifiedContent:
  #   kind: file
  #   disablesourceinput: true
  #   spec:
  #     file: https://releases.hashicorp.com/terraform/0.14.5/terraform_0.14.5_SHA256SUMS
  #     content: |
  #       53378d8de087395ece3876795111a8077f2451f080ec6150d777cc3105214520  helm-v3.5.0-darwin-amd64.tar.gz
  # LocalFileDoesNotHasLine:
  #   kind: file
  #   disablesourceinput: true
  #   spec:
  #     file: LICENSE
  #     line: 12345 # The file './LICENSE' does NOT have a 12345th line
  ## Should fail validation if uncommented
  # FailsToValidateBecauseMutuallyExclusiveAttributes:
  #   kind: file
  #   sourceid: ContentFromLocalFile
  #   spec:
  #     file: https://get.helm.sh/helm-v3.5.0-darwin-amd64.tar.gz.sha256sum
  #     content: |
  #       53378d8de087395ece3876795111a8077f2451f080ec6150d777cc3105214520  helm-v3.5.0-darwin-amd64.tar.gz
targets:
  setFileContent:
    kind: file
    sourceid: ContentFromURL
    spec:
      file: terraform_0.14.5_SHA256SUMS
      forcecreate: true
  setLineOfFile:
    kind: file
    sourceid: LineFromLocalFile
    spec:
      file: LICENSE
      line: 5
  setLineOfFileWithContent:
    kind: file
    sourceid: LineFromLocalFile
    spec:
      file: LICENSE
      line: 3
      content: oldline was "{{ source `LineFromLocalFile` }}"
  setLineWithMatchAndReplacePatterns:
    kind: file
    sourceid: ContentFromURL
    spec:
      file: LICENSE
      matchpattern: 'Copyright \(c\) (\d*) (.*)'
      replacepattern: 'Copyright (c) 2021 $2'
  setLineWithMatchAndContent:
    kind: file
    sourceid: ContentFromURL
    spec:
      file: LICENSE
      matchpattern: 'Copyright \(c\) (\d*) (.*)'
      content: 'Copyright (c) 2021 FooBar'
  setLineWithMatchAndSource:
    kind: file
    sourceid: ContentFromURL
    spec:
      file: LICENSE
      matchpattern: 'Copyright \(c\) (\d*) (.*)'
  useTemplate:
    kind: file
    sourceid: getLatestVersion # Source Value is "1.25.1"
    spec:
      file: version-info.txt
      template: templates/version.tmpl
----
