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
80 changes: 80 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
version: 2

updates:
# ---------------------------------------------------------------------------
# NuGet packages (src + tests)
#
# Minor and patch bumps are grouped into a single PR so the auto-merge
# workflow has one unambiguous update-type to act on. Major bumps are
# deliberately left OUT of the group, so each arrives as its own PR and
# stays open for manual review.
# ---------------------------------------------------------------------------
- package-ecosystem: nuget
directory: "/"
schedule:
interval: weekly
day: monday
time: "06:00"
timezone: Etc/UTC
open-pull-requests-limit: 10
commit-message:
prefix: "chore(deps)"
labels:
- dependencies
- nuget
# -------------------------------------------------------------------------
# REPO-SPECIFIC. STANDARD.md 4.10 defines this list as exactly the packages
# *this* repo floors per target framework, and those differ per repo — an
# ignore entry for a package the tree does not reference asserts a dependency
# that is not there. This repo floors exactly one, in Directory.Packages.props.
#
# It carries a deliberate per-TFM floor: a net8.0 consumer must stay on its own
# 8.0.x servicing line, so an 8.x -> 10.x major PR is never mergeable here and
# would just be weekly noise.
#
# Two caveats, both inherent to Dependabot rather than to this repo:
# 1. `ignore` matches by dependency NAME and cannot be scoped to a single
# target framework. The package is referenced under BOTH the net8.0 and
# net10.0 ItemGroups, so this also suppresses a future net10 major
# (10.x -> 11.x). Bump it by hand when a new .NET major lands.
# 2. `ignore` conditions filter SECURITY updates as well as version
# updates, so a major-version security fix would also be suppressed. Low
# risk in practice (a CVE fix for 8.0.x ships as 8.0.y, a patch), but
# worth knowing.
# -------------------------------------------------------------------------
ignore:
- dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions
update-types:
- version-update:semver-major
groups:
nuget-minor-patch:
patterns:
- "*"
update-types:
- minor
- patch

# ---------------------------------------------------------------------------
# GitHub Actions used by ci.yml (checkout, setup-dotnet, upload/download
# artifact, NuGet/login). Same grouping rule as NuGet.
# ---------------------------------------------------------------------------
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
day: monday
time: "06:00"
timezone: Etc/UTC
open-pull-requests-limit: 10
commit-message:
prefix: "chore(actions)"
labels:
- dependencies
- github-actions
groups:
actions-minor-patch:
patterns:
- "*"
update-types:
- minor
- patch
158 changes: 125 additions & 33 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,93 +1,185 @@
# CI for NextIteration.SpectreConsole.Settings.
# Canonical shape defined in NextIteration.Standards STANDARD.md section 3 — change it
# there first, then here.
#
# The single required status check is `ci`, the aggregating gate below. `build` and `test`
# must NOT be required directly: `test` is a matrix, so its check names carry the matrix
# values and change whenever the matrix does. The gate's name is stable.
#
# The test matrix runs all three platforms (STANDARD.md 3.1.1). This library has no
# OS-native backend, but it is a filesystem library: `AtomicFile` needs a different
# replace primitive on Windows (`ReplaceFile`) than on POSIX (`rename(2)`), and path
# handling, file locking and case sensitivity all differ. No EXCEPTIONS.md entry applies
# to this repo.
#
# `release` is a fifth, tag-gated job beyond STANDARD.md 3.1's four. It cuts the GitHub
# release from CHANGELOG.md after `publish`, and cannot run on a pull request.
name: CI

on:
push:
branches: [ main ]
tags:
- 'v*'
tags: [ 'v*' ]
pull_request:
branches: [ main ]

# Superseded pushes are cancelled. Tag builds are never cancelled — a half-cancelled
# release can leave an incomplete package set on nuget.org.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest

timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions/checkout@v7

# The tests multi-target net8.0 and net10.0, so both runtimes must be
# present; the build itself always uses the latest SDK installed here.
- name: Setup .NET
uses: actions/setup-dotnet@v6
# Both SDKs: shipping projects target net8.0 and net10.0 and the tests run
# against BOTH (STANDARD.md 2.3), which needs the 8.0 runtime present.
- uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
10.0.x

- uses: actions/cache@v6
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj') }}
restore-keys: nuget-${{ runner.os }}-

- name: Restore
run: dotnet restore

- name: Build
run: dotnet build --configuration Release --no-restore

- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal

- name: Pack
run: dotnet pack --configuration Release --no-build --output ./artifacts

- name: Upload package artifact
uses: actions/upload-artifact@v7
with:
name: nuget-package
# Capture both .nupkg and .snupkg so the publish job's
# `dotnet nuget push *.nupkg` can also push the matching
# symbol package next to it.
# Both .nupkg and .snupkg, so the publish job's glob also pushes symbols.
path: ./artifacts/*nupkg

test:
strategy:
fail-fast: false # one platform failing must not hide another's result
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
10.0.x

- uses: actions/cache@v6
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj') }}
restore-keys: nuget-${{ runner.os }}-

# Tests run across every shipped TFM (STANDARD.md 2.3). No --no-build: this job
# does not share a filesystem with `build`, and rebuilding is cheaper and less
# fragile than shipping obj/ between jobs.
# `-- --coverage` passes through to Microsoft.Testing.Platform's coverage
# extension (STANDARD.md 2.6). Referencing a collector without invoking it is
# worse than none: it reads as coverage in the dependency list while producing
# no data.
- name: Test
run: dotnet test --configuration Release --verbosity normal -- --coverage

- name: Upload coverage
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage-${{ matrix.os }}
path: '**/TestResults/*.coverage'
if-no-files-found: warn

# THE required status check. Aggregates everything above so the ruleset never has to
# know the matrix shape. `if: always()` is essential — without it the gate is skipped
# when a dependency fails, and a skipped check reads as success to branch protection.
ci:
needs: [ build, test ]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify every required job succeeded
env:
RESULTS: ${{ join(needs.*.result, ',') }}
run: |
echo "upstream results: $RESULTS"
case "$RESULTS" in
*failure*|*cancelled*|*skipped*)
echo "::error title=CI gate::an upstream job did not succeed ($RESULTS)"
exit 1 ;;
esac
echo "all upstream jobs succeeded"

publish:
needs: build
needs: ci
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
timeout-minutes: 15
if: startsWith(github.ref, 'refs/tags/')

permissions:
id-token: write # enable GitHub OIDC token issuance for NuGet Trusted Publishing
id-token: write # GitHub OIDC token issuance for NuGet trusted publishing
contents: read

steps:
- name: Setup .NET 10
uses: actions/setup-dotnet@v6
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'

- name: Download artifact
uses: actions/download-artifact@v8
- uses: actions/download-artifact@v8
with:
name: nuget-package
path: ./artifacts

# Exchange the GitHub OIDC token for a short-lived (1-hour) nuget.org API
# key. Requires a matching Trusted Publishing policy on nuget.org. Run this
# immediately before the push so the temporary key doesn't expire.
- name: NuGet login (OIDC -> temp API key)
# Exchanges the OIDC token for a short-lived (1h) nuget.org key. Requires a
# Trusted Publishing policy on nuget.org bound to this repo + workflow file.
# NUGET_USER is the nuget.org account name, not an email.
- name: NuGet login (OIDC to temporary API key)
uses: NuGet/login@v1
id: login
with:
# nuget.org username (profile name), NOT an email address.
user: ${{ secrets.NUGET_USER }}

- name: Publish to NuGet
run: dotnet nuget push "./artifacts/*.nupkg" --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate

run: >
dotnet nuget push "./artifacts/*.nupkg"
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
--source https://api.nuget.org/v3/index.json
--skip-duplicate

# Beyond STANDARD.md 3.1's canonical four, and downstream of `publish`, so a GitHub
# release is only ever cut for bytes that actually reached nuget.org. Tag-gated, so it
# can never run on a pull request.
release:
needs: publish
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
timeout-minutes: 10
if: startsWith(github.ref, 'refs/tags/')

permissions:
contents: write
contents: write # creating the GitHub release

steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions/checkout@v7

# Pull the section for this tag's version out of CHANGELOG.md
# (e.g. tag v0.1.0 -> the "## [0.1.0] — …" block) for the release body.
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# CodeQL code scanning. See STANDARD.md section 4.4.
name: CodeQL

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
# Weekly, so a newly published query pack finds existing code even when
# nothing has been pushed. Offset off the hour to avoid the scheduling spike.
- cron: '37 4 * * 1'

concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
analyze:
name: analyze
runs-on: ubuntu-latest
timeout-minutes: 30

permissions:
security-events: write # required to upload results
contents: read

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
10.0.x

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: csharp
# security-and-quality is broader than the default security-extended;
# these are small libraries, so the extra findings are affordable.
queries: security-and-quality

# Explicit build rather than autobuild: these repos multi-target, and
# autobuild has picked a single TFM in the past, silently analysing half
# the code. Restore is separate so a restore failure is legible.
- name: Restore
run: dotnet restore

- name: Build
run: dotnet build --configuration Release --no-restore

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:csharp"
Loading