Skip to content
Merged
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
38 changes: 38 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,44 @@ jobs:
mix docs --warnings-as-errors
mix hex.build

openfeature:
name: OpenFeature with Elixir ${{ matrix.elixir }} and OTP ${{ matrix.otp }}
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
include:
- elixir: "1.15.8"
otp: "26.2"
- elixir: "1.20.3"
otp: "29.0"

steps:
- uses: actions/checkout@v7
- uses: erlef/setup-beam@v1.24.1
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}
- name: Install provider dependencies
working-directory: openfeature
run: FEATUREVISOR_OPENFEATURE_PATH=.. mix deps.get
- name: Compile provider without warnings
working-directory: openfeature
run: FEATUREVISOR_OPENFEATURE_PATH=.. mix compile --warnings-as-errors
- name: Test provider
working-directory: openfeature
run: FEATUREVISOR_OPENFEATURE_PATH=.. mix test
- name: Format, static analysis, documentation, and package
if: matrix.elixir == '1.20.3'
working-directory: openfeature
run: |
mix format --check-formatted
FEATUREVISOR_OPENFEATURE_PATH=.. mix credo --strict
FEATUREVISOR_OPENFEATURE_PATH=.. mix dialyzer
FEATUREVISOR_OPENFEATURE_PATH=.. mix docs --warnings-as-errors
env -u FEATUREVISOR_OPENFEATURE_PATH mix hex.build

example:
name: Featurevisor example 1
runs-on: ubuntu-latest
Expand Down
61 changes: 56 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ jobs:
node-version: 24
package-manager-cache: false
- name: Install dependencies
run: mix deps.get
- name: Verify tag matches package version
run: test "${GITHUB_REF_NAME#v}" = "$(mix run --no-start --no-compile -e 'IO.write(Mix.Project.config()[:version])')"
run: |
mix deps.get
cd openfeature
FEATUREVISOR_OPENFEATURE_PATH=.. mix deps.get
- name: Verify tag matches package versions
run: |
version="${GITHUB_REF_NAME#v}"
base_version=$(mix run --no-start --no-compile -e 'IO.write(Mix.Project.config()[:version])')
provider_version=$(cd openfeature && FEATUREVISOR_OPENFEATURE_PATH=.. mix run --no-start --no-compile -e 'IO.write(Mix.Project.config()[:version])')
test "$version" = "$base_version"
test "$base_version" = "$provider_version"
- name: Run checks
run: make check
- name: Set up Featurevisor example 1 project
Expand All @@ -48,7 +56,50 @@ jobs:
tar -tf featurevisor-*.tar | grep '^contents.tar.gz$'
env:
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
- name: Publish to Hex.pm
run: mix hex.publish --yes
- name: Publish base package to Hex.pm
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
if curl --fail --silent --show-error "https://hex.pm/api/packages/featurevisor/releases/$version" >/dev/null; then
echo "featurevisor $version is already published"
else
mix hex.publish --yes
fi
env:
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
- name: Wait for base package
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
for attempt in {1..18}; do
if curl --fail --silent --show-error "https://hex.pm/api/packages/featurevisor/releases/$version" >/dev/null; then
exit 0
fi
sleep 10
done
echo "featurevisor $version did not become available on Hex.pm" >&2
exit 1
- name: Publish OpenFeature provider package to Hex.pm
shell: bash
working-directory: openfeature
run: |
version="${GITHUB_REF_NAME#v}"
if curl --fail --silent --show-error "https://hex.pm/api/packages/featurevisor_openfeature/releases/$version" >/dev/null; then
echo "featurevisor_openfeature $version is already published"
else
resolved=false
for attempt in {1..18}; do
if env -u FEATUREVISOR_OPENFEATURE_PATH mix deps.get; then
resolved=true
break
fi
sleep 10
done
test "$resolved" = true
env -u FEATUREVISOR_OPENFEATURE_PATH mix hex.publish --dry-run --yes
env -u FEATUREVISOR_OPENFEATURE_PATH mix hex.build
tar -tf featurevisor_openfeature-*.tar | grep '^contents.tar.gz$'
env -u FEATUREVISOR_OPENFEATURE_PATH mix hex.publish --yes
fi
env:
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
27 changes: 25 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
FEATUREVISOR_PROJECT ?= ../featurevisor/examples/example-1

