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
1 change: 1 addition & 0 deletions .github/workflows/qualify-released-linux-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
- tools/scan-released-sdk-filesystem.py
- tools/tests/test_released_sdk_inventory.py
- tools/tests/test_released_sdk_filesystem.py
- tools/tests/test_released_sdk_diagnostics.py
- tools/RELEASED_SDK_INVENTORY.md
workflow_dispatch:

Expand Down
22 changes: 20 additions & 2 deletions tools/collect-released-sdk-metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,10 @@ def target_policy(target):
return value


def collect_final_inventory(container, data, file_records, llvm_tools, policy):
def collect_final_inventory(container, data, file_records, llvm_tools, policy, *, progress=None):
# Only fixed local stages enter diagnostics; never paths or remote contents.
progress = progress if progress is not None else {}
progress['stage'] = 'metadata'
protocol = SimpleNamespace(BINARY_LIMIT=BINARY_LIMIT, stat_matches=stat_matches, regular_mode=regular_mode,
path_stat=path_stat, single_file_archive=single_file_archive, pax_times=pax_times)
reader = INVENTORY.InstalledInventory(container, protocol)
Expand All @@ -944,18 +947,26 @@ def collect_final_inventory(container, data, file_records, llvm_tools, policy):
require(contract.get('consumer', {}).get('commit') == policy['consumer_commit'] and
contract['consumer'].get('repository') == 'atrinik/classic' and
shader.get('source_commit') == policy['consumer_commit'], 'final-consumer-mismatch')
progress['stage'] = 'producer-tree'
maps = {'producer': reader.producer()}
progress['stage'] = 'metadata-binding'
INVENTORY.bind_metadata(maps['producer'], raw, records)
progress['stage'] = 'source-archives-and-providers'
INVENTORY.crosscheck_archives(maps['producer'], documents['runtime-sources.json'], contract, reader=reader)
progress['stage'] = 'shader-inputs'
inputs = shader.get('source_inputs')
require(isinstance(inputs, dict) and 0 < len(inputs) <= INVENTORY.MAX_MEMBERS, 'final-shader-inputs')
for path, sha in inputs.items():
INVENTORY.relative_path(path)
item = maps['producer'].get('sources/classic-shaders/' + path)
require(isinstance(item, dict) and item.get('sha256') == sha, 'final-shader-input-bytes')
progress['stage'] = 'debian-notices'
maps['debian_notices'] = reader.debian_notices(documents['runtime-sources.json'], documents['debian-sources.json'])
progress['stage'] = 'common-licenses'
maps['common_licenses'] = reader.common_licenses()
progress['stage'] = 'build-tools'
tools = reader.build_tools(documents['installed.json'])
progress['stage'] = 'inventory-report'
require(set(llvm_tools) == set(TOOL_FILES) == set(INVENTORY.LLVM_TOOL_FILES), 'final-llvm-roles')
llvm = {role: {key: item[key] for key in ('path', 'canonical_path', 'sha256', 'size')}
| {'executable': bool(int(item['mode'], 8) & 0o111)} for role, item in llvm_tools.items()}
Expand Down Expand Up @@ -1147,6 +1158,7 @@ def collect(target, context, original, folder):
'claim': 'installed-identity-only', 'image_code_executed': False,
'final_portable_cohort': False, 'artifacts': [], 'tool_bytes': {}}
container = None
inventory_progress = {}
try:
docker = runner_docker()
receipt.update(docker.negotiate())
Expand Down Expand Up @@ -1191,7 +1203,9 @@ def collect(target, context, original, folder):
receipt['tool_bytes'][role] = identity
container.fence()
if target in FINAL_TARGETS:
final_inventory = collect_final_inventory(container, data, file_records, receipt['tool_bytes'], policy)
final_inventory = collect_final_inventory(container, data, file_records, receipt['tool_bytes'], policy,
progress=inventory_progress)
inventory_progress.clear()
for name, records in final_inventory['inventories'].items():
receipt['artifacts'].append(save(folder, 'inventories/' + name + '.json', canonical_json(records)))
receipt['inventory_sha256'] = final_inventory['inventory_sha256']
Expand All @@ -1206,9 +1220,13 @@ def collect(target, context, original, folder):
receipt.update({'status': 'stopped', 'reason': str(error)})
if isinstance(error, BlobRedirectDenied):
receipt['redirect_rejection'] = error.diagnostics
except INVENTORY.InventoryError as error:
receipt.update({'status': 'stopped', 'reason': INVENTORY.failure_code(error)})
except BaseException:
receipt.update({'status': 'stopped', 'reason': 'operation-failed-no-details-retained'})
finally:
if inventory_progress:
receipt['inventory_failure'] = dict(inventory_progress)
if container is not None:
try:
# Reserve cleanup time after an operation deadline. No forced stop,
Expand Down
93 changes: 93 additions & 0 deletions tools/released_sdk_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,99 @@ class InventoryError(Exception):
"""Only fixed failure codes; never hostile archive or daemon contents."""


