Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/fix-app-submit-missing-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@getbrevo/cli': minor
---

feat(public apps): public app distribution and the review lifecycle are now GA (BEX-405)

`brevo app create --distribution public` is accepted, `Public` is selectable in the interactive distribution prompt, and `brevo app submit`, `brevo app status` and `brevo app withdraw` ship in the published package. They were built but eliminated from published builds at compile time; that gate is gone and the build now asserts the three commands are present in every artifact.

- **`brevo app status`** — an app's review lifecycle state (`draft`, `submitted`, `in_review`, `approved`, `rejected`, `changes_requested`, or `unknown`) with a human message. Read-only; `--json` gives `{ state, message }`.
- **`brevo app submit`** — opens the public-app review submission form. Requires `distribution_type: public`, an uploaded app, and a local `app-config.json` that matches the server (shown as a field-by-field diff with `(local only)` / `(server only)` tags on drift). `--json` prints `{"app_id","form_url"}` on stdout with the next-steps notes on stderr. The app is submitted only once the form itself is completed — the command changes nothing server-side.
- **`brevo app withdraw`** — withdraws an app from submission (`--force`, `--json`). An app that was never submitted prints a hint and exits `0`.

All three resolve the target app from `--app-id`, the linked `app-config.json`, or an interactive picker.

The scaffolded OAuth flow now branches on distribution: a **public** app gets Authorization Code + PKCE (RFC 7636) — `/auth/login` generates a `code_verifier` and sends `code_challenge` + `code_challenge_method=S256`, and the token exchange and refresh send the verifier with no `client_secret`, so the generated `.env.local` / `.env.example` carry none. **Private** apps keep the confidential-client flow unchanged.

Note that Brevo currently refuses public app creation from the CLI at the platform level: `brevo app create --distribution public` sends the request and the API answers `400`, which the CLI reports as *"Public apps can't be created from the CLI yet"* with the server's own message quoted. The CLI-side commands are all in place and will work as soon as the platform allows it.

`distribution_type` remains immutable after `brevo app create` — pick `private` for apps used exclusively by your own organisation and `public` for apps distributed to end users or marketplace listings. Only a public app can be submitted for review.

fix(app submit): refuse an app that was never uploaded before reading its review state, naming the real cause. A never-uploaded app has no version for a review state to hang off, and the server's message for that failure listed `name`, `logo_uri`, `scopes` and `redirect_uris` as the fields to fix — all of which could already be correct.

fix(app submit): skip the redundant app fetch when the app isn't submittable, and show missing required-field names exactly as returned by the API (BEX-454)

fix(app status): show the status message returned by the API, falling back to the built-in per-state copy when absent (BEX-454)

