Skip to content

Commit 2197ea1

Browse files
committed
rust: add command to upload release artifacts
1 parent 33654c8 commit 2197ea1

File tree

2 files changed

+179
-4
lines changed

2 files changed

+179
-4
lines changed

src/github.rs

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ use {
88
octocrab::OctocrabBuilder,
99
once_cell::sync::Lazy,
1010
serde::Deserialize,
11-
std::{collections::HashMap, io::Read, path::PathBuf},
11+
std::{
12+
collections::{BTreeMap, BTreeSet},
13+
io::Read,
14+
path::PathBuf,
15+
},
1216
zip::ZipArchive,
1317
};
1418

15-
static SUFFIXES_BY_TRIPLE: Lazy<HashMap<&'static str, Vec<&'static str>>> = Lazy::new(|| {
16-
let mut h = HashMap::new();
19+
static SUFFIXES_BY_TRIPLE: Lazy<BTreeMap<&'static str, Vec<&'static str>>> = Lazy::new(|| {
20+
let mut h = BTreeMap::new();
1721

1822
// macOS.
1923
let macos_suffixes = vec!["debug", "lto", "pgo", "pgo+lto", "install_only"];
@@ -168,3 +172,115 @@ pub async fn command_fetch_release_distributions(args: &ArgMatches<'_>) -> Resul
168172

169173
Ok(())
170174
}
175+
176+
pub async fn command_upload_release_distributions(args: &ArgMatches<'_>) -> Result<()> {
177+
let dist_dir = PathBuf::from(args.value_of("dist").expect("dist should be specified"));
178+
let datetime = args
179+
.value_of("datetime")
180+
.expect("datetime should be specified");
181+
let tag = args.value_of("tag").expect("tag should be specified");
182+
let ignore_missing = args.is_present("ignore_missing");
183+
let token = args
184+
.value_of("token")
185+
.expect("token should be specified")
186+
.to_string();
187+
let organization = args
188+
.value_of("organization")
189+
.expect("organization should be specified");
190+
let repo = args.value_of("repo").expect("repo should be specified");
191+
192+
let mut filenames = std::fs::read_dir(&dist_dir)?
193+
.into_iter()
194+
.map(|x| {
195+
let path = x?.path();
196+
let filename = path
197+
.file_name()
198+
.ok_or_else(|| anyhow!("unable to resolve file name"))?;
199+
200+
Ok(filename.to_string_lossy().to_string())
201+
})
202+
.collect::<Result<Vec<_>>>()?;
203+
filenames.sort();
204+
205+
let filenames = filenames
206+
.into_iter()
207+
.filter(|x| x.contains(datetime) && x.starts_with("cpython-"))
208+
.collect::<BTreeSet<_>>();
209+
210+
let mut python_versions = BTreeSet::new();
211+
for filename in &filenames {
212+
let parts = filename.split('-').collect::<Vec<_>>();
213+
python_versions.insert(parts[1]);
214+
}
215+
216+
let mut wanted_filenames = BTreeSet::new();
217+
for version in python_versions {
218+
for (triple, suffixes) in SUFFIXES_BY_TRIPLE.iter() {
219+
for suffix in suffixes {
220+
let extension = if suffix.contains("install_only") {
221+
"tar.gz"
222+
} else {
223+
"tar.zst"
224+
};
225+
226+
wanted_filenames.insert(format!(
227+
"cpython-{}-{}-{}-{}.{}",
228+
version, triple, suffix, datetime, extension
229+
));
230+
}
231+
}
232+
}
233+
234+
let missing = wanted_filenames.difference(&filenames).collect::<Vec<_>>();
235+
for f in &missing {
236+
println!("missing release artifact: {}", f);
237+
}
238+
if !missing.is_empty() && !ignore_missing {
239+
return Err(anyhow!("missing release artifacts"));
240+
}
241+
242+
let client = OctocrabBuilder::new().personal_token(token).build()?;
243+
let repo = client.repos(organization, repo);
244+
let releases = repo.releases();
245+
246+
let release = if let Ok(release) = releases.get_by_tag(tag).await {
247+
release
248+
} else {
249+
return Err(anyhow!(
250+
"release {} does not exist; create it via GitHub web UI",
251+
tag
252+
));
253+
};
254+
255+
for filename in wanted_filenames.intersection(&filenames) {
256+
let path = dist_dir.join(filename);
257+
let file_data = std::fs::read(&path)?;
258+
259+
let mut url = release.upload_url.clone();
260+
let path = url.path().to_string();
261+
262+
if let Some(path) = path.strip_suffix("%7B") {
263+
url.set_path(path);
264+
}
265+
266+
url.query_pairs_mut()
267+
.clear()
268+
.append_pair("name", filename.as_str());
269+
270+
println!("uploading {} to {}", filename, url);
271+
272+
let request = client
273+
.request_builder(url, reqwest::Method::POST)
274+
.header("Content-Length", file_data.len())
275+
.header("Content-Type", "application/x-tar")
276+
.body(file_data);
277+
278+
let response = client.execute(request).await?;
279+
280+
if !response.status().is_success() {
281+
return Err(anyhow!("HTTP {}", response.status()));
282+
}
283+
}
284+
285+
Ok(())
286+
}

src/main.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,59 @@ fn main_impl() -> Result<()> {
963963
.help("GitHub repository name"),
964964
),
965965
);
966+
967+
let app = app.subcommand(
968+
SubCommand::with_name("upload-release-distributions")
969+
.about("Upload release distributions to a GitHub release")
970+
.arg(
971+
Arg::with_name("token")
972+
.long("--token")
973+
.required(true)
974+
.takes_value(true)
975+
.help("GitHub API token"),
976+
)
977+
.arg(
978+
Arg::with_name("dist")
979+
.long("--dist")
980+
.required(true)
981+
.takes_value(true)
982+
.help("Directory with release artifacts"),
983+
)
984+
.arg(
985+
Arg::with_name("datetime")
986+
.long("--datetime")
987+
.required(true)
988+
.takes_value(true)
989+
.help("Date/time tag associated with builds"),
990+
)
991+
.arg(
992+
Arg::with_name("tag")
993+
.long("--tag")
994+
.required(true)
995+
.takes_value(true)
996+
.help("Release tag"),
997+
)
998+
.arg(
999+
Arg::with_name("ignore_missing")
1000+
.long("--ignore-missing")
1001+
.help("Continue even if there are missing artifacts"),
1002+
)
1003+
.arg(
1004+
Arg::with_name("organization")
1005+
.long("--org")
1006+
.takes_value(true)
1007+
.default_value("indygreg")
1008+
.help("GitHub organization"),
1009+
)
1010+
.arg(
1011+
Arg::with_name("repo")
1012+
.long("--repo")
1013+
.takes_value(true)
1014+
.default_value("python-build-standalone")
1015+
.help("GitHub repository name"),
1016+
),
1017+
);
1018+
9661019
let app = app.subcommand(
9671020
SubCommand::with_name("validate-distribution")
9681021
.about("Ensure a distribution archive conforms to standards")
@@ -989,7 +1042,13 @@ fn main_impl() -> Result<()> {
9891042
.unwrap()
9901043
.block_on(crate::github::command_fetch_release_distributions(args))
9911044
}
992-
1045+
("upload-release-distributions", Some(args)) => {
1046+
tokio::runtime::Builder::new_current_thread()
1047+
.enable_all()
1048+
.build()
1049+
.unwrap()
1050+
.block_on(crate::github::command_upload_release_distributions(args))
1051+
}
9931052
("validate-distribution", Some(args)) => command_validate_distribution(args),
9941053
_ => Err(anyhow!("invalid sub-command")),
9951054
}

0 commit comments

Comments
 (0)