# Receipt diagnostics admit these exact source-defined codes only. Unknown
# exceptions, subclasses and payloads remain opaque even if they look like codes.
FAILURE_CODES = frozenset({
'build-tool-bytes-mismatch',
'build-tool-fields',
'build-tool-identity',
'build-tool-path',
'build-tool-schema',
'file-size-limit',
'inventory-audio-source',
'inventory-audio-sources',
'inventory-cares-source',
'inventory-compiler-schema',
'inventory-contract-schema',
'inventory-curl-archives',
'inventory-curl-source',
'inventory-custom-provider',
'inventory-custom-providers',
'inventory-deadline',
'inventory-directory-entry',
'inventory-directory-mode',
'inventory-directory-owner',
'inventory-duplicate',
'inventory-empty',
'inventory-encoding',
'inventory-end-markers',
'inventory-extension-or-link',
'inventory-file-not-allowed',
'inventory-header-invalid',
'inventory-llvm-archive-path',
'inventory-llvm-source',
'inventory-manifest-bytes-mismatch',
'inventory-manifest-duplicate',
'inventory-manifest-json',
'inventory-manifest-missing-or-size',
'inventory-manifest-path',
'inventory-manifest-payloads',
'inventory-manifest-schema',
'inventory-manifest-size',
'inventory-member-type',
'inventory-metadata-binding',
'inventory-metadata-files',
'inventory-metadata-size',
'inventory-owner-invalid',
'inventory-padding',
'inventory-parent-missing',
'inventory-path-drift',
'inventory-path-invalid',
'inventory-pax-invalid',
'inventory-provider-bytes-mismatch',
'inventory-provider-identity',
'inventory-provider-reader-required',
'inventory-report-roles',
'inventory-root-mismatch',
'inventory-size-limit',
'inventory-source-bytes-mismatch',
'inventory-source-cohort',
'inventory-source-conflict',
'inventory-source-digest',
'inventory-source-documents',
'inventory-source-name',
'inventory-source-row',
'inventory-stream-limit',
'inventory-tail',
'inventory-tail-alignment',
'inventory-truncated',
'inventory-unframed',
'notice-alias-cycle-or-limit',
'notice-alias-drift',
'notice-alias-entry',
'notice-alias-extra-member',
'notice-alias-stat',
'notice-alias-target',
'notice-boundary',
'notice-directory-invalid',
'notice-documents-invalid',
'notice-mode-invalid',
'notice-package-invalid',
'notice-row-invalid',
'notice-source-invalid',
'notice-source-missing',
'scanner-inventory-mismatch',
'scanner-report-shape',
})


def failure_code(error):
if (type(error) is InventoryError and len(error.args) == 1
and type(error.args[0]) is str and error.args[0] in FAILURE_CODES):
return error.args[0]
return 'inventory-operation-failed'


