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: 19 additions & 48 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ permissions:
contents: read

env:
NUGET_PACKAGES: ${{ github.event_name == 'pull_request' && format('{0}/.nuget/packages', github.workspace) || '' }}
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages

jobs:
validation:
Expand All @@ -36,16 +36,16 @@ jobs:
permissions:
contents: read
with:
runner-labels: ${{ github.event_name == 'pull_request' && '["self-hosted","Windows","X64","smartpipe-cleanup-v1"]' || '["ubuntu-latest"]' }}
runner-labels: ${{ github.event_name == 'pull_request' && '["windows-latest"]' || '["ubuntu-latest"]' }}

hosting-integration:
name: Hosting integration (${{ matrix.os == 'self-hosted' && 'Windows' || matrix.os }})
name: Hosting integration (${{ matrix.os == 'windows-latest' && 'Windows' || matrix.os }})
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'workflow_dispatch' || (inputs.diagnostic-sha == '' && inputs.diagnostic-scenario == '' && inputs.diagnostic-repeat == ''))
runs-on: ${{ matrix.os == 'self-hosted' && fromJSON('["self-hosted","Windows","X64","smartpipe-cleanup-v1"]') || matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix: ${{ fromJSON(github.event_name == 'pull_request' && '{"os":["self-hosted"]}' || '{"os":["ubuntu-latest","windows-latest"]}') }}
matrix: ${{ fromJSON(github.event_name == 'pull_request' && '{"os":["windows-latest"]}' || '{"os":["ubuntu-latest","windows-latest"]}') }}

steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand All @@ -56,9 +56,11 @@ jobs:
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'

- name: Restore Hosting integration tests
run: dotnet restore tests/SmartPipe.Extensions.Hosting.Tests/SmartPipe.Extensions.Hosting.Tests.csproj --locked-mode
run: dotnet restore tests/SmartPipe.Extensions.Hosting.Tests/SmartPipe.Extensions.Hosting.Tests.csproj --locked-mode -p:DisableImplicitLibraryPacksFolder=true

- name: Build Hosting integration tests
run: dotnet build tests/SmartPipe.Extensions.Hosting.Tests/SmartPipe.Extensions.Hosting.Tests.csproj --configuration Release --no-restore -warnaserror
Expand All @@ -68,7 +70,7 @@ jobs:

json-file-windows:
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'workflow_dispatch' || (inputs.diagnostic-sha == '' && inputs.diagnostic-scenario == '' && inputs.diagnostic-repeat == ''))
runs-on: ${{ github.event_name == 'pull_request' && fromJSON('["self-hosted","Windows","X64","smartpipe-cleanup-v1"]') || 'windows-latest' }}
runs-on: windows-latest
timeout-minutes: 20

steps:
Expand All @@ -80,9 +82,11 @@ jobs:
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'

- name: Restore locked
run: dotnet restore SmartPipe.Core.slnx --locked-mode
run: dotnet restore SmartPipe.Core.slnx --locked-mode -p:DisableImplicitLibraryPacksFolder=true

- name: Build JSON test project
run: dotnet build tests/SmartPipe.Extensions.Json.Tests/SmartPipe.Extensions.Json.Tests.csproj --configuration Release --no-restore -warnaserror
Expand Down Expand Up @@ -124,7 +128,7 @@ jobs:
baseline-contract-windows:
name: Baseline contract (Windows)
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'workflow_dispatch' || (inputs.diagnostic-sha == '' && inputs.diagnostic-scenario == '' && inputs.diagnostic-repeat == ''))
runs-on: ${{ github.event_name == 'pull_request' && fromJSON('["self-hosted","Windows","X64","smartpipe-cleanup-v1"]') || 'windows-latest' }}
runs-on: windows-latest
timeout-minutes: 20

steps:
Expand All @@ -137,6 +141,8 @@ jobs:
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'

