Changelog

Release history for the sfp CLI — what changed in each version.

The sfp CLI carries its own version line, separate from sfp server's. Releases are published to npm as @flxbl-io/sfp and in the sfp docker images; see Install sfp. Under codev the CLI runs on the server, at the server's release.

Only releases that change the CLI are listed. A version absent from this page shipped no CLI-facing change. A version marked self-hosted is published to source.flxbl.io, where self-hosted deployments and the sfp docker images take it from.

October 2026

v52.27.0 — October 5, 2026

  • A test suite in config/sfp_testsuites.yaml can list the source or diff packages it deploys with under deploy_with. sfp build expands the suites bound to a package into a list of test classes and stores it in the artifact. A binding to a package missing from sfdx-project.json, to a package of another type, or from a suite that selects tests by tags fails the build
  • sfp release, sfp install and sfp validate org deploy a source or diff package with Apex whose artifact carries bound test suites with RunSpecifiedTests and that list of test classes. An explicit testLevel, skipTesting and isOptimizedDeployment: false take precedence over the binding
  • sfp validate org fails a bound package when any Apex class or trigger in its scope is covered below 75%, or below the package's coverage threshold when that is higher, by the bound suites alone. The failure names the classes below the threshold and the suites that ran
  • The sfp validate org report lists only the packages that failed, captioned "N of M failed", and reports a coverage failure only for a package whose coverage verdict failed
  • sfp build and sfp quickbuild skip a package whose build commit is an ancestor of the commit that last published it on the same branch, so the package is not published again under a new version. Other packages in the run still build; skipped packages are listed under "Packages skipped as superseded" and in buildResult.json with status: skipped and skipReason: superseded
  • sfp build and sfp quickbuild accept versionNumber: 0.0.0.LATEST on a dependency to a package of the same project whose version is 0.0.0.NEXT. When the dependency is not part of the build, it resolves to the version last published on the branch as recorded by sfp server, and that exact version is written into the dependent's artifact. The build fails when the published versions cannot be read from sfp server
  • sfp release treats a deployment that stops with an error before every package is attempted as failed: the packages not yet deployed are reported as failed instead of skipped, later release definitions are not deployed, and the command exits non-zero. Writes of deployment information that conflict with a concurrent release are retried
  • sfp repo patch --releasedefinitions <domain>/<release> refuses a release candidate that is aborted or has an artifact without a version. A reference such as core/RC-A.json is fetched from sfp server unless a file exists at that path

September 2026

v52.26.0 — September 30, 2026

  • A diff package with Apex that sets isOptimizedDeployment: false in its descriptor deploys with RunLocalTests in sfp release, sfp install and sfp validate org, instead of RunRelevantTests. skipTesting still takes precedence
  • sfp validate org prints the deployment's TestLevel for each package when tests run as a separate step after the deployment
  • sfp insights logins counts unique users with a single aggregate query, so the report no longer fails for orgs with more users in the 30-day window than one query batch returns
  • sfp server pool scratch init authenticates each scratch org leased from a snapshot pool before preparing it. An org that cannot be authenticated is reported as an error in the pool failure summary and is not deleted

v52.25.0 — September 29, 2026

  • sfp release --quickdeploy <deployId> --payload-fingerprint <fingerprint> commits the validation that sfp release --checkonly returned instead of deploying the package again. Pre-deployment steps are skipped, post-deployment steps run as usual, and the release is recorded as a plain release is. The release is refused when the fetched artifact's fingerprint differs from --payload-fingerprint
  • sfp release --quickdeploy-fallback deploys the package in full when Salesforce rejects the validated deploy id. Without it, a rejected id fails the release and leaves the org unchanged
  • sfp sfdmu run scans the target reports sfdmu writes, prints a records summary, and fails when any row was rejected. --no-failonrowerrors reports rejected rows as a warning without failing. The --json result carries the count under rowFailures
  • sfp sfdmu run, sfp push and sfp install print "Command completed, but N record(s) were rejected by the target org" in place of "Command succeeded." when sfdmu rejected rows
  • sfp publish skips a version already in the registry only when the stored artifact was built from the same commit and package version id. A different source fails the publish with VERSION_COLLISION, an artifact whose source cannot be read fails with IDENTITY_UNVERIFIED, and neither records release metadata, the version baseline or git tags. An artifact the internal registry rejects this way is not published to the external registry
  • sfp server environment lock --hatchet-run-id names the Hatchet run that owns the lock; the server releases the ticket once that run ends, including while it is still queued
  • sfp analyze on a pull request reuses the previous architecture and version-assist verdicts when the net diff is unchanged or the change since the last analysed commit is not significant. Set incrementalReviewEnabled: false in analyze.yaml or the server project configuration to always run a fresh analysis

