mozbridge-cli 0.7.0__tar.gz → 0.9.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.7.0 → mozbridge_cli-0.9.0}/PKG-INFO +1 -1
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/docs/commands.md +139 -10
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/pyproject.toml +1 -1
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/api.py +100 -1
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/auth.py +7 -1
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_deploy.py +40 -2
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_publish.py +160 -21
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_shared.py +14 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/config.py +21 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_deploy.py +26 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_deploy_preflight.py +22 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_diff.py +32 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_login.py +39 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_publish.py +21 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_publish_local.py +22 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_publish_multi_component.py +20 -0
- mozbridge_cli-0.9.0/tests/test_publish_site.py +320 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/.gitignore +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/README.md +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/scripts/release.sh +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/__init__.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/__main__.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/build.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_adopt.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_auth.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_config.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_doctor.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_env.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_projects.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_registry.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_secrets.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_token.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/compose.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/link.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/local_build.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/main.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/runtime_secrets.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/session.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/conftest.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_adopt.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_build.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_ci_token_auth.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_compose.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_config.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_doctor.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_env.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_link.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_local_build.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_login_cli.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_logout.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_logs.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_refresh.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_registry.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_rollback.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_runtime_secrets.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_secrets.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_session_permissions.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_status.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_token.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.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.9.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
|
|
@@ -382,6 +382,96 @@ modes):
|
|
|
382
382
|
|
|
383
383
|
---
|
|
384
384
|
|
|
385
|
+
## `mozbridge publish` — Launch sites (site-shaped projects)
|
|
386
|
+
|
|
387
|
+
Everything under `## \`mozbridge publish\`` above describes the **app-build**
|
|
388
|
+
path. A project can instead be **site-shaped** (a Launch site — static
|
|
389
|
+
hosting only, `config.shape == "site"` on the project, set by `mozbridge
|
|
390
|
+
create-site`/the dashboard's "Launch site" project type) — `publish`
|
|
391
|
+
resolves this automatically and takes a structurally different path for it.
|
|
392
|
+
This section is the site-shaped counterpart to everything above.
|
|
393
|
+
|
|
394
|
+
**Flags**: `--local` — see below, it's rejected outright for a site.
|
|
395
|
+
|
|
396
|
+
**Requires**: same as `publish` (link + login). No compose file / component
|
|
397
|
+
discovery ever runs for a site — a Launch site has no Docker image.
|
|
398
|
+
|
|
399
|
+
**What it does**:
|
|
400
|
+
1. One extra call at the very start of `publish` (for *every* project, app or
|
|
401
|
+
site): `GET /api/v1/projects/{project_id}` to resolve
|
|
402
|
+
`config.shape` — the same one discriminator the backend itself uses
|
|
403
|
+
everywhere (`is_site_project`). This is the only new cost `publish` pays
|
|
404
|
+
for an app-shaped project — one cheap GET before it does exactly what it
|
|
405
|
+
always did.
|
|
406
|
+
2. If `shape == "site"`: zips the current directory — the *exact same*
|
|
407
|
+
`build.build_zip` primitive `mozbridge build` and the app-build path use,
|
|
408
|
+
no separate site-zipping logic — then:
|
|
409
|
+
- `POST /api/v1/projects/{project_id}/publish-uploads` — creates a
|
|
410
|
+
short-lived upload session, returns `{upload_id, put_url, expires_at,
|
|
411
|
+
max_bytes, instructions}` (the site counterpart of `source-uploads`,
|
|
412
|
+
same response shape, different endpoint).
|
|
413
|
+
- `PUT {put_url}` — the raw zip bytes, `Authorization` + `X-Organization-Id`
|
|
414
|
+
+ `Content-Type: application/zip`, same wire contract as the app-build
|
|
415
|
+
upload.
|
|
416
|
+
- `POST /api/v1/projects/{project_id}/publish-from-upload`, body
|
|
417
|
+
`{upload_id, immediate: true}` — consumes the upload and publishes it
|
|
418
|
+
right away. `immediate=true` is what this CLI always sends, for parity
|
|
419
|
+
with how `publish` already behaves for an app-shaped project (no
|
|
420
|
+
separate stage-then-confirm step).
|
|
421
|
+
3. Prints the resulting task id/revision, then — since the
|
|
422
|
+
`publish-from-upload` response itself never carries the site's live URL —
|
|
423
|
+
the project's own `domain` (already known from step 1's `get_project`
|
|
424
|
+
call) as a clearly labeled `Site: https://...` line. This is the actual
|
|
425
|
+
deliverable of a site publish.
|
|
426
|
+
|
|
427
|
+
**Example**:
|
|
428
|
+
```
|
|
429
|
+
$ mozbridge publish
|
|
430
|
+
Built /tmp/mozbridge-build-a1b2.zip (12.4 KB, 4 files)
|
|
431
|
+
Site publish triggered (task_id=abc123, revision=deadbeefcafe).
|
|
432
|
+
Site: https://launchpage.mozbridge.com
|
|
433
|
+
Run `mozbridge status` to check progress, or poll the Mozbridge dashboard.
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**`--local` against a site fails immediately**: `--local` means "build and
|
|
437
|
+
push a Docker image on this machine" — meaningless for a site, which has no
|
|
438
|
+
Docker image at all. `_publish_local` checks the project's shape right after
|
|
439
|
+
resolving the login session (still before `MOZBRIDGE_BUILD_TOKEN`'s
|
|
440
|
+
runtime-secrets fetch, `docker login`, or any build/push) and, if the
|
|
441
|
+
project is a site, fails immediately:
|
|
442
|
+
```
|
|
443
|
+
$ mozbridge publish --local
|
|
444
|
+
--local is for app builds; sites don't use Docker — run `mozbridge publish`
|
|
445
|
+
without --local.
|
|
446
|
+
```
|
|
447
|
+
No docker command and no HTTP call beyond the shape check itself is ever
|
|
448
|
+
attempted.
|
|
449
|
+
|
|
450
|
+
**Failure modes** (in addition to `publish`'s own not-linked/not-logged-in
|
|
451
|
+
modes):
|
|
452
|
+
- The shape-check `get_project` call itself fails: the raw `ApiError`
|
|
453
|
+
message (same pattern as every other command), exit 1 — nothing else is
|
|
454
|
+
attempted.
|
|
455
|
+
- `create_publish_upload` fails: `Could not create a publish upload
|
|
456
|
+
session: <backend detail>`, exit 1.
|
|
457
|
+
- The `PUT` upload fails: `Could not upload the site content: <backend
|
|
458
|
+
detail>`, exit 1 — `publish-from-upload` is never called if the upload
|
|
459
|
+
failed.
|
|
460
|
+
- `publish-from-upload` itself fails: `Could not publish the site: <backend
|
|
461
|
+
detail>`, exit 1.
|
|
462
|
+
- `--local` against a site: the clear message above, exit 1, no docker or
|
|
463
|
+
HTTP call beyond the shape check.
|
|
464
|
+
|
|
465
|
+
**Known gap**: `mozbridge status`'s "recent deployments" list stays empty
|
|
466
|
+
for a site even after real publishes — see `## \`mozbridge status\`` below.
|
|
467
|
+
`mozbridge diff` describes the site-publish path in plain language instead
|
|
468
|
+
of the compose/component analysis (see `## \`mozbridge diff\`` below).
|
|
469
|
+
`mozbridge deploy` refuses outright for a site (see `## \`mozbridge
|
|
470
|
+
deploy\`` below) — `mozbridge rollback` is unaffected, it already works
|
|
471
|
+
identically for both shapes via one unified backend endpoint.
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
385
475
|
## `mozbridge diff`
|
|
386
476
|
|
|
387
477
|
A genuine dry-run/preview: shows what `mozbridge publish` would do right
|
|
@@ -393,15 +483,23 @@ now, without doing anything. Makes **no** mutating API call — GET-only
|
|
|
393
483
|
**Requires**: link + login, same as `publish` and `status`.
|
|
394
484
|
|
|
395
485
|
**What it does**:
|
|
396
|
-
1. `GET /api/v1/projects/{project_id}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
`
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
486
|
+
1. `GET /api/v1/projects/{project_id}` to resolve the project (and its
|
|
487
|
+
shape — see the site-shaped behavior below), and `GET
|
|
488
|
+
/api/v1/projects/{project_id}/deployments` — same call `status` makes —
|
|
489
|
+
and prints the single most recent deployment record for context (id,
|
|
490
|
+
timestamp, `component=version`, status, `[stable]`).
|
|
491
|
+
2. **Site-shaped project**: stops here and prints, in plain language, what
|
|
492
|
+
`mozbridge publish` would actually do for a site (zip the directory,
|
|
493
|
+
create a publish-upload session, PUT the bytes, publish-from-upload with
|
|
494
|
+
`immediate=true`) — none of the compose/component analysis below applies,
|
|
495
|
+
a Launch site has no Docker image to build. See `## \`mozbridge publish\`
|
|
496
|
+
— Launch sites` above for the real flow this describes.
|
|
497
|
+
3. **App-shaped project** (unchanged): discovers a compose file exactly as
|
|
498
|
+
`publish` does (reusing `compose.py`). If none is found, or one is found
|
|
499
|
+
but none of its services have a `build:` key, it says plainly that
|
|
500
|
+
`publish` would fall back to the single-component path (zip the whole
|
|
501
|
+
directory) — this is reported, not treated as an error.
|
|
502
|
+
4. If components are found, prints each one's resolved
|
|
405
503
|
`image_name`/`context_path`/`dockerfile_path` — computed by the exact
|
|
406
504
|
same `compose.resolve_components` function `publish` calls to build its
|
|
407
505
|
`trigger_build` request bodies, so `diff`'s output can never silently
|
|
@@ -487,6 +585,14 @@ only when `is_stable` is true on that deployment. If there are no
|
|
|
487
585
|
deployments at all, the project line still prints, followed by `No
|
|
488
586
|
deployments yet.`
|
|
489
587
|
|
|
588
|
+
**KNOWN GAP — Launch sites**: `/deployments` only ever returns rows written
|
|
589
|
+
by the app-build deploy task; a site's own publish path never writes one.
|
|
590
|
+
For a site-shaped project this command currently prints `No deployments
|
|
591
|
+
yet.` even after several real publishes, even though the project's own
|
|
592
|
+
config tracks its actual revision history. Not yet fixed — see the CLI
|
|
593
|
+
site-project-support notes rather than assuming this is a bug specific to
|
|
594
|
+
your project.
|
|
595
|
+
|
|
490
596
|
**Failure modes**:
|
|
491
597
|
- Not linked / not logged in: identical messages to `publish` above,
|
|
492
598
|
exit 1.
|
|
@@ -500,6 +606,11 @@ deployments yet.`
|
|
|
500
606
|
|
|
501
607
|
Rolls back the linked project's deployment. A real, consequential
|
|
502
608
|
production action — prompts for confirmation unless `--yes` is given.
|
|
609
|
+
Unlike `publish`/`deploy`, this command needs **no shape check** and no
|
|
610
|
+
change for this feature: the backend already unifies app- and site-shaped
|
|
611
|
+
rollback behind these same two endpoints
|
|
612
|
+
(`rollback_project_or_site`/`rollback_to_specific`), so it already works
|
|
613
|
+
identically for both.
|
|
503
614
|
|
|
504
615
|
**Flags**:
|
|
505
616
|
| Flag | Meaning |
|
|
@@ -567,7 +678,21 @@ confirmation unless `--yes` is given.
|
|
|
567
678
|
one of `--frontend`/`--backend` must be given — deploying neither makes no
|
|
568
679
|
sense.
|
|
569
680
|
|
|
570
|
-
**
|
|
681
|
+
**Not applicable to a Launch site**: a site has no image tag to deploy at
|
|
682
|
+
all. Right after resolving the link/session — before the preflight check,
|
|
683
|
+
before the confirmation prompt, before any deploy request — `deploy` checks
|
|
684
|
+
the linked project's shape (`GET /api/v1/projects/{project_id}`) and, if
|
|
685
|
+
it's a site, fails immediately:
|
|
686
|
+
```
|
|
687
|
+
$ mozbridge deploy --backend v1.9.319 --yes
|
|
688
|
+
This project is a Launch site — sites deploy via `mozbridge publish`, not
|
|
689
|
+
`mozbridge deploy` (there is no image tag to deploy for a site). Run
|
|
690
|
+
`mozbridge publish` instead.
|
|
691
|
+
```
|
|
692
|
+
No preflight call and no deploy request is ever sent for a site — this
|
|
693
|
+
never becomes a confusing raw 400 from the backend.
|
|
694
|
+
|
|
695
|
+
**What it does** (app-shaped project): prints what it's about to do (project, which service(s),
|
|
571
696
|
which tag(s), which environment), then either aborts on a declined prompt or
|
|
572
697
|
calls `POST /api/v1/projects/{project_id}/deploy` with
|
|
573
698
|
`{frontend_version, backend_version, services, environment, force}`.
|
|
@@ -620,6 +745,10 @@ Re-run with --force to override — this is not done automatically.
|
|
|
620
745
|
- Neither `--frontend` nor `--backend` given: clear error, exit 1, no API
|
|
621
746
|
call is made.
|
|
622
747
|
- Not linked / not logged in: identical messages to `publish` above, exit 1.
|
|
748
|
+
- The shape-check `get_project` call itself fails: the raw `ApiError`
|
|
749
|
+
message, exit 1 — nothing else is attempted.
|
|
750
|
+
- Linked project is a Launch site: the clear message above, exit 1, no
|
|
751
|
+
preflight/deploy call ever sent.
|
|
623
752
|
- Confirmation prompt declined (`--yes` not passed, answer is not `y`):
|
|
624
753
|
Typer's own `Abort` handling — the command exits non-zero without calling
|
|
625
754
|
the API.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mozbridge-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.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"
|
|
@@ -134,7 +134,16 @@ def put_source_upload_bytes(
|
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
def get_project(client: httpx.Client, access_token: str, org_id: int, project_id: int) -> dict:
|
|
137
|
-
"""GET /api/v1/projects/{project_id} — schemas.Project (name/slug/status/config/...).
|
|
137
|
+
"""GET /api/v1/projects/{project_id} — schemas.Project (name/slug/status/config/...).
|
|
138
|
+
|
|
139
|
+
`config` (a plain dict, see schemas.Project) is what carries the project's
|
|
140
|
+
"shape" — `(project.get("config") or {}).get("shape") == "site"` for a
|
|
141
|
+
Launch site, absent/anything else for an app build. This is the SAME
|
|
142
|
+
discriminator the backend itself uses everywhere
|
|
143
|
+
(`app/services/site_service.py:is_site_project`), so callers needing to
|
|
144
|
+
branch on shape (see cmd_shared.is_site_project) should call this rather
|
|
145
|
+
than adding a second, parallel "get the shape" endpoint.
|
|
146
|
+
"""
|
|
138
147
|
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}"
|
|
139
148
|
try:
|
|
140
149
|
resp = client.get(url, headers=_headers(access_token, org_id))
|
|
@@ -144,6 +153,96 @@ def get_project(client: httpx.Client, access_token: str, org_id: int, project_id
|
|
|
144
153
|
return resp.json()
|
|
145
154
|
|
|
146
155
|
|
|
156
|
+
def create_publish_upload(client: httpx.Client, access_token: str, org_id: int, project_id: int) -> dict:
|
|
157
|
+
"""POST /api/v1/projects/{project_id}/publish-uploads — Launch-site counterpart to
|
|
158
|
+
`create_source_upload` above.
|
|
159
|
+
|
|
160
|
+
Sites don't get a Docker build — they get a static-file publish
|
|
161
|
+
(`backend/app/features/projects/router.py`'s `create_publish_upload`,
|
|
162
|
+
~line 464), 400s if the project isn't `shape=site` (`is_site_project`).
|
|
163
|
+
|
|
164
|
+
Returns the SAME shape as `create_source_upload`
|
|
165
|
+
({upload_id, put_url, expires_at, max_bytes, instructions}) — the backend
|
|
166
|
+
reuses `schemas.SitePublishUploadSession` for both, so this is a distinct
|
|
167
|
+
function (not an alias) only because it hits a distinct, site-only route.
|
|
168
|
+
"""
|
|
169
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}/publish-uploads"
|
|
170
|
+
try:
|
|
171
|
+
resp = client.post(url, headers=_headers(access_token, org_id))
|
|
172
|
+
except httpx.HTTPError as exc:
|
|
173
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
174
|
+
_raise_for_status(resp, "create a publish upload session")
|
|
175
|
+
return resp.json()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def put_publish_upload_bytes(
|
|
179
|
+
client: httpx.Client,
|
|
180
|
+
access_token: str,
|
|
181
|
+
org_id: int,
|
|
182
|
+
put_url: str,
|
|
183
|
+
data: bytes,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""PUT the zip bytes to the put_url returned by create_publish_upload.
|
|
186
|
+
|
|
187
|
+
Same wire contract as `put_source_upload_bytes` above (Authorization +
|
|
188
|
+
X-Organization-Id + `application/zip` body) — the backend's
|
|
189
|
+
`put_publish_upload` route (router.py ~line 504) accepts the raw body the
|
|
190
|
+
same way `put_source_upload` does. Kept as a separate function (rather
|
|
191
|
+
than reusing `put_source_upload_bytes` directly) purely so a failure here
|
|
192
|
+
reads as "upload the site content", not "upload the build source" — the
|
|
193
|
+
two flows are conceptually distinct even though the bytes-on-the-wire
|
|
194
|
+
shape is identical.
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
resp = client.put(
|
|
198
|
+
put_url,
|
|
199
|
+
content=data,
|
|
200
|
+
headers={**_headers(access_token, org_id), "Content-Type": "application/zip"},
|
|
201
|
+
)
|
|
202
|
+
except httpx.HTTPError as exc:
|
|
203
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
204
|
+
_raise_for_status(resp, "upload the site content")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def publish_from_upload(
|
|
208
|
+
client: httpx.Client,
|
|
209
|
+
access_token: str,
|
|
210
|
+
org_id: int,
|
|
211
|
+
project_id: int,
|
|
212
|
+
upload_id: str,
|
|
213
|
+
*,
|
|
214
|
+
immediate: bool = True,
|
|
215
|
+
) -> dict:
|
|
216
|
+
"""POST /api/v1/projects/{project_id}/publish-from-upload, body schemas.SitePublishFromUpload.
|
|
217
|
+
|
|
218
|
+
Consumes a ready `create_publish_upload` session and ingests it exactly
|
|
219
|
+
like the multipart publish endpoint (router.py's `publish_site_from_upload`,
|
|
220
|
+
~line 612) — 400s if the project isn't `shape=site`.
|
|
221
|
+
|
|
222
|
+
`immediate` defaults True: this CLI's `publish` command always calls with
|
|
223
|
+
immediate=True, matching how `publish` already behaves for app-shaped
|
|
224
|
+
projects (no separate stage-then-confirm step) — see cmd_publish.py's
|
|
225
|
+
`_publish_site`. With immediate=True the revision is deployed right away
|
|
226
|
+
and the response's `status` comes back "queued" with `preview_url` unset;
|
|
227
|
+
with immediate=False (not currently used by this CLI, kept as a real
|
|
228
|
+
parameter rather than hardcoding True so a future `--stage` flag doesn't
|
|
229
|
+
need a new function) the revision is only staged, `status` comes back
|
|
230
|
+
"staged" and `preview_url` is populated instead.
|
|
231
|
+
|
|
232
|
+
Returns schemas.SitePublishResponse: {task_id, revision, status,
|
|
233
|
+
preview_url}. Note this response never carries the site's live URL —
|
|
234
|
+
that's `project.domain` (from `get_project`), not anything in this body.
|
|
235
|
+
"""
|
|
236
|
+
url = f"{config.API_BASE_URL}{config.PROJECTS_PATH}/{project_id}/publish-from-upload"
|
|
237
|
+
payload = {"upload_id": upload_id, "immediate": immediate}
|
|
238
|
+
try:
|
|
239
|
+
resp = client.post(url, json=payload, headers=_headers(access_token, org_id))
|
|
240
|
+
except httpx.HTTPError as exc:
|
|
241
|
+
raise ApiError(f"Could not reach Mozbridge API: {exc}") from exc
|
|
242
|
+
_raise_for_status(resp, "publish the site")
|
|
243
|
+
return resp.json()
|
|
244
|
+
|
|
245
|
+
|
|
147
246
|
def list_project_deployments(
|
|
148
247
|
client: httpx.Client, access_token: str, org_id: int, project_id: int
|
|
149
248
|
) -> list[dict]:
|
|
@@ -64,7 +64,11 @@ class DeviceAuthorization:
|
|
|
64
64
|
def start_device_authorization(client: httpx.Client) -> DeviceAuthorization:
|
|
65
65
|
resp = client.post(
|
|
66
66
|
config.DEVICE_AUTH_URL,
|
|
67
|
-
data={
|
|
67
|
+
data={
|
|
68
|
+
"client_id": config.LOGTO_CLIENT_ID,
|
|
69
|
+
"scope": config.DEVICE_SCOPE,
|
|
70
|
+
"resource": config.LOGTO_RESOURCE,
|
|
71
|
+
},
|
|
68
72
|
)
|
|
69
73
|
resp.raise_for_status()
|
|
70
74
|
data = resp.json()
|
|
@@ -89,6 +93,7 @@ def _poll_once(client: httpx.Client, device_code: str) -> dict | None:
|
|
|
89
93
|
"client_id": config.LOGTO_CLIENT_ID,
|
|
90
94
|
"grant_type": GRANT_TYPE_DEVICE_CODE,
|
|
91
95
|
"device_code": device_code,
|
|
96
|
+
"resource": config.LOGTO_RESOURCE,
|
|
92
97
|
},
|
|
93
98
|
)
|
|
94
99
|
if resp.status_code == 200:
|
|
@@ -166,6 +171,7 @@ def refresh_access_token(client: httpx.Client, refresh_token: str) -> dict:
|
|
|
166
171
|
"grant_type": "refresh_token",
|
|
167
172
|
"refresh_token": refresh_token,
|
|
168
173
|
"client_id": config.LOGTO_CLIENT_ID,
|
|
174
|
+
"resource": config.LOGTO_RESOURCE,
|
|
169
175
|
},
|
|
170
176
|
)
|
|
171
177
|
if resp.status_code == 200:
|
|
@@ -9,7 +9,7 @@ import httpx
|
|
|
9
9
|
import typer
|
|
10
10
|
|
|
11
11
|
from . import api
|
|
12
|
-
from .cmd_shared import require_link_and_session
|
|
12
|
+
from .cmd_shared import is_site_project, require_link_and_session
|
|
13
13
|
|
|
14
14
|
app = typer.Typer(add_completion=False)
|
|
15
15
|
|
|
@@ -43,7 +43,20 @@ def _print_preflight_notice(preflight: dict) -> None:
|
|
|
43
43
|
|
|
44
44
|
@app.command("status")
|
|
45
45
|
def status_cmd() -> None:
|
|
46
|
-
"""Show the linked project's status and its 5 most recent deployments.
|
|
46
|
+
"""Show the linked project's status and its 5 most recent deployments.
|
|
47
|
+
|
|
48
|
+
KNOWN GAP for a site-shaped (Launch site) project: `list_project_deployments`
|
|
49
|
+
only ever returns rows written by the app-build deploy task
|
|
50
|
+
(`models.ProjectDeployment`, created in `app/tasks.py`'s `deploy_project`
|
|
51
|
+
task) — a site's own publish path (`SiteService.ingest_zip` /
|
|
52
|
+
`_enqueue_deploy`, driven by `publish_static_site`) never writes one. So
|
|
53
|
+
for a site this command will print "No deployments yet." even after
|
|
54
|
+
several real publishes, even though the project's `config` (last_revision/
|
|
55
|
+
staged_revision/stable_revision) shows otherwise. Left unfixed
|
|
56
|
+
deliberately in this slice — see the CLI site-project-support docs for the
|
|
57
|
+
full reasoning — rather than guessed at without a confirmed design for
|
|
58
|
+
what a site's own "recent activity" list should look like.
|
|
59
|
+
"""
|
|
47
60
|
cwd = Path.cwd()
|
|
48
61
|
|
|
49
62
|
with httpx.Client(timeout=30.0) as client:
|
|
@@ -171,6 +184,14 @@ def deploy_cmd(
|
|
|
171
184
|
still proceeds to the normal confirmation flow — matching the
|
|
172
185
|
backend's own fail-open design for this gate rather than being
|
|
173
186
|
stricter than the platform itself is.
|
|
187
|
+
|
|
188
|
+
Not applicable to a site-shaped (Launch site) project: a site has no
|
|
189
|
+
image tag to deploy at all — `--frontend`/`--backend` are meaningless for
|
|
190
|
+
it. If the linked project turns out to be a site, this command fails
|
|
191
|
+
immediately (right after resolving the link/session, before the preflight
|
|
192
|
+
check or any deploy request) with a clear error pointing at `mozbridge
|
|
193
|
+
publish` instead — it never sends the nonsensical deploy request to the
|
|
194
|
+
backend and lets a raw API error stand in for that explanation.
|
|
174
195
|
"""
|
|
175
196
|
if frontend is None and backend is None:
|
|
176
197
|
typer.echo(
|
|
@@ -190,6 +211,23 @@ def deploy_cmd(
|
|
|
190
211
|
with httpx.Client(timeout=30.0) as client:
|
|
191
212
|
current_link, active_session = require_link_and_session(client, cwd)
|
|
192
213
|
|
|
214
|
+
try:
|
|
215
|
+
project = api.get_project(
|
|
216
|
+
client, active_session.access_token, current_link.org_id, current_link.project_id
|
|
217
|
+
)
|
|
218
|
+
except api.ApiError as exc:
|
|
219
|
+
typer.echo(str(exc), err=True)
|
|
220
|
+
raise typer.Exit(code=1)
|
|
221
|
+
|
|
222
|
+
if is_site_project(project):
|
|
223
|
+
typer.echo(
|
|
224
|
+
"This project is a Launch site — sites deploy via `mozbridge publish`, "
|
|
225
|
+
"not `mozbridge deploy` (there is no image tag to deploy for a site). "
|
|
226
|
+
"Run `mozbridge publish` instead.",
|
|
227
|
+
err=True,
|
|
228
|
+
)
|
|
229
|
+
raise typer.Exit(code=1)
|
|
230
|
+
|
|
193
231
|
try:
|
|
194
232
|
preflight = api.get_project_preflight(
|
|
195
233
|
client, active_session.access_token, current_link.org_id, current_link.project_id
|