Repository: axodotdev/cargo-dist
Pull request #156 (2023-02-23 17:33:22)

Dist manifest cleanup
fixes #150 

Note that there's currently a weird denormalization where an artifact is referenced by id, but also has a "name" which is just the id again, but Optional. This is intentionally left in as future space for #72.


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 22efe7fe..dcf5dc83 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -45,6 +45,8 @@ jobs:
   # Create the Github Release™ so the packages have something to be uploaded to
   create-release:
     runs-on: ubuntu-latest
+    outputs:
+      has-releases: ${{ steps.create-release.outputs.has-releases }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -52,8 +54,8 @@ jobs:
       - name: Install Rust
         run: rustup update 1.67.1 --no-self-update && rustup default 1.67.1
       - name: Install cargo-dist
-        run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease04/cargo-dist-v0.0.3-prerelease04-installer.sh | sh
-      - name: Create Release
+        run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease05/cargo-dist-v0.0.3-prerelease05-installer.sh | sh
+      - id: create-release
         run: |
           cargo dist manifest --tag=${{ github.ref_name }} --artifacts=all --no-local-paths --output-format=json > dist-manifest.json
           echo "dist manifest ran successfully"
@@ -70,25 +72,31 @@ jobs:
           gh release upload ${{ github.ref_name }} dist-manifest.json
           echo "uploaded manifest!"
 
+          # Disable all the upload-artifacts tasks if we have no actual releases
+          HAS_RELEASES=$(cat dist-manifest.json | jq --raw-output ".releases != null")
+          echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT"
+
   # Build and packages all the things
   upload-artifacts:
+    # Let the initial task tell us to not run (currently very blunt)
     needs: create-release
+    if: ${{ needs.create-release.outputs.has-releases == 'true' }}
     strategy:
       matrix:
         # For these target platforms
         include:
         - os: ubuntu-20.04
           dist-args: --artifacts=global
-          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease04/cargo-dist-v0.0.3-prerelease04-installer.sh | sh
+          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease05/cargo-dist-v0.0.3-prerelease05-installer.sh | sh
         - os: macos-11
           dist-args: --artifacts=local --target=x86_64-apple-darwin
-          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease04/cargo-dist-v0.0.3-prerelease04-installer.sh | sh
+          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease05/cargo-dist-v0.0.3-prerelease05-installer.sh | sh
         - os: ubuntu-20.04
           dist-args: --artifacts=local --target=x86_64-unknown-linux-gnu
-          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease04/cargo-dist-v0.0.3-prerelease04-installer.sh | sh
+          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease05/cargo-dist-v0.0.3-prerelease05-installer.sh | sh
         - os: windows-2019
           dist-args: --artifacts=local --target=x86_64-pc-windows-msvc
-          install-dist: irm  https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease04/cargo-dist-v0.0.3-prerelease04-installer.ps1 | iex
+          install-dist: irm  https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease05/cargo-dist-v0.0.3-prerelease05-installer.ps1 | iex
 
     runs-on: ${{ matrix.os }}
     env:
@@ -119,7 +127,9 @@ jobs:
 
   # Mark the Github Release™ as a non-draft now that everything has succeeded!
   publish-release:
+    # Only run after all the other tasks, but it's ok if upload-artifacts was skipped
     needs: [create-release, upload-artifacts]
+    if: ${{ always() && needs.create-release.result == 'success' && (needs.upload-artifacts.result == 'skipped' || needs.upload-artifacts.result == 'success') }}
     runs-on: ubuntu-latest
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/Cargo.lock b/Cargo.lock
index eb65f0bc..33e3107d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -146,7 +146,7 @@ dependencies = [
 
 [[package]]
 name = "cargo-dist"
-version = "0.0.3-prerelease04"
+version = "0.0.3-prerelease05"
 dependencies = [
  "camino",
  "cargo-dist-schema",
@@ -174,7 +174,7 @@ dependencies = [
 
 [[package]]
 name = "cargo-dist-schema"
-version = "0.0.3-prerelease04"
+version = "0.0.3-prerelease05"
 dependencies = [
  "camino",
  "insta",
diff --git a/Cargo.toml b/Cargo.toml
index 5b2e8e93..e2c970e4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@ tag-name = "v{{version}}"
 # Config for 'cargo dist'
 [workspace.metadata.dist]
 # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
-cargo-dist-version = "0.0.3-prerelease04"
+cargo-dist-version = "0.0.3-prerelease05"
 # The preferred Rust toolchain to use in CI (rustup toolchain syntax)
 rust-toolchain-version = "1.67.1"
 # CI backends to support (see 'cargo dist generate-ci')
diff --git a/RELEASES.md b/RELEASES.md
index db08e97f..b59548ab 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -1,4 +1,4 @@
-# Version 0.0.3 (not yet released!)
+# Version 0.0.3 (under development)
 
 **USE AT YOUR OWN RISK, SUPER ALPHA**
 
@@ -98,6 +98,8 @@ Release notes are now temporarily simplified for reliability:
 
 * If you are publishing `1.2.0-prerelease` and we don't find that in your RELEASES/CHANGELOG file, we will now also look for bare `1.2.0` (stripping the prerelease/build portions), on the assumption that these are the WIP release notes for the version you're prereleasing. This lets you iterate on a version without having to churn headings every time you want to cut a prerelease (we recommend including a parenthetical indicating the version is not yet released).
 
+* If the above explained deferring happens, we will modify the release note's title to include the prerelease suffix. This ensures they are easily identifiable as prereleases on GitHub's releases page.
+
 * We will no longer attempt to include your release notes for Singular Announcements (see the previous section). They will only get auto-generated installers/downloads sections. This is obviously suboptimal, and will be fixed, we just need to do design work on the proper way to handle those cases. (Please tell me in [issue #139](https://github.com/axodotdev/cargo-dist/issues/139)!)
 
 
diff --git a/cargo-dist-schema/Cargo.toml b/cargo-dist-schema/Cargo.toml
index 904dc988..aca97ecd 100644
--- a/cargo-dist-schema/Cargo.toml
+++ b/cargo-dist-schema/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "cargo-dist-schema"
 description = "Schema information for cargo-dist's dist-manifest.json"
-version = "0.0.3-prerelease04"
+version = "0.0.3-prerelease05"
 edition = "2021"
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/axodotdev/cargo-dist"
diff --git a/cargo-dist-schema/cargo-dist-json-schema.json b/cargo-dist-schema/cargo-dist-json-schema.json
index 4f3bc115..84fa435a 100644
--- a/cargo-dist-schema/cargo-dist-json-schema.json
+++ b/cargo-dist-schema/cargo-dist-json-schema.json
@@ -37,6 +37,13 @@
         "null"
       ]
     },
+    "artifacts": {
+      "description": "The artifacts included in this Announcement, referenced by releases.",
+      "type": "object",
+      "additionalProperties": {
+        "$ref": "#/definitions/Artifact"
+      }
+    },
     "dist_version": {
       "description": "The version of cargo-dist that generated this",
       "type": [
@@ -87,21 +94,6 @@
             }
           }
         },
-        {
-          "description": "Machine-readable metadata",
-          "type": "object",
-          "required": [
-            "kind"
-          ],
-          "properties": {
-            "kind": {
-              "type": "string",
-              "enum": [
-                "dist-metadata"
-              ]
-            }
-          }
-        },
         {
           "description": "Installer",
           "type": "object",
@@ -302,22 +294,8 @@
           "description": "The artifacts for this release (zips, debuginfo, metadata...)",
           "type": "array",
           "items": {
-            "$ref": "#/definitions/Artifact"
+            "type": "string"
           }
-        },
-        "changelog_body": {
-          "description": "The body of the changelog for this release",
-          "type": [
-            "string",
-            "null"
-          ]
-        },
-        "changelog_title": {
-          "description": "The title of the changelog for this release",
-          "type": [
-            "string",
-            "null"
-          ]
         }
       }
     }
diff --git a/cargo-dist-schema/src/lib.rs b/cargo-dist-schema/src/lib.rs
index e026beac..0c100b4e 100644
--- a/cargo-dist-schema/src/lib.rs
+++ b/cargo-dist-schema/src/lib.rs
@@ -8,6 +8,8 @@
 //!
 //! The root type of the schema is [`DistManifest`][].
 
+use std::collections::BTreeMap;
+
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
@@ -21,6 +23,8 @@ pub type LocalPath = String;
 ///
 /// (Should we normalize this one?)
 pub type RelPath = String;
+/// The unique ID of an Artifact
+pub type ArtifactId = String;
 
 /// A report of the releases and artifacts that cargo-dist generated
 #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
@@ -52,6 +56,10 @@ pub struct DistManifest {
     #[serde(default)]
     #[serde(skip_serializing_if = "Vec::is_empty")]
     pub releases: Vec<Release>,
+    /// The artifacts included in this Announcement, referenced by releases.
+    #[serde(default)]
+    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
+    pub artifacts: BTreeMap<ArtifactId, Artifact>,
 }
 
 /// A Release of an Application
@@ -65,15 +73,7 @@ pub struct Release {
     /// The artifacts for this release (zips, debuginfo, metadata...)
     #[serde(default)]
     #[serde(skip_serializing_if = "Vec::is_empty")]
-    pub artifacts: Vec<Artifact>,
-    /// The title of the changelog for this release
-    #[serde(default)]
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub changelog_title: Option<String>,
-    /// The body of the changelog for this release
-    #[serde(default)]
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub changelog_body: Option<String>,
+    pub artifacts: Vec<ArtifactId>,
 }
 
 /// A distributable artifact that's part of a Release
@@ -166,9 +166,6 @@ pub enum ArtifactKind {
     /// Standalone Symbols/Debuginfo for a build
     #[serde(rename = "symbols")]
     Symbols,
-    /// Machine-readable metadata
-    #[serde(rename = "dist-metadata")]
-    DistMetadata,
     /// Installer
     #[serde(rename = "installer")]
     Installer,
@@ -191,7 +188,7 @@ pub struct ExecutableAsset {
 
 impl DistManifest {
     /// Create a new DistManifest
-    pub fn new(releases: Vec<Release>) -> Self {
+    pub fn new(releases: Vec<Release>, artifacts: BTreeMap<String, Artifact>) -> Self {
         Self {
             dist_version: None,
             announcement_tag: None,
@@ -200,6 +197,7 @@ impl DistManifest {
             announcement_changelog: None,
             announcement_github_body: None,
             releases,
+            artifacts,
         }
     }
 
diff --git a/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap b/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
index 4b9ccf60..f65772af 100644
--- a/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
+++ b/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
@@ -41,6 +41,13 @@ expression: json_schema
         "null"
       ]
     },
+    "artifacts": {
+      "description": "The artifacts included in this Announcement, referenced by releases.",
+      "type": "object",
+      "additionalProperties": {
+        "$ref": "#/definitions/Artifact"
+      }
+    },
     "dist_version": {
       "description": "The version of cargo-dist that generated this",
       "type": [
@@ -91,21 +98,6 @@ expression: json_schema
             }
           }
         },
-        {
-          "description": "Machine-readable metadata",
-          "type": "object",
-          "required": [
-            "kind"
-          ],
-          "properties": {
-            "kind": {
-              "type": "string",
-              "enum": [
-                "dist-metadata"
-              ]
-            }
-          }
-        },
         {
           "description": "Installer",
           "type": "object",
@@ -306,22 +298,8 @@ expression: json_schema
           "description": "The artifacts for this release (zips, debuginfo, metadata...)",
           "type": "array",
           "items": {
-            "$ref": "#/definitions/Artifact"
+            "type": "string"
           }
-        },
-        "changelog_body": {
-          "description": "The body of the changelog for this release",
-          "type": [
-            "string",
-            "null"
-          ]
-        },
-        "changelog_title": {
-          "description": "The title of the changelog for this release",
-          "type": [
-            "string",
-            "null"
-          ]
         }
       }
     }
diff --git a/cargo-dist/Cargo.toml b/cargo-dist/Cargo.toml
index 82623568..8c96bb18 100644
--- a/cargo-dist/Cargo.toml
+++ b/cargo-dist/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "cargo-dist"
 description = "Shippable application packaging for Rust"
-version = "0.0.3-prerelease04"
+version = "0.0.3-prerelease05"
 edition = "2021"
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/axodotdev/cargo-dist"
@@ -32,7 +32,7 @@ console = { version = "0.15.1", optional = true }
 clap-cargo = { version = "0.10.0", optional = true }
 
 # Features used by the cli and library
-cargo-dist-schema = { version = "=0.0.3-prerelease04", path = "../cargo-dist-schema" }
+cargo-dist-schema = { version = "=0.0.3-prerelease05", path = "../cargo-dist-schema" }
 miette = { version = "5.3.0" }
 thiserror = "1.0.35"
 tracing = { version = "0.1.36", features = ["log"] }
diff --git a/cargo-dist/src/ci.rs b/cargo-dist/src/ci.rs
index 5666722f..3c26c5a8 100644
--- a/cargo-dist/src/ci.rs
+++ b/cargo-dist/src/ci.rs
@@ -25,13 +25,13 @@ pub fn generate_github_ci(dist: &DistGraph) -> Result<(), miette::Report> {
     std::fs::create_dir_all(&ci_dir)
         .into_diagnostic()
         .wrap_err("Failed to create ci dir")?;
-    let mut file = File::create(ci_file)
+    let mut file = File::create(&ci_file)
         .into_diagnostic()
         .wrap_err("Failed to create ci file")?;
     write_github_ci(&mut file, dist)
         .into_diagnostic()
         .wrap_err("Failed to write to CI file")?;
-    info!("successfully generated Github CI");
+    eprintln!("generated Github CI to {}", ci_file);
     Ok(())
 }
 
diff --git a/cargo-dist/src/ci.yml b/cargo-dist/src/ci.yml
index e436aaaf..42180184 100644
--- a/cargo-dist/src/ci.yml
+++ b/cargo-dist/src/ci.yml
@@ -45,6 +45,8 @@ jobs:
   # Create the Github Release™ so the packages have something to be uploaded to
   create-release:
     runs-on: ubuntu-latest
+    outputs:
+      has-releases: ${{ steps.create-release.outputs.has-releases }}
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     steps:
@@ -53,7 +55,7 @@ jobs:
         run: {{{{INSTALL_RUST}}}}
       - name: Install cargo-dist
         run: {{{{INSTALL_DIST_SH}}}}
-      - name: Create Release
+      - id: create-release
         run: |
           cargo dist manifest --tag=${{ github.ref_name }} --artifacts=all --no-local-paths --output-format=json > dist-manifest.json
           echo "dist manifest ran successfully"
@@ -70,9 +72,15 @@ jobs:
           gh release upload ${{ github.ref_name }} dist-manifest.json
           echo "uploaded manifest!"
 
+          # Disable all the upload-artifacts tasks if we have no actual releases
+          HAS_RELEASES=$(cat dist-manifest.json | jq --raw-output ".releases != null")
+          echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT"
+
   # Build and packages all the things
   upload-artifacts:
+    # Let the initial task tell us to not run (currently very blunt)
     needs: create-release
+    if: ${{ needs.create-release.outputs.has-releases == 'true' }}
     strategy:
       matrix:
         # For these target platforms
@@ -107,7 +115,9 @@ jobs:
 
   # Mark the Github Release™ as a non-draft now that everything has succeeded!
   publish-release:
+    # Only run after all the other tasks, but it's ok if upload-artifacts was skipped
     needs: [create-release, upload-artifacts]
+    if: ${{ always() && needs.create-release.result == 'success' && (needs.upload-artifacts.result == 'skipped' || needs.upload-artifacts.result == 'success') }}
     runs-on: ubuntu-latest
     env:
       GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/cargo-dist/src/lib.rs b/cargo-dist/src/lib.rs
index 721b923a..a34f479d 100644
--- a/cargo-dist/src/lib.rs
+++ b/cargo-dist/src/lib.rs
@@ -5,11 +5,18 @@
 //!
 //!
 
-use std::{collections::HashMap, fs::File, io::BufReader, ops::Not, process::Command};
+use std::{
+    collections::{BTreeMap, HashMap},
+    fs::File,
+    io::BufReader,
+    ops::Not,
+    process::Command,
+};
 
 use camino::{Utf8Path, Utf8PathBuf};
 use cargo_dist_schema::{Asset, AssetKind, DistManifest, ExecutableAsset, Release};
 use flate2::{write::ZlibEncoder, Compression, GzBuilder};
+use semver::Version;
 use tracing::{info, warn};
 use xz2::write::XzEncoder;
 use zip::ZipWriter;
@@ -79,16 +86,21 @@ pub fn do_manifest(cfg: &Config) -> Result<DistManifest> {
 fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistManifest {
     // Report the releases
     let mut releases = vec![];
+    let mut all_artifacts = BTreeMap::<String, cargo_dist_schema::Artifact>::new();
     for release in &dist.releases {
         // Gather up all the local and global artifacts
         let mut artifacts = vec![];
         for &artifact_idx in &release.global_artifacts {
-            artifacts.push(manifest_artifact(cfg, dist, artifact_idx));
+            let id = &dist.artifact(artifact_idx).id;
+            all_artifacts.insert(id.clone(), manifest_artifact(cfg, dist, artifact_idx));
+            artifacts.push(id.clone());
         }
         for &variant_idx in &release.variants {
             let variant = dist.variant(variant_idx);
             for &artifact_idx in &variant.local_artifacts {
-                artifacts.push(manifest_artifact(cfg, dist, artifact_idx));
+                let id = &dist.artifact(artifact_idx).id;
+                all_artifacts.insert(id.clone(), manifest_artifact(cfg, dist, artifact_idx));
+                artifacts.push(id.clone());
             }
         }
 
@@ -96,13 +108,11 @@ fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistManifest {
         releases.push(Release {
             app_name: release.app_name.clone(),
             app_version: release.version.to_string(),
-            changelog_title: release.changelog_title.clone(),
-            changelog_body: release.changelog_body.clone(),
             artifacts,
         })
     }
 
-    let mut manifest = DistManifest::new(releases);
+    let mut manifest = DistManifest::new(releases, all_artifacts);
     manifest.dist_version = Some(env!("CARGO_PKG_VERSION").to_owned());
     manifest.announcement_tag = dist.announcement_tag.clone();
     manifest.announcement_is_prerelease = dist.announcement_is_prerelease;
@@ -786,6 +796,12 @@ pub struct GenerateCiArgs {}
 /// Generate CI scripts (impl of `cargo dist generate-ci`)
 pub fn do_generate_ci(cfg: &Config, _args: &GenerateCiArgs) -> Result<()> {
     let dist = gather_work(cfg)?;
+    if let Some(desired_version) = &dist.desired_cargo_dist_version {
+        let current_version: Version = std::env!("CARGO_PKG_VERSION").parse().unwrap();
+        if desired_version != &current_version {
+            return Err(miette!("you're running cargo-dist {}, but 'cargo-dist-version = {}' is set in your Cargo.toml\n\nYou should update cargo-dist-version if you want to update to this version", current_version, desired_version));
+        }
+    }
     if !dist.is_init {
         return Err(miette!(
             "please run 'cargo dist init' before running any other commands!"
diff --git a/cargo-dist/src/main.rs b/cargo-dist/src/main.rs
index f567d020..904a5183 100644
--- a/cargo-dist/src/main.rs
+++ b/cargo-dist/src/main.rs
@@ -32,6 +32,8 @@ lazy_static! {
 
 fn set_report_errors_as_json() {
     *REPORT_ERROR.lock().unwrap() = Some(Box::new(move |error| {
+        // Still write a human-friendly version to stderr
+        writeln!(&mut Term::stderr(), "{error:?}").unwrap();
         // Manually invoke JSONReportHandler to format the error as a report
         // to out_.
         let mut report = String::new();
@@ -154,7 +156,8 @@ fn print_human(out: &mut Term, manifest: &DistManifest) -> Result<(), std::io::E
                 .blue()
                 .apply_to(format!("  {} {}", release.app_name, release.app_version))
         )?;
-        for artifact in &release.artifacts {
+        for artifact_id in &release.artifacts {
+            let artifact = &manifest.artifacts[artifact_id];
             // Print out the name or path of the artifact (path is more useful by noisier)
             if let Some(path) = &artifact.path {
                 // Try to highlight the actual filename for easier scanning
diff --git a/cargo-dist/src/tasks.rs b/cargo-dist/src/tasks.rs
index 1719382f..25df4aa0 100644
--- a/cargo-dist/src/tasks.rs
+++ b/cargo-dist/src/tasks.rs
@@ -1393,34 +1393,23 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             }
         };
 
-        // Try to lookup this version in the changelog
-        let version_string = format!("{}", announcing_version);
-        let release_notes = if let Some(release_notes) = changelogs.get(&*version_string) {
-            release_notes
-        } else if !announcing_version.pre.is_empty() {
-            // If we're a prerelease, try looking for this version without the prerelease suffix
-            // on the basis that we shouldn't be backfilling prereleases after the real release,
-            // so those are probably also our own WIP release notes.
-            let normal_version = Version::new(
-                announcing_version.major,
-                announcing_version.minor,
-                announcing_version.patch,
-            );
-
-            let version_string = format!("{}", normal_version);
-            let Some(release_notes) = changelogs.get(&*version_string) else {
-                info!("failed to find {version_string} in changelogs, skipping changelog generation");
+        // Try to extract the changelog entry for the announcing version.
+        // First, try to find the exact version. If that fails, try to find this version without
+        // the prerelease suffix.
+        // Because releasing a prerelease for a version that was already published before does not
+        // make much sense it is fairly safe to assume that this entry is in fact just our WIP state
+        // of the release notes.
+        let Some((title, notes)) =
+            try_extract_changelog_exact(&changelogs, announcing_version)
+                .or_else(|| try_extract_changelog_normalized(&changelogs, announcing_version))
+            else {
+                info!("failed to find {announcing_version} in changelogs, skipping changelog generation");
                 return;
             };
-            release_notes
-        } else {
-            info!("failed to find {version_string} in changelogs, skipping changelog generation");
-            return;
-        };
 
         info!("succesfully parsed changelog!");
-        self.inner.announcement_title = Some(release_notes.title.to_owned());
-        self.inner.announcement_changelog = Some(release_notes.notes.to_owned());
+        self.inner.announcement_title = Some(title);
+        self.inner.announcement_changelog = Some(notes);
     }
 
     /// If we're publishing to Github, generate some Github notes
@@ -1759,9 +1748,13 @@ pub fn gather_work(cfg: &Config) -> Result<DistGraph> {
 
     // Don't proceed if this doesn't make sense
     if rust_releases.is_empty() {
-        return Err(miette!(
-            "This workspace doesn't have anything for cargo-dist to Release!"
-        ));
+        if announcing_package.is_some() {
+            warn!("You're trying to explicitly Release a library, only minimal functionality will work");
+        } else {
+            return Err(miette!(
+                "This workspace doesn't have anything for cargo-dist to Release!"
+            ));
+        }
     }
     // If we don't have a tag yet we MUST successfully select one here or fail
     if announcement_tag.is_none() {
@@ -2285,3 +2278,57 @@ fn try_load_changelog(changelog_path: &Utf8Path) -> Result<String> {
         .wrap_err_with(|| format!("failed to read changelog at {changelog_path}"))?;
     Ok(changelog_str)
 }
+
+/// Tries to find a changelog entry with the exact version given and returns its title and notes.
+fn try_extract_changelog_exact(
+    changelogs: &parse_changelog::Changelog,
+    version: &Version,
+) -> Option<(String, String)> {
+    let version_string = format!("{}", version);
+
+    changelogs.get(&*version_string).map(|release_notes| {
+        (
+            release_notes.title.to_string(),
+            release_notes.notes.to_string(),
+        )
+    })
+}
+
+/// Tries to find a changelog entry that matches the given version's normalized form. That is, just
+/// the `major.minor.patch` part. If successful, the entry's title is modified to include the
+/// version's prerelease part before it is returned together with the notes.
+///
+/// Noop if the given version is already normalized.
+fn try_extract_changelog_normalized(
+    changelogs: &parse_changelog::Changelog,
+    version: &Version,
+) -> Option<(String, String)> {
+    if version.pre.is_empty() {
+        return None;
+    }
+
+    let version_normalized = Version::new(version.major, version.minor, version.patch);
+    let version_normalized_string = format!("{}", version_normalized);
+
+    let release_notes = changelogs.get(&*version_normalized_string)?;
+
+    // title looks something like '<prefix><version><freeform>'
+    // prefix could be 'v' or 'Version ' for example
+    let (prefix_and_version, freeform) = release_notes.title.split_at(
+        release_notes
+            .title
+            .find(&*version_normalized_string)
+            .unwrap() // impossible that this version string is not present in the header
+            + version_normalized_string.len(),
+    );
+
+    // insert prerelease suffix into the title
+    let title = format!(
+        "{}-{} {}",
+        prefix_and_version.trim(),
+        version.pre,
+        freeform.trim()
+    );
+
+    Some((title.trim().to_string(), release_notes.notes.to_string()))
+}


Test patch:
diff --git a/cargo-dist/tests/cli-tests.rs b/cargo-dist/tests/cli-tests.rs
index 8e076735..b244cb99 100644
--- a/cargo-dist/tests/cli-tests.rs
+++ b/cargo-dist/tests/cli-tests.rs
@@ -94,3 +94,63 @@ fn test_manifest() {
 
     assert!(output.status.success(), "{}", output.status);
 }
+
+#[test]
+fn test_lib_manifest() {
+    let version = std::env!("CARGO_PKG_VERSION");
+    let output = Command::new(BIN)
+        .arg("dist")
+        .arg("manifest")
+        .arg("--artifacts=all")
+        .arg("--no-local-paths")
+        .arg("--output-format=json")
+        .arg("--tag")
+        .arg(&format!("cargo-dist-schema-v{}", version))
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .output()
+        .unwrap();
+
+    // We don't want this to churn every time we do a version bump
+    insta::with_settings!({filters => vec![
+        (r"\d+\.\d+\.\d+(\-prerelease\d+)?", "1.0.0-FAKEVERSION"),
+        (r#""announcement_tag": .*"#, r#""announcement_tag": "CENSORED","#),
+        (r#""announcement_title": .*"#, r#""announcement_title": "CENSORED""#),
+        (r#""announcement_changelog": .*"#, r#""announcement_changelog": "CENSORED""#),
+        (r#""announcement_github_body": .*"#, r#""announcement_github_body": "CENSORED""#),
+        (r#""announcement_is_prerelease": .*"#, r#""announcement_is_prerelease": "CENSORED""#),
+    ]}, {
+        insta::assert_snapshot!(format_outputs(&output));
+    });
+
+    assert!(output.status.success(), "{}", output.status);
+}
+
+#[test]
+fn test_error_manifest() {
+    let output = Command::new(BIN)
+        .arg("dist")
+        .arg("manifest")
+        .arg("--artifacts=all")
+        .arg("--no-local-paths")
+        .arg("--output-format=json")
+        .arg("--tag=v0.0.0")
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .output()
+        .unwrap();
+
+    // We don't want this to churn every time we do a version bump
+    insta::with_settings!({filters => vec![
+        (r"\d+\.\d+\.\d+(\-prerelease\d+)?", "1.0.0-FAKEVERSION"),
+        (r#""announcement_tag": .*"#, r#""announcement_tag": "CENSORED","#),
+        (r#""announcement_title": .*"#, r#""announcement_title": "CENSORED""#),
+        (r#""announcement_changelog": .*"#, r#""announcement_changelog": "CENSORED""#),
+        (r#""announcement_github_body": .*"#, r#""announcement_github_body": "CENSORED""#),
+        (r#""announcement_is_prerelease": .*"#, r#""announcement_is_prerelease": "CENSORED""#),
+    ]}, {
+        insta::assert_snapshot!(format_outputs(&output));
+    });
+
+    assert!(!output.status.success(), "{}", output.status);
+}
diff --git a/cargo-dist/tests/snapshots/cli_tests__error_manifest.snap b/cargo-dist/tests/snapshots/cli_tests__error_manifest.snap
new file mode 100644
index 00000000..5e8a9b07
--- /dev/null
+++ b/cargo-dist/tests/snapshots/cli_tests__error_manifest.snap
@@ -0,0 +1,17 @@
+---
+source: cargo-dist/tests/cli-tests.rs
+expression: format_outputs(&output)
+---
+stdout:
+{"error": {"message": "This workspace doesn't have anything for cargo-dist to Release!","severity": "error","causes": [],"labels": [],"related": []}}
+
+stderr:
+analyzing workspace:
+  cargo-dist (didn't match tag v1.0.0-FAKEVERSION)
+    [bin] cargo-dist
+  cargo-dist-schema (no binaries)
+
+
+  × This workspace doesn't have anything for cargo-dist to Release!
+
+
diff --git a/cargo-dist/tests/snapshots/cli_tests__lib_manifest.snap b/cargo-dist/tests/snapshots/cli_tests__lib_manifest.snap
new file mode 100644
index 00000000..766f108c
--- /dev/null
+++ b/cargo-dist/tests/snapshots/cli_tests__lib_manifest.snap
@@ -0,0 +1,21 @@
+---
+source: cargo-dist/tests/cli-tests.rs
+expression: format_outputs(&output)
+---
+stdout:
+{
+  "dist_version": "1.0.0-FAKEVERSION",
+  "announcement_tag": "CENSORED",
+  "announcement_is_prerelease": "CENSORED"
+  "announcement_title": "CENSORED"
+  "announcement_github_body": "CENSORED"
+}
+
+stderr:
+analyzing workspace:
+  cargo-dist (didn't match tag cargo-dist-schema-v1.0.0-FAKEVERSION)
+    [bin] cargo-dist
+  cargo-dist-schema (no binaries)
+
+ WARN You're trying to explicitly Release a library, only minimal functionality will work
+
diff --git a/cargo-dist/tests/snapshots/cli_tests__manifest.snap b/cargo-dist/tests/snapshots/cli_tests__manifest.snap
index f2b44cdc..f159eaf5 100644
--- a/cargo-dist/tests/snapshots/cli_tests__manifest.snap
+++ b/cargo-dist/tests/snapshots/cli_tests__manifest.snap
@@ -15,130 +15,137 @@ stdout:
       "app_name": "cargo-dist",
       "app_version": "1.0.0-FAKEVERSION",
       "artifacts": [
+        "cargo-dist-v1.0.0-FAKEVERSION-installer.sh",
+        "cargo-dist-v1.0.0-FAKEVERSION-installer.ps1",
+        "cargo-dist-v1.0.0-FAKEVERSION-x86_64-apple-darwin.tar.xz",
+        "cargo-dist-v1.0.0-FAKEVERSION-x86_64-pc-windows-msvc.zip",
+        "cargo-dist-v1.0.0-FAKEVERSION-x86_64-unknown-linux-gnu.tar.xz"
+      ]
+    }
+  ],
+  "artifacts": {
+    "cargo-dist-v1.0.0-FAKEVERSION-installer.ps1": {
+      "name": "cargo-dist-v1.0.0-FAKEVERSION-installer.ps1",
+      "kind": "installer",
+      "target_triples": [
+        "x86_64-pc-windows-msvc"
+      ],
+      "install_hint": "# WARNING: this installer is experimental\nirm https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-v1.0.0-FAKEVERSION-installer.ps1 | iex",
+      "description": "Install prebuilt binaries via powershell script"
+    },
+    "cargo-dist-v1.0.0-FAKEVERSION-installer.sh": {
+      "name": "cargo-dist-v1.0.0-FAKEVERSION-installer.sh",
+      "kind": "installer",
+      "target_triples": [
+        "x86_64-apple-darwin",
+        "x86_64-unknown-linux-gnu"
+      ],
+      "install_hint": "# WARNING: this installer is experimental\ncurl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-v1.0.0-FAKEVERSION-installer.sh | sh",
+      "description": "Install prebuilt binaries via shell script"
+    },
+    "cargo-dist-v1.0.0-FAKEVERSION-x86_64-apple-darwin.tar.xz": {
+      "name": "cargo-dist-v1.0.0-FAKEVERSION-x86_64-apple-darwin.tar.xz",
+      "kind": "executable-zip",
+      "target_triples": [
+        "x86_64-apple-darwin"
+      ],
+      "assets": [
+        {
+          "name": "LICENSE-APACHE",
+          "path": "LICENSE-APACHE",
+          "kind": "license"
+        },
+        {
+          "name": "LICENSE-MIT",
+          "path": "LICENSE-MIT",
+          "kind": "license"
+        },
+        {
+          "name": "README.md",
+          "path": "README.md",
+          "kind": "readme"
+        },
+        {
+          "name": "RELEASES.md",
+          "path": "RELEASES.md",
+          "kind": "changelog"
+        },
+        {
+          "name": "cargo-dist",
+          "path": "cargo-dist",
+          "kind": "executable"
+        }
+      ]
+    },
+    "cargo-dist-v1.0.0-FAKEVERSION-x86_64-pc-windows-msvc.zip": {
+      "name": "cargo-dist-v1.0.0-FAKEVERSION-x86_64-pc-windows-msvc.zip",
+      "kind": "executable-zip",
+      "target_triples": [
+        "x86_64-pc-windows-msvc"
+      ],
+      "assets": [
+        {
+          "name": "LICENSE-APACHE",
+          "path": "LICENSE-APACHE",
+          "kind": "license"
+        },
+        {
+          "name": "LICENSE-MIT",
+          "path": "LICENSE-MIT",
+          "kind": "license"
+        },
+        {
+          "name": "README.md",
+          "path": "README.md",
+          "kind": "readme"
+        },
+        {
+          "name": "RELEASES.md",
+          "path": "RELEASES.md",
+          "kind": "changelog"
+        },
+        {
+          "name": "cargo-dist",
+          "path": "cargo-dist.exe",
+          "kind": "executable"
+        }
+      ]
+    },
+    "cargo-dist-v1.0.0-FAKEVERSION-x86_64-unknown-linux-gnu.tar.xz": {
+      "name": "cargo-dist-v1.0.0-FAKEVERSION-x86_64-unknown-linux-gnu.tar.xz",
+      "kind": "executable-zip",
+      "target_triples": [
+        "x86_64-unknown-linux-gnu"
+      ],
+      "assets": [
         {
-          "name": "cargo-dist-v1.0.0-FAKEVERSION-installer.sh",
-          "kind": "installer",
-          "target_triples": [
-            "x86_64-apple-darwin",
-            "x86_64-unknown-linux-gnu"
-          ],
-          "install_hint": "# WARNING: this installer is experimental\ncurl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-v1.0.0-FAKEVERSION-installer.sh | sh",
-          "description": "Install prebuilt binaries via shell script"
+          "name": "LICENSE-APACHE",
+          "path": "LICENSE-APACHE",
+          "kind": "license"
         },
         {
-          "name": "cargo-dist-v1.0.0-FAKEVERSION-installer.ps1",
-          "kind": "installer",
-          "target_triples": [
-            "x86_64-pc-windows-msvc"
-          ],
-          "install_hint": "# WARNING: this installer is experimental\nirm https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-v1.0.0-FAKEVERSION-installer.ps1 | iex",
-          "description": "Install prebuilt binaries via powershell script"
+          "name": "LICENSE-MIT",
+          "path": "LICENSE-MIT",
+          "kind": "license"
         },
         {
-          "name": "cargo-dist-v1.0.0-FAKEVERSION-x86_64-apple-darwin.tar.xz",
-          "kind": "executable-zip",
-          "target_triples": [
-            "x86_64-apple-darwin"
-          ],
-          "assets": [
-            {
-              "name": "LICENSE-APACHE",
-              "path": "LICENSE-APACHE",
-              "kind": "license"
-            },
-            {
-              "name": "LICENSE-MIT",
-              "path": "LICENSE-MIT",
-              "kind": "license"
-            },
-            {
-              "name": "README.md",
-              "path": "README.md",
-              "kind": "readme"
-            },
-            {
-              "name": "RELEASES.md",
-              "path": "RELEASES.md",
-              "kind": "changelog"
-            },
-            {
-              "name": "cargo-dist",
-              "path": "cargo-dist",
-              "kind": "executable"
-            }
-          ]
+          "name": "README.md",
+          "path": "README.md",
+          "kind": "readme"
         },
         {
-          "name": "cargo-dist-v1.0.0-FAKEVERSION-x86_64-pc-windows-msvc.zip",
-          "kind": "executable-zip",
-          "target_triples": [
-            "x86_64-pc-windows-msvc"
-          ],
-          "assets": [
-            {
-              "name": "LICENSE-APACHE",
-              "path": "LICENSE-APACHE",
-              "kind": "license"
-            },
-            {
-              "name": "LICENSE-MIT",
-              "path": "LICENSE-MIT",
-              "kind": "license"
-            },
-            {
-              "name": "README.md",
-              "path": "README.md",
-              "kind": "readme"
-            },
-            {
-              "name": "RELEASES.md",
-              "path": "RELEASES.md",
-              "kind": "changelog"
-            },
-            {
-              "name": "cargo-dist",
-              "path": "cargo-dist.exe",
-              "kind": "executable"
-            }
-          ]
+          "name": "RELEASES.md",
+          "path": "RELEASES.md",
+          "kind": "changelog"
         },
         {
-          "name": "cargo-dist-v1.0.0-FAKEVERSION-x86_64-unknown-linux-gnu.tar.xz",
-          "kind": "executable-zip",
-          "target_triples": [
-            "x86_64-unknown-linux-gnu"
-          ],
-          "assets": [
-            {
-              "name": "LICENSE-APACHE",
-              "path": "LICENSE-APACHE",
-              "kind": "license"
-            },
-            {
-              "name": "LICENSE-MIT",
-              "path": "LICENSE-MIT",
-              "kind": "license"
-            },
-            {
-              "name": "README.md",
-              "path": "README.md",
-              "kind": "readme"
-            },
-            {
-              "name": "RELEASES.md",
-              "path": "RELEASES.md",
-              "kind": "changelog"
-            },
-            {
-              "name": "cargo-dist",
-              "path": "cargo-dist",
-              "kind": "executable"
-            }
-          ]
+          "name": "cargo-dist",
+          "path": "cargo-dist",
+          "kind": "executable"
         }
       ]
     }
-  ]
+  }
 }
 
 stderr: