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
130 changes: 130 additions & 0 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Monorepo prerelease (delta-aware)
# - Packs only packages whose roots changed since the previous push (see Build/packages.manifest.json).
# - Includes global triggers (Build/*, Directory.Build.* etc.) to rebuild all packages when shared build inputs change.
# - FIX: checkout uses fetch-depth: 0 (full history) so git diffs are reliable.
# - Version: <BASE>-preview.<GITHUB_RUN_NUMBER>+sha.<shortSHA>
# - Creates a GitHub prerelease with the generated nupkgs/snupkgs attached.
name: prerelease

on:
push:
branches:
- main
- 'release/*'
- 'lts/*'

permissions:
contents: write

concurrency:
group: prerelease-${{ github.ref_name }}
cancel-in-progress: true

jobs:
build-pack-release:
runs-on: ubuntu-latest
steps:
- name: Checkout (full history for reliable diffs)
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true

- name: Setup .NET (include preview)
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
10.0.x
include-prerelease: true

- name: Cache NuGet
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
restore-keys: |
${{ runner.os }}-nuget-

- name: Detect affected packages (delta since previous push)
id: affected
shell: bash
run: |
set -euo pipefail
# Determine base commit for this push
BASE="${{ github.event.before }}"
if [[ -z "$BASE" || "$BASE" == "0000000000000000000000000000000000000000" ]]; then
# Fallback for first/force push: use initial commit
BASE="$(git rev-list --max-parents=0 HEAD | tail -n1)"
fi
CHANGED="$(git diff --name-only "$BASE" "${GITHUB_SHA}")"
echo "Changed files:"; echo "$CHANGED"

# Load manifest paths
MAP_PATHS="$(jq -r '.packages[].path' Build/packages.manifest.json)"
echo "Manifest paths:"; echo "$MAP_PATHS"

# Global triggers: rebuild ALL packages when shared build inputs change
if echo "$CHANGED" | grep -E '^(Build/|Directory\.Build\.(props|targets)$|Directory\.Packages\.props$|NuGet\.config$|global\.json$)' >/dev/null; then
PATHS_CSV="$(echo "$MAP_PATHS" | paste -sd, -)"
echo "any=true" >> "$GITHUB_OUTPUT"
echo "paths=$PATHS_CSV" >> "$GITHUB_OUTPUT"
echo "Global trigger detected; rebuilding all package roots: $PATHS_CSV"
exit 0
fi

# Per-package detection
AFFECTED=()
while IFS= read -r p; do
[[ -z "$p" ]] && continue
if echo "$CHANGED" | grep -E "^${p}(/|$)" >/dev/null; then
AFFECTED+=("$p")
fi
done <<< "$MAP_PATHS"

