Skip to content

Commit

Permalink
feat: Progress implementing release for xtask (#151)
Browse files Browse the repository at this point in the history
This commit moves closer to a full implementation of the `release`
subcommand for `cargo xtask`. This includes some improvements to the
underlying pipeline-handling code, as well as improvements to the
release workflow itself. There are now more checks run before
executing anything, as well as guaranteed rollback, and implementation
of the changelog generation is underway.

Signed-off-by: Andrew Lilley Brinker <alilleybrinker@gmail.com>
  • Loading branch information
alilleybrinker authored Mar 5, 2024
1 parent d212815 commit a5b7bcb
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 86 deletions.
80 changes: 80 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,83 @@ lto = "thin"

[workspace.metadata.release]
pre-release-commit-message = "chore: Release"


# git-cliff ~ configuration
# https://git-cliff.org/docs/configuration

[workspace.metadata.git-cliff.changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{% if version -%}
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else -%}
## [Unreleased]
{% endif -%}
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# template for the changelog footer
footer = """
{% for release in releases -%}
{% if release.version -%}
{% if release.previous.version -%}
[{{ release.version | trim_start_matches(pat="v") }}]: \
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
/compare/{{ release.previous.version }}..{{ release.version }}
{% endif -%}
{% else -%}
[unreleased]: https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
/compare/{{ release.previous.version }}..HEAD
{% endif -%}
{% endfor %}
<!-- generated by git-cliff -->
"""
# remove the leading and trailing whitespace from the templates
trim = true

[workspace.metadata.git-cliff.git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^.*: add", group = "Added" },
{ message = "^.*: support", group = "Added" },
{ message = "^.*: remove", group = "Removed" },
{ message = "^.*: delete", group = "Removed" },
{ message = "^test", group = "Fixed" },
{ message = "^fix", group = "Fixed" },
{ message = "^.*: fix", group = "Fixed" },
{ message = "^.*", group = "Changed" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = true
# regex for matching git tags
tag_pattern = "v[0-9].*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
4 changes: 3 additions & 1 deletion xtask/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ edition.workspace = true

[dependencies]
anyhow = "1.0.80"
cargo_metadata = "0.18.1"
clap = "4.5.1"
duct = "0.13.7"
env_logger = "0.11.2"
log = "0.4.20"
pathbuf = "1.0.0"
which = "6.0.0"
xshell = "0.2.5"
20 changes: 12 additions & 8 deletions xtask/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub fn args() -> ArgMatches {
.arg(
arg!(--execute)
.required(false)
.default_value("false")
.value_parser(value_parser!(bool))
.help("not a dry run, actually execute the release"),
),
Expand All @@ -37,25 +38,28 @@ pub enum Crate {
OmniBor,
}

impl Display for Crate {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
impl Crate {
pub fn name(&self) -> &'static str {
match self {
Crate::GitOid => write!(f, "gitoid"),
Crate::OmniBor => write!(f, "omnibor"),
Crate::GitOid => "gitoid",
Crate::OmniBor => "omnibor",
}
}
}

impl Display for Crate {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.name())
}
}

impl ValueEnum for Crate {
fn value_variants<'a>() -> &'a [Self] {
&[Crate::GitOid, Crate::OmniBor]
}

fn to_possible_value(&self) -> Option<PossibleValue> {
Some(match self {
Crate::GitOid => PossibleValue::new("gitoid"),
Crate::OmniBor => PossibleValue::new("omnibor"),
})
Some(PossibleValue::new(self.name()))
}
}

Expand Down
87 changes: 52 additions & 35 deletions xtask/src/pipeline.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
use anyhow::{bail, Error, Result};
use anyhow::{anyhow, bail, Error, Result};
use std::error::Error as StdError;
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::iter::Iterator;
use std::result::Result as StdResult;

/// A mutable-reference [`Step`]` trait object.
pub type DynStep<'step> = &'step mut dyn Step;

/// Run a pipeline of steps in order, rolling back if needed.
///
/// The type signature here is a little funky, but it just means that
/// it takes as a parameter something which can be turned into an owning
/// iterator over mutable references to Step trait objects.
///
/// This lets the user call it with just a plain array of trait objects,
/// also assisted by the `step!` macro.
pub fn run<'step, I, It>(steps: I) -> Result<()>
pub struct Pipeline<It>
where
It: Iterator<Item = DynStep<'step>>,
I: IntoIterator<Item = DynStep<'step>, IntoIter = It>,
It: Iterator<Item = DynStep>,
{
fn inner<'step>(steps: impl Iterator<Item = DynStep<'step>>) -> Result<()> {
steps: It,
force_rollback: bool,
}

impl<It> Pipeline<It>
where
It: Iterator<Item = DynStep>,
{
/// Construct a new pipeline.
pub fn new<I>(steps: I) -> Self
where
I: IntoIterator<Item = DynStep, IntoIter = It>,
{
Pipeline {
steps: steps.into_iter(),
force_rollback: false,
}
}

/// Force rollback at the end of the pipeline, regardless of outcome.
pub fn force_rollback(&mut self) {
self.force_rollback = true;
}

/// Run the pipeline.
pub fn run(self) -> Result<()> {
let mut forward_err = None;
let mut completed_steps = Vec::new();

// Run the steps forward.
for step in steps {
if let Err(forward) = forward(step) {
for mut step in self.steps {
if let Err(forward) = forward(step.as_mut()) {
forward_err = Some(forward);
completed_steps.push(step);
break;
Expand All @@ -35,10 +48,12 @@ where
completed_steps.push(step);
}

// If forward had an error, initiate rollback.
if let Some(forward_err) = forward_err {
for step in completed_steps {
if let Err(backward_err) = backward(step) {
// If we're forcing rollback or forward had an error, initiate rollback.
if self.force_rollback || forward_err.is_some() {
let forward_err = forward_err.unwrap_or_else(StepError::forced_rollback);

for mut step in completed_steps {
if let Err(backward_err) = backward(step.as_mut()) {
bail!(PipelineError::rollback(forward_err, backward_err));
}
}
Expand All @@ -48,14 +63,15 @@ where

Ok(())
}

inner(steps.into_iter())
}

/// A Boxed [`Step`]` trait object.
pub type DynStep = Box<dyn Step>;

#[macro_export]
macro_rules! step {
( $step:expr ) => {{
&mut $step as &mut dyn Step
Box::new($step) as Box<dyn Step>
}};
}

Expand Down Expand Up @@ -95,11 +111,8 @@ pub trait Step {
/// Note that this trait does _not_ ensure graceful shutdown if
/// you cancel an operation with a kill signal before the `undo`
/// operation can complete.
fn undo(&mut self) -> Result<()>;

/// Check if a step mutates the environment, so undo might be skipped.
fn can_skip_undo(&self) -> bool {
false
fn undo(&mut self) -> Result<()> {
Ok(())
}
}

Expand All @@ -115,11 +128,6 @@ fn forward(step: &mut dyn Step) -> StdResult<(), StepError> {

/// Helper function to run a step backward and convert the error to [`StepError`]
fn backward(step: &mut dyn Step) -> StdResult<(), StepError> {
if step.can_skip_undo() {
log::info!("skipping rollback for step '{}'", step.name());
return Ok(());
}

log::info!("rolling back step '{}'", step.name());

step.undo().map_err(|error| StepError {
Expand Down Expand Up @@ -207,6 +215,15 @@ struct StepError {
error: Error,
}

impl StepError {
fn forced_rollback() -> Self {
StepError {
name: "forced-rollback",
error: anyhow!("forced rollback"),
}
}
}

impl Display for StepError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "step '{}' failed", self.name)
Expand Down
Loading

0 comments on commit a5b7bcb

Please sign in to comment.