A purple binder, stuffed to the brim with papers. The word "Knope" is written on the front

Introduction

Knope is a CLI/CI tool which automates common tasks for developers. Things like creating changelogs, choosing and setting new versions, creating GitHub releases / tags, transitioning issues, creating and merging branches, creating pull requests... anything that is a repetitive, time-consuming task in your development cycle, this tool is made to speed up.

How it Works

Info

For some use-cases, you don't need to create a knope.toml file! If no file is detected, Knope will use the same config at runtime that it would create with knope --generate. Run knope --generate to see what you get for free, or check out the default workflows.

You create a file called knope.toml in your project directory which defines some workflows. The format of this file is described in the chapter on config, the key piece to which is the workflows array. You can get started quickly with knope --generate which will give you some starter workflows.

In order to run a workflow (whether via a custom file or a default workflow), you run knope <workflow name>. For example, knope release will run a workflow named release (and error if such a workflow does not exist). You can also run knope --help to see a list of available workflows.

CLI Arguments

Except for a few options, knope must always be run with one positional argument, the name of the workflow to be run. So knope release expects there to be a workflow named release (as there is in the default workflows). Here are all the options that can be passed, note that some of them are situational (e.g., only available when running a relevant workflow):

--help

Prints out a help message containing available workflows and options, then exits. This can be run without any positional workflow argument.

--version

Prints out the version of knope and exits. This can be run without any positional workflow argument.

--verbose

Generally makes knope spit out a lot of extra detail to stdout to help with diagnosing issues.

--generate

Generates a knope.toml file in the current directory. This cannot be used if there is already a knope.toml file present.

--upgrade

Upgrades your knope.toml file from deprecated syntax to the new syntax in preparation for the next breaking release. This can only be used if you have a knope.toml file, not if you are using the default workflows.

--validate

Checks your knope.toml to make sure every workflow in it is valid, then exits. This could be useful to run in CI to make sure that your config is always valid. The exit code of this command will be 0 only if the config is valid. This cannot be used if there is no knope.toml file present.

--dry-run

Pretends to run the selected workflow (one must be provided), but will not actually perform any work (for example, no external commands, file I/O, or API calls). Detects the same errors as --validate but also outputs info about what would happen to the standard output (likely your terminal window). For example, to see what knope release would do without creating an actual release, run knope release --dry-run.

--prerelease-label

Overrides the prerelease_label for any PrepareRelease step run. This option can only be provided after a workflow which contains a PrepareRelease step.

--override-version

Allows you to manually determine the next version for a BumpVersion or PrepareRelease instead of using a semantic versioning rule. This option can only be provided after a workflow which contains a relevant step. This has two formats, depending on whether there is one package or multiple packages.

