Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions tools/ci/nightly.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import sys
import tempfile
import tomllib
from urllib.request import urlopen
from urllib.request import Request, urlopen

from tools.ci.common import ROOT
from tools.ci.release import verify_metadata
Expand Down Expand Up @@ -151,8 +151,15 @@ def installers_ready():
):
raise ValueError("both reviewed installer deployment hashes are required")
for url, expected in pins.items():
with urlopen(url, timeout=30) as response:
data = response.read(1024 * 1024 + 1)
# Identify this client: the installer CDN rejects Python's default UA.
request = Request(url, headers={"User-Agent": "Wave-Nightly/1.0"})
try:
with urlopen(request, timeout=30) as response:
data = response.read(1024 * 1024 + 1)
except OSError as error:
raise RuntimeError(
f"cannot verify deployed installer {url}: {error}"
) from error
if len(data) > 1024 * 1024 or hashlib.sha256(data).hexdigest() != expected:
raise ValueError(f"versioned-only installer is not deployed: {url}")

Expand Down
53 changes: 52 additions & 1 deletion tools/test_nightly.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import tempfile
import unittest
from unittest.mock import patch
from urllib.error import HTTPError, URLError

from tools.ci import nightly, package, release, targets
from tools.check_release_assets import ARCHIVE_TARGETS
Expand Down Expand Up @@ -432,16 +433,66 @@ def test_installers_must_match_reviewed_deployed_bytes(self):
}
)
)
requests = []

def deployed_installer(request, *, timeout):
# Model the CDN's rejection of Python's default User-Agent.
if request.get_header("User-agent") != "Wave-Nightly/1.0":
raise HTTPError(request.full_url, 403, "Forbidden", {}, None)
self.assertEqual(timeout, 30)
requests.append(request.full_url)
return io.BytesIO(data)

with patch.object(nightly, "INSTALLERS_FILE", pins), patch.object(
nightly, "urlopen", side_effect=lambda *a, **k: io.BytesIO(data)
nightly, "urlopen", side_effect=deployed_installer
):
nightly.installers_ready()
self.assertEqual(set(requests), set(json.loads(pins.read_text())))
with patch.object(nightly, "INSTALLERS_FILE", pins), patch.object(
nightly, "urlopen", return_value=io.BytesIO(b"old")
):
with self.assertRaises(ValueError):
nightly.installers_ready()

def test_gate_reports_installer_url_and_exports_nothing_on_network_failure(self):
identity = dict(source_sha=A, compiler_version=self.version, ci_run_id=10)
urls = list(json.loads(nightly.INSTALLERS_FILE.read_text()))
pins = self.root / "pins.json"
pins.write_text(json.dumps({
url: hashlib.sha256(b"fixture").hexdigest() for url in urls
}))
for index, url in enumerate(urls):
for error in (
HTTPError(url, 403, "Forbidden", {}, None),
URLError("connection failed"),
TimeoutError("timed out"),
):
if isinstance(error, HTTPError):
self.addCleanup(error.close)
with self.subTest(url=url, error=type(error).__name__):
output = self.root / "failed-outputs"
stderr = io.StringIO()
responses = [io.BytesIO(b"fixture") for _ in range(index)] + [error]
with patch.dict(
os.environ,
{
"GITHUB_REPOSITORY": nightly.REPO,
"GITHUB_OUTPUT": str(output),
},
), patch.object(
nightly, "eligible", return_value=identity
), patch.object(
nightly, "urlopen", side_effect=responses
), patch.object(nightly, "INSTALLERS_FILE", pins), patch(
"sys.stderr", stderr
):
self.assertEqual(nightly.main(["--stage", "gate"]), 1)
self.assertIn(
f"cannot verify deployed installer {url}", stderr.getvalue()
)
self.assertIn(str(error), stderr.getvalue())
self.assertFalse(output.exists())

def test_publication_is_opt_in_and_versioned_rules_remain_strict(self):
with patch.dict(os.environ, {"GITHUB_REPOSITORY": "fork/Wave"}), patch.object(
nightly, "GitHub"
Expand Down
Loading