Repository: axodotdev/cargo-dist
Pull request #50 (2023-01-24 17:02:58)

Properly compute a bunch of facts about each package
And use this to grab more static assets. More facts are computed than are used right now, I'm not 100% sure yet which fields we want to surface and on what level of abstraction, but whenever we *do* decide to do that they're ready to go


Patch:
diff --git a/README.md b/README.md
index a3dd70c4..79fd0b56 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,31 @@ That said, the current version is Very Very Unstable And Experimental and the ex
 
 # Way-Too-Quick Start
 
+
+```sh
+# install tools
+cargo install cargo-dist
+
+# one-time setup
+cargo dist init --ci=github
+git add .
+git commit -am "wow shiny new cargo-dist CI!"
+
+# cut a release like you normally would
+
+# <manually update the version of your crate, run tests, etc>
+# then:
+git commit -am "chore: Release version 0.1.0"
+git tag v0.1.0
+cargo publish
+git push
+git push --tags
+```
+
+That's gonna do a whole bunch of stuff you might not have expected, but if it all works you'll get a Github Release™️ with built and zipped artifacts uploaded to it! Read the rest of the docs to learn more!
+
+You may have noticed "cut a release" still has a lot of tedious work. That's because we recommend using [cargo-release](https://github.com/crate-ci/cargo-release) to streamline the last step:
+
 ```sh
 # install tools
 cargo install cargo-dist
@@ -54,8 +79,7 @@ git commit -am "wow shiny new cargo-dist CI!"
 cargo release 0.1.0
 ```
 
