diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..4d6f3b5
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,66 @@
+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.
+ #
+ # There is deliberately NO `ignore` block here. STANDARD.md 4.10 requires one
+ # entry per package carrying a per-TFM floor (STANDARD.md 1.4), and this repo
+ # has none: Figgle, Figgle.Fonts and Spectre.Console are all pre-1.0 and
+ # version independently of the .NET runtime, so they take a single common
+ # floor (1.5) and their majors are genuinely reviewable. An `ignore` entry for
+ # a package this repo does not floor per TFM would assert a policy that does
+ # not exist here -- which is why 4.6 is a structural clause and not a
+ # byte-identity one. Add entries here only if a runtime-aligned
+ # `Microsoft.Extensions.*`-style dependency is ever introduced.
+ # ---------------------------------------------------------------------------
+ - 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
+ groups:
+ nuget-minor-patch:
+ patterns:
+ - "*"
+ update-types:
+ - minor
+ - patch
+
+ # ---------------------------------------------------------------------------
+ # GitHub Actions used by ci.yml (checkout, setup-dotnet, cache, upload/download
+ # artifact, NuGet/login) and codeql.yml. 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
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e65602c..6c5d9d9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,42 +1,61 @@
+# CI for NextIteration.SpectreConsole.Splash.
+# Canonical shape defined in NextIteration.Standards STANDARD.md section 3 — change it
+# there first, then here. This file is the template verbatim bar the tag glob; there are
+# no EXCEPTIONS.md entries for this repo.
+#
+# 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). Nothing here is
+# platform-specific — the library writes no files and touches no OS store — but the
+# rendered logo does depend on line-ending handling and on Spectre's console
+# capability detection, and neither is verified by a Linux-only run.
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
-
- permissions:
- contents: read
-
+ timeout-minutes: 15
steps:
- - name: Checkout
- uses: actions/checkout@v7
+ - uses: actions/checkout@v7
- - 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:
- # 8.0.x supplies the runtime the net8.0 test run executes on; the
- # build itself uses the newest installed SDK (10.0.x).
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
@@ -44,43 +63,102 @@ jobs:
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:
- contents: read # actions/checkout — an explicit `permissions` block sets unlisted scopes to `none`
- id-token: write # required for NuGet trusted publishing (OIDC token issuance)
+ id-token: write # GitHub OIDC token issuance for NuGet trusted publishing
+ contents: read
steps:
- - name: Checkout
- uses: actions/checkout@v7
-
- - 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 nuget.org API key.
- # Requires a Trusted Publishing policy configured on nuget.org that
- # matches this repo owner/name and the `ci.yml` workflow file.
- - name: NuGet login (OIDC → temp API key)
- id: nuget-login
+ # 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:
- user: ${{ secrets.NUGET_USER }} # your nuget.org username (profile name), not your email
+ user: ${{ secrets.NUGET_USER }}
- name: Publish to NuGet
- run: dotnet nuget push "./artifacts/*.nupkg" --api-key "${{ steps.nuget-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
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..f7cad91
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -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"
diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml
new file mode 100644
index 0000000..ce7ab4e
--- /dev/null
+++ b/.github/workflows/dependabot-auto-merge.yml
@@ -0,0 +1,82 @@
+name: Dependabot auto-merge
+
+# Auto-merges Dependabot minor and patch bumps once CI passes. Major bumps are
+# left untouched so they stay open for manual review.
+#
+# `on: pull_request` (not pull_request_target) is deliberate: pull_request_target
+# would run with a write token in the base-repo context, which is the classic
+# privilege-escalation footgun. On Dependabot pull_request events the GITHUB_TOKEN
+# is read-only by default, and the `permissions:` block below grants the write
+# scopes back. This is GitHub's documented recipe.
+#
+# ---------------------------------------------------------------------------
+# REQUIRED SETUP: the `AUTO_MERGE_PAT` secret must be stored as a
+# **Dependabot secret**, NOT an Actions secret:
+#
+# Settings -> Secrets and variables -> Dependabot -> New repository secret
+# gh secret set AUTO_MERGE_PAT --app dependabot
+#
+# Workflows triggered by Dependabot events only receive Dependabot secrets;
+# Actions secrets resolve to an empty string. The guard step below fails loudly
+# if that happens rather than letting the approval silently no-op.
+#
+# The PAT must belong to a CODEOWNER (the main ruleset sets
+# require_code_owner_review: true, and a GITHUB_TOKEN/bot approval cannot
+# satisfy a code-owner review). Scope: fine-grained with
+# "Pull requests: read and write" on this repo, or classic `repo`.
+# ---------------------------------------------------------------------------
+on: pull_request
+
+permissions:
+ contents: read
+ pull-requests: read
+
+jobs:
+ auto-merge:
+ runs-on: ubuntu-latest
+ if: github.event.pull_request.user.login == 'dependabot[bot]'
+
+ steps:
+ - name: Verify AUTO_MERGE_PAT is present
+ env:
+ AUTO_MERGE_PAT: ${{ secrets.AUTO_MERGE_PAT }}
+ run: |
+ if [ -z "$AUTO_MERGE_PAT" ]; then
+ echo "::error title=Missing AUTO_MERGE_PAT::Store it as a *Dependabot* secret (gh secret set AUTO_MERGE_PAT --app dependabot). Actions secrets are not available to Dependabot-triggered workflows."
+ exit 1
+ fi
+
+ - name: Fetch Dependabot metadata
+ id: meta
+ uses: dependabot/fetch-metadata@v3
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ # `--auto` does NOT merge immediately: it queues the merge behind the branch
+ # ruleset, so the required check must go green first. That check is `ci` and only
+ # `ci` (STANDARD.md 3.1) — an aggregating gate over `build` and `test`, so the
+ # matrix can be reshaped without touching the ruleset or this comment.
+ # If CI fails, the PR just stays open.
+ #
+ # The approval uses the PAT so it counts as a code-owner review. Because the
+ # ruleset also sets dismiss_stale_reviews_on_push and require_last_push_approval,
+ # a follow-up Dependabot force-push re-triggers this workflow (pull_request
+ # includes `synchronize`) and the PR is re-approved.
+ - name: Approve and enable auto-merge (minor + patch)
+ if: |
+ steps.meta.outputs.update-type == 'version-update:semver-minor' ||
+ steps.meta.outputs.update-type == 'version-update:semver-patch'
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GH_TOKEN: ${{ secrets.AUTO_MERGE_PAT }}
+ run: |
+ gh pr review --approve "$PR_URL"
+ gh pr merge --auto --squash "$PR_URL"
+
+ # No approval, no auto-merge — the PR stays open for a human.
+ - name: Leave major bumps open
+ if: steps.meta.outputs.update-type == 'version-update:semver-major'
+ env:
+ DEPS: ${{ steps.meta.outputs.dependency-names }}
+ run: |
+ echo "::notice title=Major version bump::${DEPS} is a major bump; leaving this PR open for manual review."
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e122e8a..e7491dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- **Canonical CI shape, CodeQL and Dependabot** per
+ [NextIteration.Standards](https://github.com/StuartMeeks/NextIteration.Standards)
+ `STANDARD.md` section 3 and 4. `ci.yml` now splits into `build`, a three-platform
+ `test` matrix, an aggregating `ci` gate and a tag-gated `publish`; the gate is the
+ single required status check, so the matrix can be reshaped without touching branch
+ protection. Adds `codeql.yml` (§4.4), `dependabot.yml` (§4.6) and
+ `dependabot-auto-merge.yml` (§4.7), plus per-workflow `concurrency`,
+ `timeout-minutes` and a NuGet restore cache (§3.5–3.7).
+- **Code coverage is collected** via `Microsoft.Testing.Extensions.CodeCoverage`, the
+ Microsoft.Testing.Platform equivalent of coverlet (§2.6). CI invokes it and uploads
+ the result per platform. Contributor-facing only; the collector is test-only and does
+ not reach the package.
+
### Changed
- **Test suite migrated to xUnit.net v3 (`xunit.v3` 4.0.0)** from `xunit` 2.9.3.
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8d91e1b..266a47c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -12,5 +12,14 @@
+
+
diff --git a/tests/NextIteration.SpectreConsole.Splash.Tests/NextIteration.SpectreConsole.Splash.Tests.csproj b/tests/NextIteration.SpectreConsole.Splash.Tests/NextIteration.SpectreConsole.Splash.Tests.csproj
index 1447a3d..0a1b375 100644
--- a/tests/NextIteration.SpectreConsole.Splash.Tests/NextIteration.SpectreConsole.Splash.Tests.csproj
+++ b/tests/NextIteration.SpectreConsole.Splash.Tests/NextIteration.SpectreConsole.Splash.Tests.csproj
@@ -9,13 +9,29 @@
enable
false
true
-
- $(NoWarn);CA1707
+
+ false
+
+ $(NoWarn);CA1707;CA1515;CA2007
+
+