Repository: MarcoIeni/release-plz (rust)

Issue:
New config option:
## Motivations

In cargo-binstall, we recently switched to release-plz to release library crates in the repository (a separate release scheme for our binary crate).

However, we have installation scripts which download the pre-built binary for the binary crate, from the latest release.

The release-plz breaks this as it sets the libary crates to latest release, whereas we previously just create a tag.

- Would you like to implement this feature? Might not have much time

## Solution

Add a new configuration option to disable setting the latest release.

Pull request description:
feat: add `git_release_latest` configuration option
<!-- Please explain the changes you made -->

<!--
Please, make sure:
- you have read the contributing guidelines:
  https://github.com/MarcoIeni/release-plz/blob/main/CONTRIBUTING.md
- you have formatted the code using rustfmt:
  https://github.com/rust-lang/rustfmt
- you have checked that all tests pass, by running `cargo test`
-->

Close #1583 


Patch:
diff --git a/crates/release_plz/src/config.rs b/crates/release_plz/src/config.rs
index 3f975de3..a901f0a8 100644
--- a/crates/release_plz/src/config.rs
+++ b/crates/release_plz/src/config.rs
@@ -1,7 +1,8 @@
 use cargo_metadata::camino::Utf8Path;
 use cargo_utils::to_utf8_pathbuf;
 use release_plz_core::{
-    fs_utils::to_utf8_path, set_version::SetVersionRequest, ReleaseRequest, UpdateRequest,
+    fs_utils::to_utf8_path, set_version::SetVersionRequest, GitReleaseConfig, ReleaseRequest,
+    UpdateRequest,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -197,26 +198,12 @@ pub struct PackageSpecificConfigWithName {
 impl From<PackageConfig> for release_plz_core::ReleaseConfig {
     fn from(value: PackageConfig) -> Self {
         let is_publish_enabled = value.publish != Some(false);
-        let is_git_release_enabled = value.git_release_enable != Some(false);
-        let is_git_release_draft = value.git_release_draft == Some(true);
-        let git_release_type: release_plz_core::ReleaseType = value
-            .git_release_type
-            .map(|release_type| release_type.into())
-            .unwrap_or_default();
-        let git_release_name = value.git_release_name.clone();
-        let git_release_body = value.git_release_body.clone();
         let is_git_tag_enabled = value.git_tag_enable != Some(false);
         let git_tag_name = value.git_tag_name.clone();
         let release = value.release != Some(false);
         let mut cfg = Self::default()
             .with_publish(release_plz_core::PublishConfig::enabled(is_publish_enabled))
-            .with_git_release(
-                release_plz_core::GitReleaseConfig::enabled(is_git_release_enabled)
-                    .set_draft(is_git_release_draft)
-                    .set_release_type(git_release_type)
-                    .set_name_template(git_release_name)
-                    .set_body_template(git_release_body),
-            )
+            .with_git_release(git_release(&value))
             .with_git_tag(
                 release_plz_core::GitTagConfig::enabled(is_git_tag_enabled)
                     .set_name_template(git_tag_name),
@@ -242,6 +229,28 @@ impl From<PackageConfig> for release_plz_core::ReleaseConfig {
     }
 }
 
+fn git_release(config: &PackageConfig) -> GitReleaseConfig {
+    let is_git_release_enabled = config.git_release_enable != Some(false);
+    let git_release_type: release_plz_core::ReleaseType = config
+        .git_release_type
+        .map(|release_type| release_type.into())
+        .unwrap_or_default();
+    let is_git_release_draft = config.git_release_draft == Some(true);
+    let git_release_name = config.git_release_name.clone();
+    let git_release_body = config.git_release_body.clone();
+    let mut git_release = release_plz_core::GitReleaseConfig::enabled(is_git_release_enabled)
+        .set_draft(is_git_release_draft)
+        .set_release_type(git_release_type)
+        .set_name_template(git_release_name)
+        .set_body_template(git_release_body);
+
+    if config.git_release_latest == Some(false) {
+        git_release = git_release.set_latest(false);
+    }
+
+    git_release
+}
+
 /// Configuration that can be specified both at the `[workspace]` and at the `[[package]]` level.
 #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone, JsonSchema)]
 pub struct PackageConfig {
@@ -268,6 +277,9 @@ pub struct PackageConfig {
     /// # Git Release Draft
     /// If true, will not auto-publish the release.
     pub git_release_draft: Option<bool>,
+    /// # Git Release Latest
+    /// If true, will set the git release as latest.
+    pub git_release_latest: Option<bool>,
     /// # Git Release Name
     /// Tera template of the git release name created by release-plz.
     pub git_release_name: Option<String>,
@@ -330,6 +342,7 @@ impl PackageConfig {
             git_release_enable: self.git_release_enable.or(default.git_release_enable),
             git_release_type: self.git_release_type.or(default.git_release_type),
             git_release_draft: self.git_release_draft.or(default.git_release_draft),
+            git_release_latest: self.git_release_latest.or(default.git_release_latest),
             git_release_name: self.git_release_name.or(default.git_release_name),
             git_release_body: self.git_release_body.or(default.git_release_body),
 
diff --git a/crates/release_plz_core/src/command/release.rs b/crates/release_plz_core/src/command/release.rs
index bf50f7b0..e33c933f 100644
--- a/crates/release_plz_core/src/command/release.rs
+++ b/crates/release_plz_core/src/command/release.rs
@@ -347,6 +347,7 @@ pub enum ReleaseType {
 pub struct GitReleaseConfig {
     enabled: bool,
     draft: bool,
+    latest: Option<bool>,
     release_type: ReleaseType,
     name_template: Option<String>,
     body_template: Option<String>,
@@ -363,6 +364,7 @@ impl GitReleaseConfig {
         Self {
             enabled,
             draft: false,
+            latest: None,
             release_type: ReleaseType::default(),
             name_template: None,
             body_template: None,
@@ -378,6 +380,11 @@ impl GitReleaseConfig {
         self
     }
 
+    pub fn set_latest(mut self, latest: bool) -> Self {
+        self.latest = Some(latest);
+        self
+    }
+
     pub fn set_release_type(mut self, release_type: ReleaseType) -> Self {
         self.release_type = release_type;
         self
@@ -556,6 +563,9 @@ async fn should_release(
     let prs = git_client.associated_prs(&last_commit).await?;
     let is_current_commit_from_release_pr =
         prs.iter().any(|pr| pr.branch().starts_with(BRANCH_PREFIX));
+    if !is_current_commit_from_release_pr {
+        info!("skipping release: current commit is not from a release PR");
+    }
     Ok(is_current_commit_from_release_pr)
 }
 
@@ -649,6 +659,7 @@ async fn release_package(
                 release_name,
                 release_body,
                 draft: release_config.draft,
+                latest: release_config.latest,
                 pre_release: is_pre_release,
             };
             git_client.create_release(&release_info).await?;
@@ -671,6 +682,7 @@ pub struct GitReleaseInfo {
     pub git_tag: String,
     pub release_name: String,
     pub release_body: String,
+    pub latest: Option<bool>,
     pub draft: bool,
     pub pre_release: bool,
 }
diff --git a/crates/release_plz_core/src/git/backend.rs b/crates/release_plz_core/src/git/backend.rs
index 15bf92c8..3d9706f1 100644
--- a/crates/release_plz_core/src/git/backend.rs
+++ b/crates/release_plz_core/src/git/backend.rs
@@ -3,6 +3,7 @@ use crate::{GitHub, GitReleaseInfo};
 
 use crate::pr::Pr;
 use anyhow::Context;
+use http::StatusCode;
 use reqwest::header::HeaderMap;
 use reqwest::Url;
 use reqwest_middleware::ClientBuilder;
@@ -74,6 +75,8 @@ pub struct CreateReleaseOption<'a> {
     name: &'a str,
     draft: &'a bool,
     prerelease: &'a bool,
+    /// Only supported by GitHub.
+    make_latest: Option<String>,
 }
 
 #[derive(Deserialize, Clone, Debug)]
@@ -185,12 +188,16 @@ impl GitClient {
 
     /// Same as Gitea.
     pub async fn create_github_release(&self, release_info: &GitReleaseInfo) -> anyhow::Result<()> {
+        if release_info.latest.is_some() && self.backend == BackendType::Gitea {
+            anyhow::bail!("Gitea does not support the `git_release_latest` option");
+        }
         let create_release_options = CreateReleaseOption {
             tag_name: &release_info.git_tag,
             body: &release_info.release_body,
             name: &release_info.release_name,
             draft: &release_info.draft,
             prerelease: &release_info.pre_release,
+            make_latest: release_info.latest.map(|l| l.to_string()),
         };
         self.client
             .post(format!("{}/releases", self.repo_url()))
@@ -402,7 +409,12 @@ impl GitClient {
             return Ok(vec![]);
         }
         debug!("Associated PR found. Status: {}", response.status());
-        let response = response.error_for_status()?;
+        let response = response.error_for_status().map_err(|e| match e.status() {
+            Some(StatusCode::UNPROCESSABLE_ENTITY) => {
+                anyhow::anyhow!("Received the following error from {}: {e:?}. Did you push the commit {commit}?", self.remote.base_url)
+            }
+            _ => anyhow::anyhow!(e),
+        })?;
 
         let prs = match self.backend {
             BackendType::Github => {
diff --git a/website/docs/config.md b/website/docs/config.md
index 58ae90e8..3e10dcbe 100644
--- a/website/docs/config.md
+++ b/website/docs/config.md
@@ -64,6 +64,7 @@ the following sections:
   - [`git_release_body`](#the-git_release_body-field) — Customize git release body pattern.
   - [`git_release_type`](#the-git_release_type-field) — Publish mode for git release.
   - [`git_release_draft`](#the-git_release_draft-field) — Publish git release as draft.
+  - [`git_release_latest`](#the-git_release_latest-field) — Publish git release as latest.
   - [`git_tag_enable`](#the-git_tag_enable-field) — Enable git tag.
   - [`git_tag_name`](#the-git_tag_name-field) — Customize git tag pattern.
   - [`pr_draft`](#the-pr_draft-field) — Open the release Pull Request as a draft.
@@ -88,6 +89,7 @@ the following sections:
   - [`git_release_body`](#the-git_release_body-field-package-section) — Customize git release body pattern.
   - [`git_release_type`](#the-git_release_type-field-package-section) — Git release type.
   - [`git_release_draft`](#the-git_release_draft-field-package-section) — Publish git release as draft.
+  - [`git_release_latest`](#the-git_release_latest-field-package-section) — Publish git release as latest.
   - [`git_tag_enable`](#the-git_tag_enable-field-package-section) — Enable git tag.
   - [`git_tag_name`](#the-git_tag_name-field-package-section) — Customize git tag pattern.
   - [`publish`](#the-publish-field-package-section) — Publish to cargo registry.
@@ -240,6 +242,17 @@ Supported values are:
 
 *(GitHub, Gitea only)*.
 
+#### The `git_release_latest` field
+
+- If `true`, release-plz creates the git release as latest. *(Default)*.
+- If `false`, release-plz doesn't set the git release it creates as latest.
+
+*(GitHub only)*. Gitea doesn't support this feature.
+
+:::warning
+Drafts and prereleases cannot be set as latest.
+:::
+
 #### The `git_tag_enable` field
 
 - If `true`, release-plz creates a git tag for the new package version. *(Default)*.
@@ -526,6 +539,10 @@ Overrides the [`workspace.git_release_type`](#the-git_release_type-field) field.
 
 Overrides the [`workspace.git_release_draft`](#the-git_release_draft-field) field.
 
+#### The `git_release_latest` field (`package` section)
+
+Overrides the [`workspace.git_release_latest`](#the-git_release_latest-field) field.
+
 #### The `git_tag_enable` field (`package` section)
 
 Overrides the [`workspace.git_tag_enable`](#the-git_tag_enable-field) field.


Test patch:
diff --git a/.schema/latest.json b/.schema/latest.json
index ef36f9ce..74f25c11 100644
--- a/.schema/latest.json
+++ b/.schema/latest.json
@@ -44,6 +44,7 @@
         "git_release_body": null,
         "git_release_draft": null,
         "git_release_enable": null,
+        "git_release_latest": null,
         "git_release_name": null,
         "git_release_type": null,
         "git_tag_enable": null,
@@ -303,6 +304,14 @@
             "null"
           ]
         },
+        "git_release_latest": {
+          "title": "Git Release Latest",
+          "description": "If true, will set the git release as latest.",
+          "type": [
+            "boolean",
+            "null"
+          ]
+        },
         "git_release_name": {
           "title": "Git Release Name",
           "description": "Tera template of the git release name created by release-plz.",
@@ -525,6 +534,14 @@
             "null"
           ]
         },
+        "git_release_latest": {
+          "title": "Git Release Latest",
+          "description": "If true, will set the git release as latest.",
+          "type": [
+            "boolean",
+            "null"
+          ]
+        },
         "git_release_name": {
           "title": "Git Release Name",
           "description": "Tera template of the git release name created by release-plz.",
diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml
index 76572a1b..5a9a176d 100644
--- a/tests/docker-compose.yml
+++ b/tests/docker-compose.yml
@@ -2,7 +2,7 @@ services:
   # We use Gitea as a git server.
   # We test that release-plz can open PRs, create releases, etc.
   gitea:
-    image: docker.io/gitea/gitea:1.22.0-rootless
+    image: docker.io/gitea/gitea:1.22.1-rootless
     container_name: gitea
     restart: always
     environment: