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:
    curl --proto '=https' --tlsv1.2 -sSf https://pkgw.github.io/cranko/fetch-latest.sh | sh
    
    If your CI/CD environment doesn't make Cranko available in a more standardized way, this is the recommended installation command.
  • 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:

  1. 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.
  2. 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:

  1. Choose a commit from main and propose it to rc.

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:

  1. 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 and cranko release-workflow commit.
  2. 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:

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

  1. If there is a pyproject.toml file containing a key name in a tool.cranko section, that value is used as the project name.
  2. Otherwise, if there is a setup.cfg file containing a name key in a metadata section, that value is used as the project name.
  3. Otherwise, there should be a setup.py file containing a line with the following form:
    project_name = "myproject"  # cranko project-name
    
    Specifically, Cranko will search for a line containing a comment with the text 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:

  1. If your project’s version is expressed as sys.version_info tuple, annotate it with a comment containing the text cranko project-version tuple:
    version_info = (1, 2, 0, 'final', 0)  # 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 = (0, 0, 0, 'dev', 0)  # cranko project-version tuple
    
    because Cranko will start managing the version number.
  2. If your project’s version is expressed as a string literal, annotate it with a comment containing just the text cranko project-version:
    version = '1.2.0'  # 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 = '0.dev0'  # cranko project-version tuple
    
    because Cranko will start managing the version number.

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 be xx.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:

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 project
  • version: 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 on
  • version 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

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

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

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

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 word true if the project was released. It print out the word false 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