If the single-package format is used (as it is for the default workflows, --override-version 1.0.0 will set the version to 1.0.0.

If the multi-package syntax is used (even if only one package is configured with it), you must specify the name of each package that should be overriden. For example, --override-version first-package=1.0.0 --override-version second-package=2.0.0 will set the version of first-package to 1.0.0 and second-package to 2.0.0, erroring if either of those packages is not configured.

Environment Variables

These are all the environment variables that Knope will look for when running workflows.

  1. KNOPE_PRERELEASE_LABEL works just like the --prerelease-label option. Note that the option takes precedence over the environment variable.
  2. GITHUB_TOKEN will be used to load credentials from GitHub for GitHub config.

Features

More detail on everything this program can do can be found by digging into config but here's a rough (incomplete) summary:

  1. Select issues from Jira or GitHub to work on, transition and create branches from them.
  2. Do some basic git commands like switching branches or rebasing.
  3. Bump the version of your project using semantic rules.
  4. Bump your version AND generate a Changelog entry from conventional commits.
  5. Do whatever you want by running arbitrary shell commands and substituting data from the project!

Concepts

You define a config file named knope.toml which has some metadata (e.g. package definitions) about your project, as well as a set of workflows. Each workflow consists of a series of steps that will execute in order, stopping if any step fails. Some steps require other steps to be run before they are.

The Name

Knope (pronounced like "nope") is a reference to the character Leslie Knope from the TV show Parks and Recreation. She loves doing the hard, tedious work that most people don't like doing, and she's very good at it, just like this tool!

The logo is a binder in reference to Leslie Knope's love of binders. The binder is also analogous to the knope.toml file which defines all the workflows for your project.

Installation

Knope is built in such a way that cargo-binstall can install it by downloading a binary artifact for the most popular platforms.

  1. Install cargo-binstall using your preferred method.
  2. Run cargo-binstall knope to install Knope.

Info

Is your platform not supported yet? Please contribute it by opening a pull request.

If using GitHub Actions, the easiest way to install Knope is via this action.

Download a Binary Manually

We automatically build binaries for some platforms which can be found on the Releases page.

Install via Cargo

Knope is written in Rust and published on crates.io which means it can therefore be built from source by:

  1. Installing cargo via Rustup
  2. Running cargo install knope

Warning

Building Knope can be quite slow, if possible, it's recommended to download a prebuilt binary instead.

Build from Source

  1. Install the current Rust stable toolchain via Rustup
  2. Clone the GitHub repo
  3. cargo install --path . in the cloned directory

Other

Have another method you'd prefer to use to install Knope? Let us know by opening a GitHub issue.

Default Workflows

Knope can do a lot out of the box, with no need for a config file. If no file is found, Knope will use the same config as it would create with knope --generate.

Warning

If you have a knope.toml file in your project, this page is no longer relevant to you, as default workflows are only used when no config file is found.

release

Without any config, you can run knope release to create a new release from conventional commits. This will:

  1. Update the version in any supported package files based on the semantic version determined from all commits since the last release. For more detail, see the PrepareRelease step.
  2. Update a CHANGELOG.md file (if any) with the body of relevant commits (again, see the PrepareRelease step for more detail).
  3. Commit the changes to the versioned and changelog files and push that commit.
  4. Create a release which is one of the following, see the Release step for more detail:
    1. If your remote is GitHub, create a new release on GitHub with the same body as the changelog entry. This requires a GITHUB_TOKEN environment variable to be set.
    2. If the remote is not GitHub, a tag will be created and pushed to the remote.

Additional Options

  1. --dry-run will run the workflow without modifying any files or interacting with the remote. Instead, all the steps that would happen will be printed to the screen so you can verify what will happen.
  2. --prerelease-label will tell knope to create a prerelease with a given label. For example, knope release --prerelease-label rc will create a release with the next calculated version (as if you had run knope release), but with the -rc.0 suffix (or rc.1, rc.2, etc. if you have already created a release with that label).
  3. --override-version will tell knope to use a specific version instead of calculating the next one. For example, knope release --override-version 1.2.3 will create a release with the version 1.2.3. This is especially useful when moving from a 0.x.x version to 1.0.0.

document-change

Without any config, you can run knope document-change to run the CreateChangeFile step. Because there is only one package configured by default, this step will be skipped and a special, default package will be used.

Additional Options

  1. --dry-run will run the workflow without modifying any files or interacting with the remote. Instead, all the steps that would happen will be printed to the screen so you can verify what will happen.

Config

This is the top level structure that your knope.toml must follow to be valid. If you have a knope.toml in the working directory, and it isn't valid, you'll get an error right off the bat.

Note

For basic workflows, you don't need a config file! Check out the default workflows to see if those work for you first!.

Example

[package]
# Defined set of files to bump using semantic versioning and conventional commits.

[[workflows]]
name = "First Workflow"
# Details here

[[workflows]]
name = "Second Workflow"
# Details here

[jira]
# Jira config here

[github]
# GitHub config here

Info

You can generate a basic config file using knope --generate.

When you first start knope, you will be asked to select a workflow to run. In the above example, this would look something like:

? Select a workflow
> First Workflow
  Second Workflow

You can use your arrow keys to then select an option to run. The > symbol indicates which workflow is selected. Pressing the Enter key on your keyboard will run the workflow.

See Also

  • Packages for details on [package]
  • Workflows for details on defining entries to the [[workflows]] array
  • Jira for details on defining [jira]
  • GitHub for details on defining [github]

Workflow

A workflow is the entrypoint to doing work with knope. Once you start running knope you must immediately select a workflow (by name) to be executed.

Each workflow is defined in the [[workflows]] array in your knope.toml file. Each entry contains a name attribute which is how the workflow will be displayed when running knope. There is also an array of steps declared as [[workflows.steps]] which define the individual actions to take.

Example

# knope.toml

[[workflows]]
name = "My First Workflow"
    [[workflows.steps]]
    # First step details here
    [[workflows.steps]]
    # second step details here

See Also

  • Step for details on how each [[workflows.steps]] is defined.

Step

A step is the atomic unit of work that knope operates on. In a workflow, steps will be executed sequentially until one fails or all steps are completed.

In it's simplest form, a step is declared in a workflow like this:

[[workflows]]
name = "My Workflow"
    [[workflows.steps]]
    type = "AStepType"
    more_info = "Something"

Where type matches one of the available steps listed below. Some steps also can take additional parameters in config, those go right underneath type like more_info above.

Available Steps

CreateChangeFile step

A "change file" is a specially formatted Markdown file that is used both to determine the next version of your project and to generate a changelog. This step will interactively create a new change file in the .changeset directory of your project (creating that directory if missing). When a PrepareRelease step runs, it will combine both change files and any conventional commits since the last release to generate changelogs and update versions for any configured packages.

Example

Note

The default workflows include an document-change workflow that will run this step for you. If you do not already have a knope.toml file, you do not need to create one to use this feature.

With a knope.toml file that looks like this:

[packages.first]
versioned_files = ["first/Cargo.toml"]
changelog = "first/CHANGELOG.md"
extra_changelog_sections = [
    { name = "Poems 🎭", types = ["poem"] }
]

[packages.second]
versioned_files = ["second/Cargo.toml"]
changelog = "second/CHANGELOG.md"

[[workflows]]
name = "document-change"

[[workflows.steps]]
type = "CreateChangeFile"

You could run knope document-change to start a new change file. First, you will be prompted to select the packages that this change affects, for this example, we check off both packages.

- [x] first
- [x] second

Note

If there is only one package, this step is skipped and a special, default package is automatically selected. This is the case when you do not have a knope.toml file.

For each package, you will be prompted to select the type of change you are documenting. The available change types are "Breaking", "Feature", "Fix", and any of the custom types configured in extra_changelog_sections.types. For the first package, this will look like:

Enter the type for the `first` package:

Breaking
Feature
Fix
> poem

Warning

The prompt names were chosen to better reflect the type of changes (and corresponding changelog entries), but the value written o the change file will match the standard change types.

For the second package, we would not have the poem option. Next, you will be prompted write a short summary of the change (a few words). The summary will be used both as the name of the file and a header in the changelog generated by PrepareRelease.

Let's say we enter the summary `[i carry your heart with me(i carry it in]`, this step will then generate a file .changeset/i_carry_your_heart_with_mei_carry_it_in.md with the following contents:

---
first: poem
second: major
---

#### `[i carry your heart with me(i carry it in]`

If that brief summary is not enough, you should then edit this file and add more detail below the generated heading, using all the Markdown features you want!

---
first: poem
second: major
---

#### `[i carry your heart with me(i carry it in]`

**E. E. Cummings**

<pre>
i carry your heart with me(i carry it in
my heart)i am never without it(anywhere
i go you go,my dear;and whatever is done
by only me is your doing,my darling)
                                   i fear
no fate(for you are my fate,my sweet)i want
no world(for beautiful you are my world,my true)
and it’s you are whatever a moon has always meant
and whatever a sun will always sing is you

here is the deepest secret nobody knows
(here is the root of the root and the bud of the bud
and the sky of the sky of a tree called life;which grows
higher than soul can hope or mind can hide)
and this is the wonder that's keeping the stars apart

i carry your heart(i carry it in my heart)
</pre>

When you are done, you can run knope document-change again to create another change file. When you are ready to release, run PrepareRelease to combine all the change files and conventional commits into a changelog and update the versions of any configured packages. The type of the change for each package will determine where it is placed in the changelog: so first/CHANGELOG.md will have a ### Poems 🎭 section and second/CHANGELOG.md will have a ### Breaking Changes section, each containing the summary and body of the change.

For completeness, this is what the changelog for first would look like (if there had been no other changes):

### Poems 🎭

#### `[i carry your heart with me(i carry it in]`

**E. E. Cummings**

<pre>
i carry your heart with me(i carry it in
my heart)i am never without it(anywhere
i go you go,my dear;and whatever is done
by only me is your doing,my darling)
                                   i fear
no fate(for you are my fate,my sweet)i want
no world(for beautiful you are my world,my true)
and it’s you are whatever a moon has always meant
and whatever a sun will always sing is you

here is the deepest secret nobody knows
(here is the root of the root and the bud of the bud
and the sky of the sky of a tree called life;which grows
higher than soul can hope or mind can hide)
and this is the wonder that's keeping the stars apart

i carry your heart(i carry it in my heart)
</pre>

CreatePullRequest

Create a pull request on GitHub from the current branch to a specified branch. If a pull request for those already exists, this step will overwrite the title and body of the existing pull request.

Parameters

base

The branch to create the pull request against. This is required.

title.template

A template string for the title of the pull request. This is required.

title.variables

An optional map of variables to use in the title template.

body.template

A template string for the body of the pull request. This is required.

body.variables

An optional map of variables to use in the body template.

Example

An example workflow which creates a pull request from the current branch to main using the current version of the package as the title and the changelog entry for the current version as the body:

[[workflows]]
name = "create-release-pull-request"

[[workflows.steps]]
type = "CreatePullRequest"

[workflows.steps.base]
default = "main"

[workflows.steps.title]
template = "chore: Release $version"
variables = { "$version" = "Version" }

[workflows.steps.body]
template = "Merging this PR will release the following:\n\n$changelog"
variables = { "$changelog" = "ChangelogEntry" }

For a full example of how this might be used with GitHub Actions to help automate releases, check out Knope's prepare-release workflow and Knope's release workflow.

PrepareRelease step

This step:

  1. Looks through all commits since the last version tags and parses any Conventional Commits it finds.
  2. Reads any Changesets in the .changeset folder (which you can create via CreateChangeFile). Those files are deleted after being read.
  3. Bumps the semantic version of any packages that have changed.
  4. Adds a new entry to any affected changelog files.
  5. Stages all files modified by this step with Git (effectively, git add <file> for versioned files, changelogs, and changesets). This step does not commit the changes.

When multiple packages are configured—PrepareRelease runs for each package independently. The version tag for that package will be the starting point.

Note

The last "version tag" is used as the starting point to read commits—that's the most recent tag that was created by the Release step. See that step for details on the tagging formats.

Limitations

  • The Changelog format is pretty strict. Sections will only be added for Conventional Commits and Changesets that meet certain requirements. See Changelog sections below.
  • Knope uses a simpler subset of semantic versioning which you can read about in BumpVersion
  • Knope will not allow you to update the major version of a go.mod file in most cases, as the recommended practice is to create a new go.mod file (in a new directory) for each major version. You can override this behavior using the --override-version option (to go from v1 to v2) or use multiple packages to support multiple go.mod files on different major versions.

Options

  • allow_empty: If set to true, this step will not fail if there are no changes to release. Defaults to false.

Mono-repos and multiple packages

You can have multiple packages in one repo. By default, changesets work with multiple packages and conventional commits apply to all packages. If you want to target specific conventional commits at individual packages, you need to use a conventional commit scope. This is done by adding a scopes array to the packages config and adding a conventional commit scope to the commits that should not apply to all packages. The following rules apply, in order, with respect to conventional commit scopes:

  1. If no packages define scopes in their config, all commits apply to all packages. Scopes are not considered by knope.
  2. If a commit does not have a scope, it applies to all packages.
  3. If a commit has a scope, and any package has defined a scopes array, the commit will only apply to those packages which have that scope defined in their scopes array.

Changelog format

Note

Need more changelog flexibility in order to adopt Knope? Open an issue!

Version titles

The title of each version is a combination of its semantic version (e.g., 1.2.3) and the UTC date of when it was released (e.g., (2017-04-09)). UTC is used for simplicity—in practice, the exact day of a release is not usually as important as the general timing. By default, the version will be a level two header (e.g., ## 1.2.3 (2017-04-09)), however, if your previous version was a level one header (e.g., # 1.2.2 (2017-04-08)), the new version will also be a level one header.

Change sections

Sections are only added to the changelog for each version as needed—if there are no commits that meet the requirements for a section, that section will not be added. The built-in sections will be added (when needed) in the following order:

  1. Breaking Changes for anything that triggers a major semantic version increase.
    1. Any commit whose type/scope end in ! will land in this section instead of their default section (if any). So fix!: a breaking fix will add the note "a breaking fix" to this section and nothing to the "Fixes" section.
    2. If the special BREAKING CHANGE footer is used in any commit, the message from that footer (not the main commit message) will be added here. The main commit message will be added as appropriate to the normal section. So a fix: commit with a BREAKING CHANGE footer creates entries in both the Fixes section and the Breaking Changes section.
    3. Any changeset with a change type of major (selecting "Breaking" in CreateChangeFile)
  2. Features for any commit with type feat (no !) or change type minor (selecting "Feature" in CreateChangeFile)
  3. Fixes for any commit with type fix (no !) or change type patch (selecting "Fix" in CreateChangeFile)
  4. Notes for any footer in a conventional commit called Changelog-Note.

After the built-in sections, any custom sections will be added in the order they are defined in the configuration.

Each section will be formatted as a header one level below the version header (see "Version titles" above). So if the version title is a level two header (e.g., ## 1.2.3), each section will be a level three header (e.g., ### Features).

Overriding the default sections

The default sections cannot be disabled, but their names can be changed via configuration. Specifically:

  • Breaking Changes can be changed by setting a custom section for the "major" type
  • Features can be changed by setting a custom section for the "minor" type
  • Fixes can be changed by setting a custom section for the "patch" type
  • Notes can be changed by setting a custom section for the "Changelog-Note" footer

A config which overrides all of these would look like this:

[package]
extra_changelog_sections = [
    { type = "major", name = "❗️Breaking ❗" },
    { type = "minor", name = "🚀 Features" },
    { type = "patch", name = "🐛 Fixes" },
    { footer = "Changelog-Note", name = "📝 Notes" },
]

Warning

By doing this, you are also overriding the built-in ordering of sections. Make sure to define any custom sections (whether overriding or not) in the order you want them to appear.

Versioning

Versioning is done with the same logic as the BumpVersion step, but the rule is selected automatically based on the commits since the last version tag and the files present in the .changeset directory. Generally, rule selection works as follows:

  1. If there are any breaking changes (things in the ### Breaking Changes section above), the Major rule is used.
  2. If no breaking changes, but there are any features (things in the ### Features section above), the Minor rule is used.
  3. If no breaking changes or features, but there are entries to add to the changelog (fixes, notes, or custom sections) the Patch rule is used.
  4. If there are no new entries to add to the changelog, the version will not be increased, and this step will throw an error (unless the --dry-run option is set).

Examples

Creating a Pre-release Version

If you include the prerelease_label option, the version created will be a pre-release version (treated like Pre rule in BumpVersion). This allows you to collect the commits so far to an impending future version to get them out earlier.

[package]
versioned_files = ["Cargo.toml"]
changelog = "CHANGELOG.md"

[[workflows]]
name = "prerelease"

[[workflows.steps]]
type = "PrepareRelease"
prerelease_label = "rc"

If your prerelease workflow is exactly like your release workflow, you can instead temporarily add a prerelease label by passing the --prerelease-label option or by setting the KNOPE_PRERELEASE_LABEL environment variable. This option overrides any set prerelease_label for any workflow run.

Going from Pre-release to Full Release

Let's say that in addition to the configuration from the above example, you also have a section like this:

[[workflows]]
name = "release"

[[workflows.steps]]
type = "PrepareRelease"

And your changelog looks like this (describing some pre-releases you already have):

## 2.0.0-rc.1 (2024-03-14)

### Bug Fixes

- A bug in the first `rc` that we fixed.

## 2.0.0-rc.0 (2024-02-29)

### Breaking Changes

- Cool new API

## 1.14.0 (2023-12-25)

The last 1.x release.

Now you're ready to release 2.0.0—the version that's going to come after 2.0.0-rc.1. If you run the defined release rule, it will go all the way back to the tag v1.14.0 and use the commits from that point to create the new version. In the end, you'll get version 2.0.0 with a new changelog entry like this:

## 2.0.0 (2024-04-09)

### Breaking Changes

- Cool new API

### Bug Fixes

- A bug in the first `rc` that we fixed.

Multiple Packages with Scopes

Here's a knope config with two packages: cli and lib.

[package.cli]
versioned_files = ["cli/Cargo.toml"]
changelog = "cli/CHANGELOG.md"
scopes = ["cli"]

[package.lib]
versioned_files = ["lib/Cargo.toml"]
changelog = "lib/CHANGELOG.md"
scopes = ["lib"]

[[workflows]]
name = "release"

[[workflows.steps]]
type = "PrepareRelease"

The cli package depends on the lib package, so they will likely change together. Let's say the version of cli is 1.0.0 and the version of lib is 0.8.9. We add the following commits:

  1. feat(cli): Add a new --help option to display usage and exit
  2. feat(lib)!: Change the error type of the parse function
  3. fix: Prevent a crash when parsing invalid input

The first two commits are scoped—they will only apply to the packages which have those scopes defined in their scopes array. The third commit is not scoped, so it will apply to both packages.

Note

Here, the configured scopes are the same a the name of the package. This is common, but not required.

When the release workflow is run, the cli package will be bumped to 1.1.0 and the lib package will be bumped to 0.9.0. The changelog for cli will look like this:

## 1.1.0 (2022-04-09)

### Features

- Add a new --help option to display usage and exit

### Fixes

- Prevent a crash when parsing invalid input

And the changelog for lib will look like this:

## 0.9.0 (2022-03-14)

### Breaking Changes

- Change the error type of the parse function

### Fixes

- Prevent a crash when parsing invalid input

Errors

The reasons this can fail:

  1. The version could not be bumped for some reason.
  2. The packages section is not configured correctly.
  3. There was nothing to release and allow_empty was not set to true. In this case it exits immediately so that there aren't problems with later steps.

Release Step

Release the configured packages which need to be released. If there is a GitHub config set, this creates a release on GitHub with the same release notes that were added to the changelog (if any). Otherwise, this tags the current commit as a release. In either case, a new Git tag will be created with the package's tag format. You should run PrepareRelease before this step, though not necessarily in the same workflow. PrepareRelease will update the package versions without creating a release tag. Release will create releases for any packages whose current versions do not match their latest release tag.

Tagging Format

Whenever this step is run, it will tag the current commit with the new version for each package. If only one package is defined (via the [package] section in knope.toml), this tag will be v{version} (e.g., v1.0.0 or v1.2.3-rc.4).

If multiple packages are defined, each package gets its own tag in the format {package_name}/v{version} (this is the syntax required for Go modules). See examples below for more illustration.

Warning

A note on Go modules

Knope does its best to place nicely with Go's requirements for tagging module releases, however there are cases where Knope's tagging requirements will conflict with Go's tagging requirements. In particular, if you have a package named blah which does not contain the blah/go.mod file, and a package named something_else which contains the blah/go.mod file, then both packages are going to get the blah/v{Version} tags, causing runtime errors during this step. If you have named packages, it's important to ensure that either:

  1. No package names match the name of a go module
  2. All packages with the same name as a go module contain the go.mod file for that module

GitHub Release Notes

There are several different possible release notes formats, depending on how this step is used:

  1. If run after a PrepareRelease step in the same workflow, the release notes will be the same as the changelog section created by PrepareRelease even if there is no changelog file configured—with the exception that headers are reduced by one level (for example, #### becomes ###).
  2. If run in a workflow with no PrepareRelease step before it (the new version was set another way), and there is a changelog file for the package, the release notes will be taken from the relevant changelog section. This section header must match exactly what PrepareRelease would have created. Headers will be reduced by one level (for example, #### becomes ###).
  3. If run in a workflow with no PrepareRelease step before it (the new version was set another way), and there is no changelog file for the package, the release will be created using GitHub's automatic release notes generation.

GitHub Release Assets

You can optionally include any number of assets which should be uploaded to a freshly-created release via package assets. If you do this, the following steps are taken:

  1. Create the release in draft mode
  2. Upload the assets one at a time
  3. Update the release to no longer be a draft (published)

If you have any follow-up workflows triggered by GitHub releases, you can use on: release: created to run as soon as the draft is created (without assets) or on: release: published to run only after the assets are done uploading.

Errors

This step will fail if:

  1. GitHub config is set but Knope cannot create a release on GitHub. For example:
    1. There is no GitHub token set.
    2. The GitHub token does not have permission to create releases.
    3. The release already exists on GitHub (causing a conflict).
  2. There is no GitHub config set and Knope cannot tag the current commit as a release.
  3. Could not find the correct changelog section in the configured changelog file for loading release notes.
  4. One of the configured package assets does not exist.

Examples

Create a GitHub Release for One Package

Here's a simplified version of the release workflow used for Knope.

[package]
versioned_files = ["Cargo.toml"]
changelog = "CHANGELOG.md"

[[workflows]]
name = "release"

# Generates the new changelog and Cargo.toml version based on conventional commits.
[[workflows.steps]]
type = "PrepareRelease"

# Commit the changes that PrepareRelease added
[[workflows.steps]]
type = "Command"
command = "git commit -m \"chore: Bump to version\""
variables = {"version" = "Version"}

# Push the changes to GitHub so the created tag will point to the right place.
[[workflows.steps]]
type = "Command"
command = "git push"

# Create a GitHub release with the new version and release notes created in PrepareRelease. Tag the commit just pushed with the new version.
[[workflows.steps]]
type = "Release"

[github]
owner = "knope-dev"
repo = "knope"

If PrepareRelease set the new version to "1.2.3", then a GitHub release would be created called "1.2.3" with the tag "v1.2.3".

Git-only Release for One Package

Here's what Knope's config might look like if it were not using GitHub releases:

[package]
versioned_files = ["Cargo.toml"]
changelog = "CHANGELOG.md"

[[workflows]]
name = "release"

# Generates the new changelog and Cargo.toml version based on conventional commits.
[[workflows.steps]]
type = "PrepareRelease"

# Commit the changes that PrepareRelease made
[[workflows.steps]]
type = "Command"
command = "git commit -m \"chore: Bump to version\""
variables = {"version" = "Version"}

# Create a Git tag on the fresh commit (e.g., v1.2.3)
[[workflows.steps]]
type = "Release"

# Push the commit and the new tag to our remote repository.
[[workflows.steps]]
type = "Command"
command = "git push && git push --tags"

If PrepareRelease set the new version to "1.2.3", then a Git tag would be created called "v1.2.3".

Create GitHub Releases for Multiple Packages

[packages.knope]
versioned_files = ["knope/Cargo.toml"]
changelog = "knope/CHANGELOG.md"

[packages.knope-utils]
versioned_files = ["knope-utils/Cargo.toml"]
changelog = "knope-utils/CHANGELOG.md"

[[workflows]]
name = "release"

# Updates both Cargo.toml files with their respective new versions
[[workflows.steps]]
type = "PrepareRelease"

# Commit the changes that PrepareRelease made
[[workflows.steps]]
type = "Command"
command = "git commit -m \"chore: Prepare releases\""

# Push the changes to GitHub so the created tag will point to the right place.
[[workflows.steps]]
type = "Command"
command = "git push"

# Create a GitHub release for each package.
[[workflows.steps]]
type = "Release"

[github]
owner = "knope-dev"
repo = "knope"

If PrepareRelease set the new version of the knope package to "1.2.3" and knope-utils to "0.4.5", then two GitHub release would be created:

  1. "knope 1.2.3" with tag "knope/v1.2.3"
  2. "knope-utils 0.4.5" with tag "knope-utils/v0.4.5"

Create a GitHub Release with Assets

See Knope's release workflow and knope.toml where we:

  1. Prep the release to get the new version and changelog
  2. Commit the changes
  3. Fan out into several jobs which each check out the changes and build a different binary
  4. Create a GitHub release with the new version, changelog, and the binary assets

BumpVersion step

Bump the version of all packages using a Semantic Versioning rule. At least one package must be defined for this step to operate on.

Note

It may be easier to select the appropriate version automatically using conventional commits. You can do this with the PrepareRelease step instead of this one.

Fields

  1. rule: The Semantic Versioning rule to use.
  2. label: Only applicable to Pre rule. The pre-release label to use.

Examples

Single Package Pre-Release

[package]
versioned_files = ["Cargo.toml"]

[[workflows]]
name = "pre-release"

[[workflows.steps]]
type = "BumpVersion"
rule = "Pre"
label = "rc"

With this particular example, running knope pre-release would bump the version in Cargo.toml using the "pre" rule and the "rc" label. So if the version was 0.1.2-rc.0, it would be bumped to 0.1.2-rc.1.

Multiple Packages

This step runs for each defined package independently.

[packages.knope]
versioned_files = ["knope/Cargo.toml"]

[packages.knope-utils]
versioned_files = ["knope-utils/Cargo.toml"]

[[workflows]]
name = "major"

[[workflows.steps]]
type = "BumpVersion"
rule = "Major"

In this example, running knope major would bump the version in knope/Cargo.toml and knope-utils/Cargo.toml using the "major" rule. If the versions in those files were 0.1.2 and 3.0.0 respectively, they would be bumped to 0.2.0 and 4.0.0 respectively.

Rules

Major

Increment the Major component of the semantic version and reset all other components (e.g. 1.2.3-rc.4 -> 2.0.0).

Minor

Increment the Minor component of the semantic version and reset all lesser components (e.g. 1.2.3-rc.4 -> 1.3.0 ).

Patch

Increment the Patch component of the semantic version and reset all lesser components (e.g. 1.2.3-rc.4 -> 1.2.4).

Pre

Increment the pre-release component of the semantic version or add it if missing. You must also provide a label parameter to this rule which will determine the pre-release string used. For example, running this rule with the label "rc" would change "1.2.3-rc.4" to "1.2.3-rc.5" or "1.2.3" to "1.2.4-rc.0".

Warning

Only a very specific pre-release format is supported—that is MAJOR.MINOR.PATCH-LABEL.NUMBER. For example, 1.2.3-rc.4 is supported, but 1.2.3-rc4 is not. LABEL must be specified via config or the --prerelease-label option in the CLI. NUMBER starts at 0 and increments each time the rule is applied.

Release

Remove the pre-release component of the semantic version (e.g. 1.2.3-rc.4 -> 1.2.3).

A Note on 0.x Versions

Semantic versioning dictates different handling of any version which has a major component of 0 (e.g. 0.1.2). This major version should not be incremented to 1 until the project has reached a stable state. As such, it would be irresponsible (and probably incorrect) for knope to increment to version 1.0.0 the first time there is a breaking change in a 0.x project. As such, any Major rule applied to a 0.x project will increment the Minor component, and any Minor rule will increment the Patch component. This effectively means that for the version 0.1.2:

  1. The first component (0) is ignored
  2. The second component (1) serves as the Major component, and will be incremented whenever the Major rule is applied.
  3. The third component (2) serves as both Minor and Patch and will be incremented when either rule is applied.

If you want to go from a 0.x version to a 1.x version, you must provide the --override-version command line option.

Errors

This step will fail if any of the following are true:

  1. A malformed version string is found while attempting to bump. Note that only a subset of pre-release version formats are supported.
  2. No package is defined missing or invalid.

Command step

Run a command in your current shell after optionally replacing some variables. This step is here to cover the infinite things you might want to do that knope does not yet know how to do itself. If you have a lot of these steps or a complex command, we recommend you write a script in something like Bash or Python, then simply call that script with a command.

Example

If the current version for your project is "1.0.0", the following workflow step will run git tag v.1.0.0 in your current shell.

[[workflows.steps]]
type = "Command"
command = "git tag v.version"
variables = {"version" = "Version"}

Variables

The variables attribute of this step is an object where the key is the string you wish to substitute and the value is one of the available variables. take care when selecting a key to replace as any matching string that is found will be replaced. Replacements occur in the order they are declared in the config, so earlier substitutions may be replaced by later ones.

SelectJiraIssue

Search for Jira issues by status and display the list of them in the terminal. User is allowed to select one issue which can then be used in future steps in this workflow (e.g., Command or SwitchBranches).

Errors

This step will fail if any of the following are true:

  1. knope cannot communicate with the configured Jira URL.
  2. User does not select an issue (e.g. by pressing Esc).
  3. There is no Jira config set.

Example

[[workflows]]
name = "Start some work"
    [[workflows.steps]]
    type = "SelectJiraIssue"
    status = "Backlog"

TransitionJiraIssue Step

Transition a Jira issue to a new status.

Errors

This step will fail when any of the following are true:

  1. An issue was not previously selected in this workflow using SelectJiraIssue or SelectIssueFromBranch.
  2. Cannot communicate with Jira.
  3. The configured status is invalid for the issue.

Example

[[workflows]]
name = "Start some work"
    [[workflows.steps]]
    type = "SelectJiraIssue"
    status = "Backlog"

    [[workflows.steps]]
    type = "TransitionJiraIssue"
    status = "In Progress"

SelectGitHubIssue Step

Search for GitHub issues by status and display the list of them in the terminal. Selecting an issue allows for other steps to use the issue's information (e.g., SwitchBranches).

Errors

This step will fail if any of the following are true:

  1. knope cannot communicate with GitHub.
  2. There is no GitHub config set.
  3. User does not select an issue.

Example

[[workflows]]
name = "Start some work"
    [[workflows.steps]]
    type = "SelectGitHubIssue"
    label = "selected"

SelectIssueFromBranch step

Attempt to parse issue info from the current branch for use in other steps (e.g., Command).

Errors

This step will fail if the current git branch cannot be determined or the name of that branch does not match the expected format. This is only intended to be used on branches which were created using the SwitchBranches step.

Example

[[workflows]]
name = "Finish some work"
    [[workflows.steps]]
    type = "SelectIssueFromBranch"

    [[workflows.steps]]
    type = "TransitionJiraIssue"
    status = "QA"

SwitchBranches step

Uses the name of the currently selected issue to checkout an existing or create a new branch for development. If an existing branch is not found, the user will be prompted to select an existing local branch to base the new branch off of. Remote branches are not shown.

Errors

This step fails if any of the following are true.

  1. An issue was not previously selected in this workflow using SelectJiraIssue or SelectGitHubIssue.
  2. Current directory is not a Git repository
  3. There is uncommitted work on the current branch. You must manually stash or commit any changes before performing this step.

Example

[[workflows]]
name = "Start some work"
    [[workflows.steps]]
    type = "SelectJiraIssue"
    status = "Backlog"

    [[workflows.steps]]
    type = "SwitchBranches"

RebaseBranch step

Rebase the current branch onto the branch defined by to.

Errors

Fails if any of the following are true:

  1. The current directory is not a Git repository.
  2. The to branch cannot be found locally (does not check remotes).
  3. The repo is not on the tip of a branch (e.g. detached HEAD)
  4. Rebase fails (e.g. not a clean working tree)

Example

[[workflows]]
name = "Finish some work"
    [[workflows.steps]]
    type = "RebaseBranch"
    to = "main"

Variables

Some steps, notably Command and CreatePullRequest allow you to use variables in their configuration. Typically, this allows for string substitution with some context that Knope has. Variables are always configured by providing both the string that should be replaced and the name of the variable that should replace it, so you can customize your own syntax. For example, if you wanted to insert the current package version into a command, you might provide a {"version": "Version"} variable config. This would replace any instance of the string version with Version. If you wanted a bash-like syntax, you might use {"$version": "Version"} instead—pick whatever works best for you.

Version

Version will attempt to parse the current package version and substitute that string. For example, you might use this to get the new version after running a PrepareRelease step.

Warning

This variable can only be used when a single [package] is configured, there is currently no equivalent for multi-package projects.

ChangelogEntry

ChangelogEntry is the content of the changelog (if any) for the version that is indicated by the Version variable. This follows the same rules as the Release step for creating a GitHub changelog, with the exception that it cannot use GitHub's auto-generated release notes. If no changelog entry can be found, the step fails.

Warning

This variable can only be used when a single [package] is configured, there is currently no equivalent for multi-package projects.

IssueBranch

IssueBranch will provide the same branch name that the SwitchBranches step would produce. You must have already selected an issue in this workflow using SelectJiraIssue, SelectGitHubIssue, or SelectIssueFromBranch before using this variable.

Packages

Packages are how you tell knope about collections of files that should be tracked with semantic versioning. At least one package must be defined in order to run the BumpVersion or PrepareRelease steps.

There are two ways to define packages, if you only have one package, you define it like this:

[package]
# package config here

If you have multiple packages, you define them like this:

[packages."<name>"]  # where you replace <name> with the name of the package
# package config here

[packages."<other_name>"]  # and so on
# package config here

Syntax

Each package, whether it's defined in the [package] section or in the [packages] section, can have these keys:

  1. versioned_files is an optional array of files you'd like to bump the version of. They all must have the same version—as a package only has one version.
  2. changelog is the (optional) Markdown file you'd like to add release notes to.
  3. scopes is an optional array of conventional commit scopes which should be considered for the package when running the PrepareRelease step.
  4. extra_changelog_sections is an optional array of extra sections that can be added to the changelog when running the PrepareRelease step.
  5. assets is a list of files that should be included in the release along with the name that should appear with them. These are only used for GitHub releases by the Release step.

versioned_files

A package, by Knope's definition, has a single version. There can, however, be multiple files which contain this version (e.g., Cargo.toml for a Rust crate and pyproject.toml for a Python wrapper around it). As such, you can define an array of versioned_files for each package as long as they all have the same version and all are supported formats. If no file is included in versioned_files, the latest Git tag in the format created by the Release step will be used. The file must be named exactly the way that knope expects, but it can be in nested directories. The supported file types (and names) are:

  1. Cargo.toml for Rust projects
  2. pyproject.toml for Python projects using PEP-621 or Poetry
  3. package.json for Node projects
  4. go.mod for Go projects using modules

A special note on go.mod

Go modules don't normally have their entire version in their go.mod file, only the major component and only if that component is greater than 1. However, this makes it difficult to track versions, specifically between PrepareRelease and Release if they are run in separate workflows. To bypass this, Knope will add a comment in the module line after the module path containing the full version—like module github.com/knope-dev/knope // v0.0.1. If a version exists in that format, it will be used. If not, the version will be determined by the latest Git tag.

Updating the version of a go.mod file with Knope will completely rewrite the module line, adding in the expected comment syntax. If you have another comment here, you'll want to move it before running Knope. If you have a suggestion for how to improve versioning for Go, please open an issue.

Other file formats

Want to bump the version of a file that isn't natively supported? Request it as a feature and, in the meantime, you can write a script to manually bump that file with the version produced by BumpVersion or PrepareRelease using a Command step, like this:

[package]
versioned_files = []  # With no versioned_files, the version will be determined via Git tag
changelog = "CHANGELOG.md"

[[workflows]]
name = "release"

[[workflows.steps]]
type = "PrepareRelease"

[[workflows.steps]]
type = "Command"
command = "my-command-which-bumps-a-custom-file-with version"
variables = { "version" = "Version" }

Warning

The Version variable in the Command step cannot be used when multiple packages are defined. This is a temporary limitation—if you have a specific use case for this, please file an issue.

extra_changelog_sections

You may wish to add more sections to a changelog than the defaults, you can do this by configuring custom conventional commit footers and/or changeset types to add notes to new sections in the changelog.

By default, the commit footer Changelog-Note adds to the Notes section—the configuration to do that would look like this:

[package]
versioned_files = []
changelog = "CHANGELOG.md"
extra_changelog_sections = [
  { name = "Notes", footers = ["Changelog-Note"] }
]

To leverage that same section for changeset types, we could add the types key:

[package]
versioned_files = []
changelog = "CHANGELOG.md"
extra_changelog_sections = [
  { name = "Notes", footers = ["Changelog-Note"], types = ["note"] }
]

assets

Assets is a list of files to upload to a GitHub release. They do nothing without GitHub configuration. Assets are per-package. Each asset can optionally have a name, this is what it will appear as in GitHub releases. If name is omitted, the final component of the path will be used.

[package]
versioned_files = ["Cargo.toml"]

[[package.assets]]
path = "artifact/my-binary-linux-amd64.tgz"
name = "linux-amd64.tgz"

[[package.assets]]
path = "artifact/my-binary-darwin-amd64.tgz"  # name will be "my-binary-darwin-amd64.tgz"

Examples

A Single Package with a Single Versioned File and multiple Assets

This is the relevant part of Knope's own knope.toml, where we keep release notes in a file called CHANGELOG.md at the root of the project and version the project using Cargo.toml (as this is a Rust project).

# knope.toml
[package]
versioned_files = ["Cargo.toml"]
changelog = "CHANGELOG.md"

[[package.assets]]
path = "artifact/my-binary-linux-amd64.tgz"
name = "linux-amd64.tgz"

[[package.assets]]
path = "artifact/my-binary-darwin-amd64.tgz"
name = "darwin-amd64.tgz"

A Single Package with Multiple Versioned Files

If your one package must define its version in multiple files, you can do so like this:

# knope.toml
[package]
versioned_files = ["Cargo.toml", "pyproject.toml"]
changelog = "CHANGES.md"  # You can use any filename here, but it is always Markdown

Multiple Packages

If you have multiple, separate packages which should be versioned and released separately—you define them as separate, named packages. For example, if knope was divided into two crates—it might be configured like this:

# knope.toml
[packages.knope]
versioned_files = ["knope/Cargo.toml"]
changelog = "knope/CHANGELOG.md"

[packages.knope-utils]
versioned_files = ["knope-utils/Cargo.toml"]
changelog = "knope-utils/CHANGELOG.md"

By default, the package names (e.g., knope and knope-utils) will be used as package names for changesets. No additional config is needed to independently version packages via changesets. If you want to target conventional commits at a specific package, you need to add the scopes key.

Warning

When you have one [package], the package name "default" will be used for changesets. If you switch to a multi-package setup, you will need to update all changeset files (in the .changeset directory) to use the new package names.

Info

See PrepareRelease and Release for details on what happens when those steps are run for multiple packages.

Multiple Major Versions of Go Modules

The recommended best practice for maintaining multiple major versions of Go modules is to include every major version on your main branch (rather than separate branches). In order to support multiple go modules files in Knope, you have to define them as separate packages:

# knope.toml
[packages.v1]
versioned_files = ["go.mod"]
scopes = ["v1"]

[packages.v2]
versioned_files = ["v2/go.mod"]
scopes = ["v2"]

This allows you to add features or patches to just the major version that a commit affects and release new versions of each major version independently.

Warning

If you use this multi-package syntax for go modules, you cannot use Knope to increment the major version. You'll have to create the new major version directory yourself and add a new package to knope.toml for it.

Jira

Details needed to use steps that reference Jira issues.

Example

# knope.toml

[jira]
url = "https://mysite.atlassian.net"
project = "PRJ"  # where an example issue would be PRJ-123

The first time you use a step which requires this config, you will be prompted to generate a Jira API token so Knope can perform actions on you behalf.

GitHub

Details needed to use steps that reference GitHub repos.

Example

# knope.toml

[github]
owner = "knope-dev"
repo = "knope"

The first time you use a step which requires this config, you will be prompted to generate a GitHub API token so knope can perform actions on you behalf. To bypass this prompt, you can manually set the GITHUB_TOKEN environment variable.

GitHub Actions

There are a lot of fun ways to use Knope in GitHub Actions! This section will show you some common patterns—if you have any questions or suggestions, please open a discussion!

Installing Knope

Knope is available as a GitHub Action, so you can install it like this:

- uses: knope-dev/action@v2.0.0
  with:
    version: 0.11.0

See more details and all available options in the action repo.

Recipes

Workflow Dispatch Releases

This recipe allows you to trigger the entire release process manually by either clicking a button in GitHub Actions or by using the GitHub CLI. Once that trigger occurs:

  1. A new version of the project is calculated (using PrepareRelease) and versioned files and changelogs are updated.
  2. The changes are committed back to the branch and pushed.
  3. The new commit is used to build assets.
  4. A release is created on GitHub with the new version, changelog, and assets.

Note

You should also check out the Pull Request Releases recipe which is similar, but allows you to preview the release in a pull request before accepting it.

Info

All of the examples in this recipe are for a project with a single Rust binary to release—you'll need to adapt some specifics to your use-case.

First, let's walk through the GitHub Actions workflow file:

name: Release

on: workflow_dispatch

jobs:
  prepare-release:
    runs-on: ubuntu-latest
    outputs:
      sha: ${{ steps.commit.outputs.sha }}
    steps:
      - uses: actions/checkout@v4
        name: Fetch entire history (for conventional commits)
        with:
          fetch-depth: 0
          token: ${{ secrets.PAT }}
      - name: Configure Git
        run: |
          git config --global user.name GitHub Actions
          git config user.email github-actions@github.com
      - name: Install Knope
        uses: knope-dev/action@v2.0.0
        with:
          version: 0.11.0
      - run: knope prepare-release --verbose
        name: Update versioned files and changelog
      - name: Store commit
        id: commit
        run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT

  build-artifacts:
    needs: prepare-release
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: aarch64-apple-darwin
            os: macos-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest

    runs-on: ${{ matrix.os }}
    name: ${{ matrix.target }}

    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ needs.prepare-release.outputs.sha }}

      - name: Install host target
        run: rustup target add ${{ matrix.target }}

      - name: Install musl-tools
        if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }}
        run: sudo apt-get install -y musl-tools

      - uses: Swatinem/rust-cache@v2

      - name: Build
        run: cargo build --release --target ${{ matrix.target }}

      - name: Set Archive Name (Non-Windows)
        id: archive
        run: echo "archive_name=test-${{ matrix.target }}" >> $GITHUB_ENV

      - name: Set Archive Name (Windows)
        if: ${{ matrix.os == 'windows-latest' }}
        run: echo "archive_name=test-${{ matrix.target }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append

      - name: Create Archive Folder
        run: mkdir ${{ env.archive_name }}

      - name: Copy Unix Artifact
        if: ${{ matrix.os != 'windows-latest' }}
        run: cp target/${{ matrix.target }}/release/test ${{ env.archive_name }}

      - name: Copy Windows Artifact
        if: ${{ matrix.os == 'windows-latest' }}
        run: cp target/${{ matrix.target }}/release/test.exe ${{ env.archive_name }}

      - name: Create Tar Archive
        run: tar -czf ${{ env.archive_name }}.tgz ${{ env.archive_name }}

      - name: Upload Artifact
        uses: actions/upload-artifact@v3
        with:
          path: ${{ env.archive_name }}.tgz
          if-no-files-found: error

  release:
    needs: [build-artifacts, prepare-release]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ needs.prepare-release.outputs.sha }}
      - uses: actions/download-artifact@v3
        with:
          name: ${{ env.archive_name }}
      - name: Install the latest Knope
        uses: knope-dev/action@v2.0.0
        with:
          version: 0.11.0
      - run: knope release --verbose
        env:
          GITHUB_TOKEN: ${{ secrets.PAT }}

There are three jobs here:

  1. prepare-release runs the prepare-release Knope workflow and saves the new commit as an output for use later.
  2. build-artifacts builds the assets for the release from the new commit that prepare-release created.
  3. release runs the release Knope workflow which creates the GitHub Release.

Throughout, there is use of a ${{ secrets.PAT }}, this is a GitHub Token with write permissions to "contents" which must be stored in GitHub Actions secrets. For the minimum-possible required privileges, you should create a fine-grained access token with read/write to "contents" for only this repo.

Now let's look at the Knope config which enables this GitHub workflow to work. For the sake of example, here's Knope's actual config from when this recipe was used (Knope now uses the Pull Request Releases recipe):

[package]
versioned_files = ["Cargo.toml"]
changelog = "CHANGELOG.md"

[[package.assets]]
path = "artifact/knope-x86_64-unknown-linux-musl.tgz"

[[package.assets]]
path = "artifact/knope-x86_64-pc-windows-msvc.tgz"

[[package.assets]]
path = "artifact/knope-x86_64-apple-darwin.tgz"

[[package.assets]]
path = "artifact/knope-aarch64-apple-darwin.tgz"

[[workflows]]
name = "prepare-release"

[[workflows.steps]]
type = "PrepareRelease"

[[workflows.steps]]
type = "Command"
command = "git commit -m \"chore: prepare release $version\" && git push"

[workflows.steps.variables]
"$version" = "Version"

[[workflows]]
name = "release"

[[workflows.steps]]
type = "Release"

[[workflows]]
name = "document-change"

[[workflows.steps]]
type = "CreateChangeFile"

[github]
owner = "knope-dev"
repo = "knope"

There is a single [package], but this pattern should also work for multi-package setups, just make sure all of your assets are ready at the same time. In this case, we have one versioned file Cargo.toml and one changelog CHANGELOG.md. We also have four assets, one for each platform we want to support. The name of each asset is omitted because we want to use the path as the name.

There are two relevant workflows here, the third (document-change) is used for creating changesets during development. prepare-release starts by running the PrepareRelease step, which does the work of updating Cargo.toml and CHANGELOG.md based on any conventional commits or changesets. We then run a command to commit the changes and push them back to the current branch (note that using the Version variable is not supported for multi-package setups at this time). Once this workflow runs, the project is ready to build assets.

When ready, GitHub Actions calls into the release workflow which runs a single step: Release. This will compare the latest stable tagged release to the version in Cargo.toml (or any other versioned_files) and create releases as needed by parsing the contents of CHANGELOG.md for the release's body. The release is initially created as a draft, then assets are uploaded before the release is published (so your subscribers won't be notified until it's all ready).

Pull Request Driven Releases

This recipe keeps an open pull request at all times previewing the changes the Knope will include in the next release. This pull request will let you see the next version, the changes to versioned files, and the changelog. When you merge that pull request, Knope will create a new release with the changes from the pull request.

This is the recipe that Knope uses for its own releases (at the time of writing), so let's walk through the two GitHub Actions workflows and the knope.toml that make it work.

knope.toml

We're going to walk through this in pieces for easier explanation, but all of these TOML snippets exist in the same file.

[package]

[package]
versioned_files = ["Cargo.toml"]
changelog = "CHANGELOG.md"

This first piece defines the package, Cargo.toml is both the source of the current version of the package and a place we'd like to place new version numbers. You can add more versioned_files (for example, if you also released this as a Python package with pyproject.toml). CHANGELOG.md is where we want to document changes in the source code—this is in addition to GitHub releases.

Warning

You cannot use this recipe right now with multiple packages due to limitations on variables. Instead, you can check out the workflow dispatch workflow.

[[package.assets]]

[[package.assets]]
path = "artifact/knope-x86_64-unknown-linux-musl.tgz"

[[package.assets]]
path = "artifact/knope-x86_64-pc-windows-msvc.tgz"

[[package.assets]]
path = "artifact/knope-x86_64-apple-darwin.tgz"

[[package.assets]]
path = "artifact/knope-aarch64-apple-darwin.tgz"

package.assets let us define a list of files to upload to GitHub releases. You can also upload them under a different name if you don't want to use the file name by setting name in the asset definition.

prepare-release workflow

[[workflows]]
name = "prepare-release"

[[workflows.steps]]
type = "Command"
command = "git switch -c release"

[[workflows.steps]]
type = "PrepareRelease"

[[workflows.steps]]
type = "Command"
command = "git commit -m \"chore: prepare release $version\" && git push --force --set-upstream origin release"

[workflows.steps.variables]
"$version" = "Version"

[[workflows.steps]]
type = "CreatePullRequest"
base = "main"

[workflows.steps.title]
template = "chore: prepare release $version"
variables = { "$version" = "Version" }

[workflows.steps.body]
template = "This PR was created by Knope. Merging it will create a new release\n\n$changelog"
variables = { "$changelog" = "ChangelogEntry" }

The first workflow is called prepare-release, so it can be executed by running knope prepare-release (as we'll see later in the GitHub Actions workflow). First, it creates a new branch from the current one called release, then it runs the PrepareRelease step which updates our package based on the changes that have been made since the last release. It also stages all of those changes with Git (like git add).

Next, we commit the changes that PrepareRelease made—things like:

  • Updating the version in Cargo.toml
  • Adding a new section to CHANGELOG.md with the latest release notes
  • Deleting any changesets that have been processed

This commit is pushed to the release branch, using the --force flag in this case because we don't care about the history of that branch, only the very next release. The CreatePullRequest step then creates a pull request from the current branch (release) to the specified base branch (main). We can set the title and body of this pull request using templated strings containing variables. In this case, the title contains the new Version and the body contains the new ChangelogEntry.

The pull request that this creates looks something like this:

Pull Request Preview

All we have to do now is run this prepare-release workflow in GitHub Actions whenever we want a new release preview—we'll take a look at that once we finish going through the knope.toml file.

release workflow

[[workflows]]
name = "release"

[[workflows.steps]]
type = "Release"

The release workflow is a single Release step—this creates a GitHub release for the latest version (if it hasn't already been released) and uploads any assets. In this case, it'll create a release for whatever the prepare-release workflow made earlier. We'll end up running this workflow whenever the pull request is merged.

document-change workflow

This isn't super relevant to the recipe, but it's useful to have. Changesets are what let us have really descriptive, rich text in changelogs to describe exactly how the latest changes impact our users. Running knope document-change executes the CreateChangeFile workflow to help us make changesets. A future iteration of this recipe may convert pull request comments into changesets 🤞.

[[workflows]]
name = "document-change"

[[workflows.steps]]
type = "CreateChangeFile"

[github]

The last piece is to tell Knope which GitHub repo to use for creating pull requests and releases.

[github]
owner = "knope-dev"
repo = "knope"

prepare_release.yml

There are two GitHub Actions workflows that we're going to use for this recipe—the first one goes in .github/workflows/prepare_release.yml and it creates a fresh release preview pull request on every push to the main branch:

on:
  push:
    branches: [main]
name: Create Release PR
jobs:
  prepare-release:
    if: "!contains(github.event.head_commit.message, 'chore: prepare release')" # Skip merges from releases
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.PAT }}
      - name: Configure Git
        run: |
          git config --global user.name GitHub Actions
          git config user.email github-actions@github.com
      - uses: knope-dev/action@v2.0.0
        with:
          version: 0.11.0
      - run: knope prepare-release --verbose
        env:
          GITHUB_TOKEN: ${{ secrets.PAT }}
        continue-on-error: true

Note

This workflow runs by default on every push to main, that includes when the previous release PR merges! There is an if: clause here in the first job that skips it if the commits matches the commit message that we use in the prepare-release workflow. If you change that message, you'll need to update this if: clause as well.

The steps here:

  1. Check out the entire history of the repo (so that PrepareRelease can use tags and conventional commits to determine the next version). This requires a personal access token with permission to read the contents of the repo.
  2. Configure Git so that we can commit changes (within Knope's prepare-release workflow)
  3. Install Knope
  4. Run the prepare-release workflow described above. This requires a personal access token with permission to write the pull requests of the repo.

Note

We add the continue-on-error attribute so that even if this step fails, the workflow will be marked as passing. This is because we want to be able to run this workflow on every push to main, but we don't want it to fail when there's nothing to release. However, this doesn't differentiate between legitimate errors and "nothing to release". You may want to instead use the allow_empty option in knope.toml and split the rest of the steps into a second workflow. Then, you can use some scripting in GitHub Actions to skip the rest of the workflow if there's nothing to release.

In the case of this action, we're using the same personal access token for both steps, but you could use different ones if you wanted to.

release.yml

Now that we're set up to create pull requests previewing the next release on every push to main, we need to automatically release those changes when the pull request merges. This is the job of the release workflow, which goes in .github/workflows/release.yml.

Warning

YAML is very sensitive to white space and very easy to mess up copy/pasting—so I recommend copying the whole file at the end, not the individual pieces I'm using to describe functionality.

To start off, we only want to run this workflow when our release preview pull requests merge—there are several pieces of config that handle this. First:

on:
  pull_request:
    types: [closed]
    branches: [main]

Will cause GitHub Actions to only trigger anything at all when a pull request which targets main closes. Then, in our first job, we can use this an if to narrow that down further to only our release preview pull requests, and only when they merge (not close for other reasons):

if: github.head_ref == 'release' && github.event.pull_request.merged == true

For Knope's own workflows, this first job is build-artifacts, which builds the package assets that will be uploaded when releasing. Skipping on past that job (since it probably will be different for you), we come to the release job:

release:
  needs: [build-artifacts]
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/download-artifact@v3
      with:
        name: ${{ env.archive_name }}
    - uses: knope-dev/action@v2.0.0
      with:
        version: 0.11.0
    - run: knope release
      env:
        GITHUB_TOKEN: ${{ secrets.PAT }}

    - run: gh workflow run "Deploy Book to GitHub Pages"
      env:
        GITHUB_TOKEN: ${{ secrets.PAT }}

The release job follows these steps:

  1. Check out the repo at the commit that the pull request merged
  2. Download the artifacts that were built in the build-artifacts job
  3. Install Knope
  4. Run the release workflow described above. This requires a personal access token with permission to write the contents of the repo.
  5. Kick off another workflow which updates these docs that you're reading 👋. That requires a personal access token with permission to write the actions of the repo.

Finally, Knope's workflow publishes to crates.io—meaning the whole workflow looks like this:

name: Release

on:
  pull_request:
    types: [closed]
    branches: [main]

jobs:
  build-artifacts:
    if: github.head_ref == 'release' && github.event.pull_request.merged == true
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: aarch64-apple-darwin
            os: macos-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest

    runs-on: ${{ matrix.os }}
    name: ${{ matrix.target }}

    steps:
      - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
      - uses: Swatinem/rust-cache@v2
      - name: Install host target
        run: rustup target add ${{ matrix.target }}

      - name: Install musl-tools
        if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }}
        run: sudo apt-get install -y musl-tools

      - name: Build
        run: cargo build --release --target ${{ matrix.target }}

      - name: Set Archive Name (Non-Windows)
        id: archive
        run: echo "archive_name=knope-${{ matrix.target }}" >> $GITHUB_ENV

      - name: Set Archive Name (Windows)
        if: ${{ matrix.os == 'windows-latest' }}
        run: echo "archive_name=knope-${{ matrix.target }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append

      - name: Create Archive Folder
        run: mkdir ${{ env.archive_name }}

      - name: Copy Unix Artifact
        if: ${{ matrix.os != 'windows-latest' }}
        run: cp target/${{ matrix.target }}/release/knope ${{ env.archive_name }}

      - name: Copy Windows Artifact
        if: ${{ matrix.os == 'windows-latest' }}
        run: cp target/${{ matrix.target }}/release/knope.exe ${{ env.archive_name }}

      - name: Create Tar Archive
        run: tar -czf ${{ env.archive_name }}.tgz ${{ env.archive_name }}

      - name: Upload Artifact
        uses: actions/upload-artifact@v3
        with:
          path: ${{ env.archive_name }}.tgz
          if-no-files-found: error

  release:
    needs: [build-artifacts]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v3
      - uses: knope-dev/action@v2.0.0
        with:
          version: 0.11.0
      - run: knope release
        env:
          GITHUB_TOKEN: ${{ secrets.PAT }}

      - run: gh workflow run "Deploy Book to GitHub Pages"
        env:
          GITHUB_TOKEN: ${{ secrets.PAT }}

  publish-crate:
    needs: [release]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
      - uses: Swatinem/rust-cache@v2
      - uses: katyo/publish-crates@v2
        with:
          registry-token: ${{ secrets.CARGO_TOKEN }}

Conclusion

Just to summarize, what we get with all of this is a process that:

  1. Automatically creates a pull request in GitHub every time a new commit is pushed to main. That pull request contains a preview of the next release.
  2. Automatically releases the package every time a release preview's pull request is merged.