1- import json
21import logging
32import re
43import subprocess
1413
1514logger = logging .getLogger (__name__ )
1615
17- TOKEN_NAME_PREFIX = "GitHub Actions"
1816TOKEN_EXPIRES_DAYS = 365
1917DEFAULT_WORKFLOW_PATH = Path (".github/workflows/deploy.yml" )
2018
2119
22- def _get_origin () -> str :
23- try :
24- result = subprocess .run (
25- ["git" , "config" , "--get" , "remote.origin.url" ],
26- capture_output = True ,
27- text = True ,
28- check = True ,
29- )
30- return result .stdout .strip ()
31- except subprocess .CalledProcessError :
32- logger .error (
33- "Error retrieving git remote origin URL. Make sure you're in a git repository with a remote origin set."
34- )
35- raise typer .Exit (1 ) from None
36-
37-
3820def _repo_slug_from_origin (origin : str ) -> str | None :
3921 """Extract 'owner/repo' from a GitHub remote URL."""
22+ # Handles URLs like: git@github.com:owner/repo.git or https://github.com/owner/repo.git
4023 match = re .search (r"github\.com[:/](.+?)(?:\.git)?$" , origin )
4124 return match .group (1 ) if match else None
4225
4326
4427def _check_gh_cli_installed () -> bool :
28+ """Check if the GitHub CLI (gh) is installed and available."""
4529 try :
4630 subprocess .run (["gh" , "--version" ], capture_output = True , text = True , check = True )
4731 return True
4832 except (subprocess .CalledProcessError , FileNotFoundError ):
4933 return False
5034
5135
52- def _token_name (repo_slug : str ) -> str :
53- return f"{ TOKEN_NAME_PREFIX } — { repo_slug } "
36+ def _get_remote_origin () -> str :
37+ """Get the remote origin URL of the Git repository."""
38+ result = subprocess .run (
39+ ["git" , "config" , "--get" , "remote.origin.url" ],
40+ capture_output = True ,
41+ text = True ,
42+ check = True ,
43+ )
44+ return result .stdout .strip ()
5445
5546
56- def _find_existing_token (client : APIClient , app_id : str , token_name : str ) -> str | None :
57- """Return the token ID if a token with the given name already exists."""
58- response = client .get (f"/apps/{ app_id } /tokens" )
59- response .raise_for_status ()
60- for token in response .json ()["data" ]:
61- if token ["name" ] == token_name :
62- return str (token ["id" ])
63- return None
47+ def _set_github_secret (name : str , value : str ) -> None :
48+ """Set a GitHub Actions secret via the gh CLI."""
49+ subprocess .run (
50+ ["gh" , "secret" , "set" , name , "--body" , value ],
51+ capture_output = True ,
52+ check = True ,
53+ )
6454
6555
6656def _create_or_regenerate_token (
@@ -71,7 +61,14 @@ def _create_or_regenerate_token(
7161 Returns (token_data, regenerated).
7262 """
7363 with APIClient () as client :
74- existing_id = _find_existing_token (client , app_id , token_name )
64+ existing_id = None
65+
66+ response = client .get (f"/apps/{ app_id } /tokens" )
67+ response .raise_for_status ()
68+ for token in response .json ()["data" ]:
69+ if token ["name" ] == token_name :
70+ existing_id = token ["id" ]
71+ break
7572
7673 if existing_id :
7774 response = client .post (
@@ -93,36 +90,20 @@ def _create_or_regenerate_token(
9390
9491
9592def _get_default_branch () -> str :
96- if not _check_gh_cli_installed ():
97- return "main"
93+ """Get the default branch of the Git repository."""
9894 try :
9995 result = subprocess .run (
100- ["gh " , "repo" , "view" , "--json " , "defaultBranchRef " ],
96+ ["git " , "symbolic-ref " , "refs/remotes/origin/HEAD " ],
10197 capture_output = True ,
10298 text = True ,
10399 check = True ,
104100 )
105-
106- repo_info = json .loads (result .stdout )
107- return str (repo_info ["defaultBranchRef" ]["name" ])
108- except (subprocess .CalledProcessError , KeyError , json .JSONDecodeError ):
101+ return result .stdout .strip ().split ("/" )[- 1 ]
102+ except subprocess .CalledProcessError :
109103 return "main"
110104
111105
112- def _set_github_secret (secret_name : str , secret_value : str ) -> None :
113- try :
114- subprocess .run (
115- ["gh" , "secret" , "set" , secret_name , "--body" , secret_value ],
116- capture_output = True ,
117- check = True ,
118- )
119- except (subprocess .CalledProcessError , FileNotFoundError ) as e :
120- logger .error (f"Error setting GitHub secret: { e } " )
121-
122-
123- def _write_workflow_file (
124- branch : str , workflow_path : Path = DEFAULT_WORKFLOW_PATH
125- ) -> str :
106+ def _write_workflow_file (branch : str , workflow_path : Path ) -> None :
126107 workflow_content = f"""name: Deploy to FastAPI Cloud
127108on:
128109 push:
@@ -140,7 +121,6 @@ def _write_workflow_file(
140121"""
141122 workflow_path .parent .mkdir (parents = True , exist_ok = True )
142123 workflow_path .write_text (workflow_content )
143- return str (workflow_path )
144124
145125
146126def setup_ci (
@@ -150,12 +130,11 @@ def setup_ci(
150130 help = "Path to the folder containing the app (defaults to current directory)"
151131 ),
152132 ] = None ,
153- branch : str = typer .Option (
154- "main" ,
133+ branch : str | None = typer .Option (
134+ None ,
155135 "--branch" ,
156136 "-b" ,
157- help = "Branch that triggers deploys" ,
158- show_default = True ,
137+ help = "Branch that triggers deploys (defaults to the repo's default branch)" ,
159138 ),
160139 secrets_only : bool = typer .Option (
161140 False ,
@@ -208,7 +187,15 @@ def setup_ci(
208187 )
209188 raise typer .Exit (1 )
210189
211- origin = _get_origin ()
190+ try :
191+ origin = _get_remote_origin ()
192+ except subprocess .CalledProcessError :
193+ toolkit .print (
194+ "Error retrieving git remote origin URL. Make sure you're in a git repository with a remote origin set." ,
195+ tag = "error" ,
196+ )
197+ raise typer .Exit (1 ) from None
198+
212199 if "github.com" not in origin :
213200 toolkit .print (
214201 "Remote origin is not a GitHub repository. Please set up a GitHub repo and add it as the remote origin." ,
@@ -217,12 +204,11 @@ def setup_ci(
217204 raise typer .Exit (1 )
218205
219206 repo_slug = _repo_slug_from_origin (origin ) or origin
207+ has_gh = _check_gh_cli_installed ()
220208
221- default_branch = _get_default_branch ()
222- if branch == "main" and default_branch != "main" :
223- branch = default_branch
209+ if not branch :
210+ branch = _get_default_branch ()
224211
225- # -- header --
226212 if dry_run :
227213 toolkit .print (
228214 "[yellow]This is a dry run — no changes will be made[/yellow]"
@@ -252,8 +238,7 @@ def setup_ci(
252238 toolkit .print (msg_workflow )
253239 return
254240
255- # -- create deploy token --
256- token_name = _token_name (repo_slug )
241+ token_name = f"GitHub Actions — { repo_slug } "
257242 toolkit .print ("Generating deploy token..." )
258243 toolkit .print_line ()
259244 with (
@@ -269,14 +254,16 @@ def setup_ci(
269254
270255 toolkit .print_line ()
271256
272- # -- set github secrets --
273- has_gh = _check_gh_cli_installed ()
274257 if has_gh :
275258 toolkit .print (f"Setting repo secrets on [bold]{ repo_slug } [/bold]" )
276259 toolkit .print_line ()
277260 with toolkit .progress (title = "Setting repo secrets..." ) as progress :
278- _set_github_secret ("FASTAPI_CLOUD_TOKEN" , token_data ["value" ])
279- _set_github_secret ("FASTAPI_CLOUD_APP_ID" , app_config .app_id )
261+ try :
262+ _set_github_secret ("FASTAPI_CLOUD_TOKEN" , token_data ["value" ])
263+ _set_github_secret ("FASTAPI_CLOUD_APP_ID" , app_config .app_id )
264+ except (subprocess .CalledProcessError , FileNotFoundError ):
265+ progress .set_error ("Failed to set GitHub secrets via gh CLI." )
266+ raise typer .Exit (1 ) from None
280267 progress .log (msg_secrets )
281268 else :
282269 secrets_url = f"https://github.com/{ repo_slug } /settings/secrets/actions"
@@ -292,33 +279,32 @@ def setup_ci(
292279
293280 toolkit .print_line ()
294281
295- # -- write workflow file --
296282 if not secrets_only :
297283 if file :
298284 workflow_path = Path (f".github/workflows/{ file } " )
299285 else :
300286 workflow_path = DEFAULT_WORKFLOW_PATH
301287
288+ write_workflow = True
302289 if not file and workflow_path .exists ():
303290 overwrite = toolkit .confirm (
304291 f"Workflow file [bold]{ workflow_path } [/bold] already exists. Overwrite?" ,
305292 tag = "workflow" ,
306293 default = False ,
307294 )
308295 if not overwrite :
309- new_name = typer .prompt (
310- "Enter a new filename (or press Enter to skip)" ,
311- default = "" ,
312- show_default = False ,
313- )
296+ new_name = toolkit .input (
297+ "Enter a new filename (without path) or leave blank to skip writing the workflow file:" ,
298+ tag = "workflow" ,
299+ ).strip ()
314300 if new_name :
315301 workflow_path = Path (f".github/workflows/{ new_name } " )
316302 else :
317303 toolkit .print ("Skipped writing workflow file." )
318304 toolkit .print_line ()
319- workflow_path = None # type: ignore[assignment]
305+ write_workflow = False
320306 toolkit .print_line ()
321- if workflow_path is not None :
307+ if write_workflow :
322308 msg_workflow = f"Wrote [bold]{ workflow_path } [/bold] (branch: { branch } )"
323309 with toolkit .progress (title = "Writing workflow file..." ) as progress :
324310 _write_workflow_file (branch , workflow_path )
0 commit comments