alloc-context 0.1.0__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. {alloc_context-0.1.0 → alloc_context-0.1.2}/PKG-INFO +3 -1
  2. {alloc_context-0.1.0 → alloc_context-0.1.2}/README.md +2 -0
  3. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloc_context.egg-info/PKG-INFO +3 -1
  4. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloc_context.egg-info/SOURCES.txt +1 -0
  5. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/__init__.py +1 -1
  6. {alloc_context-0.1.0 → alloc_context-0.1.2}/pyproject.toml +1 -1
  7. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_bump_version.py +2 -2
  8. alloc_context-0.1.2/tests/test_server_json.py +35 -0
  9. alloc_context-0.1.2/tests/test_workflows.py +150 -0
  10. alloc_context-0.1.0/tests/test_workflows.py +0 -102
  11. {alloc_context-0.1.0 → alloc_context-0.1.2}/LICENSE +0 -0
  12. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloc_context.egg-info/dependency_links.txt +0 -0
  13. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloc_context.egg-info/entry_points.txt +0 -0
  14. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloc_context.egg-info/requires.txt +0 -0
  15. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloc_context.egg-info/top_level.txt +0 -0
  16. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/__main__.py +0 -0
  17. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/config.py +0 -0
  18. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/horizon.py +0 -0
  19. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/__init__.py +0 -0
  20. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/cf_benchmarks.py +0 -0
  21. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/cf_history.py +0 -0
  22. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/coinbase_client.py +0 -0
  23. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/coinbase_portfolio.py +0 -0
  24. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/coingecko.py +0 -0
  25. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/coinmarketcap.py +0 -0
  26. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/env_keys.py +0 -0
  27. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/etf_flows.py +0 -0
  28. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/exchange/__init__.py +0 -0
  29. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/exchange/coinbase_adapter.py +0 -0
  30. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/exchange/kraken_adapter.py +0 -0
  31. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/exchange/live.py +0 -0
  32. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/exchange/portfolio.py +0 -0
  33. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/exchange/registry.py +0 -0
  34. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/exchange/types.py +0 -0
  35. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/exchange_http.py +0 -0
  36. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/fear_greed.py +0 -0
  37. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/fred.py +0 -0
  38. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/http_errors.py +0 -0
  39. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/kalshi.py +0 -0
  40. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/kalshi_api.py +0 -0
  41. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/kalshi_client.py +0 -0
  42. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/kalshi_files.py +0 -0
  43. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/kalshi_state.py +0 -0
  44. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/kraken_client.py +0 -0
  45. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/kraken_portfolio.py +0 -0
  46. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/macro_calendar.py +0 -0
  47. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/macro_normalize.py +0 -0
  48. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/market_snapshots.py +0 -0
  49. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/outcome.py +0 -0
  50. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/parse_helpers.py +0 -0
  51. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/ingest/runner.py +0 -0
  52. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/__init__.py +0 -0
  53. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/assets.py +0 -0
  54. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/bazaar.py +0 -0
  55. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/contracts.py +0 -0
  56. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/handlers.py +0 -0
  57. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/http.py +0 -0
  58. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/payment_middleware.py +0 -0
  59. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/server.py +0 -0
  60. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/staleness.py +0 -0
  61. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/validation.py +0 -0
  62. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/x402_bazaar_dynamic.py +0 -0
  63. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/x402_config.py +0 -0
  64. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/x402_pricing.py +0 -0
  65. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/mcp/x402_stables.py +0 -0
  66. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/__init__.py +0 -0
  67. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/band.py +0 -0
  68. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/breadth.py +0 -0
  69. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/cf_math.py +0 -0
  70. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/cluster.py +0 -0
  71. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/cluster_config.py +0 -0
  72. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/comparison.py +0 -0
  73. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/context.py +0 -0
  74. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/delta.py +0 -0
  75. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/etf.py +0 -0
  76. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/fear_greed.py +0 -0
  77. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/macro.py +0 -0
  78. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/portfolio.py +0 -0
  79. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/rebalance.py +0 -0
  80. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/regime.py +0 -0
  81. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/sentiment.py +0 -0
  82. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/snapshots.py +0 -0
  83. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/rollup/tape.py +0 -0
  84. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/status_report.py +0 -0
  85. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/store/__init__.py +0 -0
  86. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/store/db.py +0 -0
  87. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/store/jsonutil.py +0 -0
  88. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/store/meta.py +0 -0
  89. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/store/retention.py +0 -0
  90. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/store/status.py +0 -0
  91. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/timeutil.py +0 -0
  92. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/x402_production_check.py +0 -0
  93. {alloc_context-0.1.0 → alloc_context-0.1.2}/alloccontext/x402_smoke_redact.py +0 -0
  94. {alloc_context-0.1.0 → alloc_context-0.1.2}/setup.cfg +0 -0
  95. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_adr005_ingest.py +0 -0
  96. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_backup_sqlite.py +0 -0
  97. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_coinbase_portfolio.py +0 -0
  98. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_context_snapshots.py +0 -0
  99. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_db_schema.py +0 -0
  100. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_deploy.py +0 -0
  101. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_dev_stack.py +0 -0
  102. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_etf.py +0 -0
  103. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_exchanges_config.py +0 -0
  104. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_fear_greed.py +0 -0
  105. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_fred.py +0 -0
  106. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_horizon.py +0 -0
  107. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_ingest_outcome.py +0 -0
  108. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_ingest_runner.py +0 -0
  109. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_kalshi_api.py +0 -0
  110. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_kraken_portfolio.py +0 -0
  111. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_macro.py +0 -0
  112. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_market_breadth.py +0 -0
  113. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_assets_regime.py +0 -0
  114. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_bazaar.py +0 -0
  115. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_contracts.py +0 -0
  116. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_freshness.py +0 -0
  117. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_handlers.py +0 -0
  118. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_health.py +0 -0
  119. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_http_lifecycle.py +0 -0
  120. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_live_portfolio.py +0 -0
  121. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_server.py +0 -0
  122. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_validation.py +0 -0
  123. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_x402.py +0 -0
  124. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_x402_http.py +0 -0
  125. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_x402_pricing.py +0 -0
  126. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_mcp_x402_stables.py +0 -0
  127. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_qa_ingest_store.py +0 -0
  128. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_rebalance.py +0 -0
  129. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_rollup.py +0 -0
  130. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_scaffold.py +0 -0
  131. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_script_runtime.py +0 -0
  132. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_security_hardening.py +0 -0
  133. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_snapshots_and_delta.py +0 -0
  134. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_status_report.py +0 -0
  135. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_x402_bazaar_dynamic.py +0 -0
  136. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_x402_production_check.py +0 -0
  137. {alloc_context-0.1.0 → alloc_context-0.1.2}/tests/test_x402_smoke_redact.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alloc-context
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: AllocContext — BTC/ETH allocation context, drift, and rebalance facts
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/negillett/alloc-context
@@ -42,6 +42,8 @@ Dynamic: license-file
42
42
 
