mozbridge-cli 0.2.0__tar.gz → 0.2.1__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.
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/PKG-INFO +1 -1
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/docs/commands.md +190 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/pyproject.toml +1 -1
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/api.py +162 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/main.py +323 -2
- mozbridge_cli-0.2.1/tests/test_deploy.py +268 -0
- mozbridge_cli-0.2.1/tests/test_env.py +242 -0
- mozbridge_cli-0.2.1/tests/test_logs.py +252 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/.gitignore +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/README.md +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/scripts/release.sh +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/__init__.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/__main__.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/auth.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/build.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/compose.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/config.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/link.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/local_build.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/runtime_secrets.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/src/mozbridge_cli/session.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/conftest.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_build.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_ci_token_auth.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_compose.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_diff.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_link.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_local_build.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_login.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_login_cli.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_logout.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_publish.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_publish_local.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_publish_multi_component.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_refresh.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_rollback.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_runtime_secrets.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_session_permissions.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_status.py +0 -0
- {mozbridge_cli-0.2.0 → mozbridge_cli-0.2.1}/tests/test_whoami.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mozbridge-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Command-line client for the Mozbridge deployment platform: SSO login, build, publish, status, and rollback for any project hosted on Mozbridge.
|
|
5
5
|
Project-URL: Homepage, https://mozbridge.com
|
|
6
6
|
Project-URL: Documentation, https://github.com/jessin01/mozbridge/blob/main/cli/docs/commands.md
|
|
@@ -547,6 +547,196 @@ Rollback triggered (task_id=777).
|
|
|
547
547
|
|
|
548
548
|
---
|
|
549
549
|
|
|
550
|
+
## `mozbridge deploy`
|
|
551
|
+
|
|
552
|
+
Deploys an already-built image tag to the linked Mozbridge project. A real,
|
|
553
|
+
consequential production action — same category as `mozbridge rollback`: it
|
|
554
|
+
mutates production the moment it's called, **not a dry-run**. Prompts for
|
|
555
|
+
confirmation unless `--yes` is given.
|
|
556
|
+
|
|
557
|
+
**Flags**:
|
|
558
|
+
| Flag | Meaning |
|
|
559
|
+
|---|---|
|
|
560
|
+
| `--frontend TAG` | Frontend image tag to deploy. |
|
|
561
|
+
| `--backend TAG` | Backend image tag to deploy. |
|
|
562
|
+
| `--env NAME` | Environment to deploy to. Default `prod`. |
|
|
563
|
+
| `--force` | Override a blocking preflight check. Never set automatically — a separate, explicit decision each time. |
|
|
564
|
+
| `--yes` | Skip the interactive confirmation prompt. |
|
|
565
|
+
|
|
566
|
+
**Requires**: link + login, same as `publish`/`status`/`rollback`. At least
|
|
567
|
+
one of `--frontend`/`--backend` must be given — deploying neither makes no
|
|
568
|
+
sense.
|
|
569
|
+
|
|
570
|
+
**What it does**: prints what it's about to do (project, which service(s),
|
|
571
|
+
which tag(s), which environment), then either aborts on a declined prompt or
|
|
572
|
+
calls `POST /api/v1/projects/{project_id}/deploy` with
|
|
573
|
+
`{frontend_version, backend_version, services, environment, force}`.
|
|
574
|
+
**`services` is built from which of `--frontend`/`--backend` you actually
|
|
575
|
+
gave a tag** — not the API's own default of deploying both — so the CLI only
|
|
576
|
+
ever asks the platform to deploy what you specified.
|
|
577
|
+
|
|
578
|
+
The backend runs a preflight check before enqueuing the deploy. A 409
|
|
579
|
+
response means it found a real, known-to-fail condition and refused —
|
|
580
|
+
each blocking check is printed with its title, detail, and suggested
|
|
581
|
+
action, followed by a hint to re-run with `--force` if you want to override
|
|
582
|
+
it. **The CLI never retries with `--force` automatically** — that is always
|
|
583
|
+
a separate, explicit re-invocation.
|
|
584
|
+
|
|
585
|
+
**Example (`--yes`, single service)**:
|
|
586
|
+
```
|
|
587
|
+
$ mozbridge deploy --backend v1.9.319 --yes
|
|
588
|
+
This will deploy acme/web-app (prod):
|
|
589
|
+
backend -> v1.9.319
|
|
590
|
+
Deploy triggered (task_id=888).
|
|
591
|
+
Run `mozbridge logs` to watch it.
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**Example (preflight blocked)**:
|
|
595
|
+
```
|
|
596
|
+
$ mozbridge deploy --backend v1.9.319 --yes
|
|
597
|
+
This will deploy acme/web-app (prod):
|
|
598
|
+
backend -> v1.9.319
|
|
599
|
+
Preflight failed — this deploy is known to fail.
|
|
600
|
+
- No registry credential: GHCR_TOKEN is not configured for this project.
|
|
601
|
+
action: Set a registry credential in Project Settings.
|
|
602
|
+
Pass force=true to deploy anyway.
|
|
603
|
+
Re-run with --force to override — this is not done automatically.
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**Failure modes**:
|
|
607
|
+
- Neither `--frontend` nor `--backend` given: clear error, exit 1, no API
|
|
608
|
+
call is made.
|
|
609
|
+
- Not linked / not logged in: identical messages to `publish` above, exit 1.
|
|
610
|
+
- Confirmation prompt declined (`--yes` not passed, answer is not `y`):
|
|
611
|
+
Typer's own `Abort` handling — the command exits non-zero without calling
|
|
612
|
+
the API.
|
|
613
|
+
- Preflight-blocked (409, structured body): every blocking check printed
|
|
614
|
+
clearly plus a `--force` hint, exit 1 — no automatic retry.
|
|
615
|
+
- Any other API error: the raw `ApiError` message, exit 1.
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## `mozbridge logs`
|
|
620
|
+
|
|
621
|
+
Shows **BUILD/DEPLOY OPERATION logs** for the linked project — the log
|
|
622
|
+
output of a Mozbridge build or deploy task (what `publish`, `deploy`, and
|
|
623
|
+
`rollback` trigger). **This is not your running application's
|
|
624
|
+
runtime/request logs** — the platform does not expose those via any API
|
|
625
|
+
today, and this command is not `docker logs` for your live app.
|
|
626
|
+
|
|
627
|
+
**Flags**:
|
|
628
|
+
| Flag | Meaning |
|
|
629
|
+
|---|---|
|
|
630
|
+
| `TASK_ID` (argument, optional) | Task id of a build/deploy operation. Omit to use the most recent one for this project. |
|
|
631
|
+
| `--follow` / `-f` | Stream new log output as it arrives; exits when the run finishes. |
|
|
632
|
+
|
|
633
|
+
**Requires**: link + login, same as `status`/`diff`/`rollback`.
|
|
634
|
+
|
|
635
|
+
**What it does**:
|
|
636
|
+
- If `TASK_ID` is omitted: `GET /api/v1/projects/{project_id}/operations?limit=1`
|
|
637
|
+
(newest-first, same ordering `status`/`diff` already rely on for
|
|
638
|
+
deployments) to resolve the most recent operation's `task_id`.
|
|
639
|
+
- Without `--follow`: one `GET /api/v1/projects/operations/{task_id}` call,
|
|
640
|
+
printing whatever is in `result.logs` right now — a single snapshot, no
|
|
641
|
+
long-lived connection.
|
|
642
|
+
- With `--follow`: opens `GET /api/v1/projects/operations/{task_id}/stream`
|
|
643
|
+
(SSE) and prints each `log` event's content as it's parsed. The server
|
|
644
|
+
closes the stream itself once the operation reaches a terminal state
|
|
645
|
+
(success/failed/error) — the CLI exits cleanly at that point. A dropped
|
|
646
|
+
connection surfaces a clear error instead of hanging.
|
|
647
|
+
|
|
648
|
+
**Example (snapshot)**:
|
|
649
|
+
```
|
|
650
|
+
$ mozbridge logs task-123
|
|
651
|
+
Step 1/5 : FROM python:3.12
|
|
652
|
+
Step 2/5 : COPY . /app
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**Example (follow)**:
|
|
656
|
+
```
|
|
657
|
+
$ mozbridge logs task-123 --follow
|
|
658
|
+
Step 1/5 : FROM python:3.12
|
|
659
|
+
Step 2/5 : COPY . /app
|
|
660
|
+
Successfully built abc123
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**Failure modes**:
|
|
664
|
+
- Not linked / not logged in: identical messages to `publish` above, exit 1.
|
|
665
|
+
- No `TASK_ID` given and the project has no operations yet: `No operations
|
|
666
|
+
found for this project yet.`, exit 1.
|
|
667
|
+
- The operation lookup or stream request fails: the raw `ApiError` message
|
|
668
|
+
(snapshot mode) or `Log stream connection dropped: <error>` (`--follow`),
|
|
669
|
+
exit 1.
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## `mozbridge env list` / `mozbridge env set` / `mozbridge env rm`
|
|
674
|
+
|
|
675
|
+
Manages the linked project's environment variables
|
|
676
|
+
(`GET`/`POST`/`DELETE /api/v1/projects/{project_id}/env-vars[...]`).
|
|
677
|
+
`EnvVar` is a plain `{key, value}` pair — distinct from the project's
|
|
678
|
+
separate Vault-backed secrets blocks — and nothing in the platform's own
|
|
679
|
+
dashboard UI masks these values, so `env list` prints them in plain text.
|
|
680
|
+
|
|
681
|
+
**Requires**: link + login, same as `status`/`diff`/`rollback`.
|
|
682
|
+
|
|
683
|
+
### `mozbridge env list`
|
|
684
|
+
|
|
685
|
+
**Flags**: none.
|
|
686
|
+
|
|
687
|
+
**What it does**: `GET /api/v1/projects/{project_id}/env-vars`, prints every
|
|
688
|
+
key/value pair sorted by key. Prints `No environment variables set.` if
|
|
689
|
+
there are none.
|
|
690
|
+
|
|
691
|
+
```
|
|
692
|
+
$ mozbridge env list
|
|
693
|
+
DATABASE_URL postgres://user:pass@host/db
|
|
694
|
+
DEBUG false
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### `mozbridge env set KEY VALUE`
|
|
698
|
+
|
|
699
|
+
**What it does**: `POST /api/v1/projects/{project_id}/env-vars` with body
|
|
700
|
+
`{"key": KEY, "value": VALUE}`. **This is an upsert, not create-only** —
|
|
701
|
+
read directly off `backend/app/security.py:store_project_env_var`: the
|
|
702
|
+
handler loads the existing key->value dict out of Vault, does
|
|
703
|
+
`existing[key] = value`, and writes the whole dict back. Setting an
|
|
704
|
+
already-existing `KEY` silently overwrites it; there is no 409/400 and no
|
|
705
|
+
separate create-vs-update codepath.
|
|
706
|
+
|
|
707
|
+
The confirmation deliberately prints only the key, never the value, so a
|
|
708
|
+
secret-shaped `VALUE` isn't echoed a second time into your terminal beyond
|
|
709
|
+
the command invocation itself:
|
|
710
|
+
|
|
711
|
+
```
|
|
712
|
+
$ mozbridge env set DATABASE_URL postgres://user:pass@host/db
|
|
713
|
+
Set DATABASE_URL.
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### `mozbridge env rm KEY`
|
|
717
|
+
|
|
718
|
+
**What it does**: `DELETE /api/v1/projects/{project_id}/env-vars/{KEY}`.
|
|
719
|
+
**This is idempotent, not a strict "must exist" delete** — read directly
|
|
720
|
+
off `backend/app/security.py:delete_project_env_var`: it deletes `KEY`
|
|
721
|
+
`if key in existing`, and otherwise falls straight through to `return
|
|
722
|
+
True` — either way the route responds 200. Removing a `KEY` that was never
|
|
723
|
+
set still reports success; there is no 404 for a missing key, and this
|
|
724
|
+
command does not invent one.
|
|
725
|
+
|
|
726
|
+
```
|
|
727
|
+
$ mozbridge env rm DATABASE_URL
|
|
728
|
+
Removed DATABASE_URL.
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**Failure modes (all three)**:
|
|
732
|
+
- Not linked / not logged in: identical messages to `publish` above, exit 1.
|
|
733
|
+
- Any other API error (e.g. a 500 from a Vault write/delete failure): the
|
|
734
|
+
raw `ApiError` message (`Could not list env vars: <detail>` / `Could not
|
|
735
|
+
set the env var: <detail>` / `Could not remove the env var: <detail>`),
|
|
736
|
+
exit 1.
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
550
740
|
## A note on authorization for `publish` / `rollback`
|
|
551
741
|
|
|
552
742
|
Both commands work today because a plain human Logto session token passes
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mozbridge-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Command-line client for the Mozbridge deployment platform: SSO login, build, publish, status, and rollback for any project hosted on Mozbridge."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -36,6 +36,22 @@ class ApiError(Exception):
|
|
|
36
36
|
"""Raised when the Mozbridge API returns a response we can't recover from."""
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
class PreflightBlockedError(ApiError):
|
|
40
|
+
"""POST .../deploy returned 409 with the structured preflight-blocking body.
|
|
41
|
+
|
|
42
|
+
backend/app/features/projects/router.py's `_assert_preflight_clear` shapes
|
|
43
|
+
this as {message, blocking: [{key, title, detail, action}, ...], override}
|
|
44
|
+
— distinct enough from every other ApiError (which just carries a flat
|
|
45
|
+
string `detail`) that callers need the pieces separately to print each
|
|
46
|
+
blocking check on its own line, not a flattened string.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, message: str, blocking: list[dict], override: str | None) -> None:
|
|
50
|
+
super().__init__(message)
|
|
51
|
+
self.blocking = blocking
|
|
52
|
+
self.override = override
|
|
53
|
+
|
|
54
|
+
|
|
39
55
|
def _headers(access_token: str, org_id: int | None = None) -> dict[str, str]:
|
|
40
56
|
headers = {"Authorization": f"Bearer {access_token}"}
|
|
41
57
|
if org_id is not None:
|
|
@@ -237,6 +253,61 @@ def trigger_build(
|
|
|
237
253
|
return resp.json()
|
|
238
254
|
|
|
239
255
|
|
|
256
|
+
def list_project_env_vars(
|
|
257
|
+
client: httpx.Client, access_token: str, org_id: int, project_id: int
|
|
258
|
+
) -> list[dict]:
|
|
259
|
+
"""GET /api/v1/projects/{project_id}/env-vars -> list[schemas.EnvVar] ({key, value})."""
|
|
260
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}/env-vars"
|
|
261
|
+
try:
|
|
262
|
+
resp = client.get(url, headers=_headers(access_token, org_id))
|
|
263
|
+
except httpx.HTTPError as exc:
|
|
264
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
265
|
+
_raise_for_status(resp, "list env vars")
|
|
266
|
+
return resp.json()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def set_project_env_var(
|
|
270
|
+
client: httpx.Client, access_token: str, org_id: int, project_id: int, key: str, value: str
|
|
271
|
+
) -> dict:
|
|
272
|
+
"""POST /api/v1/projects/{project_id}/env-vars, body schemas.EnvVarCreate {key, value}.
|
|
273
|
+
|
|
274
|
+
This is an upsert, not create-only: router.py's update_project_env_var
|
|
275
|
+
calls security.store_project_env_var, which reads the existing
|
|
276
|
+
key->value dict out of Vault, does `existing[key] = value`, and writes
|
|
277
|
+
the whole dict back. Setting an already-existing key silently
|
|
278
|
+
overwrites it — there is no separate create-vs-update codepath and no
|
|
279
|
+
409/400 for an existing key.
|
|
280
|
+
"""
|
|
281
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}/env-vars"
|
|
282
|
+
try:
|
|
283
|
+
resp = client.post(url, json={"key": key, "value": value}, headers=_headers(access_token, org_id))
|
|
284
|
+
except httpx.HTTPError as exc:
|
|
285
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
286
|
+
_raise_for_status(resp, "set the env var")
|
|
287
|
+
return resp.json()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def delete_project_env_var(
|
|
291
|
+
client: httpx.Client, access_token: str, org_id: int, project_id: int, key: str
|
|
292
|
+
) -> dict:
|
|
293
|
+
"""DELETE /api/v1/projects/{project_id}/env-vars/{key}.
|
|
294
|
+
|
|
295
|
+
Idempotent, not a strict "must exist" delete: router.py's
|
|
296
|
+
delete_project_env_var calls security.delete_project_env_var, which
|
|
297
|
+
only deletes `key` from the existing dict `if key in existing` and
|
|
298
|
+
otherwise falls straight through to `return True` — either way the
|
|
299
|
+
route responds 200 {"status": "success", "message": "Variable <key>
|
|
300
|
+
removed"}. There is no 404 for a key that was never set.
|
|
301
|
+
"""
|
|
302
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}/env-vars/{key}"
|
|
303
|
+
try:
|
|
304
|
+
resp = client.delete(url, headers=_headers(access_token, org_id))
|
|
305
|
+
except httpx.HTTPError as exc:
|
|
306
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
307
|
+
_raise_for_status(resp, "remove the env var")
|
|
308
|
+
return resp.json()
|
|
309
|
+
|
|
310
|
+
|
|
240
311
|
def trigger_prebuilt_build(
|
|
241
312
|
client: httpx.Client,
|
|
242
313
|
access_token: str,
|
|
@@ -272,3 +343,94 @@ def trigger_prebuilt_build(
|
|
|
272
343
|
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
273
344
|
_raise_for_status(resp, "register the prebuilt image")
|
|
274
345
|
return resp.json()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def deploy_project(
|
|
349
|
+
client: httpx.Client, access_token: str, org_id: int, project_id: int, payload: dict
|
|
350
|
+
) -> dict:
|
|
351
|
+
"""POST /api/v1/projects/{project_id}/deploy, body schemas.DeployRequest.
|
|
352
|
+
|
|
353
|
+
`payload` is sent as-is — the caller (main.py's `deploy` command) builds
|
|
354
|
+
the exact DeployRequest-shaped dict, since it's the one that knows which
|
|
355
|
+
fields the human actually asked for.
|
|
356
|
+
|
|
357
|
+
A 409 here means the backend's preflight gate (`_assert_preflight_clear`)
|
|
358
|
+
found real, known-to-fail conditions and refused to enqueue the deploy —
|
|
359
|
+
handled as `PreflightBlockedError`, not folded into the generic ApiError
|
|
360
|
+
path below, so the caller can print each blocking check individually.
|
|
361
|
+
Any other non-2xx (network error, plain validation error, etc.) falls
|
|
362
|
+
through to the same `_raise_for_status` -> `ApiError` path every other
|
|
363
|
+
function in this module uses.
|
|
364
|
+
|
|
365
|
+
Returns {"status": "enqueued", "action": "deploy", "task_id": ..., ...deploy_kwargs}.
|
|
366
|
+
"""
|
|
367
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}/deploy"
|
|
368
|
+
try:
|
|
369
|
+
resp = client.post(url, json=payload, headers=_headers(access_token, org_id))
|
|
370
|
+
except httpx.HTTPError as exc:
|
|
371
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
372
|
+
|
|
373
|
+
if resp.status_code == 409:
|
|
374
|
+
try:
|
|
375
|
+
body = resp.json()
|
|
376
|
+
except ValueError:
|
|
377
|
+
body = None
|
|
378
|
+
detail = body.get("detail") if isinstance(body, dict) else None
|
|
379
|
+
if isinstance(detail, dict) and "blocking" in detail:
|
|
380
|
+
raise PreflightBlockedError(
|
|
381
|
+
detail.get("message") or "Preflight failed — this deploy is known to fail.",
|
|
382
|
+
detail.get("blocking") or [],
|
|
383
|
+
detail.get("override"),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
_raise_for_status(resp, "deploy the project")
|
|
387
|
+
return resp.json()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def list_project_operations(
|
|
391
|
+
client: httpx.Client, access_token: str, org_id: int, project_id: int, *, limit: int = 1
|
|
392
|
+
) -> list[dict]:
|
|
393
|
+
"""GET /api/v1/projects/{project_id}/operations?limit=... -> list[schemas.ProjectOperation].
|
|
394
|
+
|
|
395
|
+
Backend orders these newest-first (ProjectService.get_operations_with_logs
|
|
396
|
+
sorts by created_at.desc(), same pattern as list_project_deployments), so
|
|
397
|
+
`limit=1` is enough to resolve "the most recent operation for this
|
|
398
|
+
project" for `mozbridge logs` when no task id is given.
|
|
399
|
+
"""
|
|
400
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}/operations"
|
|
401
|
+
try:
|
|
402
|
+
resp = client.get(url, params={"limit": limit}, headers=_headers(access_token, org_id))
|
|
403
|
+
except httpx.HTTPError as exc:
|
|
404
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
405
|
+
_raise_for_status(resp, "list operations")
|
|
406
|
+
return resp.json()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def get_operation(client: httpx.Client, access_token: str, org_id: int, task_id: str) -> dict:
|
|
410
|
+
"""GET /api/v1/projects/operations/{task_id} -> schemas.ProjectOperation.
|
|
411
|
+
|
|
412
|
+
Note this route lives directly under /operations/, not
|
|
413
|
+
/{project_id}/operations/{task_id} — router.py's `get_task_operation`
|
|
414
|
+
resolves the project purely from the ProjectOperation row itself (scoped
|
|
415
|
+
by org via X-Organization-Id), so no project_id is needed in the path.
|
|
416
|
+
Used by `mozbridge logs` (without --follow) for a one-shot snapshot of
|
|
417
|
+
whatever's in `result.logs` so far.
|
|
418
|
+
"""
|
|
419
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/operations/{task_id}"
|
|
420
|
+
try:
|
|
421
|
+
resp = client.get(url, headers=_headers(access_token, org_id))
|
|
422
|
+
except httpx.HTTPError as exc:
|
|
423
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
424
|
+
_raise_for_status(resp, "fetch the operation")
|
|
425
|
+
return resp.json()
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def operation_stream_url(task_id: str) -> str:
|
|
429
|
+
"""URL for GET /api/v1/projects/operations/{task_id}/stream (SSE).
|
|
430
|
+
|
|
431
|
+
A plain string builder, not a request function — `mozbridge logs
|
|
432
|
+
--follow` needs the URL to open its own long-lived `client.stream(...)`
|
|
433
|
+
call directly (see main.py's `_iter_sse_events`), rather than going
|
|
434
|
+
through a request/response helper shaped for ordinary JSON calls.
|
|
435
|
+
"""
|
|
436
|
+
return f"{config.API_BASE_URL}{config.PROJECTS_PATH}/operations/{task_id}/stream"
|