docs: `agent-context/SKILL.md` and `agent-context/AGENTS.md` document the publication and review flow — the route from a public create to an approved app, the five refusals `submit` applies in order, the review states, and the fact that a successful `submit` has not yet submitted anything.
9 changes: 6 additions & 3 deletions .github/workflows/smoke-pre-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ jobs:
# so a suite that has never run headless can prove itself here without
# gating a merge, and it runs on every push to main — which always
# precedes a release, so the post-merge lane inherits proven ground.
# Selecting 'public' makes the runner build PREVIEW=1, unchanged from when
# this lane ran `private,public`; the published surface is covered by the
# post-merge lane, which installs from npm.
# Selecting 'public' used to make the runner build PREVIEW=1, because the
# review-lifecycle commands existed nowhere else. Since public-apps GA
# (BEX-405) every local build is the published surface, so this lane now
# exercises the review lifecycle against the same artifact npm ships. The
# post-merge lane still installs from npm and stays pinned narrower — see
# its own note.
suite: all
account: default
non_blocking: true
Expand Down
59 changes: 25 additions & 34 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ name: Smoke
# post-publish dispatch — pins both explicitly, so retuning a dropdown can never
# change what a release gate verifies. Widen a gate by editing that lane.
#
# The manual default is the LIVE surface: private + ui. Public apps are not live
# (BEX-405), so the preview suite is opt-in and refused unless against=local —
# see the "Reject a preview suite" step, which is the rule, not the comment.
# The manual default is the LIVE surface, which since public-apps GA (BEX-405)
# is every non-interactive suite: private + ui + public. Nothing is gated any
# more, so there is no longer a preview-only suite to refuse — the step that used
# to reject a public-containing suite against a published package is gone with it.
# The release lanes remain pinned at their own narrower sets on purpose; widening
# one is a deliberate edit there, and needs the suite proven headless first.
on:
workflow_call:
inputs:
Expand All @@ -46,7 +49,7 @@ on:
description: 'Comma-separated suites for --suite (blank = the live surface)'
required: false
type: string
default: 'private,ui'
default: 'private,ui,public'
account:
description: 'Which test-account secret to authenticate with'
required: false
Expand Down Expand Up @@ -81,28 +84,27 @@ on:
# The values ARE the `--suite` argument, so what the dropdown shows is
# what the runner receives — no label-to-flag mapping to keep in sync.
#
# `private,ui` is the LIVE surface and the default: private apps plus UI
# apps, both GA and both in the published build.
#
# Anything containing `public` is the PREVIEW surface and only means
# something against=local, where the runner builds PREVIEW=1. Against a
# published package those steps could only ever skip — the commands are
# eliminated from the bundle (BEX-405) — so the pairing is refused
# outright rather than reported as a pass with holes in it.
# `private,ui,public` is the LIVE surface and the default: all three app
# types are GA and all three are in the published build. `public` joined
# at public-apps GA (BEX-405) — before that it was the preview surface,
# meaningful only against=local where the runner built PREVIEW=1, and a
# public-containing suite against a published package was refused outright
# rather than reported as a pass with holes in it. Both the preview build
# and that refusal are gone; a public suite now runs anywhere.
#
# `init` drives the wizard through scripted stdin; `ui` drives create
# through a pty. Both work headless, so both can run here.
description: 'Which app types to smoke (--suite). public/all = preview, local only'
description: 'Which app types to smoke (--suite)'
type: choice
required: true
default: private,ui
default: private,ui,public
options:
- private,ui,public
- private,ui
- private
- ui
- init
- public
- private,ui,public
- init
- all
account:
# Names a SECRET, never a key. workflow_dispatch inputs are recorded on
Expand Down Expand Up @@ -159,24 +161,13 @@ jobs:
timeout-minutes: 20

steps:
# Fail before checkout: an impossible pairing should cost nothing and say
# why. The preview surface exists only in a PREVIEW=1 build, which only
# `against=local` produces — against a published package the public steps
# would skip and the run would still go green, which reads as coverage the
# run never had.
- name: Reject a preview suite against a published package
if: contains(inputs.suite, 'public') || inputs.suite == 'all'
env:
AGAINST: ${{ inputs.against }}
SUITE: ${{ inputs.suite }}
run: |
set -eu
if [ "$AGAINST" != 'local' ]; then
echo "::error::suite '${SUITE}' includes the public-app (preview) surface, which only exists in a PREVIEW=1 build. Re-run with against=local, or pick a suite without 'public'."
exit 1
fi
echo "preview suite '${SUITE}' against a local build — ok"

# No suite/against pairing is impossible any more, so nothing is rejected
# before checkout. There used to be a guard here: the review-lifecycle
# surface existed only in a PREVIEW=1 build, so a public-containing suite
# against a published package would skip every step and still go green —
# coverage the run never had. Public apps went GA (BEX-405) and the guard
# went with it. Re-add one if a suite is ever build-specific again; the
# failure it prevented is a green run, which is the worst kind.
- name: Checkout
uses: actions/checkout@v7
with:
Expand Down
16 changes: 6 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,17 @@ Brevo Developer CLI (`@getbrevo/cli`) — TypeScript CLI for managing OAuth app

Public CLI reference: https://developers.brevo.com/docs/cli-reference — the authoritative user-facing command/flag/exit-code documentation. Keep this in sync when changing user-visible behaviour.

## Public app distribution is not GA — the published build omits it (BEX-405)
## Public apps are GA — the whole surface ships (BEX-405)

Public app distribution is **not live on the Brevo platform**. The repo contains the whole surface — `brevo app create --distribution public`, `brevo app submit`, `brevo app status`, `brevo app withdraw` — but a **published build does not**: `scripts/build.mjs` eliminates the review-lifecycle commands from the bundle, and `--distribution public` is refused with a typed `CliError`.
Public app distribution and the review lifecycle are **live**: `brevo app create --distribution public`, `brevo app submit`, `brevo app status` and `brevo app withdraw` all ship in the published build. A build-time gate used to eliminate them; `scripts/build.mjs` now asserts they are **present** in the bundle (`GA_MARKERS`).

