mozbridge-cli 0.2.1__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.2.1 → mozbridge_cli-0.4.0}/PKG-INFO +8 -6
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/README.md +7 -5
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/docs/commands.md +214 -6
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/pyproject.toml +1 -1
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/scripts/release.sh +17 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/api.py +232 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_auth.py +152 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_deploy.py +363 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_doctor.py +229 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_env.py +115 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_projects.py +92 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_publish.py +498 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_registry.py +134 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_secrets.py +165 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_shared.py +31 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/cmd_token.py +180 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/config.py +7 -0
- mozbridge_cli-0.4.0/src/mozbridge_cli/main.py +64 -0
- 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.4.0/tests/test_registry.py +293 -0
- mozbridge_cli-0.4.0/tests/test_secrets.py +294 -0
- mozbridge_cli-0.4.0/tests/test_token.py +275 -0
- mozbridge_cli-0.2.1/src/mozbridge_cli/main.py +0 -1142
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/.gitignore +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/__init__.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/__main__.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/auth.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/build.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/compose.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/link.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/local_build.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/runtime_secrets.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/src/mozbridge_cli/session.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/conftest.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_build.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_ci_token_auth.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_compose.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_deploy.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_diff.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_env.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_link.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_local_build.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_login.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_login_cli.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_logout.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_logs.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_publish.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_publish_local.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_publish_multi_component.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_refresh.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_rollback.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_runtime_secrets.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_session_permissions.py +0 -0
- {mozbridge_cli-0.2.1 → mozbridge_cli-0.4.0}/tests/test_status.py +0 -0
- {mozbridge_cli-0.2.1 → 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"
|
|
@@ -10,8 +10,25 @@
|
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
|
|
12
12
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
13
|
+
REPO_ROOT="$(cd "$ROOT/.." && pwd)"
|
|
13
14
|
PYPROJECT="$ROOT/pyproject.toml"
|
|
14
15
|
|
|
16
|
+
# Hard first check: `git fetch` updates the remote-tracking ref, not the
|
|
17
|
+
# working tree — only merge/rebase/pull do that. mozbridge-cli 0.2.0 was
|
|
18
|
+
# published missing 3 commands because this build ran between a `git
|
|
19
|
+
# fetch` and the merge that would have actually updated the checkout.
|
|
20
|
+
# Refuse to build from a checkout that's behind origin/main.
|
|
21
|
+
if command -v git >/dev/null 2>&1 && git -C "$REPO_ROOT" rev-parse --git-dir >/dev/null 2>&1; then
|
|
22
|
+
git -C "$REPO_ROOT" fetch origin main --quiet 2>/dev/null || true
|
|
23
|
+
BEHIND="$(git -C "$REPO_ROOT" rev-list --count HEAD..origin/main 2>/dev/null || echo 0)"
|
|
24
|
+
if [ "${BEHIND:-0}" != "0" ]; then
|
|
25
|
+
echo "✗ Local main is ${BEHIND} commit(s) behind origin/main." >&2
|
|
26
|
+
echo " Run: git -C \"$REPO_ROOT\" merge --ff-only origin/main" >&2
|
|
27
|
+
echo " then re-run this script. Refusing to build a stale release." >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
|
|
15
32
|
usage() {
|
|
16
33
|
echo "Usage: $0 [--patch|--minor|--major] [--dry-run]" >&2
|
|
17
34
|
echo " Default bump: patch" >&2
|
|
@@ -308,6 +308,149 @@ def delete_project_env_var(
|
|
|
308
308
|
return resp.json()
|
|
309
309
|
|
|
310
310
|
|
|
311
|
+
def get_project_registry(client: httpx.Client, access_token: str, org_id: int, project_id: int) -> dict:
|
|
312
|
+
"""GET /api/v1/projects/{project_id}/registry -> schemas.RegistryCredentialStatus.
|
|
313
|
+
|
|
314
|
+
{configured, registry_url, username, updated_at} — the token/secret value
|
|
315
|
+
is never returned by this route (router.py:2343-2352), by design, so
|
|
316
|
+
there is nothing here for a caller to accidentally print.
|
|
317
|
+
"""
|
|
318
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}/registry"
|
|
319
|
+
try:
|
|
320
|
+
resp = client.get(url, headers=_headers(access_token, org_id))
|
|
321
|
+
except httpx.HTTPError as exc:
|
|
322
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
323
|
+
_raise_for_status(resp, "fetch the registry credential status")
|
|
324
|
+
return resp.json()
|
|
325
|
+
|
|
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
|
+
|
|
354
|
+
def set_project_registry(
|
|
355
|
+
client: httpx.Client,
|
|
356
|
+
access_token: str,
|
|
357
|
+
org_id: int,
|
|
358
|
+
project_id: int,
|
|
359
|
+
registry_url: str,
|
|
360
|
+
username: str,
|
|
361
|
+
token: str,
|
|
362
|
+
) -> dict:
|
|
363
|
+
"""PUT /api/v1/projects/{project_id}/registry, body schemas.RegistryCredentialSet.
|
|
364
|
+
|
|
365
|
+
Admin-gated (ensure_permify_can_administer) and validated against the
|
|
366
|
+
real registry BEFORE storing (router.py:2355-2384) — a bad credential
|
|
367
|
+
comes back as a 400 with a real message ("Could not validate registry
|
|
368
|
+
credential: ..."), which `_raise_for_status` already surfaces via
|
|
369
|
+
ApiError, same as every other function in this module.
|
|
370
|
+
"""
|
|
371
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}/registry"
|
|
372
|
+
payload = {"registry_url": registry_url, "username": username, "token": token}
|
|
373
|
+
try:
|
|
374
|
+
resp = client.put(url, json=payload, headers=_headers(access_token, org_id))
|
|
375
|
+
except httpx.HTTPError as exc:
|
|
376
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
377
|
+
_raise_for_status(resp, "set the registry credential")
|
|
378
|
+
return resp.json()
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _secrets_url(project_id: int, env_slug: str, component: str) -> str:
|
|
382
|
+
"""Shared path builder for the four secrets endpoints below — keeps the
|
|
383
|
+
env_slug/component path segments byte-for-byte identical across list
|
|
384
|
+
keys / get values / patch one key, rather than four separately-typed
|
|
385
|
+
f-strings that could drift apart.
|
|
386
|
+
"""
|
|
387
|
+
base = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}"
|
|
388
|
+
return f"{base}/environments/{env_slug}/secrets/{component}"
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def list_env_secret_keys(
|
|
392
|
+
client: httpx.Client, access_token: str, org_id: int, project_id: int, env_slug: str, component: str
|
|
393
|
+
) -> dict:
|
|
394
|
+
"""GET .../environments/{env_slug}/secrets/{component}/keys -> {keys, count}.
|
|
395
|
+
|
|
396
|
+
Key NAMES only — no values leave the server (router.py:3422-3434). This
|
|
397
|
+
is the default listing behavior for `mozbridge secrets list`.
|
|
398
|
+
"""
|
|
399
|
+
url = f"{_secrets_url(project_id, env_slug, component)}/keys"
|
|
400
|
+
try:
|
|
401
|
+
resp = client.get(url, headers=_headers(access_token, org_id))
|
|
402
|
+
except httpx.HTTPError as exc:
|
|
403
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
404
|
+
_raise_for_status(resp, "list secret keys")
|
|
405
|
+
return resp.json()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def get_env_secrets(
|
|
409
|
+
client: httpx.Client, access_token: str, org_id: int, project_id: int, env_slug: str, component: str
|
|
410
|
+
) -> dict:
|
|
411
|
+
"""GET .../environments/{env_slug}/secrets/{component} -> dict[key, value].
|
|
412
|
+
|
|
413
|
+
Returns actual secret VALUES (router.py:3412-3419). Only called from
|
|
414
|
+
`mozbridge secrets list --show-values`, gated behind an interactive
|
|
415
|
+
confirmation (or --yes) before this is ever invoked.
|
|
416
|
+
"""
|
|
417
|
+
url = _secrets_url(project_id, env_slug, component)
|
|
418
|
+
try:
|
|
419
|
+
resp = client.get(url, headers=_headers(access_token, org_id))
|
|
420
|
+
except httpx.HTTPError as exc:
|
|
421
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
422
|
+
_raise_for_status(resp, "fetch secret values")
|
|
423
|
+
return resp.json()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def patch_env_secret(
|
|
427
|
+
client: httpx.Client,
|
|
428
|
+
access_token: str,
|
|
429
|
+
org_id: int,
|
|
430
|
+
project_id: int,
|
|
431
|
+
env_slug: str,
|
|
432
|
+
component: str,
|
|
433
|
+
key: str,
|
|
434
|
+
value: str,
|
|
435
|
+
) -> dict:
|
|
436
|
+
"""PATCH .../environments/{env_slug}/secrets/{component}, body schemas.SecretPatch {key, value}.
|
|
437
|
+
|
|
438
|
+
Merges ONE key server-side over a strict read (router.py:3437-3453) —
|
|
439
|
+
the safe way to set one secret without touching the rest. `mozbridge
|
|
440
|
+
secrets set` MUST use this, never the POST replace-all endpoint below.
|
|
441
|
+
|
|
442
|
+
Returns {"status": "success", "key", "created", "total_keys"} — never
|
|
443
|
+
the merged secret values.
|
|
444
|
+
"""
|
|
445
|
+
url = _secrets_url(project_id, env_slug, component)
|
|
446
|
+
try:
|
|
447
|
+
resp = client.patch(url, json={"key": key, "value": value}, headers=_headers(access_token, org_id))
|
|
448
|
+
except httpx.HTTPError as exc:
|
|
449
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
450
|
+
_raise_for_status(resp, "set the secret")
|
|
451
|
+
return resp.json()
|
|
452
|
+
|
|
453
|
+
|
|
311
454
|
def trigger_prebuilt_build(
|
|
312
455
|
client: httpx.Client,
|
|
313
456
|
access_token: str,
|
|
@@ -387,6 +530,47 @@ def deploy_project(
|
|
|
387
530
|
return resp.json()
|
|
388
531
|
|
|
389
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
|
+
|
|
390
574
|
def list_project_operations(
|
|
391
575
|
client: httpx.Client, access_token: str, org_id: int, project_id: int, *, limit: int = 1
|
|
392
576
|
) -> list[dict]:
|
|
@@ -425,6 +609,54 @@ def get_operation(client: httpx.Client, access_token: str, org_id: int, task_id:
|
|
|
425
609
|
return resp.json()
|
|
426
610
|
|
|
427
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
|
+
|
|
428
660
|
def operation_stream_url(task_id: str) -> str:
|
|
429
661
|
"""URL for GET /api/v1/projects/operations/{task_id}/stream (SSE).
|
|
430
662
|
|