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
15 changes: 8 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ This file is the primary instruction set for human and AI agents working in this
- `src/Purview.Build/` — the dotnet tool (the pipeline): `Program.cs`, `Modules/`, `Settings/`, `Helpers/`, `appsettings.json` (tool defaults).
- `.github/actions/purview-build/` and `.github/workflows/` — the shared action and reusable workflows.
- `.github/workflows/ci.yml` and `release.yml` — this repository's own CI/CD, which dogfoods the tool.
- `docs/` — authoritative documentation:
- `docs/architecture.md` — architecture and the full configuration reference (settings keys, defaults, modules, release behavior).
- `docs/releasing.md` — versioning, branch models, and release strategy.
- `docs/migrations/` — per-consumer migration guides.
- `docs/wiki/` — authoritative documentation:
- `docs/wiki/Architecture.md` — architecture and design decisions.
- `docs/wiki/Configuration-Reference.md` — the full configuration reference (settings keys, defaults, modules, release behavior).
- `docs/wiki/Release-Flow.md` — versioning, branch models, and release strategy.
- `docs/wiki/Migration-*.md` — per-consumer migration guides.
- `README.md` — user-facing overview and minimal consumer setup.
- `purview-build.json` — this repository's own pipeline configuration.
- `Justfile` — developer recipes (`just --list`).
Expand Down Expand Up @@ -42,17 +43,17 @@ CI builds with `--warnaserror`.
## Rules and invariants

- **Versioning**: the single source of truth is the `version` field in `package.json`. The pipeline reads it (`VersionModule`); `PackModule` overrides `Version`/`PackageVersion` from it.
- **Do not push release tags manually.** The tool owns tagging (`v{version}`) and the GitHub release (`CreateGitHubReleaseModule`); releasing = bump `package.json` and merge. See `docs/releasing.md`.
- **Do not push release tags manually.** The tool owns tagging (`v{version}`) and the GitHub release (`CreateGitHubReleaseModule`); releasing = bump `package.json` and merge. See `docs/wiki/Release-Flow.md`.
- **Secrets**: supplied at runtime via env vars / CI secrets (`NUGET_APIKEY`/`NUGET_API_KEY`, `GITHUB_TOKEN`, `LOCAL_NUGET_FEED_PATH`). Never hardcode or commit them.
- **Configuration precedence**: command line > environment variables (nested keys use `__`) > `purview-build.json` > baked-in defaults (`appsettings.json`).
- **LocalNuGet is local-only**: `Release:Mode=LocalNuGet` is ignored in CI; it only works when running the tool locally.

## Change process

1. Understand the affected surface by reading `docs/architecture.md` (configuration reference) and `docs/releasing.md` before changing settings/modules.
1. Understand the affected surface by reading `docs/wiki/Architecture.md` and `docs/wiki/Configuration-Reference.md` before changing settings/modules.
2. Make the change, format with `just lint-fix`, and verify with `dotnet build -c Release --warnaserror` (and `just test` when tests exist).
3. Follow the Conventional Commit style for the commit message.

## Documentation

Keep `README.md`, `docs/architecture.md`, `docs/releasing.md`, and the workflow/action inputs in sync when changing behavior. If a new setting or default is added, it must be reflected in the `docs/architecture.md` configuration tables and in `src/Purview.Build/appsettings.json` defaults.
Keep `README.md`, `docs/wiki/Architecture.md`, `docs/wiki/Configuration-Reference.md`, and the workflow/action inputs in sync when changing behavior. If a new setting or default is added, it must be reflected in the `docs/wiki/Configuration-Reference.md` tables and in `src/src/Build/appsettings.json` defaults.
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ on:
push:
branches: [main]
concurrency:
# Serialize releases; callers own concurrency (see docs/releasing.md).
# Serialize releases; callers own concurrency (see docs/wiki/Release-Flow.md).
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
Expand Down Expand Up @@ -121,7 +121,7 @@ Add `purview-build.json` at the repository root. Everything is optional; default

Secrets must not be committed. They are supplied through `NUGET_APIKEY` (or `NuGet__ApiKey`), `GITHUB_TOKEN`, and `LOCAL_NUGET_FEED_PATH` (or `PublishLocalNuGet__LOCAL_NUGET_FEED_PATH`).

