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.
@@ -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.")