Merge pull request #2867 from ehuss/xtask-changelog

Add a script to help update the changelog
This commit is contained in:
Eric Huss
2025-09-28 16:19:00 +00:00
committed by GitHub
3 changed files with 126 additions and 2 deletions

View File

@@ -224,7 +224,8 @@ Instructions for mdBook maintainers to publish a new release:
1. Create a PR to update the version and update the CHANGELOG:
1. Update the version in `Cargo.toml`
2. Run `cargo xtask test-all` to verify that everything is passing, and to update `Cargo.lock`.
3. Update `CHANGELOG.md` with any changes that users may be interested in.
3. Run `cargo xtask changelog` to add a new entry to the changelog.
1. This will add a list of all changes at the top. You will need to move those into the appropriate categories. Most changes that are generally not relevant to a user should be removed. Rewrite the descriptions so that a user can reasonably figure out what it means.
4. Commit the changes, and open a PR.
2. After the PR has been merged, create a release in GitHub. This can either be done in the GitHub web UI, or on the command-line:
```bash

View File

@@ -0,0 +1,120 @@
//! Helper to generate a changelog for a new release.
use super::Result;
use std::fs;
use std::process::Command;
use std::process::exit;
const CHANGELOG_PATH: &str = "CHANGELOG.md";
pub(crate) fn changelog() -> Result<()> {
let previous = get_previous()?;
let current = get_current()?;
if current == previous {
eprintln!(
"error: Current version is `{current}` which is the same as the \
previous version in the changelog. Run `cargo set-version --bump <BUMP> first."
);
exit(1);
}
let prs = get_prs(&previous)?;
update_changelog(&previous, &current, &prs)?;
Ok(())
}
fn get_previous() -> Result<String> {
let contents = fs::read_to_string(CHANGELOG_PATH)?;
let version = contents
.lines()
.filter_map(|line| line.strip_prefix("## mdBook "))
.next()
.expect("at least one entry")
.to_owned();
Ok(version)
}
fn get_current() -> Result<String> {
let contents = fs::read_to_string("Cargo.toml")?;
let mut lines = contents
.lines()
.filter_map(|line| line.strip_prefix("version = "))
.map(|version| &version[1..version.len() - 1]);
let version = lines.next().expect("version should exist").to_owned();
assert_eq!(lines.next(), None);
Ok(version)
}
fn get_prs(previous: &str) -> Result<Vec<(String, String)>> {
println!("running `git fetch upstream`");
let status = Command::new("git").args(["fetch", "upstream"]).status()?;
if !status.success() {
eprintln!("error: git fetch failed");
exit(1);
}
println!("running `git log`");
const SEPARATOR: &str = "---COMMIT_SEPARATOR---";
let output = Command::new("git")
.args([
"log",
"--first-parent",
&format!("--pretty=format:%B%n{SEPARATOR}"),
"upstream/master",
&format!("v{previous}...upstream/HEAD"),
])
.output()?;
if !output.status.success() {
eprintln!("error: git log failed");
exit(1);
}
let stdout = std::str::from_utf8(&output.stdout).unwrap();
let prs = stdout
.split(&format!("{SEPARATOR}\n"))
.filter_map(|entry| {
let mut lines = entry.lines();
let first = match lines.next().unwrap().strip_prefix("Merge pull request #") {
Some(f) => f,
None => {
println!("warning: merge line not found in {entry}");
return None;
}
};
let number = first.split_whitespace().next().unwrap();
assert_eq!(lines.next(), Some(""));
let title = lines.next().expect("title is set");
assert_eq!(lines.next(), Some(""));
Some((number.to_string(), title.to_string()))
})
.collect();
Ok(prs)
}
fn update_changelog(previous: &str, current: &str, prs: &[(String, String)]) -> Result<()> {
let prs: String = prs
.iter()
.map(|(number, title)| {
format!(
"- {title}\n \
[#{number}](https://github.com/rust-lang/mdBook/pull/{number})\n"
)
})
.collect();
let new = format!(
"## mdBook {current}\n\
[v{previous}...v{current}](https://github.com/rust-lang/mdBook/compare/v{previous}...v{current})\n\
\n\
{prs}\
\n\
### Added\n\
\n\
### Changed\n\
\n\
### Fixed\n\
\n"
);
let mut contents = fs::read_to_string(CHANGELOG_PATH)?;
let insertion_point = contents.find("## ").unwrap();
contents.insert_str(insertion_point, &new);
fs::write(CHANGELOG_PATH, contents)?;
Ok(())
}

View File

@@ -5,11 +5,13 @@ use std::error::Error;
use std::process::Command;
use std::process::exit;
mod changelog;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
macro_rules! commands {
($($name:literal => $func:ident),* $(,)?) => {
($($name:literal => $func:expr),* $(,)?) => {
[$(($name, $func as fn() -> Result<()>)),*]
};
}
@@ -23,6 +25,7 @@ fn main() -> Result<()> {
"semver-checks" => semver_checks,
"eslint" => eslint,
"gui" => gui,
"changelog" => changelog::changelog,
}
.into_iter()
.collect();