Skip to content

Commit d0d55b0

Browse files
authored
chore(vdev): add deprecation fragment commands (#25638)
1 parent 0bcb4c6 commit d0d55b0

11 files changed

Lines changed: 1223 additions & 19 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#![allow(clippy::print_stdout)]
2+
#![allow(clippy::print_stderr)]
3+
4+
use anyhow::{Result, bail};
5+
use semver::Version;
6+
7+
use crate::utils::{deprecation, git, paths};
8+
9+
/// Check deprecation.d fragments are valid and that generated/deprecations.json is up to date
10+
#[derive(clap::Args, Debug)]
11+
#[command()]
12+
pub struct Cli {}
13+
14+
impl Cli {
15+
pub fn exec(self) -> Result<()> {
16+
let repo_root = paths::find_repo_root()?;
17+
let dir = repo_root.join(deprecation::DEPRECATION_DIR);
18+
19+
let json_path = repo_root.join(deprecation::DEPRECATIONS_JSON);
20+
let entries = if dir.is_dir() {
21+
deprecation::read_deprecation_fragments(&dir)?
22+
} else if json_path.is_file() {
23+
println!(
24+
"{} not found; validating generated JSON only.",
25+
dir.display()
26+
);
27+
Vec::new()
28+
} else {
29+
bail!(
30+
"Neither {} nor {} found; the deprecation fragment system is not installed in this repo.",
31+
dir.display(),
32+
json_path.display()
33+
);
34+
};
35+
36+
if entries.is_empty() {
37+
println!("No deprecation fragments found.");
38+
} else {
39+
for entry in &entries {
40+
println!(" ok {}", entry.filename);
41+
}
42+
println!("{} deprecation fragment(s) are valid.", entries.len());
43+
}
44+
45+
// Reject any fragment with a deprecated_since newer than the next
46+
// minor release. Skipped (with a warning) when the checkout has no
47+
// release tags so shallow CI/source checkouts can still validate
48+
// fragment frontmatter + generated JSON.
49+
match git::latest_release_version() {
50+
Ok(latest) => {
51+
let next_minor = Version::new(latest.major, latest.minor + 1, 0);
52+
let future: Vec<_> = entries
53+
.iter()
54+
.filter(|e| e.deprecated_since.0 > next_minor)
55+
.collect();
56+
if !future.is_empty() {
57+
for e in &future {
58+
eprintln!(
59+
" future {} (deprecated_since: {}, next release: {}.{})",
60+
e.filename, e.deprecated_since, next_minor.major, next_minor.minor
61+
);
62+
}
63+
bail!(
64+
"{} fragment(s) have a deprecated_since version newer than the next release ({}.{}). \
65+
Update deprecated_since to {} or earlier.",
66+
future.len(),
67+
next_minor.major,
68+
next_minor.minor,
69+
next_minor
70+
);
71+
}
72+
}
73+
Err(e) => {
74+
eprintln!(
75+
"Warning: skipping future-version validation; could not determine latest release version: {e}"
76+
);
77+
}
78+
}
79+
80+
let on_disk = std::fs::read_to_string(&json_path).unwrap_or_default();
81+
let expected = deprecation::rendered_json(&repo_root)?;
82+
if on_disk != expected {
83+
bail!(
84+
"{} is out of date. Run `cargo vdev deprecation generate` and commit the result.",
85+
json_path.display()
86+
);
87+
}
88+
89+
let enacted_count = deprecation::validate_enacted(&repo_root)?;
90+
println!(
91+
"{} is up to date ({} enacted entries valid).",
92+
json_path.display(),
93+
enacted_count
94+
);
95+
96+
Ok(())
97+
}
98+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#![allow(clippy::print_stdout)]
2+
3+
use anyhow::{Result, bail};
4+
use semver::Version;
5+
6+
use crate::utils::{deprecation, git, paths};
7+
8+
/// Enact a deprecation: record it as removed and delete the deprecation.d fragment
9+
#[derive(clap::Args, Debug)]
10+
#[command()]
11+
pub struct Cli {
12+
/// Filename (slug) in deprecation.d/ to enact, e.g. "azure-monitor-logs-sink"
13+
slug: String,
14+
15+
/// The Vector version in which this feature was removed, e.g. "0.58.0".
16+
/// Defaults to the next minor after the latest git tag.
17+
#[arg(long)]
18+
version: Option<Version>,
19+
}
20+
21+
impl Cli {
22+
pub fn exec(self) -> Result<()> {
23+
let repo_root = paths::find_repo_root()?;
24+
let dir = repo_root.join(deprecation::DEPRECATION_DIR);
25+
26+
// Accept "slug" or "slug.md"; reject path-like inputs to keep the
27+
// identifier unambiguous (and the file lookup safe).
28+
if self.slug.contains('/') || self.slug.contains('\\') {
29+
bail!(
30+
"expected a deprecation slug (e.g. \"azure-monitor-logs-sink\"), not a path: {}",
31+
self.slug
32+
);
33+
}
34+
let stem = self.slug.strip_suffix(".md").unwrap_or(&self.slug);
35+
let filename = format!("{stem}.md");
36+
37+
let path = dir.join(&filename);
38+
if !path.exists() {
39+
bail!("No deprecation fragment found at {}", path.display());
40+
}
41+
42+
// Parse the fragment to get entry data.
43+
let entries = deprecation::read_deprecation_fragments(&dir)?;
44+
let entry = entries
45+
.into_iter()
46+
.find(|e| e.filename == filename)
47+
.ok_or_else(|| anyhow::anyhow!("Could not parse {filename}"))?;
48+
49+
let version = if let Some(v) = self.version {
50+
// Reject `--version 0.58.0-alpha` and friends: only release-shaped
51+
// semver is valid here.
52+
if !v.pre.is_empty() || !v.build.is_empty() {
53+
bail!(
54+
"--version {v} has prerelease or build metadata; only plain X.Y.Z is allowed"
55+
);
56+
}
57+
v
58+
} else {
59+
let latest = git::latest_release_version()?;
60+
Version::new(latest.major, latest.minor + 1, 0)
61+
};
62+
63+
if !deprecation::later_minor(&version, &entry.deprecated_since.0) {
64+
bail!(
65+
"removed_in ({version}) must be in a later minor release than deprecated_since ({}); \
66+
the deprecation policy requires at least one minor release between \
67+
the announcement and removal. \
68+
Check --version or the fragment's `deprecated_since` field.",
69+
entry.deprecated_since
70+
);
71+
}
72+
73+
let enacted = deprecation::EnactedEntry {
74+
what: entry.what.clone(),
75+
deprecated_since: entry.deprecated_since.to_string(),
76+
removed_in: version.to_string(),
77+
description: entry.description.clone(),
78+
};
79+
80+
// Append to enacted JSON.
81+
deprecation::append_enacted(&repo_root, enacted)?;
82+
println!("Recorded enacted entry for: {}", entry.what);
83+
84+
// Delete the deprecation.d fragment.
85+
std::fs::remove_file(&path)?;
86+
println!("Deleted {}", path.display());
87+
88+
// Regenerate deprecations.cue.
89+
deprecation::sync_deprecations_cue(&repo_root)?;
90+
println!(
91+
"Updated {}",
92+
repo_root.join(deprecation::DEPRECATIONS_JSON).display()
93+
);
94+
95+
Ok(())
96+
}
97+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#![allow(clippy::print_stdout)]
2+
3+
use anyhow::{Result, bail};
4+
5+
use crate::utils::{deprecation, paths};
6+
7+
/// Regenerate generated/deprecations.json from deprecation.d/ fragments
8+
#[derive(clap::Args, Debug)]
9+
#[command()]
10+
pub struct Cli {}
11+
12+
impl Cli {
13+
pub fn exec(self) -> Result<()> {
14+
let repo_root = paths::find_repo_root()?;
15+
let dir = repo_root.join(deprecation::DEPRECATION_DIR);
16+
let json_path = repo_root.join(deprecation::DEPRECATIONS_JSON);
17+
// If neither input exists, the fragment system isn't installed. With
18+
// either present, we can still produce or refresh the JSON (an empty
19+
// pending list is valid once all fragments have been enacted).
20+
if !dir.is_dir() && !json_path.is_file() {
21+
bail!(
22+
"Neither {} nor {} found; the deprecation fragment system is not installed in this repo.",
23+
dir.display(),
24+
json_path.display()
25+
);
26+
}
27+
deprecation::sync_deprecations_cue(&repo_root)?;
28+
println!("Updated {}", json_path.display());
29+
Ok(())
30+
}
31+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
mod check;
2+
mod enact;
3+
mod generate;
4+
mod show;
5+
6+
crate::cli_subcommands! {
7+
"Manage and inspect deprecation notices..."
8+
check,
9+
enact,
10+
generate,
11+
show,
12+
}

0 commit comments

Comments
 (0)