diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f4fb4bf..a88d688 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -98,6 +98,8 @@ jobs: restore-keys: product-vamp-conan-${{ runner.os }}- - name: Configure native release run: python3 modulus.py configure --target x86_64_gcc12_release --update + - name: Check package tooling + run: python3 -m unittest discover -s tests -p 'test_publish_package.py' - name: Check code style run: | python3 -m pip install --no-cache-dir -r build/build_tools/docker/format-requirements.txt diff --git a/README.md b/README.md index 7848786..ff92d39 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ PACKAGE_REGISTRY_TOKEN= \ python3 package/scripts/publish_latest_generic_package.py ``` -Artifacts are published to the khodo generic package registry's required `latest` slot. Publish the updated Modulus tooling before running Product Vamp CI so packaging and publishing agree on the stable filenames. +Artifacts are published to the khodo generic package registry's required `latest` slot. Publish the updated Modulus tooling before running Product Vamp CI so packaging and publishing agree on the stable filenames. CI checks this contract immediately after configure using a tiny fixture archive, without compiling or contacting the registry. A failure here means the exported Modulus packager needs updating; rerun the full workflow after publishing Modulus, not just the upload step. ## Score Assets diff --git a/package/scripts/publish_latest_generic_package.py b/package/scripts/publish_latest_generic_package.py index 300cd75..0691a5d 100755 --- a/package/scripts/publish_latest_generic_package.py +++ b/package/scripts/publish_latest_generic_package.py @@ -139,7 +139,10 @@ def discover_artifacts(dist_dir, package_name, targets): artifacts.extend((archive, archive.with_name(archive.name + ".sha256"))) missing = [path for path in artifacts if not path.is_file()] if missing: - raise SystemExit("missing artifact files: " + ", ".join(str(path) for path in missing)) + found = sorted(path.name for path in dist_dir.glob(f"{package_name}-*.tar.gz*") if path.is_file()) + raise SystemExit("missing artifact files: " + ", ".join(str(path) for path in missing) + + "\nFound: " + (", ".join(found) or "none") + + "\nPublish the current Modulus tooling, run configure --update, and recreate the package so the builder and publisher use the same filenames.") return artifacts def upload_file(registry_url, package_name, path, auth): diff --git a/tests/test_publish_package.py b/tests/test_publish_package.py index 5ce0c47..6498098 100644 --- a/tests/test_publish_package.py +++ b/tests/test_publish_package.py @@ -1,3 +1,7 @@ +import hashlib +import subprocess +import sys +import tarfile import tempfile import unittest from pathlib import Path @@ -6,6 +10,37 @@ from unittest.mock import patch from package.scripts import publish_latest_generic_package class PackagePublishingTests(unittest.TestCase): + def test_configured_packager_matches_publisher(self): + root = Path(__file__).resolve().parents[1] + runtime = root / 'build/build_common/modulus' + if not (runtime / 'modulus/cli.py').is_file(): + self.skipTest('Configure first to check the exported Modulus packager') + with patch.object(sys, 'path', [str(runtime), *sys.path]): + import modulus.cli + import modulus.config + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + context = modulus.cli.build_context(directory, modulus.config.load_repo_config(root)) + name, registry, targets = publish_latest_generic_package.read_config(root / 'modulus.json') + def fake_build(context, command, **kwargs): + if command[:2] == ['cmake', '--install']: + stage = Path(command[2]).parent / 'stage' + (stage / 'manifest.json').write_text('{}\n') + return 0 + with patch.object(modulus.cli, 'in_docker', return_value=True), \ + patch.object(modulus.cli, 'run', side_effect=fake_build), \ + patch.object(modulus.cli, 'run_capture', return_value=subprocess.CompletedProcess([], 0, stdout='fixture\n', stderr='')), \ + patch.object(modulus.cli, 'upload_generic_artifact') as upload: + self.assertEqual(modulus.cli.package_artifacts(context, ['--no-remote']), 0) + upload.assert_not_called() + try: + artifacts = publish_latest_generic_package.discover_artifacts(directory / 'build/packages', name, targets) + except SystemExit as error: + self.fail(str(error)) + for archive, checksum in zip(artifacts[::2], artifacts[1::2]): + self.assertEqual(checksum.read_text(), f'{hashlib.sha256(archive.read_bytes()).hexdigest()} {archive.name}\n') + with tarfile.open(archive) as package: + self.assertIn('manifest.json', package.getnames()) def test_only_configured_target_archives_and_checksums_are_uploaded(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) @@ -20,6 +55,15 @@ class PackagePublishingTests(unittest.TestCase): archive.unlink() with self.assertRaisesRegex(SystemExit, 'missing artifact files'): publish_latest_generic_package.discover_artifacts(root, 'product-vamp', ['arm_64_gcc12_release']) + def test_mismatched_filenames_report_actual_artifacts_without_uploading(self): + with tempfile.TemporaryDirectory() as temporary, patch.object(publish_latest_generic_package, 'request') as request: + archive = Path(temporary) / 'product-vamp-abc123-arm_64_gcc12_release.tar.gz' + archive.write_bytes(b'fixture') + with self.assertRaises(SystemExit) as error: + publish_latest_generic_package.main(['--dist-dir', temporary, '--user', 'user', '--token', 'test-token']) + self.assertIn(f'Found: {archive.name}', str(error.exception)) + self.assertIn('Publish the current Modulus tooling', str(error.exception)) + request.assert_not_called() def test_missing_artifacts_do_not_touch_the_registry(self): with tempfile.TemporaryDirectory() as temporary, patch.object(publish_latest_generic_package, 'request') as request: with self.assertRaisesRegex(SystemExit, 'missing artifact files'):