Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
66 changes: 66 additions & 0 deletions .builders/tests/test_upload.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import email.message
import json
from hashlib import sha256
from pathlib import Path
from unittest import mock
from zipfile import ZipFile
Expand Down Expand Up @@ -602,6 +604,70 @@ def track_upload(content, path, content_type='text/plain', cache_control=None):
assert 'href="package2/"' in root_html


def test_hash_directory(tmp_path):
(tmp_path / 'a.txt').write_bytes(b'hello')
(tmp_path / 'b.txt').write_bytes(b'world')

result = upload.hash_directory(tmp_path)
assert result == upload.hash_directory(tmp_path)

(tmp_path / 'a.txt').write_bytes(b'changed')
assert upload.hash_directory(tmp_path) != result


def test_compute_input_hashes(tmp_path, monkeypatch):
dep_file = tmp_path / 'agent_requirements.in'
dep_file.write_bytes(b'requests==2.31.0\n')

workflow_file = tmp_path / 'resolve-build-deps.yaml'
workflow_file.write_bytes(b'on: push\n')

builder_dir = tmp_path / '.builders'
builder_dir.mkdir()
(builder_dir / 'upload.py').write_bytes(b'# script\n')

monkeypatch.setattr(upload, 'DIRECT_DEP_FILE', dep_file)
monkeypatch.setattr(upload, 'WORKFLOW_FILE', workflow_file)
monkeypatch.setattr(upload, 'BUILDER_DIR', builder_dir)

result = upload.compute_input_hashes()

assert set(result.keys()) == {'agent_requirements.in', '.github/workflows/resolve-build-deps.yaml', '.builders'}
assert result['agent_requirements.in'] == sha256(b'requests==2.31.0\n').hexdigest()
assert result['.github/workflows/resolve-build-deps.yaml'] == sha256(b'on: push\n').hexdigest()
assert result['.builders'] == upload.hash_directory(builder_dir)


def test_generate_lockfiles_metadata_contains_inputs(tmp_path, monkeypatch):
dep_file = tmp_path / 'agent_requirements.in'
dep_file.write_bytes(b'requests==2.31.0\n')

workflow_file = tmp_path / 'resolve-build-deps.yaml'
workflow_file.write_bytes(b'on: push\n')

builder_dir = tmp_path / '.builders'
builder_dir.mkdir()
(builder_dir / 'upload.py').write_bytes(b'# script\n')

fake_deps_dir = tmp_path / '.deps'
fake_resolved_dir = fake_deps_dir / 'resolved'
fake_deps_dir.mkdir()
fake_resolved_dir.mkdir()

monkeypatch.setattr(upload, 'DIRECT_DEP_FILE', dep_file)
monkeypatch.setattr(upload, 'WORKFLOW_FILE', workflow_file)
monkeypatch.setattr(upload, 'BUILDER_DIR', builder_dir)
monkeypatch.setattr(upload, 'RESOLUTION_DIR', fake_deps_dir)
monkeypatch.setattr(upload, 'LOCK_FILE_DIR', fake_resolved_dir)

upload.generate_lockfiles(tmp_path, {})

metadata = json.loads((fake_deps_dir / 'metadata.json').read_text())
assert 'inputs' in metadata
assert set(metadata['inputs'].keys()) == {'agent_requirements.in', '.github/workflows/resolve-build-deps.yaml', '.builders'}
assert metadata['sha256'] == sha256(b'requests==2.31.0\n').hexdigest()


def test_upload(setup_targets_dir, setup_fake_hash):
"""Basic end-to-end test of upload with a mocked bucket."""
wheels = {
Expand Down
20 changes: 20 additions & 0 deletions .builders/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
RESOLUTION_DIR = REPO_DIR / '.deps'
LOCK_FILE_DIR = RESOLUTION_DIR / 'resolved'
DIRECT_DEP_FILE = REPO_DIR / 'agent_requirements.in'
WORKFLOW_FILE = REPO_DIR / '.github/workflows/resolve-build-deps.yaml'
CACHE_CONTROL = 'public, max-age=15'
VALID_PROJECT_NAME = re.compile(r'^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$', re.IGNORECASE)
UNNORMALIZED_PROJECT_NAME_CHARS = re.compile(r'[-_.]+')
Expand Down Expand Up @@ -76,6 +77,24 @@ def hash_file(path: Path) -> str:
return sha256(f.read()).hexdigest()


def hash_directory(path: Path) -> str:
"""Compute a combined SHA256 hash of all files in a directory, sorted by relative path."""
h = sha256()
for file_path in sorted(path.rglob('*')):
if file_path.is_file():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example:

Suggested change
if file_path.is_file():
if file_path.is_file() and file_patch.suffix() != ".pyc" and not any(p.startswith('.') or p == '__pycache__' for p in file_path.parts):

(dirty+heavy+not tested, there's probably a much better way to achieve the same)

h.update(file_path.read_bytes())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hash file paths as part of the .builders digest

This digest only feeds each file's bytes into the SHA256, so distinct .builders trees can produce the same value. A simple rename like .builders/foo.sh -> .builders/bar.sh with identical contents, or repartitioning bytes across files (ab+ca+bc), leaves this hash unchanged even though .github/workflows/scripts/resolve_deps_check_should_run.sh treats any .builders/ path change as a dependency-resolution input. That means a later comparison against metadata.json can incorrectly skip a rebuild/PR after a real builder change.

Useful? React with 👍 / 👎.

return h.hexdigest()


def compute_input_hashes() -> dict[str, str]:
"""Compute SHA256 hashes for all dependency resolution inputs."""
return {
'agent_requirements.in': sha256(DIRECT_DEP_FILE.read_bytes()).hexdigest(),
'.github/workflows/resolve-build-deps.yaml': sha256(WORKFLOW_FILE.read_bytes()).hexdigest(),
'.builders': hash_directory(BUILDER_DIR),
}


def _build_number_of_wheel(wheel_info: dict) -> int:
"""Extract the build number from wheel information."""
wheel_name = PurePosixPath(wheel_info['name']).stem
Expand Down Expand Up @@ -281,6 +300,7 @@ def generate_lockfiles(targets_dir, lockfiles):
with RESOLUTION_DIR.joinpath('metadata.json').open('w', encoding='utf-8') as f:
contents = json.dumps(
{
'inputs': compute_input_hashes(),
'sha256': sha256(DIRECT_DEP_FILE.read_bytes()).hexdigest(),
},
indent=2,
Expand Down
Loading