clue2app-cli 0.2.7__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.
@@ -0,0 +1,215 @@
1
+ """Pre-flight check that catches common "app will build but never start"
2
+ mistakes before c2a creates an app on the platform.
3
+
4
+ Background: Paketo buildpacks happily produce a runnable image even when
5
+ the source has no declared web entry point. The container then exits with
6
+ ExitCode 0 at launch because there's nothing to run, and the Knative
7
+ service sits in a permanent failed state. Most users won't recognize
8
+ this — they'll see "build succeeded" and assume the deploy worked.
9
+
10
+ This module shallow-clones the user's repo, inspects the file layout,
11
+ and returns a human-readable error explaining what's missing if it
12
+ spots one of the known traps. The SDK then refuses to create the app
13
+ and the user fixes their repo before re-running.
14
+
15
+ Detection is intentionally conservative — only reject when we're highly
16
+ confident something is wrong. Unknown frameworks pass through.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ import shutil
22
+ import subprocess
23
+ import tempfile
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+
27
+
28
+ # Paketo Node-Start auto-detects these (and .cjs/.mjs variants) at repo root.
29
+ _NODE_AUTO_ENTRYPOINTS = {
30
+ "server.js", "server.cjs", "server.mjs",
31
+ "app.js", "app.cjs", "app.mjs",
32
+ "main.js", "main.cjs", "main.mjs",
33
+ "index.js", "index.cjs", "index.mjs",
34
+ }
35
+
36
+ # Python frameworks Paketo CAN auto-start without a Procfile.
37
+ # Django: manage.py at root + wsgi.py somewhere.
38
+ # Generic WSGI/ASGI apps and FastAPI/Flask all need an explicit Procfile.
39
+ _PYTHON_DJANGO_MARKERS = {"manage.py"}
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class EntryPointIssue:
44
+ """A detected problem with the repo's web entry point.
45
+
46
+ `summary` is one-liner safe for log lines / table cells.
47
+ `detail` is multi-line guidance suitable for showing to the user.
48
+ """
49
+
50
+ summary: str
51
+ detail: str
52
+
53
+ def __str__(self) -> str:
54
+ return f"{self.summary}\n\n{self.detail}"
55
+
56
+
57
+ def detect_entrypoint_issue(
58
+ git_url: str,
59
+ *,
60
+ branch: str = "main",
61
+ git_username: str = "",
62
+ git_password: str = "",
63
+ ) -> EntryPointIssue | None:
64
+ """Shallow-clone `git_url` at `branch` and inspect for known traps.
65
+
66
+ Returns None if the repo looks runnable, or an `EntryPointIssue`
67
+ describing the specific gap. Network/clone failures return None so
68
+ we don't block users on transient issues — Paketo will surface the
69
+ real problem at build time as a fallback.
70
+ """
71
+ auth_url = _inject_basic_auth(git_url, git_username, git_password)
72
+
73
+ tmp = Path(tempfile.mkdtemp(prefix="c2a-entry-check-"))
74
+ try:
75
+ proc = subprocess.run(
76
+ ["git", "clone", "--depth", "1", "--branch", branch, "--quiet", auth_url, str(tmp)],
77
+ capture_output=True,
78
+ text=True,
79
+ timeout=30,
80
+ )
81
+ if proc.returncode != 0:
82
+ # Don't block on clone failures — wrong branch name, transient
83
+ # network, private repo with bad creds, etc. The platform's own
84
+ # build step will surface these clearly enough.
85
+ return None
86
+ return _inspect_repo(tmp)
87
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
88
+ return None
89
+ finally:
90
+ shutil.rmtree(tmp, ignore_errors=True)
91
+
92
+
93
+ def _inject_basic_auth(git_url: str, username: str, password: str) -> str:
94
+ """Embed credentials into an https URL so the clone doesn't prompt."""
95
+ if not password or not git_url.startswith("https://"):
96
+ return git_url
97
+ user = username or "x-oauth-basic"
98
+ return git_url.replace("https://", f"https://{user}:{password}@", 1)
99
+
100
+
101
+ def _inspect_repo(root: Path) -> EntryPointIssue | None:
102
+ """Decide whether the repo declares a working web entry point."""
103
+ has_procfile_web = _procfile_has_web_line(root / "Procfile")
104
+ files_at_root = {p.name for p in root.iterdir() if p.is_file()}
105
+
106
+ # Java: any pom.xml / build.gradle / build.gradle.kts → buildpack runs
107
+ # the produced JAR. Always fine.
108
+ if {"pom.xml", "build.gradle", "build.gradle.kts"} & files_at_root:
109
+ return None
110
+
111
+ # Go: go.mod → buildpack runs the compiled binary. Always fine.
112
+ if "go.mod" in files_at_root:
113
+ return None
114
+
115
+ # Node: needs Procfile OR one of the auto-detected entrypoints at root.
116
+ if "package.json" in files_at_root:
117
+ if has_procfile_web:
118
+ return None
119
+ if files_at_root & _NODE_AUTO_ENTRYPOINTS:
120
+ return None
121
+ return EntryPointIssue(
122
+ summary="Node repo has no Procfile and no auto-detectable entry point",
123
+ detail=(
124
+ "Paketo's Node-Start buildpack only auto-launches one of:\n"
125
+ " server.js | app.js | main.js | index.js (or .cjs / .mjs variants)\n\n"
126
+ "Your repo has package.json but none of those at the root.\n"
127
+ "Add a Procfile, e.g. for an Express app:\n\n"
128
+ " web: node dist/server.js\n\n"
129
+ "Or rename your entry file to one of the auto-detected names."
130
+ ),
131
+ )
132
+
133
+ # Python: FastAPI/Flask/generic WSGI need a Procfile. Django (manage.py)
134
+ # is auto-detected.
135
+ python_markers = {"requirements.txt", "pyproject.toml", "Pipfile"}
136
+ if files_at_root & python_markers:
137
+ if has_procfile_web:
138
+ return None
139
+ if files_at_root & _PYTHON_DJANGO_MARKERS:
140
+ return None
141
+ framework = _detect_python_framework(root, files_at_root)
142
+ return EntryPointIssue(
143
+ summary=f"{framework or 'Python'} repo has no Procfile (only Django auto-launches without one)",
144
+ detail=_python_procfile_guidance(framework),
145
+ )
146
+
147
+ # Unknown stack — let it through. Paketo will surface anything wrong.
148
+ return None
149
+
150
+
151
+ def _procfile_has_web_line(procfile_path: Path) -> bool:
152
+ if not procfile_path.is_file():
153
+ return False
154
+ try:
155
+ for raw_line in procfile_path.read_text(errors="replace").splitlines():
156
+ line = raw_line.strip()
157
+ if not line or line.startswith("#"):
158
+ continue
159
+ # Procfile format: `<process-type>: <command>`
160
+ if re.match(r"^web\s*:\s*\S", line):
161
+ return True
162
+ except OSError:
163
+ return False
164
+ return False
165
+
166
+
167
+ def _detect_python_framework(root: Path, files_at_root: set[str]) -> str | None:
168
+ """Best-effort framework detection from common requirements files."""
169
+ needles = (
170
+ ("FastAPI", "fastapi"),
171
+ ("Flask", "flask"),
172
+ )
173
+ for candidate in ("requirements.txt", "pyproject.toml", "Pipfile"):
174
+ path = root / candidate
175
+ if path.name not in files_at_root:
176
+ continue
177
+ try:
178
+ content = path.read_text(errors="replace").lower()
179
+ except OSError:
180
+ continue
181
+ for label, needle in needles:
182
+ if needle in content:
183
+ return label
184
+ return None
185
+
186
+
187
+ def _python_procfile_guidance(framework: str | None) -> str:
188
+ if framework == "FastAPI":
189
+ return (
190
+ "Paketo's Python buildpack only auto-launches Django apps.\n"
191
+ "Detected FastAPI in your requirements; add a Procfile at the\n"
192
+ "repo root with the start command, e.g.:\n\n"
193
+ " web: uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080}\n\n"
194
+ "Replace `main:app` with `<module>:<app-variable>` if your\n"
195
+ "FastAPI app lives in a different file."
196
+ )
197
+ if framework == "Flask":
198
+ return (
199
+ "Paketo's Python buildpack only auto-launches Django apps.\n"
200
+ "Detected Flask in your requirements; add a Procfile at the\n"
201
+ "repo root with the start command, e.g.:\n\n"
202
+ " web: gunicorn app:app --bind 0.0.0.0:${PORT:-8080}\n\n"
203
+ "Replace `app:app` with `<module>:<app-variable>` if your\n"
204
+ "Flask app lives in a different file."
205
+ )
206
+ return (
207
+ "Paketo's Python buildpack only auto-launches Django apps\n"
208
+ "(detected via manage.py + wsgi.py). Your repo doesn't have that\n"
209
+ "layout, so add a Procfile at the repo root with the start command.\n\n"
210
+ "Examples:\n"
211
+ " web: uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080} # FastAPI\n"
212
+ " web: gunicorn app:app --bind 0.0.0.0:${PORT:-8080} # Flask / WSGI\n"
213
+ " web: python my_server.py # generic\n\n"
214
+ "Then commit + push and re-run the create command."
215
+ )