class ProviderReader(ABC):
"""Authenticated byte reader supplied by the source-inventory caller.

Expand Down
10 changes: 10 additions & 0 deletions tools/tests/test_collect_released_sdk_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import json
import os
from pathlib import Path
import re
import socket
import tarfile
import tempfile
Expand Down Expand Up @@ -1184,6 +1185,15 @@ def test_artifact_path_links_and_overwrite_rejected(self):

def test_workflow_is_same_repo_push_dispatch_read_only_and_pinned(self):
text = (ROOT / M.WORKFLOW).read_text()
sdk_test_pattern = 'test*released*sdk*.py'
self.assertIn(f"-p '{sdk_test_pattern}'", text)
sdk_tests = {path.relative_to(ROOT).as_posix()
for path in (ROOT / 'tools/tests').glob(sdk_test_pattern)}
self.assertTrue(sdk_tests)
push_paths = text.split(' paths:\n', 1)[1].split(' workflow_dispatch:', 1)[0]
selected_paths = set(re.findall(r'^ - (\S+)$', push_paths, re.MULTILINE))
self.assertFalse(sdk_tests - selected_paths,
f'SDK tests missing from push paths: {sorted(sdk_tests - selected_paths)}')
self.assertNotIn(' pull_request:', text)
self.assertNotIn('pull_request_target:', text)
self.assertIn(' - build/llvm-sdk118-collector-refresh', text)
Expand Down
131 changes: 131 additions & 0 deletions tools/tests/test_released_sdk_diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Copyright (c) 2026 Atrinik contributors
# SPDX-License-Identifier: MIT
"""Offline failure receipts retain only fixed inventory codes and stages."""
Comment thread
zoeyrose marked this conversation as resolved.
Resolved
import ast
import contextlib
import importlib.util
import json
from pathlib import Path
import unittest
from unittest.mock import patch

ROOT = Path(__file__).resolve().parents[2]
SPEC = importlib.util.spec_from_file_location(
'collector_diagnostic_fixtures', ROOT / 'tools/tests/test_collect_released_sdk_metadata.py')
FIXTURES = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(FIXTURES)
C = FIXTURES.M
I = C.INVENTORY
SECRET = 'PRIVATE_TOKEN https://private.invalid/archive?signature=PRIVATE_SIGNATURE'


class DiagnosticTests(unittest.TestCase):
def test_code_allowlist_covers_only_literal_inventory_failures(self):
codes = set()
for node in ast.walk(ast.parse((ROOT / 'tools/released_sdk_inventory.py').read_text())):
if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
and node.func.id in ('require', 'InventoryError')):
continue
value = node.args[1 if node.func.id == 'require' else 0]
if isinstance(value, ast.Constant):
self.assertIs(type(value.value), str)
codes.add(value.value)
else:
# require() alone forwards its already-selected code.
self.assertEqual(node.func.id, 'InventoryError')
self.assertEqual(ast.dump(value), "Name(id='code', ctx=Load())")
self.assertEqual(codes, I.FAILURE_CODES)
for code in codes:
self.assertEqual(I.failure_code(I.InventoryError(code)), code)

def test_unknown_payload_types_and_exception_subclasses_never_render(self):
class Hostile:
def __str__(self):
raise AssertionError('must not stringify')

class HostileError(I.InventoryError):
def __str__(self):
raise AssertionError('must not stringify')

class HostileString(str):
def __hash__(self):
raise AssertionError('must not hash')

for error in (I.InventoryError(SECRET), I.InventoryError('unknown-valid-looking-code'),
I.InventoryError(), I.InventoryError('inventory-tail', SECRET),
I.InventoryError(Hostile()), I.InventoryError(['inventory-tail']),
I.InventoryError(HostileString('inventory-tail')),
HostileError('inventory-tail'), RuntimeError(SECRET)):
with self.subTest(kind=type(error).__name__):
self.assertEqual(I.failure_code(error), 'inventory-operation-failed')

def collect_failure(self, stage, error):
fixture = FIXTURES.OfflineTests()
data, binary = fixture.portable_fixture()
shader_path = I.ROOT + '/shader-generation.json'
shader = json.loads(data[shader_path])
shader.update(source_commit=C.FINAL_TARGETS['portable-final']['consumer_commit'],
source_inputs={'fixture-input': '1' * 64})
data[shader_path] = FIXTURES.encoded(shader)
operations = (
('producer-tree', I.InstalledInventory, 'producer',
{'sources/classic-shaders/fixture-input': {'sha256': '1' * 64}}),
('metadata-binding', I, 'bind_metadata', None),
('source-archives-and-providers', I, 'crosscheck_archives', None),
('debian-notices', I.InstalledInventory, 'debian_notices', {}),
('common-licenses', I.InstalledInventory, 'common_licenses', {}),
('build-tools', I.InstalledInventory, 'build_tools', {}),
('inventory-report', I, 'expected_scan', None),
)
with contextlib.ExitStack() as stack:
mocked = []
for name, owner, method, result in operations:
mocked.append(stack.enter_context(patch.object(
owner, method, return_value=result, side_effect=error if name == stage else None)))
scanner = stack.enter_context(patch.object(C, 'qualify_filesystem'))
result, _ = fixture.collect_portable_fixture(data, binary, target='portable-final')
scanner.assert_not_called()
index = next(i for i, operation in enumerate(operations) if operation[0] == stage)
mocked[index].assert_called_once()
for operation in mocked[index + 1:]:
operation.assert_not_called()
self.assertEqual(result['status'], 'stopped')
self.assertEqual(result['inventory_failure'], {'stage': stage})
self.assertTrue(result['owned_container_removed'])
self.assertFalse(result['image_code_executed'])
self.assertFalse(result['final_portable_cohort'])
self.assertNotIn('inventory_sha256', result)
self.assertNotIn('metadata_sha256', result)
self.assertEqual(len(result['tool_bytes']), 12)
for secret in ('PRIVATE_TOKEN', 'private.invalid', 'PRIVATE_SIGNATURE'):
self.assertNotIn(secret, json.dumps(result))
return result

def test_each_inventory_stage_retains_fixed_code_and_stops_before_scanner(self):
for stage, code in (
('producer-tree', 'inventory-extension-or-link'),
('metadata-binding', 'inventory-metadata-binding'),
('source-archives-and-providers', 'inventory-provider-bytes-mismatch'),
('debian-notices', 'notice-alias-target'),
('common-licenses', 'file-size-limit'),
('build-tools', 'build-tool-bytes-mismatch'),
('inventory-report', 'inventory-report-roles'),
):
with self.subTest(stage=stage):
receipt = self.collect_failure(stage, I.InventoryError(code))
self.assertEqual(receipt['reason'], code)

def test_hostile_unknown_and_noninventory_failures_remain_opaque(self):
for error, reason in (
(I.InventoryError(SECRET), 'inventory-operation-failed'),
(I.InventoryError('unknown-code'), 'inventory-operation-failed'),
(RuntimeError(SECRET), 'operation-failed-no-details-retained'),
(KeyboardInterrupt(SECRET), 'operation-failed-no-details-retained'),
):
with self.subTest(kind=type(error).__name__):
receipt = self.collect_failure('producer-tree', error)
self.assertEqual(receipt['reason'], reason)


if __name__ == '__main__':
unittest.main()
Loading