diff --git a/Cargo.toml b/Cargo.toml
index ee9c57ab8350..79b9af7b0bf3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,8 @@
 [package]
 name = "clippy"
+# begin autogenerated version
 version = "0.1.84"
+# end autogenerated version
 description = "A bunch of helpful lints to avoid common pitfalls in Rust"
 repository = "https://github.com/rust-lang/rust-clippy"
 readme = "README.md"
diff --git a/clippy_config/Cargo.toml b/clippy_config/Cargo.toml
index 0cd0cabc3a6e..d1158b48e921 100644
--- a/clippy_config/Cargo.toml
+++ b/clippy_config/Cargo.toml
@@ -1,6 +1,8 @@
 [package]
 name = "clippy_config"
+# begin autogenerated version
 version = "0.1.84"
+# end autogenerated version
 edition = "2021"
 publish = false
 
diff --git a/clippy_dev/Cargo.toml b/clippy_dev/Cargo.toml
index 952a8711fb4e..d3a103eaf4c6 100644
--- a/clippy_dev/Cargo.toml
+++ b/clippy_dev/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
 
 [dependencies]
 aho-corasick = "1.0"
+chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
 clap = { version = "4.4", features = ["derive"] }
 indoc = "1.0"
 itertools = "0.12"
diff --git a/clippy_dev/src/dogfood.rs b/clippy_dev/src/dogfood.rs
index a0d57f5ab483..75a4cbd2f92e 100644
--- a/clippy_dev/src/dogfood.rs
+++ b/clippy_dev/src/dogfood.rs
@@ -1,4 +1,4 @@
-use crate::{clippy_project_root, exit_if_err};
+use crate::utils::{clippy_project_root, exit_if_err};
 use std::process::Command;
 
 /// # Panics
diff --git a/clippy_dev/src/fmt.rs b/clippy_dev/src/fmt.rs
index 8c61c35533cf..c66738592820 100644
--- a/clippy_dev/src/fmt.rs
+++ b/clippy_dev/src/fmt.rs
@@ -1,4 +1,4 @@
-use crate::clippy_project_root;
+use crate::utils::clippy_project_root;
 use itertools::Itertools;
 use rustc_lexer::{TokenKind, tokenize};
 use shell_escape::escape;
diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs
index ad385d5fbd29..9280369c23b8 100644
--- a/clippy_dev/src/lib.rs
+++ b/clippy_dev/src/lib.rs
@@ -14,69 +14,13 @@
 extern crate rustc_driver;
 extern crate rustc_lexer;
 
-use std::io;
-use std::path::PathBuf;
-use std::process::{self, ExitStatus};
-
 pub mod dogfood;
 pub mod fmt;
 pub mod lint;
 pub mod new_lint;
+pub mod release;
 pub mod serve;
 pub mod setup;
+pub mod sync;
 pub mod update_lints;
-
-#[cfg(not(windows))]
-static CARGO_CLIPPY_EXE: &str = "cargo-clippy";
-#[cfg(windows)]
-static CARGO_CLIPPY_EXE: &str = "cargo-clippy.exe";
-
-/// Returns the path to the `cargo-clippy` binary
-///
-/// # Panics
-///
-/// Panics if the path of current executable could not be retrieved.
-#[must_use]
-pub fn cargo_clippy_path() -> PathBuf {
-    let mut path = std::env::current_exe().expect("failed to get current executable name");
-    path.set_file_name(CARGO_CLIPPY_EXE);
-    path
-}
-
-/// Returns the path to the Clippy project directory
-///
-/// # Panics
-///
-/// Panics if the current directory could not be retrieved, there was an error reading any of the
-/// Cargo.toml files or ancestor directory is the clippy root directory
-#[must_use]
-pub fn clippy_project_root() -> PathBuf {
-    let current_dir = std::env::current_dir().unwrap();
-    for path in current_dir.ancestors() {
-        let result = std::fs::read_to_string(path.join("Cargo.toml"));
-        if let Err(err) = &result {
-            if err.kind() == io::ErrorKind::NotFound {
-                continue;
-            }
-        }
-
-        let content = result.unwrap();
-        if content.contains("[package]\nname = \"clippy\"") {
-            return path.to_path_buf();
-        }
-    }
-    panic!("error: Can't determine root of project. Please run inside a Clippy working dir.");
-}
-
-/// # Panics
-/// Panics if given command result was failed.
-pub fn exit_if_err(status: io::Result<ExitStatus>) {
-    match status.expect("failed to run command").code() {
-        Some(0) => {},
-        Some(n) => process::exit(n),
-        None => {
-            eprintln!("Killed by signal");
-            process::exit(1);
-        },
-    }
-}
+pub mod utils;
diff --git a/clippy_dev/src/lint.rs b/clippy_dev/src/lint.rs
index f308f5dfdfd8..125195397e6c 100644
--- a/clippy_dev/src/lint.rs
+++ b/clippy_dev/src/lint.rs
@@ -1,4 +1,4 @@
-use crate::{cargo_clippy_path, exit_if_err};
+use crate::utils::{cargo_clippy_path, exit_if_err};
 use std::process::{self, Command};
 use std::{env, fs};
 
