The Cranko Manual
Cranko is a release automation tool implementing the just-in-time versioning workflow. It is cross-platform, installable as a single executable, supports multiple languages and packaging systems, and is designed from the ground up to work with monorepos. Here’s a video that shows how it works:
If you’re just getting started, your first step should probably be to install cranko. Or, check the table of contents to the left if you’d like to skip directly to a topic of interest.
Contributions are welcome!
This book is a work in progress, and your help is welcomed! The text is written
in Markdown (specifically, CommonMark using pulldown-cmark) and rendered
into HTML using mdbook. The source code lives in the book/
subdirectory of
the main Cranko repository. To make and view changes, all you need to do is
install mdbook, then run the command:
$ mdbook serve
in the book/
directory.
Installation
Because Cranko is delivered as a single standalone executable, it is easy to install. This is very intentional!
There are several installation options:
- On a Unix-like operating system (Linux or macOS), the following command will
place the latest release of the
cranko
executable into the current directory:
If your CI/CD environment doesn't make Cranko available in a more standardized way, this is the recommended installation command.curl --proto '=https' --tlsv1.2 -sSf https://pkgw.github.io/cranko/fetch-latest.sh | sh
- On Windows, the following PowerShell commands will do the same:
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 iex ((New-Object System.Net.WebClient).DownloadString('https://pkgw.github.io/cranko/fetch-latest.ps1'))
- You can manually download precompiled binaries from the Cranko GitHub release archive.
- If you have a Rust toolchain installed, you can compile and install your own
version with
cargo install cranko
. - Finally, to develop Cranko itself, you can check out the source code and
build using the standard Rust framework:
cargo build
.
Note that, to fully implement the just-in-time
versioning workflow, the cranko
command will need
to be available both on your development machine and on the nodes powering your
CI/CD pipeline. The curl
and PowerShell commands given above should make
installation easy on just about any CI/CD system. The code for these installers
is almost directly ripped off from Rustup and Chocolatey, respectively
(thanks!), and will honor some of the environment variables used by those
installers.
Getting Started
The goal of Cranko is to help you implement a clean, reliable workflow for making releases of the software that you develop. Because Cranko is a workflow tool, there isn’t one single way to “start using” it — the best way to use it depends on your current release workflow (or lack thereof) and CI infrastructure.
That being said — once Cranko is integrated into your project, a typical release
process should look like the following. You might periodically run the cranko status
command to report on the history of commits since your latest
release(s):
$ cranko status
cranko: 10 relevant commit(s) since 0.0.3
When you're ready to make a release, you’ll run commands like this:
$ cranko stage
cranko: 12 relevant commits
$ {edit CHANGELOG.md to curate the release notes and set version bump type}
$ cranko confirm
info: cranko: micro bump (expected: 0.0.3 => 0.0.4)
info: staged rc commit to `rc` branch
$ git push origin rc
Your Cranko-powered CI pipeline will build the rc
branch, publish a new
release upon success, and update a special release
branch. You don't need to
edit any files on your main branch to “resume development”. Instead, if you
resynchronize with the origin you’ll now see:
$ git fetch origin
[...]
9fa82ad..8be356d release -> origin/release
* [new tag] cranko@0.0.4 -> cranko@0.0.4
$ cranko status
cranko: 0 relevant commit(s) since 0.0.4
Underpinning Cranko’s operation is the just-in-time versioning workflow. It’s important to understand how it works to understand how you’ll integrate Cranko into your development and deployment workflow.
Just-in-Time Versioning
Cranko implements a release workflow that we call just-in-time versioning. This workflow solves several tricky problems that might bother you about traditional release processes. On the other hand, they might not! People release software every day with standard techniques, after all. But if you’ve been bothered by the lack of rigor in some of your release workflows, just-in-time versioning might be what you've been looking for.
Just-in-time versioning addresses two particular areas where traditional practices introduce a bit of sloppiness:
- In a typical release workflow, you assign a version number to a particular commit, publish it to CI, and then deploy it if tests are successful. But this is backwards: we shouldn’t bless a commit with a release version until after it has passed the CI suite.
- Virtually every software packaging system has some kind of metadata file in
which you describe your software, including its version number —
package.json
,Cargo.toml
, etc. Because these files must be checked into your version control system, you are effectively forced to assign a version number to every commit, not just the commits that correspond to releases. What version number is appropriate for these “in-between” commits?
The discussion below will assume good familiarity with the way that the Git version control system stores revision history. If you haven’t tried to wrestle with thinking about your history as a directed acyclic graph, it might be helpful to have some references handy.
The core ideas
Say that you agree that the two points above are indeed problems. How do we address them?
To address issue #1, there’s only one possible course of action: if we want to make a release, we have to “propose” a commit to the CI system, and only bless it as a release after it passes the test suite.
In a practical workflow we’re probably not going to want to propose every single
commit from the main development branch (which we’ll call main
here). For our
purposes, it doesn’t particularly matter how commits from main
are chosen to
be proposed — just that it happens.
Once a commit has been proposed, future proposals should only come from later in
the development history: we don’t want to releases to move backwards. So, the
release proposals are a series of commits … that only moves forward … that’s a
branch! Let’s call it the rc
branch, for “release candidate”.
Say that we propose releases by pushing to an rc
branch. Some (hopefully
most!) of those proposals are accepted, and result in releases. How do we
synchronize with the main
branch and keep everything coherent, especially in
light of issue #2?
Just-in-time versioning says: don’t! On the main
branch, assign
everything a version number of 0.0.0, and never change it. When your CI system
runs on the rc
branch, before you do anything else, edit your metadata files
to assign the actual version numbers. If the build succeeds, commit those
changes and tag them as your release.
One final elaboration. Because the commits with released version numbers are
never merged back into main
, they form a series of “stubs” forking off from
the mainline development history. But these releases also form a sequence that,
logically speaking, only moves forward, so it would be nice to preserve them in
some branch-like format as well. In the Git formalism, this is possible if we’re
not afraid to construct our own merge commits. Let’s push each release commit to
a branch called release
, merging rc
into release
but discarding the
release
file tree in favor of rc
:
main: rc: release:
M8 /---------R2 (v0.3.0)
| / |
M7 /------C3 |
| / | |
M6 /------C2 (failed) |
| / | |
M5 | R1 (v0.2.0)
| | /
M4 /------C1---------/
| / |
M3 |
| |
M2 |
| /
M1-------/
This tactic isn’t strictly necessary for just-in-time versioning concept, because in principle we can preserve the release commits through Git tags alone. But it becomes very useful for navigating the release history.
The workflow in practice
In practice, the just-in-time versioning workflow involves only a handful of special steps. When a project’s CI/CD pipeline has been set up to support the workflow, the developer’s workflow for proposing releases is trivial:
- Choose a commit from
main
and propose it torc
.
In the very simplest implementation, this step could as straightforward as
running git push origin $COMMIT:rc
. For reasons described below, Cranko
implements it with two commands: cranko stage
and
cranko confirm
.
In the CI/CD pipeline, things are hardly more complicated:
- The first step in any such pipeline is to apply version numbers and create a
release commit. In Cranko, this is performed with
cranko release-workflow apply-versions
andcranko release-workflow commit
. - If the CI passes, the release is “locked in” by pushing to
release
. If not, the release commit is discarded.
Cranko provides a lot of other infrastructure to make your life easier, but the core of the just-in-time versioning workflow is this simple. Importantly: you don’t need to completely rebuild your development and CI/CD pipelines in order to adopt Cranko. There are only a small number of new steps, and existing setups can largely be preserved.
The monorepo wrinkle
The above discussion is written as if your repository contains one project with one version number. Cranko was written from the ground up, however, to support monorepos (monolithic repositories), which we will define as any repository that (somewhat confusingly) contains more than one independently versioned project. People argue about whether monorepos or, um, single-repos are better, but, empirically, there are numerous high-profile projects that have adopted a monorepo model, and once you’ve figured out how to deal with monorepos, you’ve also solved single-repos.
Fortunately, virtually everything described above can be “parallelized” over multiple projects in a single repository. (Here, a “project” is any item in a repository that has versioned releases.) Most of the work needed to support monorepos involves making sure that things like GitHub release entries and tag names are correctly treated in a per-project fashion, rather than a per-repository fashion.
In principle, you might be tempted to have one rc
branch and one release
branch for each project in a monorepo. This has an appeal, but it comes with two
problems. First, as the number of projects gets large, so does the number of
branches, which is a bit ugly. Second and more important, separating out
releases by each individual project makes it hard to coordinate releases — and
if multiple projects are being tracked in the same repository it is very likely
because releases should be coordinated.
Cranko solves this problem by adding more sophistication to the rc
and
release
processing. Pushes to the rc
branch include metadata that specify a
set of projects that are being requested for release. (This is what the
cranko stage
and cranko confirm
commands do.) Likewise, updates to release
include information about which projects actually were released. It turns out
that pushes to rc
need to contain metadata anyway, to allow the developer to
specify how the version number(s) should be bumped and release-notes content.
There is one more problem that’s more subtle. If a repo contains multiple
projects, some of them probably depend on one another. If everything on the
main
branch is versioned at 0.0.0, how do we express the version requirements
of these internal dependencies? We can’t just record those versions in the usual
packaging metadata files, because any tools that need to process these internal
dependencies will reject the version constraints (foo_cli requires foo_lib > 1.20.0, but found foo_lib = 0.0.0
).
Cranko solves this problem by asking your main
branch to include a bit of
extra metadata expressing these version requirements as commit identifiers
rather than version numbers. The underlying idea is that, because projects are
tracked in the same repository, it should really be true that at any given
commit, all of the projects within the repo are mutually compatible. Upon
release time, the required commit identifiers are translated into actual version
number requirements. Part of the stage-and-confirm process implemented by Cranko
ensures that you don’t try to release a new version of a depender project
(foo_cli
above) that requires an as-yet-unreleased version of its dependee
(foo_lib
). Cranko even has a special mechanism allowing you to make a single
commit that simulataneouly updates foo_cli
and foo_lib
and expresses
that “foo_cli
now depends on the version of foo_lib
from the Git commit that
is being made right now”.
The Cranko Bootstrapping Workflow
Cranko provides a special cranko bootstrap
command to help you start using Cranko with a preexisting repository.
Invocation
Ideally, to bootstrap a repository to use Cranko all you need to do is enter its working tree and run:
$ cranko bootstrap
Go ahead and try it! It will try to print out detailed information about what it’s doing. Since you must run the program in a Git repository working tree, if it does anything that you don’t like you can always reset your working tree to throw away the tool’s changes.
Hopefully the tool won’t crash, but these are early days and everyone’s repo is unique. If you have problem not addressed in the text below, file an issue.
Guessing the upstream
Cranko needs to know the identity of your upstream repository, which is defined
as the one that will perform automated release processing upon updates to its
rc
-like branch. The bootstrapper will begin by attempting to guess the
identity of upstream by looking for a Git remote named origin
, or choosing the
only remote if there is only one. If this guessing process fails, use the
--upstream
option to specify the name of the upstream explicitly.
The bootstrapper will save the URL of the upstream remote into the main Cranko
configuration file .config/cranko/config.toml
. You
may want to add additional likely upstream URLs to this configuration file
(e.g., both HTTPS and SSH GitHub remote URLs). Cranko identifies the upstream
from its URL, not its Git remote name, since Git remote names can vary
arbitrarily from one checkout to the next.
Autodetecting projects
The bootstrapper will search for recognized projects in the repo and print out a summary of what it finds.
NOTE: The goal is for Cranko to recognize all sorts of projects, but currently it knows a modest group of them: Rust/Cargo, NPM, and Python. If you’re giving Cranko a first try this is the limitation that is most likely to be a dealbreaker. Please file an issue reporting your needs so we know what to prioritize.
ALSO: There is a further goal that one day you’ll be able to manually configure projects that aren’t discovered in the autodetection phase, but that functionality is also not yet implemented.
Resetting versions
As per the just-in-time versioning workflow, on the main development
branch of your repository, the version numbers of all projects should be set to
some default “development” value (e.g. 0.0.0-dev.0
) that is never planned to
change. Cranko will rewrite all of the metadata files that it recognizes to
perform this zeroing.
But you’re presumably not going to want to actually reset the versioning of
all your projects. The current version numbers will be preserved in a “bootstrap”
configuration file (.config/cranko/bootstrap.toml
) that Cranko will use as a
basis for assigning new version numbers.
Transforming internal dependencies
If your repository contains more than one project, some of those projects
probably depend on each other. With zeroed-out version numbers, it is not
generally possible to express the version constraints of those internal
dependencies in existing metadata
formats. For instance, before bootstrapping,
you might have had a package foo_cli
that depends on foo_lib >= 1.3
: it
works if linked against foo_lib
version 1.3.0, but not if linked against
foo_lib
version 1.2.17. That didn’t stop being true just because the version
numbers in on your main development branch got zeroed out!
The boostrapping process transfers your preexisting internal dependency version requirements into extra Cranko metadata fields so that they will be correctly reproduced in new releases. Once you start making releases that depend on newer versions of your projects, it is recommended that you transition these “manually” coded version requirements to Cranko-native ones based in Git commit identifiers (as motivated in the just-in-time versioning section).
Next steps
Once the bootstrapper has run, you should review the changes it has made, see if they make sense, and try building the code in your repo. You may need to modify your build scripts depending on what expectations they have about the version numbers assigned in your main development branch.
After you are happy with Cranko’s changes, commit them, making sure to add the
new files in .config/cranko/
.
The next step is to modify your CI/CD system to start using the cranko release-workflow
commands to start implementing the just-in-time
versioning model — see the CI/CD Workflows section for
documentation on what to do. This phase generally takes some trial-and-error,
but in most cases you should only need to insert a few extra commands into your
CI/CD scripts at first. Generally, it is easiest to start by updating the
processes that run on updates to the main development branch (e.g. master
) and
on pull requests. If you do this work on a branch other than your main
development branch, make sure that your Cranko-ified CI/CD scripts will run on
updates to that branch.
As you work on the CI/CD configuration for main development work, you probably
won’t actually need to use any of the Cranko commands described in the Everyday
Development section. But once your basic processing is working, you
should start using those commands to simulate releases and work on setting up
the CI/CD workflows that run on updates to the new rc
branch that you will be
creating — these are the workflows that will actually run the automated release
machinery if/when your builds succeed. If you haven’t been using release
automation before, it can take some patience to set everything up properly. But,
we hope that you still soon start feeling the warm fuzzies that arise when these
usually annoying tasks start Just Working!
Cranko Developer Workflows
This section focuses on the workflows that you might use in the “inner loop” of your software development process.
Day-to-day development
If your repository uses Cranko, your standard development practices don’t need
to change. The only thing that’s different is that your version numbers should
all be set to 0.0.0-dev.0
or something similar.
The cranko status command will analyze your repository’s commit history since
the last release on the release
branch. It might tell you:
$ cranko status
tcprint: 2 relevant commit(s) since 0.1.1
drorg: 5 relevant commit(s) since 0.1.1
$
Here, relevance is determined using the prefixing scheme described in the
Concepts section. Merge commits are skipped. The cranko log command will
print Git history logs for the commits relevant to a specified project, using
the style of the git log
command.
The most release reference point is determined from your upstream’s release
branch (likely origin/release
), so make sure to git fetch
your upstream
after a release so that Cranko is comparing to the right thing.
If you are working in a monorepo and one project depends on another, you’ll need to maintain Cranko’s extra versioning metadata. TODO write me!
Requesting releases
When you’re ready to release one or more projects, it’s a two-step process. The cranko stage command will mark projects as release candidates. If run without arguments, it will use Cranko’s analysis of the repo’s commit history since the last release to determine which projects need to be staged:
$ cranko stage
tcprint: 2 relevant commits
drorg: 5 relevant commits
info: 2 of 2 projects staged
$
The only actual action taken by this command is to stub each project’s changelog with a template version bump command and summaries of the commits affecting each project since the last release. In this example, this looks like:
$ head tcprint/CHANGELOG.md
# rc: micro bump
- Add an amazing new feature
- Fix a dastardly bug
# tcprint 0.1.1 (2020-08-27)
The placeholder header line # rc: micro bump
specifies the version bump that
is being requested. At the moment, this just unilaterally defaults to a bump in
the “micro” (AKA “patch”) version number. When the release is finalized, this
placeholder will be replaced with actual release information as seen in the next
stanza.
You can edit the bump type and the actual changelog contents. We view it as important that the changelog and/or release notes can be reviewed and curated by a human.
After one or more stage
operations, you should run cranko confirm
:
$ cranko confirm
info: tcprint: micro bump (expected: 0.1.1 => 0.1.2)
info: drorg: micro bump (expected: 0.1.1 => 0.1.2)
info: internal dep: tcprint >= 0.1.1
info: staged rc commit to `rc` branch
$
This will gather up your changelog updates and create a new commit on the rc
branch. (Note that these changelog updates do not need to be staged into Git
with git add
.) The changelogs in the working directory will be reset to
whatever HEAD says they should be. The new commit on rc
bundles up a release
request, containing the set of projects intended for release, the way that
their versions should be bumped, and the changelog / release-notes contents.
Your CI/CD system should be set up so that you can trigger release process simply by running:
$ git push origin rc
You should never need to force-push to this branch. If a release request fails,
you should fix the problem on the main development branch, create a new rc
commit, and try again. TODO We should add a command to make it easy to
re-use the changelogs from the previous rc
commit.
Cranko CI/CD Workflows
This section focuses on the workflows that should be implemented in your continuous integration and deployment (CI/CD) system. You can in principle run those steps outside of the CI/CD context, but the whole point of Cranko is to automate release processes, so the strong assumption is that these steps will not be run by humans. In fact, the Cranko commands mentioned in this section will generally be need to be forced to run outside of a CI/CD environment, which they detect using the ci_info Rust crate.
Every build
For virtually every build of your repo in your CI/CD infrastructure, the first thing you should do is install Cranko (if needed) and then apply actual version numbers:
cranko release-workflow apply-versions
The Cranko architecture is intended so that your repository should be buildable without applying versions — because otherwise day-to-day development would be incredibly tedious — but it is good to apply versions everywhere in CI/CD to make sure that the relevant plumbing stays in excellent working order.
For pull request builds and merges to the main development branch, you don’t
need to do anything more. If you have a continuous deployment scheme that
publishes artifacts with every push to the main branch, you shouldn’t need to
change it. A key thing to keep in mind is that pushes to the main branch, unlike
pushes to rc
, do not include cranko confirm
metadata, and so there are no
changelogs and no specific list of projects for which releases are requested.
Intead, all projects have their versions bumped — but with development
placeholders, not realistic-looking values.
rc
builds
You will need to handle updates to the rc
branch specially. The initial build
and test process should ideally proceed in exactly the same way as occurs on the
main branch. However, after that process completes, there needs to be a single
decision point that gathers all potential release artifacts and evaluates
whether the build was successful or not. If it failed, there is nothing more to
do. If it was successful, your release deployment automation needs to kick in.
We recommend that this workflow proceed in three stages. First, ensure that all release artifacts are archived in some fashion. This way, if any later steps fail, they can be recreated manually.
Next, update the release
branch, using commands similar to the following:
$ git add .
$ cranko release-workflow commit
$ git push origin release
This “locks in” the release and ensures that any subsequent rc
submissions do
not try to recreate the releases that your pipeline is about to undertake. The
commit
command switches the Git repository’s current branch to be release
,
pointing at the newly created release commit. Commits at the tip of the
release
branch, like those at the tip of rc
, contain Cranko metadata. While
rc
commits contain release request metadata, release
commits contain
metadata about which releases were actually made (and not made).
Finally, perform whichever deployment steps are required: creating GitHub
releases, publishing packages to NPM, updating websites, etc. These operations
do not necessarily need to involve the cranko
tool at all.
However, when you’re using a monorepo, it is important to keep in mind that each
release involves some unpredictable subset of the projects in your repo. The
cranko
tool can be the source of truth about which projects were just released
and which version numbers they were assigned. Many of the cranko
commands
beyond the core workflow operations are utilities that leverage Cranko’s
knowledge of the project release graph to ease the implementation of this final
stage of the release process.
The release
branch
Your CI/CD system should do nothing when the release
branch is updated. This
branch is only for recording the success of rc
processing — all of the
interesting stuff should happen there.
Integrations: Azure Pipelines
The Azure Pipelines CI/CD service is a great match for Cranko because its ability to divide builds into stages that exchange artifacts works very nicely with Cranko’s model for CI/CD processing. This section will go over ways that you can use Cranko in the Azure Pipelines framework.
Examples
Here are some projects that use Cranko in Azure Pipelines:
- Cranko itself
- pkgw/elfx86exts, a simple single-crate project
- tectonic-typesetting/tectonic, with cross-platform Rust builds and complex deployment
- WorldWideTelescope/wwt-webgl-engine, with an NPM monorepo structure
General structure
For many projects, it works well to adopt an overall pipeline structure with two stages:
trigger:
branches:
include:
- master
- rc
stages:
- stage: BuildAndTest
jobs:
- template: azure-build-and-test.yml
- stage: Deploy
condition: and(succeeded('BuildAndTest'), ne(variables['build.reason'], 'PullRequest'))
jobs:
- template: azure-deployment.yml
The BuildAndTest
stage can contain many parallel jobs that might build your
project on, say, Linux, MacOS, and Windows platforms. If all of those jobs
succeed, and the build is not a pull request (so, it was triggered in an
update to the master
or rc
branch), the deployment stage will run.
Here, we use templates to group the jobs for the two stages into their own files. Templates are generally helpful for breaking CI/CD tasks into more manageable chunks. However, they can be a bit tricky to get the hang of; a key restriction is that templates are processed at “compile time”, and some variables or other build settings are not known until “run time”.
Installing Cranko
To install the latest version of Cranko into your build workers, we recommend
the following pair of tasks. By using a condition
here, these tasks can be run
on agents running any operating system, and the right thing will
happen. This is useful if this setup step goes into a template.
- bash: |
set -euo pipefail # note: `set -x` breaks ##vso echoes
d="$(mktemp -d /tmp/cranko.XXXXXX)"
cd "$d"
curl --proto '=https' --tlsv1.2 -sSf https://pkgw.github.io/cranko/fetch-latest.sh | sh
echo "##vso[task.prependpath]$d"
displayName: Install latest Cranko (not Windows)
condition: and(succeeded(), ne(variables['Agent.OS'], 'Windows_NT'))
- pwsh: |
$d = Join-Path $Env:Temp cranko-$(New-Guid)
[void][System.IO.Directory]::CreateDirectory($d)
cd $d
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object System.Net.WebClient).DownloadString('https://pkgw.github.io/cranko/fetch-latest.ps1'))
echo "##vso[task.prependpath]$d"
displayName: Install latest Cranko (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
If all of your agents will be running on the same operating system, you can
choose the appopriate task and remove the condition
.
Creating and transferring the release commit
If you use a multi-stage build process, a wrinkle emerges. You need to create a
single “release commit” to be published if the CI/CD succeeds. But if
publication happens in your Deploy
stage, those jobs are separate from the
build jobs that actually ran the cranko release-workflow apply-versions
and cranko release-workflow commit
commands.
We recommend publishing the release commit as an Azure Pipelines artifact. This can be accomplished pretty conveniently with the Git bundle functionality. All of your main build jobs should apply version numbers:
- bash: |
set -xeuo pipefail
git status # [see below]
cranko release-workflow apply-versions
displayName: Apply versions with Cranko
(The git status
helps on Windows, where it seems that sometimes libgit2
thinks that the working tree is dirty even though it’s not. There’s some issue
about updating the Git index file.)
One of your build jobs should also commit the version numbers into a release commit, and publish the resulting commit as a Git bundle artifact:
- bash: |
set -xeuo pipefail
git add .
cranko release-workflow commit
git show # useful diagnostic
displayName: Generate release commit
- bash: |
set -xeuo pipefail
mkdir $(Build.ArtifactStagingDirectory)/git-release
git bundle create $(Build.ArtifactStagingDirectory)/git-release/release.bundle origin/master..HEAD
displayName: Bundle release commit
- task: PublishPipelineArtifact@1
displayName: Publish git bundle artifact
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/git-release'
artifactName: git-release
(As a side note, if you run bash
tasks on Windows, there is currently a bug
where variables such as $(Build.ArtifactStagingDirectory)
are expanded as
Windows-style paths, e.g. C:\foo\bar
, rather than Unix-style paths,
/c/foo/bar
. You will either need to transform these variables, or not use bash
in Windows.)
Your deployment stages should then retrieve this artifact and apply the release commit:
# Fetch artifacts from previous stages
- download: current
# Check out source repo again
- checkout: self
submodules: recursive
- bash: |
set -xeuo pipefail
git switch -c release
git pull --ff-only $(Pipeline.Workspace)/git-release/release.bundle
displayName: Restore release commit
Standard deployment jobs
If your pipeline is running in response to an update to the rc
branch, and
your CI tests succeeded, there are several common deployment steps that you can
invoke as (more or less) independent jobs. We recommend using a
template with standard setup steps to install Cranko and recover
the release commit, as shown above. Here we’ll assume that these have been
bundled into a template named azure-deployment-setup.yml
.
We also assume here that you have a variable group called
Deployment Credentials
that includes necessary credentials in variables named
GITHUB_TOKEN
, NPM_TOKEN
, etc.
No matter which packaging system(s) you use, you should create tags and update
the upstream release
branch. This example assumes that it lives on GitHub:
- ${{ if eq(variables['Build.SourceBranchName'], 'rc') }}:
- job: branch_and_tag
pool:
vmImage: ubuntu-latest
variables:
- group: Deployment Credentials
steps:
- template: azure-deployment-setup.yml
- bash: |
cranko github install-credential-helper
displayName: Set up Git pushes
env:
GITHUB_TOKEN: $(GITHUB_TOKEN)
- bash: |
set -xeou pipefail
cranko release-workflow tag
git push --tags origin release:release
displayName: Tag and push
env:
GITHUB_TOKEN: $(GITHUB_TOKEN)
GitHub releases
If you are indeed using GitHub, Cranko can automatically create GitHub releases for you. You must ensure that this task runs after the tags are pushed, because otherwise GitHub will auto-create incorrect tags for you:
- ${{ if eq(variables['Build.SourceBranchName'], 'rc') }}:
- job: github_releases
dependsOn: branch_and_tag # otherwise, GitHub creates the tags itself!
pool:
vmImage: ubuntu-latest
variables:
- group: Deployment Credentials
steps:
- template: azure-deployment-setup.yml
- bash: cranko github install-credential-helper
displayName: Set up Git pushes
env:
GITHUB_TOKEN: $(GITHUB_TOKEN)
- bash: cranko github create-releases
displayName: Create GitHub releases
env:
GITHUB_TOKEN: $(GITHUB_TOKEN)
You might also use cranko github upload-artifacts
to upload
artifacts associated with those releases, although if you have a monorepo you
must use cranko show if-released
to check at
runtime whether the project in question was actually released.
Cargo publication
If your repository contains Cargo packages, you should publish them:
- ${{ if eq(variables['Build.SourceBranchName'], 'rc') }}:
- job: cargo_publish
pool:
vmImage: ubuntu-latest
variables:
- group: Deployment Credentials
steps:
- template: azure-deployment-setup.yml
- bash: cranko cargo foreach-released publish
displayName: Publish updated Cargo crates
env:
CARGO_REGISTRY_TOKEN: $(CARGO_REGISTRY_TOKEN)
NPM publication
Likewise for NPM packages:
- ${{ if eq(variables['Build.SourceBranchName'], 'rc') }}:
- job: npm_publish
pool:
vmImage: ubuntu-latest
variables:
- group: Deployment Credentials
steps:
- template: azure-deployment-setup.yml
- bash: cranko npm install-token
displayName: Set up NPM authentication
env:
NPM_TOKEN: $(NPM_TOKEN)
# [ do any necessary build stuff here ]
- bash: cranko npm foreach-released npm publish
displayName: Publish to NPM
- bash: shred ~/.npmrc
displayName: Clean up credentials
Integrations: Python
Cranko supports Python projects set up using PyPA-compliant tooling. Because the Python packaging ecosystem contains a lot of variation, Cranko often needs you to give it a few hints to be able to operate correctly.
Autodetection
Cranko identifies Python projects by looking for directories containing files
named setup.py
, setup.cfg
, or pyproject.toml
. It is OK if one directory
contains more than one of these files.
Project Metadata
While the Python packaging ecosystem is moving towards standardized metadata
files, there are still lots of projects where the package name and version are
specified only in the setup.py
file. The only fully correct way to extract
these metadata would be to execute arbitrary Python code, which isn’t possible
for Cranko. Instead, Cranko uses a variety of more superficial techniques to try
extract project metadata.
Project name
- If there is a
pyproject.toml
file containing a keyname
in atool.cranko
section, that value is used as the project name. - Otherwise, if there is a
setup.cfg
file containing aname
key in ametadata
section, that value is used as the project name. - Otherwise, there should be a
setup.py
file containing a line with the following form:
Specifically, Cranko will search for a line containing a comment with the textproject_name = "myproject" # cranko project-name
cranko project-name
. Within such a line, it will then search for a string literal and extract its text as the project name. Cranko’s parsing of Python string literals is quite naive — escaped characters and the like won’t work.
Project version
Cranko will extract the project version from either setup.py
, or from an
arbitrary other Python file (i.e., from myproject/version.py
or something
similar). To tell Cranko to search for the version from a file other than
setup.py
, ensure that your project has a pyproject.toml
file and add an
entry of this form:
[tool.cranko]
main_version_file = "myproject/version.py"
The path should be relative to the directory containing the pyproject.toml
file.
Within that file, there are two options:
- If your project’s version is expressed as sys.version_info tuple, annotate
it with a comment containing the text
cranko project-version tuple
:
Cranko will parse the tuple contents into a PEP-440 version and rewrite it as needed. Note that some PEP-440 versions are not expressible as sys.version_info tuples. Also, Cranko’s tuple parser is quite naive, and only handles the most basic form of Python’s tuple, integer, and string literals. When your repo is bootstrapped, this line will be rewritten to look like:version_info = (1, 2, 0, 'final', 0) # cranko project-version tuple
because Cranko will start managing the version number.version_info = (0, 0, 0, 'dev', 0) # cranko project-version tuple
- If your project’s version is expressed as a string literal, annotate
it with a comment containing just the text
cranko project-version
:
Cranko will search for a string literal in the line and parse it as a PEP-440 version. Here too, Cranko’s parsing of the literal is quite naive and only handles the most basic forms. When your repo is bootstrapped, this line will be rewritten to look like:version = '1.2.0' # cranko project-version
because Cranko will start managing the version number.version = '0.dev0' # cranko project-version tuple
Additional Annotated Files
If there are files within your Python project besides setup.py
or your
main_version_file
that can provide useful metadata to Cranko — or will need
rewriting by Cranko to update versioning and/or dependency information — you
must tell Cranko which files it should check. Otherwise, Cranko would have to
scan every file in your repository, which would significantly slow it down with
large projects.
Tell Cranko which additional files to search by adding an annotated_files
key
to a tool.cranko
section in a pyproject.toml
file for your project:
[tool.cranko]
annotated_files = [
"myproject/npmdep.py",
"myproject/rustdep.py",
]
Internal Dependencies
“Internal” dependencies refer to monorepo situations where one repository contains more than one project, and some of those projects depend on one another.
Cranko actually doesn’t yet automatically recognize internal dependencies between multiple Python projects within one repository — the monorepo model seems to be extremely rare for Python packages. It does, however, recognize internal dependencies in a generic fashion that is useful if, for instance, your repo contains a JupyterLab extension that consists of a Python package that is tightly coupled to an NPM package.
Internal dependencies can be marked by tagging the dependency version requirement in one of your annotated files. Ensure that one or more of these files contains a line of code with the following form:
npm_requirement = '1.2.0' # cranko internal-req myfrontend
In this example, the python package has a dependency on the project name
myfrontend
, and that it requires version 1.2.0 or greater. (Here we envision
that the myfrontend
project is an NPM package, so that this version
requirement is a semver requirement.) As with other annotations, all that
Cranko does here is to search for something that looks like a string literal
within the tagged line, and attempt to parse it. As far as Cranko is concerned,
the only thing that matters in the annotated line is what happens inside the
string literal delimeters. You don’t need to do anything with the associated
variable (npm_requirement
), or even assign the string literal to a variable,
if it’s not needed in your code.
When you bootstrap your project, your tagged line will be rewritten to resemble something like:
npm_requirement = '0.0.0-dev.0' # cranko internal-req myfrontend
because Cranko takes over the expression of concrete version requirements in the repo.
Versioning internal dependencies
As described in Just-in-Time Versioning, Cranko needs the
version requirements of internal dependencies to be expressed as Git commits
rather than version numbers. These requirements must be expressed in the
pyproject.toml
file using the following structuring:
[tool.cranko.internal_dep_versions]
"myfrontend" = "2937e376b962162067135f3ac8b7b6a0f1c3efea"
This entry expresses that the Python project requires a release of the
myfrontend
package that contains the Git commit 2937e376...
. When Cranko
rewrites your project files during release processing, it will translate this
requirement into a concrete version number and update your annotated files
with the appropriate expression.
TODO: write some generic docs about these requirement expressions, and link to them from here.
Integrations: Visual Studio C# Projects
Cranko has basic support for managing Visual Studio C# projects, based on
AssemblyInfo.cs
files. This support has been developed for a narrow use-case
and could potentially become much more sophisticated.
Autodetection
Cranko identifies C# projects by looking for directories that contain a file
with a name ending in .csproj
and another file with a name matching the
pattern */AssemblyInfo.cs
. Cranko will get confused if you have more than one
.csproj
file in a single directory.
Cranko additionally searches for "setup installer" project files, whose names
end in .vdproj
. If such a file is found, and it seems to refer to a single
"primary output project" recognized by Cranko (via a OutputProjectGuid
key),
the ProductVersion
key in the file will be updated to track the corresponding
project version.
Project Metadata
Project metadata are extracted in a fairly basic manner:
Project name
The project name is taken to be the contents of the last <AssemblyName>
element in the .csproj
XML file.
Project version
Cranko will extract the project version from the AssemblyVersion
attribute of
a project's AssemblyInfo.cs
file. In particular, it searches for a line
starting with the exact text [assembly: AssemblyVersion
, and extracts whatever
is between double quotation marks on that line.
C# project versions emulate the .NET System.Version type.
When updating project files, both the AssemblyVersion
and the
AssemblyFileVersion
attributes are updated, if present.
If a project has one or more associated .vdproj
installer projects, the
ProductVersion
stored with the installer(s) will lose the fourth component
(the "revision") of the project version, because four-component versions are
rejected by the installer builder. The PackageCode
and ProductCode
of the
installer will be replaced with a new, randomly-generated UUID (the same one for
both codes). This is a conservative, and possibly sketchy, approach, since it
means that different installer versions will unconditionally be treated as
"major upgrades". See Changing the Product Code for more information.
Internal Dependencies
“Internal” dependencies refer to monorepo situations where one repository contains more than one project, and some of those projects depend on one another.
Cranko automatically detects internal dependencies between C# projects by
searching for <Project>
elements in the .csproj
XML file, where the text
contents of these elements give the GUID of another project. Such elements
should be contained inside a <ProjectReference>
element but Cranko's parser
doesn't bother to require that.
As described in Just-in-Time Versioning, Cranko operates under a model where every internal dependency should be associated with a minimum compatible version of the dependee project, expressed as a Git commit rather than a version number.
There is (currently?) no place where Cranko outputs internal dependency version
requirements into the project files, because such requirements are automatically
embedded into C# assemblies at build time by the compiler. However, Cranko still
prompts you to annotate your projects with this information, because it can help
you keep track of when new project releases must be made. These requirements
should be recorded in each project's .csproj
file in the following way:
<ProjectExtensions>
<Cranko>
<CrankoInternalDepVersion>{c05266fe-6947-42f1-9863-7cdbeed60869}=manual:unused</CrankoInternalDepVersion>
<CrankoInternalDepVersion>{GUID}={req}</CrankoInternalDepVersion>
</Cranko>
</ProjectExtensions>
Each <CrankoInternalDepVersion>
item associates a dependency, identified by
its GUID, with a version requirement. You can use manual:unused
if you don't
want to track such information in detail.
Integrations: Zenodo
Cranko supports safe, automatic software DOI registration through the Zenodo service operated by CERN in collaboration with other scientific organizations.
Orientation: Software DOIs
While most people think of DOIs as associated with scholarly publications, more and more DOIs are being associated with other forms of digital academic output. And, of course, software is more and more becoming an important form of digital academic output! While it is beyond the scope of this documentation to explain software DOIs in depth, it is worth mentioning the distinction between a version DOI and a concept DOI.
Version DOIs are perhaps more familiar. Just like each release of a software package is assigned a unique version number, each release of a software package can be assigned a unique DOI corresponding to that version. If you want to know which specific version of a piece of software that someone was using, either the exact version number or the exact version DOI will tell you that.
If all you care about is knowing what software someone was running, then version DOIs don't add anything new that version numbers don't already provide. However, unlike version numbers, DOIs are first-class items in the scholarly publishing information ecosystem. When you give software a DOI, it can be integrated into that ecosystem in way that isn't possible otherwise. Probably the most important aspect of this is that software DOIs can be associated with author lists and ORCID iDs using standard scholarly metadata systems, so that researchers can get personal credit when their software is used and cited!
Because we want to be able to know exactly what piece of code a person was running, we absolutely want to create a new DOI for each release of a software package. But if that package has a whole bunch of releases, we have a whole bunch of different DOIs, which is going to make it really tedious to quantify the usage of the package overall. This is where concept DOIs come in. Concept DOIs don’t really carry any information on their own, but they can be used in the DOI metadata framework to link together different releases of the same software package in a machine-understandable way. While the DOI 10.5281/zenodo.6963051 is a machine-usable way to talk about “version 4.21.1 of the transformers” package, the concept DOI 10.5281/zenodo.3385997 is a machine-usable way to refer to the thing that is “the transformers package” overall.
Workflow Overview
Cranko’s support for Zenodo “deposition” involves a multi-stage process. It follows the principles of the just-in-time versioning approach where release metadata only ever appear in tested release artifacts.
- During the beginning of CI/CD processing, a new Zenodo deposit is preregistered, and the DOIs that will be created are obtained. These can be inserted into the source files of your software, so that it can print out its own DOI. This step can be run during pull-request processing: but instead of doing anything with the Zenodo API, fake DOIs are generated and used.
- Once CI/CD tests have all passed, you can upload artifacts if so desired, then actually publish the release. Zenodo will actually register the DOIs.
- Because Zenodo deposits are associated with version numbers, each deposit process is associated with a specific cranko project. In a monorepo scenario, you can run multiple deposits for multiple projects as you see fit.
Getting Started
To start using the Zenodo integration, you need to create a Zenodo metadata
file somewhere in your repository. This file is traditionally called
zenodo.json5
and can be stored anywhere you feel like.
While you should see the Zenodo Metadata Files page for the full
details of the file format, the short version is that it has two main fields.
The first, "metadata"
, contains the metadata that will describe your Zenodo
deposition. See the Zenodo developer documentation for a precise
definition of all of the fields that can be used here, or check out Cranko’s
own version of the file for inspiration. The contents of this file
are things you need to decide for yourself, including, most importantly, the
author list that you want to associate with your project.
The second field, "conceptrecid"
, will be used to ensure that successive
releases of your project are all tied together with the same concept DOI. When
creating the first Zenodo release of your software, you should set this to the
special value "new-for:$version"
, where $version
is the planned next version
of the project being released. For instance, you might put "new-for:0.12.0"
at
first. If the preregistration process runs for a different version, it will
error out. This precaution helps make sure that you don’t forget to update your
metadata file once the concept DOI has been created.
If you're using a monorepo, you can make as many Zenodo releases as you like during CI processing. Just run the relevant commands as many times as needed, and create a different Zenodo configuration file for each project that gets assigned DOIs.
Rewrites
The cranko zenodo preregister
command can insert the DOIs that will
be registered into your source code. You can use this functionality to create
software releases that know their own DOIs.
We suggest that you include commands in your software to print out these DOIs,
along the lines of cranko show cranko-version-doi
and cranko show cranko-concept-doi
. This way, there is an easy way for users to get the
precise DOIs relating to the software that they're running. You might also want
to insert these DOIs into logs or metadata associated with the files that your
software creates, although in many cases the version number is going to be more
understandable to users.
This insertion happens during the cranko zenodo preregister
command,
which will rewrite any files whose paths you pass to it on the command line.
The following rewrite rules are followed:
- The text
xx.xxxx/dev-build.$project.version
, where$project
is the name of the Cranko project being released, is replaced with the version DOI. To be explicit, for Cranko itself the template to be replaced would bexx.xxxx/dev-build.cranko.version
. - The text
xx.xxxx/dev-build.$project.concept
, where$project
is the name of the Cranko project being released, is replaced with the concept DOI.
If you’re feeling extra-clever, you can include these templates in your
CHANGELOG.md
entry, and your final changelog will include the DOIs of the
release that it describes. (If you do this, you’ll need to pass the path to
CHANGELOG.md
as an argument to cranko zenodo preregister
.)
If you’re building out of source control, these replacements won't happen, of
course. If a pull request or other non-release build is being processed, or if
you’re in a monorepo and the package in question isn’t being released, fake DOIs
with similar forms will be substituted in. You can add checks in your code to
see whether the DOIs start with the universal DOI prefix, "10."
, to know
whether your DOIs are real or fake.
CI/CD Workflow
Zenodo publication operations require you to have a Zenodo API token,
which you can create in the Zenodo Account Tokens page. You need to get
this token into the environment variable ZENODO_TOKEN
for the Zenodo workflow
to work.
The cranko zenodo preregister
command(s) should be run at the
beginning of your CI/CD workflow, before cranko release-workflow commit
. As
described above, the command inserts placeholders for non-release builds, so you
can run it in all of your workflows without worrying about needing to detect
whether the current build is for a project release. If you’re using a monorepo
with multiple projects that get Zenodo deposits, run the command as many times
as needed. After all invocations are done, you should git add
your modified
files to make sure they get included in the release commit.
At the end of your CI/CD workflow, if you are actually making real releases, you
should run cranko zenodo upload-artifacts
as needed, then finally
cranko zenodo publish
to publish your new deposits. Once again, in
a monorepo scenario, these commands should be run as many times as needed — with
filters in place to only execute them if the projects in question have actually
been released. This can be accomplished with cranko show if-released --exit-code
.
Continued Releases
After your first successful Zenodo deposit, you should update your
zenodo.json5
file and replace the special "conceptrecid"
field with the
Zenodo record ID corresponding to the “concept” of your software package. This
is easily findable in the concept DOI, and is also printed by
cranko zenodo preregister
.
Going forward, you should review the zenodo.json5
file periodically and update
as needed — in particular, you should be attentive to the author list. As with
any academic product, the choice of who goes on an author list, and what order
that list is in, is not something that can be automated — you have to decide how
you want to handle it.
Internal Dependencies
An internal dependency is a dependency between two projects stored in the same repository. Internal dependencies are therefore closely associated with monorepos in Cranko's terminology. You can have a monorepo that doesn't have any internal dependencies, but usually the point of a monorepo setup is to manage a group of interdependent projects.
As outlined in the introduction to Just-in-Time Versioning, internal dependencies (perhaps counterintuitively) take some extra effort to manage. This situation stems from two assumptions in the JIT model:
- Any intra-project dependency (internal or external) needs to be associated with a version requirement specifying the range of versions of the "dependee" package that the "depending" package is compatible with. In simple cases this might be expressible as "anything newer than 1.0", but version requirements can potentially be complex ("anything in the 1.x or 2.x series, but not 3.x, and not 1.10").
- In the JIT versioning model, specific version numbers shouldn't be stored in the main development branch of your repository.
It's important to note that dependency version requirements can't be determined
automatically. Say that I have a monorepo containing two projects,
awesome_database
and awesome_webserver
. It's reasonable to assume that at
any given commit, the two are compatible, but is the development version of
awesome_webserver
compatible with version 1.9 of awesome_database
? Is it
compatible with version 1.1? You could imagine some level of automated API
analysis to test source-level compatibility, but it's always possible that the
semantics of an API can change in a way that maintains source compatibility but
breaks actual usage. Ultimately the only sound approach is for a human to make
this determination.
Getting back to Cranko's challenge: at some point I'm going to want to make a
new release of awesome_webserver
with metadata saying that it requires
awesome_database >= 2.0
. How can I tell Cranko what version requirement to
insert into the awesome_webserver
project files when the main development
branch can't "know" what the most recent version of awesome_database
is?
Commit-Based Internal Dependency Version Requirements
Cranko solves this problem by requiring that you specify internal dependency version requirements as Git commits, not version numbers. For each internal dependency from a "depending" project X on a "dependee" project Y, you must specify a Git commit such that X is compatible with releases of Y whose sources contain that commit in their histories.
What does Cranko do with this information? When making a release of project X, Cranko has sufficient information to determine the oldest version of project Y containing that commit. It will rewrite project X's public metadata to encode that version requirement.
It can happen that no such release exists — perhaps project X requires a new
feature that was just added to Y, and no release of Y has yet been made. Cranko
will detect this situation and, correctly, refuse to let you make a release of
project X. However, you can release X and Y simultaneously (in one rc
push),
and Cranko will detect this and generate correct metadata.
The commit-based model implies a restriction that version requirements for internal dependencies must have the simplest form: X is compatible with any version of Y newer than Z, for some Z determined at release time. This is not expected to be restrictive in practice because Cranko assumes that at any given commit in a monorepo, all projects are compatible as expressed in the source tree.
Expressing Internal Dependency Commit Requirements
Project meta-files don't have native support for commit-based version requirements because it would be inappropriate to include information specific to a project's revision system in such files. Therefore, Cranko always has to define some kind of custom way for you to capture this metadata, with the specific mechanism depending on the project type. For instance:
- In Rust, you add
[package.metadata.internal_dep_versions]
fields in Cargo.toml - In Python, you annotate version requirement lines in your
setup.py
file or equivalent
The documentation for each language integration should specify the approach and specific syntax you should use.
Development with Internal Dependency Requirements
The cranko bootstrap
command will endeavor to update your project files
to include your pre-existing internal version requirements using a special
"manual" mode. This is required for version requirements that reach into a
project's pre-Cranko history.
Once the internal requirements are set up, you should ideally update commit
requirements as APIs are added or broken. For instance, say that project
awesome_database
adds a new API in commit A, and project awesome_webserver
starts using it sometime later in commit B. Commit B should update the
metadata to indicate that awesome_webserver
now requires a version of
awesome_database
based on commit A, or later.
If you don't remember to update the metadata immediately, that's OK. So long as
the metadata for awesome_webserver
are updated sometime before its next
release, the released files will contain the right information.
Projects
A project is a thing that is manifested in a series of releases, each release assigned a unique version. In the Cranko model, each projects’ source materials are tracked in a repository.
We will sometimes refer to projects as “software,“ but it’s worth emphasizing that there’s no reason that a project has to consist of computer source code. It could be a website, a data product, or whatever else. A project might be associated with some kind of external publishing framework, like an npm package or a Rust crate, but it doesn’t have to be.
Prefixing
Cranko associates each project with a certain prefix inside the repository file tree. These prefixes can overlap somewhat: for instance, it is very common that a repository contains a main project at its root, and sub-projects within subdirectories of that root.
By default, Cranko assumes that files inside of a project’s prefix “belong” to
that project, except when those files “belong” to a project rooted in a more
specific prefix. This mapping is used to assess which commits affect which
projects: if a project is rooted in crates/log_util
, and a commit alters the
file crates/log_util/src/color.rs
, that commit is categorized as affecting
that project. A single commit may affect zero, one, or many projects. Cranko
uses this analysis to suggest which projects may be ready for release.
Releases
Cranko’s idea of a release closely tracks the one implied by the semantic versioning specification. Each project in a repo is sent out into the world in a time-series of releases. Each release is associated with a version, a Git commit, and some set of artifacts, which are almost always “files” in the standard computing sense. All of these should be immutable and, to some extent, irrevocable: once you’ve made a release, it’s never coming back. This sounds dangerous, but a big point of release automation is to make the release process so easy that if you mess something up, it’s easy to supersede it with a fixed version.
The fundamental assumption of Cranko is that we are seeking to achieve total automation of the software release process. It is important to point out that Cranko does not seek to automate the decision to release: it is the authors’ opinion that it is important for this decision to be in human hands. (Although if you want to automate that decision too, we can’t and won’t stop you.) But once that decision has been made, as much of the process involved should proceed mechanically. We believe that the just-in-time versioning workflow provides an extremely sound basis on which to make this happen.
Because repositories can contain multiple projects, an individual commit in a repository’s history might be associated with zero, one, or many project releases. This model requires a certain amount of trust: if I release project X in commit Y, I’m implictly asserting that all projects not-X do not need to be released at the same time. There is no way for a computer to know that this is actually true. (The same kind of trust is required by Git’s merge algorithm, which assumes that if two different commits do not alter the same part of the same files, that they do not conflict with one another. This assumption is a good heuristic, but not infallible.) In Cranko’s case, the only way to avoid placing this trust in the user would be to demand that the release of any project requires the release of all projects, which is takes cautiousness to the level of absurdity.
Versions
Every project in Cranko has one or more releases, each of which is associated with a version (AKA “version number”). We can think of the version of the most recent release as being the “current” version of the project, but it is important to remember that in a given repository a project may be in an intermediate state between releases and hence between well-defined version numbers.
Cranko’s model takes pains to avoid strong assumptions about what version “numbers” look like — they don't even need to be numbers — or how they change over time. Well-specified versioning syntaxes like semver are supported, but the goal is to make it possible to use domain-specific syntaxes as well. In particular, at the moment, Cranko supports three schemes:
Python PEP-440 versions
Python packages are assumed to be versioned according to PEP-440. This is a very flexible scheme that allows any number of primary numbers as well as “alpha”, “beta”, “rc”, “dev”, and “post” sequencing. Consult PEP-440 for details.
Used by Python packages.
Semantic Versioning versions
“Semver” versions follow the Semantic Versioning 2 specification.
They generally follow a MAJOR.MINOR.MICRO
structure with optional extra
prerelease and build metadata. The semver specification is rigorously defined
(as you’d hope), so consult that document for details.
Used by Cargo and NPM packages.
.NET Versions
.NET versions emulate the .NET System.Version type. This is a simple
type following the form MAJOR.MINOR.BUILD.REVISION
, where each piece is an
integer. The maximum allowed value of each item is 65534.
Used by packages in Visual Studio C# projects.
The micro bump
version bump syntax will update the "build" component of a
version string. There is currently no syntax to bump the revision component of a
version string.
Configuration
Cranko aims to “just work” with minimal explicit configuration. That being said, flexibility is clearly important in a workflow tool. If some aspect of Cranko’s behavior isn’t configurable, the reason is probably simply that no one has gotten around to wiring up the necessary code, rather than a reluctance to allow flexibility.
The per-repository configuration file
For each Cranko-using repository, the main configuration file is located at
.config/cranko/config.toml
. Cranko can run without this file, and the hope is
that the tool can be very useful without requiring the file’s presence.
For reproducibility and testability, the goal is that as much Cranko configuration as possible can be centralized in this file, without per-user or per-environment customizations. At the moment, no other Cranko configuration files are supported.
The config.toml
file may contain the following items:
[repo]
— Configuration relating to the backing repositoryrc_name
— The name of therc
-like branchrelease_name
— The name of therelease
-like branchrelease_tag_name_format
— The format for release tag namesupstream_urls
— How the upstream remote is recognized
[projects]
— Configuration relating to individual projectsignore
— Flagging projects to be ignored
[npm]
— Configuration relating to the NPM integrationinternal_dep_protocol
— A resolver protocol to use for internal dependencies
As mentioned above, additional items are planned to be added as the need arises.
The [repo]
section
This section contains configuration relating to the backing Git repository.
The rc_name
field
This field is a string specifying the name of the rc
-like branch that will be
used. If unspecified, the default is indeed rc
. The same name will be used in
the local checkout and when consulting the upstream repository.
The release_name
field
This field is a string specifying the name of the release
-like branch that
will be used. If unspecified, the default is indeed release
. The same name
will be used in the local checkout and when consulting the upstream repository.
The release_tag_name_format
field
This field is a string specifying how the names of Git tags corresponding to
releases will be constructed. The default is {project_slug}@{version}
.
Values are interpolated using a standard curly-brace substitution scheme (as
implemented by the curly
module of the dynfmt crate). Available input
variables are:
project_slug
: the “user facing name” of the released projectversion
: the stringification of the version of the released project
The upstream_urls
field
This field is a list of strings giving the Git URLs associated with the
canonical upstream repository, which is the one that will perform automated
release processing upon updates to its rc
-like branch. For example:
upstream_urls = [
"git@github.com:pkgw/cranko.git",
"https://github.com/pkgw/cranko.git"
]
(The name of the upstream remote might change from one checkout to the next, but the set of canonical upsteam URLs should be small.)
The ordering of the URLs does not matter. If the list is empty (i.e. it is
unspecified), and there is only one remote, Cranko will use it. If there is more
than one remote but one is named origin
, Cranko will use that. Otherwise,
Cranko will error out. If more than one remote matches any of the URLs, one of
them will be used but it is unspecified which.
The [projects]
section
This section contains configuration relating to individual projects in the
repository. Cranko generallly prefers to locate this configuration in
project-appropriate metadata files (such as Cargo.toml
), but this isn't always
possible.
This “section” should be a dictionary keyed by the full “qualified names” associated with a project. For instance, for an NPM project, you might configure it with code such as:
[projects."npm:@mymonorepo/tests"]
ignore = true
The ignore
field
This field tells Cranko to ignore the existence of the project in question.
For a variety of reasons, Cranko might autodetect a project in your repository that you never intend to release. This setting allows you to pretend that such a project simply doesn’t exist. The setting is applied in the repository-wide configuration file, not in project metadata, in case the project is imported from a vendor source that doesn’t include Cranko metadata.
The [npm]
section
This section contains configuration pertaining to Cranko’s NPM integration.
The internal_dep_protocol
field
This optional string field specifies a Yarn resolution protocol to insert into
the requirements lines for dependencies internal to a monorepo. If you are using
Yarn as your package manager, setting this to "workspace"
will force Yarn to
always resolve the dependency to one within the workspace. This should help
ensure that your internal dependency version specifications are correct and
self-consistent.
Zenodo Metadata Files
Cranko's Zenodo integration involves one or more configuration files,
traditionally named zenodo.json5
. This page documents the format of these
files.
A project repository may contain zero, one, or many Zenodo metadata files. Cranko does not care about where they live in the repository tree. So long as the commands that run in your CI system refer to the right files in the right places, any filesystem layout is fine.
The Zenodo metadata file is parsed in the JSON5 format. This is a superset of JSON that is slightly more flexible, especially including support for comments.
The overall structure of the Zenodo metadata file should be as follows:
{
"conceptrecid": $string
"metadata": $object
}
conceptrecid
This field is mandatory. When publishing the first release of a project to
Zenodo, it should contain text of the form "new-for:$version"
, where
$version
is the to-be-published version of the project.
After the first release, it should be replaced with the Zenodo “record ID” of
the “concept” item corresponding to the project. This is the serial number
associated with the “Cite all version” item associated with the project. The
cranko zenodo preregister
command will print out this record ID when it runs
for a first release. But don’t worry: it's not hard to figure out this value.
The scheme above is intended to make it so that one does not accidentally create
a series of releases that are not properly linked by their concept identifier.
Because the new-for
mode captures the specific release that it is intended to
be used for, if you forget to update the field, the next release will error out.
metadata
This field is mandatory. It should be filled with Zenodo deposit metadata in the JSON format documented by Zenodo. Use whichever fields are appropriate for your project.
The following fields will be overwritten by Cranko upon preregistration:
title
will be set to"$projectname $projectversion"
publication_date
will be set to today’s date, as understood by whichever computer Cranko is running onversion
will be set to"$projectversion"
Preregistration Rewrites
Upon success of the cranko zenodo preregister
command, this file will be
rewritten to include other metadata specific to the deposit being made. These
updates should not be committed to the main branch of your repository, and you
should not depend on any particular keys being available.
cranko bootstrap
Bootstrap an existing repository to start using Cranko.
Usage
cranko bootstrap [--force] [--upstream UPSTREAM-NAME]
For detailed usage guidance, see the Bootstrapping Workflow section.
The --upstream UPSTREAM-NAME
option specifies the name of the Git remote that
should be considered the canonical “upstream” repository. If unspecified, Cranko
will guess with a preference for the remote named origin
.
The --force
option will force the command to proceed even in unexpected
circumstances, such as when the working tree contains modified files.
cranko confirm
Create a new rc
commit to request the release of one or more projects.
Usage
cranko confirm [--force]
This command gathers release request information prepared from one or more calls
to cranko stage
and synthesizes it into a new commit on the rc
branch.
Edited changelog files in the working directory are then reset to match the HEAD
commit.
The cranko confirm
command analyzes the
internal interdependencies of the
projects within the repository and will refuse to propose a release with
unsatisfied requirements. That is, if a proposed new release of project X would
require a new release of project Y but one is not being requested, the command
will exit with an error.
After the release request is recorded on the rc
branch, in a typical workflow
the release request would be submitted to the CI/CD system by pushing the branch
to the upstream repository.
Example
$ cranko stage foo_util
foo_util: 4 relevant commit(s) since 1.1.0
$ {edit util/CHANGELOG.md}
$ cranko confirm
info: foo_util: micro bump (expected: 1.1.0 => 1.1.1)
info: staged rc commit to `rc` branch
$ git push origin rc
cranko diff
Print a diff comparing the last release of a project to the state of the current working tree.
Usage
cranko diff [PROJECT-NAME]
You can leave [PROJECT-NAME]
unspecified if there's only one project in the
repo.
Example
$ cranko diff
diff --git a/Cargo.lock b/Cargo.lock
index 41bc0b8..02069cd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,22 +1,29 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
+version = 3
+
[[package]]
...
Remarks
This command is helpful to get an overview of the changes that have occurred
since the last release. It farms out its work to the git diff
subcommand,
executing a command of the form:
$ git diff [COMMIT] -- [DIR]
where [COMMIT]
is last the main-branch commit included in the most recent
release of the project in question, and [DIR]
is the primary working directory
associated with that project. In other words, this command is different than
git diff
because it compares against the most recent release commit, as
opposed to the most recent commit of any kind. It also filters the diff output
by repository path.
cranko log
Print repository history for a project since its last release.
Usage
cranko log [--stat] [PROJECT-NAME]
You can leave [PROJECT-NAME]
unspecified if there's only one project in the
repo.
The --stat
argument, if specified, is forwarded to git show
.
Example
$ cranko log
commit d262b397eae451e23c68438fb3ddde6fc64dc65a (HEAD)
Author: Peter Williams <peter@newton.cx>
Date: Sat Apr 3 10:47:18 2021 -0400
...
Remarks
This command is helpful to get an overview of the changes that might potentially
be staged for a release. It generates a list of relevant commits
and then farms out the display work to the git show
subcommand.
cranko stage
Begin the process of preparing one or more projects for release.
Usage
cranko stage [--force] [PROJECT-NAMES...]
If {PROJECT-NAMES}
is unspecified, all projects that have been affected by any
commits since their last release are staged.
Using the --force
flag and explicit {PROJECT-NAMES}
will allow you to stage
projects even if Cranko believes that they have not been affected by any commits
since their most recent releases. This can be useful if, say, you need to
re-attempt a release with updated CI configuration but no code changes.
For each project that is staged, its changelog files in the working directory are rewritten to include template release-request information and a draft set of release notes based on the Git commits affecting the project since its last release. The exact format used will depend on the project’s configuration.
You should edit these files as you see fit to prepare the release notes and set
the parameters of the proposed release. The changelog will include previous
entries which can be revised if desired. When the release information is ready,
use cranko confirm
to prepare a new commit on the rc
branch for submission
to the CI/CD system.
To “un-stage” a project, just restore its changelog files to their unmodified state.
cranko status
Print out information about unreleased changes in in the HEAD commit of the repository.
Usage
cranko status [PROJECT-NAMES]
If {PROJECT-NAMES}
is unspecified, status information is printed about all
projects.
Example
$ cranko status
tcprint: 2 relevant commit(s) since 0.1.1
drorg: 5 relevant commit(s) since 0.3.0
$
cranko cargo foreach-released
Run a Rust cargo command for all Rust/Cargo projects that have had new releases.
Usage
cranko cargo foreach-released
[--pause=SECONDS]
[--command-name=COMMAND]
[--] [CARGO-ARGS...]
This command should be run in CI processing of an update to the rc
branch,
after the release has been vetted and the release commit has been created. The
current branch should be the release
branch.
Example
$ cranko cargo foreach-released -- publish --no-verify
Note that the name of cargo
itself should not be one of the arguments.
Furthermore, due to the way that Cranko parses its command-line arguments, if
any option flags are to be passed to Cargo, you must precede the whole set of
Cargo options with a double-dash (--
). The example above would run cargo publish --no-verify
for each released package — which is
basically the whole reason that this command exists.
Automated publishing requires a Cargo API token. Ideally, such tokens should not
be included in command-line arguments. The cargo publish
command can obtain tokens from the CARGO_REGISTRY_TOKEN
environment variable
(for the Crates.io registry) or CARGO_REGISTRIES_${NAME}_TOKEN
for other
registries. See the cargo publish
docs for the official
documentation.
The --command-name
argument can be used to specify a different command to be
run instead of the default cargo
. For instance, one might use
--command-name=cross
for certain operations in a cross-compiled build using
the rust-embedded/cross framework.
The --pause
argument causes the command to pause for the specified number of
seconds between invocations of cargo
commands, when more than one command is
to be run. This is aimed at cargo publish
workflows, where you can encounter
errors if you try to publish several interdependent crates in rapid succession.
The problem appears to be that Crates.io checks the dependency specifications of
crates as they’re published, and if one crate requires a version of another that
was just published, the check fails. As of writing we don’t know how much of a
delay is enough to avoid this problem, but the Crates.io index repository is
sometimes updated multiple times in the same minute, so something like thirty
seconds is hopefully sufficient.
cranko cargo package-released-binaries
Create archives of the binary files associated with all Rust/Cargo projects that have had new releases.
Usage
cranko cargo package-released-binaries
[--command-name=COMMAND]
[--reroot=PREFIX]
--target {TARGET}
{DEST-DIR} -- [CARGO-ARGS...]
This command should be run in CI processing of an update to the rc
branch,
after the release has been vetted and the release commit has been created. The
current branch should be the release
branch.
Example
$ cranko cargo package-released-binaries -t $target /tmp/artifacts/ -- build --release
$ cranko cargo package-released-binaries \
--command-name=cross \
--reroot=$(pwd) \
-t $target \
/tmp/artifacts/ \
-- build --target=$target --features=vendored-openssl --release
For each Cargo project known to Cranko that has a new release, this command
creates a .tar.gz
or Zip archive file of its associated binaries, if they
exist. These archive files are placed in the {DEST-DIR}
directory
(/tmp/artifacts
) in the example. These can be publicized as convenient release
artifacts for projects that are delivered as standalone executables.
In order to discover these binaries, Cranko must run cargo build
, or a similar
command, for each released project. In particular, it must run a Cargo command
that accepts the --message-format=json
argument and outputs information about
compiler artifacts. Typically, the command of interest would be cargo build --release
, in which case the command line to this tool should end with -- build --release
. However, you might want to include feature flags or other
selectors as appropriate. The --message-flags=json
argument will be
automatically (and unconditionally) appended.
Unlike cranko cargo foreach-released
, this
command selects projects by passing a --package=
argument to the subcommand,
rather than changing the starting directory in which it is invoked. This
behavior is needed for the analysis to work when passing through to cross
(see
below) when there are any Rust packages not rooted at the repository root.
The created archive files will be named according to the format
{cratename}-{version}-{target}.{format}
. The archive format is .tar.gz
on
all platforms except Windows, for which it is .zip
. This format is chosen by
parsing the -t
/--target
argument, not by examining the host platform
information.
Within the archive files, the executables will be included with no pathing
information. In the typical case that there is a Cargo project named foo
with
an associated binary also named foo
, the archive will unpack into a single
file named foo
or foo.exe
. If the project contains multiple binaries, the
archive will contain all of them (unless you add a --bin
option to the Cargo
arguments).
The --command-name
argument can be used to specify a different command to be
run instead of the default cargo
. For instance, one might use
--command-name=cross
for certain operations in a cross-compiled build using
the rust-embedded/cross framework.
The --reroot
argument can be used to rewrite the paths returned by the build
tool. This extremely specific operation is needed for the rust-embedded/cross
framework, which runs inside a Docker container and therefore returns paths that
look like /target/$arch/debug/...
. The value of this argument is naively
prepended to whatever paths are returned from the tool. In the
rust-embedded/cross case, therefore, --reroot=.
obtains paths that are
meaningful on the build host.
cranko ci-util env-to-file
Write the contents of an environment variable to a file, securely.
Usage
cranko ci-util env-to-file
[--decode=[text,base64]]
{VAR-NAME} {FILE-PATH}
This command examines the value of an environment variable {VAR-NAME}
and
writes it to a file on disk at {FILE-PATH}
. Many CI systems expose credentials
and other secret values as environment variables, and sometimes one needs to get
these values into a file on disk for use by an external program. This tool
provides a relatively secure mechanism for doing so, because it avoids inserting
the variable’s value into the command-line arguments of an external program,
which is generally unavoidable when trying to accomplish this effect within a
shell script.
Example
$ cranko ci-util env-to-file --decode=base64 SECRET_KEY_BASE64 secret.key
Note that the variable name is written undecorated, without a leading $
or
wrapping %%
. This is vital! Otherwise your shell will expand the value of the
variable before running the command, which will not only cause it to fail, but
will defeat the whole goal of the command, which is to avoid revealing the
variable’s value on the terminal.
The --decode
option specifies how the value of the variable should be decoded
before writing to disk. In the default, text
, the variable’s value is treated
as Unicode text, in whatever standard is most appropriate for the operating
system, and written to the file in UTF-8 encoding. If the mode is base64
, the
variable’s value is taken to be base64-encoded text, and the decoded binary data
are written out.
The file on disk is created in “exclusive” mode, such that the tool will exit with an error if the file already exists. On Unix systems, it is created such that only the owning user has any access permissions (mode 0o600).
Files created with this tool should be scrubbed off of the filesystem after they
are no longer needed with an approprite utility such as shred
.
cranko github create-custom-release
Create a new GitHub release with customized metadata. You
probably ought to be using cranko github create-releases
instead.
Usage
cranko github create-custom-release
[--draft]
[--prerelease]
--name {NAME}
[--desc {DESC}]
{TAG-NAME}
This command creates a new release on GitHub associated with the tag
{TAG-NAME}
, which should have already been pushed to the GitHub repository.
You should probably using cranko github create-releases
instead of this command. The
create-releases
command efficiently handles monorepos with multiple packages
that may be released at different times, and it automatically calculates the tag
name, release name, and release description to use for each release. This
command should be used only to create GitHub releases that are not associated
with particular projects within the source repository. The motivating use case
is the creation of a special “continuous” GitHub prerelease that is deleted (see
cranko github delete-release
) and recreated with
each update to a project’s main development branch. Note that this command is
essentially decoupled from Cranko’s project-management infrastructure; all it
does is leverage its GitHub API authentication hooks.
By default, GitHub associates each release with a tarball and zipball of the
repository contents at the time of the release. If you want to associate
additional artifacts, use cranko github upload-artifacts
with the --by-tag
option.
Note that GitHub “draft” releases seem to be treated a bit specially by the API. If you create a draft release with this command, some other release-related operations may not work. (If you encounter such a case, please add it to the documentation here.)
cranko github create-releases
Create new GitHub releases associated with all projects that have had releases.
Usage
cranko github create-releases [PROJECT-NAMES...]
This command should be run in CI processing of an update to the rc
branch,
after the release has been vetted and the release commit has been created. The
current branch should be the release
branch.
If {PROJECT-NAMES}
is unspecified, creates releases for all projects that were
released in this run. Otherwise, creates releases only for the name projects,
if they have been released in this run. If an unreleased project is named, a
warning is issued and the project is ignored.
The GitHub releases are identified by the project name and have their description populated with the project release notes. By default, GitHub associates each release with a tarball and zipball of the repository contents at the time of the release. If you want to associate additional artifacts, use cranko github upload-artifacts.
cranko github delete-release
Delete a GitHub release associated with a given tag name.
Usage
cranko github delete-release {TAG-NAME}
This command deletes the GitHub release associated with {TAG-NAME}
.
This command is essentially a generic utility that leverages Cranko’s GitHub
integration. It is provided to support use cases that maintain a “continuous
deployment” release on GitHub that is always associated with the latest push to
a branch (such as master
). In such a use case, on every update to the branch
in question, you’ll want to delete the existing release, then recreate it and
re-populate its artifacts.
Note that this command has no safety checks or “are you sure?” prompts.
cranko github install-credential-helper
Install Cranko as a Git credential helper that will return a
GitHub Personal Access Token (PAT) stored in the environment variable
GITHUB_TOKEN
.
Usage
cranko github install-credential-helper
This command modifies the user-global Git configuration file to install Cranko
as a “credential helper” program that Git uses to
authenticate with remove servers. This particular credential helper uses the
GITHUB_TOKEN
environment variable to authenticate.
Nothing about this command is specific to the Cranko infrastructure. It just comes in handy because Cranko projects need to be able to push to their upstream repositories from CI/CD, and this is tedious to configure without a helper tool.
Furthermore, the only way in which this command is specific to GitHub is in the
name of the environment variable it references, GITHUB_TOKEN
.
The installed credential helper is implemented with a hidden sub-command cranko github _credential-helper
.
cranko github upload-artifacts
Upload artifact files to be associated with a GitHub release.
Usage
cranko github upload-artifacts
[--overwrite]
[--by-tag]
{PROJECT-NAME} {PATH1 [PATH2...]}
This command will upload several local files to GitHub and associate them with a GitHub release.
The command operates in two modes. By default, the release that’s modified is
the one associated with the Cranko project {PROJECT-NAME}
, which is expected
to have been released in the current rc
run. That release should have been
created by the cranko github create-releases
command. In this situation, this command should be run in CI processing of an
update to the rc
branch, after the release has been vetted and the release
commit has been created. The current branch should be the release
branch.
Alternatively, if the --by-tag
option is given, the {PROJECT-NAME}
argument
is treated as a Git tag name that will be looked up directly on GitHub. This
mode is useful if you are trying to upload artifacts associated with a release
created with cranko github create-custom-release
. In this case, the
notion of the “current release” is not necessary, so Cranko’s checks for the
state of the environment are not invoked.
This command assumes that a GitHub Personal Access Token (PAT) is
available in an environment variable named GITHUB_TOKEN
.
Because it does not make sense for this command to parallelize over released projects, it has relatively few tie-ins with the Cranko infrastructure. The key touch-point is how, in the default mode, this command uses the Cranko release information and project name to know which Git tag the artifact files should be associated with.
Example
# `rc` branch; we know that project foo_data has been released
$ cranko github create-releases foo_data
$ cranko github upload-artifacts foo_data compiled_v1.dat compiled_v2.data
cranko npm foreach-released
Run a command for all npm projects that have had new releases.
Usage
cranko npm foreach-released [--] [COMMAND...]
This command should be run in CI processing of an update to the rc
branch.
Example
$ cranko npm foreach-released -- npm publish
This would run npm publish
for each released package — which is
basically the whole reason that this command exists. The command is run “for”
each package in the sense that the initial directory of each executed command is
the directory containing the package’s package.json
file.
Automated publishing requires an NPM registry authentication token. Such a token
can be securely installed into the per-user .npmrc
configuration file with
cranko npm install-token
.
cranko npm install-token
Install an NPM authentication token into the per-user .npmrc
or .yarnrc.yml
configuration file to enable the publishing of NPM packages.
Usage
cranko npm install-token [--yarn] [--registry=REGISTRY]
This command appends a user-global configuration file to include an
authentication token from the environment variable NPM_TOKEN
.
By default, the configuration is targeted at the npm
command: the .npmrc
file is edited, and the default REGISTRY
is //registry.npmjs.org/
.
If the --yarn
option is specified, the .yarnrn.yml
file is instead edited,
and the default REGISTRY
is https://registry.yarnpkg.com/
. Note that in this
mode the name of the input environment variable is still NPM_TOKEN
. The same
token will work with Yarn, but needs to be placed in this different file in
order for the yarn npm publish
command to work.
Nothing about this command is specific to the Cranko infrastructure. It just
comes in handy because publishing to NPM is a common release automation task,
and there aren’t many good ways to get a credential like $NPM_TOKEN
from the
environment into a file without exposing it on the command line of a program.
For maximum security, the .npmrc
or .yarnrc.yml
file should be destroyed
with a tool like shred
after it is no longer needed.
cranko npm lerna-workaround
Rewrite internal version requirements of npm projects so that Lerna will understand them.
Usage
cranko npm lerna-workaround
This command will rewrite the package.json
files of your NPM projects.
Example
The Lerna tool is somewhat limited in its understanding of internal dependencies within a repository. If projects A and B are both at version 0.3, and project B states a requirement on version 0.3 of project A, Lerna understands the dependency. However, if project B only requires version 0.2 of project A, Lerna won't realize that the interdependency is internal. This will cause its understanding of the project dependency ordering to be incomplete, potentially leading to build-time errors.
This command can temporarily rewrite your files so that Lerna will correctly understand the internal dependencies. Once you are done using Lerna, you can use Git to revert the changes, restoring your packages to be annotated with the correct dependencies.
A sample CI workflow might look like:
$ cranko release-workflow apply-versions # write correct versions
$ git add .
$ cranko release-workflow commit # save them in a release commit
$ cranko npm lerna-workaround # write fake dep values to working tree
$ lerna bootstrap # do Lerna-y stuff
$ lerna run build
...
$ lerna run test # done with Lerna
$ git checkout . # throw away fake deps
cranko python foreach-released
Run a command for all PyPA projects that have had new releases.
Usage
cranko python foreach-released [--] [COMMAND...]
This command should be run in CI processing of an update to the rc
branch.
Example
$ cranko python foreach-released -- touch upload-me.txt
This would run the command touch upload-me.txt
for each released Python
package. The command is run “for” each package in the sense that the initial
directory of each executed command is the directory containing the package’s
project meta-files.
Note that this command is not so useful because the recommended PyPA publishing
command, twine upload
, needs to be passed the name of the distribution
file(s) to upload, and this Cranko command currently doesn’t give you a
convenient way to interpolate those names. This feature isn’t fully baked
because we’re unaware of any examples of single repositories containing multiple
Python projects, so “vectorization” over all Python releases isn’t needed. For
now, check whether your Python project was released using cranko show if-released
, and run its publishing commands manually.
cranko python install-token
Install a PyPI authentication token into the per-user .pypirc
configuration file to enable the publishing of Python packages to PyPI.
Usage
cranko python install-token [--repository=REPO]
This command appends the user-global python configuration file .pypirc
to
include an authentication token from the environment variable PYPI_TOKEN
. The
default REPO
is pypi
.
Nothing about this command is specific to the Cranko infrastructure. It just
comes in handy because publishing to PyPI is a common release automation task,
and there aren’t many good ways to get a credential like $PYPI_TOKEN
from the
environment into a file without exposing it on the command line of a program.
For maximum security, the .pypirc
file should be destroyed with a tool like
shred
after it is no longer needed.
cranko release-workflow apply-versions
Edit the files in the working tree to apply the version numbers requested in the
current rc
release request.
Usage
cranko release-workflow apply-versions [--force]
This command should be run as early as possible in all forms of your CI/CD
pipeline. It will rewrite your project metadata files (package.json
,
Cargo.toml
, etc.) to apply new version numbers as needed. On pushes to the
rc
branch, if the CI test suite passes, a final release commit should be
created with cranko release-workflow commit
and then pushed to the upstream release
branch to “lock in” the requested
releases.
For each project, new versions are computed by applying a “bump specification” to
the version logged in the metadata of the most recent commit on the release
branch. If that branch does not exist, and for newly-created projects, the
reference version defaults to 0.0.0
or its equivalent. For pushes to the rc
branch, projects whose releases have been requested have bumps applied based on
the metadata of the rc
release request. In other cases — such as PRs or pushes
to the main development branch — all project versions are bumped using the
default ”development mode” scheme, which usually applies a datecode or some
other kind of informal identifier. Artifacts built in this mode should not be
released openly.
cranko release-workflow commit
Commit staged changes to the release
branch, recording information about new
releases.
Usage
cranko release-workflow commit [--force]
This command should be run in CI processing of an update to the rc
branch,
after the release has been vetted. The current branch should be the rc
branch.
This command will switch the current branch to the release
branch, pointing at
the new release commit.
This command should be run after cranko release-workflow apply-versions
to create the final release
commit marking
the successful release of the packages submitted as part of the current rc
request. It can be run either before or after the release request is confirmed
to be successful; but if it is run before, care should be taken that the commit
is pushed to the upstream repository if and only if the CI tests are
successful.
Unlike cranko confirm
, this command respects the Git
staging workflow, operating like git commit
itself. Before running this
command, you should first run git add .
or something similar before it to
stage all changed files. Note that in some workflows, a full build will result
in modifications to files beyond those edited by the apply versions
command, although ideally this should happen as
minimally as possible. For instance, while Cranko can rewrite a Cargo.toml
file for you, it does not attempt to rewrite Cargo.lock
, which will instead be
updated by the next call to cargo build
or a similar command. Therefore, you
should make sure that your git add
command includes both the Cargo.toml
and the Cargo.lock
files when staging for the release commit.
cranko release-workflow tag
Create Git tags corresponding to the projects that were released in an rc
build.
Usage
cranko release-workflow tag
This command should be run in CI processing of an update to the rc
branch,
after the release has been vetted and the release commit has been created. The
current branch should be the release
branch.
For every project that was released in this rc
submission, a new Git version
tag is created according to its tag name format. These tags should then be
pushed to the upstream with git push --tags
.
Example
$ cranko release-workflow tag
info: created tag cranko@0.0.12 pointing at HEAD (e71c2aa)
cranko zenodo preregister
Prepare a new Zenodo deposit to be associated with a release of a project.
Usage
cranko zenodo preregister
[--force] [-f]
--metadata=JSON5-FILE
PROJECT-NAME
REWRITE-FILES[...]
This command should be run in CI processing of an update to the rc
branch,
before cranko release-workflow commit
.
Example
cranko zenodo preregister --metadata=ci/zenodo.json5 cranko src/main.rs
This will preregister a new Zenodo deposit for the cranko
project, using
metadata from the file ci/zenodo.json5
. Both that file and src/main.rs
will
be rewritten to contain DOI information generated by the preregistration.
Remarks
See the Zenodo integration documentation for an overview and description of Cranko's support for Zenodo deposition, including the rewrite format used by this command. See Zenodo Metadata Files for a specification of the metadata file used by this command.
This command can be run during pull requests, not just during formal releases.
In that case, fake DOIs will be used for the rewrite steps. These are guaranteed
to start with the text "xx.xxxx/"
, unlike real DOIs which always start with
10.
. The DOIs should be obviously fake to any user that sees them, but if you
have code that embeds or outputs those DOIs, you may wish to add tests that
check for these fake values and issue warnings as appropriate.
This command requires that the environment variable ZENODO_TOKEN
has been set
to a Zenodo API token, during release processing only. During pull request
processing, you should make sure not to provide this parameter, so that it
is not accessible to malicious submissions. As a precaution, in the latter
circumstance, the command will exit with an error if the environment variable is
non-empty.
See also
- Integrations: Zenodo
- Configuration: Zenodo Metadata Files
cranko release-workflow commit
cranko zenodo upload-artifacts
cranko zenodo publish
cranko zenodo publish
Publish a new Zenodo deposit, triggering registration of its DOI.
Usage
cranko zenodo publish
[--force] [-f]
--metadata=JSON5-FILE
This command should be run in CI processing of an update to the rc
branch,
after cranko zenodo preregister
and any invocations of
cranko zenodo upload-artifacts
.
Example
cranko zenodo publish --metadata=ci/zenodo.json5
This will publish the Zenodo deposit whose metadata are tracked in the file
ci/zenodo.json5
.
Remarks
See the Zenodo integration documentation for an overview and description of Cranko's support for Zenodo deposition. See Zenodo Metadata Files for a specification of the metadata file used by this command.
This command requires that the environment variable ZENODO_TOKEN
has been
set to a Zenodo API token.
This command should only be run during formal releases, and not during pull requests. Note also that you can choose to not run this command in your CI/CD pipeline, and instead manually publish your Zenodo deposit after review by a human. That may be tempting, because Zenodo deposits cannot be changed once they are published. However, our experience is that it is more reliable and more convenient to fully automate the publication process and fix bugs in that automation as they arise, rather than including a human in the loop. If releases and deposits are “cheap”, there’s no problem with superseding them when one turns out to have a problem.
See also
- Integrations: Zenodo
- Configuration: Zenodo Metadata Files
cranko zenodo preregister
cranko zenodo upload-artifacts
cranko zenodo upload-artifacts
Upload files to be associated with an in-progress Zenodo deposit.
Usage
cranko zenodo upload-artifacts
[--force] [-f]
--metadata=JSON5-FILE
FILES[...]
This command should be run in CI processing of an update to the rc
branch,
after cranko zenodo preregister
and before cranko zenodo publish
.
Example
cranko zenodo upload-artifacts --metadata=ci/zenodo.json5 build/mypackage-0.1.0.tar.gz
This will upload the file build/mypackage-0.1.0.tar.gz
and associate it with
the Zenodo deposit whose metadata are tracked in the file ci/zenodo.json5
.
Remarks
See the Zenodo integration documentation for an overview and description of Cranko's support for Zenodo deposition. See Zenodo Metadata Files for a specification of the metadata file used by this command.
This command requires that the environment variable ZENODO_TOKEN
has been
set to a Zenodo API token.
This command should only be run during formal releases, and not during pull requests.
See also
- Integrations: Zenodo
- Configuration: Zenodo Metadata Files
cranko zenodo preregister
cranko zenodo publish
cranko git-util reboot-branch
This command resets this history of a Git branch to be a single commit containing a specified tree of files. It can be useful to update GitHub Pages or similar services that publish content based on a Git branch that whose history is unimportant.
Usage
cranko git-util reboot-branch [-m {MESSAGE}] {BRANCH} {ROOTDIR}
Rewrites the local version of the Git branch {BRANCH}
to contain a single
commit whose contents are those of the directory {ROOTDIR}
. If specified,
{MESSAGE}
is used as the Git commit message. The commit author is generic.
The history of the named branch is completely obliterated. If it is to be pushed to any remotes, it will need to be force-pushed.
Example
# During CI build/test of `rc` commit:
$ ./website/generate.sh
# After release is locked in:
$ cranko git-util reboot-branch gh-pages ./website/content/
$ git push -f origin gh-pages
cranko help
Prints out help information
Usage
cranko help {COMMAND}
This is equivalent to cranko {COMMAND} --help
.
cranko list-commands
This command prints out the sub-commands of cranko
that are available.
Usage
cranko list-commands
Example
$ cranko list-commands
Currently available "cranko" subcommands:
confirm
git-util
github
help
list-commands
release-workflow
show
stage
status
If a command is available in $PATH
under the name cranko-extension
, it will
be available as cranko extension
.
cranko show
The cranko show
command displays various potentially useful pieces of
information about Cranko, its execution environment, and so on. It provides
several subcommands:
cranko show cranko-concept-doi
cranko show cranko-version-doi
cranko show if-released
cranko show tctag
cranko show toposort
cranko show version
cranko show cranko-concept-doi
This commands prints the concept DOI associated with the Cranko software package.
Usage
cranko show cranko-concept-doi
Remarks
The printed DOI is a citeable identifier associated with Cranko that will never change. Each individual release of Cranko is also associated with a “version DOI”, which you can use to log the specific version of Cranko that you used in a particular workflow. Citation metadata link the different version DOIs through the concept DOI.
You are unlikely to need this command in everyday workflows.
cranko show cranko-version-doi
This commands prints the DOI associated with the currently running version of Cranko.
Usage
cranko show cranko-version-doi
Remarks
Each release of Cranko should have a unique version number as well as a unique version DOI. While most DOIs resolve to scholarly publications, Cranko version DOIs “resolve” to a specific release of Cranko, logged with associated metadata and digital artifacts. If you wish to record the exact version of Cranko that you used in a workflow in the context of a scholarly citation system, use this DOI.
cranko show if-released
This command prints whether a project was just released. It expects to be run on
a CI system with the release
branch checked out, after the build has succeeded
and cranko release-workflow commit
../cicd/release-workflow-commit.md) has
been invoked.
Usage
cranko show if-released [--exit-code] [--tf] {PROJECT_NAME}
Different arguments activate different modes by which the program will indicate whether the named project was just released.
--exit-code
: the program will exit with a success exit code (0 on Unix-like systems) if the project was released. It will exit with an error exit code (1 on Unix-like systems) if the project was not released.--tf
: the program will print out the wordtrue
if the project was released. It print out the wordfalse
if the project was not released.
At least one such mechanism must be activated.
Example
$ cranko show if-released --tf myproject
false
cranko show tctag
This command prints out a thiscommit:
tag that includes the current date and
some random characters, for easy copy-pasting into Cranko internal-dependency
lines.
Usage
cranko show tctag
Example
$ cranko show tctag
thiscommit:2021-06-03:NmEuWn3
cranko show toposort
This command prints out the names of the projects in the repository, one per line, in topologically-sorted order according to internal dependencies. That is, the name of a project is only printed after the names of all of its dependencies in the repo have already been printed. Because dependency cycles are prohibited, this is always possible. The exact ordering may not be stable, even from one invocation to the next.
Usage
cranko show toposort
Example
$ cranko show toposort
tectonic_errors
tectonic_status_base
tectonic_io_base
tectonic_engine_xetex
tectonic
cranko show version
This command prints out the version assigned to a project.
Usage
cranko show version {PROJECT_NAME}
Example
$ cranko show version foo_lib
0.1.17