Skip to content
Closed
10 changes: 2 additions & 8 deletions .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,10 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
# Run this after labeler applied labels
# Dummy job to satisfy branch protection rules since the strict check was removed
check-labels:
needs:
- labeler
permissions:
pull-requests: read
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: agilepathway/label-checker@c3d16ad512e7cea5961df85ff2486bb774caf3c5 # v1.6.65
with:
one_of: breaking,security,feature,bug,refactor,upgrade,docs,lang-all,internal,release
repo_token: ${{ secrets.GITHUB_TOKEN }}
- run: echo "Strict label checking is disabled."
2 changes: 2 additions & 0 deletions src/fastapi_cloud_cli/commands/apps/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def _link_app_interactively(
with toolkit.progress("Fetching teams...", transient=True) as progress:
with client.handle_http_errors(
progress,
toolkit=toolkit,
default_message="Error fetching teams. Please try again later.",
):
response = client.get("/teams/")
Expand Down Expand Up @@ -136,6 +137,7 @@ def _link_app_interactively(
with toolkit.progress("Fetching apps...", transient=True) as progress:
with client.handle_http_errors(
progress,
toolkit=toolkit,
default_message="Error fetching apps. Please try again later.",
):
response = client.get("/apps/", params={"team_id": team["id"]})
Expand Down
6 changes: 4 additions & 2 deletions src/fastapi_cloud_cli/commands/deploy/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def _configure_app(
with toolkit.progress("Fetching teams...", transient=True) as progress:
with client.handle_http_errors(
progress,
toolkit=toolkit,
default_message="Error fetching teams. Please try again later.",
):
teams = _get_teams(client)
Expand Down Expand Up @@ -57,6 +58,7 @@ def _configure_app(
with toolkit.progress("Fetching apps...", transient=True) as progress:
with client.handle_http_errors(
progress,
toolkit=toolkit,
default_message="Error fetching apps. Please try again later.",
):
apps = _get_apps(client=client, team_id=team.id)
Expand Down Expand Up @@ -133,7 +135,7 @@ def _configure_app(
if directory != selected_app.directory:
with (
toolkit.progress(title="Updating app directory...") as progress,
client.handle_http_errors(progress),
client.handle_http_errors(progress, toolkit=toolkit),
):
app = _update_app(
client=client, app_id=selected_app.id, directory=directory
Expand All @@ -144,7 +146,7 @@ def _configure_app(
app = selected_app
else:
with toolkit.progress(title="Creating app...") as progress:
with client.handle_http_errors(progress):
with client.handle_http_errors(progress, toolkit=toolkit):
app = _create_app(
client=client,
team_id=team.id,
Expand Down
4 changes: 2 additions & 2 deletions src/fastapi_cloud_cli/commands/env/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def delete(
with toolkit.progress(
"Fetching environment variables...", transient=True
) as progress:
with client.handle_http_errors(progress):
with client.handle_http_errors(progress, toolkit=toolkit):
environment_variables = _get_environment_variables(
client=client, app_id=target_app_id
)
Expand Down Expand Up @@ -169,7 +169,7 @@ def delete(
with toolkit.progress(
"Deleting environment variable", transient=True
) as progress:
with client.handle_http_errors(progress):
with client.handle_http_errors(progress, toolkit=toolkit):
deleted = _delete_environment_variable(
client=client, app_id=target_app_id, name=name
)
Expand Down
2 changes: 1 addition & 1 deletion src/fastapi_cloud_cli/commands/env/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def get_variable(
with toolkit.progress(
"Fetching environment variables...", transient=True
) as progress:
with client.handle_http_errors(progress):
with client.handle_http_errors(progress, toolkit=toolkit):
environment_variables = _get_environment_variables(
client=client, app_id=target_app_id
)
Expand Down
2 changes: 1 addition & 1 deletion src/fastapi_cloud_cli/commands/env/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def list_variables(
with toolkit.progress(
"Fetching environment variables...", transient=True
) as progress:
with client.handle_http_errors(progress):
with client.handle_http_errors(progress, toolkit=toolkit):
environment_variables = _get_environment_variables(
client=client, app_id=target_app_id
)
Expand Down
2 changes: 1 addition & 1 deletion src/fastapi_cloud_cli/commands/env/set.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def set(
with toolkit.progress(
"Setting environment variable", transient=True
) as progress:
with client.handle_http_errors(progress):
with client.handle_http_errors(progress, toolkit=toolkit):
_set_environment_variable(
client=client,
app_id=target_app_id,
Expand Down
18 changes: 14 additions & 4 deletions src/fastapi_cloud_cli/commands/logout.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
from typing import Any

from fastapi_cloud_cli.utils.auth import delete_auth_config
from fastapi_cloud_cli.utils.cli import get_rich_toolkit
from fastapi_cloud_cli.utils.execution import JsonOutputOption


def logout() -> None:
def logout(
json_output: JsonOutputOption = False,
) -> Any:
"""
Logout from FastAPI Cloud.
"""
with get_rich_toolkit() as toolkit:
toolkit.print_title("FastAPI Cloud")
toolkit.print_line()
with get_rich_toolkit(json_output=json_output) as toolkit:
if not json_output:
toolkit.print_title("FastAPI Cloud")
toolkit.print_line()

delete_auth_config()

if json_output:
toolkit.success({"logged_out": True})
return

toolkit.print("You are now logged out!", emoji="👋")
21 changes: 19 additions & 2 deletions src/fastapi_cloud_cli/commands/setup_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from fastapi_cloud_cli.utils.apps import resolve_app_id_or_fail
from fastapi_cloud_cli.utils.auth import Identity
from fastapi_cloud_cli.utils.cli import get_rich_toolkit
from fastapi_cloud_cli.utils.execution import JsonOutputOption

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -194,6 +195,7 @@ def setup_ci(
"-f",
help="Custom workflow filename (written to .github/workflows/)",
),
json_output: JsonOutputOption = False,
) -> None:
"""Configures a GitHub Actions workflow for deploying the app on push to the specified branch.

Expand All @@ -207,7 +209,7 @@ def setup_ci(

identity = Identity()

with get_rich_toolkit() as toolkit:
with get_rich_toolkit(json_output=json_output) as toolkit:
if not identity.is_logged_in():
toolkit.print(
"No credentials found. Use [blue]`fastapi login`[/] to login.",
Expand Down Expand Up @@ -290,7 +292,9 @@ def setup_ci(
title="Generating deploy token...", done_emoji="🔑"
) as progress,
client.handle_http_errors(
progress, default_message="Error creating deploy token."
progress,
toolkit=toolkit,
default_message="Error creating deploy token.",
),
):
token_data = _create_token(
Expand Down Expand Up @@ -357,6 +361,19 @@ def setup_ci(

toolkit.print_line()

if json_output:
workflow_was_written = write_workflow if not secrets_only else False
toolkit.success(
{
"app_id": target_app_id,
"branch": branch,
"token_expired_at": token_data["expired_at"],
"secrets_set_via_gh": has_gh,
"workflow_written": workflow_was_written,
}
)
return

toolkit.print(msg_done, emoji="✅")
toolkit.print_line()
# Token expiration date is in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ), extract date portion
Expand Down
11 changes: 4 additions & 7 deletions src/fastapi_cloud_cli/utils/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,15 +324,12 @@ def __init__(self, use_deploy_token: bool = False) -> None:
def handle_http_errors(
self,
progress: Progress,
toolkit: ErrorToolkit,
default_message: str | None = None,
*,
not_found_message: str | None = None,
toolkit: ErrorToolkit | None = None,
) -> Generator[None, None, None]:
# TODO: Once every command supports JSON output, require toolkit here
# and let it be the single human/JSON error rendering boundary.

mode = toolkit.mode if toolkit else "human"
mode = toolkit.mode

try:
yield
Expand All @@ -344,7 +341,7 @@ def handle_http_errors(
" Please try again later."
)

if mode == "json" and toolkit:
if mode == "json":
toolkit.fail(
"network_error",
message,
Expand All @@ -365,7 +362,7 @@ def handle_http_errors(
)
code = get_http_error_code(e)

if mode == "json" and toolkit:
if mode == "json":
toolkit.fail(
code,
message,
Expand Down
14 changes: 14 additions & 0 deletions tests/test_cli_logout.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from pathlib import Path

from typer.testing import CliRunner
Expand Down Expand Up @@ -29,3 +30,16 @@ def test_logout_with_no_auth_file(temp_auth_config: Path) -> None:
assert "You are now logged out!" in result.output

assert not temp_auth_config.exists()


def test_logout_json_output(temp_auth_config: Path) -> None:
temp_auth_config.write_text('{"access_token": "test_token"}')

assert temp_auth_config.exists()

result = runner.invoke(app, ["logout", "--json"])

assert result.exit_code == 0
assert json.loads(result.output) == {"data": {"logged_out": True}}

assert not temp_auth_config.exists()
75 changes: 75 additions & 0 deletions tests/test_cli_setup_ci.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import subprocess
from pathlib import Path
from unittest.mock import patch
Expand Down Expand Up @@ -730,3 +731,77 @@ def test_shows_enterprise_secrets_url_when_gh_not_installed(
# Should use enterprise host, not github.com
assert "github.enterprise.com/owner/repo/settings/secrets/actions" in result.output
assert "github.com/owner/repo" not in result.output


@pytest.mark.respx
def test_setup_ci_json_output(
logged_in_cli: None,
configured_app: ConfiguredApp,
respx_mock: respx.MockRouter,
) -> None:
app_id = configured_app.app_id
_mock_token_api(respx_mock, app_id, token_value="test-token-value")

with (
changing_dir(configured_app.path),
patch(
"fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
return_value=GITHUB_ORIGIN,
),
patch(
"fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
return_value=True,
),
patch(
"fastapi_cloud_cli.commands.setup_ci._get_default_branch",
return_value="main",
),
patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
):
result = runner.invoke(app, ["setup-ci", "--json"])

assert result.exit_code == 0
output = json.loads(result.output)

assert output["data"]["app_id"] == app_id
assert output["data"]["branch"] == "main"
assert output["data"]["secrets_set_via_gh"] is True
assert output["data"]["workflow_written"] is True
assert output["data"]["token_expired_at"] == "2027-02-18T00:00:00Z"


@pytest.mark.respx
def test_setup_ci_json_output_secrets_only(
logged_in_cli: None,
configured_app: ConfiguredApp,
respx_mock: respx.MockRouter,
) -> None:
app_id = configured_app.app_id
_mock_token_api(respx_mock, app_id, token_value="test-token-value")

with (
changing_dir(configured_app.path),
patch(
"fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
return_value=GITHUB_ORIGIN,
),
patch(
"fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
return_value=True,
),
patch(
"fastapi_cloud_cli.commands.setup_ci._get_default_branch",
return_value="main",
),
patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
):
result = runner.invoke(app, ["setup-ci", "--json", "--secrets-only"])

assert result.exit_code == 0
output = json.loads(result.output)

assert output["data"]["app_id"] == app_id
assert output["data"]["branch"] == "main"
assert output["data"]["secrets_set_via_gh"] is True
assert output["data"]["workflow_written"] is False
assert output["data"]["token_expired_at"] == "2027-02-18T00:00:00Z"
Loading