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
16 changes: 12 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,19 @@ jobs:
- name: Resolve version
id: ver
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.tag }}" ]; then
echo "version=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
RAW="${{ inputs.tag }}"
else
echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
RAW="${GITHUB_REF_NAME}"
fi
# Keep tag with v for GitHub Release; strip for package/image labels
TAG="$RAW"
case "$TAG" in v*) ;; *) TAG="v$TAG" ;; esac
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved tag=$TAG version=$VERSION"

- uses: actions/setup-dotnet@v4
with:
Expand Down Expand Up @@ -78,8 +86,8 @@ jobs:
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.version }}
name: NotificationHub ${{ steps.ver.outputs.version }}
tag_name: ${{ steps.ver.outputs.tag }}
name: NotificationHub ${{ steps.ver.outputs.tag }}
generate_release_notes: true
files: |
publish/**
Expand Down
140 changes: 140 additions & 0 deletions .github/workflows/version.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: Version bump (SemVer)

on:
pull_request:
types: [closed]
branches: [dev]
workflow_dispatch:
inputs:
bump:
description: "Force bump level (major|minor|patch|auto)"
required: false
default: auto

permissions:
contents: write

concurrency:
group: version-dev
cancel-in-progress: false

jobs:
bump:
name: Compute SemVer and tag
runs-on: ubuntu-latest
# Merged PR into dev, or manual
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true)
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: dev

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Compute next version
id: semver
env:
FORCE_BUMP: ${{ github.event.inputs.bump || 'auto' }}
run: |
set -euo pipefail
CURRENT=$(python3 -c "import json; print(json.load(open('version.json'))['version'])")
echo "version.json=$CURRENT"

LAST_TAG=$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
RANGE="${LAST_TAG}..HEAD"
LOG=$(git log --pretty=format:'%s' "$RANGE")
else
LOG=$(git log --pretty=format:'%s' -30)
fi

BUMP="patch"
if echo "$LOG" | grep -Eiq '^(feat|fix|perf|refactor|chore|docs|test|ci|build|style)(\(.+\))?!:|BREAKING CHANGE:'; then
BUMP="major"
elif echo "$LOG" | grep -Eiq '^feat(\(.+\))?:'; then
BUMP="minor"
fi
case "${FORCE_BUMP}" in major|minor|patch) BUMP="$FORCE_BUMP" ;; esac

IFS=. read -r MA MI PA <<< "$CURRENT"
MA=${MA:-0}; MI=${MI:-0}; PA=${PA:-0}
case "$BUMP" in
major) MA=$((MA+1)); MI=0; PA=0 ;;
minor) MI=$((MI+1)); PA=0 ;;
patch) PA=$((PA+1)) ;;
esac
NEXT="${MA}.${MI}.${PA}"
TAG="v${NEXT}"

if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists — skip (immutable)"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi

# Avoid tag spam if only chore(release)/merge noise
if [ -z "$LOG" ]; then
echo "No commits in range — skip"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "bump=$BUMP" >> "$GITHUB_OUTPUT"
echo "version=$NEXT" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "Next $TAG ($BUMP)"

- name: Update version files and tag
if: steps.semver.outputs.skip != 'true'
env:
NEXT: ${{ steps.semver.outputs.version }}
TAG: ${{ steps.semver.outputs.tag }}
run: |
set -euo pipefail
python3 - <<'PY'
import json, pathlib, re, os
next_v = os.environ["NEXT"]
pathlib.Path("version.json").write_text(json.dumps({
"version": next_v,
"scheme": "semver",
"product": "NotificationHub"
}, indent=2) + "\n")
p = pathlib.Path("Directory.Build.props")
t = p.read_text()
t = re.sub(r"<VersionPrefix>[^<]+</VersionPrefix>", f"<VersionPrefix>{next_v}</VersionPrefix>", t)
t = re.sub(r"<AssemblyVersion>[^<]+</AssemblyVersion>", f"<AssemblyVersion>{next_v}.0</AssemblyVersion>", t)
p.write_text(t)
print("files ->", next_v)
PY
git add version.json Directory.Build.props
git commit -m "chore(release): bump version to ${NEXT}" || true
git tag -a "$TAG" -m "Release ${TAG}"

- name: Push tag (and version commit if allowed)
if: steps.semver.outputs.skip != 'true'
env:
# Optional: repo secret VERSION_BUMP_TOKEN (PAT with contents:write that can push to protected dev)
BUMP_TOKEN: ${{ secrets.VERSION_BUMP_TOKEN }}
run: |
set -euo pipefail
TAG="${{ steps.semver.outputs.tag }}"
if [ -n "${BUMP_TOKEN:-}" ]; then
git remote set-url origin "https://x-access-token:${BUMP_TOKEN}@github.com/${{ github.repository }}.git"
git push origin HEAD:dev
git push origin "$TAG"
else
# GITHUB_TOKEN cannot bypass required status checks on protected branches.
# Push tag only from current commit so release.yml still runs.
git push origin "$TAG" || {
echo "::warning::Could not push version commit to protected dev. Tag-only push attempted."
# Tag the pre-commit HEAD if commit couldn't be pushed
git push origin "$TAG"
}
fi
67 changes: 67 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Contributing to NotificationHub

Thanks for helping improve the project.

## Branching

- **`dev`** — integration branch (protected: required status checks, no force-push/delete).
- Feature work: branch from `dev`, open a PR **into `dev`**.
- Do not force-push `dev`.

## Commit messages

Follow **Conventional Commits**. Full guide: [docs/ops/commit-conventions.md](docs/ops/commit-conventions.md).

```text
type(scope): subject
```

Examples: `feat(campaigns): add CSV import`, `fix(ef): idempotent Broadcast migration`, `ci(security): pin trivy-action`.

Breaking changes: `feat(api)!: ...` and/or footer `BREAKING CHANGE:`.

## Versioning

Product version is **SemVer 2.0.0**. Guide: [docs/ops/versioning.md](docs/ops/versioning.md).

- Source of truth: `version.json` + `Directory.Build.props`
- On merge to `dev`, [version.yml](.github/workflows/version.yml) may create tag `vX.Y.Z`
- Tags trigger [release.yml](.github/workflows/release.yml) (GitHub Release + GHCR image)
- Runtime: `GET /api/v1/version`

| Signal | Bump |
|--------|------|
| Breaking (`!` / `BREAKING CHANGE`) | MAJOR |
| `feat` | MINOR |
| `fix` / `perf` / `docs` / … | PATCH |

## Checks required on `dev`

PRs must pass (among others):

- Build and Test
- Unit tests (all)
- dotnet format verify
- Build Docker Image
- Trivy scan (Docker image)
- NuGet vulnerability audit

Locally:

```bash
dotnet restore
dotnet format --verify-no-changes --severity error
dotnet test
```

## Docs

- Architecture: [docs/README.md](docs/README.md) (ADRs)
- Ops runbooks: [docs/ops/](docs/ops/)
- Plugin SDK: [docs/sdk/plugin-sdk.md](docs/sdk/plugin-sdk.md)

When you change architecture, update or add an ADR. When you change process (commits, versioning, CI), update the ops docs in the same PR.

## License

By contributing, you agree that your contributions are licensed under the same **MIT** license as the repository.
9 changes: 9 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<Deterministic>true</Deterministic>

<!-- Unified SemVer (see version.json + docs/ops/versioning.md) -->
<VersionPrefix>0.1.0</VersionPrefix>
<Version>$(VersionPrefix)</Version>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<FileVersion>$(VersionPrefix).0</FileVersion>
<InformationalVersion>$(VersionPrefix)+$(SourceRevisionId)</InformationalVersion>
<SourceRevisionId Condition="'$(SourceRevisionId)' == ''">local</SourceRevisionId>

<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>

<!--
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,14 @@ Domain unit tests (no database):
dotnet test tests/NotificationHub.Domain.Tests
```


## Contributing

- **Commits:** [Conventional Commits](docs/ops/commit-conventions.md) — `type(scope): subject` (e.g. `feat(campaigns): ...`, `fix(ef): ...`, `ci(security): ...`).
- **Versioning:** [SemVer 2.0.0](docs/ops/versioning.md) — `version.json` + tags `vMAJOR.MINOR.PATCH`; merges to `dev` drive automated bumps; releases at [GitHub Releases](https://github.com/0x-mhmdnzri/NotificationHub/releases).
- **Runtime version:** `GET /api/v1/version`
- Full guide: [CONTRIBUTING.md](CONTRIBUTING.md)

## License

[MIT](LICENSE) — use it, fork it, ship it.
Expand Down
81 changes: 81 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Security Policy

## Supported versions

Security fixes are applied to the **`dev`** integration branch and to the latest published release tag (`vMAJOR.MINOR.PATCH`).

| Version | Supported |
|---------|-----------|
| Latest release (`v*`) | ✅ |
| `dev` branch | ✅ |
| Older tags | ❌ Best-effort only |

If you run an older image or build, upgrade to the latest release when a security advisory is published.

## Reporting a vulnerability

**Please do not open a public GitHub Issue for security vulnerabilities.**

Report privately so we can fix the issue before it is disclosed:

1. Use **[GitHub Private Vulnerability Reporting](https://github.com/0x-mhmdnzri/NotificationHub/security/advisories/new)** (preferred), or
2. Email the maintainer if private reporting is unavailable: contact via the profile of [@0x-mhmdnzri](https://github.com/0x-mhmdnzri).

### What to include

- Description of the issue and impact
- Affected component (API, Host, plugin, worker, admin UI, dependency, …)
- Steps to reproduce or a proof of concept
- Suggested severity (optional)
- Your preferred credit name (optional)

### What we will do

| Step | Target |
|------|--------|
| Acknowledge receipt | Within **72 hours** |
| Initial assessment | Within **7 days** |
| Fix or mitigation plan | Depends on severity; critical issues prioritized |
| Public disclosure | Coordinated after a fix is available (or risk is accepted) |

We may ask for more detail. Please do not share exploit details publicly until a fixed release is out (or we agree otherwise).

## Scope

In scope examples:

- Authentication / API key handling
- Injection, SSRF, unsafe deserialization in the Host or plugins
- Privilege escalation across tenants or roles
- Secrets leakage in logs or responses
- High/Critical issues in **direct** production dependencies that we can upgrade or mitigate

Out of scope (unless there is a clear, exploitable impact on this project):

- Denial of service requiring unrealistic traffic volumes
- Issues only in outdated/unsupported deployments
- Vulnerabilities solely in third-party services (SendGrid, Twilio, …) with no project-side misconfiguration
- Social engineering or physical attacks

## Safe harbor

We will not pursue legal action against researchers who:

- Make a good-faith effort to avoid privacy violations, data destruction, and service disruption
- Report findings promptly and privately
- Do not exploit the issue beyond what is needed to demonstrate it

## Hardening references

Project security-related notes (operational, not a substitute for this policy):

- [docs/ops/security-hardening-phase0.md](docs/ops/security-hardening-phase0.md)
- CI: NuGet vulnerability audit, Trivy image scan, CodeQL (see `.github/workflows/security.yml`)

## Preferences for patches

- Prefer minimal, well-tested fixes
- Follow [Conventional Commits](docs/ops/commit-conventions.md); security fixes are typically `fix(security): ...`
- Versioning follows [SemVer](docs/ops/versioning.md); security fixes without contract breaks are **PATCH** releases

Thank you for helping keep NotificationHub and its users safe.
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ ADRs capture **why** a decision was made, alternatives considered, and consequen

Practical runbooks under [`ops/`](ops/):

- [commit-conventions.md](ops/commit-conventions.md) — Conventional Commits (`feat`, `fix`, `ci`, …)
- [versioning.md](ops/versioning.md) — SemVer 2.0.0, tags `vX.Y.Z`, release workflow, `GET /api/v1/version`
- [orchestration-otel-aspire.md](ops/orchestration-otel-aspire.md) — Aspire topology, Serilog, Jaeger, health
- [messaging-reliability.md](ops/messaging-reliability.md) — outbox, ack, DLQ, delayed redelivery
- [prefetch-tuning.md](ops/prefetch-tuning.md), [latency.md](ops/latency.md)
- Security hardening notes (`security-hardening-*.md`)

Contributor overview: [CONTRIBUTING.md](../CONTRIBUTING.md).

## SDK

- [plugin-sdk.md](sdk/plugin-sdk.md) — implementing channel plugins
Loading
Loading