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
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.
KNOPE_PRERELEASE_LABEL
works just like the--prerelease-label
option. Note that the option takes precedence over the environment variable.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:
- Select issues from Jira or GitHub to work on, transition and create branches from them.
- Do some basic git commands like switching branches or rebasing.
- Bump the version of your project using semantic rules.
- Bump your version AND generate a Changelog entry from conventional commits.
- 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
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
Install via cargo-binstall
(Recommended)
Knope is built in such a way that cargo-binstall
can install it by downloading a binary artifact for the most popular platforms.
- Install
cargo-binstall
using your preferred method. - Run
cargo-binstall knope
to install Knope.
Is your platform not supported yet? Please contribute it by opening a pull request.
Install via GitHub Action (Recommended)
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:
- Installing cargo via Rustup
- Running
cargo install knope
Building Knope can be quite slow, if possible, it's recommended to download a prebuilt binary instead.
Build from Source
- Install the current Rust stable toolchain via Rustup
- Clone the GitHub repo
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
.
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:
- 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. - Update a
CHANGELOG.md
file (if any) with the body of relevant commits (again, see thePrepareRelease
step for more detail). - Commit the changes to the versioned and changelog files and push that commit.
- Create a release which is one of the following, see the
Release
step for more detail:- 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. - If the remote is not GitHub, a tag will be created and pushed to the remote.
- If your remote is GitHub, create a new release on GitHub with the same body as the changelog entry. This requires a
Additional Options
--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.--prerelease-label
will tellknope
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 runknope release
), but with the-rc.0
suffix (orrc.1
,rc.2
, etc. if you have already created a release with that label).--override-version
will tellknope
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 version1.2.3
. This is especially useful when moving from a0.x.x
version to1.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
--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.
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
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
- PrepareRelease
- CreateChangeFile
- BumpVersion
- RebaseBranch
- SwitchBranches
- SelectJiraIssue
- TransitionJiraIssue
- SelectGitHubIssue
- SelectIssueFromBranch
- Command
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
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
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
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:
- Looks through all commits since the last version tags and parses any Conventional Commits it finds.
- Reads any Changesets in the
.changeset
folder (which you can create viaCreateChangeFile
). Those files are deleted after being read. - Bumps the semantic version of any packages that have changed.
- Adds a new entry to any affected changelog files.
- 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.
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 newgo.mod
file (in a new directory) for each major version. You can override this behavior using the--override-version
option (to go fromv1
tov2
) or use multiple packages to support multiplego.mod
files on different major versions.
Options
allow_empty
: If set totrue
, this step will not fail if there are no changes to release. Defaults tofalse
.
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:
- If no packages define
scopes
in their config, all commits apply to all packages. Scopes are not considered byknope
. - If a commit does not have a scope, it applies to all packages.
- 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 theirscopes
array.
Changelog format
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:
Breaking Changes
for anything that triggers a major semantic version increase.- Any commit whose type/scope end in
!
will land in this section instead of their default section (if any). Sofix!: a breaking fix
will add the note "a breaking fix" to this section and nothing to the "Fixes" section. - 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 afix:
commit with aBREAKING CHANGE
footer creates entries in both theFixes
section and theBreaking Changes
section. - Any changeset with a change type of
major
(selecting "Breaking" inCreateChangeFile
)
- Any commit whose type/scope end in
Features
for any commit with typefeat
(no!
) or change typeminor
(selecting "Feature" inCreateChangeFile
)Fixes
for any commit with typefix
(no!
) or change typepatch
(selecting "Fix" inCreateChangeFile
)Notes
for any footer in a conventional commit calledChangelog-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" },
]
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:
- If there are any breaking changes (things in the
### Breaking Changes
section above), theMajor
rule is used. - If no breaking changes, but there are any features (things in the
### Features
section above), theMinor
rule is used. - 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. - 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:
feat(cli): Add a new --help option to display usage and exit
feat(lib)!: Change the error type of the parse function
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.
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:
- The version could not be bumped for some reason.
- The packages section is not configured correctly.
- There was nothing to release and
allow_empty
was not set totrue
. 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.
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:
- No package names match the name of a go module
- 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:
- If run after a
PrepareRelease
step in the same workflow, the release notes will be the same as the changelog section created byPrepareRelease
even if there is no changelog file configuredâwith the exception that headers are reduced by one level (for example,####
becomes###
). - 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 whatPrepareRelease
would have created. Headers will be reduced by one level (for example,####
becomes###
). - 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:
- Create the release in draft mode
- Upload the assets one at a time
- 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:
- GitHub config is set but Knope cannot create a release on GitHub. For example:
- There is no GitHub token set.
- The GitHub token does not have permission to create releases.
- The release already exists on GitHub (causing a conflict).
- There is no GitHub config set and Knope cannot tag the current commit as a release.
- Could not find the correct changelog section in the configured changelog file for loading release notes.
- 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:
- "knope 1.2.3" with tag "knope/v1.2.3"
- "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:
- Prep the release to get the new version and changelog
- Commit the changes
- Fan out into several jobs which each check out the changes and build a different binary
- 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.
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
rule
: The Semantic Versioning rule to use.label
: Only applicable toPre
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".
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
:
- The first component (
0
) is ignored - The second component (
1
) serves as theMajor
component, and will be incremented whenever theMajor
rule is applied. - The third component (
2
) serves as bothMinor
andPatch
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:
- A malformed version string is found while attempting to bump. Note that only a subset of pre-release version formats are supported.
- 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:
- knope cannot communicate with the configured Jira URL.
- User does not select an issue (e.g. by pressing
Esc
). - 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:
- An issue was not previously selected in this workflow using
SelectJiraIssue
orSelectIssueFromBranch
. - Cannot communicate with Jira.
- 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:
- knope cannot communicate with GitHub.
- There is no GitHub config set.
- 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.
- An issue was not previously selected in this workflow using
SelectJiraIssue
orSelectGitHubIssue
. - Current directory is not a Git repository
- 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:
- The current directory is not a Git repository.
- The
to
branch cannot be found locally (does not check remotes). - The repo is not on the tip of a branch (e.g. detached HEAD)
- 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.
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.
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:
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.changelog
is the (optional) Markdown file you'd like to add release notes to.scopes
is an optional array of conventional commit scopes which should be considered for the package when running thePrepareRelease
step.extra_changelog_sections
is an optional array of extra sections that can be added to the changelog when running thePrepareRelease
step.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 theRelease
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:
Cargo.toml
for Rust projectspyproject.toml
for Python projects using PEP-621 or Poetrypackage.json
for Node projectsgo.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" }
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.
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.
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.
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
- Trigger a release by manually running a GitHub Actions workflow
- Maintain a Pull Request which previews the release, trigger the release by merging it
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:
- A new version of the project is calculated (using
PrepareRelease
) and versioned files and changelogs are updated. - The changes are committed back to the branch and pushed.
- The new commit is used to build assets.
- A release is created on GitHub with the new version, changelog, and assets.
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.
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:
prepare-release
runs theprepare-release
Knope workflow and saves the new commit as an output for use later.build-artifacts
builds the assets for the release from the new commit thatprepare-release
created.release
runs therelease
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.
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:
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
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:
- 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. - Configure Git so that we can commit changes (within Knope's
prepare-release
workflow) - Install Knope
- Run the
prepare-release
workflow described above. This requires a personal access token with permission to write the pull requests of the repo.
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
.
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:
- Check out the repo at the commit that the pull request merged
- Download the artifacts that were built in the
build-artifacts
job - Install Knope
- Run the
release
workflow described above. This requires a personal access token with permission to write the contents of the repo. - 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:
- 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. - Automatically releases the package every time a release preview's pull request is merged.