deployforge 0.1.0__tar.gz → 0.1.2__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.
Files changed (50) hide show
  1. {deployforge-0.1.0/src/deployforge.egg-info → deployforge-0.1.2}/PKG-INFO +1 -1
  2. {deployforge-0.1.0 → deployforge-0.1.2}/pyproject.toml +1 -1
  3. deployforge-0.1.2/src/deployforge/__init__.py +5 -0
  4. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/__version__.py +1 -1
  5. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/analyzer/project.py +5 -1
  6. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/providers/vercel.py +77 -20
  7. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/ui/terminal.py +39 -26
  8. {deployforge-0.1.0 → deployforge-0.1.2/src/deployforge.egg-info}/PKG-INFO +1 -1
  9. {deployforge-0.1.0 → deployforge-0.1.2}/tests/test_cli.py +3 -1
  10. {deployforge-0.1.0 → deployforge-0.1.2}/tests/test_providers.py +24 -8
  11. deployforge-0.1.0/src/deployforge/__init__.py +0 -3
  12. {deployforge-0.1.0 → deployforge-0.1.2}/LICENSE +0 -0
  13. {deployforge-0.1.0 → deployforge-0.1.2}/README.md +0 -0
  14. {deployforge-0.1.0 → deployforge-0.1.2}/setup.cfg +0 -0
  15. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/analyzer/__init__.py +0 -0
  16. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/analyzer/backend.py +0 -0
  17. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/analyzer/database.py +0 -0
  18. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/analyzer/frontend.py +0 -0
  19. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/analyzer/shared.py +0 -0
  20. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/cli.py +0 -0
  21. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/config.py +0 -0
  22. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/deployment/__init__.py +0 -0
  23. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/deployment/orchestrator.py +0 -0
  24. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/deployment/planner.py +0 -0
  25. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/deployment/verifier.py +0 -0
  26. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/errors/__init__.py +0 -0
  27. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/github/__init__.py +0 -0
  28. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/github/integration.py +0 -0
  29. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/integration/__init__.py +0 -0
  30. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/integration/cors.py +0 -0
  31. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/integration/environment.py +0 -0
  32. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/integration/frontend_backend.py +0 -0
  33. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/providers/__init__.py +0 -0
  34. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/providers/base.py +0 -0
  35. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/providers/render.py +0 -0
  36. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/security/__init__.py +0 -0
  37. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/security/gitignore.py +0 -0
  38. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/security/scanner.py +0 -0
  39. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/security/secrets.py +0 -0
  40. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge/ui/__init__.py +0 -0
  41. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge.egg-info/SOURCES.txt +0 -0
  42. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge.egg-info/dependency_links.txt +0 -0
  43. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge.egg-info/entry_points.txt +0 -0
  44. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge.egg-info/requires.txt +0 -0
  45. {deployforge-0.1.0 → deployforge-0.1.2}/src/deployforge.egg-info/top_level.txt +0 -0
  46. {deployforge-0.1.0 → deployforge-0.1.2}/tests/test_analyzer.py +0 -0
  47. {deployforge-0.1.0 → deployforge-0.1.2}/tests/test_config.py +0 -0
  48. {deployforge-0.1.0 → deployforge-0.1.2}/tests/test_integration.py +0 -0
  49. {deployforge-0.1.0 → deployforge-0.1.2}/tests/test_planner.py +0 -0
  50. {deployforge-0.1.0 → deployforge-0.1.2}/tests/test_security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deployforge
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: From GitHub to a Live Application. Automatically. DeployForge analyzes an application and deploys its frontend and backend to production.
5
5
  Author-email: sifuna codex <www.antonysifuna07@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deployforge"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "From GitHub to a Live Application. Automatically. DeployForge analyzes an application and deploys its frontend and backend to production."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,5 @@
