diff --git a/rs/nervous_system/tools/release-runscript/BUILD.bazel b/rs/nervous_system/tools/release-runscript/BUILD.bazel index d2196f08713..bfcc104d17e 100644 --- a/rs/nervous_system/tools/release-runscript/BUILD.bazel +++ b/rs/nervous_system/tools/release-runscript/BUILD.bazel @@ -28,6 +28,7 @@ DEPENDENCIES = [ rust_binary( name = "release-runscript", srcs = [ + "src/commit_switcher.rs", "src/main.rs", "src/utils.rs", ], diff --git a/rs/nervous_system/tools/release-runscript/src/commit_switcher.rs b/rs/nervous_system/tools/release-runscript/src/commit_switcher.rs index 03a0d8a788d..ba75aa38593 100644 --- a/rs/nervous_system/tools/release-runscript/src/commit_switcher.rs +++ b/rs/nervous_system/tools/release-runscript/src/commit_switcher.rs @@ -1,6 +1,7 @@ -use std::process::{Command, Stdio}; +use crate::utils::ic_dir; use anyhow::Result; use colored::*; +use std::process::Command; /// Helper struct to switch branches, then switch back when dropped. pub(crate) struct CommitSwitcher { @@ -29,7 +30,7 @@ impl CommitSwitcher { // stash if we have changes if has_changes { - println!("{}", format!("Stashing changes...").bright_blue()); + println!("{}", "Stashing changes...".bright_blue()); let stash = Command::new("git").current_dir(&ic).arg("stash").output()?; if !stash.status.success() { return Err( diff --git a/rs/nervous_system/tools/release-runscript/src/main.rs b/rs/nervous_system/tools/release-runscript/src/main.rs index 7f51349b752..9b6fad4e1c3 100644 --- a/rs/nervous_system/tools/release-runscript/src/main.rs +++ b/rs/nervous_system/tools/release-runscript/src/main.rs @@ -1,7 +1,9 @@ +mod commit_switcher; mod utils; use anyhow::{bail, Result}; use clap::{Parser, Subcommand}; use colored::*; +use commit_switcher::CommitSwitcher; use std::path::PathBuf; use std::process::{Command, Stdio}; use url::Url; @@ -35,6 +37,8 @@ struct CreateProposalTexts { #[derive(Debug, Parser)] struct SubmitProposals { + #[arg(long)] + commit: String, #[arg(long, num_args = 0..,)] nns_proposal_text_paths: Vec, #[arg(long, num_args = 0..,)] @@ -43,6 +47,8 @@ struct SubmitProposals { #[derive(Debug, Parser)] struct CreateForumPost { + #[arg(long)] + commit: String, #[arg(long, num_args = 0..,)] nns_proposal_text_paths: Vec, #[arg(long, num_args = 0..,)] @@ -54,6 +60,8 @@ struct CreateForumPost { } #[derive(Debug, Parser)] struct ScheduleVote { + #[arg(long)] + commit: String, #[arg(long, num_args = 0..,)] nns_proposal_ids: Vec, #[arg(long, num_args = 0..,)] @@ -61,7 +69,14 @@ struct ScheduleVote { } #[derive(Debug, Parser)] -struct UpdateChangelog; +struct UpdateChangelog { + #[arg(long)] + commit: String, + #[arg(long, num_args = 0..,)] + nns_proposal_ids: Vec, + #[arg(long, num_args = 0..,)] + sns_proposal_ids: Vec, +} #[derive(Debug, Subcommand)] enum Step { @@ -102,6 +117,7 @@ fn main() -> Result<()> { } }; + ensure_gh_setup()?; print_header(); match args.step { @@ -369,6 +385,7 @@ SNS proposal texts: {}", )?; run_submit_proposals(SubmitProposals { + commit, nns_proposal_text_paths, sns_proposal_text_paths, }) @@ -376,6 +393,7 @@ SNS proposal texts: {}", fn run_submit_proposals(cmd: SubmitProposals) -> Result<()> { let SubmitProposals { + commit, nns_proposal_text_paths, sns_proposal_text_paths, } = cmd; @@ -480,6 +498,7 @@ fn run_submit_proposals(cmd: SubmitProposals) -> Result<()> { )?; run_create_forum_post(CreateForumPost { + commit, nns_proposal_text_paths, nns_proposal_ids, sns_proposal_text_paths, @@ -489,6 +508,7 @@ fn run_submit_proposals(cmd: SubmitProposals) -> Result<()> { fn run_create_forum_post(cmd: CreateForumPost) -> Result<()> { let CreateForumPost { + commit, nns_proposal_text_paths, nns_proposal_ids, sns_proposal_text_paths, @@ -597,6 +617,7 @@ fn run_create_forum_post(cmd: CreateForumPost) -> Result<()> { // Continue to the next automated step. run_schedule_vote(ScheduleVote { + commit, nns_proposal_ids, sns_proposal_ids, }) @@ -604,6 +625,7 @@ fn run_create_forum_post(cmd: CreateForumPost) -> Result<()> { fn run_schedule_vote(cmd: ScheduleVote) -> Result<()> { let ScheduleVote { + commit, nns_proposal_ids, sns_proposal_ids, } = cmd; @@ -667,28 +689,98 @@ Calendar Event Setup: - If people don't respond, ping @trusted-neurons in #eng-release channel", )?; - run_update_changelog(UpdateChangelog) + run_update_changelog(UpdateChangelog { + commit, + nns_proposal_ids, + sns_proposal_ids, + }) } -fn run_update_changelog(_: UpdateChangelog) -> Result<()> { +fn run_update_changelog(cmd: UpdateChangelog) -> Result<()> { + let UpdateChangelog { + commit, + nns_proposal_ids, + sns_proposal_ids, + } = cmd; + + let ic = ic_dir(); + + use std::fmt::Write; print_step( 8, "Update Changelog", - "Update CHANGELOG.md file(s) for each proposal: + &format!( + "Now I'm going to update the changelog for the released canisters. This applies to the following proposals: +NNS: {} +SNS: {}", + nns_proposal_ids.iter().fold(String::new(), |mut acc, id| { + let _ = write!(acc, "\n - {}", id); + acc + }), + sns_proposal_ids.iter().fold(String::new(), |mut acc, id| { + let _ = write!(acc, "\n - {}", id); + acc + }), + ), + )?; -1. For each proposal ID: - ```bash - PROPOSAL_IDS=... + { + // switch to the commit being released + let _commit_switcher = CommitSwitcher::switch(commit)?; + + // update the changelog for each proposal + for proposal_id in nns_proposal_ids.iter().chain(sns_proposal_ids.iter()) { + println!("Updating changelog for proposal {}", proposal_id); + let script = ic.join("testnet/tools/nns-tools/add-release-to-changelog.sh"); + let output = Command::new(script) + .arg(proposal_id) + .current_dir(&ic) + .output()?; - for PROPOSAL_ID in $PROPOSAL_IDS do - ./testnet/tools/nns-tools/add-release-to-changelog.sh \\ - $PROPOSAL_ID - done - ``` + if !output.status.success() { + println!("{}", String::from_utf8_lossy(&output.stderr)); + println!("Failed to update changelog for proposal {}", proposal_id); + } + } -2. Best Practice: - - Combine this change with mainnet-canisters.json update in the same PR", - )?; + println!("Changelogs updated. Now I'm going to create a branch, commit, push it, then create a PR using `gh`."); + press_enter_to_continue()?; + + // Create branch with today's date + let today = chrono::Local::now().format("%Y-%m-%d").to_string(); + let branch_name = format!("changelog-update-{}", today); + commit_all_into_branch(&branch_name)?; + + // Create PR + let title = format!( + "chore(nervous-system): Update changelog for release {}", + today + ); + let body = &format!( + "Update CHANGELOG.md for today's release. + +## NNS {} + +## SNS {}", + nns_proposal_ids.iter().fold(String::new(), |mut acc, id| { + let _ = write!( + acc, + "\n - [{id}](https://dashboard.internetcomputer.org/proposal/{id})", + ); + acc + }), + sns_proposal_ids.iter().fold(String::new(), |mut acc, id| { + let _ = write!( + acc, + "\n - [{id}](https://dashboard.internetcomputer.org/proposal/{id})", + ); + acc + }), + ); + let pr_url = create_pr(&title, body)?; + open_webpage(&pr_url)?; + println!("PR created. Please share it with the team. It can be merged before the proposals are executed."); + } println!("{}", "\nRelease process complete!".bright_green().bold()); println!("Please verify that all steps were completed successfully."); diff --git a/rs/nervous_system/tools/release-runscript/src/utils.rs b/rs/nervous_system/tools/release-runscript/src/utils.rs index ac65b831d8e..17df3a8cda6 100644 --- a/rs/nervous_system/tools/release-runscript/src/utils.rs +++ b/rs/nervous_system/tools/release-runscript/src/utils.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{bail, Result}; use colored::*; use std::io::{self, Write}; use std::path::PathBuf; @@ -92,3 +92,134 @@ pub(crate) fn press_enter_to_continue() -> Result<()> { input(&format!("\n{}", "Press Enter to continue...".bright_blue()))?; Ok(()) } + +pub(crate) fn ensure_gh_setup() -> Result<()> { + let output = Command::new("gh").arg("--version").output()?; + if !output.status.success() { + bail!("gh is not installed. Try installing with `brew install gh`") + } + let output = Command::new("gh").arg("auth").arg("status").output()?; + if !output.status.success() { + bail!("gh is not authenticated. Try running `gh auth login`") + } + let stderr = String::from_utf8(output.stderr)?; + if !stderr.contains("Logged in to github.com") { + bail!("gh is not logged in. Try running `gh auth login`") + } + + println!("{}", "GitHub CLI is configured ✓".bright_green()); + + Ok(()) +} + +pub(crate) fn commit_all_into_branch(branch: &str) -> Result<()> { + let ic = ic_dir(); + + { + // Check if branch exists + let output = Command::new("git") + .current_dir(&ic) + .args(["branch", "--list", branch]) + .output()?; + + let branch_exists = !String::from_utf8_lossy(&output.stdout).trim().is_empty(); + if branch_exists { + if input_yes_or_no( + &format!("Branch '{}' already exists. Delete it?", branch), + false, + )? { + // Delete the branch + let output = Command::new("git") + .current_dir(&ic) + .args(["branch", "-D", branch]) + .output()?; + if !output.status.success() { + bail!( + "Failed to delete branch: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + println!( + "{}", + format!("Deleted existing branch '{}'", branch).bright_blue() + ); + } else { + bail!("Cannot continue with existing branch"); + } + } + } + + let output = Command::new("git") + .current_dir(&ic) + .args(["checkout", "-b", branch]) + .output()?; + if !output.status.success() { + return Err( + anyhow::anyhow!("{}", String::from_utf8_lossy(&output.stderr)) + .context("Failed to create branch"), + ); + } + + let output = Command::new("git") + .current_dir(&ic) + .args(["add", "."]) + .output()?; + if !output.status.success() { + return Err( + anyhow::anyhow!("{}", String::from_utf8_lossy(&output.stderr)) + .context("Failed to add all files to branch"), + ); + } + + let output = Command::new("git") + .current_dir(&ic) + .args(["commit", "-m", "chore(nervous-system): update changelog"]) + .output()?; + if !output.status.success() { + return Err( + anyhow::anyhow!("{}", String::from_utf8_lossy(&output.stderr)) + .context("Failed to commit all files to branch"), + ); + } + + Ok(()) +} + +pub(crate) fn create_pr(title: &str, body: &str) -> Result { + // push the current branch to the remote repository + // e.g. git push --set-upstream origin + let branch = Command::new("git") + .arg("branch") + .arg("--show-current") + .output()?; + let branch = String::from_utf8(branch.stdout)?; + let output = Command::new("git") + .arg("push") + .arg("--set-upstream") + .arg("origin") + .arg("--force") + .arg(branch.trim()) + .output()?; + if !output.status.success() { + return Err( + anyhow::anyhow!("{}", String::from_utf8_lossy(&output.stderr)) + .context("Failed to push branch to remote"), + ); + } + + let output = Command::new("gh") + .arg("pr") + .arg("create") + .arg("--title") + .arg(title) + .arg("--body") + .arg(body) + .output()?; + if output.status.success() { + println!("{}", "PR created successfully!".bright_green()); + let pr_url = std::str::from_utf8(&output.stdout)?; + Ok(Url::parse(pr_url)?) + } else { + bail!("Failed to create PR. Try running `gh auth login`") + } +} diff --git a/testnet/tools/nns-tools/add-release-to-changelog.sh b/testnet/tools/nns-tools/add-release-to-changelog.sh index 7e5dcb80294..e1300d3a6e8 100755 --- a/testnet/tools/nns-tools/add-release-to-changelog.sh +++ b/testnet/tools/nns-tools/add-release-to-changelog.sh @@ -7,7 +7,7 @@ source "$NNS_TOOLS_DIR/lib/include.sh" help() { print_green " Usage: $0 - PROPOSAL_ID: The ID of a recently executed Governance backend canister upgrade proposal. + PROPOSAL_ID: The ID of a recently proposed Governance backend canister upgrade proposal. Moves the pending new changelog entry from unreleased_changelog.md to CHANGELOG.md. @@ -44,10 +44,10 @@ if [[ "${LEN}" -ne 1 ]]; then fi PROPOSAL_INFO=$(echo "${PROPOSAL_INFO}" | jq '.[0]') -# Assert was executed. -EXECUTED_TIMESTAMP_SECONDS=$(echo "${PROPOSAL_INFO}" | jq '.executed_timestamp_seconds | tonumber') -if [[ "${EXECUTED_TIMESTAMP_SECONDS}" -eq 0 ]]; then - print_red "💀 Proposal ${PROPOSAL_ID} exists, but was not successfully executed." >&2 +# Get proposal creation timestam. +PROPOSED_TIMESTAMP_SECONDS=$(echo "${PROPOSAL_INFO}" | jq '.proposal_creation_timestamp_seconds | tonumber') +if [[ "${PROPOSED_TIMESTAMP_SECONDS}" -eq 0 ]]; then + print_red "💀 Proposal ${PROPOSAL_ID} exists, but had no proposal_creation_timestamp_seconds." >&2 exit 1 fi @@ -74,13 +74,13 @@ else print_red "(In particular, unable to determine which canister and commit.)" >&2 exit 1 fi -SECONDS_AGO=$(($(date +%s) - "${EXECUTED_TIMESTAMP_SECONDS}")) -EXECUTED_ON=$( +SECONDS_AGO=$(($(date +%s) - "${PROPOSED_TIMESTAMP_SECONDS}")) +PROPOSED_ON=$( date --utc \ - --date=@"${EXECUTED_TIMESTAMP_SECONDS}" \ + --date=@"${PROPOSED_TIMESTAMP_SECONDS}" \ --iso-8601 ) -print_cyan "🏃 ${GOVERNANCE_TYPE} ${CANISTER_NAME} proposal was executed ${SECONDS_AGO} seconds ago." >&2 +print_cyan "🏃 ${GOVERNANCE_TYPE} ${CANISTER_NAME} proposal was submitted ${SECONDS_AGO} seconds ago." >&2 # Fail if the proposal's commit is not checked out. if [[ $(git rev-parse HEAD) != $DESTINATION_COMMIT_ID* ]]; then @@ -117,7 +117,7 @@ if [[ -z "${NEW_FEATURES_AND_FIXES}" ]]; then print_red "💀 ${GOVERNANCE_TYPE} ${CANISTER_NAME}'s unreleased_changelog.md is EMPTY." >&2 exit 1 fi -NEW_ENTRY="# ${EXECUTED_ON}: Proposal ${PROPOSAL_ID} +NEW_ENTRY="# ${PROPOSED_ON}: Proposal ${PROPOSAL_ID} http://dashboard.internetcomputer.org/proposals/${PROPOSAL_ID}