Skip to content

Commit 7460a85

Browse files
authored
fix release package test dependencies (#72)
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 696e9db commit 7460a85

5 files changed

Lines changed: 148 additions & 5 deletions

File tree

.github/workflows/ci-rust-python-package.yaml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ jobs:
5858
mutation_cargo_packages: ${{ steps.detect.outputs.mutation_cargo_packages }}
5959
mutation_jobs: ${{ steps.detect.outputs.mutation_jobs }}
6060
has_mutation_cargo_packages: ${{ steps.detect.outputs.has_mutation_cargo_packages }}
61+
release_validation_tags: ${{ steps.detect.outputs.release_validation_tags }}
62+
has_release_validation_tags: ${{ steps.detect.outputs.has_release_validation_tags }}
6163
steps:
6264
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
6365
with:
@@ -89,6 +91,8 @@ jobs:
8991
mutation_cargo_packages="$(printf '%s' "${selection}" | python3 -c 'import json, sys; print(json.dumps(json.load(sys.stdin)["mutation_cargo_packages"]))')"
9092
mutation_jobs="$(printf '%s' "${selection}" | python3 -c 'import json, sys; print(json.dumps(json.load(sys.stdin)["mutation_jobs"]))')"
9193
has_mutation_cargo_packages="$(printf '%s' "${selection}" | python3 -c 'import json, sys; print(str(json.load(sys.stdin)["has_mutation_cargo_packages"]).lower())')"
94+
release_validation_tags="$(printf '%s' "${selection}" | python3 -c 'import json, sys; print(json.dumps(json.load(sys.stdin)["release_validation_tags"]))')"
95+
has_release_validation_tags="$(printf '%s' "${selection}" | python3 -c 'import json, sys; print(str(json.load(sys.stdin)["has_release_validation_tags"]).lower())')"
9296
if [[ "${has_plugins}" == "false" ]]; then
9397
has_plugins_output="false"
9498
else
@@ -103,6 +107,8 @@ jobs:
103107
echo "mutation_cargo_packages=${mutation_cargo_packages}"
104108
echo "mutation_jobs=${mutation_jobs}"
105109
echo "has_mutation_cargo_packages=${has_mutation_cargo_packages_output}"
110+
echo "release_validation_tags=${release_validation_tags}"
111+
echo "has_release_validation_tags=${has_release_validation_tags}"
106112
} >> "$GITHUB_OUTPUT"
107113
108114
build-test:
@@ -359,14 +365,18 @@ jobs:
359365
cargo doc "${cargo_args[@]}" --lib --no-deps --document-private-items
360366
361367
release-validation:
362-
if: github.event_name == 'pull_request'
368+
if: github.event_name == 'pull_request' && needs.validate-and-detect.outputs.has_release_validation_tags == 'true'
363369
needs: validate-and-detect
370+
strategy:
371+
fail-fast: false
372+
matrix:
373+
tag: ${{ fromJson(needs.validate-and-detect.outputs.release_validation_tags) }}
364374
permissions:
365375
contents: read
366376
pull-requests: read
367377
id-token: write
368378
uses: ./.github/workflows/release-rust-python-package.yaml
369379
with:
370-
tag: retry-with-backoff-v0.2.0
380+
tag: ${{ matrix.tag }}
371381
repository: testpypi
372382
publish_enabled: false

