Skip to content

Release pipeline automation

Elia Schito edited this page Oct 4, 2023 · 10 revisions

The Solidus repository at solidusio/solidus relies on a series of automation to streamline the processes related to delivering new versions.

⚠️ WARNING: Most of the integrations are controlled by labels on Pull Requests. For this reason, we need to be very careful before renaming or deleting them.

Triaging

All submitted PRs get assigned a label that identifies which gem the changes belong to:

  • changelog:solidus_core
  • changelog:solidus_backend
  • changelog:solidus_api
  • changelog:solidus_sample
  • changelog:solidus
  • changelog:repository: identifies updates that don't belong to any gem.

The triage is automatically done by the "Triage for Changelog" workflow by checking the files modified on a PR. On top of that, the "Ensure Changelog label" workflow fails if none of those labels is present.

Eventually, the information will be leveraged to create the release notes and update the Changelog. The changelog:skip label can be manually applied to skip a PR from that downstream automation.

Release notes drafting

Whenever a PR is merged, their changelog labels are used to add a new entry in the GitHub release draft for the next version.

That's done by the "Update Release Draft on GitHub" workflow.

⚠️ As the release notes are automatically generated, we must avoid manually updating them.

Preparing a release

  1. [For major+minor releases] Create a branch in solidus_starter_frontend corresponding to the support branch name (e.g. for v1.2.3 the branch is named v1.2)
  2. [For major+minor releases] In the new branch, update the .circleci/config.yml to point to the support branch name for the CI, as shown in this example commit
  3. Dispatch the "Prepare release" workflow (see below for detailed instructions)
  4. A new PR for the picked branch will be ready with the following:
    • References to the current Solidus version bumped to the release candidate one
    • Updated Changelog with the contents of the corresponding release notes
    • The PR contains the release:generate label (also release:generate-last_minor if marked as such)
    • The labels will be used to perform the after-release chores
  5. Manually publish the gems on rubygems through the gem release command provided by the gem-release gem (this step is not automated because it requires 2FA for better security)

Dispatching the Prepare release workflow

When the time comes to release a new Solidus version, we can manually dispatch the "Prepare release" workflow (for alternative ways of dispatching see the GitHub documentation).

To prepare a new release from GitHub's UI, navigate to the "Actions" tab and select the "Prepare release" item.

screenshot-github com-2022 12 23-05_04_40

After that, click on the "Run workflow" dropdown. It'll prompt you to select the branch from which the release needs to be prepared and whether it is the last release after preparing a major.

screenshot-github com-2022 12 23-05_14_10

Alternatively, the most straightforward way to run the workflow from the command line is through GitHub's CLI tool:

# Prepare a release on master
gh workflow run prepare_release.yml

# Prepare a release on master for the previous release after the last minor in a series
gh workflow run prepare_release.yml -F last_minor=true

# Prepare a release for v3.2
gh workflow run prepare_release.yml --ref v3.2

Performing after-release chores

Once a PR preparing a release is merged, a new PR is created, automating the post-release chores:

  • Bumping version to the next candidate .dev one.
  • Publishing the release draft and tagging the last commit accordingly.
  • Creating a new v* branch and bumping to the next patch development version for releases on master (minor and major releases).

That's coordinated thanks to the "Prepare post-release" workflow.

Extracting pipeline information

Most of the automation mentioned above needs information about the active pipeline, e.g., the current version, the candidate tag, etc. That's orchestrated by the "Extract Pipeline Context" composite action. We can summarize its work:

  • The most recent tag is extracted from the list of tags in the repository:
    • For master, that's going to be the higher one.
    • For v* branches, that will be the higher one for the branch.
  • For the to-be-released candidate:
    • For master:
      • It's one minor level higher than the current tag when Spree::VERSION doesn't track a major release.
      • It's one major level higher than the current tag when Spree::VERSION tracks a major release.
    • For v* branches, it's one patch level higher than the current tag.
  • For the next to-be-released candidate (only used to prepare the after-release chores):
    • For master:
      • It's one minor level higher than the candidate tag when we don't specify that a candidate will be the last minor release (see how to prepare a release).
      • It's one major level higher than the current tag when we specify that a candidate will be the last minor release.
    • For v* branches, it's one patch level higher than the to-be-released tag.

Diagram

flowchart LR
  Contributor("⌨️")
  Maintainer("🧹")
  TriageWorkflow("🧮 Triage")
  EnsureLabelWorkflow("🧮 Ensure Label")
  PrepareReleaseWorkflow("🧮 Prepare release")
  PreparePostReleaseWorkflow("🧮 Prepare post-release")
  Contributor-->ContributionPR--> TriageWorkflow --> EnsureLabelWorkflow --> Draft
  Maintainer-->PrepareReleaseWorkflow-->PrepareReleasePR-->PreparePostReleaseWorkflow
  PrepareReleaseWorkflow <--> Draft
  PreparePostReleaseWorkflow --> PreparePostReleasePR
  PreparePostReleaseWorkflow --> Tag
  PreparePostReleaseWorkflow -- master? --> Branch
  PreparePostReleaseWorkflow -- publish --> Draft
  subgraph "Pull requests"
    PrepareReleasePR("🔃 Prepare release (changelog:skip, release:generate)")
    PreparePostReleasePR("🔃 Prepare post-release")
    ContributionPR("🔃 Contribution (changelog:solidus_core)")
  end
  subgraph "Release notes"
    Draft("Draft")
  end
  subgraph "Tags"
    Tag("Tag")
  end
  subgraph "Branches"
    Branch("Branch")
  end