mozbridge-cli
A laptop CLI for the Mozbridge deployment platform. You sign in once via
browser-based SSO (Logto's device-authorization flow — no password, no
long-lived API key typed into a dotfile), link a local directory to a
Mozbridge org + project, and push builds from there. It never holds a
production credential of its own: every request it makes rides the same
human Logto session mozbridge login cached, hitting the same REST API the
Mozbridge web dashboard uses.
macOS-only for v1 — there are no Windows code paths.
Install
There is no published package yet (no PyPI entry, no mozbridge-cli on any
index) — install from this checkout:
pip install -e cli/
# or, for an isolated global install:
pipx install ./cli
Either one registers a mozbridge console script (from
cli/pyproject.toml's [project.scripts]: mozbridge = "mozbridge_cli.main:app_main"). Confirm it's on your PATH:
mozbridge --help
Requires Python >= 3.10.
Quickstart
mozbridge login # opens a browser to sign in
cd ~/code/my-project
mozbridge link --org acme --project web-app # bind this directory
mozbridge publish # build + upload + trigger a build
mozbridge status # watch it land
Run mozbridge link with no flags to pick org/project from a numbered list
interactively instead of naming slugs up front.
Commands
mozbridge login # device-code SSO via Logto
mozbridge whoami # show the signed-in identity (refreshes the token if needed)
mozbridge logout # delete the cached local session
mozbridge link [--org SLUG] [--project SLUG] # link this directory to a Mozbridge org + project
mozbridge build # zip the current directory (does not upload)
mozbridge publish # build + upload + trigger a build on the linked project
mozbridge diff # preview what `publish` would do right now — makes no mutating call
mozbridge status # show the linked project + its 5 most recent deployments
mozbridge rollback [--to ID] [--yes] # roll back the linked project's deployment
Full flag-by-flag reference, example output, and every documented failure mode: docs/commands.md.
How project linking works
mozbridge link writes .mozbridge/link.json in the current directory
— not in the CLI's own ~/.config/mozbridge. It records which org/project
that specific repo checkout is bound to:
{
"org_id": 1,
"org_slug": "acme",
"project_id": 10,
"project_slug": "web-app"
}
This is per-repo working-tree state, not per-user CLI state — a fresh clone
of the same repo on another machine has no link file until mozbridge link
is run there too. It is not committed. link best-effort appends
.mozbridge/ to an existing .gitignore in the current directory (and
tells you so); if there's no .gitignore there yet, it prints a note asking
you to add the entry yourself rather than creating one. Either way: make
sure .mozbridge/ is actually gitignored in real repos, since the file
contains real org/project identifiers.
build, publish, status, and rollback all read this file and fail
with a clear message if it's missing (see
docs/commands.md for the exact text).
How multi-component publish works
Most real Mozbridge tenants are multi-service (a backend + a frontend +
workers), not a single whole-directory image. mozbridge publish detects
this from a Compose file in the current directory and, when it finds one,
publishes each buildable service as its own image — one
create_source_upload → PUT → trigger_build cycle per component —
instead of the single zip-the-whole-directory flow.
Discovery order (Docker Compose's own file-discovery order, checked in
this exact sequence, first match wins): docker-compose.yml,
compose.yaml, docker-compose.yaml.
What counts as a "component": any top-level service in that file with a
build: key — either the short form (build: ./backend) or the mapping
form (build: {context: ..., dockerfile: ...}). A service with only an
image: key (no build:) is not a component and is skipped — that's how a
redis: or postgres: service in the same compose file is correctly
ignored.
Shared-context de-duplication: two components whose resolved build
context is the same directory — even if spelled differently in YAML (.
vs ./) — are zipped and uploaded exactly once, then each triggers its own
build reusing that one upload_id. This is genuinely a byte-savings and
correctness behavior, not just a nicety: two services building from the
same monorepo root would otherwise ship two identical uploads.
Per-component image naming: each component's image name is computed
client-side as
ghcr.io/{GHCR_ORG}/mozbridge-tenants/{org_slug}/{project_slug}-{service_name}
(the same base the backend computes by default for a single-component
publish, with -{service_name} appended so N components never collide on
one image).
Partial failure: every component is attempted even if an earlier one
fails — one broken service's build failing does not stop the others from
being attempted. At the end, publish prints a summary line
(Publish summary: N succeeded, M failed (of TOTAL)) followed by one OK
or FAIL line per component, and exits non-zero if anything failed.
No compose file, or none of its services build: publish falls back
unchanged to the original single-component flow — zip the whole current
directory, one upload, one trigger_build call, with no image_name,
context_path, or dockerfile_path override sent, so the backend's own
defaults apply exactly as they always have. This is the fallback path, not
a separate mode you opt into — an ordinary non-Compose project (a single
Dockerfile app, or no Dockerfile at all) publishes exactly as it did before
multi-component support existed.
Security model
- Device-flow SSO, not a stored password or long-lived API key.
mozbridge loginruns Logto's OAuth 2.0 Device Authorization Grant (RFC 8628) against a real, provisioned Logto Native (public) client. You approve the login in a browser; the CLI never sees or handles your credentials directly. - Session cache: the resulting access/refresh/ID tokens are cached at
~/.config/mozbridge/session.json. The directory is created0700and the file is written0600on every save (verified against real filesystem state, not mocked — seecli/tests/test_session_permissions.py), the same posture the project's SSH-broker design used for private key material. An expired access token is refreshed transparently before any command that needs one; if refresh itself fails (dead/revoked refresh token, or none was ever cached) you're told tomozbridge loginagain rather than being left in a half-authenticated state. - What the CLI does NOT hold: no service-token, no deploy credential, no
SSH key, no registry password. Every request uses the plain human Logto
bearer token
logincached, sent exactly like a browser session would send it. - A real, documented platform gap — stated plainly, not alarmingly:
today, request-level authorization for mutating project actions (publish,
rollback) beyond ordinary org/project membership is not yet enforced on
the backend for human-authenticated callers. Concretely:
require_deploy_scope(backend/app/features/identity/token_scopes.py) is built onrequire_token_scopes(...), whose check body returns immediately — a complete no-op — for any caller that is not amz_-prefixed ServiceToken (is_service_identity(user)isFalsefor a plain human Logto token). The real gate a human session hits is ordinary org/project membership (get_current_org+get_valid_project), not a deploy-scope check. Therollback/{deployment_id}route has no scope dependency on it at all. This is a backend finding, not something introduced by or specific to this CLI — it applies equally to the web dashboard's own session tokens, and it was surfaced (independently, twice) while building this CLI'spublishandrollbackcommands. It's tracked as a known platform gap in.llm/plans/gap-register.mdand in the session notes at the end of.llm/plans/mozbridge-agent.md— not something this doc oversells or that the CLI works around.
What's NOT built yet
mozbridge deploy— the platform-orchestrated deploy path (/cicd/deploy).publishuses a different, already-working mechanism (create_source_upload+trigger_buildwithsource_type="upload") that builds fresh source into a new image — it does not go through/cicd/deploytoday.- Build-time secrets for multi-component publish.
- Windows support.
mozbridge diff (preview what publish would do, no mutating calls) is
now built — see docs/commands.md. It
compares local components against the most recent deployment record
only, not the actually-deployed compose content — the platform doesn't
expose that anywhere yet, so a full semantic compose diff is still not
built.
See .llm/plans/mozbridge-agent.md (the "Session progress and
reconciliation" section at the end) for the full roadmap and how the CLI's
scope compares to that plan's original acceptance criteria — not
duplicated here.
CI / headless use
Every command above assumes an interactive human at a terminal —
mozbridge login opens a browser. A CI pipeline has neither, so instead of
that stored session, set MOZBRIDGE_TOKEN in the pipeline environment:
export MOZBRIDGE_TOKEN=<service-token>
mozbridge link --org acme --project web-app
mozbridge publish
When MOZBRIDGE_TOKEN is set, it is used directly as the bearer token for
every command — whoami, link, status, rollback, diff, and both
publish and publish --local's upload/registration calls — instead of the
on-disk session mozbridge login produces. It's checked before anything
session-file-related, so nothing under ~/.config/mozbridge/ is read,
written, or permission-checked in this path at all. Because of that,
mozbridge login itself refuses cleanly when MOZBRIDGE_TOKEN is set
(these are mutually exclusive auth modes — a CI token in the environment
means "don't attempt an interactive login") — unset it first if you actually
want to run the device flow.
Where the token comes from: a project- or org-scoped Mozbridge
ServiceToken. Mint one directly from this CLI — mozbridge token create --scope deploy (requires an active mozbridge login session; see
mozbridge token create --help) — or out of band via the dashboard/API
(create_service_token). Set it as your CI platform's secret store, never
as a --token flag (it would leak into shell history and process
listings) and never committed to the repo.
MOZBRIDGE_TOKEN vs MOZBRIDGE_BUILD_TOKEN — do not confuse these:
MOZBRIDGE_TOKEN |
MOZBRIDGE_BUILD_TOKEN |
|
|---|---|---|
| Authenticates | every command, as a project-scoped identity | only publish --local's one GET /cicd/runtime-secrets call |
| Replaces | the entire mozbridge login session |
nothing — it's used alongside your regular session, which still does the actual build registration |
| Scope needed | whatever the platform grants a CI/service identity for the commands you run (link/publish/status/rollback) | the build scope specifically (registry/build credentials only, never a deploy host key) |
They are two separate credentials for two separate purposes and are never
read from, or substituted for, one another anywhere in this CLI — see
session.py and runtime_secrets.py's module docstrings.
A realistic Woodpecker pipeline step:
steps:
publish:
image: python:3.12-slim
environment:
MOZBRIDGE_TOKEN:
from_secret: mozbridge_token
commands:
- pip install --quiet mozbridge-cli # or: pip install -e ./cli, per your setup
- mozbridge link --org acme --project web-app
- mozbridge publish
A GitHub Actions step looks the same, just spelled with env: and
${{ secrets.MOZBRIDGE_TOKEN }}:
- name: Publish to Mozbridge
env:
MOZBRIDGE_TOKEN: ${{ secrets.MOZBRIDGE_TOKEN }}
run: |
mozbridge link --org acme --project web-app
mozbridge publish
mozbridge link still needs to run once per checkout (it writes
.mozbridge/link.json into the working directory, which a fresh CI checkout
never has) — there is no way to skip it by pre-seeding the link file today,
so it belongs in the same pipeline step, before publish.
Configuration (advanced / override)
| Env var | Default | Purpose |
|---|---|---|
MOZBRIDGE_LOGTO_ENDPOINT |
https://auth.mozbridge.com |
Logto tenant endpoint |
MOZBRIDGE_LOGTO_CLIENT_ID |
7zkurxlp5x98iw217wrhw |
Logto Native app client id (public client, device-flow-capable) |
MOZBRIDGE_PUBLIC_API_URL (falls back to PUBLIC_API_URL) |
https://api.mozbridge.com |
Mozbridge API base |
MOZBRIDGE_GHCR_ORG |
jessin01 |
GHCR org used to compute default image names for multi-component publish |
MOZBRIDGE_CONFIG_DIR |
~/.config/mozbridge |
Where the session file lives (mainly for tests) |
MOZBRIDGE_TOKEN |
unset | CI/headless auth — see CI / headless use above |
All of these have real, working defaults — the CLI works out of the box against production Mozbridge. The env vars exist for pointing at a non-production instance (and for test isolation).
Troubleshooting
"Not logged in. Run mozbridge login first." — no session file
exists yet at ~/.config/mozbridge/session.json (or whoami/link/etc.
was run before ever logging in). Run mozbridge login.
"Session expired and cannot be refreshed. Run mozbridge login again."
— your cached session has no refresh token. Re-run mozbridge login.
"Session refresh failed: ... — run mozbridge login again." — the
access token was expired and the cached refresh token was rejected by
Logto (revoked, or itself expired). Re-run mozbridge login.
"Session is no longer valid. Run mozbridge login again." (from
whoami specifically) — the API returned 401 even with a token that looked
valid client-side (e.g. revoked server-side). Re-run mozbridge login.
"This directory is not linked to a Mozbridge project. Run mozbridge link first." — build/publish/status/rollback all require a
.mozbridge/link.json in the current directory. Run mozbridge link
(from the same directory you'll run these commands in).
A failed publish — each step (create_source_upload, the PUT
upload, trigger_build) surfaces the backend's own error detail text
verbatim, prefixed with what it was trying to do, e.g.:
Could not upload the build source: Zip too large
Could not create a source upload session: Not a member of this organization
Could not trigger the build: <backend detail text>
For a multi-component publish, a failure on one component does not stop
the others — you get a per-component OK/FAIL summary at the end (see
docs/commands.md for the exact shape) and the
process exits non-zero if anything failed.
Metadata
Release files for mozbridge-cli 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| mozbridge_cli-0.10.0.tar.gz | 107.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mozbridge_cli-0.10.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 179.3 kB
Release files / mozbridge_cli-0.10.0.tar.gz
| Download URL | mozbridge_cli-0.10.0.tar.gz |
|---|---|
| Size | 107.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bd85f6398247a943eb5bfe3a141d29be4d7ca56ad4b3e5678b6317d3afafb9aa
|
|
BLAKE2b-256 checksum How to use checksums |
e597adec99c541f752752e7721ad8e92bd10aaf08f4f979e7564c8b25d90cd36
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / mozbridge_cli-0.10.0-py3-none-any.whl
| Download URL | mozbridge_cli-0.10.0-py3-none-any.whl |
|---|---|
| Size | 72.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b8291cbfd92127e1f75e05e7127420fd96e40d934ec265659434a2ce50904fbe
|
|
BLAKE2b-256 checksum How to use checksums |
7f74210930427057964df990383adb711985c19c8062ef64470685065c8f6934
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for mozbridge-cli 0.10.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.