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
18 changes: 14 additions & 4 deletions .github/PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
# Secrets & publishing

Release by pushing a version tag:
## Branch flow

- **`dev`** — day-to-day development; the `CI` workflow (lint + tests) runs on push and PRs.
- **`main`** — release branch; merge `dev` → `main` after bumping versions with `scripts/set-version.sh`.
- **`publish.yml`** — runs on push to `main` and on `v*` tags; publishes PyPI, npm, and NuGet packages.

## Release

Bump versions, merge to `main`, or push a version tag:

```bash
git tag v0.1.0
git push origin v0.1.0
./scripts/set-version.sh 1.2.7
git tag v1.2.7
git push origin main --tags
```

That triggers:
That triggers `publish.yml`, which calls:

- `publish-pypi.yml` → PyPI package `fusion-framework`
- `publish-npm.yml` → npm package `fusion-framework`
- `publish-nuget.yml` → NuGet package `Fusion-Framework`

## Required GitHub configuration

Expand Down
57 changes: 56 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# CI runs on `dev` only. Feature branches open PRs against `dev`; releases merge `dev` → `main`
# (see publish.yml). `main` does not run this workflow.

name: CI

on:
push:
branches: [master, main]
branches: [dev]
pull_request:
branches: [dev]

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
Expand All @@ -13,8 +17,56 @@ env:
CARGO_TERM_COLOR: always

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- uses: Swatinem/rust-cache@v2

- name: rustfmt
run: cargo fmt --all -- --check

- name: clippy
# fusion-ffi uses raw FFI pointers; tighten to `-D warnings` once FFI lints are addressed.
run: cargo clippy --workspace --all-targets

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install ruff
run: python -m pip install ruff

- name: ruff check
run: ruff check crates/fusion-py/python

- name: ruff format
run: ruff format --check crates/fusion-py/python

# Node binding is a thin CommonJS wrapper over N-API; no eslint config in this repo.
- uses: actions/setup-node@v4
with:
node-version: "22"

- name: node syntax check
run: node --check crates/fusion-node/index.js

- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"

- name: dotnet format
run: dotnet format --verify-no-changes bindings/csharp/FusionFramework/FusionFramework.csproj

rust:
name: Rust workspace
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -27,6 +79,7 @@ jobs:

python:
name: Python wheel (${{ matrix.os }})
needs: lint
strategy:
fail-fast: false
matrix:
Expand All @@ -51,6 +104,7 @@ jobs:

node:
name: Node addon (${{ matrix.settings.target }})
needs: lint
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -84,6 +138,7 @@ jobs:

csharp:
name: C# package
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: Publish npm

on:
push:
tags:
- "v*"
workflow_call:
workflow_dispatch:

# Trusted Publishing via OIDC — no NPM_TOKEN secret needed.
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/publish-nuget.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: Publish NuGet

on:
push:
tags:
- "v*"
workflow_call:
workflow_dispatch:

# Trusted Publishing via OIDC — no NUGET_API_KEY secret needed.
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: Publish PyPI

on:
push:
tags:
- "v*"
workflow_call:
workflow_dispatch:

# Trusted Publishing via OIDC — no PYPI_API_TOKEN secret needed.
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Release flow: merge `dev` → `main` after bumping versions with scripts/set-version.sh.
# This workflow publishes PyPI, npm, and NuGet packages when code lands on `main` or on `v*` tags.
# Tag pushes use the tag for package versions; `main` pushes use manifest versions as-is.
# CI (lint + tests) runs on `dev` only — see ci.yml.

name: Publish

on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:

concurrency:
group: publish-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
pypi:
uses: ./.github/workflows/publish-pypi.yml
secrets: inherit

npm:
uses: ./.github/workflows/publish-npm.yml
secrets: inherit

nuget:
uses: ./.github/workflows/publish-nuget.yml
secrets: inherit

# TODO: enable when crates.io publishing and API token / trusted publishing are configured.
crates-io:
if: false
runs-on: ubuntu-latest
steps:
- run: echo "TODO cargo publish workspace crates (fusion-core is not published yet)"
92 changes: 92 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions bindings/csharp/FusionFramework/ContentNegotiation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace FusionFramework;

/// <summary>Shared content negotiation helpers (fusion-core parity).</summary>
internal static class ContentNegotiation
{
public static bool PrefersJson(string? accept, string? formatQuery)
{
if (string.Equals(formatQuery, "json", StringComparison.OrdinalIgnoreCase))
return true;

accept = accept?.Trim();
if (string.IsNullOrEmpty(accept))
return false;

var bestJson = -1.0f;
var bestHtml = -1.0f;

foreach (var part in accept.Split(','))
{
var tokens = part.Trim().Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var media = tokens.Length > 0 ? tokens[0].ToLowerInvariant() : "";
var q = 1.0f;
for (var i = 1; i < tokens.Length; i++)
{
if (tokens[i].StartsWith("q=", StringComparison.OrdinalIgnoreCase)
&& float.TryParse(tokens[i][2..], out var parsed))
{
q = parsed;
}
}

switch (media)
{
case "application/json":
case "text/json":
bestJson = Math.Max(bestJson, q);
break;
case "text/html":
case "application/xhtml+xml":
bestHtml = Math.Max(bestHtml, q);
break;
}
}

return bestJson > 0 && bestJson >= bestHtml;
}
}
Loading
Loading