v52.24.0 — September 27, 2026 · self-hosted

  • sfp release --checkonly validates a single-package domain against the target org with one check-only deployment and returns the validated deploy id. Nothing is committed to the org and no release history is recorded; it cannot be combined with --dryrun
  • sfp release --json reports the deployment error for a failed package instead of null
  • The CLI adds the operating system certificate store to the certificate authorities it trusts, so sfp auth login and sfp server update reach an sfp server whose certificate is issued by a private CA. On Node.js earlier than 22.19, set NODE_EXTRA_CA_CERTS instead
  • sfp auth login --global-auth continues when the server configuration cannot be read, using --sfp-server-url as the sign-in callback base
  • sfp server callback post accepts "comment": { "key": "<key>" } in the context to keep one comment per pull request, updated in place, on GitHub and Azure DevOps
  • sfp validate org reports a run with nothing to deploy as "No changes to validate" instead of "Validation passed". The check still concludes as success
  • sfp insights connected-apps --json returns the org's external client apps under externalClientApps, and under hiddenManagedApps the number of managed connected apps left out without --include-managed. When the org cannot list external client apps, the reason is given under externalClientAppsSkipped
  • sfp insights content lists built-in list views that Salesforce reports without an object as Standard views (no object)

v52.23.1 — September 24, 2026

  • Improvements to check-only validation in sfp validate org
  • sfp validate org now respects dependencyOnDomains
  • sfp validate org --deploywithtests always runs Diff packages with RunRelevantTests
  • sfp sync org --releaseconfig accepts multiple domains and syncs all of them

v52.23.0 — September 23, 2026 · self-hosted

  • sfp validate org builds its validation check from the same report as every other surface, laid out for the platform configured on sfp server
  • sfp validate org no longer writes a second check for a run sfp server already reports
  • Validation results carry per-class coverage for each package
  • sfp server callback post keeps one validation comment per Azure DevOps pull request, updated in place

v52.22.0 — September 23, 2026

  • sfp releasecandidate generate no longer fails for a source-only release configuration with no promotion target
  • String replacement globs match the same way on install, sfp push and sfp pull
  • sfp push reports only the replacements that changed a file, and --json lists the rules that matched nothing under unmatchedRules
  • Non-regex replacement values are applied as written, so a $ in a value is kept

v52.21.2 — September 22, 2026

  • New sfp releasecandidate sync-check lists the domains on a branch whose last release to an environment did not succeed
  • sfp release records a release attempt for each domain on sfp server
  • Improvements to AI credential handling in sfp analyze

v52.21.1 — September 22, 2026

  • Source packages with isOptimizedDeployment: false deploy with RunLocalTests again
  • Fixes to command aliases and topic descriptions for sfp pull, sfp push, sfp sfdmu run, sfp server review-envs and sfp server variable

v52.21.0 — September 21, 2026

  • sfp release --branch <branch> releases the latest published release candidate of each domain on a branch tracked by the project, in place of --releasedefinition or --releasecandidate; the three flags are mutually exclusive. --repository and --sfp-server-url are both required with it, and --domain — repeatable — restricts the release to the domains named. A candidate is eligible only when its domain build succeeded and the candidate was not aborted.
  • sfp release --branch prints the resolved domain, release candidate and commit as a table, then the branch head commit and the domains whose candidate commit differs from it, and lists the domains skipped for want of an eligible candidate. The same resolution is carried under branchResolution in the --json output.
  • sfp release --branch fails before it releases anything when the branch is not among the branches tracked in the project's server configuration, when a --domain is not one of the branch's domains, or when no selected domain has an eligible candidate.

v52.20.1 — September 20, 2026

  • sfp sandbox create sets a sandbox's add-on features from features in the --definition-file definition — an array of strings such as SandboxStorage. When the definition also names a sourceSandboxName, the features recorded on that source sandbox take precedence over the ones in the definition.
  • sfp sandbox create fails when the lookup of sourceSandboxName errors, instead of warning and creating the sandbox without a source. A source sandbox that simply returns no record is still reported as not found, and creation continues without a source.

v52.20.0 — September 20, 2026

  • A source or diff package that carries a Flow in pre-destructive/flows/ or post-destructive/flows/ has that Flow deactivated and every one of its versions deleted through the Tooling API before the metadata deployment, and the Flow is dropped from that deployment. The deletion is not part of the deployment transaction, so it is not undone when the deployment fails. See Destructive changes.
  • A Flow that is absent from the org is recorded as already absent and installation continues. A lookup, deactivation or deletion failure is reported as a warning, that Flow's delete is skipped, and the remaining components are installed. Progress is folded into a Destructive Flows log group that closes with a table of each Flow, whether it was deactivated, how many versions were deleted, and the result.
  • enableFlowDestructiveHandling in a package's sfdx-project.json descriptor turns that handling off for the package, restoring plain metadata deletion. It defaults to true and is independent of enableFlowActivation. The handling is also skipped, and the plain Flow member retained, for a check-only deployment and for a dry-run installation.
  • sfp repo diff --create-package stages two synthetic source packages when the changed packages disagree on enableFlowDestructiveHandling — the metadata from the packages that keep it first, then the opted-out metadata in <name>-flow-optout — and each is deployed separately. Packages that agree still produce one synthetic package.
  • An installation whose components are all removed — a package whose only members were destructive Flows — completes without a metadata deployment rather than submitting an empty one.
  • sfp org flow activate, sfp org flow deactivate and sfp org flow cleanup escape --developername and --namespaceprefix in the SOQL that looks the Flow definition up. An explicitly empty --namespaceprefix now matches only flows with no namespace; it was previously ignored, matching any namespace. Each command's output is folded into a log group.
  • sfp org flow cleanup logs each version as it is deleted, with its position in the run, and names the attempt and the Salesforce error when a deletion is retried.

v52.19.2 — September 19, 2026

  • sfp server update pauses Hatchet worker assignments in bounded cycles instead of holding one pause for the whole drain. --pause-timeout sets the maximum seconds of a single pause, defaulting to 600; when it expires without a confirmed drain the assignments are restored for a 300-second cooldown and a new cycle starts, until the drain is confirmed or --drain-timeout — still 3600 seconds by default — is spent. --pause-timeout must not exceed --drain-timeout, and both flags reject a value below 1.
  • sfp server update updates the tenant files and pre-pulls the new images before it pauses assignments, rather than pre-pulling in parallel with the drain, and recreates Caddy inside the pause window.
  • sfp server update aborts before it stops any service when no drain is confirmed within that budget. Assignments are restored first, and the error names the number of pause cycles, the running and queued workflow counts, and the names of up to 100 running tasks.
  • sfp build continues when a package it has just built is an ordering-only dependency of a package still to be built — a source, diff or data package of the same project, which dependency resolution removes from the dependent's resolved dependency list. Stamping the completed version onto a dependency that is no longer listed previously threw and failed the build.
  • sfp build fails on an invalid preDeploy/replacements.yml in a package: a missing or empty name, glob or pattern, or an environment value that is not a string, boolean or number. It previously logged a warning and built the package without its replacements.
  • Replacement YAML — a package's preDeploy/replacements.yml, and the file passed to sfp push --replacementsoverride — keeps an unquoted date such as 2026-09-17 or timestamp such as 2026-09-17T12:34:56+05:30 as its original text instead of reading it as a date. A boolean or numeric value is applied as its text, and a null, array or object value is a configuration error.
  • A replacement whose value is an empty string is applied, removing the matched text, and an exact org alias takes precedence over default even when the alias value is empty. An empty value was previously treated as no value, leaving the pattern in place. This covers sfp push and the source-package install path that sfp install and sfp release run.
  • sfp push treats a package's preDeploy/replacements.yml, or --replacementsoverride, as authoritative for that package: the replacements entries in sfdx-project.json no longer apply to it as well. A package with no package-level configuration keeps the project-level replacements, and in a push carrying both kinds those project rules are matched against project-relative paths, so a hidden directory in the checkout path no longer suppresses a match.
  • sfp push --replacementsoverride merges the override entries into the package configuration by replacement name, the override's environment values taking precedence, instead of applying both lists.
  • sfp push fails with Unable to prepare push replacements: <reason> when it cannot prepare the replacements. It previously logged a warning and pushed the source without them.
  • sfp push applies package replacements to every file a component carries rather than only its top-level content path, and no longer counts a file in a sibling directory whose name starts with the package directory's name as being inside the package.
  • sfp push removes the temporary conversion context it creates for replacements once the deployment is submitted, including when the conversion or the submission fails.
  • The header line every command prints carries -Build:<number> once when a build number is set, and omits it otherwise. It previously repeated the -Build: label, and printed an empty one when no build number was set.

