Skip to content

Commit cb90e62

Browse files
iliakurdd-agent-integrations-bot[bot]
andauthored
Replace paths: filter with content-hash gate in resolve-build-deps.yaml (DataDog#23458)
* Replace paths: filter with content-hash gate in resolve-build-deps.yaml The workflow was over-triggering on merge commits whose push diffs swept in already-landed .builders/ changes unrelated to the PR. Replace the paths: filter with a content-hash gate backed by .builders/inputs_hash.py. - inputs_hash.py: replace COMMON_INPUTS (literal paths) with SHARED_INPUTS and RESOLUTION_INPUTS (glob patterns); add compute_target, pinned_target, compute_resolution, pinned_resolution, verify_resolution, status; replace old compute/pinned CLI subcommands with status and verify-resolution. - upload.py: import inputs_hash, extend _write_builder_inputs to accept a resolution hash, emit [resolution] + [images] TOML sections, pass compute_resolution() at the call site. - resolve-build-deps.yaml: drop paths: filter; add gate job that runs inputs_hash.py status; add if: needs_resolution == 'true' to test job; read rebuild_<target> flags from gate outputs in build job. - verify-deps-pin.yaml: new workflow that runs inputs_hash.py verify-resolution on gh-readonly-queue/** pushes to block merges with a stale resolution pin. - tests/test_inputs_hash.py: new test file covering _hash_paths stability, compute/pinned/verify for targets and resolution, status fresh/stale cases, CLI output shape, and a coverage test that fails CI if a new .builders/ file is not classified. NOTE: verify-deps-pin must be added to required status checks on master and release branches before this fully closes the concurrent-PR race window. * review feedback * Update dependency resolution * tests and introduce gate runs for master and release branches. * Update dependency resolution * implement clear feedback * Overhaul hashing in response to reviews; stop writing metadata.json * fix type hint * Update dependency resolution * Update dependency resolution * more feedback * Update dependency resolution --------- Co-authored-by: dd-agent-integrations-bot[bot] <dd-agent-integrations-bot[bot]@users.noreply.github.com>
1 parent d509d38 commit cb90e62

14 files changed

Lines changed: 1111 additions & 280 deletions

.builders/inputs_hash.py

Lines changed: 370 additions & 93 deletions
Large diffs are not rendered by default.

.builders/targets.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{"platform": "linux", "arch": "x86_64", "runner_os": "ubuntu-22.04"},
3+
{"platform": "linux", "arch": "aarch64", "runner_os": "ubuntu-22.04-arm"},
4+
{"platform": "windows", "arch": "x86_64", "runner_os": "windows-2022"},
5+
{"platform": "macos", "arch": "x86_64", "runner_os": "macos-14-large"},
6+
{"platform": "macos", "arch": "aarch64", "runner_os": "macos-14"}
7+
]

.builders/tests/test_inputs_hash.py

Lines changed: 457 additions & 0 deletions
Large diffs are not rendered by default.

.builders/tests/test_upload.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from zipfile import ZipFile
55

66
import pytest
7+
8+
import inputs_hash
79
import upload
810

911

@@ -466,7 +468,7 @@ def test_lockfile_generation(tmp_path, setup_targets_dir, frozen_timestamp):
466468
with mock.patch.object(upload, "RESOLUTION_DIR", fake_deps_dir), \
467469
mock.patch.object(upload, "LOCK_FILE_DIR", fake_resolved_dir):
468470

469-
upload.generate_lockfiles(targets_dir, lockfile)
471+
upload.generate_lockfiles(targets_dir, lockfile, 'dummy_hash')
470472
lock_files = list(fake_resolved_dir.glob("*.txt"))
471473
assert lock_files, "No lock files generated"
472474
lockfile_map = {lock_file.name: lock_file.read_text().strip() for lock_file in lock_files}
@@ -496,7 +498,7 @@ def test_generate_lockfiles_skips_targets_with_no_wheels(tmp_path):
496498

497499
with mock.patch.object(upload, "RESOLUTION_DIR", fake_deps_dir), \
498500
mock.patch.object(upload, "LOCK_FILE_DIR", fake_resolved_dir):
499-
upload.generate_lockfiles(str(tmp_path / "targets"), lockfile)
501+
upload.generate_lockfiles(str(tmp_path / "targets"), lockfile, 'dummy_hash')
500502

501503
lock_files = {f.name for f in fake_resolved_dir.glob("*.txt")}
502504
assert lock_files == {f"linux-x86_64_{upload.CURRENT_PYTHON_VERSION}.txt"}
@@ -516,7 +518,48 @@ def test_generate_lockfiles_accepts_string_path(tmp_path):
516518
with mock.patch.object(upload, "RESOLUTION_DIR", fake_deps_dir), \
517519
mock.patch.object(upload, "LOCK_FILE_DIR", fake_resolved_dir):
518520
# Should not raise TypeError: unsupported operand type(s) for /: 'str' and 'str'
519-
upload.generate_lockfiles(str(tmp_path / "targets"), lockfile)
521+
upload.generate_lockfiles(str(tmp_path / "targets"), lockfile, 'dummy_hash')
522+
523+
524+
def test_generate_lockfiles_writes_resolution_pin_and_target_inputs(tmp_path):
525+
"""generate_lockfiles writes the gate-passed resolution_hash plus per-target inputs_sha256 into builder_inputs.toml.
526+
527+
Pins the four-hop contract (gate output → workflow env → CLI flag →
528+
write call) so a refactor that drops resolution_hash anywhere along the
529+
way fails here rather than producing a stale or empty [resolution]
530+
section in the bot commit.
531+
"""
532+
import tomllib
533+
534+
lockfile = {
535+
'linux-x86_64': ['dep @ https://example.com/dep.whl#sha256=abc', ''],
536+
'macos-x86_64': ['dep @ https://example.com/dep-macos.whl#sha256=def', ''],
537+
}
538+
539+
fake_deps_dir = tmp_path / '.deps'
540+
fake_resolved_dir = fake_deps_dir / 'resolved'
541+
fake_deps_dir.mkdir()
542+
fake_resolved_dir.mkdir()
543+
targets_dir = tmp_path / 'targets'
544+
(targets_dir / 'linux-x86_64').mkdir(parents=True)
545+
(targets_dir / 'linux-x86_64' / 'inputs_sha256').write_text('linux-hash', encoding='utf-8')
546+
(targets_dir / 'linux-x86_64' / 'image_digest').write_text('sha256:linux-digest', encoding='utf-8')
547+
(targets_dir / 'macos-x86_64').mkdir(parents=True)
548+
(targets_dir / 'macos-x86_64' / 'inputs_sha256').write_text('macos-hash', encoding='utf-8')
549+
550+
with mock.patch.object(upload, 'RESOLUTION_DIR', fake_deps_dir), \
551+
mock.patch.object(upload, 'LOCK_FILE_DIR', fake_resolved_dir):
552+
upload.generate_lockfiles(targets_dir, lockfile, 'gate-passed-hash')
553+
554+
pin_path = fake_deps_dir / 'builder_inputs.toml'
555+
with pin_path.open('rb') as f:
556+
pin = tomllib.load(f)
557+
558+
assert pin[inputs_hash.SECTION_RESOLUTION][inputs_hash.HASH_KEY] == 'gate-passed-hash'
559+
assert pin[inputs_hash.SECTION_IMAGES] == {
560+
'linux-x86_64': 'linux-hash',
561+
'macos-x86_64': 'macos-hash',
562+
}
520563

521564

522565
def test_collect_and_validate_wheels(tmp_path):

.builders/upload.py

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
from google.cloud import storage
1414

15+
import inputs_hash
16+
1517
if TYPE_CHECKING:
1618
from google.cloud.storage.bucket import Bucket as GCSBucket
1719

@@ -20,7 +22,6 @@
2022
REPO_DIR = BUILDER_DIR.parent
2123
RESOLUTION_DIR = REPO_DIR / '.deps'
2224
LOCK_FILE_DIR = RESOLUTION_DIR / 'resolved'
23-
DIRECT_DEP_FILE = REPO_DIR / 'agent_requirements.in'
2425
CACHE_CONTROL = 'public, max-age=15'
2526
VALID_PROJECT_NAME = re.compile(r'^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$', re.IGNORECASE)
2627
UNNORMALIZED_PROJECT_NAME_CHARS = re.compile(r'[-_.]+')
@@ -274,18 +275,9 @@ def upload_string(self, content: str, blob_path: str, content_type: str = 'text/
274275
blob.patch()
275276

276277

277-
def generate_lockfiles(targets_dir, lockfiles):
278+
def generate_lockfiles(targets_dir, lockfiles, resolution_hash):
278279
targets_dir = Path(targets_dir)
279280
LOCK_FILE_DIR.mkdir(parents=True, exist_ok=True)
280-
with RESOLUTION_DIR.joinpath('metadata.json').open('w', encoding='utf-8') as f:
281-
contents = json.dumps(
282-
{
283-
'sha256': sha256(DIRECT_DEP_FILE.read_bytes()).hexdigest(),
284-
},
285-
indent=2,
286-
sort_keys=True,
287-
)
288-
f.write(f'{contents}\n')
289281

290282
image_digests = {}
291283
builder_inputs = {}
@@ -311,31 +303,10 @@ def generate_lockfiles(targets_dir, lockfiles):
311303
contents = json.dumps(image_digests, indent=2, sort_keys=True)
312304
f.write(f'{contents}\n')
313305

314-
_write_builder_inputs(RESOLUTION_DIR / 'builder_inputs.toml', builder_inputs)
315-
316-
317-
_BUILDER_INPUTS_HEADER = """\
318-
# Content hashes of the inputs that determine each builder image.
319-
#
320-
# The `Resolve Dependencies and Build Wheels` workflow compares these
321-
# hashes against hashes computed from the working tree (via
322-
# .builders/inputs_hash.py) to decide whether to rebuild a builder image
323-
# from scratch or pull the existing one by digest from image_digests.json.
324-
#
325-
# This file is rewritten by .builders/upload.py whenever dependency
326-
# resolution publishes new artifacts and should not be edited by hand.
327-
# The set of files covered by each hash is defined by COMMON_INPUTS in
328-
# .builders/inputs_hash.py plus everything under .builders/images/<target>/.
329-
330-
[inputs]
331-
"""
332-
333-
334-
def _write_builder_inputs(path: Path, hashes: dict[str, str]) -> None:
335-
lines = [_BUILDER_INPUTS_HEADER.rstrip('\n')]
336-
for target in sorted(hashes):
337-
lines.append(f'{target} = "{hashes[target]}"')
338-
path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
306+
inputs_hash.write_pinned_hashes(
307+
RESOLUTION_DIR / 'builder_inputs.toml',
308+
inputs_hash.PinnedHashes(resolution=resolution_hash, images=builder_inputs),
309+
)
339310

340311

341312
def upload(targets_dir: Path, bucket: Bucket | None = None) -> dict[str, list[str]]:
@@ -384,6 +355,14 @@ def upload(targets_dir: Path, bucket: Bucket | None = None) -> dict[str, list[st
384355
if __name__ == '__main__':
385356
parser = argparse.ArgumentParser(prog='builder', allow_abbrev=False)
386357
parser.add_argument('targets_dir')
358+
parser.add_argument(
359+
'--resolution-hash',
360+
required=True,
361+
help=(
362+
'Hex sha256 of the resolution inputs, computed by the gate job '
363+
'and passed in to avoid recomputing against the working tree.'
364+
),
365+
)
387366
args = parser.parse_args()
388367
lockfiles = upload(args.targets_dir)
389-
generate_lockfiles(args.targets_dir, lockfiles)
368+
generate_lockfiles(args.targets_dir, lockfiles, args.resolution_hash)

.deps/builder_inputs.toml

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
# Content hashes of the inputs that determine each builder image.
1+
# Content hashes of the inputs that determine the resolution pipeline and each
2+
# builder image.
23
#
3-
# The `Resolve Dependencies and Build Wheels` workflow compares these
4-
# hashes against hashes computed from the working tree (via
5-
# .builders/inputs_hash.py) to decide whether to rebuild a builder image
6-
# from scratch or pull the existing one by digest from image_digests.json.
4+
# The `Resolve Dependencies and Build Wheels` workflow uses these hashes to
5+
# gate the full pipeline (resolution.hash) and to decide whether to rebuild a
6+
# builder image from scratch or pull the existing one by digest (images.*).
7+
# The `verify-deps-pin` workflow checks resolution.hash on merge-queue refs.
78
#
8-
# This file is rewritten by .builders/upload.py whenever dependency
9-
# resolution publishes new artifacts and should not be edited by hand.
10-
# The set of files covered by each hash is defined by COMMON_INPUTS in
11-
# .builders/inputs_hash.py plus everything under .builders/images/<target>/.
9+
# This file is rewritten by .builders/upload.py whenever dependency resolution
10+
# publishes new artifacts and should not be edited by hand.
11+
# Hash inputs are defined in .builders/inputs_hash.py (SHARED_INPUTS,
12+
# RESOLUTION_INPUTS).
13+
[resolution]
14+
hash = "090005fe2acdb03816c94f40949836db31b29350bc0746a0a7bc8c5d896119c8"
1215

13-
[inputs]
14-
linux-aarch64 = "e3b9a63fa95c9d58a54926fd42d1a20ae738a6c05ff8338d995b8cd9c5497ef2"
15-
linux-x86_64 = "58a8e70bf5f870e210d586662843cdc9948b5d90638e3487f1e07a2b10f27f19"
16-
windows-x86_64 = "130572854fdcd9ca5d021af13087afa7384b1e659959087db21c77ac144ad313"
16+
[images]
17+
linux-aarch64 = "5239191575da3521adfbb9992114201c71c65ff3056de4ae2e475390d218d194"
18+
linux-x86_64 = "83d13d3171972f56b1336d325eecae73bf5e2329a24142f25e68ae50c542ce6a"
19+
macos-aarch64 = "8a4b86487b09da7dda2165487464a2e80bbe623a6db56b5eb23db35eec3ea4f0"
20+
macos-x86_64 = "0c4f87d5e5df769f19ad8527208e8cdcc7268b1741e1b5e67483133c9c0073c1"
21+
windows-x86_64 = "d7cc3720c0ac20a28c20d0fd606d6d0a84023e97682c39994ac99c1cdef9e817"

.deps/image_digests.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"linux-aarch64": "sha256:bced710c0b1812c224caaa951b412ba3fbf2d299dcb4cb001b58898cd9e119ae",
3-
"linux-x86_64": "sha256:82a4605484daf57ff3edfc2c4a10d19cb3ee592ba5cdf6a53605c99a76856ae3",
4-
"windows-x86_64": "sha256:7ebc2f51eca870f551f663159b399e2cd98a15830f2a495a775dfd124f7f859b"
2+
"linux-aarch64": "sha256:293e22087f45e4f460a527f2583c9592ae702a55b318cc6066f255e60470a40e",
3+
"linux-x86_64": "sha256:57380fd04afcf90a50087ad3d3e32c2075a801bab93478be4a83c0a2cabeb867",
4+
"windows-x86_64": "sha256:5f99e8d346605ce93a0656908bbd4da6258ce9b67798c18a9a327a5f08403be7"
55
}

0 commit comments

Comments
 (0)