new: add pkh put, a native dput replacement for PPA uploads

Upload built source packages over SFTP with host-key verification
(Launchpad fingerprints pinned in host_keys.yml, ask-to-accept
otherwise), Launchpad account discovery (git config lp.user), and
pre-flight checks the upload queue itself never does: changes file
discovery/validation, PPA existence via the Launchpad API, target
series validity, and debian/control Section validity (sections
bundled in distro_info.yml). Upload log prevents duplicate uploads
unless --force.
This commit is contained in:
2026-09-16 21:38:53 +02:00
parent 9228ff448b
commit f27d27ea99
13 changed files with 2023 additions and 97 deletions
+29
View File
@@ -33,6 +33,8 @@ struct DistData {
base_url: String,
archive_keyring: String,
pockets: Vec<String>,
#[serde(default)]
sections: Vec<String>,
series: SeriesInfo,
}
@@ -316,6 +318,20 @@ pub fn get_dist_pockets(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
Ok(pockets)
}
/// Get the valid `Section` values of a distribution's packages, as accepted
/// by its archives (a `section/subsection` in debian/control validates on
/// the part before the '/')
pub fn get_sections(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
let dist_data = DATA.dist.get(dist).ok_or_else(|| {
format!(
"Unknown distribution '{}'. Supported distributions are: {}.",
dist,
supported_dists().join(", ")
)
})?;
Ok(dist_data.sections.clone())
}
/// Get the sources URL for a distribution, series, pocket, and component
pub fn get_sources_url(base_url: &str, series: &str, pocket: &str, component: &str) -> String {
let pocket_full = if pocket.is_empty() {
@@ -487,6 +503,19 @@ pub async fn get_debian_series_number(series: &str) -> Result<Option<String>, Bo
mod tests {
use super::*;
#[test]
fn test_get_sections() {
// Both distributions bundle the policy section list
for dist in ["debian", "ubuntu"] {
let sections = get_sections(dist).unwrap();
assert!(sections.contains(&"utils".to_string()));
assert!(sections.contains(&"devel".to_string()));
// 'unknown' is exactly what archives reject
assert!(!sections.contains(&"unknown".to_string()));
}
assert!(get_sections("not-a-distro").is_err());
}
#[test]
fn test_parse_series_csv_malformed_rows() {
// A short row (missing 'codename') is skipped, a row with an invalid