43
43
  # AllocContext
44
44
 
45
+ mcp-name: io.github.negillett/alloc-context
46
+
45
47
  **Allocation context for BTC/ETH** — drift, band checks, USD rebalance moves,
46
48
  and a fused market backdrop (Fear & Greed, Kalshi, ETF flows, macro) as
47
49
  deterministic JSON over MCP.
@@ -1,5 +1,7 @@
1
1
  # AllocContext
2
2
 
3
+ mcp-name: io.github.negillett/alloc-context
4
+
3
5
  **Allocation context for BTC/ETH** — drift, band checks, USD rebalance moves,
4
6
  and a fused market backdrop (Fear & Greed, Kalshi, ETF flows, macro) as
5
7
  deterministic JSON over MCP.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alloc-context
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: AllocContext — BTC/ETH allocation context, drift, and rebalance facts
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/negillett/alloc-context
@@ -42,6 +42,8 @@ Dynamic: license-file
42
42
 
43
43
  # AllocContext
44
44
 
45
+ mcp-name: io.github.negillett/alloc-context
46
+
45
47
  **Allocation context for BTC/ETH** — drift, band checks, USD rebalance moves,
46
48
  and a fused market backdrop (Fear & Greed, Kalshi, ETF flows, macro) as
47
49
  deterministic JSON over MCP.
@@ -125,6 +125,7 @@ tests/test_rollup.py
125
125
  tests/test_scaffold.py
126
126
  tests/test_script_runtime.py
127
127
  tests/test_security_hardening.py
128
+ tests/test_server_json.py
128
129
  tests/test_snapshots_and_delta.py
129
130
  tests/test_status_report.py
130
131
  tests/test_workflows.py