- name: Restore locked
run: dotnet restore SmartPipe.Core.slnx --locked-mode -p:DisableImplicitLibraryPacksFolder=true
Expand All @@ -156,7 +162,7 @@ jobs:
diagnostic-consumer:
name: Diagnostic consumer (${{ inputs.diagnostic-scenario }})
if: github.event_name == 'workflow_dispatch' && (inputs.diagnostic-sha != '' || inputs.diagnostic-scenario != '' || inputs.diagnostic-repeat != '')
runs-on: [self-hosted, Windows, X64, smartpipe-cleanup-v1]
runs-on: windows-latest
timeout-minutes: 45
steps:
- name: Validate diagnostic inputs
Expand Down Expand Up @@ -190,9 +196,11 @@ jobs:
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'

- name: Restore locked
run: dotnet restore SmartPipe.Core.slnx --locked-mode
run: dotnet restore SmartPipe.Core.slnx --locked-mode -p:DisableImplicitLibraryPacksFolder=true

- name: Build
run: dotnet build SmartPipe.Core.slnx --configuration Release --no-restore -warnaserror
Expand Down Expand Up @@ -235,40 +243,3 @@ jobs:
if ($summaryText.Length -gt 8192) { $summaryText = $summaryText.Substring(0, 8192) + [Environment]::NewLine + '... summary truncated ...' }
Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY -Value $summaryText
if ($failed) { exit 1 }

cleanup-self-hosted:
name: Cleanup self-hosted workspace
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
needs: [validation, hosting-integration, json-file-windows, baseline-contract-windows]
runs-on: [self-hosted, Windows, X64, smartpipe-cleanup-v1]
steps:
- name: Cleanup generated outputs
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($env:GITHUB_WORKSPACE)) { throw 'GITHUB_WORKSPACE is required.' }
$workspace = [IO.Path]::GetFullPath($env:GITHUB_WORKSPACE).TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar)
if ((Get-Item -LiteralPath $workspace -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Workspace is a reparse point.' }
$prefix = "$workspace$([IO.Path]::DirectorySeparatorChar)"
$targets = [Collections.Generic.List[string]]::new()
$targets.Add((Join-Path $workspace 'artifacts'))
$targets.Add((Join-Path $workspace 'BenchmarkDotNet.Artifacts'))
$targets.Add((Join-Path $workspace '.nuget'))
$pending = [Collections.Generic.Stack[string]]::new()
$pending.Push($workspace)
while ($pending.Count) {
foreach ($directory in Get-ChildItem -LiteralPath $pending.Pop() -Force -Directory) {
if ($directory.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue }
if ($directory.Name -in 'bin', 'obj') { $targets.Add($directory.FullName) }
else { $pending.Push($directory.FullName) }
}
}
foreach ($target in $targets | Sort-Object Length -Descending -Unique) {
$fullPath = [IO.Path]::GetFullPath($target)
if (!$fullPath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { throw "Outside workspace: $fullPath" }
if (Test-Path -LiteralPath $fullPath -PathType Container) {
if ((Get-Item -LiteralPath $fullPath -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "Reparse point: $fullPath" }
if (Get-ChildItem -LiteralPath $fullPath -Force -Recurse | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }) { throw "Reparse point: $fullPath" }
Remove-Item -LiteralPath $fullPath -Recurse -Force
}
}
34 changes: 21 additions & 13 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Hosted .NET static analysis
name: CodeQL

on:
push:
Expand All @@ -10,10 +10,14 @@ on:

permissions:
contents: read
security-events: write

env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages

jobs:
analyze:
name: Hosted .NET static analysis
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand All @@ -24,15 +28,19 @@ jobs:
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'

- name: Initialize CodeQL
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
languages: csharp

- name: Restore locked
shell: pwsh
run: |
dotnet restore SmartPipe.Core.slnx --locked-mode
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Build static analysis
shell: pwsh
run: |
dotnet build SmartPipe.Core.slnx --configuration Release --no-restore -warnaserror
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Build
run: dotnet build SmartPipe.Core.slnx -c Release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
ram: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && '16384' || '' }}
threads: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && '2' || '' }}
48 changes: 5 additions & 43 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -1,58 +1,20 @@
name: Repository security audit
name: Dependency Review

