Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
078dcb2
feat: add Brevo Function commands with smoke tests
satyamdev10 Aug 14, 2026
6322f3c
feat: gate Brevo Function behind __BREVO_PREVIEW__ and add fn alias
satyamdev10 Aug 14, 2026
255350a
feat: add brevo function activate, deactivate and delete commands
satyamdev10 Aug 17, 2026
0500fe4
Merge pull request #60 from getbrevo/features_fn-manage-commands
satyamdev10 Aug 17, 2026
a88d524
Merge branch 'features_set-dp-function' into review_dp_function
satyamdev10 Aug 18, 2026
a9bf6c3
Merge pull request #62 from getbrevo/review_dp_function
satyamdev10 Aug 18, 2026
2432ea1
feat: add brevo fn init command with AI generation, template flow, an…
satyamdev10 Aug 24, 2026
ec17251
fix: use draft_id instead of code for post-iterate execute preview
satyamdev10 Aug 24, 2026
1c9e410
feat: show friendly message when Brevo Functions is not enabled (403)
satyamdev10 Aug 24, 2026
c0d04c6
refactor: address SonarCloud issues — reduce complexity, fix nested t…
satyamdev10 Aug 24, 2026
e10f859
refactor: address remaining SonarCloud warnings
satyamdev10 Aug 24, 2026
b5ea13e
refactor: fix last S6551 warning in formatCellValue (init.ts)
satyamdev10 Aug 24, 2026
1fdd35b
fix: hide attribute_id from predefined template preview table
satyamdev10 Aug 24, 2026
4b2bf1f
fix: remove Attribute ID line from template preview output
satyamdev10 Aug 24, 2026
7a820d1
fix: address PR #69 review comments
satyamdev10 Aug 25, 2026
1b1dafc
fix: resolve SonarCloud issues — cognitive complexity and boolean param
satyamdev10 Aug 25, 2026
c0feef8
Merge pull request #69 from getbrevo/features/dp-function-init-and-im…
satyamdev10 Aug 25, 2026
ace5c39
feat: add `brevo fn deploy` command for deploying draft functions
satyamdev10 Aug 26, 2026
4b99091
fix: resolve SonarCloud issues in deploy.ts — cognitive complexity an…
satyamdev10 Aug 26, 2026
8f0a64f
fix: replace backtracking regex with lastIndexOf in deriveNameFromDes…
satyamdev10 Aug 26, 2026
9e1d28a
Merge pull request #78 from getbrevo/features/dp-function-deploy
satyamdev10 Aug 26, 2026
78e383f
chore: sync main into features_set-dp-function
satyamdev10 Sep 1, 2026
8a1b102
Merge pull request #86 from getbrevo/sync/main-into-features_set-dp-f…
satyamdev10 Sep 1, 2026
08f20a0
feat: link deployed function to app after creation
satyamdev10 Sep 1, 2026
7113be3
fix: address PR #85 review — shared module, JSON safety, tests
satyamdev10 Sep 1, 2026
0830310
refactor: extract shared deploy helpers to reduce code duplication
satyamdev10 Sep 1, 2026
3732bb5
chore: remove changeset file
satyamdev10 Sep 1, 2026
0f6ca0c
fix: resolve SonarCloud duplication and nested template literal
satyamdev10 Sep 1, 2026
f2115a9
Merge pull request #85 from getbrevo/features/dp-function-link-to-app
satyamdev10 Sep 1, 2026
f8bfc6a
feat: remove preview gate from brevo function commands (GA)
satyamdev10 Sep 1, 2026
aeb137b
feat: add function suite to smoke test live surfaces
satyamdev10 Sep 2, 2026
ac3df08
feat: expand function smoke suite with mutation steps, init flow, and…
satyamdev10 Sep 2, 2026
3977db5
fix: resolve SonarCloud duplication in function smoke not-found probes
satyamdev10 Sep 2, 2026
f94d73e
Merge pull request #90 from getbrevo/feat/ga-brevo-function-commands
satyamdev10 Sep 2, 2026
e58fd7c
Merge branch 'main' into features_set-dp-function
piyushsarin-sib Sep 3, 2026
2eea156
Merge branch 'main' into features_set-dp-function
piyushsarin-sib Sep 3, 2026
b6d3bc0
fix: resolve SonarCloud duplication in function action commands
satyamdev10 Sep 4, 2026
fcc349c
Merge pull request #101 from getbrevo/fix/sonar-function-duplication
satyamdev10 Sep 4, 2026
8cf295b
fix: reduce SonarCloud duplication across function commands and smoke…
satyamdev10 Sep 4, 2026
12f4373
Merge pull request #102 from getbrevo/fix/sonar-function-duplication-v2
satyamdev10 Sep 4, 2026
6b8bcfe
feat: add app_type field to app-config.json template
satyamdev10 Sep 4, 2026
3d11d14
Merge branch 'features_set-dp-function' into fix/pr100-review-comments
satyamdev10 Sep 4, 2026
bf9b7e2
feat: address PR #100 review comments
satyamdev10 Sep 4, 2026
53810ea
test: add backward compatibility and smoke tests for app_type field
satyamdev10 Sep 4, 2026
53550a1
chore: add changeset for function app-type registry
satyamdev10 Sep 4, 2026
b37eb29
refactor: rename app_type to appType in config for camelCase consistency
satyamdev10 Sep 4, 2026
dae10cf
fix: update stale doc comments for appType and add app_type migration
satyamdev10 Sep 4, 2026
1d90504
refactor: revert appType back to app_type in config
satyamdev10 Sep 4, 2026
943781b
Merge pull request #103 from getbrevo/fix/pr100-review-comments
satyamdev10 Sep 4, 2026
be7d9cf
BEX-470 refactor(config): normalize app-config.json keys to snake_cas…
piyushsarin-sib Sep 4, 2026
ec681f7
fix: address PR #100 review comments for brevo function
satyamdev10 Sep 4, 2026
96577b2
fix: resolve merge conflicts with features_set-dp-function
satyamdev10 Sep 4, 2026
fad4501
Merge pull request #105 from getbrevo/fix/pr100-review-comments
satyamdev10 Sep 4, 2026
032828a
BEX-471 feat(json): emit snake_case twins for every camelCase --json …
piyushsarin-sib Sep 4, 2026
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
25 changes: 25 additions & 0 deletions .changeset/function-app-type-registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@getbrevo/cli': minor
---