v52.19.1 — September 17, 2026

  • sfp analyze prints the analysis result as JSON only when --json is set. It previously printed the raw result to stdout alongside the human-readable output whenever a linter named in --fail-on had a blocking finding or could not run.
  • The [AI_RESPONSE] entry carrying a prompt's full response — written by the architecture and version-assist linters of sfp analyze — is logged at debug level rather than info, so it no longer appears in the default console output. It is dispatched straight to the run's logger, so a file or log sink attached to the run still receives it.

v52.19.0 — September 16, 2026

  • sfp server update logs the Hatchet worker-assignment commands it runs over SSH against a remote host, and their output, at debug level rather than info. A command that exits non-zero is still reported at error level.
  • sfp server update names the worker-assignment pause before it runs, and on resume reports how many workers had their assignments restored and how many registrations the restart replaced. That report is printed whether the update finishes or fails.

v52.18.2 — September 15, 2026

  • sfp server callback post --context-id takes the UUID of a callback context held on the server, in place of the inline JSON on --context. The two flags are mutually exclusive and exactly one is required; the command reads the context from the server with the server URL and application token before it posts.
  • sfp server callback post fails before posting on a --context-id that is not a UUID, a --context that is not valid JSON, a context that is not a JSON object, and a context whose username, accessLevel, targetOrg, taskId, taskType or message is not a string.

v52.18.0 — September 15, 2026

  • A package install that composes partial CustomObject metadata — layout assignments from mutators/layout-assignments.yml, and the search-layouts contribution that augments them — carries the target org's object settings into that partial: the supported feature flags, description, gender, startsWith, each targeted record type's description, and the name field's trackHistory and trackFeedHistory. A CustomObject deployment resets allowInChatterGroups, enableActivities and enableReports and clears description, gender and startsWith when they are omitted, so the composed partial previously reset them on install.
  • That composition stops before deployment when the org's CustomObject metadata is incomplete — a missing label or nameField, an AutoNumber name field with no displayFormat, a value that is not a valid boolean, or an enableBulkApi/enableSharing/enableStreamingApi group that is inconsistent — instead of composing a partial that would reset a property.
  • Each setting is written only when the Metadata API version supports it. The version is the one the install deploys with, falling back to the package's apiVersion and then to the component set's.
  • sfp validate org runs its AI error analysis before it publishes the validation result, so the analysis reaches the published result and its markdown as well as the CI check. It previously ran only while the CI check was being created.
  • sfp validate org cancels the AI error analysis at its timeout — three minutes, unless errorAnalysis.timeout in the AI configuration sets another, in milliseconds — and aborts the agent session rather than leaving the request in flight. A transient failure is retried up to three attempts, after 5 and 10 seconds, within that same deadline.
  • The validation markdown records AI error analysis unavailable: <reason> when the analysis produces no insight, where the reason is no credentials, timed out, unparseable response, attempts exhausted or analysis failed.
  • sfp validate org reads its AI configuration from the validation worktree — config/ai-assist.yaml, then the .yml spelling, then config/ai-architecture.yaml and .yml — rather than from the directory the command was started in. The analysis is skipped when the run failed because no changes were detected in the packages to be built, and runs whenever --opencode-server is set.
  • sfp releasecandidate unbundle removes the file when a conflict has no copy of it on the side --conflict-strategy selects, instead of resolving markers in a file that side deleted.
  • sfp releasecandidate unbundle fails when it cannot resolve the new SHA of a cherry-picked commit. It previously logged a warning and continued, leaving the deployment step lineage incomplete.
  • sfp releasecandidate unbundle-analyze counts only re-applied commits in the kept-commit total it prints and returns. A commit that turned out empty after conflict resolution, and was skipped, is no longer counted.
  • sfp auth login and sfp server auth login serve branded sign-in result pages on the CLI's loopback receiver. The failure page escapes the detail returned by the authorization server and names sfp auth login as the command to run again.
  • sfp publish includes the repository and an ISO 8601 timestamp in the webhook events it triggers on sfp server. The repository is read from SFP_REPOSITORY, GITHUB_REPOSITORY or GITLAB_REPOSITORY, in that order.

