diff --git a/.github/actions/setup-dotnet/action.yml b/.github/actions/setup-dotnet/action.yml
new file mode 100644
index 00000000..8a425bc7
--- /dev/null
+++ b/.github/actions/setup-dotnet/action.yml
@@ -0,0 +1,25 @@
+name: "Setup .NET"
+description: "Installs the .NET SDK pinned by global.json and restores the NuGet package cache"
+
+inputs:
+ cache-key-suffix:
+ description: "Extra qualifier appended to the NuGet cache key, e.g. the runner architecture"
+ required: false
+ default: ""
+
+runs:
+ using: "composite"
+ steps:
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
+ with:
+ global-json-file: global.json
+
+ - name: Cache NuGet packages
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}${{ inputs.cache-key-suffix }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
+ restore-keys: |
+ ${{ runner.os }}${{ inputs.cache-key-suffix }}-nuget-
+ ${{ runner.os }}-nuget-
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ca34e304..9bd3bfa1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,106 +3,104 @@ name: CI
on:
push:
branches: [main]
- paths-ignore:
- - "**.md"
pull_request:
branches: [main]
- paths-ignore:
- - "**.md"
+ merge_group:
+ workflow_dispatch:
+
+concurrency:
+ # Supersede in-flight runs for the same pull request, but never cancel a merge queue run:
+ # a cancelled run reports a non-success conclusion and ejects the pull request from the
+ # queue. Deliberately not set in the called workflows - sharing a group between caller and
+ # callee makes a run cancel itself.
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+permissions:
+ contents: read
jobs:
build:
+ name: Build
permissions:
contents: read
- pull-requests: write
- strategy:
- matrix:
- os: [ubuntu-latest, windows-latest]
+ uses: ./.github/workflows/reusable-build.yml
- runs-on: ${{ matrix.os }}
+ coverage:
+ name: Coverage
+ # Coverage isn't required for the merge queue.
+ if: github.event_name != 'merge_group'
+ permissions:
+ contents: read
+ uses: ./.github/workflows/reusable-coverage.yml
+ secrets:
+ codecov-token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
- steps:
- - name: Checkout
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- with:
- fetch-depth: 0
- submodules: recursive
-
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
- with:
- global-json-file: global.json
-
- - name: Cache NuGet packages
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- path: ~/.nuget/packages
- key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
- restore-keys: |
- ${{ runner.os }}-nuget-
-
- - name: Restore
- run: dotnet restore
-
- - name: Build
- run: dotnet build -c Release --no-restore
-
- - name: Test
- run: dotnet test -c Release --no-build --report-github --ignore-exit-code 8
-
- - name: aot-publish test
- run: |
- dotnet publish ./samples/AspNetCore/Samples.AspNetCore.csproj
+ format:
+ name: Format
+ permissions:
+ contents: read
+ uses: ./.github/workflows/reusable-format.yml
+
+ e2e:
+ name: E2E
+ permissions:
+ contents: read
+ uses: ./.github/workflows/reusable-e2e.yml
+
+ aot:
+ name: AOT
+ permissions:
+ contents: read
+ uses: ./.github/workflows/reusable-aot.yml
packaging:
+ name: Packaging
needs: build
-
permissions:
contents: read
packages: write
- id-token: write
- attestations: write
+ uses: ./.github/workflows/reusable-packaging.yml
+
+ ci-gate:
+ name: CI Gate
+ if: always()
+ needs: [build, coverage, format, e2e, aot, packaging]
+
+ permissions:
+ contents: read
runs-on: ubuntu-latest
+ timeout-minutes: 5
steps:
- - name: Checkout
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- with:
- fetch-depth: 0
- submodules: recursive
-
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
- with:
- global-json-file: global.json
-
- - name: Cache NuGet packages
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- path: ~/.nuget/packages
- key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
- restore-keys: |
- ${{ runner.os }}-nuget-
-
- - name: Restore
- run: dotnet restore
-
- - name: Pack NuGet packages (CI versions)
- if: startsWith(github.ref, 'refs/heads/')
- run: dotnet pack -c Release --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}"
-
- - name: Pack NuGet packages (PR versions)
- if: startsWith(github.ref, 'refs/pull/')
- run: dotnet pack -c Release --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}"
-
- - name: Publish NuGet packages (base)
- if: github.event.pull_request.head.repo.fork == false
- run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.GITHUB_TOKEN }}" --source https://nuget.pkg.github.com/open-feature/index.json
-
- - name: Publish NuGet packages (fork)
- if: github.event.pull_request.head.repo.fork == true
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- with:
- name: nupkgs
- path: src/**/*.nupkg
+ - name: Check that every gated job succeeded
+ env:
+ NEEDS_JSON: ${{ toJSON(needs) }}
+ # Jobs allowed to report `skipped`. Anything else that skips fails the gate, so an
+ # accidental `if:` cannot quietly stop gating a job.
+ ALLOWED_SKIPS: coverage
+ run: |
+ {
+ echo "### CI Gate"
+ echo ""
+ echo "| Job | Result |"
+ echo "| --- | --- |"
+ echo "$NEEDS_JSON" | jq -r 'to_entries[] | "| \(.key) | \(.value.result) |"'
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ failing=$(echo "$NEEDS_JSON" | jq -r --arg allowed "$ALLOWED_SKIPS" '
+ ($allowed | split(",") | map(select(length > 0))) as $ok
+ | to_entries[]
+ | select(.value.result != "success")
+ | select(.value.result != "skipped" or ([.key] - $ok | length) > 0)
+ | "\(.key): \(.value.result)"
+ ')
+
+ if [ -n "$failing" ]; then
+ echo "::error::One or more gated jobs did not succeed:"
+ echo "$failing"
+ exit 1
+ fi
+
+ echo "All gated jobs succeeded or were intentionally skipped."
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
deleted file mode 100644
index 5aed21c6..00000000
--- a/.github/workflows/e2e.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-name: E2E Test
-
-on:
- pull_request:
- types:
- - opened
- - synchronize
- - reopened
- branches:
- - main
- merge_group:
-
-jobs:
- e2e-tests:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- pull-requests: write
- steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- with:
- fetch-depth: 0
-
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
- with:
- global-json-file: global.json
-
- - name: Cache NuGet packages
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- path: ~/.nuget/packages
- key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
- restore-keys: |
- ${{ runner.os }}-nuget-
-
- - name: Initialize Tests
- run: |
- git submodule update --init --recursive
- cp spec/specification/assets/gherkin/*.feature test/OpenFeature.E2ETests/Features/
-
- - name: Run Tests
- run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --report-github
diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml
index 3778dbcd..483d05b9 100644
--- a/.github/workflows/lint-pr.yml
+++ b/.github/workflows/lint-pr.yml
@@ -1,6 +1,8 @@
name: "Lint PR"
on:
+ # Required for the merge queue mandatory check
+ merge_group:
pull_request_target:
types:
- opened
@@ -10,11 +12,16 @@ on:
jobs:
main:
name: Validate PR title
- runs-on: ubuntu-latest
+ runs-on: ubuntu-slim
permissions:
pull-requests: write
steps:
+ - name: Skip for merge queue
+ if: github.event_name == 'merge_group'
+ run: echo "PR title lint is not applicable to merge_group; passing required check."
+
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6
+ if: github.event_name == 'pull_request_target'
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -22,7 +29,7 @@ jobs:
- uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
- if: always() && (steps.lint_pr_title.outputs.error_message != null)
+ if: always() && github.event_name == 'pull_request_target' && steps.lint_pr_title.outputs.error_message != null
with:
header: pr-title-lint-error
message: |
@@ -35,7 +42,7 @@ jobs:
${{ steps.lint_pr_title.outputs.error_message }}
```
# Delete a previous comment when the issue has been resolved
- - if: ${{ steps.lint_pr_title.outputs.error_message == null }}
+ - if: github.event_name == 'pull_request_target' && steps.lint_pr_title.outputs.error_message == null
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: pr-title-lint-error
diff --git a/.github/workflows/aot-compatibility.yml b/.github/workflows/reusable-aot.yml
similarity index 72%
rename from .github/workflows/aot-compatibility.yml
rename to .github/workflows/reusable-aot.yml
index d5ed0bb5..9b177443 100644
--- a/.github/workflows/aot-compatibility.yml
+++ b/.github/workflows/reusable-aot.yml
@@ -1,18 +1,15 @@
-name: AOT Compatibility
+name: Reusable / AOT Compatibility
on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
- merge_group:
- workflow_dispatch:
+ workflow_call:
jobs:
- aot-compatibility:
- name: AOT Test (${{ matrix.os }}, ${{ matrix.arch }})
+ aot:
+ name: ${{ matrix.runtime }}
+
permissions:
contents: read
+
strategy:
fail-fast: false
matrix:
@@ -50,20 +47,12 @@ jobs:
with:
fetch-depth: 0
submodules: recursive
+ persist-credentials: false
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
- with:
- global-json-file: global.json
-
- - name: Cache NuGet packages
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ - name: Setup .NET
+ uses: ./.github/actions/setup-dotnet
with:
- path: ~/.nuget/packages
- key: ${{ runner.os }}-${{ matrix.arch }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
- restore-keys: |
- ${{ runner.os }}-${{ matrix.arch }}-nuget-
- ${{ runner.os }}-nuget-
+ cache-key-suffix: -${{ matrix.arch }}
- name: Restore dependencies
shell: pwsh
diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml
new file mode 100644
index 00000000..68fa9004
--- /dev/null
+++ b/.github/workflows/reusable-build.yml
@@ -0,0 +1,43 @@
+name: Reusable / Build & Test
+
+# Callable building block for ci.yml. `workflow_call` is the only trigger on purpose: a
+# reusable workflow that also triggered on its own would run twice for every event.
+
+on:
+ workflow_call:
+
+jobs:
+ build:
+ name: ${{ matrix.os }}
+
+ permissions:
+ contents: read
+
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ fetch-depth: 0
+ submodules: recursive
+ persist-credentials: false
+
+ - name: Setup .NET
+ uses: ./.github/actions/setup-dotnet
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build -c Release --no-restore
+
+ - name: Test
+ run: dotnet test -c Release --no-build --report-github --ignore-exit-code 8
+
+ - name: aot-publish test
+ run: dotnet publish ./samples/AspNetCore/Samples.AspNetCore.csproj
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/reusable-coverage.yml
similarity index 67%
rename from .github/workflows/code-coverage.yml
rename to .github/workflows/reusable-coverage.yml
index c11d5052..d76eeb7e 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/reusable-coverage.yml
@@ -1,20 +1,19 @@
-name: Code Coverage
+name: Reusable / Code Coverage
on:
- push:
- branches: [main]
- paths-ignore:
- - "**.md"
- pull_request:
- branches: [main]
- paths-ignore:
- - "**.md"
+ workflow_call:
+ secrets:
+ codecov-token:
+ description: "Codecov upload token."
+ required: true
jobs:
- build-test-report:
+ coverage:
+ name: ${{ matrix.os }}
+
permissions:
contents: read
- pull-requests: write
+
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
@@ -22,22 +21,14 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
+ persist-credentials: false
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
- with:
- global-json-file: global.json
-
- - name: Cache NuGet packages
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- path: ~/.nuget/packages
- key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }}
- restore-keys: |
- ${{ runner.os }}-nuget-
+ - name: Setup .NET
+ uses: ./.github/actions/setup-dotnet
- name: Run Test
run: dotnet test --coverlet --coverlet-output-format opencover --coverlet-exclude "[GitHubActionsTestLogger*]*" --ignore-exit-code 8
@@ -52,7 +43,7 @@ jobs:
disable_search: true
fail_ci_if_error: true
verbose: true
- token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
+ token: ${{ secrets.codecov-token }}
# On Windows, avoid passing a glob that expands into multiple args
- name: Find coverage reports (Windows)
@@ -77,4 +68,4 @@ jobs:
disable_search: true
fail_ci_if_error: true
verbose: true
- token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
+ token: ${{ secrets.codecov-token }}
diff --git a/.github/workflows/reusable-e2e.yml b/.github/workflows/reusable-e2e.yml
new file mode 100644
index 00000000..e6c73527
--- /dev/null
+++ b/.github/workflows/reusable-e2e.yml
@@ -0,0 +1,30 @@
+name: Reusable / E2E Tests
+
+on:
+ workflow_call:
+
+jobs:
+ e2e:
+ name: dotnet test
+
+ permissions:
+ contents: read
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ fetch-depth: 0
+ submodules: recursive
+ persist-credentials: false
+
+ - name: Setup .NET
+ uses: ./.github/actions/setup-dotnet
+
+ - name: Initialize Tests
+ run: cp spec/specification/assets/gherkin/*.feature test/OpenFeature.E2ETests/Features/
+
+ - name: Run Tests
+ run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --report-github
diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/reusable-format.yml
similarity index 59%
rename from .github/workflows/dotnet-format.yml
rename to .github/workflows/reusable-format.yml
index 79ee5d76..6518694b 100644
--- a/.github/workflows/dotnet-format.yml
+++ b/.github/workflows/reusable-format.yml
@@ -1,26 +1,25 @@
-name: dotnet format
+name: Reusable / Format
on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
+ workflow_call:
jobs:
- check-format:
- runs-on: ubuntu-latest
+ format:
+ name: dotnet format
+
permissions:
contents: read
- pull-requests: write
+
+ runs-on: ubuntu-latest
steps:
- - name: Check out code
+ - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
-
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
- global-json-file: global.json
+ persist-credentials: false
+
+ - name: Setup .NET
+ uses: ./.github/actions/setup-dotnet
- name: dotnet format
run: |
diff --git a/.github/workflows/reusable-packaging.yml b/.github/workflows/reusable-packaging.yml
new file mode 100644
index 00000000..a556033d
--- /dev/null
+++ b/.github/workflows/reusable-packaging.yml
@@ -0,0 +1,69 @@
+name: Reusable / Packaging
+
+on:
+ workflow_call:
+
+jobs:
+ packaging:
+ name: dotnet pack
+
+ permissions:
+ contents: read
+ packages: write
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ fetch-depth: 0
+ submodules: recursive
+ persist-credentials: false
+
+ - name: Setup .NET
+ uses: ./.github/actions/setup-dotnet
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Compute version suffix
+ id: suffix
+ env:
+ EVENT_NAME: ${{ github.event_name }}
+ run: |
+ case "$EVENT_NAME" in
+ push) prefix=ci ;;
+ pull_request) prefix=pr ;;
+ *) prefix=mq ;;
+ esac
+ echo "value=${prefix}.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" >> "$GITHUB_OUTPUT"
+
+ # Always pack, even when nothing is published: a broken pack is a real build break
+ # and should fail the gate rather than only surfacing at release time.
+ - name: Pack NuGet packages
+ env:
+ VERSION_SUFFIX: ${{ steps.suffix.outputs.value }}
+ run: dotnet pack -c Release --no-restore --version-suffix "$VERSION_SUFFIX"
+
+ # `github.event.pull_request` is absent outside `pull_request`, and GitHub coerces the
+ # missing value to 0 when comparing against a boolean, so `... .fork == false` is also
+ # true on `push` and `merge_group`. The event name must therefore be checked
+ # explicitly, otherwise every merge queue entry would publish a package for a
+ # `gh-readonly-queue/*` ref that is thrown away moments later.
+ - name: Publish NuGet packages (base)
+ if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
+ env:
+ NUGET_API_KEY: ${{ secrets.GITHUB_TOKEN }}
+ run: dotnet nuget push "src/**/*.nupkg" --api-key "$NUGET_API_KEY" --source https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Publish NuGet packages (fork)
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: nupkgs
+ path: src/**/*.nupkg
+ # The artifact is the only way a fork contributor gets the packages, so an empty
+ # upload should fail rather than warn: pack succeeded but produced nothing means
+ # something changed about packability, and it would otherwise go unnoticed.
+ if-no-files-found: error
diff --git a/OpenFeature.slnx b/OpenFeature.slnx
index e3f02767..dee775bf 100644
--- a/OpenFeature.slnx
+++ b/OpenFeature.slnx
@@ -15,6 +15,10 @@
+
+
+
+
@@ -22,12 +26,15 @@
-
-
-
-
+
+
+
+
+
+
+