deployforge 0.1.0__py3-none-any.whl
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.
- deployforge/__init__.py +3 -0
- deployforge/__version__.py +3 -0
- deployforge/analyzer/__init__.py +23 -0
- deployforge/analyzer/backend.py +326 -0
- deployforge/analyzer/database.py +120 -0
- deployforge/analyzer/frontend.py +179 -0
- deployforge/analyzer/project.py +389 -0
- deployforge/analyzer/shared.py +109 -0
- deployforge/cli.py +825 -0
- deployforge/config.py +195 -0
- deployforge/deployment/__init__.py +19 -0
- deployforge/deployment/orchestrator.py +331 -0
- deployforge/deployment/planner.py +172 -0
- deployforge/deployment/verifier.py +65 -0
- deployforge/errors/__init__.py +53 -0
- deployforge/github/__init__.py +21 -0
- deployforge/github/integration.py +127 -0
- deployforge/integration/__init__.py +20 -0
- deployforge/integration/cors.py +30 -0
- deployforge/integration/environment.py +62 -0
- deployforge/integration/frontend_backend.py +39 -0
- deployforge/providers/__init__.py +32 -0
- deployforge/providers/base.py +151 -0
- deployforge/providers/render.py +218 -0
- deployforge/providers/vercel.py +205 -0
- deployforge/security/__init__.py +4 -0
- deployforge/security/gitignore.py +35 -0
- deployforge/security/scanner.py +125 -0
- deployforge/security/secrets.py +110 -0
- deployforge/ui/__init__.py +1 -0
- deployforge/ui/terminal.py +151 -0
- deployforge-0.1.0.dist-info/METADATA +218 -0
- deployforge-0.1.0.dist-info/RECORD +37 -0
- deployforge-0.1.0.dist-info/WHEEL +5 -0
- deployforge-0.1.0.dist-info/entry_points.txt +2 -0
- deployforge-0.1.0.dist-info/licenses/LICENSE +21 -0
- deployforge-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Security preflight scanner.
|
|
2
|
+
|
|
3
|
+
Scans a project for sensitive files, secret patterns, and high-entropy
|
|
4
|
+
assignments before a deployment is attempted. Secret values are never
|
|
5
|
+
included in findings — only non-reversible fingerprints.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from deployforge.analyzer.shared import walk_files
|
|
14
|
+
from deployforge.security.secrets import (
|
|
15
|
+
HIGH_ENTROPY_RE,
|
|
16
|
+
SECRET_PATTERNS,
|
|
17
|
+
Finding,
|
|
18
|
+
fingerprint,
|
|
19
|
+
is_high_entropy,
|
|
20
|
+
is_sensitive_path,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
IGNORED_SCAN_DIRS = {".git", ".github", "node_modules", ".venv", "__pycache__", "dist", "build"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SecurityReport:
|
|
28
|
+
findings: list[Finding]
|
|
29
|
+
files_scanned: int
|
|
30
|
+
sensitive_files: list[str] = field(default_factory=list)
|
|
31
|
+
gitignore_ok: bool = True
|
|
32
|
+
gitignore_missing: list[str] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def high_confidence(self) -> bool:
|
|
36
|
+
return any(f.confidence == "high" for f in self.findings)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def blocked(self) -> bool:
|
|
40
|
+
return self.high_confidence
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _scan_file(path: Path, relative_to: Path, strict: bool) -> list[Finding]:
|
|
44
|
+
try:
|
|
45
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
46
|
+
except OSError:
|
|
47
|
+
return []
|
|
48
|
+
rel = path.relative_to(relative_to).as_posix()
|
|
49
|
+
findings: list[Finding] = []
|
|
50
|
+
|
|
51
|
+
if is_sensitive_path(rel):
|
|
52
|
+
findings.append(Finding(name="Sensitive file", file=rel, line=None, confidence="high"))
|
|
53
|
+
|
|
54
|
+
for line_no, line in enumerate(text.splitlines(), start=1):
|
|
55
|
+
if not line.strip():
|
|
56
|
+
continue
|
|
57
|
+
for pattern, name in SECRET_PATTERNS:
|
|
58
|
+
match = pattern.search(line)
|
|
59
|
+
if match:
|
|
60
|
+
value = match.group(0)
|
|
61
|
+
if "PRIVATE KEY" in name:
|
|
62
|
+
value = "-----BEGIN " + "PRIVATE KEY-----"
|
|
63
|
+
findings.append(
|
|
64
|
+
Finding(
|
|
65
|
+
name=name,
|
|
66
|
+
file=rel,
|
|
67
|
+
line=line_no,
|
|
68
|
+
confidence="high",
|
|
69
|
+
value_fingerprint=fingerprint(value),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
break
|
|
73
|
+
match = HIGH_ENTROPY_RE.search(line)
|
|
74
|
+
if match:
|
|
75
|
+
value = match.group("value")
|
|
76
|
+
if value and is_high_entropy(value):
|
|
77
|
+
confidence = "high" if strict else "medium"
|
|
78
|
+
findings.append(
|
|
79
|
+
Finding(
|
|
80
|
+
name="High-entropy assignment",
|
|
81
|
+
file=rel,
|
|
82
|
+
line=line_no,
|
|
83
|
+
confidence=confidence,
|
|
84
|
+
value_fingerprint=fingerprint(value),
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return findings
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def scan_project(root: Path, level: str = "normal") -> SecurityReport:
|
|
91
|
+
"""Scan *root* and return a security report.
|
|
92
|
+
|
|
93
|
+
``level`` is one of ``"off"``, ``"normal"``, ``"strict"``.
|
|
94
|
+
"""
|
|
95
|
+
normal = level != "off"
|
|
96
|
+
strict = level == "strict"
|
|
97
|
+
findings: list[Finding] = []
|
|
98
|
+
sensitive_files: list[str] = []
|
|
99
|
+
files_scanned = 0
|
|
100
|
+
|
|
101
|
+
from deployforge.security.gitignore import validate_gitignore
|
|
102
|
+
|
|
103
|
+
gitignore_ok, gitignore_missing = validate_gitignore(root)
|
|
104
|
+
|
|
105
|
+
for path in walk_files(root):
|
|
106
|
+
if any(part in IGNORED_SCAN_DIRS for part in path.parts):
|
|
107
|
+
continue
|
|
108
|
+
if path.name in {".gitignore"}:
|
|
109
|
+
continue
|
|
110
|
+
files_scanned += 1
|
|
111
|
+
if not normal:
|
|
112
|
+
continue
|
|
113
|
+
file_findings = _scan_file(path, root, strict)
|
|
114
|
+
findings.extend(file_findings)
|
|
115
|
+
for f in file_findings:
|
|
116
|
+
if f.name == "Sensitive file":
|
|
117
|
+
sensitive_files.append(f.file)
|
|
118
|
+
|
|
119
|
+
return SecurityReport(
|
|
120
|
+
findings=findings,
|
|
121
|
+
files_scanned=files_scanned,
|
|
122
|
+
sensitive_files=sorted(set(sensitive_files)),
|
|
123
|
+
gitignore_ok=gitignore_ok,
|
|
124
|
+
gitignore_missing=gitignore_missing,
|
|
125
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Secret patterns and sensitive-file rules for the security preflight."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
# Files that should never reach a repository or deployment configuration.
|
|
10
|
+
SENSITIVE_FILENAMES = (
|
|
11
|
+
".env",
|
|
12
|
+
".env.local",
|
|
13
|
+
".env.production",
|
|
14
|
+
".env.development",
|
|
15
|
+
".env.test",
|
|
16
|
+
"credentials.json",
|
|
17
|
+
"service-account.json",
|
|
18
|
+
"client_secret.json",
|
|
19
|
+
"id_rsa",
|
|
20
|
+
"id_ed25519",
|
|
21
|
+
"id_dsa",
|
|
22
|
+
"id_ecdsa",
|
|
23
|
+
".netrc",
|
|
24
|
+
".npmrc",
|
|
25
|
+
"dockerconfigjson",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
SENSITIVE_EXTENSIONS = (".pem", ".key", ".p12", ".pfx", ".p8", ".keystore")
|
|
29
|
+
|
|
30
|
+
SENSITIVE_SUFFIXES = ("_rsa", "_dsa", "_ed25519", "_ecdsa")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Finding:
|
|
35
|
+
name: str
|
|
36
|
+
file: str
|
|
37
|
+
line: int | None
|
|
38
|
+
confidence: str
|
|
39
|
+
value_fingerprint: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_AUTH = r"[^/\s@]+:[^@\s]+@"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def fingerprint(value: str) -> str:
|
|
46
|
+
digest = hashlib.sha256(value.encode("utf-8", errors="ignore")).hexdigest()
|
|
47
|
+
return digest[:10]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# (pattern, name)
|
|
51
|
+
SECRET_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
|
|
52
|
+
(re.compile(r"AKIA[0-9A-Z]{16}"), "AWS access key"),
|
|
53
|
+
(re.compile(r"AIza[0-9A-Za-z\-_]{35}", re.IGNORECASE), "Google API key"),
|
|
54
|
+
(re.compile(r"ghp_[A-Za-z0-9]{36}"), "GitHub personal access token"),
|
|
55
|
+
(re.compile(r"gho_[A-Za-z0-9]{36}"), "GitHub OAuth token"),
|
|
56
|
+
(re.compile(r"github_pat_[A-Za-z0-9_]{22,}"), "GitHub fine-grained token"),
|
|
57
|
+
(re.compile(r"xox[baprs]-[A-Za-z0-9\-]{10,}"), "Slack token"),
|
|
58
|
+
(re.compile(r"sk_live_[0-9A-Za-z]{24,}"), "Stripe live secret key"),
|
|
59
|
+
(re.compile(r"sk_test_[0-9A-Za-z]{24,}"), "Stripe test secret key"),
|
|
60
|
+
(re.compile(r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----"), "Private key"),
|
|
61
|
+
(
|
|
62
|
+
re.compile(rf"postgres(?:ql)?://{_AUTH}"),
|
|
63
|
+
"Database connection string",
|
|
64
|
+
),
|
|
65
|
+
(re.compile(rf"mongo(db|\+srv)?://{_AUTH}"), "MongoDB connection string"),
|
|
66
|
+
(re.compile(rf"redis://{_AUTH}"), "Redis connection string"),
|
|
67
|
+
(
|
|
68
|
+
re.compile(
|
|
69
|
+
r"(?i)(?P<key>aws_secret_access_key|secret_key|api_secret|client_secret|"
|
|
70
|
+
r"jwt_secret|private_key)\s*[=:]\s*[\"']?[A-Za-z0-9+/=_\-]{16,}[\"']?"
|
|
71
|
+
),
|
|
72
|
+
"Secret assignment",
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# High-entropy token assignments (>= 24 mixed-case chars plus digits).
|
|
77
|
+
HIGH_ENTROPY_RE = re.compile(
|
|
78
|
+
r"(?i)((?P<key>api[_\-]?key|token|password|secret))\s*[=:]\s*"
|
|
79
|
+
r"[\"'](?P<value>[A-Za-z0-9+/=_\-]{24,})[\"']"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
_MIXED_CASE_AND_DIGIT = re.compile(r"[A-Z].*[a-z].*[0-9]|[a-z].*[A-Z].*[0-9]|[0-9].*[A-Za-z]")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_high_entropy(value: str) -> bool:
|
|
86
|
+
"""Guard against colored hex/counts: require mixed case *and* digits."""
|
|
87
|
+
return bool(_MIXED_CASE_AND_DIGIT.match(value)) and sum(c.isdigit() for c in value) >= 2
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def is_sensitive_filename(name: str) -> bool:
|
|
91
|
+
lower = name.lower()
|
|
92
|
+
if lower in SENSITIVE_FILENAMES:
|
|
93
|
+
return True
|
|
94
|
+
if lower.endswith(SENSITIVE_EXTENSIONS):
|
|
95
|
+
return True
|
|
96
|
+
return casefold_suffix(lower)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def casefold_suffix(lower: str) -> bool:
|
|
100
|
+
return any(lower.endswith(suffix) for suffix in SENSITIVE_SUFFIXES)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def is_sensitive_path(path: str) -> bool:
|
|
104
|
+
"""True for file names or extensions that should never be deployed."""
|
|
105
|
+
lower = path.lower()
|
|
106
|
+
if any(part in lower for part in (".env",)) and lower.split("/")[-1].startswith(".env"):
|
|
107
|
+
return True
|
|
108
|
+
if is_sensitive_filename(path.split("/")[-1]):
|
|
109
|
+
return True
|
|
110
|
+
return lower.endswith(SENSITIVE_EXTENSIONS)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from deployforge.ui.terminal import * # noqa: F401,F403
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Terminal UI helpers for DeployForge built on Rich.
|
|
2
|
+
|
|
3
|
+
All rendering goes through this module so the surface can be replaced without
|
|
4
|
+
touching the rest of the codebase. Works on every OS; animation is skipped
|
|
5
|
+
automatically on non-TTY output (CI, pipes, redirects).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.live import Live
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
TAGLINE = "GitHub → Production Automation"
|
|
23
|
+
CREDIT = "built by sifuna codex"
|
|
24
|
+
|
|
25
|
+
HEADER = "DEPLOYFORGE"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _animation_enabled() -> bool:
|
|
29
|
+
flag = os.environ.get("DEPLOYFORGE_NO_ANIMATION")
|
|
30
|
+
if flag and flag.strip().lower() in {"1", "true", "yes", "on"}:
|
|
31
|
+
return False
|
|
32
|
+
return bool(console.is_terminal) and not os.environ.get("CI")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _banner_frames() -> list[Text]:
|
|
36
|
+
"""Build the DEPLOYFORGE reveal frames (letters sweep in one by one)."""
|
|
37
|
+
frames: list[Text] = []
|
|
38
|
+
for i in range(len(HEADER) + 1):
|
|
39
|
+
text = Text()
|
|
40
|
+
text.append(" " * 4)
|
|
41
|
+
text.append(HEADER[:i], style="bold white on blue")
|
|
42
|
+
text.append(HEADER[i:], style="bold dim white")
|
|
43
|
+
frames.append(text)
|
|
44
|
+
flash = Text(" " * 4)
|
|
45
|
+
flash.append(HEADER, style="bold white on red")
|
|
46
|
+
frames.append(flash)
|
|
47
|
+
return frames
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _render_banner_text(header: Text) -> Text:
|
|
51
|
+
text = Text()
|
|
52
|
+
text.append("\n")
|
|
53
|
+
text.append_text(header)
|
|
54
|
+
text.append("\n\n")
|
|
55
|
+
text.append(TAGLINE, style="bold cyan")
|
|
56
|
+
text.append("\n")
|
|
57
|
+
text.append(CREDIT, style="italic dim")
|
|
58
|
+
return text
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _static_banner() -> None:
|
|
62
|
+
text = _render_banner_text(Text(" " * 4 + HEADER, style="bold white on blue"))
|
|
63
|
+
console.print(Panel.fit(text, border_style="blue", padding=(1, 3)))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def banner() -> None:
|
|
67
|
+
if not _animation_enabled():
|
|
68
|
+
_static_banner()
|
|
69
|
+
return
|
|
70
|
+
try:
|
|
71
|
+
with Live(console=console, refresh_per_second=24, transient=True) as live:
|
|
72
|
+
for frame in _banner_frames():
|
|
73
|
+
live.update(_render_banner_text(frame))
|
|
74
|
+
time.sleep(0.045)
|
|
75
|
+
time.sleep(0.15)
|
|
76
|
+
except (OSError, KeyboardInterrupt):
|
|
77
|
+
pass
|
|
78
|
+
console.print()
|
|
79
|
+
_static_banner()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def section(title: str) -> None:
|
|
83
|
+
console.print()
|
|
84
|
+
console.rule(title, style="blue")
|
|
85
|
+
console.print()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def ok(message: str) -> None:
|
|
89
|
+
console.print(f"[bold green]\u2713[/bold green] {message}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def warn(message: str) -> None:
|
|
93
|
+
console.print(f"[bold yellow]\u26a0[/bold yellow] {message}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def info(message: str) -> None:
|
|
97
|
+
console.print(f"[dim]\u203a[/dim] {message}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def step(message: str) -> None:
|
|
101
|
+
console.print(f"[bold]{message}[/bold]")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def note(message: str) -> None:
|
|
105
|
+
console.print(f"[dim]{message}[/dim]")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def error(message: str, hint: str | None = None) -> None:
|
|
109
|
+
console.print(f"[bold red]\u2717[/bold red] {message}")
|
|
110
|
+
if hint:
|
|
111
|
+
console.print(hint)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def link(url: str) -> None:
|
|
115
|
+
console.print(url, style="bold underline cyan")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def fail(message: str, hint: str | None = None) -> None:
|
|
119
|
+
console.print(f"[bold red]\u2717[/bold red] {message}")
|
|
120
|
+
if hint:
|
|
121
|
+
console.print(hint)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@contextmanager
|
|
125
|
+
def spinner(message: str) -> Iterator[None]:
|
|
126
|
+
"""Render a spinner while a blocking operation runs."""
|
|
127
|
+
with console.status(f"[bold]{message}[/bold]", spinner="dots12"):
|
|
128
|
+
yield
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def prompt_yes_no(message: str, default: bool = True) -> bool:
|
|
132
|
+
suffix = " [Y/n]" if default else " [y/N]"
|
|
133
|
+
try:
|
|
134
|
+
answer = input(f"{message}{suffix} ")
|
|
135
|
+
except (EOFError, KeyboardInterrupt):
|
|
136
|
+
return default
|
|
137
|
+
cleaned = answer.strip().lower()
|
|
138
|
+
if not cleaned:
|
|
139
|
+
return default
|
|
140
|
+
return cleaned in ("y", "yes")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def live_panel(title: str, lines: list[tuple[str, str]]) -> None:
|
|
144
|
+
"""Render the final live-application summary panel."""
|
|
145
|
+
text = Text()
|
|
146
|
+
text.append(title, style="bold white on blue")
|
|
147
|
+
text.append("\n\n")
|
|
148
|
+
for label, value in lines:
|
|
149
|
+
text.append(f"{label}\n", style="bold")
|
|
150
|
+
text.append(f"{value}\n\n", style="cyan")
|
|
151
|
+
console.print(Panel.fit(text, border_style="blue", padding=(1, 2)))
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deployforge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: From GitHub to a Live Application. Automatically. DeployForge analyzes an application and deploys its frontend and backend to production.
|
|
5
|
+
Author-email: sifuna codex <www.antonysifuna07@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aasz253/DeployForge
|
|
8
|
+
Project-URL: Repository, https://github.com/aasz253/DeployForge
|
|
9
|
+
Project-URL: Documentation, https://github.com/aasz253/DeployForge#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/aasz253/DeployForge/issues
|
|
11
|
+
Keywords: deploy,vercel,render,devsecops,cli,automation,github
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
22
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: typer<1.0,>=0.12
|
|
27
|
+
Requires-Dist: rich<15.0,>=13.7
|
|
28
|
+
Requires-Dist: requests<3.0,>=2.31
|
|
29
|
+
Requires-Dist: PyYAML<7.0,>=6.0
|
|
30
|
+
Requires-Dist: keyring<26.0,>=24.3
|
|
31
|
+
Requires-Dist: platformdirs<5.0,>=4.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov<6.0,>=5.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff<1.0,>=0.5; extra == "dev"
|
|
36
|
+
Requires-Dist: mypy<2.0,>=1.10; extra == "dev"
|
|
37
|
+
Requires-Dist: types-PyYAML>=6.0; extra == "dev"
|
|
38
|
+
Requires-Dist: bandit<2.0,>=1.7; extra == "dev"
|
|
39
|
+
Requires-Dist: build<2.0,>=1.2; extra == "dev"
|
|
40
|
+
Requires-Dist: twine<6.0,>=5.0; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# DeployForge
|
|
44
|
+
|
|
45
|
+
**From GitHub to production. Automatically.**
|
|
46
|
+
|
|
47
|
+
DeployForge is the second stage of the PushForge → DeployForge workflow.
|
|
48
|
+
PushForge handles **Local → GitHub**, while DeployForge handles **GitHub → Production** (frontend → Vercel, backend → Render).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install deployforge
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or with [pipx](https://pypa.github.io/pipx/) (recommended):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pipx install deployforge
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
DeployForge is a single, cross-platform CLI — it works on Windows, macOS, and Linux.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 1. Push your code with PushForge
|
|
72
|
+
pushforge
|
|
73
|
+
|
|
74
|
+
# 2. Deploy with DeployForge
|
|
75
|
+
deployforge
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
DeployForge automatically:
|
|
79
|
+
1. Analyzes your project structure (Next.js, Vite, FastAPI, Express, Django…)
|
|
80
|
+
2. Creates a deployment plan (what goes to Vercel, what goes to Render)
|
|
81
|
+
3. Runs security preflight checks (scans for leaked secrets)
|
|
82
|
+
4. Deploys backend → Render, frontend → Vercel
|
|
83
|
+
5. Wires environment variables (`NEXT_PUBLIC_API_URL`, `FRONTEND_URL`, `CORS_ORIGIN`)
|
|
84
|
+
6. Sets up CORS on the backend
|
|
85
|
+
7. Verifies all deployments and prints live URLs
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Commands
|
|
90
|
+
|
|
91
|
+
| Command | Description |
|
|
92
|
+
|---------|-------------|
|
|
93
|
+
| `deployforge` | Full automated deployment (analyze → deploy → verify) |
|
|
94
|
+
| `deployforge init` | Create `.deployforge/config.yml` for a project |
|
|
95
|
+
| `deployforge analyze` | Analyze project structure and show deployment plan |
|
|
96
|
+
| `deployforge deploy` | Deploy directly (with flags `--dry-run`, `--debug`) |
|
|
97
|
+
| `deployforge status` | Show status of deployed services |
|
|
98
|
+
| `deployforge verify` | Verify all deployed URLs are reachable and healthy |
|
|
99
|
+
| `deployforge security` | Run security preflight scan |
|
|
100
|
+
| `deployforge doctor` | Diagnose environment and credentials |
|
|
101
|
+
| `deployforge config` | Show resolved configuration |
|
|
102
|
+
| `deployforge logs` | Show recent deployment history |
|
|
103
|
+
| `deployforge version` | Print version |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Options
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
--dry-run Show what DeployForge would do without making changes
|
|
111
|
+
--non-interactive Skip confirmations (CI-friendly)
|
|
112
|
+
--skip-security Disable the security preflight gate
|
|
113
|
+
--debug Show debug detail for troubleshooting
|
|
114
|
+
--timeout INT Deployment wait timeout in seconds (default: 900)
|
|
115
|
+
--plan TEXT Render plan: free | starter | pro (default: starter)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## How It Works with PushForge
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
+-------------------+ +-------------------+ +-------------------+
|
|
124
|
+
| Your Machine | | GitHub | | Production |
|
|
125
|
+
| | | | | |
|
|
126
|
+
| PushForge ───────┼────►│ Source Code │ | Vercel (frontend)│
|
|
127
|
+
| Local → GitHub │ | │ | Render (backend) │
|
|
128
|
+
| | │ DeployForge ─────┼────►│ Live URLs │
|
|
129
|
+
+-------------------+ | GitHub → Prod | +-------------------+
|
|
130
|
+
+-------------------+
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**You don't need PushForge installed** to use DeployForge — it detects Git repositories automatically. But the two tools are designed to work together seamlessly.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Authentication
|
|
138
|
+
|
|
139
|
+
DeployForge reads provider tokens from environment variables (in order):
|
|
140
|
+
|
|
141
|
+
| Provider | Environment Variables |
|
|
142
|
+
|----------|----------------------|
|
|
143
|
+
| Vercel | `DEPLOYFORGE_VERCEL_TOKEN`, `VERCEL_TOKEN` |
|
|
144
|
+
| Render | `DEPLOYFORGE_RENDER_API_KEY`, `RENDER_API_KEY` |
|
|
145
|
+
|
|
146
|
+
Tokens can also be stored securely in your OS keyring via:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
deployforge config --set vercel_token
|
|
150
|
+
deployforge config --set render_api_key
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
> Credentials are **never** stored in project files or printed in logs. Only SHA-256 fingerprints are recorded.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
DeployForge creates `.deployforge/config.yml` when you run `deployforge init`:
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
frontend:
|
|
163
|
+
provider: vercel
|
|
164
|
+
directory: frontend
|
|
165
|
+
|
|
166
|
+
backend:
|
|
167
|
+
provider: render
|
|
168
|
+
runtime: python
|
|
169
|
+
directory: backend
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Environment variables from `.env` files are automatically wired:
|
|
173
|
+
- `NEXT_PUBLIC_API_URL` → set to the backend Render URL
|
|
174
|
+
- `FRONTEND_URL` → set to the frontend Vercel URL (on the backend)
|
|
175
|
+
- CORS is configured automatically
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Security
|
|
180
|
+
|
|
181
|
+
DeployForge runs a security preflight before deployment:
|
|
182
|
+
- Scans for API keys, tokens, passwords in tracked files
|
|
183
|
+
- Blocks deployment if high-entropy strings or credential patterns are found (configurable)
|
|
184
|
+
- Never transmits or stores secrets — only fingerprints them locally
|
|
185
|
+
|
|
186
|
+
Disable with `--skip-security` for trusted codebases.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Development
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Clone and set up
|
|
194
|
+
git clone https://github.com/aasz253/DeployForge.git
|
|
195
|
+
cd DeployForge
|
|
196
|
+
python -m venv .venv && source .venv/bin/activate
|
|
197
|
+
pip install -e ".[dev]"
|
|
198
|
+
|
|
199
|
+
# Run tests
|
|
200
|
+
pytest
|
|
201
|
+
|
|
202
|
+
# Lint & type-check
|
|
203
|
+
ruff check src tests
|
|
204
|
+
mypy src
|
|
205
|
+
|
|
206
|
+
# Format
|
|
207
|
+
ruff format src tests
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT — see [LICENSE](LICENSE).
|
|
215
|
+
|
|
216
|
+
## Security Policy
|
|
217
|
+
|
|
218
|
+
See [SECURITY.md](SECURITY.md) for credential handling and disclosure.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
deployforge/__init__.py,sha256=UleEE3Slnrtj03JlPKTgi5L4JStQK6475F8KmLtDXJ0,95
|
|
2
|
+
deployforge/__version__.py,sha256=RMGMLDY_b6QXh8Ibh7InaQt9BEYanxRRitNsAAuI1yA,62
|
|
3
|
+
deployforge/cli.py,sha256=9CRr6tYSXi_LPbmU5OzzWIkKzlgc1QMs7BRctVNiQJU,28702
|
|
4
|
+
deployforge/config.py,sha256=jjjBdBIkerYWoNCPR7JdYpLhKOeHCamCF3f7wdKNrYQ,6190
|
|
5
|
+
deployforge/analyzer/__init__.py,sha256=LW7FdviG53Os_zn_pOXQveA9mi68pNHAKUObkyT5NGg,604
|
|
6
|
+
deployforge/analyzer/backend.py,sha256=gdoxF-aag6t5-_YIe4Zz5n0GzZB2uKbSRBLSmAK8BbY,10218
|
|
7
|
+
deployforge/analyzer/database.py,sha256=BMBAypd9src4GJIWFno_l8AqzA32rjS9bk8Ob88ciDo,3520
|
|
8
|
+
deployforge/analyzer/frontend.py,sha256=MnQJd6riva84eHD7TtFz2qxB1ZG4xhkrGqwwqmCllS4,5386
|
|
9
|
+
deployforge/analyzer/project.py,sha256=KXTRs2kpIEo6zjRR7o5MJj9fOVnYUwRqklkd2owa1IE,12691
|
|
10
|
+
deployforge/analyzer/shared.py,sha256=UR3QRaLHaG7u4aOlLPhZ2zoSqJCRcRXa-G33DGvzC9w,3112
|
|
11
|
+
deployforge/deployment/__init__.py,sha256=YVK69Qwig_gO6gzfEm4wYNFXgVhzUR9T1N06ppaGjhI,464
|
|
12
|
+
deployforge/deployment/orchestrator.py,sha256=vM88ON8546nTQ-v32Z27Ub6yxaWpfyPLd2YkqpWR5U8,11184
|
|
13
|
+
deployforge/deployment/planner.py,sha256=_yXHzdDRGMUDZCU9EwHhjxlraorD3Qf8A2KodNb7bbk,4997
|
|
14
|
+
deployforge/deployment/verifier.py,sha256=TaO0CoA6c8NfuILa4_zF0Cnv6Nc7s59cCfOe9ynv9-c,1994
|
|
15
|
+
deployforge/errors/__init__.py,sha256=0xXqOIJGBxDmfOtXXIeNclJT9UvRepndS4OWXbCzeIU,1511
|
|
16
|
+
deployforge/github/__init__.py,sha256=nkMppkcIUTrFyuTv5OreVQ3yxMhHq3fAcMosRqlqoaA,378
|
|
17
|
+
deployforge/github/integration.py,sha256=j3r_7b9WxSMdPhQEBF1t8LlOGIk8sx21ICoU2Flq_3A,3716
|
|
18
|
+
deployforge/integration/__init__.py,sha256=MwFZhxry-tOlQot7h3a_qgJameyp_N9dCA7qlPdeXrg,494
|
|
19
|
+
deployforge/integration/cors.py,sha256=oPrbTHO0EP5ak7nJ2Ff3aVuiihRklEfGEZpPw0qijGU,1072
|
|
20
|
+
deployforge/integration/environment.py,sha256=SgMdqoIYcKITtCQTGxdWmrQ9NyIz6FaA0fsdnWs8ZgQ,1734
|
|
21
|
+
deployforge/integration/frontend_backend.py,sha256=bqLfwS--uJboU-0d9LlY12itZs9LqGF1Rqr0OQSMmQ0,1359
|
|
22
|
+
deployforge/providers/__init__.py,sha256=4N_SGEdWnKvnKcYZMEif0zN4PQz53nvGXShSq0o9lAQ,728
|
|
23
|
+
deployforge/providers/base.py,sha256=cXcWyA4YzGLLyOlevXXtZ6gMFH6QDgpcqac1LYOw7qM,4701
|
|
24
|
+
deployforge/providers/render.py,sha256=PAeBD_uk98keyXevU7RXc8E5Z7fr0b1F95QP_85CflU,8121
|
|
25
|
+
deployforge/providers/vercel.py,sha256=R5Swt9M0bsfvt_o7YB-UF2LQsnwzZcZK5giKefvg0ZU,7140
|
|
26
|
+
deployforge/security/__init__.py,sha256=AlJDJeGzbDELlx9yoPsFcFmoUT677OiWpeapNnbNgWU,204
|
|
27
|
+
deployforge/security/gitignore.py,sha256=Ys4ea-kMF75CTMwx8r2aclK7aypcqu5KsNdZ-JeFgeg,1015
|
|
28
|
+
deployforge/security/scanner.py,sha256=358i3yoG1ubsRblqlozqxUBjYfhmkPdExpM2u4kMup0,3949
|
|
29
|
+
deployforge/security/secrets.py,sha256=YKmp0cV5zPSOAg9SNNJGHJbwXJm4fxj_JzUeEIwvuoY,3483
|
|
30
|
+
deployforge/ui/__init__.py,sha256=hrMVtuRKykMiVJ9zgF0Xa9qD6IxAEDKkAa0SyW_aN-o,57
|
|
31
|
+
deployforge/ui/terminal.py,sha256=QMxLlLX8lsYNFXz9FQ8rKkD_4m0bfvLzaIkpYuAC3rU,4212
|
|
32
|
+
deployforge-0.1.0.dist-info/licenses/LICENSE,sha256=dbm8YZQMr1BMBNJFL46Ori447BiqXWmfxkwLxb5jT8w,1068
|
|
33
|
+
deployforge-0.1.0.dist-info/METADATA,sha256=nZmPVrU7ofZY4PvTUUEBmjP-_3W36I1yVV23dHW72gk,6814
|
|
34
|
+
deployforge-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
35
|
+
deployforge-0.1.0.dist-info/entry_points.txt,sha256=GkQxyLtktbLJG85EMMV_nQSeba_G57HtoysUfs5-m5I,54
|
|
36
|
+
deployforge-0.1.0.dist-info/top_level.txt,sha256=6kmVDrN44FwZopoBzn1x1KhftUMTOFlJFvg0xCJBdUI,12
|
|
37
|
+
deployforge-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 sifuna codex
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
deployforge
|