See [architecture and configuration](docs/architecture.md) and [release strategy](docs/releasing.md).
See the [Documentation](#documentation) section below for the architecture, configuration reference, and release strategy.

## Pipeline

Expand All @@ -140,4 +140,12 @@ This repository dogfoods the shared tool: CI builds and packs the tool from sour

On a push to `main`, the release workflow rebuilds and reinstalls the tool from the current source, then runs it with `Release__Mode=NuGet`, `NuGet__FeedUrl` pointing at nuget.org, and `Release__UploadArtifacts=true`. The tool therefore publishes the immutable package to `https://api.nuget.org/v3/index.json` and tags and releases itself (`v{Version}` + generated-notes GitHub release with the package attached) — exactly like every other purview-dev repository. Maintainers bump the `package.json` version and merge; they do not create release tags manually.

GitHub initially creates NuGet packages as private. To make sure every package is **Internal** (consumable by all Purview-Dev members), an organization owner should set the org default: Purview-Dev → Settings → Packages → **Package Creation** → **Internal**, and change any already-published package's visibility in its **Package settings** → **Danger Zone**. See [docs/releasing.md](docs/releasing.md) for the exact steps and the `gh api` alternative.
GitHub initially creates NuGet packages as private. To make sure every package is **Internal** (consumable by all Purview-Dev members), an organization owner should set the org default: Purview-Dev → Settings → Packages → **Package Creation** → **Internal**, and change any already-published package's visibility in its **Package settings** → **Danger Zone**. See [docs/wiki/Release-Flow.md](docs/wiki/Release-Flow.md) for the exact steps and the `gh api` alternative.

## Documentation

- [Homepage](https://purview.dev/projects/build/)
- [Documentation](https://purview.dev/docs/build/)
- [Architecture](docs/wiki/Architecture.md)
- [Configuration reference](docs/wiki/Configuration-Reference.md)
- [Release flow](docs/wiki/Release-Flow.md)
72 changes: 72 additions & 0 deletions docs/wiki/Architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Architecture

## Decision

The shared artifact is a .NET tool NuGet package, not a reusable workflow and not an MSBuild SDK. Modular Pipelines is an executable orchestration system, so a tool is its natural package boundary. A tool manifest gives each consumer deterministic version pinning and Renovate/Dependabot-compatible upgrades. It also keeps GitHub Actions as a thin host; the same command runs locally, in GitHub Actions, or in another CI service.

The implementation is the generalized `PipelineCLI` that originated in `sourcegeneratorframework` (its most advanced version, including pack validation). It supersedes the earlier `Purview.Build` modules.

The repository additionally exposes:

- a **composite action** (`.github/actions/purview-build`) that installs a pinned `Purview.Build` version and runs it, for repositories embedding the build in their own jobs, and
- two **reusable workflows** (`purview-build.yml`, `purview-release.yml`) that wrap that logic with structured inputs/secrets, reducing a consumer to one reusable-workflow job plus `purview-build.json`.

An MSBuild SDK remains a possible future companion for shared compile-time properties, analyzers, or package metadata. It should not own CI orchestration.

## Ownership boundary

The package owns module implementation, dependency ordering, safe defaults, secret lookup, NuGet/GitHub integration, and diagnostics. Each repository owns its tool-version pin, paths and discovery patterns, feature switches, and release-mode selection. A project needing truly custom behavior can invoke its own command before/after the shared tool; a generally useful variation should be added as a typed option here.

## Module ordering

The pipeline is registered in `Program.cs` in this order, with explicit `[DependsOn]` edges defining the graph:

```text
VersionModule ──────────────┐
RestoreModule → BuildModule ├→ RunTestsModule → PackModule → ValidatePackModule
LintModule (independent) │
VersionModule ──────────────┘
```

Explicit `[DependsOn]` edges:

- `BuildModule` depends on `RestoreModule`.
- `RunTestsModule` depends on `BuildModule`.
- `PackModule` depends on `RunTestsModule` and `VersionModule`.
- `ValidatePackModule` depends on `PackModule`.
- `PublishNuGetModule` depends on `PackModule`, `ValidatePackModule`, and `RunTestsModule`.
- `PublishLocalNuGetModule` depends on `PackModule` and `ValidatePackModule`.
- `CreateGitHubReleaseModule` depends on `PublishNuGetModule`, `ValidatePackModule`, and `VersionModule`.

See [Pipeline Modules](Pipeline-Modules.md) for per-module behavior and skip conditions.

## Repository root resolution

The tool locates the repository root by walking up from the current working directory to the nearest `package.json` (`PathHelpers.FindRepositoryRoot`); `Environment.CurrentDirectory` is set to that root before modules run. `MODULAR_PIPELINES_DIRECTORY` can override the directory containing `appsettings.json` when the defaults do not apply.

Command-line overrides use configuration syntax, for example:

```shell
dotnet purview-build --Build:TestPatterns=*IntegrationTests.csproj --Build:RunPack=false
```

## Project, testing, and release support

- **Project types**: the pipeline is dotnet-first (libraries, source generators, analyzers, MSBuild SDKs, Aspire hosting extensions). Non-dotnet project types (`Web` for full-stack apps, `WebExtension` for JS/Azure DevOps extensions) are designed as future module additions gated by configuration.
- **Testing types**: TUnit on Microsoft.Testing.Platform (default) and xUnit, both configurable via `TestFramework`/`TestFilter`. Non-dotnet runners (Vitest, Playwright, Jest, Astro) are future modules.
- **Release types**: nuget.org (API key or Trusted Publishing), GitHub Packages internal feed, local NuGet feed, GitHub release (optionally with package/vsix assets), and future Aspire-deploy / Azure DevOps marketplace publishing.

## Release behavior

- `None`: build/test/pack may run, but nothing publishes.
- `LocalNuGet`: pushes packages to the resolved local feed for developer testing. Only honoured when the tool runs **locally**; it is ignored in CI, so it cannot be driven through the reusable workflows.
- `NuGet`: pushes packages to the configured feed and, by default, creates a GitHub release.
- `GitHubRelease`: creates a GitHub release (optionally uploading `ArtifactsFolder` assets) without publishing NuGet packages.

The workflow decides whether a version is eligible to release (for example, only an untagged version on `main` or `release`) and sets `Release__Mode`. Credentials remain CI secrets.

## See also

- [Configuration Reference](Configuration-Reference.md)
- [Pipeline Modules](Pipeline-Modules.md)
- [Release Flow](Release-Flow.md)
84 changes: 40 additions & 44 deletions docs/architecture.md → docs/wiki/Configuration-Reference.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
# Architecture and configuration
# Configuration Reference

## Decision
Configuration is optional in a consuming repository; defaults are baked into the tool. Add `purview-build.json` at the repository root to override them.

The shared artifact is a .NET tool NuGet package, not a reusable workflow and not an MSBuild SDK. Modular Pipelines is an executable orchestration system, so a tool is its natural package boundary. A tool manifest gives each consumer deterministic version pinning and Renovate/Dependabot-compatible upgrades. It also keeps GitHub Actions as a thin host; the same command runs locally, in GitHub Actions, or in another CI service.
## Precedence

The implementation is the generalized `PipelineCLI` that originated in `sourcegeneratorframework` (its most advanced version, including pack validation). It supersedes the earlier `Purview.Build` modules.
Command line > environment variables > `purview-build.json` > baked-in defaults (`appsettings.json`) > code-level defaults.

The repository additionally exposes:
- Environment variables use `__` for nesting, for example `Release__Mode=NuGet`.
- Command-line overrides use configuration syntax, for example `--Build:RunPack=false`.
- Secrets must not be committed; they are supplied at runtime through env vars / CI secrets. See [Secrets and Environment Variables](Secrets-and-Environment-Variables.md).

- a **composite action** (`.github/actions/purview-build`) that installs a pinned `Purview.Build` version and runs it, for repositories embedding the build in their own jobs, and
- two **reusable workflows** (`purview-build.yml`, `purview-release.yml`) that wrap that logic with structured inputs/secrets, reducing a consumer to one reusable-workflow job plus `purview-build.json`.

An MSBuild SDK remains a possible future companion for shared compile-time properties, analyzers, or package metadata. It should not own CI orchestration.

## Ownership boundary

The package owns module implementation, dependency ordering, safe defaults, secret lookup, NuGet/GitHub integration, and diagnostics. Each repository owns its tool-version pin, paths and discovery patterns, feature switches, and release-mode selection. A project needing truly custom behavior can invoke its own command before/after the shared tool; a generally useful variation should be added as a typed option here.

## Configuration reference

### `Build`
## `Build`

| Key | Default | Purpose |
| --- | --- | --- |
Expand All @@ -37,7 +28,7 @@ The package owns module implementation, dependency ordering, safe defaults, secr
| `RunPack` | `true` | Enable packing |
| `ValidatePack` | `true` | Enable pack validation |

### `PackValidation`
## `PackValidation`

| Key | Default | Purpose |
| --- | --- | --- |
Expand All @@ -51,7 +42,9 @@ The package owns module implementation, dependency ordering, safe defaults, secr

Content entry paths and package-id keys are matched as globs (case-insensitive), e.g. `tools/**/Foo.dll` or `**/*.pdb`. Required content is satisfied when any package entry matches; forbidden content fails when any entry matches. The assembly checks (`RequireSourceLink`, `RequireDeterministic`, `RequiredCompilerFlags`) inspect each `.dll`/`.exe` in the `.nupkg` (PE header) and its sibling portable PDB in the `.snupkg` (custom debug info records); they only apply to assemblies the package ships symbols for. Determinism is detected via the PE's Reproducible debug directory entry, source link via the PDB's Source Link record, and compiler flags via the PDB's key/value compiler-flags record (matched case-insensitively, e.g. `optimization=release`).

### `NuGet`
> **Tool defaults vs code defaults.** The shipped `appsettings.json` sets `RequireSourceLink: false`, `RequireDeterministic: false`, and `RequiredCompilerFlags: []`. The C# property initializers in `PackValidationSettings` default those to `true`/`true`/`["optimization=release"]`, but because `appsettings.json` always loads and wins over code defaults, the effective shipped defaults are the `false`/`false`/`[]` values shown above.

## `NuGet`

| Key | Default | Purpose |
| --- | --- | --- |
Expand All @@ -60,7 +53,7 @@ Content entry paths and package-id keys are matched as globs (case-insensitive),
| `APIKey` | unset | Secret; use `NUGET_APIKEY` or `NuGet__ApiKey` |
| `EnvAPIKey` | unset | Binds `NuGet__NUGET_APIKEY`; also falls back to process env `NUGET_APIKEY`/`NUGET_API_KEY` |

### `PublishLocalNuGet`
## `PublishLocalNuGet`

| Key | Default | Purpose |
| --- | --- | --- |
Expand All @@ -70,42 +63,45 @@ Content entry paths and package-id keys are matched as globs (case-insensitive),
| `ShutdownDotnetBuilderServer` | `true` | Shut down the dotnet build server after publishing |
| `ClearPackageCache` | `true` | Clear the local NuGet package caches for the published packages |

### `GitHub`
## `GitHub`

| Key | Default | Purpose |
| --- | --- | --- |
| `AccessToken` | unset | Secret; use `GITHUB_TOKEN` |
| `EnvAccessToken` | unset | Binds `GitHub__GITHUB_TOKEN`; also falls back to process env `GITHUB_TOKEN` |
| `ProductHeader` | `Purview.Build.Pipeline` | GitHub API product header |

### `Release`
## `Release`

| Key | Default | Purpose |
| --- | --- | --- |
| `Mode` | `None` | `None`, `LocalNuGet`, `NuGet`, or `GitHubRelease` |
| `UploadArtifacts` | `false` | Upload every file in `Build:ArtifactsFolder` as GitHub release assets |

## Project, testing, and release support

- **Project types**: the pipeline is dotnet-first (libraries, source generators, analyzers, MSBuild SDKs, Aspire hosting extensions). Non-dotnet project types (`Web` for full-stack apps, `WebExtension` for JS/Azure DevOps extensions) are designed as future module additions gated by configuration.
- **Testing types**: TUnit on Microsoft.Testing.Platform (default) and xUnit, both configurable via `TestFramework`/`TestFilter`. Non-dotnet runners (Vitest, Playwright, Jest, Astro) are future modules.
- **Release types**: nuget.org (API key or Trusted Publishing), GitHub Packages internal feed, local NuGet feed, GitHub release (optionally with package/vsix assets), and future Aspire-deploy / Azure DevOps marketplace publishing.

## Release behavior

- `None`: build/test/pack may run, but nothing publishes.
- `LocalNuGet`: pushes packages to the resolved local feed for developer testing. Only honoured when the tool runs **locally**; it is ignored in CI (Modular Pipelines detects a non-CI environment), so it cannot be driven through the reusable workflows.
- `NuGet`: pushes packages to the configured feed and, by default, creates a GitHub release.
- `GitHubRelease`: creates a GitHub release (optionally uploading `ArtifactsFolder` assets) without publishing NuGet packages.

The workflow decides whether a version is eligible to release (for example, only an untagged version on `main` or `release`) and sets `Release__Mode`. Credentials remain CI secrets.

## Repository root resolution

The tool locates the repository root by walking up from the current working directory to the nearest `package.json`; `Environment.CurrentDirectory` is set to that root before modules run. `MODULAR_PIPELINES_DIRECTORY` can override the directory containing `appsettings.json` when the defaults do not apply.
## Example

```json
{
"Build": {
"Solution": "src/MyProduct.slnx",
"TestRoot": "src/tests",
"TestPatterns": "*Tests.csproj",
"TestFilter": "/*/*/*/*[Category=Unit]"
},
"PackValidation": {
"RequireSymbolPackage": true,
"RequireSourceLink": true,
"RequireDeterministic": true,
"RequiredCompilerFlags": ["optimization=release"],
"RequiredContent": {
"my.product": ["lib/netstandard2.0/My.Product.dll"]
}
},
"Release": { "Mode": "None" }
}
```

Command-line overrides use configuration syntax, for example:
## See also

```shell
dotnet purview-build --Build:TestPatterns=*IntegrationTests.csproj --Build:RunPack=false
```
- [Pipeline Modules](Pipeline-Modules.md)
- [Secrets and Environment Variables](Secrets-and-Environment-Variables.md)
Loading