Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b074338
feat(abstractions): add shared ValiFlowDiagnostics observability helper
UBF21 Oct 4, 2026
8a5b4ac
feat(vali-flow): add GenericRepository, extract QuerySpecificationBui…
UBF21 Oct 4, 2026
29ce856
refactor(inmemory): split evaluator into partial files, fix Update le…
UBF21 Oct 4, 2026
9d38d2b
fix(sql): prevent SQL injection via identifier guard, fix builder bug…
UBF21 Oct 4, 2026
4187c9f
fix(nosql): translate closure-captured null values to NullNode like l…
UBF21 Oct 4, 2026
2ae8b47
fix(nosql): escape RediSearch query injection, cap Mongo/Elasticsearc…
UBF21 Oct 4, 2026
d328fde
feat(nosql): add diagnostics tracing to DynamoDB translator
UBF21 Oct 4, 2026
62d1074
feat(nosql): add Couchbase, CosmosDb, and Firestore providers
UBF21 Oct 4, 2026
0d4cdee
feat(benchmarks): add EF Core benchmark using EFCore.InMemory provider
UBF21 Oct 4, 2026
a562c97
docs: update README and architecture/benchmarks docs for new providers
UBF21 Oct 4, 2026
63af978
fix(vali-flow): preserve tracked entity's PK in UpsertRangeAsync.Part…
UBF21 Oct 5, 2026
8673246
test(vali-flow): raise EF Core evaluator coverage from 74.37% to 97.42%
UBF21 Oct 5, 2026
36d7c59
fix(vali-flow): serialize concurrent BulkInsertOrUpdateAsync calls pe…
UBF21 Oct 5, 2026
4750f1a
fix(vali-flow): generic transient retry for bulk upsert concurrency a…
UBF21 Oct 6, 2026
8383ed1
fix(vali-flow): raise BulkUpsertRetryPolicy default budget from 4 to …
UBF21 Oct 6, 2026
d946be1
fix(vali-flow): raise BulkUpsertRetryPolicy budget from 7 to 12 attempts
UBF21 Oct 6, 2026
06c7483
fix(vali-flow): gate bulk upsert contention per table, not per Update…
UBF21 Oct 6, 2026
2202ca7
fix(tests): eliminate flaky Activity-capture tests across Sql/EF-Core…
UBF21 Oct 6, 2026
14b0705
fix(nosql-couchbase): bind decimal values as native N1QL numbers, not…
UBF21 Oct 6, 2026
193756b
docs: add release pipeline design spec
UBF21 Oct 6, 2026
c38d0f2
chore: gitignore .superpowers/ (subagent-driven-development scratch w…
UBF21 Oct 6, 2026
f16c2bb
chore(abstractions): bump version to 1.2.0 for release
UBF21 Oct 6, 2026
409e671
chore(wave-2): bump versions and re-point Vali-Flow.Abstractions to P…
UBF21 Oct 6, 2026
7c771bb
docs: add release pipeline implementation plan
UBF21 Oct 6, 2026
82a3480
chore(wave-3): bump versions and re-point Vali-Flow.NoSql to PackageR…
UBF21 Oct 6, 2026
66b0f3c
fix(packaging): add missing VF_logo.png to Couchbase/CosmosDb/Firestore
UBF21 Oct 6, 2026
0736c8d
docs: catch up CHANGELOG.md for Oct 2-6 work (3 new NoSQL providers, …
UBF21 Oct 6, 2026
6cf6f5a
docs: state .NET 10 forward compatibility wherever .NET 8/9 is mentioned
UBF21 Oct 6, 2026
87e90f7
ci: add reusable CI workflow (build, test matrix, 95% coverage gate, …
UBF21 Oct 6, 2026
1b94921
ci: add manual release workflow (3-wave dependency-ordered NuGet publ…
UBF21 Oct 6, 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
366 changes: 366 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,366 @@
name: CI

# Pull requests, pushes to main and reuse from release.yml (workflow_call). Single required check
# for branch protection: "CI OK".
on:
pull_request:
push:
branches: [main]
workflow_call:
inputs:
version-suffix:
description: Prerelease label appended to every package version (for example rc.1). Empty packs the versions declared in the csproj files.
type: string
default: ''

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true

jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
9.0.x

- name: Restore
run: dotnet restore vali-flow.sln

- name: Build
run: dotnet build vali-flow.sln -c Release --no-restore

test:
name: test / ${{ matrix.os }} / ${{ matrix.tfm }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
tfm: [net8.0, net9.0]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
9.0.x

- name: Environment
shell: bash
run: |
mkdir -p logs
{
echo "### ${{ matrix.os }} / ${{ matrix.tfm }}"
uname -a || true
dotnet --info
} 2>&1 | tee logs/00-environment.log

- name: Restore
shell: bash
run: |
set -o pipefail
dotnet restore vali-flow.sln 2>&1 | tee logs/01-restore.log

- name: Build
shell: bash
run: |
set -o pipefail
dotnet build vali-flow.sln -c Release --no-restore -v:minimal -bl:logs/build.binlog 2>&1 | tee logs/02-build.log

# The 11 test projects, one per package family (Vali-Flow.NoSql.Tests also covers the
# MongoDB translator — there is no separate Vali-Flow.NoSql.MongoDB.Tests project).
- name: Unit tests with coverage on ${{ matrix.tfm }}
shell: bash
run: |
set -o pipefail
status=0
projects="Vali-Flow.Abstractions.Tests Vali-Flow.Tests Vali-Flow.InMemory.Tests Vali-Flow.Sql.Tests Vali-Flow.NoSql.Tests Vali-Flow.NoSql.Elasticsearch.Tests Vali-Flow.NoSql.Redis.Tests Vali-Flow.NoSql.DynamoDB.Tests Vali-Flow.NoSql.Couchbase.Tests Vali-Flow.NoSql.CosmosDb.Tests Vali-Flow.NoSql.Firestore.Tests"
for project in $projects; do
echo "::group::$project on ${{ matrix.tfm }}"
dotnet test "$project" -c Release --no-build -f "${{ matrix.tfm }}" \
--logger "console;verbosity=detailed" \
--logger "trx;LogFileName=$project-${{ matrix.tfm }}.trx" \
--collect:"XPlat Code Coverage" --results-directory "TestResults/${{ matrix.tfm }}/$project" \
2>&1 | tee "logs/03-test-$project.log" || status=1
echo "::endgroup::"
done
exit $status

- name: Test totals
if: always()
shell: bash
run: |
python3 - <<'PY'
import glob
import os
import xml.etree.ElementTree as ET

rows = ["| Project | Total | Passed | Failed | Skipped |", "|---|---:|---:|---:|---:|"]
for path in sorted(glob.glob("TestResults/**/*.trx", recursive=True)):
counters = ET.parse(path).getroot().find(".//{*}Counters")
if counters is None:
continue
name = os.path.basename(path)[:-4]
failed = int(counters.get("failed", 0))
skipped = int(counters.get("total", 0)) - int(counters.get("executed", 0))
rows.append(f"| {name} | {counters.get('total')} | {counters.get('passed')} | {failed}{' (!)' if failed else ''} | {skipped} |")
report = "### Tests ${{ matrix.os }} / ${{ matrix.tfm }}\n\n" + "\n".join(rows) + "\n"
print(report)
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as handle:
handle.write(report + "\n")
PY

- name: Upload coverage reports
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.os }}-${{ matrix.tfm }}
path: TestResults/**/coverage.cobertura.xml
if-no-files-found: error
retention-days: 7

- name: Upload full trace
if: always()
uses: actions/upload-artifact@v4
with:
name: trace-${{ matrix.os }}-${{ matrix.tfm }}
path: |
logs/**
TestResults/**/*.trx
if-no-files-found: warn
retention-days: 14

trace:
name: Full trace (single file)
needs: test
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: trace-*
path: trace

- name: Merge every log into one file
shell: bash
run: |
out=full-trace.txt
: > "$out"
while IFS= read -r f; do
rel="${f#trace/}"
{
echo
echo "=================================================================="
echo "== ${rel%%/*} :: ${rel#*/}"
echo "=================================================================="
cat "$f"
} >> "$out"
done < <(find trace -name '*.log' | sort)
wc -l "$out"
{
echo "### Full trace"
echo
echo "Download the **full-trace** artifact of this run: one text file with the environment, restore, build and"
echo "detailed test output of every OS x target framework cell."
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload the single-file trace
uses: actions/upload-artifact@v4
with:
name: full-trace
path: full-trace.txt
if-no-files-found: error
retention-days: 14

coverage:
# NOTE: 95% is a starting gate, not an audited baseline for the 10 packages not measured in the
# session that stood this pipeline up (Vali-Flow itself was: 97.42%). Adjust per-library after
# the first green run shows the real numbers.
name: Coverage gate (>= 95% per library)
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: coverage-*
path: coverage

- name: Merge reports and enforce the gate
env:
THRESHOLD: '95'
run: |
python3 - <<'PY'
import glob
import os
import sys
import xml.etree.ElementTree as ET

LIBS = [
"Vali-Flow.Abstractions", "Vali-Flow", "Vali-Flow.InMemory", "Vali-Flow.Sql",
"Vali-Flow.NoSql", "Vali-Flow.NoSql.MongoDB", "Vali-Flow.NoSql.Elasticsearch",
"Vali-Flow.NoSql.Redis", "Vali-Flow.NoSql.DynamoDB", "Vali-Flow.NoSql.Couchbase",
"Vali-Flow.NoSql.CosmosDb", "Vali-Flow.NoSql.Firestore",
]
THRESHOLD = float(os.environ["THRESHOLD"])
OSES = ["ubuntu-latest", "windows-latest"]
TFMS = ["net8.0", "net9.0"]

missing = [
f"coverage-{os_name}-{tfm}"
for os_name in OSES
for tfm in TFMS
if not glob.glob(f"coverage/coverage-{os_name}-{tfm}/**/coverage.cobertura.xml", recursive=True)
]

lines = {lib: {} for lib in LIBS}
reports = glob.glob("coverage/**/coverage.cobertura.xml", recursive=True)
for path in reports:
for package in ET.parse(path).getroot().iter("package"):
store = lines.get(package.get("name"))
if store is None:
continue
for cls in package.iter("class"):
parts = cls.get("filename").replace("\\", "/").split("/")
file_key = "/".join(parts[-2:])
for line in cls.iter("line"):
key = (file_key, int(line.get("number")))
store[key] = max(store.get(key, 0), int(line.get("hits")))

rows = ["| Library | Lines | Covered | Line coverage | Gate |", "|---|---:|---:|---:|:---:|"]
failed = []
for lib in LIBS:
total = len(lines[lib])
covered = sum(1 for hits in lines[lib].values() if hits > 0)
pct = 100.0 * covered / total if total else 0.0
ok = total > 0 and pct >= THRESHOLD
if not ok:
failed.append(lib)
rows.append(f"| {lib} | {total} | {covered} | {pct:.1f}% | {'pass' if ok else 'FAIL'} |")

cells = ", ".join(f"{o} / {t}" for o in OSES for t in TFMS)
report = (f"### Unit-test line coverage (gate >= {THRESHOLD:g}%, {len(reports)} reports merged)\n\n"
+ "\n".join(rows)
+ f"\n\nExpected cells: {cells}\n"
+ (f"\nMissing reports: {', '.join(missing)}\n" if missing else "\nAll 4 OS x TFM reports present.\n"))
print(report)
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as handle:
handle.write(report + "\n")
if missing:
sys.exit("Coverage gate failed: missing reports for " + ", ".join(missing))
if failed:
sys.exit("Coverage gate failed for: " + ", ".join(failed))
PY

pack:
name: Pack NuGet packages
needs: build
runs-on: ubuntu-latest
env:
VERSION_SUFFIX: ${{ inputs.version-suffix }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
9.0.x

- name: Apply the prerelease label
if: ${{ inputs.version-suffix != '' }}
run: |
if ! [[ "$VERSION_SUFFIX" =~ ^[0-9A-Za-z.-]+$ ]]; then
echo "::error::version-suffix may only contain letters, digits, dots and hyphens"
exit 1
fi
cat > Directory.Build.targets <<'XML'
<Project>
<PropertyGroup Condition="'$(PrereleaseLabel)' != ''">
<Version>$(Version)-$(PrereleaseLabel)</Version>
</PropertyGroup>
</Project>
XML

- name: Pack every package in dependency order
run: |
for project in Vali-Flow.Abstractions Vali-Flow Vali-Flow.InMemory Vali-Flow.Sql Vali-Flow.NoSql Vali-Flow.NoSql.MongoDB Vali-Flow.NoSql.Elasticsearch Vali-Flow.NoSql.Redis Vali-Flow.NoSql.DynamoDB Vali-Flow.NoSql.Couchbase Vali-Flow.NoSql.CosmosDb Vali-Flow.NoSql.Firestore; do
dotnet pack "$project/$project.csproj" -c Release -o artifacts ${VERSION_SUFFIX:+-p:PrereleaseLabel="$VERSION_SUFFIX"}
done

- name: Verify package contents
run: |
python3 - <<'PY'
import glob
import sys
import xml.etree.ElementTree as ET
import zipfile

PACKAGES = {
"Vali-Flow.Abstractions", "Vali-Flow", "Vali-Flow.InMemory", "Vali-Flow.Sql",
"Vali-Flow.NoSql", "Vali-Flow.NoSql.MongoDB", "Vali-Flow.NoSql.Elasticsearch",
"Vali-Flow.NoSql.Redis", "Vali-Flow.NoSql.DynamoDB", "Vali-Flow.NoSql.Couchbase",
"Vali-Flow.NoSql.CosmosDb", "Vali-Flow.NoSql.Firestore",
}
TFMS = {"net8.0", "net9.0"}

errors = []
found = {}
for path in sorted(glob.glob("artifacts/*.nupkg")):
with zipfile.ZipFile(path) as archive:
names = archive.namelist()
root = ET.fromstring(archive.read(next(n for n in names if n.endswith(".nuspec"))))
ns = {"n": root.tag.split("}")[0].strip("{")}
pid = root.find("n:metadata/n:id", ns).text
version = root.find("n:metadata/n:version", ns).text
libs = {n.split("/")[1] for n in names if n.startswith("lib/") and n.endswith(".dll")}
found[pid] = (version, libs)

for pid in sorted(PACKAGES - set(found)):
errors.append(f"{pid}: package missing from artifacts/")
for pid, (version, libs) in sorted(found.items()):
if libs != TFMS:
errors.append(f"{pid} {version}: lib/ has {sorted(libs)}, expected {sorted(TFMS)}")
print(f"{pid} {version}: lib {sorted(libs)}")
if errors:
sys.exit("Package check failed:\n " + "\n ".join(errors))
print("Package check passed.")
PY

- name: Upload packages
uses: actions/upload-artifact@v4
with:
name: nuget-packages
path: artifacts/*
if-no-files-found: error
retention-days: 14

ci-ok:
name: CI OK
if: always()
needs: [build, test, coverage, pack]
runs-on: ubuntu-latest
steps:
- name: Every job must have succeeded
env:
RESULTS: ${{ toJSON(needs.*.result) }}
run: |
echo "$RESULTS"
if echo "$RESULTS" | grep -Eq '"(failure|cancelled|skipped)"'; then
exit 1
fi
Loading
Loading