on:
pull_request:
branches: [ main, release/2.2.0, sp220/checkpoint-c, sp220/checkpoint-d ]

permissions:
contents: read
pull-requests: read

jobs:
repository-security-audit:
name: Repository security audit
if: github.event.pull_request.head.repo.full_name == github.repository
dependency-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
global-json-file: global.json

- name: Restore locked
shell: pwsh
run: |
dotnet restore SmartPipe.Core.slnx --locked-mode
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Build repository checks
shell: pwsh
run: |
dotnet build eng/SmartPipe.RepositoryChecks/SmartPipe.RepositoryChecks.csproj --configuration Release --no-restore -warnaserror
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Verify repository package contracts
shell: pwsh
run: |
dotnet run --project eng/SmartPipe.RepositoryChecks/SmartPipe.RepositoryChecks.csproj --configuration Release --no-build --no-restore -- verify --profile sp220-05 --format github --failures-only
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Vulnerable package scan
shell: pwsh
run: |
New-Item -ItemType Directory -Path artifacts/audit -Force | Out-Null
dotnet package list --project SmartPipe.Core.slnx --vulnerable --include-transitive --format json --output-version 1 --no-restore > artifacts/audit/vulnerable.json
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Verify direct production audit policy
shell: pwsh
run: dotnet run --project eng/SmartPipe.RepositoryChecks/SmartPipe.RepositoryChecks.csproj --configuration Release --no-build --no-restore -- verify-nuget-audit --repo-root . --report artifacts/audit/vulnerable.json

- name: Deprecated package scan
shell: pwsh
run: |
dotnet package list --project SmartPipe.Core.slnx --deprecated --include-transitive --format json --output-version 1 --no-restore > artifacts/audit/deprecated.json
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Dependency review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
11 changes: 8 additions & 3 deletions .github/workflows/reusable-release-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ permissions:
contents: read

env:
NUGET_PACKAGES: ${{ github.event_name == 'pull_request' && format('{0}/.nuget/packages', github.workspace) || '' }}
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages

jobs:
build-test-pack:
Expand All @@ -40,9 +40,11 @@ jobs:
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'

- name: Restore locked
run: dotnet restore SmartPipe.Core.slnx --locked-mode
run: dotnet restore SmartPipe.Core.slnx --locked-mode -p:DisableImplicitLibraryPacksFolder=true

- name: Test release version validation
shell: pwsh
Expand Down Expand Up @@ -264,6 +266,7 @@ jobs:
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ inputs.artifact-name }}
retention-days: ${{ inputs.artifact-name == 'packages' && 7 || 90 }}
path: |
artifacts/packages
artifacts/consumers/**/result.json
Expand All @@ -284,9 +287,11 @@ jobs:
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'

- name: Restore locked
run: dotnet restore SmartPipe.Core.slnx --locked-mode
run: dotnet restore SmartPipe.Core.slnx --locked-mode -p:DisableImplicitLibraryPacksFolder=true

- name: Build concurrency projects
shell: pwsh
Expand Down
75 changes: 10 additions & 65 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,68 +80,18 @@ unbounded-memory symptom in progress notes.
README examples are intentionally minimal. CI consumer smoke is the executable
check for the public quick-start scenarios.

## Dedicated Windows runner operations
## Hosted CI operations

The same-repository Windows jobs use the exact labels
`self-hosted`, `Windows`, `X64`, and `smartpipe-cleanup-v1`. The installation
root is deliberately fixed at `C:\SmartPipe-Runner`; do not point the hook at a
developer checkout, `_tool`, the runner binaries, or a shared temporary root.
GitHub-hosted runners provide the CI environment. Same-repository pull requests
run validation and Windows-specific lanes on `windows-latest`; push and manual
dispatch runs keep the Hosting integration Linux and Windows matrix. CodeQL and
Dependency Review use their official public-repository workflows.

Install or remove the repository-owned hook only while the runner is idle:

