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.
Files changed (60) hide show
  1. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/PKG-INFO +1 -1
  2. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/docs/commands.md +139 -10
  3. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/pyproject.toml +1 -1
  4. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/api.py +100 -1
  5. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/auth.py +7 -1
  6. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_deploy.py +40 -2
  7. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_publish.py +160 -21
  8. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_shared.py +14 -0
  9. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/config.py +21 -0
  10. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_deploy.py +26 -0
  11. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_deploy_preflight.py +22 -0
  12. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_diff.py +32 -0
  13. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_login.py +39 -0
  14. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_publish.py +21 -0
  15. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_publish_local.py +22 -0
  16. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_publish_multi_component.py +20 -0
  17. mozbridge_cli-0.9.0/tests/test_publish_site.py +320 -0
  18. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/.gitignore +0 -0
  19. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/README.md +0 -0
  20. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/scripts/release.sh +0 -0
  21. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/__init__.py +0 -0
  22. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/__main__.py +0 -0
  23. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/build.py +0 -0
  24. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_adopt.py +0 -0
  25. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_auth.py +0 -0
  26. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_config.py +0 -0
  27. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_doctor.py +0 -0
  28. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_env.py +0 -0
  29. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_projects.py +0 -0
  30. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_registry.py +0 -0
  31. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_secrets.py +0 -0
  32. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/cmd_token.py +0 -0
  33. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/compose.py +0 -0
  34. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/link.py +0 -0
  35. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/local_build.py +0 -0
  36. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/main.py +0 -0
  37. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/runtime_secrets.py +0 -0
  38. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/src/mozbridge_cli/session.py +0 -0
  39. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/conftest.py +0 -0
  40. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_adopt.py +0 -0
  41. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_build.py +0 -0
  42. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_ci_token_auth.py +0 -0
  43. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_compose.py +0 -0
  44. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_config.py +0 -0
  45. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_doctor.py +0 -0
  46. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_env.py +0 -0
  47. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_link.py +0 -0
  48. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_local_build.py +0 -0
  49. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_login_cli.py +0 -0
  50. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_logout.py +0 -0
  51. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_logs.py +0 -0
  52. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_refresh.py +0 -0
  53. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_registry.py +0 -0
  54. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_rollback.py +0 -0
  55. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_runtime_secrets.py +0 -0
  56. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_secrets.py +0 -0
  57. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_session_permissions.py +0 -0
  58. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_status.py +0 -0
  59. {mozbridge_cli-0.7.0 → mozbridge_cli-0.9.0}/tests/test_token.py +0 -0
  60. {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.7.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}/deployments` — same call `status`
397
- makes — and prints the single most recent deployment record for
398
- context (id, timestamp, `component=version`, status, `[stable]`).
399
- 2. Discovers a compose file exactly as `publish` does (reusing
400
- `compose.py`). If none is found, or one is found but none of its
401
- services have a `build:` key, it says plainly that `publish` would fall
402
- back to the single-component path (zip the whole directory) — this is
403
- reported, not treated as an error.
404
- 3. If components are found, prints each one's resolved
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
- **What it does**: prints what it's about to do (project, which service(s),
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.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={"client_id": config.LOGTO_CLIENT_ID, "scope": config.DEVICE_SCOPE},
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