**This does not restrict work in this repo.** Building, changing, testing, and QA-ing the public-app code paths is expected and unblocked — write the code, run the commands, create public apps against test environments. Build with `PREVIEW=1 yarn link:dev` (or `yarn build:preview`) and the full surface is there. Never refuse or hedge on a public-app task in this repo.
**Nothing is gated, and the gate is gone** — `src/lib/preview.ts`, `src/globals.d.ts`, the esbuild `define` block, the `LEAK_MARKERS` / `LEAK_STRINGS` checks, `build:preview` and the three preview modules (`commands/preview-definitions.ts`, `lang/preview-messages.ts`, `lib/preview-constants.ts`) are all deleted. **There is one build** — no `PREVIEW=1`, no `__BREVO_PREVIEW__`. If a feature ever has to be held back from a published build again, read `CLAUDE.md` → *If you ever need to gate a feature again* first: the mechanism and its two traps are written down there, and flipping a readiness row is not sufficient on its own.

- **The guard is the build, not the docs.** This replaced a documentation-only notice (and then a runtime check). `agent-context/SKILL.md` and `agent-context/AGENTS.md` no longer carry a *⚠️ not available yet* section or an *Exception — internal Brevo accounts* clause; they carry one rule instead — `brevo --help` is the complete surface. Don't reintroduce prohibition prose: an agent can't be led into a command that isn't in the binary.
- **There is deliberately no runtime escape hatch.** The earlier gate unlocked on an `@brevo.com` account or `BREVO_ENABLE_PREVIEW=1`; both are gone. A compile-time guard any user can switch back on is a runtime guard wearing a costume, and it has to ship the surface in order to reveal it. **Do not add one back.**
- **Two layers, no soft middle.** The build removes the surface; the Brevo API refuses public-app creation independently (`400 invalid_parameter`).
- **`FEATURE_STAGE` in `src/lib/preview.ts` is the single source of truth** for what is gated — but flipping a row to `'ga'` is necessary and **not sufficient** for a command, because gated definitions live in `src/commands/preview-definitions.ts` behind a *build* flag. See `RELEASE-CHECKLIST.md`.
- **When public apps go GA**, work through `RELEASE-CHECKLIST.md` → *Before public-apps GA* in one pass.
**`brevo app submit` is a form hand-off, not a state transition** — it opens a Google Form and changes nothing server-side, so exit `0` does not mean "submitted". The initial review state is `draft` (not `configured`, renamed by BEX-382), and reviewer feedback goes out by email, never through `app status`.

## UI apps are GA — they ship in every build (BEX-290)
## UI apps are GA — the whole surface ships (BEX-290)

UI apps (action links that render inside Brevo CRM records) are **out of the pre-GA gate**: the *UI app* choice at `brevo app create`'s app-type prompt, `brevo app install [account-id]` and `brevo app uninstall [account-id]` all ship in the published build. Their `FEATURE_STAGE` rows are `'ga'`, their command definitions live in `src/commands/definitions.ts`, their strings in `src/lang/en.ts`, and their names are gone from `LEAK_MARKERS` in `scripts/build.mjs`. Only the public-apps surface above remains gated.
UI apps (action links that render inside Brevo CRM records) shipped at BEX-290: the *UI app* choice at `brevo app create`'s app-type prompt, `brevo app install [account-id]` and `brevo app uninstall [account-id]` are all in the published build. Their command definitions live in `src/commands/definitions.ts`, their strings in `src/lang/en.ts`, and their bindings are asserted **present** by `GA_MARKERS` in `scripts/build.mjs`. Public apps followed at BEX-405 and the pre-GA gate was torn down after it.

A UI app is **prompt-only**: there is no `--type` flag and no per-field flags, so non-interactive runs always create an OAuth app. `extension_type` values are camelCase (`actionLink`, `iframeExtension`, `legacyComponent`) and the old snake_case spellings are rejected. The `ui_app` block's **field names are confirmed** against both of the platform's consumers, the manifest read path and the extensibility UI kit (BEX-308 / BEX-350) — it is the stored app snapshot verbatim. See `CLAUDE.md` → *UI apps are GA* for the full contract.

Expand Down
Loading