diff --git a/clippy_dev/src/main.rs b/clippy_dev/src/main.rs
index fc15913354cd..56ed60256f16 100644
--- a/clippy_dev/src/main.rs
+++ b/clippy_dev/src/main.rs
@@ -3,7 +3,7 @@
 #![warn(rust_2018_idioms, unused_lifetimes)]
 
 use clap::{Args, Parser, Subcommand};
-use clippy_dev::{dogfood, fmt, lint, new_lint, serve, setup, update_lints};
+use clippy_dev::{dogfood, fmt, lint, new_lint, release, serve, setup, sync, update_lints, utils};
 use std::convert::Infallible;
 
 fn main() {
@@ -23,9 +23,9 @@ fn main() {
             if print_only {
                 update_lints::print_lints();
             } else if check {
-                update_lints::update(update_lints::UpdateMode::Check);
+                update_lints::update(utils::UpdateMode::Check);
             } else {
-                update_lints::update(update_lints::UpdateMode::Change);
+                update_lints::update(utils::UpdateMode::Change);
             }
         },
         DevCommand::NewLint {
@@ -35,7 +35,7 @@ fn main() {
             r#type,
             msrv,
         } => match new_lint::create(&pass, &name, &category, r#type.as_deref(), msrv) {
-            Ok(()) => update_lints::update(update_lints::UpdateMode::Change),
+            Ok(()) => update_lints::update(utils::UpdateMode::Change),
             Err(e) => eprintln!("Unable to create lint: {e}"),
         },
         DevCommand::Setup(SetupCommand { subcommand }) => match subcommand {
@@ -75,6 +75,12 @@ fn main() {
             uplift,
         } => update_lints::rename(&old_name, new_name.as_ref().unwrap_or(&old_name), uplift),
         DevCommand::Deprecate { name, reason } => update_lints::deprecate(&name, &reason),
+        DevCommand::Sync(SyncCommand { subcommand }) => match subcommand {
+            SyncSubcommand::UpdateNightly => sync::update_nightly(),
+        },
+        DevCommand::Release(ReleaseCommand { subcommand }) => match subcommand {
+            ReleaseSubcommand::BumpVersion => release::bump_version(),
+        },
     }
 }
 
@@ -225,6 +231,10 @@ enum DevCommand {
         /// The reason for deprecation
         reason: String,
     },
+    /// Sync between the rust repo and the Clippy repo
+    Sync(SyncCommand),
+    /// Manage Clippy releases
+    Release(ReleaseCommand),
 }
 
 #[derive(Args)]
@@ -291,3 +301,29 @@ enum RemoveSubcommand {
     /// Remove the tasks added with 'cargo dev setup vscode-tasks'
     VscodeTasks,
 }
+
+#[derive(Args)]
+struct SyncCommand {
+    #[command(subcommand)]
+    subcommand: SyncSubcommand,
+}
+
+#[derive(Subcommand)]
+enum SyncSubcommand {
+    #[command(name = "update_nightly")]
+    /// Update nightly version in rust-toolchain and `clippy_utils`
+    UpdateNightly,
+}
+
+#[derive(Args)]
+struct ReleaseCommand {
+    #[command(subcommand)]
+    subcommand: ReleaseSubcommand,
+}
+
+#[derive(Subcommand)]
+enum ReleaseSubcommand {
+    #[command(name = "bump_version")]
+    /// Bump the version in the Cargo.toml files
+    BumpVersion,
+}
diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs
index ee626d60b864..35dd986ff614 100644
--- a/clippy_dev/src/new_lint.rs
+++ b/clippy_dev/src/new_lint.rs
@@ -1,4 +1,4 @@
-use crate::clippy_project_root;
+use crate::utils::{clippy_project_root, clippy_version};
 use indoc::{formatdoc, writedoc};
 use std::fmt;
 use std::fmt::Write as _;
