ripple-board 0.1.0__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 (64) hide show
  1. ripple_board-0.1.0/MANIFEST.in +11 -0
  2. ripple_board-0.1.0/PKG-INFO +164 -0
  3. ripple_board-0.1.0/README.md +132 -0
  4. ripple_board-0.1.0/pyproject.toml +54 -0
  5. ripple_board-0.1.0/ripple/__init__.py +5 -0
  6. ripple_board-0.1.0/ripple/adapters/__init__.py +1 -0
  7. ripple_board-0.1.0/ripple/adapters/bitbucket.py +105 -0
  8. ripple_board-0.1.0/ripple/adapters/github.py +101 -0
  9. ripple_board-0.1.0/ripple/adapters/gitlab.py +116 -0
  10. ripple_board-0.1.0/ripple/adapters/jira.py +263 -0
  11. ripple_board-0.1.0/ripple/adapters/worker.py +399 -0
  12. ripple_board-0.1.0/ripple/api/__init__.py +1 -0
  13. ripple_board-0.1.0/ripple/api/app.py +2502 -0
  14. ripple_board-0.1.0/ripple/cli.py +351 -0
  15. ripple_board-0.1.0/ripple/compat/__init__.py +1 -0
  16. ripple_board-0.1.0/ripple/compat/contracts.py +224 -0
  17. ripple_board-0.1.0/ripple/config/__init__.py +1 -0
  18. ripple_board-0.1.0/ripple/config/redaction.py +18 -0
  19. ripple_board-0.1.0/ripple/config/ripple.config.example.json +37 -0
  20. ripple_board-0.1.0/ripple/config/settings.py +209 -0
  21. ripple_board-0.1.0/ripple/core/analyzer.py +283 -0
  22. ripple_board-0.1.0/ripple/core/context.py +79 -0
  23. ripple_board-0.1.0/ripple/core/estate.py +118 -0
  24. ripple_board-0.1.0/ripple/core/graph.py +148 -0
  25. ripple_board-0.1.0/ripple/core/impact.py +213 -0
  26. ripple_board-0.1.0/ripple/core/indexer.py +1752 -0
  27. ripple_board-0.1.0/ripple/core/java_ast.py +701 -0
  28. ripple_board-0.1.0/ripple/core/models.py +295 -0
  29. ripple_board-0.1.0/ripple/core/planner.py +644 -0
  30. ripple_board-0.1.0/ripple/core/semantic.py +100 -0
  31. ripple_board-0.1.0/ripple/core/understanding.py +164 -0
  32. ripple_board-0.1.0/ripple/delivery/__init__.py +1 -0
  33. ripple_board-0.1.0/ripple/delivery/changeset.py +79 -0
  34. ripple_board-0.1.0/ripple/delivery/gitlab_delivery.py +276 -0
  35. ripple_board-0.1.0/ripple/delivery/onboarding.py +33 -0
  36. ripple_board-0.1.0/ripple/incidents/__init__.py +1 -0
  37. ripple_board-0.1.0/ripple/incidents/otel.py +76 -0
  38. ripple_board-0.1.0/ripple/memory/__init__.py +1 -0
  39. ripple_board-0.1.0/ripple/memory/store.py +55 -0
  40. ripple_board-0.1.0/ripple/observability/__init__.py +1 -0
  41. ripple_board-0.1.0/ripple/observability/jobs.py +172 -0
  42. ripple_board-0.1.0/ripple/policy/__init__.py +1 -0
  43. ripple_board-0.1.0/ripple/policy/audit.py +28 -0
  44. ripple_board-0.1.0/ripple/policy/engine.py +23 -0
  45. ripple_board-0.1.0/ripple/server.py +111 -0
  46. ripple_board-0.1.0/ripple/storage/__init__.py +1 -0
  47. ripple_board-0.1.0/ripple/storage/postgres.py +106 -0
  48. ripple_board-0.1.0/ripple/storage/sqlite.py +161 -0
  49. ripple_board-0.1.0/ripple/subscription/__init__.py +0 -0
  50. ripple_board-0.1.0/ripple/subscription/license.py +194 -0
  51. ripple_board-0.1.0/ripple/verification/__init__.py +1 -0
  52. ripple_board-0.1.0/ripple/verification/engine.py +97 -0
  53. ripple_board-0.1.0/ripple/web/index.html +437 -0
  54. ripple_board-0.1.0/ripple/web/main.js +3811 -0
  55. ripple_board-0.1.0/ripple/web/styles.css +3340 -0
  56. ripple_board-0.1.0/ripple/workspace/__init__.py +1 -0
  57. ripple_board-0.1.0/ripple/workspace/manager.py +45 -0
  58. ripple_board-0.1.0/ripple_board.egg-info/PKG-INFO +164 -0
  59. ripple_board-0.1.0/ripple_board.egg-info/SOURCES.txt +62 -0
  60. ripple_board-0.1.0/ripple_board.egg-info/dependency_links.txt +1 -0
  61. ripple_board-0.1.0/ripple_board.egg-info/entry_points.txt +5 -0
  62. ripple_board-0.1.0/ripple_board.egg-info/requires.txt +17 -0
  63. ripple_board-0.1.0/ripple_board.egg-info/top_level.txt +1 -0
  64. ripple_board-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,11 @@
