Skip to content

Commit

Permalink
feat(nervous-system): release runscript automatically creates changel…
Browse files Browse the repository at this point in the history
…og PRs (#3806)

This is the final step in the release process. Now everything is
automated!

Here is an example of a PR it creates: #3807

To create the PR, we use `gh`, which we assert you have on your computer
before the first step. (This ensures that you don't get to the last step
and then hit an error which would be slightly annoying to recover from.
Instead, you get the error right away, and it should be smooth sailing
after that.)

The intention of this change is that you create the PR to update the
changelog right when you create the proposals.

> Yes, we can optimistically assume that the proposal will pass. If it
doesn't, we can revert the CHANGELOG.md. Branch predict FTW.
- @daniel-wong-dfinity-org 

A consequence of is that the changelog now shows the date of proposal
submission, rather than execution.

[← Previous PR](#3796)
  • Loading branch information
anchpop authored Feb 6, 2025
1 parent f6b7233 commit cc2a047
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 28 deletions.
1 change: 1 addition & 0 deletions rs/nervous_system/tools/release-runscript/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ DEPENDENCIES = [
rust_binary(
name = "release-runscript",
srcs = [
"src/commit_switcher.rs",
"src/main.rs",
"src/utils.rs",
],
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
122 changes: 107 additions & 15 deletions rs/nervous_system/tools/release-runscript/src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<PathBuf>,
#[arg(long, num_args = 0..,)]
Expand All @@ -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<PathBuf>,
#[arg(long, num_args = 0..,)]
Expand All @@ -54,14 +60,23 @@ struct CreateForumPost {
}
#[derive(Debug, Parser)]
struct ScheduleVote {
#[arg(long)]
commit: String,
#[arg(long, num_args = 0..,)]
nns_proposal_ids: Vec<String>,
#[arg(long, num_args = 0..,)]
sns_proposal_ids: Vec<String>,
}

#[derive(Debug, Parser)]
struct UpdateChangelog;
struct UpdateChangelog {
#[arg(long)]
commit: String,
#[arg(long, num_args = 0..,)]
nns_proposal_ids: Vec<String>,
#[arg(long, num_args = 0..,)]
sns_proposal_ids: Vec<String>,
}

#[derive(Debug, Subcommand)]
enum Step {
Expand Down Expand Up @@ -102,6 +117,7 @@ fn main() -> Result<()> {
}
};

ensure_gh_setup()?;
print_header();

match args.step {
Expand Down Expand Up @@ -369,13 +385,15 @@ SNS proposal texts: {}",
)?;

run_submit_proposals(SubmitProposals {
commit,
nns_proposal_text_paths,
sns_proposal_text_paths,
})
}

fn run_submit_proposals(cmd: SubmitProposals) -> Result<()> {
let SubmitProposals {
commit,
nns_proposal_text_paths,
sns_proposal_text_paths,
} = cmd;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -597,13 +617,15 @@ 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,
})
}

fn run_schedule_vote(cmd: ScheduleVote) -> Result<()> {
let ScheduleVote {
commit,
nns_proposal_ids,
sns_proposal_ids,
} = cmd;
Expand Down Expand Up @@ -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.");
Expand Down
133 changes: 132 additions & 1 deletion rs/nervous_system/tools/release-runscript/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{bail, Result};
use colored::*;
use std::io::{self, Write};
use std::path::PathBuf;
Expand Down Expand Up @@ -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<url::Url> {
// push the current branch to the remote repository
// e.g. git push --set-upstream origin <branch-name>
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`")
}
}
Loading

0 comments on commit cc2a047

Please sign in to comment.