.github/workflows/release-rust-python-package.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,8 @@ jobs:
213213
if [[ ! -f "${venv_python}" ]]; then
214214
venv_python="${tmpdir}/venv/Scripts/python.exe"
215215
fi
216-
"${venv_python}" -m pip install dist/*.whl pytest pytest-asyncio PyYAML
216+
uv pip install --python "${venv_python}" --group dev PyYAML
217+
"${venv_python}" -m pip install dist/*.whl
217218
if [[ -d "${GITHUB_WORKSPACE}/plugins/tests/${{ needs.resolve.outputs.slug }}" ]]; then
218219
mkdir -p "${tmpdir}/tests"
219220
cp -R "${GITHUB_WORKSPACE}/plugins/tests/${{ needs.resolve.outputs.slug }}" "${tmpdir}/tests/${{ needs.resolve.outputs.slug }}"
@@ -265,7 +266,8 @@ jobs:
265266
if [[ ! -f "${venv_python}" ]]; then
266267
venv_python="${tmpdir}/venv/Scripts/python.exe"
267268
fi
268-
"${venv_python}" -m pip install dist/*.tar.gz pytest pytest-asyncio PyYAML
269+
uv pip install --python "${venv_python}" --group dev PyYAML
270+
"${venv_python}" -m pip install dist/*.tar.gz
269271
if [[ -d "${GITHUB_WORKSPACE}/plugins/tests/${{ needs.resolve.outputs.slug }}" ]]; then
270272
mkdir -p "${tmpdir}/tests"
271273
cp -R "${GITHUB_WORKSPACE}/plugins/tests/${{ needs.resolve.outputs.slug }}" "${tmpdir}/tests/${{ needs.resolve.outputs.slug }}"

tests/test_plugin_catalog.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1537,9 +1537,48 @@ def test_ci_selection_returns_has_plugins_contract(self) -> None:
15371537
"mutation_cargo_packages": [],
15381538
"has_mutation_cargo_packages": False,
15391539
"mutation_jobs": [],
1540+
"release_validation_tags": [],
1541+
"has_release_validation_tags": False,
15401542
},
15411543
)
15421544

1545+
def test_ci_selection_detects_plugin_version_bump_for_release_validation(self) -> None:
1546+
with tempfile.TemporaryDirectory() as tmpdir:
1547+
root = Path(tmpdir)
1548+
git = lambda *args: subprocess.run( # noqa: E731
1549+
["git", *args],
1550+
cwd=root,
1551+
text=True,
1552+
capture_output=True,
1553+
check=True,
1554+
)
1555+
git("init")
1556+
git("config", "user.name", "Test User")
1557+
git("config", "user.email", "test@example.com")
1558+
(root / "Cargo.toml").write_text(
1559+
'[workspace]\nmembers = ["plugins/rust/python-package/rate_limiter", "plugins/rust/python-package/pii_filter"]\n'
1560+
)
1561+
rate_limiter = self._create_plugin(root, "rate_limiter")
1562+
self._create_plugin(root, "pii_filter")
1563+
git("add", ".")
1564+
git("commit", "--no-verify", "-m", "seed layout")
1565+
base_sha = git("rev-parse", "HEAD").stdout.strip()
1566+
1567+
(rate_limiter / "Cargo.toml").write_text(
1568+
'[package]\nname = "rate_limiter"\nversion = "0.0.2"\nrepository = "https://github.com/IBM/cpex-plugins"\n'
1569+
)
1570+
(rate_limiter / "cpex_rate_limiter" / "plugin-manifest.yaml").write_text(
1571+
'description: "rate_limiter"\nauthor: "ContextForge Team"\nversion: "0.0.2"\nkind: "cpex_rate_limiter.rate_limiter.RateLimiterPlugin"\navailable_hooks:\n - "tool_pre_invoke"\n'
1572+
)
1573+
git("add", ".")
1574+
git("commit", "--no-verify", "-m", "bump rate limiter")
1575+
1576+
result = run_catalog("ci-selection", str(root), "diff", base_sha, "HEAD")
1577+
self.assertEqual(result.returncode, 0, result.stderr)
1578+
payload = json.loads(result.stdout)
1579+
self.assertEqual(payload["release_validation_tags"], ["rate-limiter-v0.0.2"])
1580+
self.assertTrue(payload["has_release_validation_tags"])
1581+
15431582
def test_ci_selection_treats_catalog_test_change_as_not_shared(self) -> None:
15441583
with tempfile.TemporaryDirectory() as tmpdir:
15451584
root = Path(tmpdir)
@@ -1581,6 +1620,8 @@ def test_ci_selection_treats_catalog_test_change_as_not_shared(self) -> None:
15811620
"mutation_cargo_packages": [],
15821621
"has_mutation_cargo_packages": False,
15831622
"mutation_jobs": [],
1623+
"release_validation_tags": [],
1624+
"has_release_validation_tags": False,
15841625
},
15851626
)
15861627

@@ -1625,6 +1666,8 @@ def test_ci_selection_treats_shared_tool_changes_as_all_plugins(self) -> None:
16251666
"mutation_cargo_packages": [],
16261667
"has_mutation_cargo_packages": False,
16271668
"mutation_jobs": [],
1669+
"release_validation_tags": [],
1670+
"has_release_validation_tags": False,
16281671
},
16291672
)
16301673

@@ -1671,6 +1714,8 @@ def test_ci_selection_treats_tooling_config_changes_as_all_plugins(self) -> None
16711714
"mutation_cargo_packages": [],
16721715
"has_mutation_cargo_packages": False,
16731716
"mutation_jobs": [],
1717+
"release_validation_tags": [],
1718+
"has_release_validation_tags": False,
16741719
},
16751720
)
16761721

@@ -1749,6 +1794,8 @@ def test_ci_selection_treats_cargo_lock_change_as_all_plugins(self) -> None:
17491794
"mutation_cargo_packages": [],
17501795
"has_mutation_cargo_packages": False,
17511796
"mutation_jobs": [],
1797+
"release_validation_tags": [],
1798+
"has_release_validation_tags": False,
17521799
},
17531800
)
17541801

@@ -1792,6 +1839,8 @@ def test_ci_selection_treats_deny_config_change_as_all_plugins(self) -> None:
17921839
"mutation_cargo_packages": [],
17931840
"has_mutation_cargo_packages": False,
17941841
"mutation_jobs": [],
1842+
"release_validation_tags": [],
1843+
"has_release_validation_tags": False,
17951844
},
17961845
)
17971846

@@ -1837,6 +1886,8 @@ def test_changed_returns_plugin_for_plugin_integration_test_change(self) -> None
18371886
"mutation_cargo_packages": [],
18381887
"has_mutation_cargo_packages": False,
18391888
"mutation_jobs": [],
1889+
"release_validation_tags": [],
1890+
"has_release_validation_tags": False,
18401891
},
18411892
)
18421893

@@ -1882,6 +1933,8 @@ def test_ci_selection_treats_shared_plugin_tests_change_as_all_plugins(self) ->
18821933
"mutation_cargo_packages": [],
18831934
"has_mutation_cargo_packages": False,
18841935
"mutation_jobs": [],
1936+
"release_validation_tags": [],
1937+
"has_release_validation_tags": False,
18851938
},
18861939
)
18871940

@@ -1937,6 +1990,8 @@ def test_ci_selection_treats_shared_crate_changes_as_all_plugins(self) -> None:
19371990
"test_packages": ["rate_limiter"],
19381991
}
19391992
],
1993+
"release_validation_tags": [],
1994+
"has_release_validation_tags": False,
19401995
},
19411996
)
19421997

@@ -2177,6 +2232,8 @@ def test_ci_selection_reports_cargo_packages_for_single_plugin_diff(self) -> Non
21772232
"mutation_cargo_packages": [],
21782233
"has_mutation_cargo_packages": False,
21792234
"mutation_jobs": [],
2235+
"release_validation_tags": [],
2236+
"has_release_validation_tags": False,
21802237
},
21812238
)
21822239

@@ -2225,6 +2282,8 @@ def test_ci_selection_reports_mutation_package_for_single_rust_diff(self) -> Non
22252282
"mutation_jobs": [
22262283
{"cargo_package": "rate_limiter", "in_diff": True, "test_packages": []}
22272284
],
2285+
"release_validation_tags": [],
2286+
"has_release_validation_tags": False,
22282287
},
22292288
)
22302289

@@ -2271,6 +2330,14 @@ def test_ci_selection_field_prints_json_and_bool_scalars(self) -> None:
22712330
self.assertEqual(result.returncode, 0, result.stderr)
22722331
self.assertEqual(result.stdout.strip(), "true")
22732332

2333+
result = run_catalog("ci-selection-field", str(REPO_ROOT), "all", "", "", "release_validation_tags")
2334+
self.assertEqual(result.returncode, 0, result.stderr)
2335+
self.assertEqual(json.loads(result.stdout), [])
2336+
2337+
result = run_catalog("ci-selection-field", str(REPO_ROOT), "all", "", "", "has_release_validation_tags")
2338+
self.assertEqual(result.returncode, 0, result.stderr)
2339+
self.assertEqual(result.stdout.strip(), "false")
2340+
22742341
def test_ci_selection_field_supports_diff_mode(self) -> None:
22752342
with tempfile.TemporaryDirectory() as tmpdir:
22762343
root = Path(tmpdir)
@@ -2498,7 +2565,7 @@ def test_ci_workflow_uses_make_targets_for_plugin_checks(self) -> None:
24982565
self.assertIn("working-directory: plugins/rust/python-package/${{ matrix.plugin }}", workflow)
24992566
self.assertIn("release-validation:", workflow)
25002567
self.assertIn("uses: ./.github/workflows/release-rust-python-package.yaml", workflow)
2501-
self.assertIn("tag: retry-with-backoff-v0.2.0", workflow)
2568+
self.assertIn("tag: ${{ matrix.tag }}", workflow)
25022569
self.assertIn("repository: testpypi", workflow)
25032570
self.assertIn("publish_enabled: false", workflow)
25042571
self.assertNotIn("tools/plugin_catalog.py ci-selection-field", workflow)
@@ -2574,6 +2641,9 @@ def test_ci_workflow_includes_parity_jobs_for_rust_plugin_checks(self) -> None:
25742641
documentation_section = self._extract_workflow_job_section(
25752642
workflow, "documentation"
25762643
)
2644+
release_validation_section = self._extract_workflow_job_section(
2645+
workflow, "release-validation"
2646+
)
25772647
detect_run = self._extract_workflow_step_run(
25782648
workflow, "validate-and-detect", step_id="detect"
25792649
)
@@ -2621,6 +2691,8 @@ def test_ci_workflow_includes_parity_jobs_for_rust_plugin_checks(self) -> None:
26212691
self.assertIn("mutation_cargo_packages: ${{ steps.detect.outputs.mutation_cargo_packages }}", workflow)
26222692
self.assertIn("mutation_jobs: ${{ steps.detect.outputs.mutation_jobs }}", workflow)
26232693
self.assertIn("has_mutation_cargo_packages: ${{ steps.detect.outputs.has_mutation_cargo_packages }}", workflow)
2694+
self.assertIn("release_validation_tags: ${{ steps.detect.outputs.release_validation_tags }}", workflow)
2695+
self.assertIn("has_release_validation_tags: ${{ steps.detect.outputs.has_release_validation_tags }}", workflow)
26242696
self.assertIn("security-policy:", workflow)
26252697
self.assertIn("mutation-testing:", workflow)
26262698
self.assertIn("coverage:", workflow)
@@ -2630,6 +2702,8 @@ def test_ci_workflow_includes_parity_jobs_for_rust_plugin_checks(self) -> None:
26302702
self.assertIn("if: github.event_name == 'pull_request' && needs.validate-and-detect.outputs.has_mutation_cargo_packages == 'true'", mutants_section)
26312703
self.assertIn("if: needs.validate-and-detect.outputs.has_plugins == 'true'", coverage_section)
26322704
self.assertIn("if: needs.validate-and-detect.outputs.has_plugins == 'true'", documentation_section)
2705+
self.assertIn("if: github.event_name == 'pull_request' && needs.validate-and-detect.outputs.has_release_validation_tags == 'true'", release_validation_section)
2706+
self.assertIn("tag: ${{ fromJson(needs.validate-and-detect.outputs.release_validation_tags) }}", release_validation_section)
26332707
self.assertNotIn("cargo-audit", security_section)
26342708
self.assertNotIn("cargo audit", security_section)
26352709
self.assertIn("cargo install cargo-deny", security_section)
@@ -3259,7 +3333,9 @@ def test_release_workflow_tests_artifacts_outside_source_tree(self) -> None:
32593333
'venv_python="${tmpdir}/venv/Scripts/python.exe"',
32603334
workflow,
32613335
)
3336+
self.assertIn('uv pip install --python "${venv_python}" --group dev PyYAML', workflow)
32623337
self.assertIn('"${venv_python}" -m pip install', workflow)
3338+
self.assertNotIn('pytest pytest-asyncio PyYAML', workflow)
32633339
self.assertIn('"${venv_python}" -m pytest', workflow)
32643340
self.assertIn(
32653341
"actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd",

tools/plugin_catalog.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,31 @@ def _git_changed_paths(root: Path, base: str, head: str) -> list[str]:
486486
return [line for line in completed.stdout.splitlines() if line.strip()]
487487

488488

489+
def _git_file_text(root: Path, revision: str, path: str) -> str | None:
490+
completed = subprocess.run(
491+
["git", "show", f"{revision}:{path}"],
492+
cwd=root,
493+
text=True,
494+
capture_output=True,
495+
check=False,
496+
)
497+
if completed.returncode != 0:
498+
return None
499+
return completed.stdout
500+
501+
502+
def _cargo_version_from_text(text: str) -> str | None:
503+
try:
504+
payload = tomllib.loads(text)
505+
except tomllib.TOMLDecodeError:
506+
return None
507+
package = payload.get("package", {})
508+
if not isinstance(package, dict):
509+
return None
510+
version = package.get("version")
511+
return version if isinstance(version, str) else None
512+
513+
489514
def changed_plugins(root: Path, base: str, head: str) -> list[str]:
490515
plugins = discover_plugins(root)
491516
return _changed_plugins_for_records(root, plugins, _git_changed_paths(root, base, head))
@@ -563,6 +588,21 @@ def add_job(cargo_package: str, *, in_diff: bool, test_packages: list[str] | Non
563588
return [jobs[key] for key in sorted(jobs)]
564589

565590

591+
def _release_validation_tags_for_records(
592+
root: Path, plugins: list[PluginRecord], changed_paths: list[str], base: str
593+
) -> list[str]:
594+
tags: list[str] = []
595+
for plugin in plugins:
596+
cargo_path = f"{plugin.path}/Cargo.toml"
597+
if cargo_path not in changed_paths:
598+
continue
599+
old_text = _git_file_text(root, base, cargo_path)
600+
old_version = _cargo_version_from_text(old_text) if old_text is not None else None
601+
if old_version is not None and old_version != plugin.version:
602+
tags.append(f"{plugin.slug.replace('_', '-')}-v{plugin.version}")
603+
return sorted(tags)
604+
605+
566606
def ci_selection(root: Path, mode: str, base: str | None = None, head: str | None = None) -> dict:
567607
plugins = discover_plugins(root)
568608
plugin_lookup = {plugin.slug: plugin for plugin in plugins}
@@ -576,12 +616,16 @@ def ci_selection(root: Path, mode: str, base: str | None = None, head: str | Non
576616
}
577617
for slug in selected
578618
]
619+
release_validation_tags: list[str] = []
579620
else:
580621
if base is None or head is None:
581622
raise CatalogError("ci-selection diff mode requires base and head revisions")
582623
changed_paths = _git_changed_paths(root, base, head)
583624
selected = _changed_plugins_for_records(root, plugins, changed_paths)
584625
mutation_jobs = _mutation_jobs_for_records(root, plugins, changed_paths)
626+
release_validation_tags = _release_validation_tags_for_records(
627+
root, plugins, changed_paths, base
628+
)
585629
cargo_packages = [plugin_lookup[slug].cargo_package_name for slug in selected]
586630
mutation_cargo_packages = [str(job["cargo_package"]) for job in mutation_jobs]
587631
return {
@@ -592,6 +636,8 @@ def ci_selection(root: Path, mode: str, base: str | None = None, head: str | Non
592636
"mutation_cargo_packages": mutation_cargo_packages,
593637
"has_mutation_cargo_packages": bool(mutation_cargo_packages),
594638
"mutation_jobs": mutation_jobs,
639+
"release_validation_tags": release_validation_tags,
640+
"has_release_validation_tags": bool(release_validation_tags),
595641
}
596642

597643

@@ -840,6 +886,8 @@ def build_parser() -> argparse.ArgumentParser:
840886
"mutation_cargo_packages",
841887
"has_mutation_cargo_packages",
842888
"mutation_jobs",
889+
"release_validation_tags",
890+
"has_release_validation_tags",
843891
),
844892
)
845893

tools/validate_ci_selection.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,14 @@ def main() -> int:
4848
payload.get("mutation_cargo_packages"), "mutation_cargo_packages"
4949
)
5050
mutation_jobs = _assert_mutation_jobs(payload.get("mutation_jobs"))
51+
release_validation_tags = payload.get("release_validation_tags")
5152
has_plugins = payload.get("has_plugins")
5253
plugin_count = payload.get("plugin_count")
5354

55+
if not isinstance(release_validation_tags, list) or any(
56+
not isinstance(item, str) for item in release_validation_tags
57+
):
58+
raise AssertionError("release_validation_tags must be a string list")
5459
if not isinstance(has_plugins, bool):
5560
raise AssertionError("has_plugins must be bool")
5661
if not isinstance(plugin_count, int) or plugin_count != len(plugins):
@@ -66,6 +71,8 @@ def main() -> int:
6671
"mutation_cargo_packages": mutation_cargo_packages,
6772
"mutation_jobs": mutation_jobs,
6873
"has_mutation_cargo_packages": bool(mutation_cargo_packages),
74+
"release_validation_tags": release_validation_tags,
75+
"has_release_validation_tags": bool(release_validation_tags),
6976
}
7077
)
7178
)

0 commit comments

Comments
 (0)