Skip to content

Commit 0d98ce4

Browse files
authored
Make dry runs exercise the mirror publishing code (#1055)
1 parent e0502bd commit 0d98ce4

File tree

4 files changed

+108
-51
lines changed

4 files changed

+108
-51
lines changed

.github/workflows/release.yml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ jobs:
101101
dist/*.tar.zst
102102
103103
- name: Publish to Astral mirror
104-
if: ${{ github.event.inputs.dry-run == 'false' }}
105104
env:
106105
AWS_ACCESS_KEY_ID: ${{ secrets.MIRROR_R2_ACCESS_KEY_ID }}
107106
AWS_SECRET_ACCESS_KEY: ${{ secrets.MIRROR_R2_SECRET_ACCESS_KEY }}
@@ -110,11 +109,19 @@ jobs:
110109
R2_BUCKET: ${{ secrets.MIRROR_R2_BUCKET_NAME }}
111110
PROJECT: python-build-standalone
112111
VERSION: ${{ github.event.inputs.tag }}
112+
DRY_RUN: ${{ github.event.inputs.dry-run }}
113113
run: |
114-
just release-upload-mirror \
115-
${R2_BUCKET} \
116-
github/${PROJECT}/releases/download/${VERSION}/ \
117-
${VERSION}
114+
if [ "${DRY_RUN}" = 'true' ]; then
115+
just release-upload-mirror-dry-run \
116+
${R2_BUCKET} \
117+
github/${PROJECT}/releases/download/${VERSION}/ \
118+
${VERSION}
119+
else
120+
just release-upload-mirror \
121+
${R2_BUCKET} \
122+
github/${PROJECT}/releases/download/${VERSION}/ \
123+
${VERSION}
124+
fi
118125
119126
publish-versions:
120127
needs: release

Justfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ release-upload-mirror bucket prefix tag:
103103
--bucket {{bucket}} \
104104
--prefix {{prefix}}
105105

106+
# Dry-run the mirror upload without writing to the bucket.
107+
# Requires `release-run` or `release-dry-run` to have been run so that dist/SHA256SUMS exists.
108+
release-upload-mirror-dry-run bucket prefix tag:
109+
uv run python -m pythonbuild.mirror \
110+
--dist dist \
111+
--tag {{tag}} \
112+
--bucket {{bucket}} \
113+
--prefix {{prefix}} \
114+
-n
115+
106116
# Perform the release job. Assumes that the GitHub Release has been created.
107117
release-run token commit tag:
108118
#!/bin/bash

src/github.rs

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -448,61 +448,73 @@ pub async fn command_upload_release_distributions(args: &ArgMatches) -> Result<(
448448
return Err(anyhow!("missing {} release artifacts", missing.len()));
449449
}
450450

451-
let (client, token) = new_github_client(args)?;
452-
let repo_handler = client.repos(organization, repo);
453-
let releases = repo_handler.releases();
451+
let mut digests = BTreeMap::new();
454452

455-
let release = if let Ok(release) = releases.get_by_tag(tag).await {
456-
release
457-
} else {
458-
return if dry_run {
459-
println!("release {tag} does not exist; exiting dry-run mode...");
460-
Ok(())
461-
} else {
462-
Err(anyhow!(
463-
"release {tag} does not exist; create it via GitHub web UI"
464-
))
453+
for (source, dest) in &wanted_filenames {
454+
if !filenames.contains(source) {
455+
continue;
456+
}
457+
458+
let local_filename = dist_dir.join(source);
459+
460+
// Compute digests in a separate pass so we can always materialize
461+
// SHA256SUMS locally before any GitHub interaction, including in dry-run
462+
// mode. This also avoids trying to reuse the streamed upload body for hashing.
463+
let digest = {
464+
let file = tokio::fs::File::open(local_filename).await?;
465+
let mut stream = tokio_util::io::ReaderStream::with_capacity(file, 1048576);
466+
let mut hasher = Sha256::new();
467+
while let Some(chunk) = stream.next().await {
468+
hasher.update(&chunk?);
469+
}
470+
hex::encode(hasher.finalize())
465471
};
466-
};
472+
digests.insert(dest.clone(), digest);
473+
}
467474

468-
let mut digests = BTreeMap::new();
475+
let shasums = digests
476+
.iter()
477+
.map(|(filename, digest)| format!("{digest} {filename}\n"))
478+
.collect::<Vec<_>>()
479+
.join("");
480+
481+
std::fs::write(dist_dir.join("SHA256SUMS"), shasums.as_bytes())?;
482+
483+
if dry_run {
484+
println!("wrote local SHA256SUMS; skipping GitHub upload and verification");
485+
return Ok(());
486+
}
487+
488+
let (client, token) = new_github_client(args)?;
489+
let repo_handler = client.repos(organization, repo);
490+
let releases = repo_handler.releases();
491+
let release = releases
492+
.get_by_tag(tag)
493+
.await
494+
.map_err(|_| anyhow!("release {tag} does not exist; create it via GitHub web UI"))?;
469495

470496
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(5);
471497
let raw_client = Client::new();
472498

473499
{
474500
let mut fs = vec![];
475501

476-
for (source, dest) in wanted_filenames {
477-
if !filenames.contains(&source) {
502+
for (source, dest) in &wanted_filenames {
503+
if !filenames.contains(source) {
478504
continue;
479505
}
480506

481-
let local_filename = dist_dir.join(&source);
507+
let local_filename = dist_dir.join(source);
482508
fs.push(upload_release_artifact(
483509
&raw_client,
484510
&retry_policy,
485511
&GitHubUploadRetryStrategy,
486512
token.clone(),
487513
&release,
488514
dest.clone(),
489-
UploadSource::Filename(local_filename.clone()),
515+
UploadSource::Filename(local_filename),
490516
dry_run,
491517
));
492-
493-
// reqwest wants to take ownership of the body, so it's hard for us to do anything
494-
// clever with reading the file once and calculating the sha256sum while we read.
495-
// So we open and read the file again.
496-
let digest = {
497-
let file = tokio::fs::File::open(local_filename).await?;
498-
let mut stream = tokio_util::io::ReaderStream::with_capacity(file, 1048576);
499-
let mut hasher = Sha256::new();
500-
while let Some(chunk) = stream.next().await {
501-
hasher.update(&chunk?);
502-
}
503-
hex::encode(hasher.finalize())
504-
};
505-
digests.insert(dest.clone(), digest.clone());
506518
}
507519

508520
let mut buffered = futures::stream::iter(fs).buffer_unordered(16);
@@ -512,14 +524,6 @@ pub async fn command_upload_release_distributions(args: &ArgMatches) -> Result<(
512524
}
513525
}
514526

515-
let shasums = digests
516-
.iter()
517-
.map(|(filename, digest)| format!("{digest} {filename}\n"))
518-
.collect::<Vec<_>>()
519-
.join("");
520-
521-
std::fs::write(dist_dir.join("SHA256SUMS"), shasums.as_bytes())?;
522-
523527
upload_release_artifact(
524528
&raw_client,
525529
&retry_policy,
@@ -534,11 +538,6 @@ pub async fn command_upload_release_distributions(args: &ArgMatches) -> Result<(
534538

535539
// Check that content wasn't munged as part of uploading. This once happened
536540
// and created a busted release. Never again.
537-
if dry_run {
538-
println!("skipping SHA256SUMs check");
539-
return Ok(());
540-
}
541-
542541
let release = releases
543542
.get_by_tag(tag)
544543
.await

src/github_api_tester.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,47 @@ async def test_upload(server, upload_release_distributions, tag):
328328
assert assets[0].contents == f"{SHA256_20MEG} {filename}\n".encode()
329329

330330

331+
async def test_dry_run_writes_shasums_without_contacting_github(tmp_path):
332+
dist = tmp_path / "dist"
333+
dist.mkdir()
334+
335+
filename = dist / FILENAME
336+
filename.touch()
337+
os.truncate(filename, 20_000_000)
338+
339+
tag = "missing-release"
340+
with trio.fail_after(300):
341+
await trio.run_process(
342+
[
343+
"cargo",
344+
"run",
345+
"--",
346+
"upload-release-distributions",
347+
"--github-uri",
348+
# Use a guaranteed-bad loopback port so this fails fast if the
349+
# command unexpectedly tries to contact GitHub in dry-run mode.
350+
"http://127.0.0.1:1",
351+
"--token",
352+
"no-token-needed",
353+
"--dist",
354+
dist,
355+
"--datetime",
356+
"19700101T1234",
357+
"--ignore-missing",
358+
"--tag",
359+
tag,
360+
"-n",
361+
]
362+
)
363+
364+
release_filename = FILENAME.replace("3.0.0", f"3.0.0+{tag}").replace(
365+
"-19700101T1234", ""
366+
)
367+
assert (dist / "SHA256SUMS").read_bytes() == (
368+
f"{SHA256_20MEG} {release_filename}\n".encode()
369+
)
370+
371+
331372
# Work around https://github.com/pgjones/hypercorn/issues/238 not being in a release
332373
# Without it, test failures are unnecessarily noisy
333374
hypercorn.trio.lifespan.LifespanFailureError = trio.Cancelled

0 commit comments

Comments
 (0)