diff --git a/crates/infra/cli/src/commands/publish/cargo/mod.rs b/crates/infra/cli/src/commands/publish/cargo/mod.rs index 75b4bd64bd..1e32d3389c 100644 --- a/crates/infra/cli/src/commands/publish/cargo/mod.rs +++ b/crates/infra/cli/src/commands/publish/cargo/mod.rs @@ -2,12 +2,15 @@ use std::iter::once; use std::path::Path; use anyhow::Result; +use clap::Parser; use infra_utils::cargo::CargoWorkspace; use infra_utils::commands::Command; use infra_utils::git::TemporaryChangeset; use infra_utils::paths::PathExtensions; use itertools::Itertools; +use crate::utils::DryRun; + const USER_FACING_CRATES: &[&str] = &[ // Sorted by dependency order (from dependencies to dependents): "metaslang_cst", @@ -16,36 +19,44 @@ const USER_FACING_CRATES: &[&str] = &[ "slang_solidity", ]; -pub fn publish_cargo(dry_run: bool) -> Result<()> { - let mut changeset = TemporaryChangeset::new( - "infra/cargo-publish", - "prepare Cargo packages for publishing", - )?; +#[derive(Clone, Debug, Parser)] +pub struct CargoController { + #[command(flatten)] + dry_run: DryRun, +} + +impl CargoController { + pub fn execute(&self) -> Result<()> { + let mut changeset = TemporaryChangeset::new( + "infra/cargo-publish", + "prepare Cargo packages for publishing", + )?; - let mut changed_crates = vec![]; + let mut changed_crates = vec![]; - for crate_name in USER_FACING_CRATES { - if prepare_for_publish(crate_name, &mut changeset)? { - changed_crates.push(crate_name); + for crate_name in USER_FACING_CRATES { + if prepare_for_publish(crate_name, &mut changeset)? { + changed_crates.push(crate_name); + } } - } - if changed_crates.is_empty() { - println!("No crates to publish."); - return Ok(()); - } + if changed_crates.is_empty() { + println!("No crates to publish."); + return Ok(()); + } - update_cargo_lock(&mut changeset)?; + update_cargo_lock(&mut changeset)?; - changeset.commit_changes()?; + changeset.commit_changes()?; - for crate_name in &changed_crates { - run_cargo_publish(crate_name, dry_run)?; - } + for crate_name in &changed_crates { + run_cargo_publish(crate_name, self.dry_run)?; + } - changeset.revert_changes()?; + changeset.revert_changes()?; - Ok(()) + Ok(()) + } } fn prepare_for_publish(crate_name: &str, changeset: &mut TemporaryChangeset) -> Result { @@ -111,13 +122,13 @@ fn update_cargo_lock(changeset: &mut TemporaryChangeset) -> Result<()> { Ok(()) } -fn run_cargo_publish(crate_name: &str, dry_run: bool) -> Result<()> { +fn run_cargo_publish(crate_name: &str, dry_run: DryRun) -> Result<()> { let mut command = Command::new("cargo") .arg("publish") .property("--package", crate_name) .flag("--all-features"); - if dry_run { + if dry_run.get() { command = command.flag("--dry-run"); } diff --git a/crates/infra/cli/src/commands/publish/changesets/mod.rs b/crates/infra/cli/src/commands/publish/changesets/mod.rs index 4186f025aa..8208e5d25e 100644 --- a/crates/infra/cli/src/commands/publish/changesets/mod.rs +++ b/crates/infra/cli/src/commands/publish/changesets/mod.rs @@ -1,96 +1,104 @@ +//! This repository versions and releases all its artifacts together, generating the same changelog. +//! Unfortunately, changesets does not support combining changelogs from multiple packages into one. +//! +//! So, we let changesets bump the version of the single NPM package we ship, and generate its changelog. +//! Then our build process copies the new version and the single changelog to other packages and crates. +//! +//! Additionally, changesets can only bump versions of packages in the root workspace. +//! However, NAPI platform-specific packages cannot be added to the workspace, because other platforms will fail "npm install". +//! So we have to bump the versions over ourselves anyways. + use anyhow::Result; +use clap::Parser; use infra_utils::cargo::CargoWorkspace; use infra_utils::commands::Command; use infra_utils::paths::{FileWalker, PathExtensions}; use crate::toolchains::napi::{NapiConfig, NapiResolver}; -/// This repository versions and releases all its artifacts together, generating the same changelog. -/// Unfortunately, changesets does not support combining changelogs from multiple packages into one. -/// -/// So, we let changesets bump the version of the single NPM package we ship, and generate its changelog. -/// Then our build process copies the new version and the single changelog to other packages and crates. -/// -/// Additionally, changesets can only bump versions of packages in the root workspace. -/// However, NAPI platform-specific packages cannot be added to the workspace, because other platforms will fail "npm install". -/// So we have to bump the versions over ourselves anyways. -pub fn publish_changesets() -> Result<()> { - let resolver = NapiResolver::Solidity; - let package_dir = resolver.main_package_dir(); - - let package_version = NapiConfig::local_version(&package_dir)?; - println!("Package version: {package_version}"); - - let workspace_version = CargoWorkspace::local_version()?; - println!("Workspace version: {workspace_version}"); - - assert_eq!( - package_version, workspace_version, - "Package version does not match workspace version." - ); - - // This command will: - // 1) Consume/delete any changeset files currently in "$REPO_ROOT/.changeset" - // 2) Update the CHANGELOG.md file for the NPM package. - // 3) Bump the version in its package.json accordingly. - - Command::new("changeset").arg("version").run()?; - - let updated_version = NapiConfig::local_version(&package_dir)?; - println!("Updated version: {updated_version}"); - - if package_version == updated_version { - println!("No version changes. Skipping."); - return Ok(()); - } +#[derive(Clone, Debug, Parser)] +pub struct ChangesetsController {} + +impl ChangesetsController { + #[allow(clippy::unused_self)] // for compatibility with other controllers: + pub fn execute(&self) -> Result<()> { + let resolver = NapiResolver::Solidity; + let package_dir = resolver.main_package_dir(); + + let package_version = NapiConfig::local_version(&package_dir)?; + println!("Package version: {package_version}"); - // Format the updated package files: + let workspace_version = CargoWorkspace::local_version()?; + println!("Workspace version: {workspace_version}"); - let package_dir = resolver.main_package_dir(); - Command::new("prettier") - .property("--write", package_dir.unwrap_str()) - .run()?; + assert_eq!( + package_version, workspace_version, + "Package version does not match workspace version." + ); - // Update NPM lock file: + // This command will: + // 1) Consume/delete any changeset files currently in "$REPO_ROOT/.changeset" + // 2) Update the CHANGELOG.md file for the NPM package. + // 3) Bump the version in its package.json accordingly. - Command::new("npm") - .arg("install") - .flag("--package-lock-only") - .run()?; + Command::new("changeset").arg("version").run()?; + + let updated_version = NapiConfig::local_version(&package_dir)?; + println!("Updated version: {updated_version}"); + + if package_version == updated_version { + println!("No version changes. Skipping."); + return Ok(()); + } - // Update Cargo workspace: + // Format the updated package files: - println!("Updating Cargo workspace version."); - CargoWorkspace::update_version(&updated_version)?; + let package_dir = resolver.main_package_dir(); + Command::new("prettier") + .property("--write", package_dir.unwrap_str()) + .run()?; - // Update Cargo lock file: + // Update NPM lock file: - Command::new("cargo") - .arg("update") - .flag("--workspace") - .run()?; + Command::new("npm") + .arg("install") + .flag("--package-lock-only") + .run()?; - // Update other CHANGELOG files: + // Update Cargo workspace: - let source_changelog = package_dir.join("CHANGELOG.md"); + println!("Updating Cargo workspace version."); + CargoWorkspace::update_version(&updated_version)?; - for destination_changelog in FileWalker::from_repo_root().find(["**/CHANGELOG.md"])? { - if source_changelog != destination_changelog { - println!("Updating: {destination_changelog:?}"); - std::fs::copy(&source_changelog, &destination_changelog)?; + // Update Cargo lock file: + + Command::new("cargo") + .arg("update") + .flag("--workspace") + .run()?; + + // Update other CHANGELOG files: + + let source_changelog = package_dir.join("CHANGELOG.md"); + + for destination_changelog in FileWalker::from_repo_root().find(["**/CHANGELOG.md"])? { + if source_changelog != destination_changelog { + println!("Updating: {destination_changelog:?}"); + std::fs::copy(&source_changelog, &destination_changelog)?; + } } - } - Command::new("git") - .args(["stash", "push"]) - .flag("--include-untracked") - .property("--message", "applied changesets") - .run()?; + Command::new("git") + .args(["stash", "push"]) + .flag("--include-untracked") + .property("--message", "applied changesets") + .run()?; - println!(); - println!("Source files are now updated with the new version, and stored in a 'git stash'."); - println!("The calling CI workflow will now use this stash to create a PR if needed."); - println!(); + println!(); + println!("Source files are now updated with the new version, and stored in a 'git stash'."); + println!("The calling CI workflow will now use this stash to create a PR if needed."); + println!(); - Ok(()) + Ok(()) + } } diff --git a/crates/infra/cli/src/commands/publish/github_release/mod.rs b/crates/infra/cli/src/commands/publish/github_release/mod.rs index b10eefaac0..db6d2ab72e 100644 --- a/crates/infra/cli/src/commands/publish/github_release/mod.rs +++ b/crates/infra/cli/src/commands/publish/github_release/mod.rs @@ -1,6 +1,7 @@ use std::path::Path; use anyhow::Result; +use clap::Parser; use infra_utils::cargo::CargoWorkspace; use infra_utils::github::GitHub; use infra_utils::paths::PathExtensions; @@ -8,32 +9,41 @@ use itertools::Itertools; use markdown::{Block, Span}; use semver::Version; -pub fn publish_github_release(dry_run: bool) -> Result<()> { - let current_version = CargoWorkspace::local_version()?; - println!("Current version: {current_version}"); +use crate::utils::DryRun; - let previous_version = GitHub::latest_release_version()?; - println!("Latest published version: {previous_version}"); +#[derive(Clone, Debug, Parser)] +pub struct GithubReleaseController { + #[command(flatten)] + dry_run: DryRun, +} - if current_version == previous_version { - println!("Skipping release, since the workspace version is already published."); - return Ok(()); - } +impl GithubReleaseController { + pub fn execute(&self) -> Result<()> { + let current_version = CargoWorkspace::local_version()?; + println!("Current version: {current_version}"); - let notes = extract_latest_changelogs(¤t_version, &previous_version)?; - let tag_name = format!("v{current_version}"); + let previous_version = GitHub::latest_release_version()?; + println!("Latest published version: {previous_version}"); - println!("Creating release '{tag_name}' with contents:"); - println!(); - println!("{}", notes.lines().map(|l| format!(" │ {l}")).join("\n")); - println!(); + if current_version == previous_version { + println!("Skipping release, since the workspace version is already published."); + return Ok(()); + } - if dry_run { - println!("Skipping release, because of the dry run."); - return Ok(()); - } + let notes = extract_latest_changelogs(¤t_version, &previous_version)?; + let tag_name = format!("v{current_version}"); - GitHub::create_new_release(tag_name, notes) + println!("Creating release '{tag_name}' with contents:"); + println!(); + println!("{}", notes.lines().map(|l| format!(" │ {l}")).join("\n")); + println!(); + + if self.dry_run.get() { + return Ok(()); + } + + GitHub::create_new_release(tag_name, notes) + } } fn extract_latest_changelogs( diff --git a/crates/infra/cli/src/commands/publish/mkdocs/mod.rs b/crates/infra/cli/src/commands/publish/mkdocs/mod.rs index c17133f093..35a7f81c2d 100644 --- a/crates/infra/cli/src/commands/publish/mkdocs/mod.rs +++ b/crates/infra/cli/src/commands/publish/mkdocs/mod.rs @@ -1,7 +1,35 @@ use anyhow::Result; +use clap::{Parser, ValueEnum}; +use infra_utils::cargo::CargoWorkspace; use crate::toolchains::mkdocs::Mkdocs; +use crate::utils::DryRun; -pub fn publish_mkdocs(dry_run: bool) -> Result<()> { - Mkdocs::publish(dry_run) +#[derive(Clone, Debug, Parser)] +pub struct MkdocsController { + /// The target version to publish. + #[arg(long)] + target: PublishTarget, + + #[command(flatten)] + dry_run: DryRun, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, ValueEnum)] +enum PublishTarget { + MainBranch, + LatestRelease, +} + +impl MkdocsController { + pub fn execute(&self) -> Result<()> { + let (version, alias) = match self.target { + PublishTarget::MainBranch => ("main".to_string(), None), + PublishTarget::LatestRelease => { + (CargoWorkspace::local_version()?.to_string(), Some("latest")) + } + }; + + Mkdocs::publish(&version, alias, self.dry_run) + } } diff --git a/crates/infra/cli/src/commands/publish/mod.rs b/crates/infra/cli/src/commands/publish/mod.rs index e25ed31a18..5e46a3f630 100644 --- a/crates/infra/cli/src/commands/publish/mod.rs +++ b/crates/infra/cli/src/commands/publish/mod.rs @@ -5,59 +5,48 @@ mod mkdocs; mod npm; use anyhow::Result; -use clap::{Parser, ValueEnum}; -use infra_utils::github::GitHub; -use infra_utils::terminal::Terminal; +use clap::{Parser, Subcommand}; -use crate::commands::publish::cargo::publish_cargo; -use crate::commands::publish::changesets::publish_changesets; -use crate::commands::publish::github_release::publish_github_release; -use crate::commands::publish::mkdocs::publish_mkdocs; -use crate::commands::publish::npm::publish_npm; -use crate::utils::ClapExtensions; +use crate::commands::publish::cargo::CargoController; +use crate::commands::publish::changesets::ChangesetsController; +use crate::commands::publish::github_release::GithubReleaseController; +use crate::commands::publish::mkdocs::MkdocsController; +use crate::commands::publish::npm::NpmController; #[derive(Clone, Debug, Parser)] pub struct PublishController { + #[command(subcommand)] command: PublishCommand, - - #[arg(long)] - dry_run: bool, } -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, ValueEnum)] -enum PublishCommand { +#[derive(Clone, Debug, Subcommand)] +pub enum PublishCommand { /// Consume pending changesets, update changelogs and package versions, then send a PR. - Changesets, - // Publish the documentation to GitHub pages. - Mkdocs, + Changesets(ChangesetsController), + /// Publish the documentation to GitHub pages. + Mkdocs(MkdocsController), /// Publish source packages to [npmjs.com]. - Npm, + Npm(NpmController), /// Publish source crates to [crates.io]. - Cargo, + Cargo(CargoController), /// Publish a new release in the GitHub repository. - GithubRelease, + GithubRelease(GithubReleaseController), } impl PublishController { pub fn execute(&self) -> Result<()> { - Terminal::step(format!("publish {name}", name = self.command.clap_name())); - - let dry_run = if self.dry_run { - println!("Attempting a dry run, since it was requested on the command line."); - true - } else if !GitHub::is_running_in_ci() { - println!("Attempting a dry run, since we are not running in CI."); - true - } else { - false - }; + self.command.execute() + } +} - match self.command { - PublishCommand::Changesets => publish_changesets(), - PublishCommand::Mkdocs => publish_mkdocs(dry_run), - PublishCommand::Npm => publish_npm(dry_run), - PublishCommand::Cargo => publish_cargo(dry_run), - PublishCommand::GithubRelease => publish_github_release(dry_run), +impl PublishCommand { + pub fn execute(&self) -> Result<()> { + match self { + PublishCommand::Changesets(command) => command.execute(), + PublishCommand::Mkdocs(command) => command.execute(), + PublishCommand::Npm(command) => command.execute(), + PublishCommand::Cargo(command) => command.execute(), + PublishCommand::GithubRelease(command) => command.execute(), } } } diff --git a/crates/infra/cli/src/commands/publish/npm/mod.rs b/crates/infra/cli/src/commands/publish/npm/mod.rs index be7e90dce8..8ee151063b 100644 --- a/crates/infra/cli/src/commands/publish/npm/mod.rs +++ b/crates/infra/cli/src/commands/publish/npm/mod.rs @@ -1,41 +1,51 @@ use std::path::Path; use anyhow::Result; +use clap::Parser; use infra_utils::commands::Command; use infra_utils::paths::PathExtensions; use crate::toolchains::napi::{ NapiCompiler, NapiConfig, NapiPackageKind, NapiProfile, NapiResolver, }; +use crate::utils::DryRun; -pub fn publish_npm(dry_run: bool) -> Result<()> { - let resolver = NapiResolver::Solidity; +#[derive(Clone, Debug, Parser)] +pub struct NpmController { + #[command(flatten)] + dry_run: DryRun, +} - NapiCompiler::run(resolver, NapiProfile::Release)?; +impl NpmController { + pub fn execute(&self) -> Result<()> { + let resolver = NapiResolver::Solidity; - // Publish platform-specific packages first, as the main package now depends on their latest version: + NapiCompiler::run(resolver, NapiProfile::Release)?; - for platform_dir in resolver.platforms_dir().collect_children()? { - let platform = platform_dir.unwrap_name().to_owned(); - publish_package( - resolver, - &platform_dir, - &NapiPackageKind::Platform(platform), - dry_run, - )?; - } + // Publish platform-specific packages first, as the main package now depends on their latest version: - // Then publish the main package, that depends on the previously published platform-specific packages: + for platform_dir in resolver.platforms_dir().collect_children()? { + let platform = platform_dir.unwrap_name().to_owned(); + publish_package( + resolver, + &platform_dir, + &NapiPackageKind::Platform(platform), + self.dry_run, + )?; + } - let package_dir = resolver.main_package_dir(); - publish_package(resolver, &package_dir, &NapiPackageKind::Main, dry_run) + // Then publish the main package, that depends on the previously published platform-specific packages: + + let package_dir = resolver.main_package_dir(); + publish_package(resolver, &package_dir, &NapiPackageKind::Main, self.dry_run) + } } fn publish_package( resolver: NapiResolver, package_dir: &Path, kind: &NapiPackageKind, - dry_run: bool, + dry_run: DryRun, ) -> Result<()> { println!("Publishing: {package_dir:?}"); @@ -56,7 +66,7 @@ fn publish_package( .args(["publish", output_dir.unwrap_str()]) .property("--access", "public"); - if dry_run { + if dry_run.get() { command = command.flag("--dry-run"); } diff --git a/crates/infra/cli/src/toolchains/mkdocs/mod.rs b/crates/infra/cli/src/toolchains/mkdocs/mod.rs index f4e721e2b4..e5e2c62811 100644 --- a/crates/infra/cli/src/toolchains/mkdocs/mod.rs +++ b/crates/infra/cli/src/toolchains/mkdocs/mod.rs @@ -5,6 +5,7 @@ use infra_utils::commands::Command; use infra_utils::paths::PathExtensions; use crate::toolchains::pipenv::PipEnv; +use crate::utils::DryRun; pub struct Mkdocs; @@ -25,7 +26,7 @@ impl Mkdocs { .run() } - pub fn publish(dry_run: bool) -> Result<()> { + pub fn publish(version: &str, alias: Option<&str>, dry_run: DryRun) -> Result<()> { // Fetch the latest github pages from the remote repository: Command::new("git") .args(["fetch", "origin", "gh-pages"]) diff --git a/crates/infra/cli/src/utils.rs b/crates/infra/cli/src/utils.rs index 944c25a7bd..979e23def2 100644 --- a/crates/infra/cli/src/utils.rs +++ b/crates/infra/cli/src/utils.rs @@ -1,5 +1,6 @@ use anyhow::{Ok, Result}; -use clap::ValueEnum; +use clap::{Parser, ValueEnum}; +use infra_utils::github::GitHub; pub trait OrderedCommand: Clone + Ord + PartialEq + ValueEnum { fn execute(&self) -> Result<()>; @@ -37,3 +38,24 @@ impl ClapExtensions for T { .to_owned(); } } + +#[derive(Clone, Copy, Debug, Parser)] +pub struct DryRun { + /// Attempt a dry run, instead of actually executing the command. + #[arg(long)] + dry_run: bool, +} + +impl DryRun { + pub fn get(self) -> bool { + if self.dry_run { + println!("Attempting a dry run, since it was requested on the command line."); + true + } else if !GitHub::is_running_in_ci() { + println!("Attempting a dry run, since we are not running in CI."); + true + } else { + false + } + } +}