Add `function` as a first-class app type in the CLI's app-type registry. A Brevo Function app is now detected by the presence of `brevo_function` in both the local config and the server record, and participates in the same resolution, capability, and recoverability checks as `oauth` and `ui` apps. The capability matrix grants a private Function app no OAuth or UI capabilities (it ships its own `brevo function deploy` flow); public distribution adds `review-lifecycle`. A new `app_type` field is written to `app-config.json` on `brevo app create` and `brevo app scaffold` as informational metadata (`"oauth"`, `"ui"`, or `"function"`); it is never sent to the server and legacy configs without it continue to work unchanged. Additional fixes: `fetchFunctionList` now paginates past 50 functions, `sseStream` refreshes the OAuth token before connecting, `brevo function deploy` appears in help output, and `resolveFunctionId` replaces a duplicated IIFE in the shared action runner.

Refuse a `brevo app upload` whose `app_type` contradicts the blocks it sits next to. `app_type` stays informational — the presence of `ui_app` / `brevo_function` / `auth` is still the only discriminator, and `isUiAppConfig()` is still the only place it is read — but a file labelled `"oauth"` that carries a `ui_app` block is a hand-edit that half-landed, and `upload` now says so before any round trip, naming both the declared and the detected type and how to reconcile them. A config that omits `app_type` (every file written by an earlier release) is unaffected: the check is skipped entirely rather than defaulted. The label is never sent to Brevo.

Normalize every key in `app-config.json` to snake_case, matching the wire contract. The file was meant to be snake_case throughout but several keys were camelCase; they are renamed as follows:

- `appId` → `app_id`
- `appName` → `app_name`
- `logoUri` → `logo_uri`
- `appType` → `app_type`
- `auth.redirectUris` → `auth.redirect_uris`

`version`, `distribution_type`, `auth.scopes`, `ui_app` and `brevo_function` are unchanged, and nothing sent to or received from the Brevo API changes.

**Backward compatibility is preserved.** Every command still reads the camelCase spellings written by earlier releases (and the older `auth.redirectUrls`), through one shared normalizer in the config reader. When a file carries both spellings of a key with different values the snake_case one wins and a one-line notice is printed to stderr.

**Legacy files are migrated on write.** `brevo app create`, `brevo app upload`, `brevo app scaffold` and `brevo app start` now write snake_case keys only, and the camelCase copies are dropped. `brevo app upload` and `brevo app scaffold` also rewrite an in-sync legacy file on their "nothing to change" paths, so running either once is enough to migrate a project. Values are never changed by the migration.

The only `--json` output that echoes config key names is `brevo app scaffold`'s `diffs[].field`, which now reports `app_name`, `redirect_uris` and `logo_uri` instead of `appName`, `redirectUris` and `logoUri`. The `appId` / `appName` / `logoUri` keys in other commands' `--json` output are unchanged.

Every `--json` document now carries each camelCase key together with its snake_case twin: `appId` and `app_id`, `clientId` and `client_id`, `upToDate` and `up_to_date`, and in the error envelope `exitCode` / `exit_code` and `statusCode` / `status_code`. `brevo app create --json`'s `redirectUri` and `brevo app credentials --json`'s `redirectUris` are twinned as `redirect_uris`, the wire name. Array documents (`brevo app list --json`) alias each element. Nested objects are left as they are — `ui_app`, the upload diff's `current` / `next`, Function records — since they were already snake_case or are the user's own data. Nothing is removed: every existing `jq .appId` keeps working. This is the deprecation step toward one spelling for machine-readable output, matching `app-config.json` and the API; the camelCase keys will be removed in the next major release, and new scripts should read the snake_case ones.
22 changes: 16 additions & 6 deletions .github/workflows/smoke-post-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,10 @@ jobs:
against: published
# Pinned for the same reason as the pre-merge lane.
#
# `private,ui` — the LIVE surface, which is the whole point of this lane:
# it installs from npm, so it is the only check on what a customer
# actually gets. `ui` is not implied by `private` (the two suites are
# disjoint) and UI apps have shipped in every build since 2.2.0, so
# omitting it left `app install` / `app uninstall` and the whole `ui_app`
# path unverified against the published package.
# `private,ui` — the LIVE surface minus function, which runs on a
# dedicated account (see smoke-function below). `ui` is not implied by
# `private` (the two suites are disjoint). Both have shipped in every
# build since their respective GA milestones.
#
# No 'public': those commands are eliminated from the published bundle, so
# the steps could only ever skip — and smoke.yml now refuses that pairing
Expand All @@ -38,3 +36,15 @@ jobs:
account: default
expected_version: ${{ github.event.release.tag_name }}
secrets: inherit

smoke-function:
uses: ./.github/workflows/smoke.yml
with:
against: published
# Function commands need a dedicated account with dp-functions enabled.
# Split into its own job so it authenticates with the function account
# independently of the private/ui suites.
suite: function
account: function
expected_version: ${{ github.event.release.tag_name }}
secrets: inherit
32 changes: 23 additions & 9 deletions .github/workflows/smoke-pre-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,29 @@ jobs:
# manual "Run workflow" button, and a release gate must not change what it
# covers because someone retuned a dropdown.
#
# `all` = private + public + ui + init. This is the widest lane on
# purpose, and it is the right place to be widest: it is `non_blocking`,
# 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.
suite: all
# `private,public,ui,init` — everything except function, which runs on a
# dedicated account (see smoke-function below). This is the widest lane
# on purpose (minus function), and it is the right place to be widest: it
# is `non_blocking`, 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.
suite: private,public,ui,init
account: default
non_blocking: true
secrets: inherit

smoke-function:
if: >-
github.event_name == 'push' ||
Comment thread
satyamdev10 marked this conversation as resolved.
(github.event_name == 'pull_request' && github.head_ref == 'changeset-release/main')
uses: ./.github/workflows/smoke.yml
with:
against: local
# Function commands need a dedicated account with dp-functions enabled.
suite: function
account: function
non_blocking: true
secrets: inherit
27 changes: 16 additions & 11 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ on:
description: 'Comma-separated suites for --suite (blank = the live surface)'
required: false
type: string
default: 'private,ui'
default: 'private,ui,function'
account:
description: 'Which test-account secret to authenticate with'
required: false
Expand Down Expand Up @@ -81,8 +81,8 @@ 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.
# `private,ui,function` is the LIVE surface and the default: private apps,
# UI apps and Brevo Functions — all GA and 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
Expand All @@ -95,14 +95,16 @@ on:
description: 'Which app types to smoke (--suite). public/all = preview, local only'
type: choice
required: true
default: private,ui
default: private,ui,function
options:
- private,ui,function
- private,ui
- private
- ui
- function
- init
- public
- private,ui,public
- private,ui,function,public
- all
account:
# Names a SECRET, never a key. workflow_dispatch inputs are recorded on
Expand All @@ -121,6 +123,7 @@ on:
options:
- default
- alt
- function
environment:
# 'staging' points BREVO_API_URL / BREVO_OAUTH_PROXY_URL / BREVO_OAUTH_BASE_URL
# at repo secrets (BREVO_STAGING_*) rather than the CLI's production defaults —
Expand Down Expand Up @@ -276,15 +279,17 @@ jobs:
ENVIRONMENT: ${{ inputs.environment }}
KEY_DEFAULT: ${{ secrets.BREVO_TEST_API_KEY }}
KEY_ALT: ${{ secrets.BREVO_TEST_API_KEY_ALT }}
KEY_FUNCTION: ${{ secrets.BREVO_TEST_API_KEY_FUNCTION }}
KEY_STAGING_DEFAULT: ${{ secrets.BREVO_TEST_API_KEY_STAGING }}
KEY_STAGING_ALT: ${{ secrets.BREVO_TEST_API_KEY_STAGING_ALT }}
run: |
set -eu
case "${ENVIRONMENT:-production}:${ACCOUNT:-default}" in
production:default) key="$KEY_DEFAULT"; name='BREVO_TEST_API_KEY' ;;
production:alt) key="$KEY_ALT"; name='BREVO_TEST_API_KEY_ALT' ;;
staging:default) key="$KEY_STAGING_DEFAULT"; name='BREVO_TEST_API_KEY_STAGING' ;;
staging:alt) key="$KEY_STAGING_ALT"; name='BREVO_TEST_API_KEY_STAGING_ALT' ;;
production:default) key="$KEY_DEFAULT"; name='BREVO_TEST_API_KEY' ;;
production:alt) key="$KEY_ALT"; name='BREVO_TEST_API_KEY_ALT' ;;
production:function) key="$KEY_FUNCTION"; name='BREVO_TEST_API_KEY_FUNCTION' ;;
staging:default) key="$KEY_STAGING_DEFAULT"; name='BREVO_TEST_API_KEY_STAGING' ;;
staging:alt) key="$KEY_STAGING_ALT"; name='BREVO_TEST_API_KEY_STAGING_ALT' ;;
*)
echo "::error::unknown environment:account '${ENVIRONMENT:-production}:${ACCOUNT:-default}' — add a case for it in this step"
exit 1
Expand All @@ -298,7 +303,7 @@ jobs:
echo "key=$key" >> "$GITHUB_OUTPUT"
echo "authenticating as account '${ACCOUNT:-default}' on '${ENVIRONMENT:-production}' (secret ${name})"

- name: Run smoke test (${{ inputs.suite || 'private,ui' }})
- name: Run smoke test (${{ inputs.suite || 'private,ui,function' }})
# Non-blocking only where the caller asks for it (the pre-merge release
# PR). Absent on workflow_dispatch, where `inputs.non_blocking` is null.
continue-on-error: ${{ inputs.non_blocking == true }}
Expand All @@ -316,4 +321,4 @@ jobs:
BREVO_OAUTH_BASE_URL: ${{ steps.environment.outputs.oauth_base_url }}
# `${SUITE:-…}` covers unset AND empty — an empty --suite= is a runner
# error ("needs at least one suite name"), so it must not reach it.
run: yarn smoke --ci --against="$AGAINST" --suite="${SUITE:-private,ui}" --report=smoke-report.json
run: yarn smoke --ci --against="$AGAINST" --suite="${SUITE:-private,ui,function}" --report=smoke-report.json
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ The `type === 'corporate'` discriminator on `/v3/account/info` that account reso

**Two blocks are compared through one normalization**, `canonicalizeUiApp` / `uiAppEquals` in `src/app-types/ui/compare.ts` (it delegates the wire-only strip to `src/app-types/wire.ts`). It was private to `app upload` until `app install`'s drift check needed the same answer; a second implementation would have drifted from this one the first time a server-stamped key was added. `app upload`'s per-field diff is `formatPlacementDiffLines` in `src/app-types/ui/fields.ts`, table-driven off the same `VALUE_ROWS` as the plain renderer so a new per-entry field cannot appear in one and not the other. It matches entries by `surface_point_name` (never by index — list order is registry order, not authored order) and falls back to plain lines when the server echoes no block at all, since an absent block is no evidence that a placement is new.

**The presence of `ui_app` is the app-type discriminator** — there is no `appType` key in `app-config.json`. Every branch that needs to tell the two types apart goes through `isUiAppConfig()` in `src/lib/config.ts`; use it rather than testing for the key inline, so the discriminator can change in one place.
**The presence of `ui_app` is the app-type discriminator** — the `app_type` key in `app-config.json` is a label, written by `app create` / `app scaffold` so the file says in one word what it is (BEX-468). **It is read for validation, never for detection.** `app upload`'s `assertAppTypeAgrees` compares it against the blocks and refuses a contradiction with a migration hint; nothing anywhere branches on it, and a config that omits it — every file written before BEX-468 — uploads unchanged, which is why the check is an early return on absence rather than a default. Ordered last in `runLocalPreflight`, deliberately: the label is the least authoritative statement in the file, so a structural refusal (a malformed `ui_app`, `APP_UI_IFRAME_PRIVATE_ONLY`) must always be the error a partner sees first — fixing a label and re-uploading only to hit the real reason is the failure that ordering avoids. **`app_type` is file-only and never travels**: `UploadAppPayload` and the create body in `src/types.ts` are closed structs that `upload.ts` / `create.ts` build key by key, so there is no spread that could carry it onto the wire and no strip step to keep in sync — bo-be needs no change to accept a field that never arrives. Keep both payloads closed if either is refactored; `upload.test.ts` pins the payload's key set. **Every key in `app-config.json` is snake_case** (`app_id`, `app_name`, `logo_uri`, `app_type`, `auth.redirect_uris`, …); the camelCase spellings earlier releases wrote are folded on read by the alias tables in `src/lib/config.ts` and dropped on the next write, and `app upload` / `app scaffold` rewrite an in-sync legacy file on their no-op paths (`migrateProjectConfigKeys`) so the migration is not gated on drift. Every branch that needs to tell the two types apart goes through `isUiAppConfig()` in `src/lib/config.ts`; use it rather than testing for the key inline, so the discriminator can change in one place.

## Non-interactive (non-TTY) UI app creation — required before merging `feat/ui-app-noninteractive-create` to `main`

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Command-line tool to create, manage, and test [Brevo](https://www.brevo.com/) OA
> 📖 Full command and option documentation: **[Brevo CLI reference](https://developers.brevo.com/docs/cli-reference)**

> [!WARNING]
> **Upgrade to the latest released version.** All versions from **1.1.1** up to (but not including) **2.0.0** should be migrated to at least **2.1.0**. The `2.0.0` release introduced **breaking changes**, so some CLI commands may not work as expected on older versions. **`2.1.0` and above** also carry further `app-config.json` migrations (e.g. legacy `auth.redirectUrls` → `auth.redirectUris`) that are applied automatically the next time the CLI writes your config (`brevo app upload`, `brevo app start`, …).
> **Upgrade to the latest released version.** All versions from **1.1.1** up to (but not including) **2.0.0** should be migrated to at least **2.1.0**. The `2.0.0` release introduced **breaking changes**, so some CLI commands may not work as expected on older versions. **`2.1.0` and above** also carry further `app-config.json` migrations (e.g. legacy `auth.redirectUrls` → `auth.redirect_uris`, and the camelCase keys `appId` / `appName` / `logoUri` / `appType` / `auth.redirectUris` → their snake_case spellings) that are applied automatically the next time the CLI writes your config (`brevo app upload`, `brevo app start`, …).
>
> Upgrade with `npm install -g @getbrevo/cli@latest` (or `yarn global add @getbrevo/cli@latest`, or `brew upgrade brevo`), then confirm with `brevo --version`.

Expand Down
Loading