forge-cicd 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.
- forge_cicd-0.1.0/PKG-INFO +79 -0
- forge_cicd-0.1.0/README.md +60 -0
- forge_cicd-0.1.0/forge/__init__.py +0 -0
- forge_cicd-0.1.0/forge/commands/_render_logs.py +46 -0
- forge_cicd-0.1.0/forge/commands/create.py +137 -0
- forge_cicd-0.1.0/forge/commands/credentials.py +66 -0
- forge_cicd-0.1.0/forge/commands/deploy.py +99 -0
- forge_cicd-0.1.0/forge/commands/deploy_status.py +117 -0
- forge_cicd-0.1.0/forge/commands/init.py +35 -0
- forge_cicd-0.1.0/forge/commands/link.py +65 -0
- forge_cicd-0.1.0/forge/commands/login.py +55 -0
- forge_cicd-0.1.0/forge/commands/logs.py +100 -0
- forge_cicd-0.1.0/forge/commands/status.py +104 -0
- forge_cicd-0.1.0/forge/commands/unlink.py +44 -0
- forge_cicd-0.1.0/forge/commands/worker.py +260 -0
- forge_cicd-0.1.0/forge/config.py +20 -0
- forge_cicd-0.1.0/forge/main.py +52 -0
- forge_cicd-0.1.0/forge/ui.py +199 -0
- forge_cicd-0.1.0/forge_cicd.egg-info/PKG-INFO +79 -0
- forge_cicd-0.1.0/forge_cicd.egg-info/SOURCES.txt +24 -0
- forge_cicd-0.1.0/forge_cicd.egg-info/dependency_links.txt +1 -0
- forge_cicd-0.1.0/forge_cicd.egg-info/entry_points.txt +2 -0
- forge_cicd-0.1.0/forge_cicd.egg-info/requires.txt +3 -0
- forge_cicd-0.1.0/forge_cicd.egg-info/top_level.txt +1 -0
- forge_cicd-0.1.0/pyproject.toml +29 -0
- forge_cicd-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forge-cicd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Forge — spec-driven CI/CD for full-stack projects.
|
|
5
|
+
Author-email: Tahir Siddique <tahir@example.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/TahirSiddique092/forge
|
|
7
|
+
Project-URL: Repository, https://github.com/TahirSiddique092/forge
|
|
8
|
+
Project-URL: Issues, https://github.com/TahirSiddique092/forge/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: typer[all]
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: rich>=13.0.0
|
|
19
|
+
|
|
20
|
+
# Forge CI/CD CLI
|
|
21
|
+
|
|
22
|
+
Forge is a spec-driven CI/CD platform designed for modern, full-stack applications. It bridges the gap between your local terminal and production deployments by orchestrating builds, running CI steps in local/remote workers, and managing deployments to platforms like Railway and Vercel.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Spec-driven**: Define your build and deploy pipeline in a simple YAML or JSON spec.
|
|
27
|
+
- **Local Workers**: Run your CI jobs locally using Docker while still reporting status to the cloud.
|
|
28
|
+
- **Integrated Deployments**: Native support for Railway (backend) and Vercel (frontend).
|
|
29
|
+
- **GitHub Integration**: Securely authenticate with GitHub and link your repositories effortlessly.
|
|
30
|
+
- **Real-time Logs**: Stream build and deployment logs directly to your terminal.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Forge requires Python 3.9 or higher.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install forge-cicd
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
1. **Initialize your project**:
|
|
43
|
+
```bash
|
|
44
|
+
forge init
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
2. **Login with GitHub**:
|
|
48
|
+
```bash
|
|
49
|
+
forge login
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
3. **Link your repository**:
|
|
53
|
+
```bash
|
|
54
|
+
forge link <your-project-id>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
4. **Set your cloud credentials**:
|
|
58
|
+
```bash
|
|
59
|
+
forge set-cred railway
|
|
60
|
+
forge set-cred vercel
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
5. **Start a local worker** (optional, for CI execution):
|
|
64
|
+
```bash
|
|
65
|
+
forge worker start
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
6. **Deploy**:
|
|
69
|
+
```bash
|
|
70
|
+
forge deploy
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Documentation
|
|
74
|
+
|
|
75
|
+
For full documentation and advanced usage, visit [github.com/TahirSiddique092/forge](https://github.com/TahirSiddique092/forge).
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
Forge is released under the MIT License.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Forge CI/CD CLI
|
|
2
|
+
|
|
3
|
+
Forge is a spec-driven CI/CD platform designed for modern, full-stack applications. It bridges the gap between your local terminal and production deployments by orchestrating builds, running CI steps in local/remote workers, and managing deployments to platforms like Railway and Vercel.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Spec-driven**: Define your build and deploy pipeline in a simple YAML or JSON spec.
|
|
8
|
+
- **Local Workers**: Run your CI jobs locally using Docker while still reporting status to the cloud.
|
|
9
|
+
- **Integrated Deployments**: Native support for Railway (backend) and Vercel (frontend).
|
|
10
|
+
- **GitHub Integration**: Securely authenticate with GitHub and link your repositories effortlessly.
|
|
11
|
+
- **Real-time Logs**: Stream build and deployment logs directly to your terminal.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Forge requires Python 3.9 or higher.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install forge-cicd
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
1. **Initialize your project**:
|
|
24
|
+
```bash
|
|
25
|
+
forge init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. **Login with GitHub**:
|
|
29
|
+
```bash
|
|
30
|
+
forge login
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. **Link your repository**:
|
|
34
|
+
```bash
|
|
35
|
+
forge link <your-project-id>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
4. **Set your cloud credentials**:
|
|
39
|
+
```bash
|
|
40
|
+
forge set-cred railway
|
|
41
|
+
forge set-cred vercel
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
5. **Start a local worker** (optional, for CI execution):
|
|
45
|
+
```bash
|
|
46
|
+
forge worker start
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
6. **Deploy**:
|
|
50
|
+
```bash
|
|
51
|
+
forge deploy
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Documentation
|
|
55
|
+
|
|
56
|
+
For full documentation and advanced usage, visit [github.com/TahirSiddique092/forge](https://github.com/TahirSiddique092/forge).
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
Forge is released under the MIT License.
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
def print_run_logs(run: dict):
|
|
2
|
+
commit = run.get("commit", "")[:7]
|
|
3
|
+
message = run.get("message") or "(no commit message)"
|
|
4
|
+
status = run.get("status", "unknown")
|
|
5
|
+
created = run.get("created_at", "")
|
|
6
|
+
logs = run.get("logs") or {}
|
|
7
|
+
|
|
8
|
+
print("")
|
|
9
|
+
print(f"Run {run['id']}")
|
|
10
|
+
print("-" * 72)
|
|
11
|
+
print(f"Commit {commit} {message}")
|
|
12
|
+
print(f"Status {status}")
|
|
13
|
+
print(f"Created {created}")
|
|
14
|
+
print("")
|
|
15
|
+
|
|
16
|
+
steps = logs.get("steps", [])
|
|
17
|
+
|
|
18
|
+
if not steps:
|
|
19
|
+
print("No logs available for this run.")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
print("Steps")
|
|
23
|
+
print("-" * 72)
|
|
24
|
+
|
|
25
|
+
for idx, step in enumerate(steps, start=1):
|
|
26
|
+
print(f"[{idx}] {step['name']}")
|
|
27
|
+
print(f"Command:")
|
|
28
|
+
print(f" {step.get('command', '(unknown)')}")
|
|
29
|
+
print("")
|
|
30
|
+
|
|
31
|
+
if step.get("stdout"):
|
|
32
|
+
print("Output:")
|
|
33
|
+
for line in step["stdout"].splitlines():
|
|
34
|
+
print(f" {line}")
|
|
35
|
+
|
|
36
|
+
if step.get("stderr"):
|
|
37
|
+
print("Error:")
|
|
38
|
+
for line in step["stderr"].splitlines():
|
|
39
|
+
print(f" {line}")
|
|
40
|
+
|
|
41
|
+
print("")
|
|
42
|
+
print(
|
|
43
|
+
f"Result {step['status']} "
|
|
44
|
+
f"({step.get('duration_ms', 0)} ms)"
|
|
45
|
+
)
|
|
46
|
+
print("")
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
from rich.prompt import Prompt
|
|
5
|
+
from forge.config import load_config, save_config
|
|
6
|
+
from forge import ui
|
|
7
|
+
|
|
8
|
+
BACKEND_URL = os.getenv("FORGE_BACKEND_URL", "https://forge-backend-wwp9.onrender.com")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create(name: str = typer.Argument(..., help="Project name")):
|
|
12
|
+
"""
|
|
13
|
+
Create a new Forge project and link this directory to it.
|
|
14
|
+
|
|
15
|
+
Creates a project with a standard backend (Railway) and frontend (Vercel)
|
|
16
|
+
component spec. To use a custom spec, create the project via the dashboard.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
cfg = load_config()
|
|
21
|
+
except Exception:
|
|
22
|
+
cfg = {}
|
|
23
|
+
|
|
24
|
+
session_token = cfg.get("session_token")
|
|
25
|
+
if not session_token:
|
|
26
|
+
ui.error("Not authenticated. Run [bold]forge login[/bold] first.")
|
|
27
|
+
raise typer.Exit(1)
|
|
28
|
+
|
|
29
|
+
ui.blank()
|
|
30
|
+
ui.header("Backend Configuration (Railway)")
|
|
31
|
+
backend_choice = Prompt.ask(
|
|
32
|
+
"Framework",
|
|
33
|
+
choices=["fastapi", "flask", "express", "custom"],
|
|
34
|
+
default="fastapi",
|
|
35
|
+
console=ui.console
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if backend_choice == "fastapi":
|
|
39
|
+
b_runtime = "python"
|
|
40
|
+
b_install = "pip install -r requirements.txt"
|
|
41
|
+
b_build = "uvicorn app.main:app --host 0.0.0.0 --port $PORT"
|
|
42
|
+
|
|
43
|
+
elif backend_choice == "flask":
|
|
44
|
+
b_runtime = "python"
|
|
45
|
+
b_install = "pip install -r requirements.txt"
|
|
46
|
+
b_build = "gunicorn app:app -b 0.0.0.0:$PORT"
|
|
47
|
+
|
|
48
|
+
elif backend_choice == "express":
|
|
49
|
+
b_runtime = "node"
|
|
50
|
+
b_install = "npm install"
|
|
51
|
+
b_build = "npm start"
|
|
52
|
+
else:
|
|
53
|
+
b_runtime = Prompt.ask(" Runtime", choices=["python", "node"], default="python", console=ui.console)
|
|
54
|
+
b_install = Prompt.ask(" Install Command", default="pip install -r requirements.txt", console=ui.console)
|
|
55
|
+
b_build = Prompt.ask(" Build/Start Command", default="uvicorn app.main:app --host 0.0.0.0 --port $PORT", console=ui.console)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
b_root = Prompt.ask("Root directory", default="backend", console=ui.console)
|
|
59
|
+
|
|
60
|
+
ui.blank()
|
|
61
|
+
ui.header("Frontend Configuration (Vercel)")
|
|
62
|
+
frontend_choice = Prompt.ask(
|
|
63
|
+
"Framework",
|
|
64
|
+
choices=["nextjs", "vite", "custom"],
|
|
65
|
+
default="nextjs",
|
|
66
|
+
console=ui.console
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if frontend_choice == "nextjs" or frontend_choice == "vite":
|
|
70
|
+
f_runtime = "node"
|
|
71
|
+
f_install = "npm install"
|
|
72
|
+
f_build = "npm run build"
|
|
73
|
+
else:
|
|
74
|
+
f_runtime = Prompt.ask(" Runtime", choices=["node", "python"], default="node", console=ui.console)
|
|
75
|
+
f_install = Prompt.ask(" Install Command", default="npm install", console=ui.console)
|
|
76
|
+
f_build = Prompt.ask(" Build Command", default="npm run build", console=ui.console)
|
|
77
|
+
|
|
78
|
+
f_root = Prompt.ask("Root directory", default="frontend", console=ui.console)
|
|
79
|
+
ui.blank()
|
|
80
|
+
|
|
81
|
+
payload = {
|
|
82
|
+
"name": name,
|
|
83
|
+
"spec": {
|
|
84
|
+
"components": [
|
|
85
|
+
{
|
|
86
|
+
"name": "backend",
|
|
87
|
+
"platform": "railway",
|
|
88
|
+
"root_dir": b_root,
|
|
89
|
+
"runtime": b_runtime,
|
|
90
|
+
"install_command": b_install,
|
|
91
|
+
"build_command": b_build,
|
|
92
|
+
"test_command": None,
|
|
93
|
+
"env_vars": {},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "frontend",
|
|
97
|
+
"platform": "vercel",
|
|
98
|
+
"root_dir": f_root,
|
|
99
|
+
"runtime": f_runtime,
|
|
100
|
+
"install_command": f_install,
|
|
101
|
+
"build_command": f_build,
|
|
102
|
+
"test_command": None,
|
|
103
|
+
"env_vars": {},
|
|
104
|
+
},
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
with ui.console.status("[dim]Creating project...[/dim]", spinner="dots"):
|
|
110
|
+
r = requests.post(
|
|
111
|
+
f"{BACKEND_URL}/projects",
|
|
112
|
+
json=payload,
|
|
113
|
+
headers={"Authorization": f"Bearer {session_token}"},
|
|
114
|
+
timeout=15,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if r.status_code != 200:
|
|
118
|
+
ui.error(f"Failed to create project: {r.text}")
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
|
|
121
|
+
data = r.json()
|
|
122
|
+
proj_id = data["project_details"]["project_id"]
|
|
123
|
+
worker_tok = data["worker_details"]["worker_token"]
|
|
124
|
+
|
|
125
|
+
cfg["project_id"] = proj_id
|
|
126
|
+
cfg["worker_token"] = worker_tok
|
|
127
|
+
save_config(cfg)
|
|
128
|
+
|
|
129
|
+
ui.success(f"Project [bold]{name}[/bold] created.")
|
|
130
|
+
ui.blank()
|
|
131
|
+
ui.label("Project ID", proj_id)
|
|
132
|
+
ui.label("Worker token", worker_tok)
|
|
133
|
+
ui.blank()
|
|
134
|
+
ui.info("Next steps:")
|
|
135
|
+
ui.console.print(" 1. [bold]forge set-cred railway[/bold] — add your Railway API key")
|
|
136
|
+
ui.console.print(" 2. [bold]forge set-cred vercel[/bold] — add your Vercel token")
|
|
137
|
+
ui.console.print(" 3. [bold]forge link[/bold] — link this repo to the project")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
from forge.config import load_config
|
|
5
|
+
from forge import ui
|
|
6
|
+
|
|
7
|
+
BACKEND_URL = os.getenv("FORGE_BACKEND_URL", "https://forge-backend-wwp9.onrender.com")
|
|
8
|
+
|
|
9
|
+
_VALID_PROVIDERS = ["railway", "vercel"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def set_credential(
|
|
13
|
+
provider: str = typer.Argument(
|
|
14
|
+
...,
|
|
15
|
+
help="Hosting provider: railway or vercel",
|
|
16
|
+
),
|
|
17
|
+
token: str = typer.Option(
|
|
18
|
+
...,
|
|
19
|
+
prompt="API token",
|
|
20
|
+
hide_input=True,
|
|
21
|
+
help="API key / token for the provider (input is hidden)",
|
|
22
|
+
),
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Save a hosting provider API token.
|
|
26
|
+
|
|
27
|
+
The token is encrypted at rest and never stored locally.
|
|
28
|
+
Run this once per provider before your first deployment.
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
Valid options:
|
|
32
|
+
railway — Railway API token
|
|
33
|
+
vercel — Vercel personal access token
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
if provider not in _VALID_PROVIDERS:
|
|
37
|
+
ui.error(
|
|
38
|
+
f"Unknown provider '{provider}'. "
|
|
39
|
+
f"Valid options: {', '.join(_VALID_PROVIDERS)}"
|
|
40
|
+
)
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
cfg = load_config()
|
|
45
|
+
except Exception:
|
|
46
|
+
ui.error("Not initialized. Run [bold]forge init[/bold] first.")
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
|
|
49
|
+
session_token = cfg.get("session_token")
|
|
50
|
+
if not session_token:
|
|
51
|
+
ui.error("Not authenticated. Run [bold]forge login[/bold] first.")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
with ui.console.status(f"[dim]Saving {provider} credentials...[/dim]", spinner="dots"):
|
|
55
|
+
r = requests.post(
|
|
56
|
+
f"{BACKEND_URL}/credentials",
|
|
57
|
+
json={"provider": provider, "token": token},
|
|
58
|
+
headers={"Authorization": f"Bearer {session_token}"},
|
|
59
|
+
timeout=10,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if r.status_code == 200:
|
|
63
|
+
ui.success(f"{provider.capitalize()} credentials saved.")
|
|
64
|
+
else:
|
|
65
|
+
ui.error(f"Failed to save credentials: {r.text}")
|
|
66
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
import webbrowser
|
|
6
|
+
from forge.config import load_config
|
|
7
|
+
from forge import ui
|
|
8
|
+
from rich.live import Live
|
|
9
|
+
|
|
10
|
+
BACKEND_URL = os.getenv("FORGE_BACKEND_URL", "https://forge-backend-wwp9.onrender.com")
|
|
11
|
+
|
|
12
|
+
def deploy():
|
|
13
|
+
"""
|
|
14
|
+
Trigger a deployment and follow its progress in real-time.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
cfg = load_config()
|
|
18
|
+
except Exception:
|
|
19
|
+
ui.error("Not initialized. Run [bold]forge init[/bold] first.")
|
|
20
|
+
raise typer.Exit(1)
|
|
21
|
+
|
|
22
|
+
project_id = cfg.get("project_id")
|
|
23
|
+
token = cfg.get("session_token")
|
|
24
|
+
|
|
25
|
+
if not project_id or not token:
|
|
26
|
+
ui.error("Not linked or authenticated. Run [bold]forge link[/bold] and [bold]forge login[/bold].")
|
|
27
|
+
raise typer.Exit(1)
|
|
28
|
+
|
|
29
|
+
# 1. Trigger Deployment
|
|
30
|
+
ui.info(f"Initiating deployment for project [cyan]{project_id}[/cyan]...")
|
|
31
|
+
try:
|
|
32
|
+
r = requests.post(
|
|
33
|
+
f"{BACKEND_URL}/projects/{project_id}/deploy",
|
|
34
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
35
|
+
timeout=15,
|
|
36
|
+
)
|
|
37
|
+
except requests.RequestException as e:
|
|
38
|
+
ui.error(f"Failed to reach backend: {e}")
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
|
|
41
|
+
if r.status_code != 200:
|
|
42
|
+
ui.error(f"Deployment trigger failed ({r.status_code}): {r.text}")
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
|
|
45
|
+
ui.success("Deployment sequence started! Tracking progress...")
|
|
46
|
+
ui.blank()
|
|
47
|
+
|
|
48
|
+
# 2. Polling Loop with Live UI
|
|
49
|
+
last_status = None
|
|
50
|
+
with Live(ui.console.print("[dim]Waiting for status...[/dim]"), refresh_per_second=1) as live:
|
|
51
|
+
while True:
|
|
52
|
+
try:
|
|
53
|
+
status_res = requests.get(
|
|
54
|
+
f"{BACKEND_URL}/projects/{project_id}/deploy/status",
|
|
55
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
56
|
+
timeout=10,
|
|
57
|
+
)
|
|
58
|
+
if status_res.status_code != 200:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
data = status_res.json()
|
|
62
|
+
raw_status = data.get("deploy_status") or "unknown"
|
|
63
|
+
components = data.get("components") or {}
|
|
64
|
+
|
|
65
|
+
# Update the Live display
|
|
66
|
+
# We reuse your component table logic for the UI
|
|
67
|
+
live.update(ui.render_deployment_progress(raw_status, components))
|
|
68
|
+
|
|
69
|
+
if "success" in raw_status.lower() or "failed" in raw_status.lower():
|
|
70
|
+
last_status = data
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
except Exception:
|
|
74
|
+
pass # Continue polling on transient network errors
|
|
75
|
+
|
|
76
|
+
time.sleep(3) # Poll every 3 seconds
|
|
77
|
+
|
|
78
|
+
# 3. Final Result Handling
|
|
79
|
+
raw_final_status = last_status.get("deploy_status", "unknown")
|
|
80
|
+
components = last_status.get("components", {})
|
|
81
|
+
|
|
82
|
+
if raw_final_status == "success":
|
|
83
|
+
ui.blank()
|
|
84
|
+
ui.success("Deployment complete.")
|
|
85
|
+
|
|
86
|
+
# Automatically open the primary frontend URL
|
|
87
|
+
for name, info in components.items():
|
|
88
|
+
url = info.get("url")
|
|
89
|
+
if url and ("vercel.app" in url or "vercel.com" in url):
|
|
90
|
+
ui.info(f"Opening [cyan]{url}[/cyan] in browser...")
|
|
91
|
+
try:
|
|
92
|
+
webbrowser.open(url)
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
break
|
|
96
|
+
else:
|
|
97
|
+
ui.blank()
|
|
98
|
+
ui.error(f"Deployment failed: {raw_final_status}")
|
|
99
|
+
ui.info("Check logs or dashboard for details.")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
import webbrowser
|
|
5
|
+
from forge.config import load_config
|
|
6
|
+
from forge import ui
|
|
7
|
+
|
|
8
|
+
BACKEND_URL = os.getenv("FORGE_BACKEND_URL", "https://forge-backend-wwp9.onrender.com")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def deploy_status():
|
|
12
|
+
"""
|
|
13
|
+
Show the current status of the most recent deployment.
|
|
14
|
+
A full deployment across multiple components can take
|
|
15
|
+
several minutes depending on your Railway and Vercel configurations.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
cfg = load_config()
|
|
20
|
+
except Exception:
|
|
21
|
+
ui.error("Not initialized. Run [bold]forge init[/bold] first.")
|
|
22
|
+
raise typer.Exit(1)
|
|
23
|
+
|
|
24
|
+
project_id = cfg.get("project_id")
|
|
25
|
+
if not project_id:
|
|
26
|
+
ui.error("Not linked to a project. Run [bold]forge link[/bold] first.")
|
|
27
|
+
raise typer.Exit(1)
|
|
28
|
+
|
|
29
|
+
token = cfg.get("session_token")
|
|
30
|
+
if not token:
|
|
31
|
+
ui.error("Not authenticated. Run [bold]forge login[/bold] first.")
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
r = requests.get(
|
|
36
|
+
f"{BACKEND_URL}/projects/{project_id}/deploy/status",
|
|
37
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
38
|
+
timeout=10,
|
|
39
|
+
)
|
|
40
|
+
except requests.RequestException as e:
|
|
41
|
+
ui.error(f"Request failed: {e}")
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
|
|
44
|
+
if r.status_code != 200:
|
|
45
|
+
ui.error(f"Could not fetch deployment status ({r.status_code}).")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
|
|
48
|
+
data = r.json()
|
|
49
|
+
raw_status = data.get("deploy_status") or "unknown"
|
|
50
|
+
components = data.get("components") or {}
|
|
51
|
+
|
|
52
|
+
# ── Header ────────────────────────────────────────────────────────────────
|
|
53
|
+
ui.blank()
|
|
54
|
+
ui.label("Project", project_id)
|
|
55
|
+
ui.console.print(f" [dim]{'Status':<14}[/dim]", end="")
|
|
56
|
+
ui.console.print(ui.status_badge(raw_status))
|
|
57
|
+
ui.blank()
|
|
58
|
+
|
|
59
|
+
if raw_status == "no_deployment_found":
|
|
60
|
+
ui.warn("No deployment has been triggered yet. Run [bold]forge deploy[/bold] first.")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# ── Component breakdown ───────────────────────────────────────────────────
|
|
64
|
+
if components:
|
|
65
|
+
ui.console.rule("[dim]Components[/dim]", style="dim")
|
|
66
|
+
ui.blank()
|
|
67
|
+
ui.deploy_components_table(components)
|
|
68
|
+
|
|
69
|
+
# ── Final outcome ─────────────────────────────────────────────────────────
|
|
70
|
+
if raw_status == "success":
|
|
71
|
+
ui.blank()
|
|
72
|
+
ui.success("Deployment complete.")
|
|
73
|
+
|
|
74
|
+
# Open the first Vercel URL in the browser
|
|
75
|
+
for name, info in components.items():
|
|
76
|
+
url = info.get("url") or ""
|
|
77
|
+
if "vercel.app" in url or "vercel.com" in url:
|
|
78
|
+
ui.info(f"Opening [cyan]{url}[/cyan] in your browser.")
|
|
79
|
+
try:
|
|
80
|
+
webbrowser.open(url)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
elif "failed" in raw_status.lower():
|
|
86
|
+
ui.blank()
|
|
87
|
+
reason = raw_status.replace("failed: ", "").strip()
|
|
88
|
+
|
|
89
|
+
if "GITHUB_INTEGRATION_MISSING" in reason:
|
|
90
|
+
parts = reason.split("|")
|
|
91
|
+
provider_part = parts[0].split(":")[1].strip().lower()
|
|
92
|
+
repo = parts[1].strip() if len(parts) > 1 else "your repository"
|
|
93
|
+
|
|
94
|
+
app_name = "Railway" if provider_part == "railway" else "Vercel"
|
|
95
|
+
provider_part = "railway-app" if provider_part == "railway" else "vercel"
|
|
96
|
+
app_url = f"https://github.com/apps/{provider_part}"
|
|
97
|
+
|
|
98
|
+
ui.error(f"Deployment failed: {app_name} cannot access your repository.")
|
|
99
|
+
ui.blank()
|
|
100
|
+
ui.rule("Action Required")
|
|
101
|
+
ui.blank()
|
|
102
|
+
ui.info(f"The {app_name} GitHub App is not installed or lacks permissions.")
|
|
103
|
+
ui.blank()
|
|
104
|
+
|
|
105
|
+
ui.label("1. Install", app_url)
|
|
106
|
+
ui.label("2. Permission", f"Grant access to: {repo}")
|
|
107
|
+
ui.label("3. Retry", "Run [bold]forge deploy[/bold] again")
|
|
108
|
+
|
|
109
|
+
ui.blank()
|
|
110
|
+
ui.rule()
|
|
111
|
+
else:
|
|
112
|
+
ui.error(f"Deployment failed: {reason}")
|
|
113
|
+
ui.info("Check your Railway / Vercel dashboard for detailed logs.")
|
|
114
|
+
|
|
115
|
+
else:
|
|
116
|
+
ui.blank()
|
|
117
|
+
ui.info("Deployment is in progress. Run this command again to check for updates.")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
from forge.config import load_config, save_config
|
|
5
|
+
from forge import ui
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def init():
|
|
9
|
+
"""Initialize forge in the current git repository."""
|
|
10
|
+
|
|
11
|
+
if not os.path.exists(".git"):
|
|
12
|
+
ui.error("Not a git repository. Run this command from your project root.")
|
|
13
|
+
raise typer.Exit(1)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
repo = subprocess.check_output(
|
|
17
|
+
["git", "remote", "get-url", "origin"],
|
|
18
|
+
stderr=subprocess.DEVNULL,
|
|
19
|
+
).decode().strip()
|
|
20
|
+
except Exception:
|
|
21
|
+
repo = "unknown"
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
config = load_config()
|
|
25
|
+
except Exception:
|
|
26
|
+
config = {}
|
|
27
|
+
|
|
28
|
+
config["repo"] = repo
|
|
29
|
+
save_config(config)
|
|
30
|
+
|
|
31
|
+
ui.success("forge initialized")
|
|
32
|
+
ui.blank()
|
|
33
|
+
ui.label("Repository", repo)
|
|
34
|
+
ui.blank()
|
|
35
|
+
ui.info("Next: run [bold]forge login[/bold] to authenticate with GitHub.")
|