.PHONY: deps format format-check compile test credo dialyzer docs package check escript test-example-1
.PHONY: deps deps-openfeature format format-check compile test test-openfeature credo dialyzer docs docs-openfeature package package-openfeature check check-base check-openfeature escript test-example-1

deps:
mix deps.get

deps-openfeature:
cd openfeature && FEATUREVISOR_OPENFEATURE_PATH=.. mix deps.get

format:
mix format

Expand All @@ -17,6 +20,9 @@ compile:
test:
mix test

test-openfeature:
cd openfeature && FEATUREVISOR_OPENFEATURE_PATH=.. mix test

credo:
mix credo --strict

Expand All @@ -26,13 +32,30 @@ dialyzer:
docs:
mix docs --warnings-as-errors

docs-openfeature:
cd openfeature && FEATUREVISOR_OPENFEATURE_PATH=.. mix docs --warnings-as-errors

package:
mix hex.build

package-openfeature:
cd openfeature && env -u FEATUREVISOR_OPENFEATURE_PATH mix hex.build

escript:
mix escript.build

check: format-check compile test credo dialyzer docs package
check-base: format-check compile test credo dialyzer docs package

check-openfeature: deps-openfeature
cd openfeature && mix format --check-formatted
cd openfeature && FEATUREVISOR_OPENFEATURE_PATH=.. mix compile --warnings-as-errors
cd openfeature && FEATUREVISOR_OPENFEATURE_PATH=.. mix test
cd openfeature && FEATUREVISOR_OPENFEATURE_PATH=.. mix credo --strict
cd openfeature && FEATUREVISOR_OPENFEATURE_PATH=.. mix dialyzer
cd openfeature && FEATUREVISOR_OPENFEATURE_PATH=.. mix docs --warnings-as-errors
cd openfeature && env -u FEATUREVISOR_OPENFEATURE_PATH mix hex.build

check: check-base check-openfeature