@@ -1,3 +1,3 @@
1
1
  """AllocContext — BTC/ETH allocation context and rebalance facts."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "alloc-context"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "AllocContext — BTC/ETH allocation context, drift, and rebalance facts"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -45,7 +45,7 @@ def test_resolve_target_version_rejects_downgrade():
45
45
 
46
46
 
47
47
  def test_resolve_target_version_rejects_unchanged_exact():
48
- with pytest.raises(ValueError, match="tag-only"):
48
+ with pytest.raises(ValueError, match="already"):
49
49
  resolve_target_version(current="0.1.0", bump=None, exact="0.1.0")
50
50
 
51
51
 
@@ -55,7 +55,7 @@ def test_check_version_passes_when_in_sync(tmp_path: Path):
55
55
  dest = tmp_path / rel
56
56
  dest.parent.mkdir(parents=True, exist_ok=True)
57
57
  shutil.copy(src, dest)
58
- check_version("0.1.0", root=tmp_path)
58
+ check_version(read_current_version(REPO_ROOT), root=tmp_path)
59
59
 
60
60
 
61
61
  def test_check_version_fails_when_out_of_sync(tmp_path: Path):
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ REPO_ROOT = Path(__file__).resolve().parent.parent
7
+ SERVER_JSON = REPO_ROOT / "server.json"
8
+
9
+ # Official MCP Registry limit (see registry validation errors).
10
+ REGISTRY_DESCRIPTION_MAX_LEN = 100
11
+
12
+
13
+ def test_server_json_description_within_registry_limit():
14
+ data = json.loads(SERVER_JSON.read_text(encoding="utf-8"))
15
+ description = data["description"]
16
+ assert len(description) <= REGISTRY_DESCRIPTION_MAX_LEN, (
17
+ f"server.json description is {len(description)} chars; "
18
+ f"registry max is {REGISTRY_DESCRIPTION_MAX_LEN}"
19
+ )
20
+
21
+
22
+ def test_server_json_version_matches_pyproject():
23
+ import tomllib
24
+
25
+ py_ver = tomllib.loads(
26
+ (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")
27
+ )["project"]["version"]
28
+ data = json.loads(SERVER_JSON.read_text(encoding="utf-8"))
29
+ assert data["version"] == py_ver
30
+ assert data["packages"][0]["version"] == py_ver
31
+
32
+
33
+ def test_readme_includes_mcp_registry_name_for_pypi():
34
+ readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
35
+ assert "mcp-name: io.github.negillett/alloc-context" in readme
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+
7
+ REPO_ROOT = Path(__file__).resolve().parent.parent
8
+ WORKFLOWS_DIR = REPO_ROOT / ".github" / "workflows"
9
+
10
+
11
+ def _load_workflow(name: str) -> dict:
12
+ path = WORKFLOWS_DIR / name
13
+ assert path.exists(), f"missing workflow {name}"
14
+ with path.open() as handle:
15
+ return yaml.safe_load(handle)
16
+
17
+
18
+ def _job_steps(workflow: dict, job_name: str | None = None) -> list[dict]:
19
+ jobs = workflow["jobs"]
20
+ if job_name is None:
21
+ job_name = next(iter(jobs))
22
+ return jobs[job_name]["steps"]
23
+
24
+
25
+ def _workflow_on(workflow: dict) -> dict:
26
+ # PyYAML may parse bare `on:` as boolean True.
27
+ return workflow.get("on") or workflow[True]
28
+
29
+
30
+ def test_ci_runs_pytest():
31
+ workflow = _load_workflow("ci.yml")
32
+ steps = _job_steps(workflow, "test")
33
+ pytest_steps = [
34
+ step for step in steps if step.get("run", "").strip().startswith("pytest")
35
+ ]
36
+ assert pytest_steps, "ci workflow must run pytest"
37
+
38
+
39
+ def test_ci_runs_actionlint_from_workspace_binary():
40
+ workflow = _load_workflow("ci.yml")
41
+ steps = _job_steps(workflow, "test")
42
+ lint_steps = [step for step in steps if "actionlint" in step.get("run", "")]
43
+ assert lint_steps, "ci workflow must lint workflows"
44
+ run_script = lint_steps[0]["run"]
45
+ assert "./actionlint" in run_script
46
+ assert "1.7.12" in run_script
47
+
48
+
49
+ def test_ci_has_no_deploy_job():
50
+ workflow = _load_workflow("ci.yml")
51
+ assert "deploy" not in workflow["jobs"]
52
+
53
+
54
+ def test_bump_release_workflow_removed():
55
+ assert not (WORKFLOWS_DIR / "bump-release.yml").exists()
56
+
57
+
58
+ def test_release_pr_workflow_opens_pr():
59
+ workflow = _load_workflow("release-pr.yml")
60
+ on = _workflow_on(workflow)
61
+ assert "workflow_dispatch" in on
62
+ inputs = on["workflow_dispatch"]["inputs"]
63
+ assert "bump" in inputs
64
+ assert "exact_version" in inputs
65
+ # Opening a release PR must not publish or deploy.
66
+ assert "tag_only" not in inputs
67
+
68
+ steps = _job_steps(workflow, "open-release-pr")
69
+ runs = [step.get("run", "") for step in steps]
70
+ assert any("scripts/bump_version.py" in run for run in runs)
71
+ assert any("git push -u origin" in run and "release/v" in run for run in runs)
72
+ assert any("gh pr create" in run for run in runs)
73
+
74
+
75
+ def test_release_workflow_triggers_on_main_push_only():
76
+ workflow = _load_workflow("release.yml")
77
+ on = _workflow_on(workflow)
78
+ assert on["push"]["branches"] == ["main"]
79
+ # No manual or tag trigger — releases are driven by merges to main.
80
+ assert "workflow_dispatch" not in on
81
+ assert "tags" not in on["push"]
82
+ assert workflow["concurrency"]["group"].startswith("release-")
83
+
84
+
85
+ def test_release_workflow_gates_on_untagged_version():
86
+ workflow = _load_workflow("release.yml")
87
+ check = workflow["jobs"]["check"]
88
+ assert check["outputs"]["release"]
89
+ check_runs = [step.get("run", "") for step in _job_steps(workflow, "check")]
90
+ assert any("--current" in run for run in check_runs)
91
+ assert any("ls-remote --tags" in run for run in check_runs)
92
+ # Every downstream job is conditioned on the release decision.
93
+ for job_name in ("test", "publish-pypi", "publish-mcp-registry", "deploy", "finalize"):
94
+ cond = workflow["jobs"][job_name]["if"]
95
+ assert "needs.check.outputs.release" in cond
96
+
97
+
98
+ def test_release_workflow_publishes_then_deploys_then_finalizes():
99
+ workflow = _load_workflow("release.yml")
100
+ jobs = workflow["jobs"]
101
+
102
+ publish_steps = _job_steps(workflow, "publish-pypi")
103
+ publish_runs = [step.get("run", "") for step in publish_steps]
104
+ assert any("python -m build" in run for run in publish_runs)
105
+ pypi_step = next(
106
+ step
107
+ for step in publish_steps
108
+ if step.get("uses", "").startswith("pypa/gh-action-pypi-publish")
109
+ )
110
+ # Idempotent re-runs must not fail on an already-uploaded version.
111
+ assert pypi_step["with"]["skip-existing"] is True
112
+
113
+ registry_runs = [
114
+ step.get("run", "") for step in _job_steps(workflow, "publish-mcp-registry")
115
+ ]
116
+ assert any("publish-mcp-registry.sh" in run for run in registry_runs)
117
+
118
+ deploy_steps = _job_steps(workflow, "deploy")
119
+ names = [step.get("name", "") for step in deploy_steps]
120
+ assert "Rsync to VPS" in names
121
+ assert "Install on VPS" in names
122
+ install = next(step for step in deploy_steps if step.get("name") == "Install on VPS")
123
+ assert "deploy/remote-install.sh" in install["run"]
124
+
125
+ finalize = jobs["finalize"]
126
+ assert set(finalize["needs"]) == {
127
+ "check",
128
+ "publish-pypi",
129
+ "publish-mcp-registry",
130
+ "deploy",
131
+ }
132
+ finalize_runs = [step.get("run", "") for step in _job_steps(workflow, "finalize")]
133
+ assert any("git tag" in run for run in finalize_runs)
134
+ assert any("gh release create" in run for run in finalize_runs)
135
+
136
+
137
+ def test_release_workflow_drops_branch_juggling_jobs():
138
+ workflow = _load_workflow("release.yml")
139
+ jobs = workflow["jobs"]
140
+ for removed in ("gate", "prepare", "validate-version", "finalize-tag", "sync-main"):
141
+ assert removed not in jobs
142
+
143
+
144
+ def test_publish_mcp_registry_workflow_dispatch():
145
+ workflow = _load_workflow("publish-mcp-registry.yml")
146
+ on = _workflow_on(workflow)
147
+ assert "workflow_dispatch" in on
148
+ runs = [step.get("run", "") for step in _job_steps(workflow, "publish")]
149
+ assert any("install-mcp-publisher" in run for run in runs)
150
+ assert any("publish-mcp-registry.sh" in run for run in runs)
@@ -1,102 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
-
5
- import yaml
6
-
7
- REPO_ROOT = Path(__file__).resolve().parent.parent
8
- WORKFLOWS_DIR = REPO_ROOT / ".github" / "workflows"
9
-
10
-
11
- def _load_workflow(name: str) -> dict:
12
- path = WORKFLOWS_DIR / name
13
- assert path.exists(), f"missing workflow {name}"
14
- with path.open() as handle:
15
- return yaml.safe_load(handle)
16
-
17
-
18
- def _job_steps(workflow: dict, job_name: str | None = None) -> list[dict]:
19
- jobs = workflow["jobs"]
20
- if job_name is None:
21
- job_name = next(iter(jobs))
22
- return jobs[job_name]["steps"]
23
-
24
-
25
- def _workflow_on(workflow: dict) -> dict:
26
- # PyYAML may parse bare `on:` as boolean True.
27
- return workflow.get("on") or workflow[True]
28
-
29
-
30
- def test_ci_runs_pytest():
31
- workflow = _load_workflow("ci.yml")
32
- steps = _job_steps(workflow, "test")
33
- pytest_steps = [
34
- step for step in steps if step.get("run", "").strip().startswith("pytest")
35
- ]
36
- assert pytest_steps, "ci workflow must run pytest"
37
-
38
-
39
- def test_ci_runs_actionlint_from_workspace_binary():
40
- workflow = _load_workflow("ci.yml")
41
- steps = _job_steps(workflow, "test")
42
- lint_steps = [step for step in steps if "actionlint" in step.get("run", "")]
43
- assert lint_steps, "ci workflow must lint workflows"
44
- run_script = lint_steps[0]["run"]
45
- assert "./actionlint" in run_script
46
- assert "1.7.12" in run_script
47
-
48
-
49
- def test_ci_has_no_deploy_job():
50
- workflow = _load_workflow("ci.yml")
51
- assert "deploy" not in workflow["jobs"]
52
-
53
-
54
- def test_bump_release_workflow_removed():
55
- assert not (WORKFLOWS_DIR / "bump-release.yml").exists()
56
-
57
-
58
- def test_release_workflow_unified_pipeline():
59
- workflow = _load_workflow("release.yml")
60
- on = _workflow_on(workflow)
61
- assert "workflow_dispatch" in on
62
- inputs = on["workflow_dispatch"]["inputs"]
63
- assert "bump" in inputs
64
- assert "exact_version" in inputs
65
- assert "tag_only" in inputs
66
- assert on["push"]["tags"] == ["v[0-9]+.[0-9]+.[0-9]+"]
67
- assert workflow["concurrency"]["group"].startswith("release-")
68
-
69
- jobs = workflow["jobs"]
70
- assert jobs["deploy"]["needs"] == ["validate-version", "publish-pypi"]
71
- assert jobs["finalize-tag"]["needs"] == [
72
- "prepare",
73
- "validate-version",
74
- "publish-pypi",
75
- "deploy",
76
- ]
77
-
78
- prepare_runs = [step.get("run", "") for step in _job_steps(workflow, "prepare")]
79
- assert any("scripts/bump_version.py" in run for run in prepare_runs)
80
-
81
- validate_runs = [
82
- step.get("run", "") for step in _job_steps(workflow, "validate-version")
83
- ]
84
- assert any("scripts/bump_version.py --check" in run for run in validate_runs)
85
-
86
- deploy_steps = _job_steps(workflow, "deploy")
87
- names = [step.get("name", "") for step in deploy_steps]
88
- assert "Rsync to VPS" in names
89
- assert "Install on VPS" in names
90
- install = next(step for step in deploy_steps if step.get("name") == "Install on VPS")
91
- assert "deploy/remote-install.sh" in install["run"]
92
-
93
- publish_steps = _job_steps(workflow, "publish-pypi")
94
- publish_runs = [step.get("run", "") for step in publish_steps]
95
- assert any("python -m build" in run for run in publish_runs)
96
- assert any(
97
- step.get("uses", "").startswith("pypa/gh-action-pypi-publish")
98
- for step in publish_steps
99
- )
100
-
101
- finalize_runs = [step.get("run", "") for step in _job_steps(workflow, "finalize-tag")]
102
- assert any("git push origin" in run and "TAG" in run for run in finalize_runs)
File without changes
File without changes