@@ -186,23 +186,8 @@ fn to_camel_case(name: &str) -> String {
 }
 
 pub(crate) fn get_stabilization_version() -> String {
-    fn parse_manifest(contents: &str) -> Option<String> {
-        let version = contents
-            .lines()
-            .filter_map(|l| l.split_once('='))
-            .find_map(|(k, v)| (k.trim() == "version").then(|| v.trim()))?;
-        let Some(("0", version)) = version.get(1..version.len() - 1)?.split_once('.') else {
-            return None;
-        };
-        let (minor, patch) = version.split_once('.')?;
-        Some(format!(
-            "{}.{}.0",
-            minor.parse::<u32>().ok()?,
-            patch.parse::<u32>().ok()?
-        ))
-    }
-    let contents = fs::read_to_string("Cargo.toml").expect("Unable to read `Cargo.toml`");
-    parse_manifest(&contents).expect("Unable to find package version in `Cargo.toml`")
+    let (minor, patch) = clippy_version();
+    format!("{minor}.{patch}.0")
 }
 
 fn get_test_file_contents(lint_name: &str, msrv: bool) -> String {
diff --git a/clippy_dev/src/release.rs b/clippy_dev/src/release.rs
new file mode 100644
index 000000000000..ac7551687010
--- /dev/null
+++ b/clippy_dev/src/release.rs
@@ -0,0 +1,27 @@
+use std::fmt::Write;
+use std::path::Path;
+
+use crate::utils::{UpdateMode, clippy_version, replace_region_in_file};
+
+const CARGO_TOML_FILES: [&str; 4] = [
+    "clippy_config/Cargo.toml",
+    "clippy_lints/Cargo.toml",
+    "clippy_utils/Cargo.toml",
+    "Cargo.toml",
+];
+
+pub fn bump_version() {
+    let (minor, mut patch) = clippy_version();
+    patch += 1;
+    for file in &CARGO_TOML_FILES {
+        replace_region_in_file(
+            UpdateMode::Change,
+            Path::new(file),
+            "# begin autogenerated version\n",
+            "# end autogenerated version",
+            |res| {
+                writeln!(res, "version = \"0.{minor}.{patch}\"").unwrap();
+            },
+        );
+    }
+}
diff --git a/clippy_dev/src/sync.rs b/clippy_dev/src/sync.rs
new file mode 100644
index 000000000000..3522d182e90a
--- /dev/null
+++ b/clippy_dev/src/sync.rs
@@ -0,0 +1,33 @@
+use std::fmt::Write;
+use std::path::Path;
+
+use chrono::offset::Utc;
+
+use crate::utils::{UpdateMode, replace_region_in_file};
+
+pub fn update_nightly() {
+    // Update rust-toolchain nightly version
+    let date = Utc::now().format("%Y-%m-%d").to_string();
+    replace_region_in_file(
+        UpdateMode::Change,
+        Path::new("rust-toolchain"),
+        "# begin autogenerated nightly\n",
+        "# end autogenerated nightly",
+        |res| {
+            writeln!(res, "channel = \"nightly-{date}\"").unwrap();
+        },
+    );
+
+    // Update clippy_utils nightly version
+    replace_region_in_file(
+        UpdateMode::Change,
+        Path::new("clippy_utils/README.md"),
+        "<!-- begin autogenerated nightly -->\n",
+        "<!-- end autogenerated nightly -->",
+        |res| {
+            writeln!(res, "```").unwrap();
+            writeln!(res, "nightly-{date}").unwrap();
+            writeln!(res, "```").unwrap();
+        },
+    );
+}
diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs
index 795456ad3c56..612d1c0ae139 100644
--- a/clippy_dev/src/update_lints.rs
+++ b/clippy_dev/src/update_lints.rs
@@ -1,4 +1,4 @@
-use crate::clippy_project_root;
+use crate::utils::{UpdateMode, clippy_project_root, exit_with_failure, replace_region_in_file};
 use aho_corasick::AhoCorasickBuilder;
 use itertools::Itertools;
 use rustc_lexer::{LiteralKind, TokenKind, tokenize, unescape};
@@ -17,12 +17,6 @@ const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev u
 
 const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
 
-#[derive(Clone, Copy, PartialEq, Eq)]
-pub enum UpdateMode {
-    Check,
-    Change,
-}
-
 /// Runs the `update_lints` command.
 ///
 /// This updates various generated values from the lint source code.
@@ -511,14 +505,6 @@ fn process_file(path: impl AsRef<Path>, update_mode: UpdateMode, content: &str)
     }
 }
 