```powershell
gh auth status
pwsh -NoProfile -File eng\runner\install-runner.ps1
pwsh -NoProfile -File eng\runner\uninstall-runner.ps1
```

The scripts resolve the exact runner name from `.runner` (`agentName`); an
optional `-RunnerName` is accepted only when it exactly matches that value.
They fail closed for missing or ambiguous configuration. The installer checks
the repository, queued/in-progress Actions runs, and remote runner state before
mutation. It writes only the hook's `.env` entry, copies the hook plus its
safety helper into the runner's `hooks` directory, registers exactly
`smartpipe-cleanup-v1` through the GitHub runner-label API while preserving
other labels, stops listeners tied to the exact root, launches one hidden
`run.cmd`, and waits for exactly one online, idle listener. Uninstall removes
only that custom label and the owned entry/copies, preserves unrelated labels
and `.env` lines, then performs the same bounded one-listener restart. A failed
operation reports recovery guidance; never convert the runner to a service as
part of this operation. The second owned `.env` entry points
`DOTNET_INSTALL_DIR` at `_work\_tool\dotnet`, giving `actions/setup-dotnet` a
writable persistent directory without granting access to
`C:\Program Files\dotnet`.
The hook entry is `ACTIONS_RUNNER_HOOK_JOB_STARTED`; upgrades remove the legacy
`ACTIONS_RUNNER_HOOK_JOB_COMPLETED` entry and hook copy before writing the new
owned state. Before any file, label, stop, or restart mutation, every
`Runner.Listener.exe` must be classifiable to this exact root. Missing or
unreadable process metadata and listeners belonging to another root fail closed
with their PIDs; ambiguous listeners are never stopped automatically.

The job-start hook accepts only `MrFr3di/SmartPipe-Core`, verifies the checkout
remote, and canonicalizes every target beneath the dedicated runner root. It
runs before the next job starts, after the runner has completed the previous
job's process cleanup, removes the exact prior checkout, and recreates its
empty workspace directory before the next checkout. It also removes the known
`SmartPipe.Core`, `SmartPipe-Core`, `CodeQL`, and `codeql` directories below
`RUNNER_TEMP`. Missing temp targets are successful. Any outside path, broad
root, reparse point, unsafe repository, non-empty recreation, or deletion error
fails closed before removal. An existing empty workspace is accepted
idempotently; any non-empty workspace must pass the exact repository/origin
authorization before removal. The existing workflow cleanup jobs remain as
defense in depth.

For a compact, transition-only pull-request view:

```powershell
pwsh -NoProfile -File eng\runner\monitor-pr.ps1 -PullRequest 123 -MaxPolls 120
```

The monitor uses `gh pr view`, prints only a changed head/state/merge/check
summary, and stops at `MERGED`, `CLOSED`, or the poll bound. For each newly
failed head it retrieves one failed-run log, prints a bounded first-causal
slice, and removes its task-specific temporary log directory on exit. `-Once`
is useful for a single snapshot. It does not upload logs or alter GitHub state.
Restore-heavy jobs set `NUGET_PACKAGES` below `GITHUB_WORKSPACE` and use the
built-in `actions/setup-dotnet` cache keyed by `**/packages.lock.json`. Build
outputs, credentials, and secrets are never cached. Generic CI package
artifacts are retained for seven days; versioned release artifacts retain the
repository's normal release retention.

The optional diagnostic dispatch runs one exact commit and one internal
consumer scenario without changing normal push or pull-request behavior:
Expand All @@ -158,8 +108,3 @@ lowercase letters, digits, and hyphens, and repeat must be `1` through `5`.
The job restores, builds, and packs once, then reports bounded run snippets in
the step summary without artifacts. Normal jobs run when all three inputs are
empty.

If rollout must be reverted, stop the idle listener, run the uninstaller,
restart the listener, and revert the workflow change with a normal commit.
Do not delete the runner root or use `git clean`; safe cleanup is intentionally
recoverable and scoped to the exact approved boundaries.
Loading
Loading