mozbridge-cli 0.3.0__tar.gz → 0.4.0__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.3.0 → mozbridge_cli-0.4.0}/PKG-INFO +8 -6
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/README.md +7 -5
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/docs/commands.md +214 -6
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/pyproject.toml +1 -1
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/api.py +116 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/cmd_deploy.py +56 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_doctor.py +229 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_registry.py +134 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_token.py +180 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/config.py +7 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/main.py +23 -8
- mozbridge_cli-0.4.0/tests/test_deploy_preflight.py +259 -0
- mozbridge_cli-0.4.0/tests/test_doctor.py +407 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_registry.py +98 -1
- mozbridge_cli-0.4.0/tests/test_token.py +275 -0
- mozbridge_cli-0.3.0/src/mozbridge_cli/cmd_registry.py +0 -97
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/.gitignore +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/scripts/release.sh +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/__init__.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/__main__.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/auth.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/build.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/cmd_auth.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/cmd_env.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/cmd_projects.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/cmd_publish.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/cmd_secrets.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/cmd_shared.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/compose.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/link.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/local_build.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/runtime_secrets.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/src/mozbridge_cli/session.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/conftest.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_build.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_ci_token_auth.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_compose.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_deploy.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_diff.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_env.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_link.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_local_build.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_login.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_login_cli.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_logout.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_logs.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_publish.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_publish_local.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_publish_multi_component.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_refresh.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_rollback.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_runtime_secrets.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_secrets.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_session_permissions.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_status.py +0 -0
- {mozbridge_cli-0.3.0 → mozbridge_cli-0.4.0}/tests/test_whoami.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mozbridge-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -250,11 +250,13 @@ written, or permission-checked in this path at all. Because of that,
|
|
|
250
250
|
means "don't attempt an interactive login") — unset it first if you actually
|
|
251
251
|
want to run the device flow.
|
|
252
252
|
|
|
253
|
-
**Where the token comes from**: a project-scoped Mozbridge
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
253
|
+
**Where the token comes from**: a project- or org-scoped Mozbridge
|
|
254
|
+
`ServiceToken`. Mint one directly from this CLI — `mozbridge token create
|
|
255
|
+
--scope deploy` (requires an active `mozbridge login` session; see
|
|
256
|
+
`mozbridge token create --help`) — or out of band via the dashboard/API
|
|
257
|
+
(`create_service_token`). Set it as your CI platform's secret store, never
|
|
258
|
+
as a `--token` flag (it would leak into shell history and process
|
|
259
|
+
listings) and never committed to the repo.
|
|
258
260
|
|
|
259
261
|
**`MOZBRIDGE_TOKEN` vs `MOZBRIDGE_BUILD_TOKEN` — do not confuse these**:
|
|
260
262
|
|
|
@@ -222,11 +222,13 @@ written, or permission-checked in this path at all. Because of that,
|
|
|
222
222
|
means "don't attempt an interactive login") — unset it first if you actually
|
|
223
223
|
want to run the device flow.
|
|
224
224
|
|
|
225
|
-
**Where the token comes from**: a project-scoped Mozbridge
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
225
|
+
**Where the token comes from**: a project- or org-scoped Mozbridge
|
|
226
|
+
`ServiceToken`. Mint one directly from this CLI — `mozbridge token create
|
|
227
|
+
--scope deploy` (requires an active `mozbridge login` session; see
|
|
228
|
+
`mozbridge token create --help`) — or out of band via the dashboard/API
|
|
229
|
+
(`create_service_token`). Set it as your CI platform's secret store, never
|
|
230
|
+
as a `--token` flag (it would leak into shell history and process
|
|
231
|
+
listings) and never committed to the repo.
|
|
230
232
|
|
|
231
233
|
**`MOZBRIDGE_TOKEN` vs `MOZBRIDGE_BUILD_TOKEN` — do not confuse these**:
|
|
232
234
|
|
|
@@ -575,12 +575,25 @@ calls `POST /api/v1/projects/{project_id}/deploy` with
|
|
|
575
575
|
gave a tag** — not the API's own default of deploying both — so the CLI only
|
|
576
576
|
ever asks the platform to deploy what you specified.
|
|
577
577
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
action
|
|
582
|
-
|
|
583
|
-
|
|
578
|
+
Before the confirmation prompt, `deploy` also proactively calls `GET
|
|
579
|
+
/projects/{id}/preflight` (the platform's own pre-deploy checks) and
|
|
580
|
+
prints anything at `warn`/`fail` level — title, detail, and suggested
|
|
581
|
+
action — so a likely-to-fail condition is visible before you commit, not
|
|
582
|
+
just after a 409. When every check is `pass`, nothing extra is printed —
|
|
583
|
+
the happy path stays exactly as quiet as before this existed. A failed
|
|
584
|
+
lookup here (network error, etc.) is swallowed silently and never blocks
|
|
585
|
+
`deploy` — this is informational only, matching the backend's own
|
|
586
|
+
fail-open design; the real gate is still the backend's own check at
|
|
587
|
+
enqueue time. These checks are platform-level (registry credentials, DNS,
|
|
588
|
+
disk, Vault role, and similar) — they do **not** detect a project's own
|
|
589
|
+
compose/template misconfiguration.
|
|
590
|
+
|
|
591
|
+
The backend also runs its own preflight check before enqueuing the
|
|
592
|
+
deploy. A 409 response means it found a real, known-to-fail condition and
|
|
593
|
+
refused — each blocking check is printed with its title, detail, and
|
|
594
|
+
suggested action, followed by a hint to re-run with `--force` if you want
|
|
595
|
+
to override it. **The CLI never retries with `--force` automatically** —
|
|
596
|
+
that is always a separate, explicit re-invocation.
|
|
584
597
|
|
|
585
598
|
**Example (`--yes`, single service)**:
|
|
586
599
|
```
|
|
@@ -737,6 +750,201 @@ Removed DATABASE_URL.
|
|
|
737
750
|
|
|
738
751
|
---
|
|
739
752
|
|
|
753
|
+
## `mozbridge registry status` / `mozbridge registry set`
|
|
754
|
+
|
|
755
|
+
Manages the linked project's own container-registry credential, used by
|
|
756
|
+
`publish --local` builds and by platform-side deploys when no
|
|
757
|
+
platform-provisioned registry applies. GET never returns a token/secret
|
|
758
|
+
value — `schemas.RegistryCredentialStatus` has no such field — so there is
|
|
759
|
+
no `registry get` that prints one.
|
|
760
|
+
|
|
761
|
+
### `mozbridge registry status`
|
|
762
|
+
|
|
763
|
+
**What it does**: `GET /api/v1/projects/{project_id}/registry`. If the
|
|
764
|
+
project has its own credential configured, prints it (`registry_url`,
|
|
765
|
+
`username`, `updated_at`) and stops — a project-level credential always
|
|
766
|
+
takes precedence. Only when the project has none does it also call `GET
|
|
767
|
+
/api/v1/organizations/{org_id}/registry` and report the org-level default
|
|
768
|
+
instead, since that's what a build/deploy will actually fall back to.
|
|
769
|
+
This mirrors the real resolution order
|
|
770
|
+
(`app/services/registry_credentials.resolve_registry_credential`: project
|
|
771
|
+
override → org default → none) rather than only ever showing the
|
|
772
|
+
project's own (possibly misleading) empty state.
|
|
773
|
+
|
|
774
|
+
```
|
|
775
|
+
$ mozbridge registry status
|
|
776
|
+
No project-level registry credential configured.
|
|
777
|
+
Builds/deploys will use the ORG-level default:
|
|
778
|
+
registry_url: ghcr.io
|
|
779
|
+
username: acme-bot
|
|
780
|
+
updated_at: 2026-09-01T12:00:00Z
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
If neither is configured:
|
|
784
|
+
|
|
785
|
+
```
|
|
786
|
+
$ mozbridge registry status
|
|
787
|
+
No registry credential configured at the project or org level.
|
|
788
|
+
`--local` builds will fail without one — run `mozbridge registry set`.
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
### `mozbridge registry set`
|
|
792
|
+
|
|
793
|
+
**Flags**: `--url` (default `ghcr.io`), `--username` (required), `--token`
|
|
794
|
+
(required — the registry password/token, not a Mozbridge service token).
|
|
795
|
+
|
|
796
|
+
**What it does**: `PUT /api/v1/projects/{project_id}/registry`. Always
|
|
797
|
+
writes the **project-level** credential — there is no `--org` flag on this
|
|
798
|
+
command, so an org-level default is configured from the dashboard
|
|
799
|
+
(**Organization Settings → Container Registry**), not from here. The
|
|
800
|
+
backend validates the credential against the real registry before storing
|
|
801
|
+
it; a bad credential is rejected with the registry's own error, not
|
|
802
|
+
silently saved.
|
|
803
|
+
|
|
804
|
+
```
|
|
805
|
+
$ mozbridge registry set --url ghcr.io --username acme-bot --token ghp_xxx
|
|
806
|
+
Registry credential set (ghcr.io, username=acme-bot).
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
**Failure modes**: not linked / not logged in, exit 1. Registry rejects
|
|
810
|
+
the credential (bad token, wrong URL): the raw validation error, exit 1.
|
|
811
|
+
|
|
812
|
+
---
|
|
813
|
+
|
|
814
|
+
## `mozbridge secrets list` / `mozbridge secrets set`
|
|
815
|
+
|
|
816
|
+
Manages the linked project's Vault-backed, per-environment/per-component
|
|
817
|
+
secrets (`backend/app/features/projects/router.py`'s
|
|
818
|
+
`/environments/{env}/secrets/{component}` routes) — distinct from the
|
|
819
|
+
plain env-vars `env` wraps. The underlying replace-all POST endpoint
|
|
820
|
+
deletes any key not included in the request; this CLI deliberately never
|
|
821
|
+
calls it — `secrets set` uses PATCH (merge one key) only, and `api.py` has
|
|
822
|
+
no wrapper for the replace-all route at all, so there is nothing here
|
|
823
|
+
that could call it by accident.
|
|
824
|
+
|
|
825
|
+
### `mozbridge secrets list`
|
|
826
|
+
|
|
827
|
+
**Flags**: `--component` (`backend` default, or `frontend`/`infra`),
|
|
828
|
+
`--env` (`prod` default), `--show-values` (fetch and print actual VALUES
|
|
829
|
+
instead of just key names — prompts for confirmation unless `--yes` is
|
|
830
|
+
also passed).
|
|
831
|
+
|
|
832
|
+
**What it does**: defaults to the keys-only endpoint — no secret value
|
|
833
|
+
ever leaves the server for a plain `mozbridge secrets list`. With
|
|
834
|
+
`--show-values`, confirms (real secret material is about to print to your
|
|
835
|
+
terminal and end up in scrollback/history — same gating as `deploy`) then
|
|
836
|
+
fetches and prints actual values.
|
|
837
|
+
|
|
838
|
+
```
|
|
839
|
+
$ mozbridge secrets list
|
|
840
|
+
backend/prod: 2 secret(s)
|
|
841
|
+
DATABASE_URL
|
|
842
|
+
STRIPE_SECRET_KEY
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
### `mozbridge secrets set KEY VALUE`
|
|
846
|
+
|
|
847
|
+
**Flags**: `--component` (`backend` default), `--env` (`prod` default).
|
|
848
|
+
|
|
849
|
+
**What it does**: `PATCH .../secrets/{component}` — merges this one key
|
|
850
|
+
server-side over a strict read of what's already there; never touches any
|
|
851
|
+
other key. There is no `secrets rm` — the backend has no single-key
|
|
852
|
+
delete endpoint, only PATCH-to-add/update or the wholesale-replace POST
|
|
853
|
+
this CLI avoids on purpose. Removing a key today means the dashboard, or
|
|
854
|
+
accepting the replace-all risk directly.
|
|
855
|
+
|
|
856
|
+
```
|
|
857
|
+
$ mozbridge secrets set STRIPE_SECRET_KEY sk_live_xxx
|
|
858
|
+
Set STRIPE_SECRET_KEY (backend/prod).
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
**Failure modes (both commands)**: not linked / not logged in, exit 1.
|
|
862
|
+
Any other API error: the raw `ApiError` message, exit 1.
|
|
863
|
+
|
|
864
|
+
---
|
|
865
|
+
|
|
866
|
+
## `mozbridge token create`
|
|
867
|
+
|
|
868
|
+
Mints a new Mozbridge `ServiceToken` without leaving the CLI — wraps `POST
|
|
869
|
+
/api/v1/tokens` (`token_router.create_service_token`), authenticated by
|
|
870
|
+
the CLI's existing human login session. A service token cannot call this
|
|
871
|
+
endpoint to mint further tokens (enforced server-side), which is exactly
|
|
872
|
+
why this command requires `mozbridge login`, not `MOZBRIDGE_TOKEN`.
|
|
873
|
+
|
|
874
|
+
**Flags**:
|
|
875
|
+
| Flag | Meaning |
|
|
876
|
+
|---|---|
|
|
877
|
+
| `--scope` | Repeatable. Required at least once — no silent default. Real scopes: `read`, `write`, `deploy`, `admin`, `build`. |
|
|
878
|
+
| `--name` | Token name. Defaults to a generated `mozbridge-cli-<slug>-<random>` name. |
|
|
879
|
+
| `--org` | Mint an org-scoped token (bound to the linked org, no single project) instead of the default project-scoped one. |
|
|
880
|
+
| `--project` | Mint a project-scoped token — already the default; pass only for explicitness. Mutually exclusive with `--org`. |
|
|
881
|
+
| `--expires` | A single duration: `45m`, `24h`, `30d`, `2w`. Omit for a token that never expires. |
|
|
882
|
+
|
|
883
|
+
**What it does**: calls `create_service_token` with the given scopes,
|
|
884
|
+
bound to the linked project (default) or org (`--org`). The raw token
|
|
885
|
+
value is printed once, in full — the one place in this CLI where printing
|
|
886
|
+
a real secret to the terminal is correct, since there is no `mozbridge
|
|
887
|
+
token show` to retrieve it again.
|
|
888
|
+
|
|
889
|
+
```
|
|
890
|
+
$ mozbridge token create --scope build --expires 30d
|
|
891
|
+
Token created: mozbridge-cli-acme-web-app-3f9a2c1e (project-scoped, scopes=build)
|
|
892
|
+
expires_at: 2026-10-14T12:00:00Z
|
|
893
|
+
|
|
894
|
+
======================================================================
|
|
895
|
+
SAVE THIS TOKEN NOW — it will never be shown again:
|
|
896
|
+
|
|
897
|
+
mbt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
898
|
+
|
|
899
|
+
Store it in a password manager or your CI provider's secret store
|
|
900
|
+
immediately. There is no `mozbridge token show` — this is the only time
|
|
901
|
+
the raw value is ever returned.
|
|
902
|
+
======================================================================
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
**Failure modes**: no `--scope` given: clear error, exit 1, no API call.
|
|
906
|
+
Both `--org` and `--project` given: clear error, exit 1. Invalid
|
|
907
|
+
`--expires` format: clear error naming the expected shape, exit 1. Not
|
|
908
|
+
linked / not logged in: identical messages to `publish` above, exit 1.
|
|
909
|
+
Any other API error: the raw `ApiError` message, exit 1.
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
## `mozbridge doctor`
|
|
914
|
+
|
|
915
|
+
A self-diagnostic checklist for this CLI's setup — same motivation as
|
|
916
|
+
sibling package `cruxhive-mcp`'s own `cruxhive-doctor`: enough moving
|
|
917
|
+
parts (cached session, per-repo link file, registry credential, optional
|
|
918
|
+
build token, local docker daemon) that "is my setup actually working"
|
|
919
|
+
deserves one command instead of five.
|
|
920
|
+
|
|
921
|
+
**What it checks, in order**: logged in, linked (verified with a real
|
|
922
|
+
`GET /projects/{id}` call, not just the presence of
|
|
923
|
+
`.mozbridge/link.json`), project-level registry credential configured,
|
|
924
|
+
the platform's own pre-deploy checks (the same `GET .../preflight` call
|
|
925
|
+
`deploy` now runs proactively), whether `MOZBRIDGE_BUILD_TOKEN` is set,
|
|
926
|
+
and whether Docker is available locally. Each check is independent — one
|
|
927
|
+
erroring API call is reported as "could not check (error)" on its own
|
|
928
|
+
line and never prevents the rest from running.
|
|
929
|
+
|
|
930
|
+
```
|
|
931
|
+
$ mozbridge doctor
|
|
932
|
+
✓ Logged in: session valid
|
|
933
|
+
✓ Linked: acme/web-app (.mozbridge/link.json present) — resolved via API
|
|
934
|
+
○ Registry configured: no project-level registry credential configured (normal, not an error — ...)
|
|
935
|
+
✓ Pre-deploy checks: all clear (runs the platform's own pre-deploy checks)
|
|
936
|
+
○ MOZBRIDGE_BUILD_TOKEN set: not set — only required for `mozbridge publish --local`
|
|
937
|
+
✓ Docker available: docker CLI + daemon reachable
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
**Exit code**: non-zero only when a MEANINGFUL gate failed — not logged
|
|
941
|
+
in, or not linked. Registry, pre-deploy checks, `MOZBRIDGE_BUILD_TOKEN`,
|
|
942
|
+
and Docker are informational only: none of them are required for every
|
|
943
|
+
workflow this CLI supports, so a "not configured"/"not set"/"not found"
|
|
944
|
+
result never fails the command.
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
740
948
|
## A note on authorization for `publish` / `rollback`
|
|
741
949
|
|
|
742
950
|
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.
|
|
7
|
+
version = "0.4.0"
|
|
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"
|
|
@@ -324,6 +324,33 @@ def get_project_registry(client: httpx.Client, access_token: str, org_id: int, p
|
|
|
324
324
|
return resp.json()
|
|
325
325
|
|
|
326
326
|
|
|
327
|
+
def get_org_registry(client: httpx.Client, access_token: str, org_id: int) -> dict:
|
|
328
|
+
"""GET /api/v1/organizations/{org_id}/registry -> schemas.RegistryCredentialStatus.
|
|
329
|
+
|
|
330
|
+
Same response shape as get_project_registry above — {configured,
|
|
331
|
+
registry_url, username, updated_at}, token never included
|
|
332
|
+
(backend/app/features/identity/router.py:521-537's get_org_registry
|
|
333
|
+
mirrors the project-level route's own never-return-the-token design).
|
|
334
|
+
This is the org's default registry credential, used as a fallback for
|
|
335
|
+
any project that has no project-level override — see
|
|
336
|
+
app/services/registry_credentials.resolve_registry_credential, whose
|
|
337
|
+
real resolution order is project override -> org default -> none.
|
|
338
|
+
|
|
339
|
+
No X-Organization-Id header: get_org_registry is gated by
|
|
340
|
+
ensure_permify_org_access(user, org_id, db), which takes org_id
|
|
341
|
+
straight from the path, not from that header — same as
|
|
342
|
+
list_organizations above, the only other /organizations route this
|
|
343
|
+
module calls.
|
|
344
|
+
"""
|
|
345
|
+
url = f"{config.API_BASE_URL}{config.ORGANIZATIONS_PATH}/{org_id}/registry"
|
|
346
|
+
try:
|
|
347
|
+
resp = client.get(url, headers=_headers(access_token))
|
|
348
|
+
except httpx.HTTPError as exc:
|
|
349
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
350
|
+
_raise_for_status(resp, "fetch the org registry credential status")
|
|
351
|
+
return resp.json()
|
|
352
|
+
|
|
353
|
+
|
|
327
354
|
def set_project_registry(
|
|
328
355
|
client: httpx.Client,
|
|
329
356
|
access_token: str,
|
|
@@ -503,6 +530,47 @@ def deploy_project(
|
|
|
503
530
|
return resp.json()
|
|
504
531
|
|
|
505
532
|
|
|
533
|
+
def get_project_preflight(client: httpx.Client, access_token: str, org_id: int, project_id: int) -> dict:
|
|
534
|
+
"""GET /api/v1/projects/{project_id}/preflight -> preflight.Preflight.as_dict().
|
|
535
|
+
|
|
536
|
+
Read-only (backend/app/features/projects/router.py:1534's
|
|
537
|
+
project_preflight docstring: "nothing here provisions, binds or
|
|
538
|
+
writes"). Returns:
|
|
539
|
+
|
|
540
|
+
{project_id, project_slug, host, can_deploy,
|
|
541
|
+
blocking: [key, ...], warnings: [key, ...],
|
|
542
|
+
checks: [{key, level, title, detail, action}, ...]}
|
|
543
|
+
|
|
544
|
+
(see backend/app/services/preflight.py's Preflight.as_dict; `level` is
|
|
545
|
+
one of "pass"/"warn"/"fail", `action` is None when there's nothing to
|
|
546
|
+
do). `can_deploy` is `not blocking` — exactly the 10 checks
|
|
547
|
+
run_preflight assembles (placement, host_ready, architecture, dns,
|
|
548
|
+
registry, disk, vault_role, site_placement, secrets, observability).
|
|
549
|
+
This is the SAME gate `deploy_project` above already hits reactively as
|
|
550
|
+
a 409 (`_assert_preflight_clear` calls this same `run_preflight`); this
|
|
551
|
+
wrapper lets a caller run it proactively, before committing to a
|
|
552
|
+
deploy, not instead of that reactive path.
|
|
553
|
+
|
|
554
|
+
Does not detect a project's compose/template mismatch or any other
|
|
555
|
+
app-specific misconfiguration — none of the 10 checks cover that class
|
|
556
|
+
of problem. Callers must not describe this as catching it.
|
|
557
|
+
|
|
558
|
+
The backend's own gate fails OPEN: if preflight itself cannot be
|
|
559
|
+
computed (vault down, host lookup error, etc.), the real deploy is not
|
|
560
|
+
blocked. This read-only endpoint has no gate to fail open around, but a
|
|
561
|
+
caller of this wrapper should apply the same philosophy on its own
|
|
562
|
+
side: treat an ApiError here as "could not check", not as a reason to
|
|
563
|
+
block anything itself.
|
|
564
|
+
"""
|
|
565
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}/preflight"
|
|
566
|
+
try:
|
|
567
|
+
resp = client.get(url, headers=_headers(access_token, org_id))
|
|
568
|
+
except httpx.HTTPError as exc:
|
|
569
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
570
|
+
_raise_for_status(resp, "run pre-deploy checks")
|
|
571
|
+
return resp.json()
|
|
572
|
+
|
|
573
|
+
|
|
506
574
|
def list_project_operations(
|
|
507
575
|
client: httpx.Client, access_token: str, org_id: int, project_id: int, *, limit: int = 1
|
|
508
576
|
) -> list[dict]:
|
|
@@ -541,6 +609,54 @@ def get_operation(client: httpx.Client, access_token: str, org_id: int, task_id:
|
|
|
541
609
|
return resp.json()
|
|
542
610
|
|
|
543
611
|
|
|
612
|
+
def create_service_token(
|
|
613
|
+
client: httpx.Client,
|
|
614
|
+
access_token: str,
|
|
615
|
+
org_id: int,
|
|
616
|
+
*,
|
|
617
|
+
name: str,
|
|
618
|
+
token_type: str,
|
|
619
|
+
scopes: list[str],
|
|
620
|
+
project_id: int | None = None,
|
|
621
|
+
expires_at: str | None = None,
|
|
622
|
+
) -> dict:
|
|
623
|
+
"""POST /api/v1/tokens, body identity.token_router.TokenCreate.
|
|
624
|
+
|
|
625
|
+
Authenticated by the caller's regular LOGIN session access_token, never
|
|
626
|
+
a service token — create_service_token (token_router.py:146) is gated
|
|
627
|
+
by get_current_user and explicitly rejects a service-token caller (403
|
|
628
|
+
"Service tokens cannot mint new tokens"), a deliberate anti-escalation
|
|
629
|
+
design. `org_id` is sent as the X-Organization-Id header for
|
|
630
|
+
consistency with every other call in this module, though this route
|
|
631
|
+
does not itself depend on get_current_org: `org_id` in the JSON body
|
|
632
|
+
only matters for an org-scoped token (the backend 400s without it), and
|
|
633
|
+
is ignored/overwritten server-side for a project-scoped one
|
|
634
|
+
(project.organization_id wins over anything the client sends).
|
|
635
|
+
|
|
636
|
+
`project_id`/`expires_at` are omitted from the body entirely when None,
|
|
637
|
+
same "let the backend's own default apply" convention as trigger_build
|
|
638
|
+
above — expires_at omitted means the token never expires (schemas
|
|
639
|
+
default), project_id omitted means an org-scoped token.
|
|
640
|
+
|
|
641
|
+
Returns TokenCreated: the full token record plus `token`, the raw value
|
|
642
|
+
— shown exactly once here, never retrievable from the API again.
|
|
643
|
+
"""
|
|
644
|
+
payload: dict = {"name": name, "type": token_type, "scopes": scopes}
|
|
645
|
+
if project_id is not None:
|
|
646
|
+
payload["project_id"] = project_id
|
|
647
|
+
if token_type == "org":
|
|
648
|
+
payload["org_id"] = org_id
|
|
649
|
+
if expires_at is not None:
|
|
650
|
+
payload["expires_at"] = expires_at
|
|
651
|
+
url = f"{config.API_BASE_URL}{config.TOKENS_PATH}"
|
|
652
|
+
try:
|
|
653
|
+
resp = client.post(url, json=payload, headers=_headers(access_token, org_id))
|
|
654
|
+
except httpx.HTTPError as exc:
|
|
655
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
656
|
+
_raise_for_status(resp, "create the service token")
|
|
657
|
+
return resp.json()
|
|
658
|
+
|
|
659
|
+
|
|
544
660
|
def operation_stream_url(task_id: str) -> str:
|
|
545
661
|
"""URL for GET /api/v1/projects/operations/{task_id}/stream (SSE).
|
|
546
662
|
|
|
@@ -14,6 +14,33 @@ from .cmd_shared import require_link_and_session
|
|
|
14
14
|
app = typer.Typer(add_completion=False)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _print_preflight_notice(preflight: dict) -> None:
|
|
18
|
+
"""Print anything the platform's pre-deploy checks flagged, before the
|
|
19
|
+
confirm prompt. Silent when everything is clean — the happy path stays
|
|
20
|
+
exactly as quiet/fast as it was before this existed, no "10 lines of
|
|
21
|
+
ok" noise.
|
|
22
|
+
|
|
23
|
+
`preflight` is api.get_project_preflight's response
|
|
24
|
+
({can_deploy, checks: [{key, level, title, detail, action}, ...], ...}
|
|
25
|
+
— see that function's docstring for the full shape). Only "warn"/"fail"
|
|
26
|
+
level checks are ever shown; "pass" checks are never printed.
|
|
27
|
+
"""
|
|
28
|
+
checks = preflight.get("checks") or []
|
|
29
|
+
notable = [c for c in checks if c.get("level") in ("fail", "warn")]
|
|
30
|
+
if not notable:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
typer.echo("The platform's pre-deploy checks found something to flag:")
|
|
34
|
+
for check in notable:
|
|
35
|
+
marker = "FAIL" if check.get("level") == "fail" else "WARN"
|
|
36
|
+
title = check.get("title") or check.get("key") or "(unnamed check)"
|
|
37
|
+
detail = check.get("detail") or ""
|
|
38
|
+
typer.echo(f" [{marker}] {title}: {detail}")
|
|
39
|
+
if check.get("action"):
|
|
40
|
+
typer.echo(f" action: {check['action']}")
|
|
41
|
+
typer.echo()
|
|
42
|
+
|
|
43
|
+
|
|
17
44
|
@app.command("status")
|
|
18
45
|
def status_cmd() -> None:
|
|
19
46
|
"""Show the linked project's status and its 5 most recent deployments."""
|
|
@@ -126,6 +153,24 @@ def deploy_cmd(
|
|
|
126
153
|
Only the services you actually gave a tag for are deployed — `services`
|
|
127
154
|
is built from --frontend/--backend, not left to the API's own default of
|
|
128
155
|
both, unless you passed both.
|
|
156
|
+
|
|
157
|
+
Before the confirmation prompt, this also runs the platform's own
|
|
158
|
+
pre-deploy checks (GET .../preflight — the same 10 checks the backend
|
|
159
|
+
already runs reactively on every deploy attempt, covering placement,
|
|
160
|
+
host readiness, architecture, DNS, registry auth, disk, vault role,
|
|
161
|
+
site placement, secrets, and observability) and prints anything that
|
|
162
|
+
isn't clean, so a real, known-to-fail condition is visible before you
|
|
163
|
+
confirm rather than only after a rejected attempt. This is additive:
|
|
164
|
+
the existing reactive handling of a 409 preflight-blocked response
|
|
165
|
+
below is unchanged and still the actual enforcement point — this
|
|
166
|
+
earlier check is informational only and cannot itself refuse the
|
|
167
|
+
deploy. It does not check for a project's compose/template mismatch or
|
|
168
|
+
any other app-specific misconfiguration; that class of problem is
|
|
169
|
+
outside what these 10 checks cover. If the check itself cannot be run
|
|
170
|
+
(network error, backend error), this command notes that plainly and
|
|
171
|
+
still proceeds to the normal confirmation flow — matching the
|
|
172
|
+
backend's own fail-open design for this gate rather than being
|
|
173
|
+
stricter than the platform itself is.
|
|
129
174
|
"""
|
|
130
175
|
if frontend is None and backend is None:
|
|
131
176
|
typer.echo(
|
|
@@ -145,6 +190,17 @@ def deploy_cmd(
|
|
|
145
190
|
with httpx.Client(timeout=30.0) as client:
|
|
146
191
|
current_link, active_session = require_link_and_session(client, cwd)
|
|
147
192
|
|
|
193
|
+
try:
|
|
194
|
+
preflight = api.get_project_preflight(
|
|
195
|
+
client, active_session.access_token, current_link.org_id, current_link.project_id
|
|
196
|
+
)
|
|
197
|
+
except api.ApiError as exc:
|
|
198
|
+
typer.echo(f"Note: {exc} — proceeding without them.")
|
|
199
|
+
except Exception as exc: # noqa: BLE001 - a preflight lookup failure must never block deploy
|
|
200
|
+
typer.echo(f"Note: could not run pre-deploy checks (error: {exc}) — proceeding without them.")
|
|
201
|
+
else:
|
|
202
|
+
_print_preflight_notice(preflight)
|
|
203
|
+
|
|
148
204
|
project_desc = f"{current_link.org_slug}/{current_link.project_slug}"
|
|
149
205
|
typer.echo(f"This will deploy {project_desc} ({environment}):")
|
|
150
206
|
if frontend is not None:
|