diff --git a/AGENTS.md b/AGENTS.md index 9df216b..d4c3bc6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`). @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/README.md b/README.md index 1061307..4ca3845 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 @@ -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) diff --git a/docs/wiki/Architecture.md b/docs/wiki/Architecture.md new file mode 100644 index 0000000..1b3178c --- /dev/null +++ b/docs/wiki/Architecture.md @@ -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) \ No newline at end of file diff --git a/docs/architecture.md b/docs/wiki/Configuration-Reference.md similarity index 52% rename from docs/architecture.md rename to docs/wiki/Configuration-Reference.md index ed05014..5884143 100644 --- a/docs/architecture.md +++ b/docs/wiki/Configuration-Reference.md @@ -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 | | --- | --- | --- | @@ -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 | | --- | --- | --- | @@ -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 | | --- | --- | --- | @@ -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 | | --- | --- | --- | @@ -70,7 +63,7 @@ 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 | | --- | --- | --- | @@ -78,34 +71,37 @@ Content entry paths and package-id keys are matched as globs (case-insensitive), | `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) \ No newline at end of file diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md new file mode 100644 index 0000000..d20d5f8 --- /dev/null +++ b/docs/wiki/Getting-Started.md @@ -0,0 +1,92 @@ +# Getting Started + +`Purview.Build` is consumed from a repository that owns its own configuration. The pipeline code lives here; consumers reference the composite action or one of the reusable workflows (or run the tool locally) and configure it with `purview-build.json`. + +## Two independent version axes + +- The `@ref` suffix on a reusable-workflow or composite-action reference selects the workflow/action **code**: `@main` always runs the latest code, while a release tag (e.g. `@v0.2.0`) pins it for reproducibility. There is no `@latest`; the `@ref` is required for cross-repository references. +- The `build-version` input selects the installed `Purview.Build` **tool**. Omit it to always install the latest stable tool from nuget.org, or pin an exact version (e.g. `build-version: 0.2.0`) for reproducibility. + +Mixing a pinned old `@ref` with a floating `build-version` runs newer tool code through older workflow inputs. + +## Minimal repository setup (reusable workflow) + +```yaml +# .github/workflows/pr.yml +name: PR +on: + pull_request: + branches: [main] +jobs: + build: + uses: purview-dev/build/.github/workflows/purview-build.yml@main + # `build-version` is optional; when omitted, the latest stable Purview.Build + # from nuget.org is installed. Pin it (e.g. `build-version: 0.2.0`) for + # reproducible builds. + secrets: inherit +``` + +```yaml +# .github/workflows/release.yml — release on main +name: Release +on: + push: + branches: [main] +concurrency: + # Serialize releases; callers own concurrency (see Release Flow). + group: release-${{ github.ref }} + cancel-in-progress: false +jobs: + release: + uses: purview-dev/build/.github/workflows/purview-release.yml@main + with: + release-mode: NuGet + secrets: inherit +``` + +For the **main-as-head / release-branch model**, point the release caller at the release branch instead: + +```yaml +on: + push: + branches: [release] +``` + +The reusable release workflow checks whether `v{version}` (read from `package.json`) is already tagged and skips if so, so merging `main` into `release` releases exactly once. + +The reusable workflows install the pinned CLI version (or the latest stable when `build-version` is omitted) from nuget.org; the consuming repository adds `purview-build.json` and a root `package.json` version. It does not need a copied pipeline project or package-source credentials. + +## Minimal repository setup (composite action) + +```yaml +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: purview-dev/build/.github/actions/purview-build@main + env: + Build__TestFilter: "/*/*/*/*[Category=Unit]" +``` + +The action's `build-version` input is optional. When omitted, `dotnet tool install` resolves the latest stable `Purview.Build` from nuget.org; pass an exact version to pin the build. + +## Local use + +```shell +dotnet tool install Purview.Build --tool-path ./.tools +./.tools/purview-build +``` + +Omit `--version` to install the latest stable release. + +## Release a consuming repository + +Releasing consists of bumping the `version` field in the repository's root `package.json` and merging the validated pull request into the release head. The pipeline owns tagging (`v{version}`) and the GitHub release; maintainers must not push release tags manually. See [Release Flow](Release-Flow.md). + +## See also + +- [Architecture](Architecture.md) +- [Configuration Reference](Configuration-Reference.md) +- [Pipeline Modules](Pipeline-Modules.md) +- [Secrets and Environment Variables](Secrets-and-Environment-Variables.md) \ No newline at end of file diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..dfc360e --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,53 @@ +# Purview.Build Wiki + +This wiki is the project documentation hub for **Purview.Build** — the shared build/test/release system for the `purview-dev` organisation. It is a Generalized Modular Pipelines pipeline (based on the `PipelineCLI` originally developed in `sourcegeneratorframework`) packaged as a pinned .NET tool and exposed through a shared GitHub composite action and thin reusable workflows. + +Consuming repositories own configuration (`purview-build.json`); they do not own pipeline source code. Version, paths, feature switches, and release-mode selection are per-repository. + +## Delivery surfaces + +The same implementation is available three ways: + +1. **`Purview.Build` dotnet tool** — NuGet package published to nuget.org. Run anywhere a .NET SDK exists (locally via `just`, in GitHub Actions, or another CI service). +2. **Composite action** `purview-dev/build/.github/actions/purview-build` — for repositories that want the action embedded directly in one of their own jobs. +3. **Reusable workflows** `purview-dev/build/.github/workflows/purview-build.yml` and `.../purview-release.yml` — thin `workflow_call` wrappers with structured inputs/secrets. + +## Start here + +- [Getting Started](Getting-Started.md) +- [Architecture](Architecture.md) +- [Configuration Reference](Configuration-Reference.md) +- [Pipeline Modules](Pipeline-Modules.md) +- [Pack Validation](Pack-Validation.md) +- [Release Flow](Release-Flow.md) +- [Local Development](Local-Development.md) +- [Secrets and Environment Variables](Secrets-and-Environment-Variables.md) +- [Repository CI/CD](Repository-CI-CD.md) +- [Migration: aspire-resourcekit](Migration-aspire-resourcekit.md) + +## Pipeline + +```text +Version ───────────────┐ +Restore → Build → Test ├→ Pack → Validate → Publish → GitHub release + └→ Lint │ +Version ───────────────┘ +``` + +`Version` reads the SemVer `version` field from `package.json`. Lint restores local tools and runs CSharpier. Tests are discovered under `Build:TestRoot`/`Build:TestPatterns` and run with a TUnit tree-node filter (or an xUnit filter). Pack validation inspects each `.nupkg`/`.snupkg` against required/forbidden content rules (glob patterns) and can enforce source link, deterministic builds, and compiler flags on the packaged assemblies. Publication and GitHub release steps are controlled by `Release:Mode` (`None`, `LocalNuGet`, `NuGet`, `GitHubRelease`) and independently by the `Build__Run*` switches. `LocalNuGet` is only honoured when the tool runs locally; it is ignored in CI. + +## Requirements + +- .NET SDK 10.0 or later. +- `just` for local recipes (`just --list`). +- A repository root `package.json` whose `version` field is the single release version source. + +## Repository layout + +| Path | Purpose | +| --- | --- | +| `src/src/Build` | The packable `Purview.Build` tool: `Program.cs`, `Modules/`, `Settings/`, `Helpers/`, `appsettings.json` | +| `.github/actions/purview-build` | The shared composite action | +| `.github/workflows` | The reusable `purview-build.yml`/`purview-release.yml` and this repository's own `ci.yml`/`release.yml` | +| `docs/wiki` | This wiki | +| `purview-build.json` | This repository's own pipeline configuration (the tool dogfoods itself) | \ No newline at end of file diff --git a/docs/wiki/Local-Development.md b/docs/wiki/Local-Development.md new file mode 100644 index 0000000..c08867a --- /dev/null +++ b/docs/wiki/Local-Development.md @@ -0,0 +1,55 @@ +# Local Development + +Local work uses the `just` recipes in the `Justfile` (which delegate to plain `dotnet`/`bun` commands) or the shared pipeline tool directly. + +## Tool installation + +```shell +dotnet tool install Purview.Build --tool-path ./.tools +./.tools/purview-build +``` + +Omit `--version` to install the latest stable release. For a pinned local tool manifest, add it to `.config/dotnet-tools.json` and run `dotnet tool restore`. + +## `just` recipes + +| Recipe | Purpose | +| --- | --- | +| `just build` | Build `src/Build.slnx` (Debug) | +| `just test` / `just test-unit` | Run tests with a TUnit tree-node filter (unit filter: `/*/*/*/*[Category=Unit]`) | +| `just lint-check` / `just lint-fix` | CSharpier check / format | +| `just pack` | `dotnet pack` with the current `package.json` version | +| `just pipeline-pr` | Run the shared tool (restore, build, lint, tests) | +| `just pipeline-build` | Run the shared tool without tests | +| `just pipeline-release` | Run the shared tool with `Release:Mode=NuGet` | +| `just pipeline-tests` | Run the shared tool with tests enabled | +| `just pipeline-local-release` | Lint-fix, then run the shared tool with `Release:Mode=LocalNuGet` | +| `just clean-all` / `just scrub` | Clean build outputs | + +`just pipeline-*` installs the `Purview.Build` tool to `.tools/purview-build` from nuget.org when it is not already present. + +## Local NuGet publishing + +`Release:Mode=LocalNuGet` pushes packages to a local feed and is only honoured when the tool runs **locally** — it is ignored in CI. + +```shell +LOCAL_NUGET_FEED_PATH=p:/_sync-projects/.local-nuget/ ./.tools/purview-build --Release:Mode=LocalNuGet +``` + +The local feed path must be an absolute path. `just` runs recipes through the shell, which strips backslashes from unquoted arguments; a Windows path such as `p:\_sync-projects\.local-nuget\` arrives as the drive-relative `p:_sync-projects.local-nuget` and is rejected. Use the `LOCAL_NUGET_FEED_PATH` environment variable or forward slashes: + +```shell +just pipeline-local-release --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/ +``` + +By default the module overwrites existing packages, clears the NuGet global-packages and HTTP caches for the published packages, and shuts down the dotnet build server afterwards (all configurable under `PublishLocalNuGet`). + +## Repository root resolution + +The tool locates the repository root by walking up from the current working directory to the nearest `package.json`. Run `purview-build` from within the repository. `MODULAR_PIPELINES_DIRECTORY` can override the directory containing `appsettings.json`. + +## See also + +- [Configuration Reference](Configuration-Reference.md) +- [Pipeline Modules](Pipeline-Modules.md) +- [Release Flow](Release-Flow.md) \ No newline at end of file diff --git a/docs/migrations/aspire-resourcekit.md b/docs/wiki/Migration-aspire-resourcekit.md similarity index 96% rename from docs/migrations/aspire-resourcekit.md rename to docs/wiki/Migration-aspire-resourcekit.md index 277045f..9996b4d 100644 --- a/docs/migrations/aspire-resourcekit.md +++ b/docs/wiki/Migration-aspire-resourcekit.md @@ -21,4 +21,4 @@ This repository currently contains a vendored copy of `build/PipelineCLI`. Migra 4. In the release caller set `release-mode: NuGet` and `secrets: inherit` (`NUGET_APIKEY` and `GITHUB_TOKEN` are read by the shared workflow). 5. Run the PR pipeline, then delete `build/PipelineCLI` and its pipeline-only central package declarations (`ModularPipelines*`, `NuGet.Packaging/Versioning`). -The old solution path and unit-test filter are preserved exactly. Other repositories migrate by changing only the JSON paths/patterns; for example `dotnet-project-sdk` can list unit and integration project globs in `Build:TestPatterns`. +The old solution path and unit-test filter are preserved exactly. Other repositories migrate by changing only the JSON paths/patterns; for example `dotnet-project-sdk` can list unit and integration project globs in `Build:TestPatterns`. \ No newline at end of file diff --git a/docs/wiki/Pack-Validation.md b/docs/wiki/Pack-Validation.md new file mode 100644 index 0000000..a14588b --- /dev/null +++ b/docs/wiki/Pack-Validation.md @@ -0,0 +1,41 @@ +# Pack Validation + +`ValidatePackModule` inspects every `.nupkg`/`.snupkg` produced in `Build:ArtifactsFolder` and fails the pipeline when any package has validation errors. Each package is reported as valid/invalid in the summary. + +## Symbol package pairing (`RequireSymbolPackage`) + +Every `.nupkg` must have a matching `.snupkg` (same id/version) and vice versa. A package without its symbol sibling is an error. + +## Symbol package contents (`RequireSymbolFiles`) + +Every `.snupkg` must contain at least one `.pdb`. Symbol packages must not contain non-symbol files other than OPC metadata (`[Content_Types].xml`, `_rels/`, `package/services/metadata/`) and the `.nuspec`. + +## PDB placement in `.nupkg` + +The `.nupkg` must not contain `.pdb` files outside `tools/` — PDBs are delivered through the `.snupkg`. The exception is tool packages (`PackAsTool`), whose runtime PDBs legitimately live under `tools/`; the `Purview.Build` csproj strips those PDBs from the `.nupkg` after `GenerateNuspec` so the `.snupkg` keeps them for source-link validation. + +## Content rules (`RequiredContent` / `ForbiddenContent`) + +Both maps are keyed by package-id glob (case-insensitive; `"*"` matches every package) and contain entry-path glob lists (forward slashes, e.g. `tools/**/Purview.Build.dll` or `**/*.pdb`). + +- **Required**: the rule is satisfied when any package entry matches the glob; a missing match is an error. +- **Forbidden**: any matching entry is an error. + +## Assembly inspection + +When any of `RequireSourceLink`, `RequireDeterministic`, or `RequiredCompilerFlags` is enabled, each `.dll`/`.exe` in the `.nupkg` that the package ships symbols for (a sibling PDB exists in the `.snupkg` or `.nupkg`) is inspected: + +- **Deterministic (`RequireDeterministic`)**: the PE must carry the Reproducible debug directory entry (`PEReader.ReadDebugDirectory` with type `Reproducible`). Bundled third-party binaries without symbols are not judged. +- **Source link (`RequireSourceLink`)**: the matching portable PDB must contain a Source Link record (custom debug info GUID `CC110556-A091-4D38-9FEC-25AB9A351A6A`). +- **Compiler flags (`RequiredCompilerFlags`)**: the PDB's compiler-flags record (custom debug info GUID `B5FEEC05-8CD0-4A83-96DA-466284BB4BD8`, stored as NUL-separated `key=value` pairs) must contain each required flag, matched case-insensitively (e.g. `optimization=release`). + +Unreadable packages and invalid PE/PDB files are reported as errors. + +## Filename checks + +Package file names must match the nuspec id/version, i.e. `..nupkg` / `..snupkg`. + +## See also + +- [Pipeline Modules](Pipeline-Modules.md) +- [Configuration Reference](Configuration-Reference.md) \ No newline at end of file diff --git a/docs/wiki/Pipeline-Modules.md b/docs/wiki/Pipeline-Modules.md new file mode 100644 index 0000000..c57f6b2 --- /dev/null +++ b/docs/wiki/Pipeline-Modules.md @@ -0,0 +1,80 @@ +# Pipeline Modules + +The pipeline is a Modular Pipelines orchestration. Modules are registered in `Program.cs`; explicit `[DependsOn]` edges define ordering, while `ModuleConfiguration` skip conditions gate opt-in behavior. Module categories are `Build` and `Release`. + +```text +Version ───────────────┐ +Restore → Build → Test ├→ Pack → Validate → Publish → GitHub release + └→ Lint │ +Version ───────────────┘ +``` + +## VersionModule + +Reads the SemVer `version` field from the repository root `package.json` and produces a `NuGetVersion`. Fails when the file is missing, the field is missing/empty, or the value is not valid SemVer. The version feeds `PackModule` (via `Version`/`PackageVersion`) and `CreateGitHubReleaseModule` (via the `v{version}` tag). + +## RestoreModule + +Runs `dotnet restore` against `Build:Solution`. + +## BuildModule + +Depends on `RestoreModule`. Runs `dotnet build` against `Build:Solution` with `Build:Configuration` and `--no-restore`. + +## LintModule + +Skip condition: skipped when `Build:RunLint` is false. + +Restores the repository's local tools (`dotnet tool restore` against `.config/dotnet-tools.json`, retried up to 3 times with a 2-second backoff on failure) and then runs `dotnet tool run csharpier check `. The repository root is resolved by walking up to the nearest `package.json`. + +## RunTestsModule + +Depends on `BuildModule`. Skip condition: skipped when `Build:RunTests` is false. + +Discovers test projects by enumerating `*.csproj` recursively under `Build:TestRoot`, matching file names against `Build:TestPatterns` (comma-separated glob/name patterns, case-insensitive), then restricting the run list with `Build:TestProjects` (default `*`). If no projects match, it logs a warning and returns an empty result. + +Runs each test project with `dotnet test --no-build --no-restore` in parallel, using `Build:Configuration`: + +- **TUnit** (default): passes `--ignore-exit-code 8` (Microsoft.Testing.Platform exits with code 8 when no tests are selected; treated as success) and, when `Build:TestFilter` is non-empty, `--treenode-filter `. +- **xUnit**: when `Build:TestFilter` is non-empty, passes `--filter `. + +Per-project timings are logged, ordered by elapsed time. + +## PackModule + +Depends on `RunTestsModule` and `VersionModule`. Skip condition: skipped when `Build:RunPack` is false. + +Creates `Build:ArtifactsFolder` and runs `dotnet pack` against `Build:Solution` with `Build:Configuration`, `--output `, and `-p:PackageVersion= -p:Version=` where the version comes from `VersionModule`. + +## ValidatePackModule + +Depends on `PackModule`. Skip condition: skipped when `Build:ValidatePack` is false. + +Inspects every `.nupkg`/`.snupkg` in `Build:ArtifactsFolder`. Fails the run if any package has errors. Produces a summary of valid/invalid package counts. See [Pack Validation](Pack-Validation.md) for the full rule set. + +## PublishNuGetModule + +Category `Release`. Depends on `PackModule`, `ValidatePackModule`, and `RunTestsModule`. Skip condition: skipped unless `Release:Mode` is `NuGet` **and** either `NuGet:TrustedPublishing` is true or an API key resolves via `NuGet:GetNuGetAPIKey()`. + +Pushes every `*.nupkg` in `Build:ArtifactsFolder` to `NuGet:FeedUrl` with `--skip-duplicate`. When `NuGet:TrustedPublishing` is true, pushes without an API key (NuGet Trusted Publishing / OIDC federation). + +## PublishLocalNuGetModule + +Depends on `PackModule` and `ValidatePackModule`. Skip condition: skipped unless the tool is running **locally** (`ctx.IsRunningLocally()`) and `Release:Mode` is `LocalNuGet`. This mode is intentionally ignored in CI. + +Validates `PublishLocalNuGet:LocalFeedPath` (resolved via `GetLocalFeedPath()`, falling back to `PublishLocalNuGet__LOCAL_NUGET_FEED_PATH` and then process env `LOCAL_NUGET_FEED_PATH`). The path must be absolute; drive-relative paths such as `p:foo` (backslashes stripped by a sh-style shell) are rejected with a remediation message. See [Local Development](Local-Development.md). + +Moves the `.nupkg`/`.snupkg` files from `Build:ArtifactsFolder` into the local feed (skipping existing files unless `OverwriteExistingPackages` is true), optionally clears the NuGet global-packages and HTTP caches for the published packages (`ClearPackageCache`), and optionally shuts down the dotnet build server (`ShutdownDotnetBuilderServer`). + +## CreateGitHubReleaseModule + +Category `Release`. Depends on `PublishNuGetModule`, `ValidatePackModule`, and `VersionModule`. Skip condition: skipped unless `Release:Mode` is `NuGet` or `GitHubRelease` **and** a GitHub token resolves via `GitHub:GetGitHubToken()`. + +Creates a GitHub release with tag `v{version}` and `GenerateReleaseNotes = true`. When `Release:UploadArtifacts` is true, uploads every file in `Build:ArtifactsFolder` as a release asset. The tag must not already exist; callers gate release eligibility (the tool does not skip an existing tag itself). + +## See also + +- [Architecture](Architecture.md) +- [Configuration Reference](Configuration-Reference.md) +- [Pack Validation](Pack-Validation.md) +- [Release Flow](Release-Flow.md) \ No newline at end of file diff --git a/docs/releasing.md b/docs/wiki/Release-Flow.md similarity index 93% rename from docs/releasing.md rename to docs/wiki/Release-Flow.md index 755ce11..c2318b8 100644 --- a/docs/releasing.md +++ b/docs/wiki/Release-Flow.md @@ -1,4 +1,4 @@ -# Versioning and release strategy +# Versioning and Release Flow `Purview.Build` follows SemVer. The package version is the compatibility contract for configuration keys, defaults, module ordering, and tool behavior. @@ -31,10 +31,12 @@ The `release-branch` input is retained for backward compatibility only. ## This repository's CI/CD -This repository dogfoods the shared tool. CI performs restore, warnings-as-errors compilation, packing, installation from the generated package, then runs `purview-build` against this repository so the project builds and packs itself. +This repository dogfoods the shared tool. CI performs restore, warnings-as-errors compilation, packing, installation from the generated package, then runs `purview-build` against this repository so the project builds and packs itself. See [Repository CI/CD](Repository-CI-CD.md). On a push to `main`, the release workflow reads and validates the `package.json` version, skips when `v{version}` already exists, then builds and installs the tool from the current source and runs it with `Release__Mode=NuGet`, `NuGet__FeedUrl` set to nuget.org, and `Release__UploadArtifacts=true`. The tool performs the release build/pack steps, publishes the immutable package to `https://api.nuget.org/v3/index.json` using the `NUGET_APIKEY` secret, and creates `v{version}` plus a generated-notes GitHub release with the package attached — tagging itself exactly like every other purview-dev repository. The tool therefore owns tagging; maintainers must not push release tags manually. +## GitHub package visibility + GitHub creates NuGet packages as private on first publication. To make sure every package is **Internal** (visible to all Purview-Dev members), set both: 1. **Organization default (prevents future private packages)** — org owner: @@ -56,7 +58,7 @@ NuGet versions are immutable; `--skip-duplicate` makes recovery safe if publicat ## For local validation ```shell -dotnet pack src/Purview.Build/Purview.Build.csproj -c Release -o artifacts -p:Version=0.2.4 -p:PackageVersion=0.2.4 +dotnet pack src/src/Build/Build.csproj -c Release -o artifacts -p:Version=0.2.4 -p:PackageVersion=0.2.4 dotnet tool install Purview.Build --tool-path ./.tools --add-source ./artifacts ./.tools/purview-build ``` @@ -66,3 +68,8 @@ To publish packages built by a consumer to a local feed for development: ```shell LOCAL_NUGET_FEED_PATH=p:/_sync-projects/.local-nuget/ ./.tools/purview-build --Release:Mode=LocalNuGet ``` + +## See also + +- [Getting Started](Getting-Started.md) +- [Repository CI/CD](Repository-CI-CD.md) \ No newline at end of file diff --git a/docs/wiki/Repository-CI-CD.md b/docs/wiki/Repository-CI-CD.md new file mode 100644 index 0000000..12d87c4 --- /dev/null +++ b/docs/wiki/Repository-CI-CD.md @@ -0,0 +1,38 @@ +# Repository CI/CD + +This repository dogfoods the shared `Purview.Build` tool: it builds and packs the tool from source, installs the generated package, then runs `purview-build` against itself so the project builds and packs itself. + +## CI (`ci.yml`) + +Runs on pull requests and pushes to `main`: + +1. Check out the repository. +2. Restore `src/Build.slnx`. +3. **Build gate**: `dotnet build src/Build.slnx --configuration Release --no-restore --warnaserror`. +4. Read and SemVer-validate the `package.json` version. +5. Pack the tool from source (`dotnet pack src/Build.slnx --configuration Release --no-build --output artifacts -p:Version=… -p:PackageVersion=…`). +6. Install the packed tool from the `artifacts` source into a temp tool path. +7. **Dogfood**: run the freshly installed `purview-build` against this repository (with `GITHUB_TOKEN`). The tool restores, builds, lints, runs tests, packs, and validates itself. + +## Release (`release.yml`) + +Runs on push to `main` and is serialized by its own `concurrency` group (`purview-build-release`, `cancel-in-progress: false`): + +1. Read and SemVer-validate the `package.json` version; skip the whole job when `v{version}` is already tagged (the tag check makes re-merges safe). +2. Restore and build `src/Build.slnx` with `--warnaserror`. +3. Pack the tool from source with `-p:ContinuousIntegrationBuild=true`. +4. Verify the `NUGET__APIKEY` secret is set. +5. Install the packed tool. +6. **Run the release pipeline** with `Release__Mode=NuGet`, `NuGet__FeedUrl=https://api.nuget.org/v3/index.json`, `Release__UploadArtifacts=true`, `Build__RunTests=false`, `Build__RunLint=false`, and `Build__ValidatePack=true`, passing `GITHUB_TOKEN` and `NUGET_APIKEY`. + +The tool therefore publishes the immutable package to nuget.org 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 package visibility + +GitHub initially creates NuGet packages as private. An organization owner should set the org default to **Internal** (Purview-Dev → Settings → Packages → **Package Creation** → **Internal**) and change any already-published package's visibility in its **Package settings** → **Danger Zone**. See [Release Flow](Release-Flow.md) for the exact steps and the `gh api` alternative. + +## See also + +- [Release Flow](Release-Flow.md) +- [Getting Started](Getting-Started.md) +- [Architecture](Architecture.md) \ No newline at end of file diff --git a/docs/wiki/Secrets-and-Environment-Variables.md b/docs/wiki/Secrets-and-Environment-Variables.md new file mode 100644 index 0000000..d13c18e --- /dev/null +++ b/docs/wiki/Secrets-and-Environment-Variables.md @@ -0,0 +1,28 @@ +# Secrets and Environment Variables + +Secrets must never be committed. They are supplied at runtime via environment variables / CI secrets and read by the pipeline through the settings' lookup helpers. + +## Precedence recap + +Command line > environment variables > `purview-build.json` > baked-in defaults. Nested environment keys use `__`, for example `Release__Mode=NuGet`. Because env vars take precedence over `purview-build.json`, an empty forwarded env var can silently override a configured value — the reusable workflows only forward optional test settings when the caller actually provides them. + +## Secrets + +| Secret | Where it is used | Environment-var bound alias | +| --- | --- | --- | +| `NUGET_APIKEY` | NuGet push | `NuGet__NUGET_APIKEY` (binds `EnvAPIKey`); also read directly from process env `NUGET_APIKEY`/`NUGET_API_KEY` | +| `NuGet__ApiKey` | NuGet push | `APIKey` | +| `GITHUB_TOKEN` | GitHub release creation | `GitHub__GITHUB_TOKEN` (binds `EnvAccessToken`); also read directly from process env `GITHUB_TOKEN` | +| `LOCAL_NUGET_FEED_PATH` | Local NuGet publishing | `PublishLocalNuGet__LOCAL_NUGET_FEED_PATH` (binds `EnvLocalFeedPath`); also read directly from process env `LOCAL_NUGET_FEED_PATH` | + +The config binder does not map plain `NUGET_APIKEY`/`GITHUB_TOKEN`/`LOCAL_NUGET_FEED_PATH` process env vars under their settings sections, so the settings classes fall back to reading the process environment directly. + +## Test filter forwarding + +The reusable workflows (`purview-build.yml`, `purview-release.yml`) forward the caller's `test-filter` and `test-projects` inputs as `Build__TestFilter`/`Build__TestProjects` **only when they are non-empty**. An empty forwarded value would override a consuming repository's `purview-build.json` (env vars take precedence over JSON) and silently disable the filter — see commit `4d72bf7`. + +## See also + +- [Configuration Reference](Configuration-Reference.md) +- [Pipeline Modules](Pipeline-Modules.md) +- [Release Flow](Release-Flow.md) \ No newline at end of file diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md new file mode 100644 index 0000000..0d38431 --- /dev/null +++ b/docs/wiki/_Sidebar.md @@ -0,0 +1,11 @@ +- [Home](Home.md) +- [Getting Started](Getting-Started.md) +- [Architecture](Architecture.md) +- [Configuration Reference](Configuration-Reference.md) +- [Pipeline Modules](Pipeline-Modules.md) +- [Pack Validation](Pack-Validation.md) +- [Release Flow](Release-Flow.md) +- [Local Development](Local-Development.md) +- [Secrets and Environment Variables](Secrets-and-Environment-Variables.md) +- [Repository CI/CD](Repository-CI-CD.md) +- [Migration: aspire-resourcekit](Migration-aspire-resourcekit.md) \ No newline at end of file diff --git a/package.json b/package.json index 84bc16f..65b7c86 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,13 @@ { "name": "purview-build", "version": "0.2.4", - "private": true + "private": true, + "homepage": "https://purview.dev/projects/build/", + "bugs": { + "url": "https://github.com/purview-dev/build/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/purview-dev/build.git" + } } \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ffa7a83..5b865ee 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -7,10 +7,16 @@ + + https://purview.dev/ + $(PurviewHomepage)projects/build/ + $(PurviewHomepage)docs/build/ + + Purview Contributors Purview Developers - $(RepositoryUrl) + $(PurviewProjectUrl) $(RepositoryUrl) git purview-logo.png