Featurevisor

Larger projects

Promotions

Promotions copy definitions from one set to another. A change made in dev moves to staging and then production without being written three times. Each step is previewed first and lands as an ordinary diff, so a release goes through the same review as any other change.

Before you start

Promotions need a project with one set per release lane. This creates dev, staging and production sets, each with its own checkoutFlow feature:

Command
npx @featurevisor/cli@latest init \
--example=environments

Both sets must pass lint. promote checks them before it plans anything, then lints the merged destination again before writing a file.

Preview a promotion

Without --apply, promote only shows what would change:

Command
npx featurevisor promote \
--from=dev \
--to=staging
Output, trimmed
Mode: preview
Conflict policy: source
Updated
sets/staging/features/checkoutFlow.yml
sets/staging/targets/all.yml
sets/staging/tests/features/checkoutFlow.spec.yml
Conflicts
feature checkoutFlow description
feature checkoutFlow rules.everyone.percentage
target all description
test features.checkoutFlow assertions

A conflict is a value both sets have that differs, such as the percentage of the everyone rule. By default dev's value wins, as Resolve conflicts explains.

A promoted feature brings what it depends on: its segments and their attributes, the features its conditions reference, its schemas and its tests. Add --show-unchanged to list the files that would stay as they are.

Apply it

Command
npx featurevisor promote \
--from=dev \
--to=staging \
--apply

The promoted files are ordinary changes. Review the diff, then lint and test the destination before you commit:

Commands
npx featurevisor lint --set=staging
npx featurevisor test --set=staging

If writing fails partway, the files already written stay written. Check the diff before running it again.

How definitions merge

A definition missing from the destination is created. One that exists in both is merged:

  • Values that differ are conflicts, settled by the conflict policy.
  • Rules and keyed overrides are matched by key.

The first matching rule decides, so promotion keeps the destination's order. Rules both sets have are updated in place, and a rule only the destination has keeps its slot. In the promotion above, staging's internal rule stays first, and its everyone rule takes dev's percentage. A new rule from the source goes in before the rule that follows it in the source, so it never lands after a catch all it was meant to precede.

Choose what to promote

By default every definition in the source is a candidate. Narrow it with a selector:

Command
npx featurevisor promote \
--from=dev \
--to=staging \
--target=web
  • --target=web selects what that target would put in its datafile, and promotes the target too.
  • --tag=web selects features and global variables with that tag.
  • --include-features="checkout*" selects features by key, with * matching any characters.
  • --exclude-features="experimental*" leaves matching features out.

Each selector can be repeated. A definition must match every --target, --tag and --include-features given, and --exclude-features removes keys last. What the selection depends on still comes along, so promoting chatFileUpload brings liveChat too, even if no selector matched it.

An unknown target or tag fails and lists the ones that exist. A selection that matches nothing fails too, unless you pass --allow-empty.

Tests and scenarios come along only when every feature and global variable they reference is being promoted or already exists in the destination. A target a test refers to is created in the destination if it is missing.

Resolve conflicts

--conflicts decides what happens when both sets have a value and they differ:

PolicyResult
sourceThe source value wins. This is the default.
destinationThe destination keeps its value.
failNothing is written, and the conflicts are listed.
Command
npx featurevisor promote \
--from=dev \
--to=staging \
--conflicts=fail

Two details matter for rules and overrides matched by key:

  • An override that switches between value and mutate takes one form or the other, never both. Its conditions expression follows the selected conflict policy.
  • Reordering rules, or global variable overrides, is a conflict, because order decides which one applies and how mutations accumulate. source takes the source order, destination keeps the existing order, and fail stops. Rules and overrides that are protected or only in the destination keep their place, and when the destination's order wins, new global variable overrides go after the existing ones.

Protect what belongs to one lane

Add promotable: false to something that a lane owns, such as production's rollout percentage or a test that only production runs. It takes precedence over --conflicts.

A whole definition

At the top level, it protects the whole file:

sets/production/features/checkoutFlow.yml
description: Checkout flow
promotable: false
bucketBy: userId
rules:
- key: everyone
conditions: '*'
percentage: 0
SourceDestinationResult
promotable: falseExistsDestination unchanged
Promotablepromotable: falseDestination unchanged
promotable: falseMissingCreated, keeping promotable: false

This works for attributes, segments, features, global variables, schemas, targets, test specs and scenarios. Definitions are matched by key, which comes from the file path.

One rule

On a rule, it keeps that rule in its own lane. Dev's internal-testing rule is never copied out of dev:

sets/dev/features/checkoutFlow.yml
description: Checkout flow
bucketBy: userId
rules:
- key: internal-testing
promotable: false
conditions: internal
percentage: 100
- key: everyone
conditions: '*'
percentage: 50

A destination rule with promotable: false is kept as it is when a rule with the same key arrives. With environments, rules are matched within each environment. Segments used only by an omitted rule are not promoted for it.

One override

Feature variable overrides and global variable overrides can be protected one at a time. Once one override in a variable's list uses promotable, every override in that list needs a unique key:

sets/production/features/checkoutFlow.yml
description: Checkout flow
bucketBy: userId
variablesSchema:
message:
type: string
defaultValue: Welcome
rules:
- key: everyone
conditions: '*'
percentage: 100
variableOverrides:
message:
- key: production-owner
promotable: false
conditions: internal
value: Production preview

A global variable's overrides work the same way. Production keeps its own support address:

sets/production/variables/supportEmail.yml
description: Support email address
type: string
defaultValue: [email protected]
overrides:
- key: production-owner
promotable: false
conditions: '*'

Nested overrides are matched by key at every level, and protecting a parent protects everything under it.

One test assertion

Production turns checkoutFlow on for the internal team and keeps it off for everyone else. Its spec protects the second fact, so promoting dev's spec cannot replace it. To protect a single assertion, give every assertion in the spec a key:

sets/production/tests/features/checkoutFlow.spec.yml
feature: checkoutFlow
assertions:
- key: production-rollout
promotable: false
description: Off for everyone else
at: 50
context:
userId: 8f2a
team: sales
expectedToBeEnabled: false
- key: internal-team
description: On for the internal team
at: 50
context:
userId: c4e1
team: engineering
expectedToBeEnabled: true

Keys must exist in both sets' specs, since matching assertions by position would be unsafe, and promote stops with guidance if only one side has them. A matrix or children belong to their assertion and cannot be protected separately. A scenario has no assertions, so protect it at its top level. Assertion keys also give the Catalog stable links.

promotable is not supported on matrix cases, children, conditions, force entries, variations, variablesSchema entries or schema properties. Lint rejects it in most of them.

Allow only some directions

By default, any set can be promoted to any other. promotionFlows allows only the directions you list:

featurevisor.config.js
module.exports = {
sets: true,
promotionFlows: [
{ from: 'dev', to: 'staging' },
{ from: 'staging', to: 'production' },
],
}

promote --from=dev --to=production now fails, so changes pass through staging first.

Keep an audit record

--audit writes a record of an applied promotion to .featurevisor/promotions/:

Command
npx featurevisor promote \
--from=dev \
--to=staging \
--apply \
--audit=markdown

--audit and --audit=json write JSON for scripts, and --audit=markdown writes a summary for people. Nothing is written without --apply.

Previous
Sets