-That's gonna do a whole bunch of stuff you might not have expected (but I left off the --execute flag from `cargo-release` so you won't actually break anything if you really did just copy paste that 😇). Read the rest of the docs to know what it does!
-
+(I left off the --execute flag from `cargo-release` so you won't actually break anything if you really did just copy paste that 😇)
 
 
 
@@ -75,7 +99,7 @@ cargo binstall cargo-dist --no-symlinks
 ## Build From Source With Cargo
 
 ```sh
-cargo install dist --profile=dist
+cargo install cargo-dist --profile=dist
 ```
 
 (`--profile=dist` may get you a slightly more optimized binary.)
@@ -168,10 +192,21 @@ Other commands like `cargo dist build` (bare `cargo dist`) will always default t
 
 Once you've completed setup (run `cargo dist init --ci=...`), you're ready to start cutting releases!
 
-The github workflow will trigger whenever you push a [git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging)
-to the main branch of your repository that looks like a version number (`v1`, `v1.2.0`, `v0.1.0-prerelease2`, etc.).
+The github workflow will trigger whenever you push a [git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) to the main branch of your repository that looks like a version number (`v1`, `v1.2.0`, `v0.1.0-prerelease2`, etc.).
 
-The easiest way to do this is to use [cargo-release](https://github.com/crate-ci/cargo-release):
+You might do that with something like this:
+
+```sh
+# <first manually update the version of your crate, run tests, etc>
+# then:
+git commit -am "chore: Release version 0.1.0"
+git tag v0.1.0
+cargo publish
+git push
+git push --tags
+```
+
+That's a bunch of junk to remember to do, so we recommend using [cargo-release](https://github.com/crate-ci/cargo-release) to streamline all of that:
 
 ```sh
 cargo release 0.1.0
@@ -189,9 +224,7 @@ cargo-release will then automatically:
 4. Publish to crates.io (disable this with `--no-publish`)
 4. Push to your repo's main branch
 
-You don't actually need to use cargo-release, it's just a great tool for streamlining your release process that we recommend everyone using!
-
-When you *do* push a tag the CI will take over and do the following:
+When you *do* push a tag (and the commit it points to) the CI will take over and do the following:
 
 1. Create a *draft* Github Release™️ (with [taiki-e/create-github-release-action](https://github.com/taiki-e/create-gh-release-action))
 2. Build your application for all the target platforms, wrap them in zips/tars, and upload them to the Github Release™️
diff --git a/RELEASES.md b/RELEASES.md
index abd5507c..8cd72c5f 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -1,3 +1,14 @@
+# Next Version (unreleased)
+
+cargo-dist:
+
+* Added proper detection of README/LICENSE/RELEASES/CHANGELOG files, which are now copied to the root of executable-zips.
+
+cargo-dist-schema:
+
+* Changed PathBufs to Strings since the paths may be from a different OS and Rust Paths are generally platform-specific. Seemed like a ticking timebomb for some weird corner case.
+* Added "changelog" as a valid AssetKind
+
 # Version 0.0.1 (2023-01-23)
 
 This is the first alpha release of cargo-dist with some minimal functionality!
diff --git a/cargo-dist-schema/cargo-dist-json-schema.json b/cargo-dist-schema/cargo-dist-json-schema.json
index c5ab99c3..42261228 100644
--- a/cargo-dist-schema/cargo-dist-json-schema.json
+++ b/cargo-dist-schema/cargo-dist-json-schema.json
@@ -194,6 +194,21 @@
             }
           }
         },
+        {
+          "description": "A CHANGELOG or RELEASES file",
+          "type": "object",
+          "required": [
+            "kind"
+          ],
+          "properties": {
+            "kind": {
+              "type": "string",
+              "enum": [
+                "changelog"
+              ]
+            }
+          }
+        },
         {
           "description": "Unknown to this version of cargo-dist-schema\n\nThis is a fallback for forward/backward-compat",
           "type": "object",
diff --git a/cargo-dist-schema/src/lib.rs b/cargo-dist-schema/src/lib.rs
index 337b7d06..484342b9 100644
--- a/cargo-dist-schema/src/lib.rs
+++ b/cargo-dist-schema/src/lib.rs
@@ -10,8 +10,17 @@
 
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-// FIXME: ideally these would be UTF8PathBufs but JsonSchema doesn't support (yet?)
-use std::path::PathBuf;
+
+/// A local system path on the machine cargo-dist was run.
+///
+/// This is a String because when deserializing this may be a path format from a different OS!
+pub type LocalPath = String;
+/// A relative path inside an artifact
+///
+/// This is a String because when deserializing this may be a path format from a different OS!
+///
+/// (Should we normalize this one?)
+pub type RelPath = String;
 
 /// A report of the releases and artifacts that cargo-dist generated
 #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
@@ -57,7 +66,7 @@ pub struct Artifact {
     /// The location of the artifact on the local system
     #[serde(skip_serializing_if = "Option::is_none")]
     #[serde(default)]
-    pub path: Option<PathBuf>,
+    pub path: Option<LocalPath>,
     /// Assets included in the bundle (like executables and READMEs)
     #[serde(skip_serializing_if = "Vec::is_empty")]
     #[serde(default)]
@@ -78,7 +87,7 @@ pub struct Asset {
     /// The path of the asset relative to the root of the artifact
     #[serde(default)]
     #[serde(skip_serializing_if = "Option::is_none")]
-    pub path: Option<PathBuf>,
+    pub path: Option<RelPath>,
     /// The kind of asset this is
     #[serde(flatten)]
     pub kind: AssetKind,
@@ -98,6 +107,9 @@ pub enum AssetKind {
     /// A LICENSE file
     #[serde(rename = "license")]
     License,
+    /// A CHANGELOG or RELEASES file
+    #[serde(rename = "changelog")]
+    Changelog,
     /// Unknown to this version of cargo-dist-schema
     ///
     /// This is a fallback for forward/backward-compat
@@ -160,6 +172,7 @@ fn emit() {
     use std::fs::File;
     use std::io::BufWriter;
     use std::io::Write;
+    use std::path::PathBuf;
 
     let schema = DistManifest::json_schema();
     let json_schema = serde_json::to_string_pretty(&schema).unwrap();
@@ -174,5 +187,5 @@ fn emit() {
         .open(schema)
         .unwrap();
     let mut file = BufWriter::new(file);
-    writeln!(&mut file, "{}", json_schema).unwrap();
+    writeln!(&mut file, "{json_schema}").unwrap();
 }
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 85deed7b..ff7a6878 100644
--- a/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
+++ b/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
@@ -198,6 +198,21 @@ expression: json_schema
             }
           }
         },
+        {
+          "description": "A CHANGELOG or RELEASES file",
+          "type": "object",
+          "required": [
+            "kind"
+          ],
+          "properties": {
+            "kind": {
+              "type": "string",
+              "enum": [
+                "changelog"
+              ]
+            }
+          }
+        },
         {
           "description": "Unknown to this version of cargo-dist-schema\n\nThis is a fallback for forward/backward-compat",
           "type": "object",
diff --git a/cargo-dist/Cargo.toml b/cargo-dist/Cargo.toml
index 1df96597..1b53d4ee 100644
--- a/cargo-dist/Cargo.toml
+++ b/cargo-dist/Cargo.toml
@@ -5,6 +5,7 @@ version = "0.0.1"
 edition = "2021"
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/axodotdev/cargo-dist"
+readme = "../README.md"
 exclude = [
   "book/*",
   "src/snapshots/*",
diff --git a/cargo-dist/src/lib.rs b/cargo-dist/src/lib.rs
index 9420d188..95c93068 100644
--- a/cargo-dist/src/lib.rs
+++ b/cargo-dist/src/lib.rs
@@ -47,9 +47,6 @@ const METADATA_DIST: &str = "dist";
 const TARGET_DIST: &str = "distrib";
 /// The profile we will build with
 const PROFILE_DIST: &str = "dist";
-/// Some files we'll try to grab.
-//TODO: LICENSE-* files, somehow!
-const BUILTIN_FILES: &[&str] = &["README.md", "CHANGELOG.md", "RELEASES.md"];
 
 /// The key for referring to linux as an "os"
 const OS_LINUX: &str = "linux";
@@ -247,7 +244,7 @@ pub(crate) struct ArtifactTarget {
     /// Additional static assets to add to the artifact
     ///
     /// i.e. `README.md`
-    static_assets: Vec<Utf8PathBuf>,
+    static_assets: Vec<(StaticAssetKind, Utf8PathBuf)>,
     /// The kind of artifact this is
     kind: ArtifactKind,
 }
@@ -263,6 +260,16 @@ struct ReleaseTarget {
     artifacts: Vec<ArtifactTargetIdx>,
 }
 
+#[derive(Debug)]
+enum StaticAssetKind {
+    /// A README file
+    Readme,
+    /// A LICENSE file
+    License,
+    /// A CHANGLEOG or RELEASES file
+    Changelog,
+}
+
 /// The style of bundle for a [`ArtifactTarget`][].
 #[derive(Debug)]
 enum BundleStyle {
@@ -467,12 +474,9 @@ fn gather_work(cfg: &Config) -> Result<DistGraph> {
         // TODO: make app name configurable? Use some other fields in the PackageMetadata?
         let app_name = exe.exe_name.clone();
         let package_id = exe.package_id.clone();
+        let package_info = &workspace.package_info[&&package_id];
         // TODO: allow apps to be versioned separately from packages?
-        let version = pkg_graph
-            .metadata(&exe.package_id)
-            .unwrap()
-            .version()
-            .clone();
+        let version = package_info.version.clone();
         let build_target = &mut targets[exe.build_target.0];
 
         // Register this executable as an asset we'll build
@@ -496,13 +500,16 @@ fn gather_work(cfg: &Config) -> Result<DistGraph> {
 
         // TODO: make bundled assets configurable
         // TODO: narrow this scope to the package of the binary..?
-        let exe_static_assets = BUILTIN_FILES
-            .iter()
-            .filter_map(|f| {
-                let file = workspace_dir.join(f);
-                file.exists().then_some(file)
-            })
-            .collect();
+        let mut exe_static_assets = vec![];
+        if let Some(readme) = &package_info.readme_file {
+            exe_static_assets.push((StaticAssetKind::Readme, readme.clone()));
+        }
+        if let Some(changelog) = &package_info.changelog_file {
+            exe_static_assets.push((StaticAssetKind::Changelog, changelog.clone()));
+        }
+        for license in &package_info.license_files {
+            exe_static_assets.push((StaticAssetKind::License, license.clone()));
+        }
 
         // TODO: make the bundle name configurable?
         let exe_dir_name = format!("{app_name}-v{version}-{target_triple}");
@@ -604,10 +611,8 @@ fn gather_work(cfg: &Config) -> Result<DistGraph> {
 
     // Add installers (currently all 1:1 with releases rather than targets)
     for ((app_name, version), (package_id, release)) in &mut releases {
-        let repo = pkg_graph
-            .metadata(package_id)
-            .ok()
-            .and_then(|pkg| pkg.repository());
+        let package_info = &workspace.package_info[&&*package_id];
+        let repo = package_info.repository_url.as_deref();
         for installer in &cfg.installers {
             let file_path;
             let file_name;
@@ -627,6 +632,7 @@ fn gather_work(cfg: &Config) -> Result<DistGraph> {
                     installer_impl = InstallerImpl::GithubShell(InstallerInfo {
                         app_name: app_name.clone(),
                         app_version: format!("v{version}"),
+                        // FIXME: we should probably actually report an error instead of defaulting?
                         repo_url: repo.unwrap_or("").to_owned(),
                     });
                 }
@@ -638,6 +644,7 @@ fn gather_work(cfg: &Config) -> Result<DistGraph> {
                     installer_impl = InstallerImpl::GithubPowershell(InstallerInfo {
                         app_name: app_name.clone(),
                         app_version: format!("v{version}"),
+                        // FIXME: we should probably actually report an error instead of defaulting?
                         repo_url: repo.unwrap_or("").to_owned(),
                     });
                 }
@@ -684,12 +691,64 @@ fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistManifest {
                 .iter()
                 .map(|artifact_idx| {
                     let artifact = &dist.artifacts[artifact_idx.0];
+                    let mut assets = vec![];
+
+                    let built_assets = artifact
+                        .built_assets
+                        .iter()
+                        .filter_map(|(asset_idx, asset_path)| {
+                            let asset = &dist.built_assets[asset_idx.0];
+                            match asset {
+                                BuiltAsset::Executable(exe) => {
+                                    let symbols_artifact = exe.symbols_artifact.map(|a| {
+                                        dist.artifacts[a.0]
+                                            .file_path
+                                            .file_name()
+                                            .unwrap()
+                                            .to_owned()
+                                    });
+                                    Some(Asset {
+                                        name: Some(exe.exe_name.clone()),
+                                        path: Some(asset_path.to_string()),
+                                        kind: AssetKind::Executable(ExecutableAsset {
+                                            symbols_artifact,
+                                        }),
+                                    })
+                                }
+                                BuiltAsset::Symbols(_sym) => {
+                                    // Symbols are their own assets, so no need to report
+                                    None
+                                }
+                            }
+                        });
+
+                    let static_assets = artifact
+                        .static_assets
+                        .iter()
+                        .map(|(kind, asset)| {
+                            let kind = match kind {
+                                StaticAssetKind::Changelog => AssetKind::Changelog,
+                                StaticAssetKind::License => AssetKind::License,
+                                StaticAssetKind::Readme => AssetKind::Readme,
+                            };
+                            Asset {
+                                name: Some(asset.file_name().unwrap().to_owned()),
+                                path: Some(asset.file_name().unwrap().to_owned()),
+                                kind,
+                            }
+                        });
+
+                    assets.extend(built_assets);
+                    assets.extend(static_assets);
+                    // Sort the assets by name to make things extra stable
+                    assets.sort_by(|k1, k2| k1.name.cmp(&k2.name));
+
                     Artifact {
                         name: artifact.file_path.file_name().unwrap().to_owned(),
                         path: if cfg.no_local_paths {
                             None
                         } else {
-                            Some(artifact.file_path.clone().into_std_path_buf())
+                            Some(artifact.file_path.to_string())
                         },
                         target_triples: artifact.target_triples.clone(),
                         install_hint: match &artifact.bundle {
@@ -707,35 +766,7 @@ fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistManifest {
                             BundleStyle::Tar(_) => None,
                             BundleStyle::UncompressedFile => None,
                         },
-                        assets: artifact
-                            .built_assets
-                            .iter()
-                            .filter_map(|(asset_idx, asset_path)| {
-                                let asset = &dist.built_assets[asset_idx.0];
-                                match asset {
-                                    BuiltAsset::Executable(exe) => {
-                                        let symbols_artifact = exe.symbols_artifact.map(|a| {
-                                            dist.artifacts[a.0]
-                                                .file_path
-                                                .file_name()
-                                                .unwrap()
-                                                .to_owned()
-                                        });
-                                        Some(Asset {
-                                            name: Some(exe.exe_name.clone()),
-                                            path: Some(asset_path.clone().into_std_path_buf()),
-                                            kind: AssetKind::Executable(ExecutableAsset {
-                                                symbols_artifact,
-                                            }),
-                                        })
-                                    }
-                                    BuiltAsset::Symbols(_sym) => {
-                                        // Symbols are their own assets, so no need to report
-                                        None
-                                    }
-                                }
-                            })
-                            .collect(),
+                        assets,
                         kind: artifact.kind.clone(),
                     }
                 })
@@ -1121,7 +1152,7 @@ fn populate_artifact_dir_with_static_assets(
 
     info!("populating artifact dir: {}", artifact_dir_path);
     // Copy assets
-    for asset in &artifact.static_assets {
+    for (_kind, asset) in &artifact.static_assets {
         let asset_file_name = asset.file_name().unwrap();
         let bundled_asset = artifact_dir_path.join(asset_file_name);
         info!("  adding {bundled_asset}");
@@ -1327,6 +1358,11 @@ struct WorkspaceInfo<'pkg_graph> {
     info: Workspace<'pkg_graph>,
     /// The workspace members.
     members: PackageSet<'pkg_graph>,
+    /// Computed info about the packages beyond what Guppy tells us
+    ///
+    /// This notably includes finding readmes and licenses even if the user didn't
+    /// specify their location -- something Cargo does but Guppy (and cargo-metadata) don't.
+    package_info: HashMap<&'pkg_graph PackageId, PackageInfo>,
     /// Path to the Cargo.toml of the workspace (may be a package's Cargo.toml)
     manifest_path: Utf8PathBuf,
     /// If the manifest_path points to a package, this is the one.
@@ -1350,14 +1386,181 @@ fn workspace_info(pkg_graph: &PackageGraph) -> Result<WorkspaceInfo> {
         .packages(DependencyDirection::Forward)
         .find(|p| p.manifest_path() == manifest_path);
 
+    let workspace_root = workspace.root();
+    let mut package_info = HashMap::new();
+    for package in members.packages(DependencyDirection::Forward) {
+        let info = compute_package_info(workspace_root, &package)?;
+        package_info.insert(package.id(), info);
+    }
+
     Ok(WorkspaceInfo {
         info: workspace,
         members,
+        package_info,
         manifest_path,
         root_package,
     })
 }
 
+/// Computed info about the packages beyond what Guppy tells us
+///
+/// This notably includes finding readmes and licenses even if the user didn't
+/// specify their location -- something Cargo does but Guppy (and cargo-metadata) don't.
+#[derive(Debug)]
+struct PackageInfo {
+    /// Name of the package
+    name: String,
+    /// Version of the package
+    version: Version,
+    /// A brief description of the package
+    description: Option<String>,
+    /// Authors of the package (may be empty)
+    authors: Vec<String>,
+    /// The license the package is provided under
+    license: Option<String>,
+    /// URL to the repository for this package
+    ///
+    /// This URL can be used by various CI/Installer helpers. In the future we
+    /// might also use it for auto-detecting "hey you're using github, here's the
+    /// recommended github setup".
+    ///
+    /// i.e. `--installer=github-shell` uses this as the base URL for fetching from
+    /// a Github Release™️.
+    repository_url: Option<String>,
+    /// URL to the homepage for this package.
+    ///
+    /// Currently this isn't terribly important or useful?
+    homepage_url: Option<String>,
+    /// URL to the documentation for this package.
+    ///
+    /// This will default to docs.rs if not specified, which is the default crates.io behaviour.
+    ///
+    /// Currently this isn't terribly important or useful?
+    documentation_url: Option<String>,
+    /// Path to the README file for this package.
+    ///
+    /// By default this should be copied into a zip containing this package's binary.
+    readme_file: Option<Utf8PathBuf>,
+    /// Paths to the LICENSE files for this package.
+    ///
+    /// By default these should be copied into a zip containing this package's binary.
+    ///
+    /// Cargo only lets you specify one such path, but that's because the license path
+    /// primarily exists as an escape hatch for someone's whacky-wild custom license.
+    /// But for our usecase we want to find those paths even if they're bog standard
+    /// MIT/Apache, which traditionally involves two separate license files.
+    license_files: Vec<Utf8PathBuf>,
+    /// Paths to the CHANGELOG or RELEASES file for this package
+    ///
+    /// By default this should be copied into a zip containing this package's binary.
+    ///
+    /// We will *try* to parse this
+    changelog_file: Option<Utf8PathBuf>,
+}
+
+fn compute_package_info(
+    workspace_root: &Utf8Path,
+    package: &PackageMetadata,
+) -> Result<PackageInfo> {
+    let mut info = PackageInfo {
+        name: package.name().to_owned(),
+        version: package.version().to_owned(),
+        description: package.description().map(ToOwned::to_owned),
+        authors: package.authors().to_vec(),
+        license: package.license().map(ToOwned::to_owned),
+        repository_url: package.repository().map(ToOwned::to_owned),
+        homepage_url: package.homepage().map(ToOwned::to_owned),
+        documentation_url: package.documentation().map(ToOwned::to_owned),
+        readme_file: package.readme().map(ToOwned::to_owned),
+        license_files: package
+            .license_file()
+            .map(ToOwned::to_owned)
+            .into_iter()
+            .collect(),
+        changelog_file: None,
+    };
+
+    // We don't want to search for any license files if one is manually given
+    // (need to check that here since we can find multiple licenses).
+    let search_for_license_file = info.license_files.is_empty();
+
+    // If there's no documentation URL provided, default assume it's docs.rs like crates.io does
+    if info.documentation_url.is_none() {
+        info.documentation_url = Some(format!("https://docs.rs/{}/{}", info.name, info.version));
+    }
+
+    // Is there a better way to get the path to sniff?
+    // Should we spider more than just package_root and workspace_root?
+    // Should we more carefully prevent grabbing LICENSES from both dirs?
+    // Should we not spider the workspace root for README since Cargo has a proper field for this?
+    // Should we check for a "readme=..." on the workspace root Cargo.toml?
+    let manifest_path = package.manifest_path();
+    let package_root = manifest_path
+        .parent()
+        .expect("package manifest had no parent!?");
+
+    for dir in &[package_root, workspace_root] {
+        let entries = dir
+            .read_dir_utf8()
+            .into_diagnostic()
+            .wrap_err("Failed to read workspace dir")?;
+        for entry in entries {
+            let entry = entry
+                .into_diagnostic()
+                .wrap_err("Failed to read workspace dir entry")?;
+            let meta = entry
+                .file_type()
+                .into_diagnostic()
+                .wrap_err("Failed to read workspace dir entry's metadata")?;
+            if meta.is_file() {
+                if entry.file_name().starts_with("README") {
+                    if info.readme_file.is_none() {
+                        let path = entry.path().to_owned();
+                        info!("Found README for {}: {}", info.name, path);
+                        info.readme_file = Some(path);
+                    } else {
+                        info!(
+                            "Ignoring candidate README for {}: {}",
+                            info.name,
+                            entry.path()
+                        );
+                    }
+                } else if entry.file_name().starts_with("LICENSE")
+                    || entry.file_name().starts_with("UNLICENSE")
+                {
+                    if search_for_license_file {
+                        let path = entry.path().to_owned();
+                        info!("Found LICENSE for {}: {}", info.name, path);
+                        info.license_files.push(path);
+                    } else {
+                        info!(
+                            "Ignoring candidate LICENSE for {}: {}",
+                            info.name,
+                            entry.path()
+                        );
+                    }
+                } else if entry.file_name().starts_with("CHANGELOG")
+                    || entry.file_name().starts_with("RELEASES")
+                {
+                    if info.changelog_file.is_none() {
+                        let path = entry.path().to_owned();
+                        info!("Found CHANGELOG for {}: {}", info.name, path);
+                        info.changelog_file = Some(path);
+                    } else {
+                        info!(
+                            "Ignoring candidate CHANGELOG for {}: {}",
+                            info.name,
+                            entry.path()
+                        );
+                    }
+                }
+            }
+        }
+    }
+
+    Ok(info)
+}
+
 fn target_symbol_kind(target: &str) -> Option<SymbolKind> {
     #[allow(clippy::if_same_then_else)]
     if target.contains("windows-msvc") {


Test patch:
diff --git a/cargo-dist/tests/snapshots/cli_tests__manifest.snap b/cargo-dist/tests/snapshots/cli_tests__manifest.snap
index 216b1fcd..0c7755c3 100644
--- a/cargo-dist/tests/snapshots/cli_tests__manifest.snap
+++ b/cargo-dist/tests/snapshots/cli_tests__manifest.snap
@@ -24,6 +24,26 @@ stdout:
             "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",
@@ -39,6 +59,26 @@ stdout:
             "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",
@@ -53,6 +93,26 @@ stdout:
             "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",