v52.17.0 — September 11, 2026

  • sfp publish --pushgittag pushes the tags for all published packages in a single git push, instead of one push per package. A transient failure — a dropped connection, an unresolved host, an HTTP 5xx — is retried three times, after 2, 5 and 15 seconds. An authentication, permission, protected-tag or ruleset rejection is not retried.
  • sfp publish completes when the tag push does not, instead of failing the run. It lists the tags that still need pushing and the git push origin refs/tags/… command that pushes them, with remote URLs and credentials stripped from the reported error, and leaves the --gittaglimit and --gittagage cleanup for that retry.
  • sfp releasecandidate unbundle --source-branch defaults to the branch recorded on the release candidate, falling back to main when the candidate records none. It previously always defaulted to main. An explicit value still takes precedence.
  • sfp releasecandidate unbundle removes its temporary worktree when the run fails as well as when it succeeds, and raises a failure through the command's own error handling — so --json reports it — instead of exiting immediately.
  • sfp releasecandidate unbundle reports how many manual deployment steps were re-anchored onto the rewritten branch and how many were flagged for review. The command fails when the server cannot record that anchor migration, after the branch has been pushed.

v52.16.1 — September 10, 2026

  • sfp server update pauses Hatchet worker assignments before it drains, then waits only for workflows that occupy a worker slot. Queued tasks are no longer waited on; they stay pending and run once the workers return. The drain is checked again after the image pre-pull and the Caddy recreation, and the paused workers are restored when the update finishes or fails.
  • sfp server update aborts before it stops any service when the drain fails or the running-workflow count cannot be read. It previously logged a warning and continued with the update. --force still skips both the pause and the drain, and --drain-timeout still bounds the wait, in seconds, defaulting to 3600.
  • sfp scratch delete completes without error when the Dev Hub holds no active scratch org record for the username. The lookup previously raised No ActiveScratchOrg found for username <username>, which the command reported as a missing org or a permissions failure.

v52.16.0 — September 8, 2026

  • sfp org login --server --default-devhub accepts --repository (alias --repo) to authenticate with the DevHub selected for that project, and fails when the server resolves the DevHub from anywhere else. sfp server org get-default-devhub takes the same flag. Both name where the DevHub came from — the project's default DevHub, a legacy project registration, or the tenant default.
  • sfp scratch login without --devhub-username has the server resolve the DevHub that owns the scratch org, instead of trying the tenant default DevHub and then each registered DevHub in turn.
  • sfp sandbox login without --production-username has the server resolve the production org bound to the sandbox, instead of trying each registered production org in turn. A sandbox registered without a production connection logs in, and the output omits the Production Org line.
  • sfp server pool monitor names the pool it is reconciling when it reads sandbox state from Salesforce, so the state covers that pool's own instances rather than every sandbox under the Dev Hub.
  • sfp scratch delete fails when the Dev Hub rejects the deletion, instead of reporting success. A username with no active scratch org record in the Dev Hub is left alone.
  • --passphrase on the SSH-based server commands — sfp server init, start, stop, status, logs, scale and update — is passed to the SSH connection, so an encrypted --identity-file can be used. The key is read when the flags are parsed and is no longer written into the lifecycle configuration that debug logging serialises.
  • Those same commands report an SFTP error raised while checking for a directory on the remote host, instead of treating the directory as absent.