1
+ include README.md
2
+ recursive-include ripple/web *.html *.js *.css
3
+ recursive-include ripple/config *.json
4
+ global-exclude *.py[cod]
5
+ global-exclude __pycache__
6
+ recursive-exclude * __pycache__
7
+ recursive-exclude .ripple *
8
+ recursive-exclude tests *
9
+ exclude .env
10
+ exclude .env.*
11
+ exclude config/ripple.config.json
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: ripple-board
3
+ Version: 0.1.0
4
+ Summary: Architecture-aware autonomous engineering control plane for Java/Spring microservices.
5
+ Author: Ripple
6
+ Project-URL: Homepage, https://github.com/ripple-board/ripple-board
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Environment :: Console
9
+ Classifier: Framework :: FastAPI
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: fastapi>=0.110
19
+ Requires-Dist: javalang>=0.13.0
20
+ Requires-Dist: tree-sitter>=0.22.0
21
+ Requires-Dist: tree-sitter-java>=0.21.0
22
+ Requires-Dist: uvicorn>=0.27
23
+ Provides-Extra: api
24
+ Requires-Dist: fastapi>=0.110; extra == "api"
25
+ Requires-Dist: uvicorn>=0.27; extra == "api"
26
+ Provides-Extra: ast
27
+ Requires-Dist: javalang>=0.13.0; extra == "ast"
28
+ Requires-Dist: tree-sitter>=0.22.0; extra == "ast"
29
+ Requires-Dist: tree-sitter-java>=0.21.0; extra == "ast"
30
+ Provides-Extra: monetization
31
+ Requires-Dist: pymongo[srv]>=4.6; extra == "monetization"
32
+
33
+ # Ripple
34
+
35
+ Ripple is an architecture-aware engineering control plane for enterprise microservices. This starter implements the first V1 slice from the product blueprint:
36
+
37
+ - Discover Java/Spring repositories from a local group directory.
38
+ - Extract structured architecture facts into nodes and edges.
39
+ - Analyze a requirement against the architecture graph.
40
+ - Calculate bounded blast radius with evidence and risk.
41
+ - Generate a structured ChangePlan.
42
+ - Expose the same flow through a small CLI and optional FastAPI app.
43
+
44
+ The implementation keeps optional enterprise integrations separate, but a normal install includes the local web/API server and Java indexing support.
45
+
46
+ ## Quick Start
47
+
48
+ ```bash
49
+ python3 -m ripple.cli index examples --out .ripple/graph.json
50
+ python3 -m ripple.cli analyze "Add riskCategory to RiskResponse and include it in audit findings" --graph .ripple/graph.json
51
+ python3 -m ripple.cli plan "Add riskCategory to RiskResponse and include it in audit findings" --graph .ripple/graph.json
52
+ python3 -m ripple.cli verify "Add riskCategory to RiskResponse and include it in audit findings" --graph .ripple/graph.json --workspace examples
53
+ python3 -m ripple.cli changeset "Add riskCategory to RiskResponse and include it in audit findings" --graph .ripple/graph.json
54
+ python3 -m unittest discover -s tests
55
+ ```
56
+
57
+ ## Real Jira and GitLab Configuration
58
+
59
+ Put secrets in environment variables or a local `.env` file. Do not commit `.env`; it is ignored.
60
+
61
+ ```bash
62
+ cp .env.example .env
63
+ cp config/ripple.config.example.json config/ripple.config.json
64
+ ripple-board config
65
+ ripple-board jira GRC-4721
66
+ ripple-board gitlab-projects your/gitlab/group
67
+ ```
68
+
69
+ `ripple-board config` redacts tokens before printing configuration.
70
+
71
+ ## API Development
72
+
73
+ Run the FastAPI app directly during local development:
74
+
75
+ ```bash
76
+ pip install -e .
77
+ uvicorn ripple.api.app:app --reload
78
+ ```
79
+
80
+ ## Installable Local Product
81
+
82
+ Ripple can be installed on a user's workstation and run locally:
83
+
84
+ ```bash
85
+ python -m pip install ripple-board
86
+ ripple-board server --host 127.0.0.1 --port 8010
87
+ ```
88
+
89
+ For development from this repository:
90
+
91
+ ```bash
92
+ python -m pip install -e .
93
+ ripple-board server --host 127.0.0.1 --port 8010
94
+ ```
95
+
96
+ The web console is then available at `http://127.0.0.1:8010`.
97
+
98
+ ## Run 24/7 On The Local Machine
99
+
100
+ On Linux systems with user-level systemd, install Ripple as a background service:
101
+
102
+ ```bash
103
+ ripple-board service install --host 127.0.0.1 --port 8010 --working-directory "$PWD"
104
+ ```
105
+
106
+ Manage it with:
107
+
108
+ ```bash
109
+ ripple-board service status
110
+ ripple-board service uninstall
111
+ ```
112
+
113
+ The service is installed at `~/.config/systemd/user/ripple-board.service`, starts immediately, restarts on failure, and starts again when the user's systemd session starts. For true boot-time availability before login, the machine owner can enable lingering with `loginctl enable-linger "$USER"`.
114
+
115
+ ## Publishing
116
+
117
+ Build and validate the package before uploading:
118
+
119
+ ```bash
120
+ python -m pip install build twine
121
+ python -m build
122
+ python -m twine check dist/*
123
+ ```
124
+
125
+ Publish to TestPyPI first, install into a fresh virtual environment, then publish the same release to PyPI:
126
+
127
+ ```bash
128
+ python -m twine upload --repository testpypi dist/*
129
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple ripple-board
130
+ ripple-board --help
131
+ ripple-board-server --help
132
+ ```
133
+
134
+ ## Current Scope
135
+
136
+ This is the foundation, not the full platform. It starts the PDF roadmap at phase 1 and phase 3:
137
+
138
+ - Structured architecture graph emitted from source.
139
+ - Local service discovery.
140
+ - Requirement routing.
141
+ - Relation-aware impact traversal.
142
+ - Transparent risk scoring.
143
+ - Reviewable change planning.
144
+
145
+ Next major steps are GitLab/Jira adapters, persistent PostgreSQL storage, stronger Java parsing, verification gates, and ChangeSet/MR delivery.
146
+
147
+ ## Implemented Product Layers
148
+
149
+ - Architecture graph: JSON and SQLite persistence.
150
+ - Indexing: Java/Spring route, Feign, entity, DTO, Kafka listener, and config fact extraction.
151
+ - Requirement analysis: local text and file-backed Jira ingestion.
152
+ - Blast radius: relation-aware bounded traversal with risk levels and evidence.
153
+ - Planning: structured ChangePlan with approval requirements and verification evidence.
154
+ - Policy: approval gates for medium/high risk changes.
155
+ - Verification: per-service command planning and execution.
156
+ - Compatibility: REST contract snapshot and diff primitives.
157
+ - Delivery: ChangeSet persistence and planned MR records.
158
+ - Worker adapter: dry-run worker behind the same interface intended for Claude/Codex/Gemini.
159
+ - Workspace manager: isolated local workspace copies.
160
+ - Incident path: OpenTelemetry-style JSON normalization.
161
+ - Audit: JSONL audit trail for major CLI actions.
162
+ - Web/API: optional FastAPI endpoints and a static console shell.
163
+
164
+ Production GitLab/Jira/LLM integrations still need real credentials and network access. PostgreSQL/pgvector can replace the SQLite store behind the same graph persistence boundary.
@@ -0,0 +1,132 @@
1
+ # Ripple
2
+
3
+ Ripple is an architecture-aware engineering control plane for enterprise microservices. This starter implements the first V1 slice from the product blueprint:
4
+
5
+ - Discover Java/Spring repositories from a local group directory.
6
+ - Extract structured architecture facts into nodes and edges.
7
+ - Analyze a requirement against the architecture graph.
8
+ - Calculate bounded blast radius with evidence and risk.
9
+ - Generate a structured ChangePlan.
10
+ - Expose the same flow through a small CLI and optional FastAPI app.
11
+
12
+ The implementation keeps optional enterprise integrations separate, but a normal install includes the local web/API server and Java indexing support.
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ python3 -m ripple.cli index examples --out .ripple/graph.json
18
+ python3 -m ripple.cli analyze "Add riskCategory to RiskResponse and include it in audit findings" --graph .ripple/graph.json
19
+ python3 -m ripple.cli plan "Add riskCategory to RiskResponse and include it in audit findings" --graph .ripple/graph.json
20
+ python3 -m ripple.cli verify "Add riskCategory to RiskResponse and include it in audit findings" --graph .ripple/graph.json --workspace examples
21
+ python3 -m ripple.cli changeset "Add riskCategory to RiskResponse and include it in audit findings" --graph .ripple/graph.json
22
+ python3 -m unittest discover -s tests
23
+ ```
24
+
25
+ ## Real Jira and GitLab Configuration
26
+
27
+ Put secrets in environment variables or a local `.env` file. Do not commit `.env`; it is ignored.
28
+
29
+ ```bash
30
+ cp .env.example .env
31
+ cp config/ripple.config.example.json config/ripple.config.json
32
+ ripple-board config
33
+ ripple-board jira GRC-4721
34
+ ripple-board gitlab-projects your/gitlab/group
35
+ ```
36
+
37
+ `ripple-board config` redacts tokens before printing configuration.
38
+
39
+ ## API Development
40
+
41
+ Run the FastAPI app directly during local development:
42
+
43
+ ```bash
44
+ pip install -e .
45
+ uvicorn ripple.api.app:app --reload
46
+ ```
47
+
48
+ ## Installable Local Product
49
+
50
+ Ripple can be installed on a user's workstation and run locally:
51
+
52
+ ```bash
53
+ python -m pip install ripple-board
54
+ ripple-board server --host 127.0.0.1 --port 8010
55
+ ```
56
+
57
+ For development from this repository:
58
+
59
+ ```bash
60
+ python -m pip install -e .
61
+ ripple-board server --host 127.0.0.1 --port 8010
62
+ ```
63
+
64
+ The web console is then available at `http://127.0.0.1:8010`.
65
+
66
+ ## Run 24/7 On The Local Machine
67
+
68
+ On Linux systems with user-level systemd, install Ripple as a background service:
69
+
70
+ ```bash
71
+ ripple-board service install --host 127.0.0.1 --port 8010 --working-directory "$PWD"
72
+ ```
73
+
74
+ Manage it with:
75
+
76
+ ```bash
77
+ ripple-board service status
78
+ ripple-board service uninstall
79
+ ```
80
+
81
+ The service is installed at `~/.config/systemd/user/ripple-board.service`, starts immediately, restarts on failure, and starts again when the user's systemd session starts. For true boot-time availability before login, the machine owner can enable lingering with `loginctl enable-linger "$USER"`.
82
+
83
+ ## Publishing
84
+
85
+ Build and validate the package before uploading:
86
+
87
+ ```bash
88
+ python -m pip install build twine
89
+ python -m build
90
+ python -m twine check dist/*
91
+ ```
92
+
93
+ Publish to TestPyPI first, install into a fresh virtual environment, then publish the same release to PyPI:
94
+
95
+ ```bash
96
+ python -m twine upload --repository testpypi dist/*
97
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple ripple-board
98
+ ripple-board --help
99
+ ripple-board-server --help
100
+ ```
101
+
102
+ ## Current Scope
103
+
104
+ This is the foundation, not the full platform. It starts the PDF roadmap at phase 1 and phase 3:
105
+
106
+ - Structured architecture graph emitted from source.
107
+ - Local service discovery.
108
+ - Requirement routing.
109
+ - Relation-aware impact traversal.
110
+ - Transparent risk scoring.
111
+ - Reviewable change planning.
112
+
113
+ Next major steps are GitLab/Jira adapters, persistent PostgreSQL storage, stronger Java parsing, verification gates, and ChangeSet/MR delivery.
114
+
115
+ ## Implemented Product Layers
116
+
117
+ - Architecture graph: JSON and SQLite persistence.
118
+ - Indexing: Java/Spring route, Feign, entity, DTO, Kafka listener, and config fact extraction.
119
+ - Requirement analysis: local text and file-backed Jira ingestion.
120
+ - Blast radius: relation-aware bounded traversal with risk levels and evidence.
121
+ - Planning: structured ChangePlan with approval requirements and verification evidence.
122
+ - Policy: approval gates for medium/high risk changes.
123
+ - Verification: per-service command planning and execution.
124
+ - Compatibility: REST contract snapshot and diff primitives.
125
+ - Delivery: ChangeSet persistence and planned MR records.
126
+ - Worker adapter: dry-run worker behind the same interface intended for Claude/Codex/Gemini.
127
+ - Workspace manager: isolated local workspace copies.
128
+ - Incident path: OpenTelemetry-style JSON normalization.
129
+ - Audit: JSONL audit trail for major CLI actions.
130
+ - Web/API: optional FastAPI endpoints and a static console shell.
131
+
132
+ Production GitLab/Jira/LLM integrations still need real credentials and network access. PostgreSQL/pgvector can replace the SQLite store behind the same graph persistence boundary.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ripple-board"
7
+ version = "0.1.0"
8
+ description = "Architecture-aware autonomous engineering control plane for Java/Spring microservices."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{name = "Ripple"}]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Framework :: FastAPI",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development",
22
+ ]
23
+ dependencies = [
24
+ "fastapi>=0.110",
25
+ "javalang>=0.13.0",
26
+ "tree-sitter>=0.22.0",
27
+ "tree-sitter-java>=0.21.0",
28
+ "uvicorn>=0.27",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/ripple-board/ripple-board"
33
+
34
+ [project.optional-dependencies]
35
+ api = ["fastapi>=0.110", "uvicorn>=0.27"]
36
+ ast = ["javalang>=0.13.0", "tree-sitter>=0.22.0", "tree-sitter-java>=0.21.0"]
37
+ monetization = ["pymongo[srv]>=4.6"]
38
+
39
+ [project.scripts]
40
+ ripple-board = "ripple.cli:main"
41
+ ripple-board-server = "ripple.server:main"
42
+ ripple = "ripple.cli:main"
43
+ ripple-server = "ripple.server:main"
44
+
45
+ [tool.setuptools.packages.find]
46
+ include = ["ripple*"]
47
+ exclude = ["config*", "examples*", "tests*"]
48
+
49
+ [tool.setuptools.package-data]
50
+ "ripple.config" = ["*.json"]
51
+ "ripple.web" = ["*.html", "*.js", "*.css"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
@@ -0,0 +1,5 @@
1
+ """Ripple architecture intelligence starter package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """External integration boundaries for GitLab, Jira, coding agents, and verification."""
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ import urllib.error
6
+ import urllib.parse
7
+ import urllib.request
8
+
9
+ from ripple.adapters.gitlab import GitLabAdapter, GitLabProject
10
+ from ripple.config.settings import BitbucketSettings
11
+
12
+
13
+ class BitbucketAdapter(GitLabAdapter):
14
+ def __init__(self, settings: BitbucketSettings) -> None:
15
+ self.settings = settings
16
+
17
+ def list_projects(self, group: str = "") -> list[GitLabProject]:
18
+ workspace = group.strip()
19
+ if not workspace:
20
+ raise RuntimeError("Bitbucket workspace is required to list repositories.")
21
+ path = f"/2.0/repositories/{urllib.parse.quote(workspace)}?pagelen=100"
22
+ projects = []
23
+ while path:
24
+ data = self._request("GET", path)
25
+ for raw in data.get("values", []):
26
+ if raw.get("is_private") is None and raw.get("type") != "repository":
27
+ continue
28
+ links = raw.get("links", {})
29
+ clone_url = _clone_url(links.get("clone", []))
30
+ main_branch = raw.get("mainbranch") or {}
31
+ projects.append(
32
+ GitLabProject(
33
+ id=str(raw.get("full_name") or raw.get("uuid") or raw.get("slug")),
34
+ name=str(raw.get("slug") or raw.get("name") or ""),
35
+ clone_url=clone_url,
36
+ default_branch=main_branch.get("name") or "master",
37
+ )
38
+ )
39
+ path = _relative_next(data.get("next", ""), _api_base(self.settings.base_url))
40
+ return projects
41
+
42
+ def create_merge_request(self, project: GitLabProject, source_branch: str, target_branch: str, title: str, description: str = "") -> str:
43
+ data = self._request(
44
+ "POST",
45
+ f"/2.0/repositories/{urllib.parse.quote(project.id, safe='/')}/pullrequests",
46
+ {
47
+ "title": title,
48
+ "description": description,
49
+ "source": {"branch": {"name": source_branch}},
50
+ "destination": {"branch": {"name": target_branch}},
51
+ "close_source_branch": True,
52
+ },
53
+ )
54
+ return data.get("links", {}).get("html", {}).get("href", "")
55
+
56
+ def authenticated_url(self, url: str) -> str:
57
+ if not self.settings.username or not self.settings.app_password or not url.startswith(("http://", "https://")):
58
+ return ""
59
+ parsed = urllib.parse.urlsplit(url)
60
+ username = urllib.parse.quote(self.settings.username, safe="")
61
+ password = urllib.parse.quote(self.settings.app_password, safe="")
62
+ netloc = f"{username}:{password}@{parsed.hostname or ''}"
63
+ if parsed.port:
64
+ netloc = f"{netloc}:{parsed.port}"
65
+ return urllib.parse.urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment))
66
+
67
+ def _request(self, method: str, path: str, body: dict | None = None) -> dict:
68
+ url = f"{_api_base(self.settings.base_url)}{path}" if path.startswith("/") else path
69
+ payload = json.dumps(body).encode() if body is not None else None
70
+ request = urllib.request.Request(url, data=payload, method=method)
71
+ token = base64.b64encode(f"{self.settings.username}:{self.settings.app_password}".encode()).decode()
72
+ request.add_header("Authorization", f"Basic {token}")
73
+ request.add_header("Accept", "application/json")
74
+ request.add_header("Content-Type", "application/json")
75
+ try:
76
+ with urllib.request.urlopen(request, timeout=30) as response:
77
+ text = response.read().decode()
78
+ except urllib.error.HTTPError as exc:
79
+ detail = exc.read().decode(errors="ignore")[:500]
80
+ raise RuntimeError(f"Bitbucket API {method} {path} failed with HTTP {exc.code}: {detail}") from exc
81
+ except urllib.error.URLError as exc:
82
+ raise RuntimeError(f"Bitbucket API {method} {path} failed: {exc.reason}") from exc
83
+ return json.loads(text) if text else {}
84
+
85
+
86
+ def _clone_url(clones: list[dict]) -> str:
87
+ https = next((item.get("href", "") for item in clones if item.get("name") == "https"), "")
88
+ return https or next((item.get("href", "") for item in clones if item.get("href")), "")
89
+
90
+
91
+ def _relative_next(url: str, base_url: str) -> str:
92
+ if not url:
93
+ return ""
94
+ parsed = urllib.parse.urlsplit(url)
95
+ base = urllib.parse.urlsplit(base_url)
96
+ if parsed.netloc and parsed.netloc != base.netloc:
97
+ return ""
98
+ return f"{parsed.path}?{parsed.query}" if parsed.query else parsed.path
99
+
100
+
101
+ def _api_base(base_url: str) -> str:
102
+ normalized = base_url.strip().strip("/")
103
+ if normalized in {"https://bitbucket.org", "http://bitbucket.org", "bitbucket.org"}:
104
+ return "https://api.bitbucket.org"
105
+ return normalized
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import urllib.error
5
+ import urllib.parse
6
+ import urllib.request
7
+
8
+ from ripple.adapters.gitlab import GitLabAdapter, GitLabProject
9
+ from ripple.config.settings import GitHubSettings
10
+
11
+
12
+ class GitHubAdapter(GitLabAdapter):
13
+ def __init__(self, settings: GitHubSettings) -> None:
14
+ self.settings = settings
15
+
16
+ def list_projects(self, group: str = "") -> list[GitLabProject]:
17
+ path = f"/orgs/{urllib.parse.quote(group)}/repos?per_page=100&type=all" if group else "/user/repos?per_page=100&type=all"
18
+ projects = []
19
+ while path:
20
+ data, headers = self._request("GET", path, with_headers=True)
21
+ for raw in data:
22
+ if raw.get("archived") or raw.get("disabled"):
23
+ continue
24
+ projects.append(
25
+ GitLabProject(
26
+ id=str(raw.get("full_name") or raw.get("id")),
27
+ name=str(raw.get("name") or raw.get("full_name") or ""),
28
+ clone_url=raw.get("clone_url") or raw.get("html_url") or "",
29
+ default_branch=raw.get("default_branch") or "master",
30
+ )
31
+ )
32
+ path = _next_link(headers.get("Link", ""))
33
+ return projects
34
+
35
+ def create_merge_request(self, project: GitLabProject, source_branch: str, target_branch: str, title: str, description: str = "") -> str:
36
+ data, _headers = self._request(
37
+ "POST",
38
+ f"/repos/{urllib.parse.quote(project.id, safe='/')}/pulls",
39
+ {
40
+ "title": title,
41
+ "head": source_branch,
42
+ "base": target_branch,
43
+ "body": description,
44
+ "maintainer_can_modify": True,
45
+ },
46
+ with_headers=True,
47
+ )
48
+ return data.get("html_url", "")
49
+
50
+ def authenticated_url(self, url: str) -> str:
51
+ if not self.settings.token or not url.startswith(("http://", "https://")):
52
+ return ""
53
+ parsed = urllib.parse.urlsplit(url)
54
+ username = urllib.parse.quote("x-access-token", safe="")
55
+ password = urllib.parse.quote(self.settings.token, safe="")
56
+ netloc = f"{username}:{password}@{parsed.hostname or ''}"
57
+ if parsed.port:
58
+ netloc = f"{netloc}:{parsed.port}"
59
+ return urllib.parse.urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment))
60
+
61
+ def _request(self, method: str, path: str, body: dict | None = None, with_headers: bool = False):
62
+ url = f"{_api_base(self.settings.base_url)}{path}"
63
+ payload = json.dumps(body).encode() if body is not None else None
64
+ request = urllib.request.Request(url, data=payload, method=method)
65
+ request.add_header("Authorization", f"Bearer {self.settings.token}")
66
+ request.add_header("Accept", "application/vnd.github+json")
67
+ request.add_header("Content-Type", "application/json")
68
+ request.add_header("X-GitHub-Api-Version", "2022-11-28")
69
+ try:
70
+ with urllib.request.urlopen(request, timeout=30) as response:
71
+ text = response.read().decode()
72
+ data = json.loads(text) if text else {}
73
+ headers = dict(response.headers.items())
74
+ except urllib.error.HTTPError as exc:
75
+ detail = exc.read().decode(errors="ignore")[:500]
76
+ raise RuntimeError(f"GitHub API {method} {path} failed with HTTP {exc.code}: {detail}") from exc
77
+ except urllib.error.URLError as exc:
78
+ raise RuntimeError(f"GitHub API {method} {path} failed: {exc.reason}") from exc
79
+ return (data, headers) if with_headers else data
80
+
81
+
82
+ def _api_base(base_url: str) -> str:
83
+ normalized = base_url.strip().strip("/")
84
+ if normalized in {"https://github.com", "http://github.com", "github.com"}:
85
+ return "https://api.github.com"
86
+ if normalized.endswith("/api/v3"):
87
+ return normalized
88
+ return f"{normalized}/api/v3"
89
+
90
+
91
+ def _next_link(header: str) -> str:
92
+ for part in header.split(","):
93
+ if 'rel="next"' not in part:
94
+ continue
95
+ start = part.find("<")
96
+ end = part.find(">")
97
+ if start >= 0 and end > start:
98
+ url = part[start + 1 : end]
99
+ parsed = urllib.parse.urlsplit(url)
100
+ return f"{parsed.path}?{parsed.query}" if parsed.query else parsed.path
101
+ return ""