1
+ """DeployForge — from GitHub to a Live Application. Automatically."""
2
+
3
+ from deployforge.__version__ import __version__
4
+
5
+ __all__ = ["__version__"]
@@ -1,3 +1,3 @@
1
1
  """Package version for DeployForge."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -376,9 +376,13 @@ def analyze_project(root: Path, config: ProjectConfig | None = None) -> ProjectA
376
376
  if not has_git:
377
377
  repo = RepoInfo(exists=False, branch=current_branch(root) or "main")
378
378
 
379
+ project_name = suggest_name(root)
380
+ if (repo.exists or has_git) and repo.name:
381
+ slug = re.sub(r"[^a-zA-Z0-9\-_.]+", "-", repo.name).strip("-._").lower()
382
+ project_name = slug or project_name
379
383
  analysis = ProjectAnalysis(
380
384
  root=root,
381
- name=suggest_name(root),
385
+ name=project_name,
382
386
  frontends=frontends,
383
387
  backends=backends,
384
388
  database=detect_database(root),
@@ -81,7 +81,6 @@ class VercelProvider(DeploymentProvider):
81
81
  payload: dict[str, Any] = {
82
82
  "name": name,
83
83
  "gitRepository": {"type": "github", "repo": repo},
84
- "public": not private,
85
84
  }
86
85
  preset = FRAMEWORK_PRESETS.get(framework or "", framework)
87
86
  if preset:
@@ -89,25 +88,58 @@ class VercelProvider(DeploymentProvider):
89
88
  if directory and directory not in (".", ""):
90
89
  payload["rootDirectory"] = directory
91
90
 
92
- data = self.http.request(
93
- "POST",
94
- f"{API_BASE}/v10/projects",
95
- headers=self._headers(),
96
- json=payload,
97
- )
91
+ try:
92
+ data = self.http.request(
93
+ "POST",
94
+ f"{API_BASE}/v10/projects",
95
+ headers=self._headers(),
96
+ json=payload,
97
+ )
98
+ except ProviderError as exc:
99
+ if exc.status_code == 409:
100
+ existing = self._fetch_project(name)
101
+ if existing and existing.get("id"):
102
+ return self._from_project(existing, name, framework)
103
+ raise
104
+ raise
98
105
  if not isinstance(data, dict):
99
106
  msg = "Vercel returned an unexpected response while creating the project."
100
107
  raise ProviderError(msg)
101
- project_id = str(data.get("id") or "")
102
- project_name = str(data.get("name") or name)
103
- if not project_id:
108
+ if not data.get("id"):
104
109
  raise ProviderError("Vercel did not return a project id.")
110
+ return self._from_project(data, name, framework)
111
+
112
+ def _fetch_project(self, name: str) -> dict[str, Any] | None:
113
+ """Read an existing project so a repeat run can reuse it."""
114
+ try:
115
+ data = self.http.request(
116
+ "GET",
117
+ f"{API_BASE}/v9/projects/{name}",
118
+ headers=self._headers(),
119
+ )
120
+ except ProviderError as exc:
121
+ if exc.status_code == 404:
122
+ return None
123
+ raise
124
+ return data if isinstance(data, dict) else None
125
+
126
+ def _from_project(
127
+ self, data: dict[str, Any], fallback_name: str, framework: str | None = None
128
+ ) -> DeploymentResult:
129
+ project_id = str(data.get("id") or "")
130
+ project_name = str(data.get("name") or fallback_name)
131
+ repo_id: int | None = None
132
+ link = data.get("link") or {}
133
+ if isinstance(link, dict):
134
+ rid = link.get("repoId")
135
+ if rid is not None:
136
+ repo_id = int(rid)
105
137
  return DeploymentResult(
106
138
  provider="vercel",
107
139
  project_name=project_name,
108
140
  service_id=project_id,
109
141
  status="created",
110
- extra={"framework": framework},
142
+ extra={"framework": framework, "repo_id": repo_id},
111
143
  )
112
144
 
113
145
  def set_env(self, project: DeploymentResult, key: str, value: str) -> None:
@@ -125,15 +157,25 @@ class VercelProvider(DeploymentProvider):
125
157
  )
126
158
 
127
159
  def deploy(self, project: DeploymentResult, *, ref: str | None = None) -> DeploymentResult:
160
+ git_source: dict[str, Any] = {"type": "github", "ref": ref or "main"}
161
+ repo_id = (project.extra or {}).get("repo_id")
162
+ if repo_id is not None:
163
+ git_source["repoId"] = repo_id
128
164
  payload: dict[str, Any] = {
129
165
  "name": project.project_name,
130
166
  "target": "production",
131
- "gitSource": {"type": "github", "ref": ref or "main"},
167
+ "gitSource": git_source,
168
+ "projectSettings": {
169
+ "framework": FRAMEWORK_PRESETS.get(
170
+ (project.extra or {}).get("framework", ""),
171
+ (project.extra or {}).get("framework"),
172
+ )
173
+ },
132
174
  }
133
175
  try:
134
176
  data = self.http.request(
135
177
  "POST",
136
- f"{API_BASE}/v13/deployments",
178
+ f"{API_BASE}/v13/deployments?skipAutoDetectionConfirmation=1",
137
179
  headers=self._headers(),
138
180
  json=payload,
139
181
  )
@@ -152,12 +194,31 @@ class VercelProvider(DeploymentProvider):
152
194
  extra=project.extra,
153
195
  )
154
196
 
155
- def get_status(self, project: DeploymentResult) -> DeployStatus:
197
+ def _fetch_deployment(self, project: DeploymentResult) -> dict[str, Any] | None:
198
+ service_id = project.service_id
199
+ if service_id.startswith("prj_"):
200
+ data = self.http.request(
201
+ "GET",
202
+ f"{API_BASE}/v6/deployments",
203
+ headers=self._headers(),
204
+ params={"projectId": service_id, "limit": 1},
205
+ )
206
+ deployments = data.get("deployments") if isinstance(data, dict) else None
207
+ if not isinstance(deployments, list) or not deployments:
208
+ return None
209
+ first = deployments[0]
210
+ return first if isinstance(first, dict) else None
156
211
  data = self.http.request(
157
212
  "GET",
158
- f"{API_BASE}/v13/deployments/{project.service_id}",
213
+ f"{API_BASE}/v13/deployments/{service_id}",
159
214
  headers=self._headers(),
160
215
  )
216
+ if not isinstance(data, dict):
217
+ raise ProviderError("Vercel returned an unexpected deployment status.")
218
+ return data
219
+
220
+ def get_status(self, project: DeploymentResult) -> DeployStatus:
221
+ data = self._fetch_deployment(project)
161
222
  if not isinstance(data, dict):
162
223
  raise ProviderError("Vercel returned an unexpected deployment status.")
163
224
  state = str(data.get("readyState") or data.get("state") or "QUEUED")
@@ -188,11 +249,7 @@ class VercelProvider(DeploymentProvider):
188
249
  return status.url
189
250
 
190
251
  def get_logs(self, project: DeploymentResult) -> list[str]:
191
- data = self.http.request(
192
- "GET",
193
- f"{API_BASE}/v13/deployments/{project.service_id}",
194
- headers=self._headers(),
195
- )
252
+ data = self._fetch_deployment(project)
196
253
  if not isinstance(data, dict):
197
254
  return []
198
255
  logs: list[str] = []
@@ -19,10 +19,17 @@ from rich.text import Text
19
19
 
20
20
  console = Console()
21
21
 
22
- TAGLINE = "GitHub Production Automation"
23
- CREDIT = "built by sifuna codex"
22
+ BANNER = r""" ____ _____ ____ _ _____ _______ ___ ____ ____ _____
23
+ | _ \| ____| _ \| | / _ \ \ / / ___/ _ \| _ \ / ___| ____|
24
+ | | | | _| | |_) | | | | | \ V /| |_ | | | | |_) | | _| _|
25
+ | |_| | |___| __/| |__| |_| || | | _|| |_| | _ <| |_| | |___
26
+ |____/|_____|_| |_____\___/ |_| |_| \___/|_| \_\____|_____|"""
27
+
28
+ ART_COLOR = "bold cornflower_blue"
29
+ SWEEP_COLOR = "bold cyan"
24
30
 
25
- HEADER = "DEPLOYFORGE"
31
+ TAGLINE = "GitHub → Production Automation"
32
+ CREDIT = "built by sifana codex"
26
33
 
27
34
 
28
35
  def _animation_enabled() -> bool:
@@ -32,25 +39,13 @@ def _animation_enabled() -> bool:
32
39
  return bool(console.is_terminal) and not os.environ.get("CI")
33
40
 
34
41
 
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
42
+ def _banner_lines() -> list[str]:
43
+ return [line.rstrip() for line in BANNER.splitlines() if line.strip()]
48
44
 
49
45
 
50
- def _render_banner_text(header: Text) -> Text:
46
+ def _banner_text(art: str, style: str) -> Text:
51
47
  text = Text()
52
- text.append("\n")
53
- text.append_text(header)
48
+ text.append(art, style=style)
54
49
  text.append("\n\n")
55
50
  text.append(TAGLINE, style="bold cyan")
56
51
  text.append("\n")
@@ -58,9 +53,27 @@ def _render_banner_text(header: Text) -> Text:
58
53
  return text
59
54
 
60
55
 
56
+ def _sweep_frames() -> Iterator[Text]:
57
+ """Sweep a light across the DEPLOYFORGE wordmark, left to right."""
58
+ lines = _banner_lines()
59
+ maxlen = max(len(line) for line in lines)
60
+ for width in range(0, maxlen + 2):
61
+ frame = Text()
62
+ for line in lines:
63
+ frame.append(line[:width], style=SWEEP_COLOR)
64
+ if width < len(line):
65
+ frame.append(line[width:], style="dim")
66
+ frame.append("\n")
67
+ frame.append("\n")
68
+ frame.append(TAGLINE, style="bold cyan")
69
+ frame.append("\n")
70
+ frame.append(CREDIT, style="italic dim")
71
+ yield frame
72
+
73
+
61
74
  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)))
75
+ art = "\n".join(_banner_lines())
76
+ console.print(Panel.fit(_banner_text(art, ART_COLOR), border_style="cyan", padding=(1, 2)))
64
77
 
65
78
 
66
79
  def banner() -> None:
@@ -68,11 +81,11 @@ def banner() -> None:
68
81
  _static_banner()
69
82
  return
70
83
  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)
84
+ with Live(console=console, refresh_per_second=30, transient=True) as live:
85
+ for frame in _sweep_frames():
86
+ live.update(Panel.fit(frame, border_style="cyan", padding=(1, 2)))
87
+ time.sleep(0.018)
88
+ time.sleep(0.18)
76
89
  except (OSError, KeyboardInterrupt):
77
90
  pass
78
91
  console.print()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deployforge
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: From GitHub to a Live Application. Automatically. DeployForge analyzes an application and deploys its frontend and backend to production.
5
5
  Author-email: sifuna codex <www.antonysifuna07@gmail.com>
6
6
  License: MIT
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
6
 
7
+ import pytest
7
8
  from typer.testing import CliRunner
8
9
 
9
10
  from deployforge.cli import app
@@ -103,7 +104,8 @@ def test_status_empty(tmp_path: Path) -> None:
103
104
  assert "STATUS" in result.output
104
105
 
105
106
 
106
- def test_verify_no_urls(tmp_path: Path) -> None:
107
+ def test_verify_no_urls(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
108
+ monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data"))
107
109
  result = runner.invoke(app, ["verify"])
108
110
  assert result.exit_code == 0
109
111
  assert "No deployed URLs" in result.output or "running" in result.output.lower()
@@ -89,11 +89,19 @@ class TestVercelProvider:
89
89
  def test_deploy(self) -> None:
90
90
  http = FakeHTTP(
91
91
  {
92
- "https://api.vercel.com/v13/deployments": {"id": "dpl_1", "readyState": "BUILDING"},
92
+ "https://api.vercel.com/v13/deployments?skipAutoDetectionConfirmation=1": {
93
+ "id": "dpl_1",
94
+ "readyState": "BUILDING",
95
+ },
93
96
  }
94
97
  )
95
98
  provider = VercelProvider(token="tok", client=http)
96
- project = DeploymentResult(provider="vercel", project_name="p", service_id="prj_1")
99
+ project = DeploymentResult(
100
+ provider="vercel",
101
+ project_name="p",
102
+ service_id="prj_1",
103
+ extra={"repo_id": 123},
104
+ )
97
105
  deployed = provider.deploy(project, ref="main")
98
106
  assert deployed.service_id == "dpl_1"
99
107
  assert deployed.status == "building"
@@ -101,9 +109,13 @@ class TestVercelProvider:
101
109
  def test_get_status_ready(self) -> None:
102
110
  http = FakeHTTP(
103
111
  {
104
- "https://api.vercel.com/v13/deployments/prj_1": {
105
- "readyState": "READY",
106
- "alias": ["my-app.vercel.app"],
112
+ "https://api.vercel.com/v6/deployments": {
113
+ "deployments": [
114
+ {
115
+ "readyState": "READY",
116
+ "alias": ["my-app.vercel.app"],
117
+ }
118
+ ]
107
119
  },
108
120
  }
109
121
  )
@@ -116,9 +128,13 @@ class TestVercelProvider:
116
128
  def test_get_status_error(self) -> None:
117
129
  http = FakeHTTP(
118
130
  {
119
- "https://api.vercel.com/v13/deployments/prj_1": {
120
- "readyState": "ERROR",
121
- "errorMessage": "Build failed",
131
+ "https://api.vercel.com/v6/deployments": {
132
+ "deployments": [
133
+ {
134
+ "readyState": "ERROR",
135
+ "errorMessage": "Build failed",
136
+ }
137
+ ]
122
138
  },
123
139
  }
124
140
  )
@@ -1,3 +0,0 @@
1
- """DeployForge — from GitHub to a Live Application. Automatically."""
2
-
3
- __version__ = "0.1.0"
File without changes
File without changes
File without changes