v52.15.1 — September 7, 2026

  • sfp server update and sfp server init copy the Supabase schema to a remote host through a temporary archive that is extracted and then removed. Removing it now tolerates an archive that is already gone, so a repeated or concurrent run against the same tenant directory no longer fails after the extraction has succeeded.

v52.15.0 — September 4, 2026

  • sfp auth login and sfp server auth login sign in to flxbl cloud when no server URL is given, through the global auth service over OAuth 2.1 with PKCE and a loopback receiver bound to 127.0.0.1. --sfp-server-url no longer defaults to http://localhost:3029.
  • sfp auth login --sfp-server-url reads the server's /sfp/api/config before signing in: a server that reports itself as self-hosted routes the sign-in to the auth service it publishes, and an unreachable URL fails before a browser opens. --global-auth and --no-global-auth force either service and no longer carry a default.
  • sfp auth login prints the sign-in URL before it opens a browser, so a session where no browser opens can still complete the sign-in. On the self-hosted path the browser is redirected to the server's hosted /auth/callback page, which shows an sfp1_ login code to paste into the terminal when it cannot reach the CLI's loopback.
  • sfp install and sfp release group the output of each pre- and post-deployer and of the preDeployment and postDeployment scripts, and keep the component checksum skip pre-fetch lines inside the eligibility group.

v52.14.3 — September 2, 2026

  • sfp analyze --output-format github renders code-analyzer and architecture findings as a Markdown list — severity, rule, location and gate on the first line, the message on the next — instead of an HTML table. --output-format markdown keeps its tables.
  • sfp analyze and sfp project version suggest pass the architecture and version-assist instructions to the AI as its system prompt, rather than prepending them to the pull request content being analysed. The architecture instructions return the empty JSON result the parser reads when there is no architecturally significant change, in place of a plain sentence.

v52.14.2 — September 2, 2026

  • sfp project preflight reports a repository with no sfdx-project.json, or one that cannot be parsed, as a not-configured status with the cause listed under issues. Previously the read error aborted the run.

v52.14.1 — September 2, 2026

  • sfp cascade merge-assist no longer stops responding when the AI agent asks to access a directory outside the working tree. The request is answered instead of left pending.

v52.14.0 — September 1, 2026

  • sfp validate grounds its AI analysis of a failure. Commands it suggests are checked against the CLI's own command set, and browserforce settings are reported as org-UI settings rather than as CLI flags.
  • A package build reports a missing Package2 registration in the Dev Hub as that, instead of as a generic creation failure.
  • Repository and npm-registry calls to sfp server report the server's own error message instead of a generic substitute.

August 2026

v52.12.0 — August 30, 2026

  • sfp analyze runs the version-assist AI linter only when it is asked for. --version-assist runs it, --no-version-assist keeps it off even when the project enables it, and with neither flag it runs only when the server project settings explicitly enable it.
  • A package install that fails on a transport error — a dropped connection, DNS, or TLS failure — names the underlying cause instead of reporting fetch failed.
  • sfp server scale sizes both Hatchet worker pools. --workers sets the general pool and sizes the long-running pool proportionally; --longrunning-workers sets the long-running pool explicitly; --drain-timeout bounds the wait for in-flight work, and --force scales down without draining.

v52.10.2 — August 27, 2026

  • sfp analyze reports a linter that was skipped as skipped, instead of reporting it as a pass.

v52.10.0 — August 26, 2026

  • sfp validate org --waittime sets the metadata deployment wait for each package, in minutes. Defaults to 120.
  • sfp org drift analyze --max-ai-findings skips the AI review when the mechanical candidate count exceeds the threshold. Defaults to 50.
  • sfp apextests resume bounds result fetching, so a large-org run stores its results instead of abandoning them on a timeout.
  • sfp server init creates the admin team, so a newly initialised admin can manage the organisation.

v52.9.3 — August 25, 2026

  • Dependency resolution ignores source package dependencies declared inside the same project.

On this page