From 6c1e301b41c8342ec973e0a7ae268941abcb026b Mon Sep 17 00:00:00 2001 From: Ed Ball Date: Mon, 18 May 2026 10:28:49 -0700 Subject: [PATCH 1/6] Rename apm-install convention to apm Update the convention settings to use install instead of packages and only pass --update when explicitly requested. Refresh the README, tests, and in-repo references for the new path and behavior. --- README.md | 2 +- conventions/agentic-repo/convention.yml | 4 +- conventions/apm-install/README.md | 33 ------------ conventions/apm/README.md | 36 +++++++++++++ .../{apm-install => apm}/convention.Tests.ps1 | 54 ++++++++++++++++--- .../{apm-install => apm}/convention.ps1 | 27 +++++++--- .../{apm-install => apm}/convention.yml | 0 7 files changed, 107 insertions(+), 49 deletions(-) delete mode 100644 conventions/apm-install/README.md create mode 100644 conventions/apm/README.md rename conventions/{apm-install => apm}/convention.Tests.ps1 (82%) rename conventions/{apm-install => apm}/convention.ps1 (69%) rename conventions/{apm-install => apm}/convention.yml (100%) diff --git a/README.md b/README.md index 9bf0263..2b9d574 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ | Convention | Description | | --- | --- | | [agentic-repo](./conventions/agentic-repo/README.md) | Applies conventions useful for repositories that keep agent customization files in source control. | -| [apm-install](./conventions/apm-install/README.md) | Installs configured packages and updates existing packages for the Copilot APM target with `apm`. | +| [apm](./conventions/apm/README.md) | Installs configured packages for the Copilot APM target with `apm` and optionally updates existing packages. | | [build-script](./conventions/build-script/README.md) | Installs the published `build.ps1` script at the repository root. | | [config-text-section](./conventions/config-text-section/README.md) | Manages one named text section in a repository file. | | [copilot-lsp](./conventions/copilot-lsp/README.md) | Manages project-scoped GitHub Copilot CLI LSP server definitions in `.github/lsp.json`. | diff --git a/conventions/agentic-repo/convention.yml b/conventions/agentic-repo/convention.yml index 49c3849..c23e328 100644 --- a/conventions/agentic-repo/convention.yml +++ b/conventions/agentic-repo/convention.yml @@ -27,7 +27,9 @@ conventions: .agents/ apm.lock.yaml apm.yml - - path: ../apm-install + - path: ../apm + settings: + update: true pull-request: auto-merge: true diff --git a/conventions/apm-install/README.md b/conventions/apm-install/README.md deleted file mode 100644 index 42a734f..0000000 --- a/conventions/apm-install/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# apm-install - -Installs configured packages and updates existing packages for the Copilot APM target with `apm`. - -## Settings - -- `packages`: Optional sequence of package identifiers to pass to `apm install --update --target copilot`. Defaults to no configured packages. - -## Behavior - -The convention requires the `apm` command to be available when it runs. `copilot` is the only target currently supported. - -If no packages are configured and the repository has no root `apm.yml`, the convention leaves the repository unchanged. After `apm install` completes, if the only changed file is `apm.lock.yaml`, the convention restores that file so update-only no-op runs stay clean. - -## Examples - -Install and update: - -```yaml -conventions: - - path: Faithlife/CodingGuidelines/conventions/apm-install - settings: - packages: - - richlander/dotnet-inspect/skills/dotnet-inspect - - microsoft/playwright-cli/skills/playwright-cli -``` - -Update only: - -```yaml -conventions: - - path: Faithlife/CodingGuidelines/conventions/apm-install -``` diff --git a/conventions/apm/README.md b/conventions/apm/README.md new file mode 100644 index 0000000..94dc324 --- /dev/null +++ b/conventions/apm/README.md @@ -0,0 +1,36 @@ +# apm + +Installs configured packages for the Copilot APM target with `apm` and optionally updates existing packages. + +## Settings + +- `install`: Optional sequence of package identifiers to pass to `apm install --target copilot`. Defaults to no configured packages. +- `update`: Optional boolean that adds `--update` to `apm install` when `true`. Defaults to `false`. + +## Behavior + +The convention requires the `apm` command to be available when it runs. `copilot` is the only target currently supported. + +If no packages are configured and the repository has no root `apm.yml`, the convention leaves the repository unchanged. When `update` is `true`, the convention adds `--update`; otherwise it runs a plain install. After `apm install` completes, if the only changed file is `apm.lock.yaml`, the convention restores that file so update-only no-op runs stay clean. + +## Examples + +Install specific packages: + +```yaml +conventions: + - path: Faithlife/CodingGuidelines/conventions/apm + settings: + install: + - richlander/dotnet-inspect/skills/dotnet-inspect + - microsoft/playwright-cli/skills/playwright-cli +``` + +Update only: + +```yaml +conventions: + - path: Faithlife/CodingGuidelines/conventions/apm + settings: + update: true +``` diff --git a/conventions/apm-install/convention.Tests.ps1 b/conventions/apm/convention.Tests.ps1 similarity index 82% rename from conventions/apm-install/convention.Tests.ps1 rename to conventions/apm/convention.Tests.ps1 index c0e38d6..cc3248b 100644 --- a/conventions/apm-install/convention.Tests.ps1 +++ b/conventions/apm/convention.Tests.ps1 @@ -7,8 +7,8 @@ $utf8 = [System.Text.UTF8Encoding]::new($false) [Console]::OutputEncoding = $utf8 $OutputEncoding = $utf8 -# Define the Pester suite for the apm-install convention. -Describe 'apm-install convention' { +# Define the Pester suite for the apm convention. +Describe 'apm convention' { BeforeAll { # Load the convention script and shared test helpers. $script:conventionScriptPath = Join-Path $PSScriptRoot 'convention.ps1' @@ -81,7 +81,7 @@ exit 0 } } - It 'runs apm install --update --target copilot' { + It 'runs apm install --target copilot by default' { # Set up a repository with apm.yml and a fake argument capture file. $testDirectory = New-TemporaryDirectory $toolDirectory = New-TemporaryDirectory @@ -109,6 +109,48 @@ exit 0 # Run the convention and assert it invokes apm with the default arguments. { Invoke-ConventionScript -ScriptPath $conventionScriptPath -RepositoryRoot $testDirectory -InputPath $inputPath } | Should -Not -Throw + ((Get-Content -LiteralPath $argumentsPath -Raw).TrimEnd("`r", "`n")) | Should -Be 'install --target copilot' + } + finally { + # Restore process state and remove temporary files. + $env:PATH = $originalPath + Remove-Item Env:APM_ARGUMENTS_PATH -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $inputPath -Force + Remove-Item -LiteralPath $toolDirectory -Recurse -Force + Remove-Item -LiteralPath $testDirectory -Recurse -Force + } + } + + It 'adds --update only when the update setting is true' { + # Set up a repository with apm.yml and an explicit update request. + $testDirectory = New-TemporaryDirectory + $toolDirectory = New-TemporaryDirectory + $argumentsPath = Join-Path $toolDirectory 'apm-arguments.txt' + $inputPath = New-ConventionInputFile -Settings @{ + update = $true + } + $originalPath = $env:PATH + + try { + # Arrange a fake apm command that records its argument list. + [System.IO.File]::WriteAllText((Join-Path $testDirectory 'apm.yml'), "packages: []`n", $utf8) + Initialize-TestRepository -Path $testDirectory + NewFakeApmCommand -ToolDirectory $toolDirectory -WindowsScript @' +@echo off +setlocal +> "%APM_ARGUMENTS_PATH%" echo %* +exit /b 0 +'@ -BashScript @' +#!/usr/bin/env bash +printf '%s\n' "$*" > "$APM_ARGUMENTS_PATH" +exit 0 +'@ + + $env:APM_ARGUMENTS_PATH = $argumentsPath + $env:PATH = $toolDirectory + [System.IO.Path]::PathSeparator + $originalPath + + # Run the convention and assert the update flag is included only when requested. + { Invoke-ConventionScript -ScriptPath $conventionScriptPath -RepositoryRoot $testDirectory -InputPath $inputPath } | Should -Not -Throw ((Get-Content -LiteralPath $argumentsPath -Raw).TrimEnd("`r", "`n")) | Should -Be 'install --update --target copilot' } finally { @@ -121,13 +163,13 @@ exit 0 } } - It 'passes configured packages to apm install --update --target copilot' { + It 'passes configured install packages to apm install --target copilot' { # Set up convention input that includes configured apm packages. $testDirectory = New-TemporaryDirectory $toolDirectory = Join-Path $testDirectory 'tools' $argumentsPath = Join-Path $testDirectory 'apm-arguments.txt' $inputPath = New-ConventionInputFile -Settings @{ - packages = @( + install = @( 'richlander/dotnet-inspect/skills/dotnet-inspect' 'microsoft/playwright-cli/skills/playwright-cli' ) @@ -153,7 +195,7 @@ exit 0 # Run the convention and assert configured packages are appended. { & $conventionScriptPath $inputPath } | Should -Not -Throw - ((Get-Content -LiteralPath $argumentsPath -Raw).TrimEnd("`r", "`n")) | Should -Be 'install --update --target copilot richlander/dotnet-inspect/skills/dotnet-inspect microsoft/playwright-cli/skills/playwright-cli' + ((Get-Content -LiteralPath $argumentsPath -Raw).TrimEnd("`r", "`n")) | Should -Be 'install --target copilot richlander/dotnet-inspect/skills/dotnet-inspect microsoft/playwright-cli/skills/playwright-cli' } finally { # Restore process state and remove temporary files. diff --git a/conventions/apm-install/convention.ps1 b/conventions/apm/convention.ps1 similarity index 69% rename from conventions/apm-install/convention.ps1 rename to conventions/apm/convention.ps1 index 9a2c1d0..c237925 100644 --- a/conventions/apm-install/convention.ps1 +++ b/conventions/apm/convention.ps1 @@ -7,26 +7,37 @@ $utf8 = [System.Text.UTF8Encoding]::new($false) [Console]::OutputEncoding = $utf8 $OutputEncoding = $utf8 -# Collect optional package settings from the convention input. -$packages = @() +# Collect optional install settings from the convention input. +$packagesToInstall = @() +$shouldUpdate = $false $conventionInput = Get-Content -LiteralPath $args[0] -Raw | ConvertFrom-Json -AsHashtable $settings = $conventionInput.settings -if ($null -ne $settings -and $settings.ContainsKey('packages') -and $null -ne $settings.packages) { - [string[]] $packages = @($settings.packages) +if ($null -ne $settings -and $settings.ContainsKey('install') -and $null -ne $settings.install) { + [string[]] $packagesToInstall = @($settings.install) +} + +if ($null -ne $settings -and $settings.ContainsKey('update') -and $null -ne $settings.update) { + $shouldUpdate = [bool] $settings.update } # Skip when neither an apm manifest nor explicit packages are available. -if ($packages.Count -eq 0 -and -not (Test-Path -LiteralPath 'apm.yml')) { +if ($packagesToInstall.Count -eq 0 -and -not (Test-Path -LiteralPath 'apm.yml')) { Write-Host 'Skipping apm install because apm.yml is absent and no packages were configured.' return } # Build the apm install command for the copilot target. -$apmArguments = @('install', '--update', '--target', 'copilot') +$apmArguments = @('install') + +if ($shouldUpdate) { + $apmArguments += '--update' +} + +$apmArguments += @('--target', 'copilot') -if ($packages.Count -gt 0) { - $apmArguments += $packages +if ($packagesToInstall.Count -gt 0) { + $apmArguments += $packagesToInstall } # Verify apm is available before invoking it. diff --git a/conventions/apm-install/convention.yml b/conventions/apm/convention.yml similarity index 100% rename from conventions/apm-install/convention.yml rename to conventions/apm/convention.yml From cbc3938cf44d4f931af3b3568d45e32a04ff37b1 Mon Sep 17 00:00:00 2001 From: Ed Ball Date: Mon, 18 May 2026 12:20:17 -0700 Subject: [PATCH 2/6] Add C# gitattributes convention and docs --- README.md | 1 + conventions/gitattributes-csharp/README.md | 14 ++++ .../gitattributes-csharp/convention.Tests.ps1 | 77 +++++++++++++++++++ .../gitattributes-csharp/convention.yml | 11 +++ .../gitattributes-csharp/files/.gitattributes | 2 + sections/csharp/README.md | 1 + sections/csharp/gitattributes.md | 11 +++ 7 files changed, 117 insertions(+) create mode 100644 conventions/gitattributes-csharp/README.md create mode 100644 conventions/gitattributes-csharp/convention.Tests.ps1 create mode 100644 conventions/gitattributes-csharp/convention.yml create mode 100644 conventions/gitattributes-csharp/files/.gitattributes create mode 100644 sections/csharp/gitattributes.md diff --git a/README.md b/README.md index 2b9d574..032418a 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ | [faithlife-dotnet-library-build](./conventions/faithlife-dotnet-library-build/README.md) | Creates or refreshes Faithlife .NET library build project files under `tools/Build`. | | [faithlife-dotnet-library-workflow](./conventions/faithlife-dotnet-library-workflow/README.md) | Installs the published Faithlife .NET library workflows at `.github/workflows/ci.yml` and `.github/workflows/copilot-setup-steps.yml`. | | [faithlife-license-mit](./conventions/faithlife-license-mit/README.md) | Applies [license-mit](./conventions/license-mit/README.md) with `copyright-holder` set to `Faithlife`. | +| [gitattributes-csharp](./conventions/gitattributes-csharp/README.md) | Ensures the repository-root `.gitattributes` contains the standard C# section from [files/.gitattributes](./conventions/gitattributes-csharp/files/.gitattributes). | | [gitattributes-lf](./conventions/gitattributes-lf/README.md) | Ensures the repository-root `.gitattributes` starts with `* text=auto eol=lf`. | | [gitattributes-section](./conventions/gitattributes-section/README.md) | Manages a named section within the repository-root `.gitattributes` file. | | [gitignore-section](./conventions/gitignore-section/README.md) | Manages a named section within the repository-root `.gitignore` file. | diff --git a/conventions/gitattributes-csharp/README.md b/conventions/gitattributes-csharp/README.md new file mode 100644 index 0000000..b1b8b5a --- /dev/null +++ b/conventions/gitattributes-csharp/README.md @@ -0,0 +1,14 @@ +# gitattributes-csharp + +Ensures the repository-root `.gitattributes` contains the standard C# section from [files/.gitattributes](./files/.gitattributes). + +## Behavior + +The convention manages the fixed `csharp` section and reads the section text from the packaged [files/.gitattributes](./files/.gitattributes) asset. Existing `.gitattributes` content outside the managed section is preserved. + +## Example + +```yaml +conventions: + - path: Faithlife/CodingGuidelines/conventions/gitattributes-csharp +``` diff --git a/conventions/gitattributes-csharp/convention.Tests.ps1 b/conventions/gitattributes-csharp/convention.Tests.ps1 new file mode 100644 index 0000000..7a99fec --- /dev/null +++ b/conventions/gitattributes-csharp/convention.Tests.ps1 @@ -0,0 +1,77 @@ +#requires -PSEdition Core +#requires -Version 7.0 +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$utf8 = [System.Text.UTF8Encoding]::new($false) +[Console]::InputEncoding = $utf8 +[Console]::OutputEncoding = $utf8 +$OutputEncoding = $utf8 + +Describe 'gitattributes-csharp convention' { + BeforeAll { + # Load shared test helpers for temporary repositories and convention execution. + $script:testHelpersPath = Join-Path $PSScriptRoot '..' 'scripts' 'TestHelpers.ps1' + . $script:testHelpersPath + } + + It 'creates .gitattributes with the shared C# section' { + $testDirectory = New-TemporaryDirectory + + try { + # Arrange an isolated repository with the C# gitattributes convention enabled. + Copy-TestConventionAssets -TestDirectory $testDirectory + [System.IO.Directory]::CreateDirectory((Join-Path $testDirectory '.github')) | Out-Null + [System.IO.File]::WriteAllText((Join-Path $testDirectory '.github/conventions.yml'), @" +conventions: +- path: ../conventions/gitattributes-csharp +"@, $utf8) + Initialize-TestRepository -Path $testDirectory + + # Apply the convention under test. + { Invoke-RepoConventionsApply -TestDirectory $testDirectory } | Should -Not -Throw + + # Read the generated gitattributes and packaged section for comparison. + $gitattributesPath = Join-Path $testDirectory '.gitattributes' + $content = Get-Content -LiteralPath $gitattributesPath -Raw + $normalizedContent = ($content -replace "`r`n", "`n") + $expectedSection = ((Get-Content -LiteralPath (Join-Path $testDirectory 'conventions/gitattributes-csharp/files/.gitattributes') -Raw) -replace "`r`n", "`n").TrimEnd("`n") + + # Assert the generated file contains the managed C# section. + (Test-Path -LiteralPath $gitattributesPath) | Should -Be $true + $content | Should -Match "(?m)^# DO NOT EDIT: csharp convention\r?$" + $content | Should -Match "(?m)^# generated from https://github\.com/Faithlife/CodingGuidelines/blob/master/sections/csharp/gitattributes\.md\r?$" + $content | Should -Match "(?m)^\*\.cs text diff=csharp\r?$" + $normalizedContent.Contains($expectedSection) | Should -Be $true + } + finally { + Remove-Item -LiteralPath $testDirectory -Recurse -Force + } + } + + It 'commits .gitattributes changes with the packaged commit message' { + $testDirectory = New-TemporaryDirectory + + try { + # Arrange an isolated repository with the C# gitattributes convention enabled. + Copy-TestConventionAssets -TestDirectory $testDirectory + [System.IO.Directory]::CreateDirectory((Join-Path $testDirectory '.github')) | Out-Null + [System.IO.File]::WriteAllText((Join-Path $testDirectory '.github/conventions.yml'), @" +conventions: +- path: ../conventions/gitattributes-csharp +"@, $utf8) + Initialize-TestRepository -Path $testDirectory + $initialHead = Get-CommitId -TestDirectory $testDirectory + + # Apply the convention and allow it to create its packaged commit. + { Invoke-RepoConventionsApply -TestDirectory $testDirectory } | Should -Not -Throw + + # Assert the commit message and clean working tree match expectations. + (Get-CommitId -TestDirectory $testDirectory -Revision 'HEAD~1') | Should -Be $initialHead + (@(Get-CommitSubjects -TestDirectory $testDirectory -Count 1))[0] | Should -Be 'Update C# gitattributes settings' + (@(Get-GitStatusLines -TestDirectory $testDirectory)).Count | Should -Be 0 + } + finally { + Remove-Item -LiteralPath $testDirectory -Recurse -Force + } + } +} diff --git a/conventions/gitattributes-csharp/convention.yml b/conventions/gitattributes-csharp/convention.yml new file mode 100644 index 0000000..4074beb --- /dev/null +++ b/conventions/gitattributes-csharp/convention.yml @@ -0,0 +1,11 @@ +commit: + message: "Update C# gitattributes settings" + +conventions: + - path: ../gitattributes-section + settings: + name: csharp + text: ${{ readText("files/.gitattributes") }} + +pull-request: + auto-merge: true \ No newline at end of file diff --git a/conventions/gitattributes-csharp/files/.gitattributes b/conventions/gitattributes-csharp/files/.gitattributes new file mode 100644 index 0000000..e6622fe --- /dev/null +++ b/conventions/gitattributes-csharp/files/.gitattributes @@ -0,0 +1,2 @@ +# generated from https://github.com/Faithlife/CodingGuidelines/blob/master/sections/csharp/gitattributes.md +*.cs text diff=csharp \ No newline at end of file diff --git a/sections/csharp/README.md b/sections/csharp/README.md index a858975..dca11b7 100644 --- a/sections/csharp/README.md +++ b/sections/csharp/README.md @@ -2,6 +2,7 @@ # C# Coding Guidelines * [.editorconfig for C#](./editorconfig.md) +* [.gitattributes for C#](./gitattributes.md) * [global.json](./globaljson.md) * [NoWarn usage](./nowarn.md) diff --git a/sections/csharp/gitattributes.md b/sections/csharp/gitattributes.md new file mode 100644 index 0000000..7cb300f --- /dev/null +++ b/sections/csharp/gitattributes.md @@ -0,0 +1,11 @@ +# .gitattributes for C# + +The first line of `.gitattributes` file should be [as indicated here](../gitattributes.md). + +The `.gitattributes` file for a C# repository should also include this line, which improves git handling of C# files in some situations: + +```text +*.cs text diff=csharp +``` + +Repository convention: [gitattributes-csharp](../../conventions/gitattributes-csharp/) From 3ec9d16c52bcd023e0631b22f5049909ef519148 Mon Sep 17 00:00:00 2001 From: Ed Ball Date: Mon, 18 May 2026 12:23:38 -0700 Subject: [PATCH 3/6] Add convention --- .github/conventions.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/conventions.yml b/.github/conventions.yml index 4560e23..e705cea 100644 --- a/.github/conventions.yml +++ b/.github/conventions.yml @@ -9,6 +9,7 @@ conventions: - path: ../conventions/editorconfig-ps1 - path: ../conventions/editorconfig-yaml - path: ../conventions/editorconfig-csharp + - path: ../conventions/gitattributes-csharp - path: ../conventions/faithlife-license-mit - path: ../conventions/dotnet-sdk-10 - path: ./conventions/update-editorconfig-csharp From 528e536b78df2028392ea72a91422880aa224af5 Mon Sep 17 00:00:00 2001 From: Ed Ball Date: Mon, 18 May 2026 12:23:57 -0700 Subject: [PATCH 4/6] Update C# editorconfig settings --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index f65f303..9d84cc1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -143,6 +143,7 @@ dotnet_diagnostic.CA1819.severity = suggestion dotnet_diagnostic.CA1822.severity = suggestion dotnet_diagnostic.CA1826.severity = suggestion dotnet_diagnostic.CA1848.severity = suggestion +dotnet_diagnostic.CA1861.severity = suggestion dotnet_diagnostic.CA1873.severity = suggestion dotnet_diagnostic.CA2000.severity = none dotnet_diagnostic.CA2227.severity = none From 6f6e986599b2b354e1695a48570b6ba49931f05b Mon Sep 17 00:00:00 2001 From: Ed Ball Date: Mon, 18 May 2026 12:23:59 -0700 Subject: [PATCH 5/6] Update C# gitattributes settings --- .gitattributes | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitattributes b/.gitattributes index 23e9308..3e9060c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,8 @@ apm.lock.yaml linguist-generated=true .github/instructions/** linguist-generated=true .github/prompts/** linguist-generated=true # END DO NOT EDIT + +# DO NOT EDIT: csharp convention +# generated from https://github.com/Faithlife/CodingGuidelines/blob/master/sections/csharp/gitattributes.md +*.cs text diff=csharp +# END DO NOT EDIT From 8e113486349a1906308d8122fc00719833a9088c Mon Sep 17 00:00:00 2001 From: Ed Ball Date: Sat, 16 May 2026 15:27:16 -0700 Subject: [PATCH 6/6] Don't fail fast --- conventions/faithlife-dotnet-library-workflow/files/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/conventions/faithlife-dotnet-library-workflow/files/ci.yml b/conventions/faithlife-dotnet-library-workflow/files/ci.yml index c28c02c..c89d356 100644 --- a/conventions/faithlife-dotnet-library-workflow/files/ci.yml +++ b/conventions/faithlife-dotnet-library-workflow/files/ci.yml @@ -25,6 +25,7 @@ jobs: name: Build ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: