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.
- ripple_board-0.1.0/MANIFEST.in +11 -0
- ripple_board-0.1.0/PKG-INFO +164 -0
- ripple_board-0.1.0/README.md +132 -0
- ripple_board-0.1.0/pyproject.toml +54 -0
- ripple_board-0.1.0/ripple/__init__.py +5 -0
- ripple_board-0.1.0/ripple/adapters/__init__.py +1 -0
- ripple_board-0.1.0/ripple/adapters/bitbucket.py +105 -0
- ripple_board-0.1.0/ripple/adapters/github.py +101 -0
- ripple_board-0.1.0/ripple/adapters/gitlab.py +116 -0
- ripple_board-0.1.0/ripple/adapters/jira.py +263 -0
- ripple_board-0.1.0/ripple/adapters/worker.py +399 -0
- ripple_board-0.1.0/ripple/api/__init__.py +1 -0
- ripple_board-0.1.0/ripple/api/app.py +2502 -0
- ripple_board-0.1.0/ripple/cli.py +351 -0
- ripple_board-0.1.0/ripple/compat/__init__.py +1 -0
- ripple_board-0.1.0/ripple/compat/contracts.py +224 -0
- ripple_board-0.1.0/ripple/config/__init__.py +1 -0
- ripple_board-0.1.0/ripple/config/redaction.py +18 -0
- ripple_board-0.1.0/ripple/config/ripple.config.example.json +37 -0
- ripple_board-0.1.0/ripple/config/settings.py +209 -0
- ripple_board-0.1.0/ripple/core/analyzer.py +283 -0
- ripple_board-0.1.0/ripple/core/context.py +79 -0
- ripple_board-0.1.0/ripple/core/estate.py +118 -0
- ripple_board-0.1.0/ripple/core/graph.py +148 -0
- ripple_board-0.1.0/ripple/core/impact.py +213 -0
- ripple_board-0.1.0/ripple/core/indexer.py +1752 -0
- ripple_board-0.1.0/ripple/core/java_ast.py +701 -0
- ripple_board-0.1.0/ripple/core/models.py +295 -0
- ripple_board-0.1.0/ripple/core/planner.py +644 -0
- ripple_board-0.1.0/ripple/core/semantic.py +100 -0
- ripple_board-0.1.0/ripple/core/understanding.py +164 -0
- ripple_board-0.1.0/ripple/delivery/__init__.py +1 -0
- ripple_board-0.1.0/ripple/delivery/changeset.py +79 -0
- ripple_board-0.1.0/ripple/delivery/gitlab_delivery.py +276 -0
- ripple_board-0.1.0/ripple/delivery/onboarding.py +33 -0
- ripple_board-0.1.0/ripple/incidents/__init__.py +1 -0
- ripple_board-0.1.0/ripple/incidents/otel.py +76 -0
- ripple_board-0.1.0/ripple/memory/__init__.py +1 -0
- ripple_board-0.1.0/ripple/memory/store.py +55 -0
- ripple_board-0.1.0/ripple/observability/__init__.py +1 -0
- ripple_board-0.1.0/ripple/observability/jobs.py +172 -0
- ripple_board-0.1.0/ripple/policy/__init__.py +1 -0
- ripple_board-0.1.0/ripple/policy/audit.py +28 -0
- ripple_board-0.1.0/ripple/policy/engine.py +23 -0
- ripple_board-0.1.0/ripple/server.py +111 -0
- ripple_board-0.1.0/ripple/storage/__init__.py +1 -0
- ripple_board-0.1.0/ripple/storage/postgres.py +106 -0
- ripple_board-0.1.0/ripple/storage/sqlite.py +161 -0
- ripple_board-0.1.0/ripple/subscription/__init__.py +0 -0
- ripple_board-0.1.0/ripple/subscription/license.py +194 -0
- ripple_board-0.1.0/ripple/verification/__init__.py +1 -0
- ripple_board-0.1.0/ripple/verification/engine.py +97 -0
- ripple_board-0.1.0/ripple/web/index.html +437 -0
- ripple_board-0.1.0/ripple/web/main.js +3811 -0
- ripple_board-0.1.0/ripple/web/styles.css +3340 -0
- ripple_board-0.1.0/ripple/workspace/__init__.py +1 -0
- ripple_board-0.1.0/ripple/workspace/manager.py +45 -0
- ripple_board-0.1.0/ripple_board.egg-info/PKG-INFO +164 -0
- ripple_board-0.1.0/ripple_board.egg-info/SOURCES.txt +62 -0
- ripple_board-0.1.0/ripple_board.egg-info/dependency_links.txt +1 -0
- ripple_board-0.1.0/ripple_board.egg-info/entry_points.txt +5 -0
- ripple_board-0.1.0/ripple_board.egg-info/requires.txt +17 -0
- ripple_board-0.1.0/ripple_board.egg-info/top_level.txt +1 -0
- 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 @@
|
|
|
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 ""
|