if [[ ${#AFFECTED[@]} -eq 0 ]]; then
echo "any=false" >> "$GITHUB_OUTPUT"
echo "paths=" >> "$GITHUB_OUTPUT"
echo "No package paths affected; skipping prerelease."
else
PATHS_CSV=$(IFS=, echo "${AFFECTED[*]}")
echo "any=true" >> "$GITHUB_OUTPUT"
echo "paths=$PATHS_CSV" >> "$GITHUB_OUTPUT"
echo "Affected package roots: $PATHS_CSV"
fi

- name: Compute prerelease version
id: ver
if: steps.affected.outputs.any == 'true'
shell: bash
run: |
BASE=$(tr -d '\r\n' < Build/VERSION)
SHA=$(git rev-parse --short=7 HEAD)
echo "version=${BASE}-preview.${GITHUB_RUN_NUMBER}+sha.${SHA}" >> "$GITHUB_OUTPUT"

- name: Pack (delta only)
if: steps.affected.outputs.any == 'true'
shell: bash
run: |
chmod +x Build/pack.sh
./Build/pack.sh --no-prerelease --release-version "${{ steps.ver.outputs.version }}" --paths "${{ steps.affected.outputs.paths }}"

- name: Upload artifacts
if: steps.affected.outputs.any == 'true'
uses: actions/upload-artifact@v4
with:
name: nupkg
path: artifacts/nupkg/*
if-no-files-found: error
retention-days: 7

- name: Create GitHub prerelease
if: steps.affected.outputs.any == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.ver.outputs.version }}
name: v${{ steps.ver.outputs.version }}
prerelease: true
generate_release_notes: true
files: |
artifacts/nupkg/*.nupkg
artifacts/nupkg/*.snupkg
170 changes: 170 additions & 0 deletions .github/workflows/promote.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Promote a stable release
# - Computes a stable semantic version (from input or bump based on Build/VERSION).
# - Selects which packages to include (ids -> paths via Build/packages.manifest.json).
# - Packs with the exact stable version and creates a GitHub Release (non-prerelease).
# - Optional: publish to NuGet.org if NUGET_API_KEY is provided.
name: promote

on:
workflow_dispatch:
inputs:
version:
description: "Stable version to release (overrides bump)"
required: false
type: string
bump:
description: "Semver bump to compute from Build/VERSION"
required: false
default: patch
type: choice
options: [patch, minor, major]
commit:
description: "Commit SHA or ref to build from"
required: false
type: string
packages:
description: "Comma-separated package IDs to promote (default: all manifest packages)"
required: false
type: string
publish_nuget:
description: "Publish to NuGet.org if NUGET_API_KEY is set"
required: false
type: boolean
default: false

permissions:
contents: write

concurrency:
group: promote-${{ inputs.version || github.ref_name }}
cancel-in-progress: false

jobs:
build-pack-release:
runs-on: ubuntu-latest
steps:
- name: Checkout (full history + tags)
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit || github.ref }}
fetch-depth: 0
fetch-tags: true

- name: Setup .NET (include preview)
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
10.0.x
include-prerelease: true

- name: Cache NuGet
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
restore-keys: |
${{ runner.os }}-nuget-

- name: Plan release (version and selection)
id: plan
shell: bash
run: |
set -euo pipefail
base=$(tr -d '\r\n' < Build/VERSION)
# Compute version
if [[ -n "${{ inputs.version }}" ]]; then
version="${{ inputs.version }}"
else
bump="${{ inputs.bump }}"
IFS='.' read -r MA MI PA <<< "${base%%-*}"
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
version="$MA.$MI.$PA"
fi
echo "version=$version" >> "$GITHUB_OUTPUT"

# Compute selected package paths from manifest
if [[ -n "${{ inputs.packages }}" ]]; then
mapfile -t want < <(echo "${{ inputs.packages }}" | tr ',' '\n' | sed 's/^\s*//; s/\s*$//;' | awk 'NF')
if [[ ${#want[@]} -eq 0 ]]; then selected_paths=""; else
# Build jq filter for selected ids
jq_query='['
for id in "${want[@]}"; do jq_query+=""$id","; done
jq_query=${jq_query%,}
jq_query+=']'
selected_paths=$(jq -r --argjson ids "${jq_query}" '.packages | map(select(.id as $i | $ids | index($i))) | .[].path' Build/packages.manifest.json | paste -sd, -)
fi
else
selected_paths=$(jq -r '.packages[].path' Build/packages.manifest.json | paste -sd, -)
fi

if [[ -z "$selected_paths" ]]; then
echo "any=false" >> "$GITHUB_OUTPUT"
echo "paths=" >> "$GITHUB_OUTPUT"
else
echo "any=true" >> "$GITHUB_OUTPUT"
echo "paths=$selected_paths" >> "$GITHUB_OUTPUT"
fi

# Determine previous stable version from existing tags: vMAJOR.MINOR.PATCH
prev=$(git tag --list 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//' | sort -V | tail -n1 || true)
echo "prev=$prev" >> "$GITHUB_OUTPUT"

- name: Validate version monotonicity
if: steps.plan.outputs.any == 'true'
shell: bash
run: |
set -euo pipefail
new="${{ steps.plan.outputs.version }}"
prev="${{ steps.plan.outputs.prev }}"
if [[ -n "$prev" ]]; then
max=$(printf '%s\n%s\n' "$prev" "$new" | sort -V | tail -n1)
if [[ "$max" != "$new" ]]; then
echo "Requested version $new must be greater than previous stable $prev" >&2
exit 1
fi
if [[ "$new" == "$prev" ]]; then
echo "Requested version $new equals previous stable $prev; must be greater." >&2
exit 1
fi
fi

- name: Pack stable
if: steps.plan.outputs.any == 'true'
shell: bash
run: |
chmod +x Build/pack.sh
./Build/pack.sh --no-prerelease --release-version "${{ steps.plan.outputs.version }}" --paths "${{ steps.plan.outputs.paths }}"

- name: Upload artifacts
if: steps.plan.outputs.any == 'true'
uses: actions/upload-artifact@v4
with:
name: nupkgs-stable
path: artifacts/nupkg/*
if-no-files-found: error

- name: Create GitHub release
if: steps.plan.outputs.any == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.plan.outputs.version }}
name: v${{ steps.plan.outputs.version }}
prerelease: false
draft: false
generate_release_notes: true
files: |
artifacts/nupkg/*.nupkg
artifacts/nupkg/*.snupkg

- name: Publish to NuGet.org
if: ${{ inputs.publish_nuget && secrets.NUGET_API_KEY != '' }}
shell: bash
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
dotnet nuget push "artifacts/nupkg/*.nupkg" --skip-duplicate --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ Note: Ensure `codex` is on your PATH, or pass `new CodexCliAgent<FixSpell, strin

See fuller, two-part end‑to‑end examples in `/Architecture/EndToEndExample.md`.

## Samples

Explore runnable examples in `/samples`. Open `samples/Coven.Samples.sln` to browse all samples side-by-side, or use each sample’s individual `.sln` in its folder.

# Appendix

- [Code Index](/INDEX.md)
Expand Down
49 changes: 49 additions & 0 deletions build/ReleaseProcess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Release process overview

- Pre-releases: On pushes to `main`, we only create a prerelease when changes affect packages listed in our manifest. The workflow packs just the affected packages and creates a GitHub prerelease tagged `v<base>-preview.<run>+sha.<shortSha>` with attached `.nupkg/.snupkg`.
- Promotion: Use a manual workflow to promote to a stable release. It rebuilds from the chosen ref with the provided version and may publish to NuGet.org.

Versioning

- Base version: `Build/VERSION` (e.g., `0.1.0`).
- Prerelease: `<base>-preview.<run>+sha.<shortSha>`.
- Stable: exact value entered when promoting (e.g., `0.1.0`).

Package manifest

- File: `Build/packages.manifest.json`.
- Purpose: Declare which packages are publishable and define the path scope used for change detection.
- Schema:

{
"packages": [
{ "id": "<NuGetId>", "path": "<relative/source/path>" }
]
}

- Example entries include `Coven.Core` at `src/Coven.Core`, `Coven.Spellcasting` at `src/Coven.Spellcasting`, etc. Tests are omitted.

Pre-release change detection

1) Determine changed files for the push to `main` (the merge commit of a PR).
2) Mark a package as affected if any changed file is under its `path` (recursive).
3) If no packages are affected, skip prerelease/tag creation for that run.
4) If any are affected, pack only those packages and create/update the prerelease with the computed version.

Promotion to stable

- Trigger the “promote” workflow (Actions → promote).
- Version: either provide `version` explicitly or choose a `bump` (patch/minor/major) to compute from `Build/VERSION`.
- Package selection: provide a comma-separated list of package IDs via the `packages` input to promote a subset; omit to promote all manifest packages.
- Monotonic versioning: the workflow validates that the chosen version is strictly greater than the latest existing stable tag (`vMAJOR.MINOR.PATCH`). If not, the job fails and nothing is published.
- If `NUGET_API_KEY` is present and `publish_nuget: true`, packages are pushed to NuGet.org.

Local packing

- Prerelease build: `./Build/pack.sh` or `./Build/pack.ps1` (packs all publishable projects when run locally).
- Stable build: `./Build/pack.sh --no-prerelease --release-version 0.1.0` or `./Build/pack.ps1 -PreRelease:$false -ReleaseVersion 0.1.0`.

Notes

- CI installs .NET SDK 10 (preview) for `net10.0` targets.
- Test projects are excluded and should not be listed in the manifest.
2 changes: 2 additions & 0 deletions build/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
0.1.0

Loading