test-example-1: test escript
./featurevisor test --projectDirectoryPath=$(FEATUREVISOR_PROJECT) --onlyFailures
83 changes: 80 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The SDK supports concurrent evaluations, structured diagnostics, lifecycle modul
- [Modules](#modules)
- [Child instance](#child-instance)
- [Close](#close)
- [OpenFeature](#openfeature)
- [CLI usage](#cli-usage)
- [Test](#test)
- [Benchmark](#benchmark)
Expand All @@ -52,7 +53,7 @@ Add `featurevisor` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:featurevisor, "~> 1.0"}
{:featurevisor, "~> 1.2"}
]
end
```
Expand Down Expand Up @@ -446,6 +447,78 @@ Close invokes module close callbacks and removes listeners, diagnostic subscript

Do not call `close/1` directly for a supervised instance. Stop its supervisor or remove its child instead. A closed handle evaluates against an empty datafile, so flags return `false`, variations and variables return `nil`, and the revision returns `"unknown"`.

## OpenFeature

The OpenFeature provider is published as a separate Hex package. Applications
that only use the Featurevisor SDK do not install or compile OpenFeature or the
provider code.

```elixir
def deps do
[
{:featurevisor, "~> 1.2"},
{:featurevisor_openfeature, "~> 1.2"},
{:open_feature, "~> 0.1.3"}
]
end
```

Create a provider that owns its Featurevisor instance:

```elixir
provider =
FeaturevisorOpenFeature.Provider.new(
featurevisor_options: %{datafile: datafile}
)

{:ok, provider} = OpenFeature.set_provider(provider)
client = OpenFeature.get_client()

enabled =
OpenFeature.Client.get_boolean_value(client, "checkout", false,
context: %{"targetingKey" => "user-123"}
)
```

You can also pass an existing Featurevisor instance. The provider borrows it
and does not close it:

```elixir
provider = FeaturevisorOpenFeature.Provider.new(featurevisor: f)
```

OpenFeature uses one flag key while Featurevisor supports flags, variations,
feature variables, and global variables:

| OpenFeature key | Featurevisor evaluation |
| --- | --- |
| `checkout` | Flag for feature `checkout` |
| `checkout:variation` | Variation for feature `checkout` |
| `checkout:title` | Variable `title` inside feature `checkout` |
| `variable:supportEmail` | Global variable `supportEmail` |

`targeting_key_field`, `key_separator`, `variation_key`, and
`global_variable_prefix` customize this mapping. The targeting key maps to
`userId` by default. The global variable prefix defaults to `variable` and
cannot contain the separator.

The official OpenFeature Elixir SDK represents evaluation context as a map. The
provider accepts `"targetingKey"`, `"targeting_key"`, or `:targeting_key` and
copies its string value to the configured Featurevisor targeting field.

The provider implements boolean, string, number, and map resolution.
Featurevisor evaluation reasons, variation values, revision, schema version,
rule keys, bucket information, and override information are mapped to
OpenFeature resolution details and flag metadata. Missing definitions, type
mismatches, and invalid datafiles use standard OpenFeature errors. Replacing an
invalid datafile with a valid one recovers the provider.

The OpenFeature Elixir SDK does not currently expose provider tracking.
Featurevisor modules and diagnostics continue to run inside the Featurevisor
instance.

See the [OpenFeature provider guide](https://featurevisor.com/docs/sdks/openfeature/) for the shared key convention and providers for other languages.

## CLI usage

Build the escript from this repository:
Expand Down Expand Up @@ -522,7 +595,7 @@ make test-example-1

The integration target executes all expanded `example-1` assertions, including Target datafiles, through the Elixir evaluator.

`make check` remains the fast package gate. The separate `make test-example-1` target requires the sibling Featurevisor monorepo, so checks and publishing workflows run that integration explicitly after the package gate.
`make check` verifies both the base SDK and OpenFeature provider. The separate `make test-example-1` target requires the sibling Featurevisor monorepo, so checks and publishing workflows run that integration explicitly after the package gate.

## Publishing

Expand All @@ -533,7 +606,11 @@ make check
mix hex.publish --dry-run
```

The release workflow verifies the tag, package contents, documentation, and `example-1` integration before publishing to Hex.pm.
The repository publishes `featurevisor` and `featurevisor_openfeature` with the same version. The release workflow verifies the tag, package contents, documentation, and `example-1` integration, then publishes the base package before the provider package.

Hex can only resolve the provider's exact base package dependency after the
matching `featurevisor` release is visible. The workflow waits for that release
before it performs the final provider dry run and publication.

The published Hex package intentionally includes runtime source, `mix.exs`, the README, and the licence. Tests and the conformance fixture remain repository verification assets and are not shipped to consumers.

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Featurevisor.MixProject do
use Mix.Project

@version "1.1.0"
@version "1.2.0"
@source_url "https://github.com/featurevisor/featurevisor-elixir"

def project do
Expand Down
3 changes: 3 additions & 0 deletions openfeature/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
inputs: ["{mix,.formatter}.exs", "{lib,test}/**/*.{ex,exs}"]
]
20 changes: 20 additions & 0 deletions openfeature/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/_build/
/cover/
/deps/
/doc/
/erl_crash.dump
*.ez
/featurevisor_openfeature-*.tar
/.elixir_ls/
/.lexical/
/.vscode/
/.idea/
/.DS_Store
/.env
/.env.*
!/.env.example
*.pem
*.key
*.p12
*.pfx
*.secret
21 changes: 21 additions & 0 deletions openfeature/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Fahad Heylaal

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading