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
67 changes: 54 additions & 13 deletions .agents/skills/purview-buildsdk-settings-docs-sync/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,49 @@ metadata:

# Keeping settings, docs and skills in step

> **Repository-local.** This skill lives in `.agents/skills/`, which is gitignored, not packed and not
> published. It is deliberately **not** linked from `AGENTS.md`: `RepositorySkillsSelfTests` fails any
> AGENTS.md link that points into `.agents/skills/`, because a fresh clone does not have this folder.
> **Repository-local.** This skill lives in `.agents/skills/` and is tracked by name (the rest of that
> folder is gitignored); it is not packed and not published. It is deliberately **not** linked from
> `AGENTS.md`: `RepositorySkillsSelfTests` fails any AGENTS.md link that points into `.agents/skills/`.
> The published skills are authored under `skills/` and are a different thing entirely.

## The general sweep is a published skill

This skill is the **settings** sweep for this repository. Any other documentation change — a renamed
recipe, a new wiki page, a moved page, a skill whose scope grew, an audit of whether the docs are
still true — follows the published
[`purview-buildsdk-docs-sync`](../../../skills/purview-buildsdk-docs-sync/SKILL.md) skill: every
claim checked against the code, and the wiki kept hierarchical. In this repository the wiki's root
pages are exactly `Getting-Started.md` (the site's `docs.rootPage`), `Configuration-Reference.md` and
`Repository-Bootstrap.md` (the site catalogue's `docsPage` deep links); every other page lives in one
topic folder, and `DocumentationWikiTests` holds the folders, `_Sidebar.md` and `Home.md` to that.

## Start from the schema

`src/src/BuildSdk/Sdk/Sdk.props.buildschema.json` is the canonical description of every SDK property —
description, type and default — and `PropertySchemaTests` holds the code, the `CompilerVisibleProperty`
list, `README.md` and `docs/wiki/Configuration-Reference.md` to it. The generic method (where a
sidecar goes, when a property is pre-import, when to export it, how to rename or retire one) is the
published [`purview-buildsdk-msbuild-properties`](../../../skills/purview-buildsdk-msbuild-properties/SKILL.md)
skill. In this repository, adding a property means:

1. `Sdk/Props/Defaults.props` — the default, guarded with `'$(X)' == ''`.
2. `Sdk/Sdk.props.buildschema.json` — the entry. `defaultValue` is what a plain class library evaluates
to; without one, end the description with `No default: <why>.`; when `Sdk.props` (or a file it
imports) reads the property, say `Set it before the \`Sdk.props\` import.` The test computes both
from the XML and fails if the description disagrees.
3. `Sdk/Sdk.props` — a `CompilerVisibleProperty` with a `<Description>`, only if an analyzer reads it.
4. `README.md` and `docs/wiki/Configuration-Reference.md` — a row in the table for its area (the
Default cell opens with the backticked default, or italics when there is none), plus a row in each
*Compiler-visible SDK properties* table when exported.
5. `CHANGELOG.md` — under the `package.json` version's heading (see the Changelog row below).
6. `just properties-check` — every failure names the file and the row to add.

A property that must not appear in IntelliSense goes in
`src/tests/BuildSdk.IntegrationTests/PropertySchema/SdkPropertyClassification.cs` with a reason:
`ExternallyOwned` (MSBuild/.NET/NuGet), `Internal` (unprefixed implementation detail),
`ComputedDefaults` (machine- or project-dependent value). Prefer a `_` prefix for anything new and
internal — then no list is needed.

## The rule this exists for

A property is written down in several places, and only some of them are checked by a test. The failure
Expand Down Expand Up @@ -44,11 +82,12 @@ Find the row and make sure every file agrees.
| -- | -- | -- |
| Runtime default (all projects) | `src/src/BuildSdk/Sdk/Props/Defaults.props` | The default when unset, plus the explanatory comment. Sets the value in `Sdk.props`, which is also what makes it compiler-visible. |
| Per-TFM default | `src/src/BuildSdk/Sdk/Targets/Modern.targets` | The `net11.0`+ feature selection. Must agree with `Defaults.props`. |
| Property schema | `src/src/BuildSdk/Sdk/Sdk.props.buildschema.json` | Description, type and `defaultValue` — what csproj IntelliSense shows. `PropertySchemaTests` fails when it disagrees with the code or the docs. |
| Compiler-visible description | `src/src/BuildSdk/Sdk/Sdk.props` | The `CompilerVisibleProperty` `<Description>`, which is what an analyzer author reads. |
| Pinning test | `src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs` | The test that fails when the default changes silently. |
| Changelog | `CHANGELOG.md` | A `[Unreleased]` entry: `### Changed` for behaviour, `### Documentation` for docs-only. |
| Changelog | `CHANGELOG.md` | An entry under `## [<package.json version>] - <date>`: `### Changed` for behaviour, `### Documentation` for docs-only. Never `[Unreleased]` during active development — a merge to `main` publishes the version, so the entry would read as unreleased until it was already live. `[Unreleased]` is only for a pre-release version (`-alpha`, `-beta`, `-rc`). |
| README | `README.md` | The feature table row, the property table(s), the quick start, and the `.NET 11 features` section + tiers paragraph. |
| Wiki | `docs/wiki/Configuration-Reference.md`, `Getting-Started.md`, `Version-Detection.md`, `Project-Type-Detection.md` | The property tables, the required-settings and target-framework sections, and the compiler-visible section. |
| Wiki | `docs/wiki/Configuration-Reference.md`, `Getting-Started.md`, `configuration/Version-Detection.md`, `concepts/Project-Type-Detection.md` | The property tables, the required-settings and target-framework sections, and the compiler-visible section. |
| Shipped skills | `skills/purview-buildsdk-*/SKILL.md` and `references/*.md` | The property tables and the sections below. |
| AGENTS.md | `AGENTS.md` | The "authored set, and what each one answers" table, when a skill's scope grows. |

Expand All @@ -62,8 +101,9 @@ Two traps worth naming:
which it is, in the docs, next to the property.

A **feature** bigger than a single property gets its own guide as well. SourceLink is the worked
example, because the decision is per repository host: add a wiki page `docs/wiki/<Feature>.md` and
register it in `_Sidebar.md` **and** `Home.md`, add a `## <Feature>` section to `README.md`, and — when
example, because the decision is per repository host: add a wiki page in its topic folder
(`docs/wiki/packaging/SourceLink.md`) and register it in `_Sidebar.md` **and** that section's table in
`Home.md` — `DocumentationWikiTests` fails until both list it — add a `## <Feature>` section to `README.md`, and — when
it is configuration — a `references/<feature>.md` under
`skills/purview-buildsdk-configuration/` that the `SKILL.md` links. Fix any wrong package or property
name you find while documenting: the SourceLink pages used to name `Microsoft.SourceLink.AzureDevOps.Git`,
Expand All @@ -83,7 +123,7 @@ which does not exist.
6. `docs/wiki/Configuration-Reference.md` — the General row **and** the `.NET 11 features` table.
7. Skills — the `MemorySafetyRules`/`PurviewRuntimeAsync` rows in
`skills/purview-buildsdk-configuration/SKILL.md` and `references/properties.md`.
8. `CHANGELOG.md` — a `[Unreleased]` `### Changed` entry that names the old default, the new one, how to
8. `CHANGELOG.md` — a `### Changed` entry under the `package.json` version's heading that names the old default, the new one, how to
opt in, and what is unchanged.

Keep `AllowUnsafeBlocks` (and anything else) at its compiler default: a feature flip must not quietly
Expand All @@ -110,8 +150,8 @@ Every skill and doc that talks about configuring a project must explain how to s
framework(s), consistently:

- the default set is `net10.0;net11.0` (`Current`);
- an explicit `TargetFramework`/`TargetFrameworks` must be declared **unconditionally** (the SDK reads
it from the project XML, so a `Condition` is not seen — `PurviewConditionedProjectDeclaration`);
- an explicit `TargetFramework`/`TargetFrameworks` always wins over every set, wherever it is set
except `Directory.Build.targets` (`Targets/TargetFrameworkOwnership.targets` reconciles it);
- a curated `PurviewTargetFrameworkSet` (`Current`, `Latest`, `Supported`, `Broad`, `All`) or a
redefined `PurviewTargetFrameworks<Set>` (pre-import, in `Directory.Build.props`);
- a Roslyn component keeps `netstandard2.0` and ignores the sets (`PurviewTargetFrameworkSetIgnored`).
Expand All @@ -126,7 +166,7 @@ The `CompilerVisibleProperty` → `build_property.*` mechanism, plus a one-line

- `skills/purview-buildsdk-configuration/SKILL.md` and `references/properties.md`,
- `skills/purview-buildsdk-project-detection/SKILL.md` (the analyzers section),
- `README.md`, `docs/wiki/Configuration-Reference.md` and `docs/wiki/Project-Type-Detection.md`.
- `README.md`, `docs/wiki/Configuration-Reference.md` and `docs/wiki/concepts/Project-Type-Detection.md`.

Say both halves: **consume** the exported property instead of re-deriving SDK behaviour, and **add a
new one** by listing it as a `CompilerVisibleProperty` item (with a `<Description>`) in `Sdk.props` —
Expand All @@ -145,7 +185,7 @@ skills tooling does not.
| `purview-buildsdk-project-detection` | the target-framework subsection, the `build_property.*` note |
| `purview-buildsdk-repository-review` | the read list (`global.json`, `package.json`), the target-framework finding |
| `purview-buildsdk-testing` | the test-project target-framework paragraph |
| `purview-buildsdk-skill-tests`, `-skill-installation`, `-skills-justfile` | **not** project configuration — leave them alone |
| `purview-buildsdk-skill-tests`, `-skill-installation`, `-justfile` | **not** project configuration — leave them alone |

Also update each skill's frontmatter `description` so the change makes the skill **fire** on the right
question ("setting the target framework(s)", "pinning the SDK version in global.json").
Expand All @@ -157,12 +197,13 @@ just build
just skills-validate # PRSKL0005, the pack-time check
just version-sync-check # the version pins must still match package.json
just lint-check # CSharpier over the repo
just properties-check # schema vs props, defaults, compiler-visible list, README and wiki tables

# Targeted and fast (MTP: --treenode-filter, never --filter):
dotnet test --project src/BuildSdk.slnx --configuration Debug \
--treenode-filter "/*/*/Dotnet11FeaturesTests/*" --ignore-exit-code 8
dotnet test --project src/BuildSdk.slnx --configuration Debug \
--treenode-filter "/*/*/(TargetFrameworkSetTests)|(CompilerVisiblePropertyTests)|(CoreDefaultsTests)/*" --ignore-exit-code 8
--treenode-filter "/*/*/(TargetFrameworkSetTests)|(PropertySchemaTests)|(CoreDefaultsTests)/*" --ignore-exit-code 8
dotnet test --project src/BuildSdk.slnx --configuration Debug \
--treenode-filter "/*/*/(RepositorySkillTests)|(RepositorySkillsSelfTests)|(PurviewSkillValidationTests)/*" --ignore-exit-code 8
```
Expand Down
62 changes: 62 additions & 0 deletions .build/just/bump-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Bumps one part of the semantic version in package.json, for `just bump <major|minor|patch>`.
//
// The parts after the one bumped are reset to 0, and a pre-release or build suffix is dropped:
//
// 2.0.1 major -> 3.0.0 minor -> 2.1.0 patch -> 2.0.2
// 2.1.0-rc.1 patch -> 2.1.1
//
// Only the "version" value is rewritten, in place, so the file keeps its own indentation, key order
// and line endings - re-serialising the JSON would reformat it.
//
// bun bump-version.ts <major|minor|patch> [package.json] [--dry-run]

import { readFileSync, writeFileSync } from "node:fs";

const parts = ["major", "minor", "patch"] as const;
type Part = (typeof parts)[number];

const args = process.argv.slice(2);
const dryRun = args.includes("--dry-run");
const [part, file = "package.json"] = args.filter((arg) => arg !== "--dry-run");

if (!parts.includes(part as Part)) {
console.error(`Expected one of ${parts.join(", ")}, not '${part ?? ""}'.`);
process.exit(1);
}

const text = readFileSync(file, "utf8");
const versionPattern = /("version"\s*:\s*")([^"]*)(")/;
const match = versionPattern.exec(text);
if (!match) {
console.error(`No "version" field in ${file}.`);
process.exit(1);
}

const current = match[2];
const semver = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(current);
if (!semver) {
console.error(`'${current}' in ${file} is not a major.minor.patch version.`);
process.exit(1);
}

let [major, minor, patch] = semver.slice(1, 4).map(Number);
switch (part as Part) {
case "major":
[major, minor, patch] = [major + 1, 0, 0];
break;
case "minor":
[minor, patch] = [minor + 1, 0];
break;
case "patch":
patch += 1;
break;
}

const next = `${major}.${minor}.${patch}`;
if (dryRun) {
console.log(` ${file}: ${current} -> ${next} (dry run, nothing written)`);
process.exit(0);
}

writeFileSync(file, text.replace(versionPattern, `$1${next}$3`));
console.log(` ${file}: ${current} -> ${next}`);
53 changes: 53 additions & 0 deletions .build/just/dotnet.just
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# .NET recipes: tools, restore, build, clean, test and pack, all against one solution.
#
# Override these in the root Justfile. `solution_file` is the one nearly every repository sets; "."
# lets dotnet find the single solution or project in the repository root.

solution_file := "."
build_configuration := "Debug"
artifacts_folder := "./artifacts"

# Every test. A TUnit/MTP tree-node filter: /assembly/namespace/class/test.
test_filter := "/*/*/*/*/"

# Restore the repository's .NET local tools (.config/dotnet-tools.json)
[group('Build and Test')]
ensure-tools:
dotnet tool restore

# Restore NuGet packages for the solution
[group('Build and Test')]
restore *args:
echo "==> Restoring {{ BLUE }}{{ solution_file }}{{ NORMAL }}"
dotnet restore "{{ solution_file }}" {{ args }}

# Build the solution with build_configuration (default: Debug)
[group('Build and Test')]
build *args:
echo "==> Building {{ BLUE }}{{ solution_file }}{{ NORMAL }} ({{ GREEN }}{{ current_version }}{{ NORMAL }}) with configuration {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}"
dotnet build "{{ solution_file }}" --configuration "{{ build_configuration }}" {{ args }}

# Clean the solution with build_configuration (default: Debug)
[group('Build and Test')]
clean *args:
echo "==> Cleaning {{ BLUE }}{{ solution_file }}{{ NORMAL }} with configuration {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}"
dotnet clean "{{ solution_file }}" --configuration "{{ build_configuration }}" {{ args }}

# Run the tests matching a tree-node filter (default: every test)
#
# just test "/*/*/*/*[Category=Unit]"
# just test "/*/*/ProjectHarnessTests/*"
#
# Exit code 8 - "no test ran" - is ignored, so a filter that matches nothing in one test project does
# not fail the run for the others.
[doc('Run the tests matching a tree-node filter (default: every test)')]
[group('Build and Test')]
test filter=test_filter *args:
echo "==> Testing {{ BLUE }}{{ solution_file }}{{ NORMAL }} ({{ YELLOW }}{{ build_configuration }}{{ NORMAL }}) with filter {{ GREEN }}{{ filter }}{{ NORMAL }}"
dotnet test --project "{{ solution_file }}" --configuration "{{ build_configuration }}" --treenode-filter "{{ filter }}" --ignore-exit-code 8 {{ args }}

# Pack every packable project into artifacts_folder (default: ./artifacts)
[group('Build and Test')]
pack artifact_folder=artifacts_folder *args:
echo "==> Packing {{ BLUE }}{{ solution_file }}{{ NORMAL }} ({{ GREEN }}{{ current_version }}{{ NORMAL }}) to {{ YELLOW }}{{ artifact_folder }}{{ NORMAL }}"
dotnet pack "{{ solution_file }}" --configuration "{{ build_configuration }}" --output "{{ artifact_folder }}" {{ args }}
56 changes: 56 additions & 0 deletions .build/just/env.just
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# The environment every recipe runs in: telemetry opt-outs, and the .NET CLI speed-ups.
#
# `export` hands each value to every process a recipe starts - dotnet, bun, pwsh, git. Each one is a
# default: `env(NAME, value)` keeps whatever the caller's environment already says, so a developer
# who has opted back in, or a CI job that sets its own value, is never overridden.

# -- Telemetry and tracking --------------------------------------------------------------------------

# The .NET CLI, MSBuild and Aspire.
export DOTNET_CLI_TELEMETRY_OPTOUT := env("DOTNET_CLI_TELEMETRY_OPTOUT", "1")
export MSBUILD_TELEMETRY_OPTOUT := env("MSBUILD_TELEMETRY_OPTOUT", "1")
export ASPIRE_CLI_TELEMETRY_OPTOUT := env("ASPIRE_CLI_TELEMETRY_OPTOUT", "true")
export ASPIRE_DASHBOARD_TELEMETRY_OPTOUT := env("ASPIRE_DASHBOARD_TELEMETRY_OPTOUT", "true")

# The Azure SDKs and the Azure MCP server.
export AZURE_CORE_COLLECT_TELEMETRY := env("AZURE_CORE_COLLECT_TELEMETRY", "0")
export AZURE_MCP_COLLECT_TELEMETRY := env("AZURE_MCP_COLLECT_TELEMETRY", "0")

# PowerShell, for scripts a recipe runs through pwsh, and Git Credential Manager.
export POWERSHELL_TELEMETRY_OPTOUT := env("POWERSHELL_TELEMETRY_OPTOUT", "1")
export GCM_TELEMETRY_OPTOUT := env("GCM_TELEMETRY_OPTOUT", "1")

# JavaScript tooling: Next.js, Astro, Yarn, and the funding banners printed by postinstall scripts.
export NEXT_TELEMETRY_DISABLED := env("NEXT_TELEMETRY_DISABLED", "1")
export ASTRO_TELEMETRY_DISABLED := env("ASTRO_TELEMETRY_DISABLED", "1")
export YARN_ENABLE_TELEMETRY := env("YARN_ENABLE_TELEMETRY", "0")
export DISABLE_OPENCOLLECTIVE := env("DISABLE_OPENCOLLECTIVE", "true")

# Claude Code, when a recipe launches it.
export CLAUDE_CLI_TELEMETRY_OPTOUT := env("CLAUDE_CLI_TELEMETRY_OPTOUT", "1")
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC := env("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1")
export DISABLE_ERROR_REPORTING := env("DISABLE_ERROR_REPORTING", "1")
export DISABLE_TELEMETRY := env("DISABLE_TELEMETRY", "1")

# The cross-tool conventions, honoured by Bun and many other CLIs.
export DO_NOT_TRACK := env("DO_NOT_TRACK", "1")
export TELEMETRY_DISABLED := env("TELEMETRY_DISABLED", "1")

# -- .NET CLI speed and noise ------------------------------------------------------------------------

# No welcome banner or first-run notice.
export DOTNET_NOLOGO := env("DOTNET_NOLOGO", "1")

# The NativeAOT CLI host (.NET 11): near-instant `dotnet --version`, `--help` and tool launches, with
# everything else falling back to the managed CLI. On by default; opt out with DOTNET_CLI_ENABLEAOT=false.
export DOTNET_CLI_ENABLEAOT := env("DOTNET_CLI_ENABLEAOT", "true")

# The MSBuild server (DOTNET_CLI_USE_MSBUILD_SERVER) is deliberately left to the SDK's own default.
# Forcing it on makes every `dotnet build` share one long-lived MSBuild process, and its NuGet SDK
# resolver keeps the settings it first read: a build under a different nuget.config - a test
# harness's throwaway consumer, a second repository - then resolves SDKs with the wrong sources and
# fallback folders, and fails with MSB4242. Set it yourself where every build shares one NuGet
# configuration; `just scrub` shuts the server down.

# Multithreaded MSBuild (-mt) is deliberately not set here. Purview.BuildSdk bootstraps it into the
# repository's Directory.Build.rsp, so a plain `dotnet build` and an IDE build get it as well.
Loading
Loading