repro-cli 0.1.0__tar.gz → 0.2.1__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.
- repro_cli-0.2.1/LICENSE +21 -0
- repro_cli-0.2.1/PKG-INFO +145 -0
- repro_cli-0.2.1/README.md +134 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/pyproject.toml +1 -1
- repro_cli-0.2.1/repro/docker/runner.py +290 -0
- repro_cli-0.2.1/repro_cli.egg-info/PKG-INFO +145 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro_cli.egg-info/SOURCES.txt +1 -0
- repro_cli-0.1.0/PKG-INFO +0 -8
- repro_cli-0.1.0/README.md +0 -0
- repro_cli-0.1.0/repro/docker/runner.py +0 -182
- repro_cli-0.1.0/repro_cli.egg-info/PKG-INFO +0 -8
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/__init__.py +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/cli.py +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/detector/__init__.py +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/detector/detector.py +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/docker/__init__.py +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/github/__init__.py +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/github/issue.py +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro_cli.egg-info/dependency_links.txt +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro_cli.egg-info/entry_points.txt +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/repro_cli.egg-info/top_level.txt +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/setup.cfg +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/tests/test_port_parsing.py +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/tests/test_repo_validation.py +0 -0
- {repro_cli-0.1.0 → repro_cli-0.2.1}/tests/test_runtime_detection.py +0 -0
repro_cli-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sukhdev Thukral
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
repro_cli-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: repro-cli
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Disposable Docker environments for any GitHub issue
|
|
5
|
+
Author: Sukhdev Thukral
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# REPRO
|
|
13
|
+
<img width="1279" height="238" alt="Screenshot 2026-08-09 203800" src="https://github.com/user-attachments/assets/f6c12a93-be02-45f3-a237-d310c24ac395" />
|
|
14
|
+
|
|
15
|
+
> Disposable Docker environments for any GitHub issue.
|
|
16
|
+
One command. No setup.
|
|
17
|
+
<br>
|
|
18
|
+
<img width="800" height="456" alt="ezgif com-video-to-gif-converter" src="https://github.com/user-attachments/assets/635990fc-ae46-4995-aded-c74b965c0c1d" />
|
|
19
|
+
<br>
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install repro-cli
|
|
23
|
+
repro https://github.com/org/repo/issues/42
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's it, your terminal boots an isolated sandbox - repo cloned, dependencies installed and IF you have VS Code, it opens automatically **attached directly to the container** so you get a real editor and a single unified terminal, not just a shell. Type `exit` and the container disappears. Your files stay on disk, exactly where they were.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install repro-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Requirements:**
|
|
37
|
+
- Python 3.9+
|
|
38
|
+
- [Docker](https://docs.docker.com/get-docker/) (INSTALLED AND RUNNING)
|
|
39
|
+
- VS Code (optional, but recommended - enables the unified editor/terminal experience below)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Open a sandbox for any publish GitHub issue
|
|
47
|
+
repro https://github.com/org/repo/issues/123
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
#Forward a port (e.g. to view a web app running inside the sandbox)
|
|
52
|
+
repro https://github.com/org/repo/issues/123 -p 3000
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Map a different host port to a container port
|
|
57
|
+
repro https://github.com/org/repo/issues/123 -p 8080:3000
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Private repos - pass a GitHub token
|
|
62
|
+
repro https://github.com/org/private-repo/issues/7 --token ghp_xxx
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
#Or set it once as an env var instead of typing it every single time!
|
|
67
|
+
export DEVSANDBOX_GITHUB_TOKEN=ghp_xxx
|
|
68
|
+
repro https://github.com/org/private-repo/issues/7
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Skip auto-opening an editor
|
|
73
|
+
repro https://github.com/org/repo/issues/123 --no-editor
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
#Help
|
|
78
|
+
repro --help
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## What it does
|
|
82
|
+
|
|
83
|
+
1. **Processes** the GitHub issue URL
|
|
84
|
+
2. **Detects** the repo's runtime i.e Node, Python, Go, Rust, Ruby, PHP, Java, or a devcontainer based on whats actually in the repo.
|
|
85
|
+
3. **Spins up** a disposable, isolated Docker container with the right base image.
|
|
86
|
+
4. **Bind-mounts** a real folder on your machine into the container, so nothing is trapped and files persist after you exit
|
|
87
|
+
5. **Clones** the repo directly into that shared folder and install dependencies
|
|
88
|
+
6. **Attaches VS Code directly to the running container** (auto-installing the Dev Containers extension if needed) - one window, one terminal, running *inside* the sandbox.
|
|
89
|
+
7. *Destroys* the container the moment you exit. Your edited files remain exactly where they were.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## The unified editor experience
|
|
94
|
+
|
|
95
|
+
- **Real editor**: VS Code attaches directly into the container, so its built-in terminal *is* the sandbox shell
|
|
96
|
+
- **Real persistence**: because the folder is bind-mounted, not just cloned inside the container, your work survives after `exit`.
|
|
97
|
+
If VS Code isn't installed, `repro` falls back gracefully, you still get a real folder path printed at the end, ready to open in whatever editor you use.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Supported runtimes
|
|
102
|
+
| File detected | Runtime | Base image |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `package.json` | Node.js | `node:20-alpine` |
|
|
105
|
+
| `yarn.lock` | Node.js (yarn) | `node:20-alpine` |
|
|
106
|
+
| `pnpm-lock.yaml` | Node.js (pnpm) | `node:20-alpine` |
|
|
107
|
+
| `requirements.txt` | Python | `python:3.12-slim` |
|
|
108
|
+
| `pyproject.toml` | Python | `python:3.12-slim` |
|
|
109
|
+
| `Pipfile` | Python (pipenv) | `python:3.12-slim` |
|
|
110
|
+
| `go.mod` | Go | `golang:1.21-alpine` |
|
|
111
|
+
| `Cargo.toml` | Rust | `rust:1.75-slim` |
|
|
112
|
+
| `Gemfile` | Ruby | `ruby:3.3-slim` |
|
|
113
|
+
| `composer.json` | PHP | `php:8.3-cli` |
|
|
114
|
+
| `pom.xml` | Java (Maven) | `maven:3.9-eclipse-temurin-21` |
|
|
115
|
+
| `build.gradle` | Java (Gradle) | `gradle:8-jdk21` |
|
|
116
|
+
| `.devcontainer` | devcontainer | MS universal image |
|
|
117
|
+
| *(none matched)* | Universal fallback | `ubuntu:22.04` |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## The GitHub App
|
|
122
|
+
|
|
123
|
+
Install [**repro-sandbox-bot**](https://github.com/apps/repro-sandbox-bot) on any repo and it automatically comments the exact `repro` command on every new issue, so that contributors never have to think about setup.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
🏖 Open this issue in DevSandbox
|
|
127
|
+
|
|
128
|
+
Spin up a disposable Docker environment with this repo already checked out:
|
|
129
|
+
|
|
130
|
+
repro https://github.com/org/repo/issues/42
|
|
131
|
+
|
|
132
|
+
Don't have repro yet? Install it here.
|
|
133
|
+
|
|
134
|
+
Comment /sandbox on any issue to get this again.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
You can also trigger it on demand by commenting `/sandbox` on any issue. The bot runs 24/7, independent of your machine.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
## Contributing
|
|
141
|
+
PRs welcome. Open an issue first for anything beyond small fixes - and yes, you can use `repro` itself to work on `repro`.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# REPRO
|
|
2
|
+
<img width="1279" height="238" alt="Screenshot 2026-08-09 203800" src="https://github.com/user-attachments/assets/f6c12a93-be02-45f3-a237-d310c24ac395" />
|
|
3
|
+
|
|
4
|
+
> Disposable Docker environments for any GitHub issue.
|
|
5
|
+
One command. No setup.
|
|
6
|
+
<br>
|
|
7
|
+
<img width="800" height="456" alt="ezgif com-video-to-gif-converter" src="https://github.com/user-attachments/assets/635990fc-ae46-4995-aded-c74b965c0c1d" />
|
|
8
|
+
<br>
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install repro-cli
|
|
12
|
+
repro https://github.com/org/repo/issues/42
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's it, your terminal boots an isolated sandbox - repo cloned, dependencies installed and IF you have VS Code, it opens automatically **attached directly to the container** so you get a real editor and a single unified terminal, not just a shell. Type `exit` and the container disappears. Your files stay on disk, exactly where they were.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install repro-cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Requirements:**
|
|
26
|
+
- Python 3.9+
|
|
27
|
+
- [Docker](https://docs.docker.com/get-docker/) (INSTALLED AND RUNNING)
|
|
28
|
+
- VS Code (optional, but recommended - enables the unified editor/terminal experience below)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Open a sandbox for any publish GitHub issue
|
|
36
|
+
repro https://github.com/org/repo/issues/123
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
#Forward a port (e.g. to view a web app running inside the sandbox)
|
|
41
|
+
repro https://github.com/org/repo/issues/123 -p 3000
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Map a different host port to a container port
|
|
46
|
+
repro https://github.com/org/repo/issues/123 -p 8080:3000
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Private repos - pass a GitHub token
|
|
51
|
+
repro https://github.com/org/private-repo/issues/7 --token ghp_xxx
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
#Or set it once as an env var instead of typing it every single time!
|
|
56
|
+
export DEVSANDBOX_GITHUB_TOKEN=ghp_xxx
|
|
57
|
+
repro https://github.com/org/private-repo/issues/7
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Skip auto-opening an editor
|
|
62
|
+
repro https://github.com/org/repo/issues/123 --no-editor
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
#Help
|
|
67
|
+
repro --help
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## What it does
|
|
71
|
+
|
|
72
|
+
1. **Processes** the GitHub issue URL
|
|
73
|
+
2. **Detects** the repo's runtime i.e Node, Python, Go, Rust, Ruby, PHP, Java, or a devcontainer based on whats actually in the repo.
|
|
74
|
+
3. **Spins up** a disposable, isolated Docker container with the right base image.
|
|
75
|
+
4. **Bind-mounts** a real folder on your machine into the container, so nothing is trapped and files persist after you exit
|
|
76
|
+
5. **Clones** the repo directly into that shared folder and install dependencies
|
|
77
|
+
6. **Attaches VS Code directly to the running container** (auto-installing the Dev Containers extension if needed) - one window, one terminal, running *inside* the sandbox.
|
|
78
|
+
7. *Destroys* the container the moment you exit. Your edited files remain exactly where they were.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## The unified editor experience
|
|
83
|
+
|
|
84
|
+
- **Real editor**: VS Code attaches directly into the container, so its built-in terminal *is* the sandbox shell
|
|
85
|
+
- **Real persistence**: because the folder is bind-mounted, not just cloned inside the container, your work survives after `exit`.
|
|
86
|
+
If VS Code isn't installed, `repro` falls back gracefully, you still get a real folder path printed at the end, ready to open in whatever editor you use.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Supported runtimes
|
|
91
|
+
| File detected | Runtime | Base image |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `package.json` | Node.js | `node:20-alpine` |
|
|
94
|
+
| `yarn.lock` | Node.js (yarn) | `node:20-alpine` |
|
|
95
|
+
| `pnpm-lock.yaml` | Node.js (pnpm) | `node:20-alpine` |
|
|
96
|
+
| `requirements.txt` | Python | `python:3.12-slim` |
|
|
97
|
+
| `pyproject.toml` | Python | `python:3.12-slim` |
|
|
98
|
+
| `Pipfile` | Python (pipenv) | `python:3.12-slim` |
|
|
99
|
+
| `go.mod` | Go | `golang:1.21-alpine` |
|
|
100
|
+
| `Cargo.toml` | Rust | `rust:1.75-slim` |
|
|
101
|
+
| `Gemfile` | Ruby | `ruby:3.3-slim` |
|
|
102
|
+
| `composer.json` | PHP | `php:8.3-cli` |
|
|
103
|
+
| `pom.xml` | Java (Maven) | `maven:3.9-eclipse-temurin-21` |
|
|
104
|
+
| `build.gradle` | Java (Gradle) | `gradle:8-jdk21` |
|
|
105
|
+
| `.devcontainer` | devcontainer | MS universal image |
|
|
106
|
+
| *(none matched)* | Universal fallback | `ubuntu:22.04` |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## The GitHub App
|
|
111
|
+
|
|
112
|
+
Install [**repro-sandbox-bot**](https://github.com/apps/repro-sandbox-bot) on any repo and it automatically comments the exact `repro` command on every new issue, so that contributors never have to think about setup.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
🏖 Open this issue in DevSandbox
|
|
116
|
+
|
|
117
|
+
Spin up a disposable Docker environment with this repo already checked out:
|
|
118
|
+
|
|
119
|
+
repro https://github.com/org/repo/issues/42
|
|
120
|
+
|
|
121
|
+
Don't have repro yet? Install it here.
|
|
122
|
+
|
|
123
|
+
Comment /sandbox on any issue to get this again.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
You can also trigger it on demand by commenting `/sandbox` on any issue. The bot runs 24/7, independent of your machine.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
## Contributing
|
|
130
|
+
PRs welcome. Open an issue first for anything beyond small fixes - and yes, you can use `repro` itself to work on `repro`.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
class DockerNotFoundError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
class DockerNotRunningError(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class InvalidPortError(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class InvalidRepoError(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
22
|
+
|
|
23
|
+
def check_docker_available():
|
|
24
|
+
"""Verify Docker CLI exists and the daemon is reachable before trying anythg"""
|
|
25
|
+
if shutil.which("docker") is None:
|
|
26
|
+
raise DockerNotFoundError(
|
|
27
|
+
"Docker is not installed or not on your PATH.\n"
|
|
28
|
+
" Install it from: https://docs.docker.com/get-docker/"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
result = subprocess.run(["docker", "info"], capture_output=True, text=True)
|
|
32
|
+
if result.returncode != 0:
|
|
33
|
+
raise DockerNotRunningError(
|
|
34
|
+
"Docker is installed but the daemon isnt running.\n"
|
|
35
|
+
" Start Docker Desktop and wait for the icon to go steady, then try again."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_repo_identifiers(owner: str, repo:str):
|
|
40
|
+
""" Guard against shell injection -> owner/repo gets interpolated into a shell command, so anything outside GitHub's allowed charst is rejected
|
|
41
|
+
before it even reaches da subprocess"""
|
|
42
|
+
for label, value in (("owner", owner), ("repo", repo)):
|
|
43
|
+
if not SAFE_NAME.match(value):
|
|
44
|
+
raise InvalidRepoError(f'invalid {label} "{value}" - contains disallowed characters')
|
|
45
|
+
if value in (".", ".."):
|
|
46
|
+
raise InvalidRepoError(f'invalid {label} "{value}"')
|
|
47
|
+
if value.startswith("."):
|
|
48
|
+
raise InvalidRepoError(f'invalid {label} "{value}" - cannot start with a dot')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_port_spec(spec: str) -> tuple[int, int]:
|
|
52
|
+
""" Parse a --port value into (host_port, container_port).
|
|
53
|
+
Accepts "3000" (same both sides) or "8080:3000" (host:container).
|
|
54
|
+
"""
|
|
55
|
+
parts = spec.split(":")
|
|
56
|
+
|
|
57
|
+
if len(parts) == 1:
|
|
58
|
+
host, container = parts[0], parts[0]
|
|
59
|
+
elif len(parts) == 2:
|
|
60
|
+
host, container = parts
|
|
61
|
+
else:
|
|
62
|
+
raise InvalidPortError(f'invalid port spec "{spec}" - use PORT or HOST:CONTAINER')
|
|
63
|
+
|
|
64
|
+
for label, value in (("host", host), ("container", container)):
|
|
65
|
+
if not value.isdigit():
|
|
66
|
+
raise InvalidPortError(f'invalid {label} port "{value}" in "{spec}" - must be a number')
|
|
67
|
+
port_num = int(value)
|
|
68
|
+
if not(1 <= port_num <= 65535):
|
|
69
|
+
raise InvalidPortError(f'{label} port {port_num} out of range (1-65535)')
|
|
70
|
+
|
|
71
|
+
return int(host), int(container)
|
|
72
|
+
|
|
73
|
+
DEVCONTAINERS_EXT_ID = "ms-vscode-remote.remote-containers"
|
|
74
|
+
|
|
75
|
+
def has_devcontainers_extension() -> bool:
|
|
76
|
+
"""Check if VS Code's DEV containers extension is installed"""
|
|
77
|
+
try:
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
["code", "--list-extensions"],
|
|
80
|
+
capture_output=True, text=True, timeout=5,
|
|
81
|
+
shell=(os.name == "nt"),
|
|
82
|
+
)
|
|
83
|
+
return DEVCONTAINERS_EXT_ID in result.stdout
|
|
84
|
+
except Exception:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def install_devcontainers_extension() -> bool:
|
|
88
|
+
print("📦 Installing VS Code's 'Dev Containers' extension (one-time setup)...")
|
|
89
|
+
try:
|
|
90
|
+
result = subprocess.run(
|
|
91
|
+
["code", "--install-extension", DEVCONTAINERS_EXT_ID],
|
|
92
|
+
capture_output=True, text=True, timeout=60,
|
|
93
|
+
shell=(os.name=="nt")
|
|
94
|
+
)
|
|
95
|
+
if result.returncode == 0:
|
|
96
|
+
print("✅ Dev Containers extension installed.\n")
|
|
97
|
+
return True
|
|
98
|
+
print("⚠️ Couldn't auto-install the extension. Falling back to folder mode.\n")
|
|
99
|
+
return False
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"⚠️ Couldn't auto-install the extension ({e}). Falling back to folder mode.\n")
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def attach_vscode_to_container(container_name: str, work_dir:str):
|
|
105
|
+
for _ in range(20):
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
["docker", "inspect", "--format", "{{.State.Running}}", container_name],
|
|
108
|
+
capture_output=True, text=True,
|
|
109
|
+
)
|
|
110
|
+
if result.returncode == 0 and result.stdout.strip() == "true":
|
|
111
|
+
break
|
|
112
|
+
time.sleep(0.5)
|
|
113
|
+
else:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
id_result = subprocess.run(
|
|
117
|
+
["docker", "inspect", "--format", "{{.Id}}", container_name],
|
|
118
|
+
capture_output=True, text=True,
|
|
119
|
+
)
|
|
120
|
+
if id_result.returncode != 0:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
container_id = id_result.stdout.strip()
|
|
124
|
+
hex_id = container_id.encode().hex()
|
|
125
|
+
uri = f"vscode-remote://attached-container+{hex_id}{work_dir}"
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
subprocess.Popen(["code", "--folder-uri", uri], shell=(os.name=="nt"))
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def run_sandbox(issue: dict, runtime: dict, ports: list =None, token: str = None, no_editor: bool = False):
|
|
133
|
+
check_docker_available()
|
|
134
|
+
ports = ports or []
|
|
135
|
+
|
|
136
|
+
owner = issue["owner"]
|
|
137
|
+
repo = issue["repo"]
|
|
138
|
+
number = issue["number"]
|
|
139
|
+
|
|
140
|
+
validate_repo_identifiers(owner, repo)
|
|
141
|
+
|
|
142
|
+
if not isinstance(number, int):
|
|
143
|
+
raise InvalidRepoError("issue number must be an integer")
|
|
144
|
+
|
|
145
|
+
clone_url_public = f"https://github.com/{owner}/{repo}.git"
|
|
146
|
+
work_dir = f"/sandbox/{repo}"
|
|
147
|
+
container_name = f"repro-{owner}-{repo}-{number}"
|
|
148
|
+
|
|
149
|
+
subprocess.run(["docker", "rm", "-f", container_name], capture_output=True)
|
|
150
|
+
|
|
151
|
+
host_dir = tempfile.mkdtemp(prefix=f"repro-{owner}-{repo}-{number}-")
|
|
152
|
+
|
|
153
|
+
print(f"📁 Sandbox files will live at: {host_dir}")
|
|
154
|
+
print(" (this folder survives after you exit - edit here with your own editor)\n")
|
|
155
|
+
|
|
156
|
+
editor_mode = None
|
|
157
|
+
if not no_editor and shutil.which("code"):
|
|
158
|
+
if not has_devcontainers_extension():
|
|
159
|
+
install_devcontainers_extension()
|
|
160
|
+
|
|
161
|
+
if has_devcontainers_extension():
|
|
162
|
+
editor_mode="attached"
|
|
163
|
+
print("🖊️ VS Code will attach directly to the container once it's up...")
|
|
164
|
+
print(" (its built-in terminal will run INSIDE the sandbox — one window, no split)\n")
|
|
165
|
+
else:
|
|
166
|
+
editor_mode= "folder"
|
|
167
|
+
print("🖊️ Opening the folder in VS Code (editing only)...")
|
|
168
|
+
print(" 💡 Install the 'Dev Containers' extension for a fully unified terminal.\n")
|
|
169
|
+
elif not no_editor:
|
|
170
|
+
print("💡 No auto-attach editor detected (currently supports VS Code :( .\n")
|
|
171
|
+
print(" Open the folder above in whatever editor you use - its a real folder on your machine!! .\n")
|
|
172
|
+
|
|
173
|
+
startup_script = build_startup_script(clone_url_public, work_dir, runtime["install"], number, ports, has_token=bool(token))
|
|
174
|
+
|
|
175
|
+
args = [
|
|
176
|
+
"docker", "run",
|
|
177
|
+
"--rm",
|
|
178
|
+
"-i",
|
|
179
|
+
"-t",
|
|
180
|
+
"--name", container_name,
|
|
181
|
+
"--hostname", f"sandbox-issue-{number}",
|
|
182
|
+
"-v", f"{host_dir}:{work_dir}",
|
|
183
|
+
"-e", f"ISSUE_NUMBER={number}",
|
|
184
|
+
"-e", f"REPO={owner}/{repo}",
|
|
185
|
+
"-e", "TERM=xterm-256color",
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
for host_port, container_port in ports:
|
|
189
|
+
args += ["-p", f"127.0.0.1:{host_port}:{container_port}"]
|
|
190
|
+
|
|
191
|
+
env_file_path = None
|
|
192
|
+
if token:
|
|
193
|
+
fd, env_file_path = tempfile.mkstemp(prefix="repro-", suffix=".env")
|
|
194
|
+
try:
|
|
195
|
+
os.chmod(env_file_path, 0o600)
|
|
196
|
+
with os.fdopen(fd, "w") as f:
|
|
197
|
+
f.write(f"GIT_TOKEN={token}\n")
|
|
198
|
+
args+=["--env-file", env_file_path]
|
|
199
|
+
except Exception:
|
|
200
|
+
if os.path.exists(env_file_path):
|
|
201
|
+
os.remove(env_file_path)
|
|
202
|
+
raise
|
|
203
|
+
|
|
204
|
+
args += [runtime["image"], "sh", "-c", startup_script]
|
|
205
|
+
|
|
206
|
+
print(f"🚀 Starting sandbox for {owner}/{repo} issue #{number}")
|
|
207
|
+
print(" (container will be deleted automatically when you exit)\n")
|
|
208
|
+
|
|
209
|
+
env = os.environ.copy()
|
|
210
|
+
env["DOCKER_CLI_HINTS"] = "false"
|
|
211
|
+
|
|
212
|
+
if editor_mode == "attached":
|
|
213
|
+
threading.Thread(
|
|
214
|
+
target=attach_vscode_to_container,
|
|
215
|
+
args=(container_name, work_dir),
|
|
216
|
+
daemon=True,
|
|
217
|
+
).start()
|
|
218
|
+
elif editor_mode == "folder":
|
|
219
|
+
try:
|
|
220
|
+
subprocess.Popen(["code", host_dir], shell=(os.name=="nt"))
|
|
221
|
+
except Exception as e:
|
|
222
|
+
print(f"⚠️ Couldn't auto-open VS Code: {e}\n")
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
try:
|
|
226
|
+
result = subprocess.run(args, env=env)
|
|
227
|
+
except KeyboardInterrupt:
|
|
228
|
+
print("Interrupted - cleaning up the sandbox...")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if result.returncode not in (0,130):
|
|
232
|
+
if result.returncode == 125:
|
|
233
|
+
print(f"\n⚠️ Docker couldn't start the container (exit 125) — this often means"
|
|
234
|
+
f"the image failed to pull. Check your internet connection and try again.")
|
|
235
|
+
else:
|
|
236
|
+
print(f"Sandbox exited with code {result.returncode} - check the output above.")
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
print("\n Sandbox destroyed. Back to reality.")
|
|
240
|
+
|
|
241
|
+
finally:
|
|
242
|
+
if env_file_path and os.path.exists(env_file_path):
|
|
243
|
+
os.remove(env_file_path)
|
|
244
|
+
print(f"📁 Your files are still here: {host_dir}")
|
|
245
|
+
print(" (delete manually whenever you're done)")
|
|
246
|
+
|
|
247
|
+
def build_startup_script(clone_url_public, work_dir, install_cmd, issue_number, ports, has_token: bool = False) -> str:
|
|
248
|
+
install_step = (
|
|
249
|
+
f'echo "Installing dependencies....." && {install_cmd}'
|
|
250
|
+
if install_cmd
|
|
251
|
+
else 'echo "No install step, dropping into shell... "'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
host_and_path = clone_url_public[len("https://"):]
|
|
255
|
+
clone_cmd = (
|
|
256
|
+
f'if [ -n "$GIT_TOKEN" ]; then '
|
|
257
|
+
f'CLONE_URL="https://${{GIT_TOKEN}}@{host_and_path}";'
|
|
258
|
+
f'else CLONE_URL="{clone_url_public}"; fi && '
|
|
259
|
+
f'git clone --depth=1 "$CLONE_URL" {work_dir}'
|
|
260
|
+
f'|| (echo "❌ Clone failed — check the repo exists, the URL is correct, and your token has access." && exit 1)'
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
lines = [
|
|
264
|
+
"set -e",
|
|
265
|
+
"which git > /dev/null 2>&1 || (apk add --no-cache git curl 2>/dev/null || (apt-get update -qq && apt-get install -y -qq git curl))",
|
|
266
|
+
'echo ""',
|
|
267
|
+
'echo "=================================="',
|
|
268
|
+
f'echo " Repro - Issue #{issue_number}"',
|
|
269
|
+
'echo "=================================="',
|
|
270
|
+
'echo ""',
|
|
271
|
+
f'echo "Cloning repo..."',
|
|
272
|
+
clone_cmd,
|
|
273
|
+
f"cd {work_dir} && git remote set-url origin {clone_url_public} && unset GIT_TOKEN",
|
|
274
|
+
install_step,
|
|
275
|
+
'echo ""',
|
|
276
|
+
f'echo "Ready! You are in: {work_dir}"',
|
|
277
|
+
f'echo "Working on issue #{issue_number}"',
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
if ports:
|
|
281
|
+
lines.append('echo ""')
|
|
282
|
+
for host_port, container_port in ports:
|
|
283
|
+
lines.append(f'echo "Port {container_port} forwarded -> http://localhost:{host_port}"')
|
|
284
|
+
lines.append('echo " Start your app inside this shell (e.g. npm start / node server.js) to use it."')
|
|
285
|
+
|
|
286
|
+
lines.append('echo ""')
|
|
287
|
+
lines.append(f"cd {work_dir} && exec sh")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
return " && ".join(lines)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: repro-cli
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Disposable Docker environments for any GitHub issue
|
|
5
|
+
Author: Sukhdev Thukral
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# REPRO
|
|
13
|
+
<img width="1279" height="238" alt="Screenshot 2026-08-09 203800" src="https://github.com/user-attachments/assets/f6c12a93-be02-45f3-a237-d310c24ac395" />
|
|
14
|
+
|
|
15
|
+
> Disposable Docker environments for any GitHub issue.
|
|
16
|
+
One command. No setup.
|
|
17
|
+
<br>
|
|
18
|
+
<img width="800" height="456" alt="ezgif com-video-to-gif-converter" src="https://github.com/user-attachments/assets/635990fc-ae46-4995-aded-c74b965c0c1d" />
|
|
19
|
+
<br>
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install repro-cli
|
|
23
|
+
repro https://github.com/org/repo/issues/42
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's it, your terminal boots an isolated sandbox - repo cloned, dependencies installed and IF you have VS Code, it opens automatically **attached directly to the container** so you get a real editor and a single unified terminal, not just a shell. Type `exit` and the container disappears. Your files stay on disk, exactly where they were.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install repro-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Requirements:**
|
|
37
|
+
- Python 3.9+
|
|
38
|
+
- [Docker](https://docs.docker.com/get-docker/) (INSTALLED AND RUNNING)
|
|
39
|
+
- VS Code (optional, but recommended - enables the unified editor/terminal experience below)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Open a sandbox for any publish GitHub issue
|
|
47
|
+
repro https://github.com/org/repo/issues/123
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
#Forward a port (e.g. to view a web app running inside the sandbox)
|
|
52
|
+
repro https://github.com/org/repo/issues/123 -p 3000
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Map a different host port to a container port
|
|
57
|
+
repro https://github.com/org/repo/issues/123 -p 8080:3000
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Private repos - pass a GitHub token
|
|
62
|
+
repro https://github.com/org/private-repo/issues/7 --token ghp_xxx
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
#Or set it once as an env var instead of typing it every single time!
|
|
67
|
+
export DEVSANDBOX_GITHUB_TOKEN=ghp_xxx
|
|
68
|
+
repro https://github.com/org/private-repo/issues/7
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Skip auto-opening an editor
|
|
73
|
+
repro https://github.com/org/repo/issues/123 --no-editor
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
#Help
|
|
78
|
+
repro --help
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## What it does
|
|
82
|
+
|
|
83
|
+
1. **Processes** the GitHub issue URL
|
|
84
|
+
2. **Detects** the repo's runtime i.e Node, Python, Go, Rust, Ruby, PHP, Java, or a devcontainer based on whats actually in the repo.
|
|
85
|
+
3. **Spins up** a disposable, isolated Docker container with the right base image.
|
|
86
|
+
4. **Bind-mounts** a real folder on your machine into the container, so nothing is trapped and files persist after you exit
|
|
87
|
+
5. **Clones** the repo directly into that shared folder and install dependencies
|
|
88
|
+
6. **Attaches VS Code directly to the running container** (auto-installing the Dev Containers extension if needed) - one window, one terminal, running *inside* the sandbox.
|
|
89
|
+
7. *Destroys* the container the moment you exit. Your edited files remain exactly where they were.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## The unified editor experience
|
|
94
|
+
|
|
95
|
+
- **Real editor**: VS Code attaches directly into the container, so its built-in terminal *is* the sandbox shell
|
|
96
|
+
- **Real persistence**: because the folder is bind-mounted, not just cloned inside the container, your work survives after `exit`.
|
|
97
|
+
If VS Code isn't installed, `repro` falls back gracefully, you still get a real folder path printed at the end, ready to open in whatever editor you use.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Supported runtimes
|
|
102
|
+
| File detected | Runtime | Base image |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `package.json` | Node.js | `node:20-alpine` |
|
|
105
|
+
| `yarn.lock` | Node.js (yarn) | `node:20-alpine` |
|
|
106
|
+
| `pnpm-lock.yaml` | Node.js (pnpm) | `node:20-alpine` |
|
|
107
|
+
| `requirements.txt` | Python | `python:3.12-slim` |
|
|
108
|
+
| `pyproject.toml` | Python | `python:3.12-slim` |
|
|
109
|
+
| `Pipfile` | Python (pipenv) | `python:3.12-slim` |
|
|
110
|
+
| `go.mod` | Go | `golang:1.21-alpine` |
|
|
111
|
+
| `Cargo.toml` | Rust | `rust:1.75-slim` |
|
|
112
|
+
| `Gemfile` | Ruby | `ruby:3.3-slim` |
|
|
113
|
+
| `composer.json` | PHP | `php:8.3-cli` |
|
|
114
|
+
| `pom.xml` | Java (Maven) | `maven:3.9-eclipse-temurin-21` |
|
|
115
|
+
| `build.gradle` | Java (Gradle) | `gradle:8-jdk21` |
|
|
116
|
+
| `.devcontainer` | devcontainer | MS universal image |
|
|
117
|
+
| *(none matched)* | Universal fallback | `ubuntu:22.04` |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## The GitHub App
|
|
122
|
+
|
|
123
|
+
Install [**repro-sandbox-bot**](https://github.com/apps/repro-sandbox-bot) on any repo and it automatically comments the exact `repro` command on every new issue, so that contributors never have to think about setup.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
🏖 Open this issue in DevSandbox
|
|
127
|
+
|
|
128
|
+
Spin up a disposable Docker environment with this repo already checked out:
|
|
129
|
+
|
|
130
|
+
repro https://github.com/org/repo/issues/42
|
|
131
|
+
|
|
132
|
+
Don't have repro yet? Install it here.
|
|
133
|
+
|
|
134
|
+
Comment /sandbox on any issue to get this again.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
You can also trigger it on demand by commenting `/sandbox` on any issue. The bot runs 24/7, independent of your machine.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
## Contributing
|
|
141
|
+
PRs welcome. Open an issue first for anything beyond small fixes - and yes, you can use `repro` itself to work on `repro`.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
repro_cli-0.1.0/PKG-INFO
DELETED
repro_cli-0.1.0/README.md
DELETED
|
File without changes
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import shutil
|
|
3
|
-
import re
|
|
4
|
-
import subprocess
|
|
5
|
-
import tempfile
|
|
6
|
-
|
|
7
|
-
class DockerNotFoundError(Exception):
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
class DockerNotRunningError(Exception):
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
class InvalidPortError(Exception):
|
|
14
|
-
pass
|
|
15
|
-
|
|
16
|
-
class InvalidRepoError(Exception):
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
20
|
-
|
|
21
|
-
def check_docker_available():
|
|
22
|
-
"""Verify Docker CLI exists and the daemon is reachable before trying anythg"""
|
|
23
|
-
if shutil.which("docker") is None:
|
|
24
|
-
raise DockerNotFoundError(
|
|
25
|
-
"Docker is not installed or not on your PATH.\n"
|
|
26
|
-
" Install it from: https://docs.docker.com/get-docker/"
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
result = subprocess.run(["docker", "info"], capture_output=True, text=True)
|
|
30
|
-
if result.returncode != 0:
|
|
31
|
-
raise DockerNotRunningError(
|
|
32
|
-
"Docker is installed but the daemon isnt running.\n"
|
|
33
|
-
" Start Docker Desktop and wait for the icon to go steady, then try again."
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
def validate_repo_identifiers(owner: str, repo:str):
|
|
37
|
-
""" Guard against shell injection -> owner/repo gets interpolated into a shell command, so anything outside GitHub's allowed charst is rejected
|
|
38
|
-
before it even reaches da subprocess"""
|
|
39
|
-
for label, value in (("owner", owner), ("repo", repo)):
|
|
40
|
-
if not SAFE_NAME.match(value):
|
|
41
|
-
raise InvalidRepoError(f'invalid {label} "{value}" - contains disallowed characters')
|
|
42
|
-
if value in (".", ".."):
|
|
43
|
-
raise InvalidRepoError(f'invalid {label} "{value}"')
|
|
44
|
-
if value.startswith("."):
|
|
45
|
-
raise InvalidRepoError(f'invalid {label} "{value}" - cannot start with a dot')
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def parse_port_spec(spec: str) -> tuple[int, int]:
|
|
49
|
-
""" Parse a --port value into (host_port, container_port).
|
|
50
|
-
Accepts "3000" (same both sides) or "8080:3000" (host:container).
|
|
51
|
-
"""
|
|
52
|
-
parts = spec.split(":")
|
|
53
|
-
|
|
54
|
-
if len(parts) == 1:
|
|
55
|
-
host, container = parts[0], parts[0]
|
|
56
|
-
elif len(parts) == 2:
|
|
57
|
-
host, container = parts
|
|
58
|
-
else:
|
|
59
|
-
raise InvalidPortError(f'invalid port spec "{spec}" - use PORT or HOST:CONTAINER')
|
|
60
|
-
|
|
61
|
-
for label, value in (("host", host), ("container", container)):
|
|
62
|
-
if not value.isdigit():
|
|
63
|
-
raise InvalidPortError(f'invalid {label} port "{value}" in "{spec}" - must be a number')
|
|
64
|
-
port_num = int(value)
|
|
65
|
-
if not(1 <= port_num <= 65535):
|
|
66
|
-
raise InvalidPortError(f'{label} port {port_num} out of range (1-65535)')
|
|
67
|
-
|
|
68
|
-
return int(host), int(container)
|
|
69
|
-
|
|
70
|
-
def run_sandbox(issue: dict, runtime: dict, ports: list =None, token: str = None):
|
|
71
|
-
check_docker_available()
|
|
72
|
-
ports = ports or []
|
|
73
|
-
|
|
74
|
-
owner = issue["owner"]
|
|
75
|
-
repo = issue["repo"]
|
|
76
|
-
number = issue["number"]
|
|
77
|
-
|
|
78
|
-
validate_repo_identifiers(owner, repo)
|
|
79
|
-
|
|
80
|
-
if not isinstance(number, int):
|
|
81
|
-
raise InvalidRepoError("issue number must be an integer")
|
|
82
|
-
|
|
83
|
-
clone_url_public = f"https://github.com/{owner}/{repo}.git"
|
|
84
|
-
clone_url_auth = f"https://{token}@github.com/{owner}/{repo}.git" if token else clone_url_public
|
|
85
|
-
|
|
86
|
-
work_dir = f"/sandbox/{repo}"
|
|
87
|
-
container_name = f"repro-{owner}-{repo}-{number}"
|
|
88
|
-
|
|
89
|
-
startup_script = build_startup_script(clone_url_auth, clone_url_public, work_dir, runtime["install"], number, ports, has_token=bool(token))
|
|
90
|
-
|
|
91
|
-
args = [
|
|
92
|
-
"docker", "run",
|
|
93
|
-
"--rm",
|
|
94
|
-
"-i",
|
|
95
|
-
"-t",
|
|
96
|
-
"--name", container_name,
|
|
97
|
-
"--hostname", f"sandbox-issue-{number}",
|
|
98
|
-
"-e", f"ISSUE_NUMBER={number}",
|
|
99
|
-
"-e", f"REPO={owner}/{repo}",
|
|
100
|
-
"-e", "TERM=xterm-256color",
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
for host_port, container_port in ports:
|
|
104
|
-
args += ["-p", f"127.0.0.1:{host_port}:{container_port}"]
|
|
105
|
-
|
|
106
|
-
env_file_path = None
|
|
107
|
-
if token:
|
|
108
|
-
#writing the toekn to a temp file passed via --env-fle instead of -e/embedded-in-command
|
|
109
|
-
fd, env_file_path = tempfile.mkstemp(prefix="repro-", suffix=".env")
|
|
110
|
-
try:
|
|
111
|
-
os.chmod(env_file_path, 0o600)
|
|
112
|
-
with os.fdopen(fd, "w") as f:
|
|
113
|
-
f.write(f"GIT_TOKEN={token}\n")
|
|
114
|
-
except Exception:
|
|
115
|
-
if os.path.exists(env_file_path):
|
|
116
|
-
os.remove(env_file_path)
|
|
117
|
-
raise
|
|
118
|
-
|
|
119
|
-
args += [runtime["image"], "sh", "-c", startup_script]
|
|
120
|
-
|
|
121
|
-
print(f"Starting sandbox for {owner}/{repo} issue #{number}")
|
|
122
|
-
print(" (container will be deleted automatically when you exit)\n")
|
|
123
|
-
|
|
124
|
-
env = os.environ.copy()
|
|
125
|
-
env["DOCKER_CLI_HINTS"] = "false"
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
result = subprocess.run(args, env=env)
|
|
129
|
-
except KeyboardInterrupt:
|
|
130
|
-
print("Interrupted - cleaning up the sandbox...")
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
if result.returncode not in (0,130):
|
|
134
|
-
print(f"Sandbox exited with code {result.returncode} - check the output above.")
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
print("\n Sandbox destroyed. Back to reality.")
|
|
138
|
-
|
|
139
|
-
def build_startup_script(clone_url_auth, clone_url_public, work_dir, install_cmd, issue_number, ports) -> str:
|
|
140
|
-
install_step = (
|
|
141
|
-
f'echo "Installing dependencies....." && {install_cmd}'
|
|
142
|
-
if install_cmd
|
|
143
|
-
else 'echo "No install step, dropping into shell... "'
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
host_and_path = clone_url_public[len("https://"):]
|
|
147
|
-
clone_cmd = (
|
|
148
|
-
f'if [ -n "$GIT_TOEKN" ]; then '
|
|
149
|
-
f'CLONE_URL="https://${{GIT_TOKEN}}@{host_and_path}";'
|
|
150
|
-
f'else CLONE_URL="{clone_url_public}"; fi && '
|
|
151
|
-
f'git clone --depth=1 "$CLONE_URL" {work_dir} 2>&1 | tail -5'
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
lines = [
|
|
155
|
-
"set -e",
|
|
156
|
-
"which git > /dev/null 2>&1 || (apk add --no-cache git curl)",
|
|
157
|
-
'echo ""',
|
|
158
|
-
'echo "=================================="',
|
|
159
|
-
f'echo " DevSandbox - Issue #{issue_number}"',
|
|
160
|
-
'echo "=================================="',
|
|
161
|
-
'echo ""',
|
|
162
|
-
f'echo "Cloning repo..."',
|
|
163
|
-
clone_cmd,
|
|
164
|
-
f"git clone --depth=1 {clone_url_auth} {work_dir} 2>&1 | tail -5",
|
|
165
|
-
f"cd {work_dir} && git remote set-url origin {clone_url_public} && unset GIT_TOKEN",
|
|
166
|
-
install_step,
|
|
167
|
-
'echo ""',
|
|
168
|
-
f'echo "Ready! You are in: {work_dir}"',
|
|
169
|
-
f'echo "Working on issue #{issue_number}"',
|
|
170
|
-
]
|
|
171
|
-
|
|
172
|
-
if ports:
|
|
173
|
-
lines.append('echo ""')
|
|
174
|
-
for host_port, container_port in ports:
|
|
175
|
-
lines.append(f'echo "Port {container_port} forwarded -> http://localhost:{host_port}"')
|
|
176
|
-
lines.append('echo " Start your app inside this shell (e.g. npm start / node server.js) to use it."')
|
|
177
|
-
|
|
178
|
-
lines.append('echo ""')
|
|
179
|
-
lines.append(f"cd {work_dir} && exec sh")
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return " && ".join(lines)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|