-fn exit_with_failure() {
-    println!(
-        "Not all lints defined properly. \
-                 Please run `cargo dev update_lints` to make sure all lints are defined properly."
-    );
-    std::process::exit(1);
-}
-
 /// Lint data parsed from the Clippy source code.
 #[derive(Clone, PartialEq, Eq, Debug)]
 struct Lint {
@@ -851,61 +837,6 @@ fn remove_line_splices(s: &str) -> String {
     });
     res
 }
-
-/// Replaces a region in a file delimited by two lines matching regexes.
-///
-/// `path` is the relative path to the file on which you want to perform the replacement.
-///
-/// See `replace_region_in_text` for documentation of the other options.
-///
-/// # Panics
-///
-/// Panics if the path could not read or then written
-fn replace_region_in_file(
-    update_mode: UpdateMode,
-    path: &Path,
-    start: &str,
-    end: &str,
-    write_replacement: impl FnMut(&mut String),
-) {
-    let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {e}", path.display()));
-    let new_contents = match replace_region_in_text(&contents, start, end, write_replacement) {
-        Ok(x) => x,
-        Err(delim) => panic!("Couldn't find `{delim}` in file `{}`", path.display()),
-    };
-
-    match update_mode {
-        UpdateMode::Check if contents != new_contents => exit_with_failure(),
-        UpdateMode::Check => (),
-        UpdateMode::Change => {
-            if let Err(e) = fs::write(path, new_contents.as_bytes()) {
-                panic!("Cannot write to `{}`: {e}", path.display());
-            }
-        },
-    }
-}
-
-/// Replaces a region in a text delimited by two strings. Returns the new text if both delimiters
-/// were found, or the missing delimiter if not.
-fn replace_region_in_text<'a>(
-    text: &str,
-    start: &'a str,
-    end: &'a str,
-    mut write_replacement: impl FnMut(&mut String),
-) -> Result<String, &'a str> {
-    let (text_start, rest) = text.split_once(start).ok_or(start)?;
-    let (_, text_end) = rest.split_once(end).ok_or(end)?;
-
-    let mut res = String::with_capacity(text.len() + 4096);
-    res.push_str(text_start);
-    res.push_str(start);
-    write_replacement(&mut res);
-    res.push_str(end);
-    res.push_str(text_end);
-
-    Ok(res)
-}
-
 fn try_rename_file(old_name: &Path, new_name: &Path) -> bool {
     match OpenOptions::new().create_new(true).write(true).open(new_name) {
         Ok(file) => drop(file),
diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs
new file mode 100644
index 000000000000..b87fcca13b1c
--- /dev/null
+++ b/clippy_dev/src/utils.rs
@@ -0,0 +1,142 @@
+use std::path::{Path, PathBuf};
+use std::process::{self, ExitStatus};
+use std::{fs, io};
+
+#[cfg(not(windows))]
+static CARGO_CLIPPY_EXE: &str = "cargo-clippy";
+#[cfg(windows)]
+static CARGO_CLIPPY_EXE: &str = "cargo-clippy.exe";
+
+/// Returns the path to the `cargo-clippy` binary
+///
+/// # Panics
+///
+/// Panics if the path of current executable could not be retrieved.
+#[must_use]
+pub fn cargo_clippy_path() -> PathBuf {
+    let mut path = std::env::current_exe().expect("failed to get current executable name");
+    path.set_file_name(CARGO_CLIPPY_EXE);
+    path
+}
+
+/// Returns the path to the Clippy project directory
+///
+/// # Panics
+///
+/// Panics if the current directory could not be retrieved, there was an error reading any of the
+/// Cargo.toml files or ancestor directory is the clippy root directory
+#[must_use]
+pub fn clippy_project_root() -> PathBuf {
+    let current_dir = std::env::current_dir().unwrap();
+    for path in current_dir.ancestors() {
+        let result = fs::read_to_string(path.join("Cargo.toml"));
+        if let Err(err) = &result {
+            if err.kind() == io::ErrorKind::NotFound {
+                continue;
+            }
+        }
+
+        let content = result.unwrap();
+        if content.contains("[package]\nname = \"clippy\"") {
+            return path.to_path_buf();
+        }
+    }
+    panic!("error: Can't determine root of project. Please run inside a Clippy working dir.");
+}
+
+/// # Panics
+/// Panics if given command result was failed.
+pub fn exit_if_err(status: io::Result<ExitStatus>) {
+    match status.expect("failed to run command").code() {
+        Some(0) => {},
+        Some(n) => process::exit(n),
+        None => {
+            eprintln!("Killed by signal");
+            process::exit(1);
+        },
+    }
+}
+
+pub(crate) fn clippy_version() -> (u32, u32) {
+    fn parse_manifest(contents: &str) -> Option<(u32, u32)> {
+        let version = contents
+            .lines()
+            .filter_map(|l| l.split_once('='))
+            .find_map(|(k, v)| (k.trim() == "version").then(|| v.trim()))?;
+        let Some(("0", version)) = version.get(1..version.len() - 1)?.split_once('.') else {
+            return None;
+        };
+        let (minor, patch) = version.split_once('.')?;
+        Some((minor.parse().ok()?, patch.parse().ok()?))
+    }
+    let contents = fs::read_to_string("Cargo.toml").expect("Unable to read `Cargo.toml`");
+    parse_manifest(&contents).expect("Unable to find package version in `Cargo.toml`")
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum UpdateMode {
+    Check,
+    Change,
+}
+
+pub(crate) fn exit_with_failure() {
+    println!(
+        "Not all lints defined properly. \
+                 Please run `cargo dev update_lints` to make sure all lints are defined properly."
+    );
+    process::exit(1);
+}
+
+/// Replaces a region in a file delimited by two lines matching regexes.
+///
+/// `path` is the relative path to the file on which you want to perform the replacement.
+///
+/// See `replace_region_in_text` for documentation of the other options.
+///
+/// # Panics
+///
+/// Panics if the path could not read or then written
+pub(crate) fn replace_region_in_file(
+    update_mode: UpdateMode,
+    path: &Path,
+    start: &str,
+    end: &str,
+    write_replacement: impl FnMut(&mut String),
+) {
+    let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {e}", path.display()));
+    let new_contents = match replace_region_in_text(&contents, start, end, write_replacement) {
+        Ok(x) => x,
+        Err(delim) => panic!("Couldn't find `{delim}` in file `{}`", path.display()),
+    };
+
+    match update_mode {
+        UpdateMode::Check if contents != new_contents => exit_with_failure(),
+        UpdateMode::Check => (),
+        UpdateMode::Change => {
+            if let Err(e) = fs::write(path, new_contents.as_bytes()) {
+                panic!("Cannot write to `{}`: {e}", path.display());
+            }
+        },
+    }
+}
+
+/// Replaces a region in a text delimited by two strings. Returns the new text if both delimiters
+/// were found, or the missing delimiter if not.
+pub(crate) fn replace_region_in_text<'a>(
+    text: &str,
+    start: &'a str,
+    end: &'a str,
+    mut write_replacement: impl FnMut(&mut String),
+) -> Result<String, &'a str> {
+    let (text_start, rest) = text.split_once(start).ok_or(start)?;
+    let (_, text_end) = rest.split_once(end).ok_or(end)?;
+
+    let mut res = String::with_capacity(text.len() + 4096);
+    res.push_str(text_start);
+    res.push_str(start);
+    write_replacement(&mut res);
+    res.push_str(end);
+    res.push_str(text_end);
+
+    Ok(res)
+}
diff --git a/clippy_lints/Cargo.toml b/clippy_lints/Cargo.toml
index 63ea6faf60db..be99dcc2c7c5 100644
--- a/clippy_lints/Cargo.toml
+++ b/clippy_lints/Cargo.toml
@@ -1,6 +1,8 @@
 [package]
 name = "clippy_lints"
+# begin autogenerated version
 version = "0.1.84"
+# end autogenerated version
 description = "A bunch of helpful lints to avoid common pitfalls in Rust"
 repository = "https://github.com/rust-lang/rust-clippy"
 readme = "README.md"
diff --git a/clippy_utils/Cargo.toml b/clippy_utils/Cargo.toml
index fb2acf700ab3..4f95889a53a7 100644
--- a/clippy_utils/Cargo.toml
+++ b/clippy_utils/Cargo.toml
@@ -1,6 +1,8 @@
 [package]
 name = "clippy_utils"
+# begin autogenerated version
 version = "0.1.84"
+# end autogenerated version
 edition = "2021"
 description = "Helpful tools for writing lints, provided as they are used in Clippy"
 repository = "https://github.com/rust-lang/rust-clippy"
diff --git a/rust-toolchain b/rust-toolchain
index e32e0cb36047..0a2e9d89b6e8 100644
--- a/rust-toolchain
+++ b/rust-toolchain
@@ -1,4 +1,6 @@
 [toolchain]
+# begin autogenerated nightly
 channel = "nightly-2024-11-14"
+# end autogenerated nightly
 components = ["cargo", "llvm-tools", "rust-src", "rust-std", "rustc", "rustc-dev", "rustfmt"]
 profile = "minimal"