Skip to content

CI/CD

This guide covers the CI/CD pipeline for the Granit framework: compilation, quality gates, security scanning, static analysis, NuGet packaging, and publication to GitHub Packages, nuget.org, and Cloudflare Pages.

The pipeline runs on:

  • Every pull request targeting develop or main
  • Every push to develop or main
  • Every version tag matching v[0-9]+.[0-9]+.[0-9]+

Concurrent runs on the same ref are cancelled automatically (cancel-in-progress: true).

flowchart TD
    BAT[build-and-test] --> SC[sonarcloud]
    BAT --> PACK[pack]
    PACK --> PGH[publish-github]
    PACK --> PNO[publish-nuget]

    IT[integration-test]
    SD[secret-detection]
    TR[trivy]
    CQL[codeql]
    AUD[audit]
    DOCS[docs]

Most jobs run in parallel. The only sequential chain is: build-and-test → sonarcloud and build-and-test → pack → publish.

Single job combining compile, format check, unit tests, and architecture tests. Avoids the 750 MB upload/download overhead of sharing bin//obj/ artifacts.

StepCommandBlocking
Builddotnet build -c Release -m:4 -p:SkipIntegrationTests=trueYes
Formatdotnet format --verify-no-changes --no-restoreYes
Unit testsdotnet test with OpenCover + Cobertura coverageYes
Architecture testsdotnet test tests/Granit.ArchitectureTests/...Yes

Coverage reports (coverage.opencover.xml) and test results (*.trx) are uploaded as artifacts for 7 days. The sonarcloud job downloads the coverage artifact from here.

Timeout: 15 minutes.

Runs in parallel with build-and-test (no dependency). Spins up a PostgreSQL 18 service container and runs each *.Tests.Integration project sequentially. Marked continue-on-error: true — service container availability is not guaranteed in all runner environments.

Service container:

image: postgres:18
env:
POSTGRES_DB: granit_test
POSTGRES_USER: granit_test
POSTGRES_PASSWORD: test_password

Environment variables exposed to tests: POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD.

Three security jobs run in parallel, independent of all other jobs:

JobToolBlocking
secret-detectionGitleaks 8.30.0 — detects committed secretsYes
trivyAqua Trivy — filesystem scan for HIGH/CRITICAL CVEsYes
codeqlGitHub CodeQL — semantic C# analysisNo (results in Security tab)

Gitleaks performs a full history scan (--log-opts="--all"). Trivy uses a pinned action digest for supply-chain security. CodeQL results appear in the repository Security › Code scanning alerts tab.

Static analysis with coverage integration. Needs build-and-test (downloads the coverage artifact). Excluded paths: .nuget/**, docs-site/**. Coverage exclusions: test projects, *Module.cs, *HostApplicationBuilderExtensions.cs.

Requires Java 17 (Temurin) for the SonarScanner CLI. sonar.qualitygate.wait=true — the job waits for the quality gate result. Marked continue-on-error: true — pipeline is not blocked by SonarCloud availability.

Timeout: 30 minutes.

Skipped on pull requests from forks (no access to SONAR_TOKEN).

Runs dotnet list package --vulnerable --include-transitive. Fails if any vulnerable package is found. The vulnerability report is saved as an artifact for 7 days. Marked continue-on-error: true (advisory).

Needs build-and-test. Runs only on develop, main, or version tags. Rebuilds the solution from the NuGet cache (no artifact sharing).

ContextVersion format
Tag vX.Y.ZX.Y.Z (stable release)
Branch develop or main0.1.0-dev.<run_number> (prerelease)

Packed .nupkg files are uploaded as the nupkgs artifact for 7 days.

Needs pack. Runs only on develop and main (not on tags). Pushes .nupkg files to GitHub Packages using the automatic GITHUB_TOKEN (packages: write permission declared on the job). Uses --skip-duplicate so re-running the pipeline is safe.

Needs pack. Runs only on version tags (refs/tags/v*). Pushes .nupkg files to nuget.org using the NUGET_API_KEY secret and the nuget-publish deployment environment (protection rules apply).

Independent job — no dependency on build-and-test. Runs on develop and main. Builds the Astro Starlight site with pnpm 10 and Node.js 22, then deploys to Cloudflare Pages:

project-name: granit-docs
branch: ${{ github.ref_name }} (develop or main)

Preview deployments on develop, production on main.

Every job uses actions/cache keyed by runner.os and a hash of **/*.csproj + Directory.Packages.props. With a warm cache, NuGet restore completes in 5–10 seconds.

Each job that compiles rebuilds independently — no bin//obj/ artifacts are shared. This avoids the ~750 MB upload/download per run.

Core commands available locally and in CI:

Terminal window
# Compile (skip integration tests — no Docker needed locally)
dotnet build -p:SkipIntegrationTests=true
# Run unit tests with coverage
dotnet test -p:SkipIntegrationTests=true -p:SkipArchitectureTests=true \
--collect:"XPlat Code Coverage"
# Run architecture tests only
dotnet test tests/Granit.ArchitectureTests/Granit.ArchitectureTests.csproj
# Verify formatting
dotnet format --verify-no-changes
# Pack for local feed
dotnet pack -c Release -o ./nupkgs
SecretDescriptionUsed by
NUGET_API_KEYnuget.org API keypublish-nuget (tag builds only)
CLOUDFLARE_API_TOKENCloudflare API tokendocs
CLOUDFLARE_ACCOUNT_IDCloudflare account IDdocs
SecretDescriptionUsed by
SONAR_TOKENSonarCloud authentication tokensonarcloud

GITHUB_TOKEN is provided automatically by GitHub Actions and is used for Gitleaks scanning, GitHub Packages publishing (packages: write), and CodeQL results upload (security-events: write).

Applications that depend on Granit prerelease packages add GitHub Packages as a NuGet source:

<!-- nuget.config -->
<packageSources>
<add key="github-granit"
value="https://nuget.pkg.github.com/granit-fx/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="github-granit">
<package pattern="Granit.*" />
</packageSource>
</packageSourceMapping>

Authentication uses GITHUB_TOKEN in CI. For local development, add credentials in packageSourceCredentials:

<packageSourceCredentials>
<github-granit>
<add key="Username" value="YOUR_GITHUB_USERNAME" />
<add key="ClearTextPassword" value="YOUR_PERSONAL_ACCESS_TOKEN" />
</github-granit>
</packageSourceCredentials>

The personal access token needs the read:packages scope.

Before any pull request is approved, the following gates must pass:

  • dotnet build succeeds
  • dotnet test passes
  • dotnet format --verify-no-changes passes
  • Architecture tests pass
  • No HIGH/CRITICAL vulnerabilities (Trivy)
  • Secret detection scan clean (Gitleaks)
  • Documentation updated (if applicable)

The job has a 15-minute limit. If tests are slow, check for non-parallelized test collections or missing [assembly: CollectionBehavior(MaxParallelThreads = 4)]. Adjust MaxCpuCount in the test step if needed.

Verify that:

  1. The build-and-test job produced **/coverage.opencover.xml artifacts.
  2. The sonarcloud job successfully downloaded the coverage artifact.
  3. sonar.cs.opencover.reportsPaths points to **/coverage.opencover.xml.

A NuGet dependency has a known vulnerability. Check the vulnerability-report artifact. Update the affected package or, if no fix is available, document the risk assessment in the PR description.

Integration tests fail with connection errors

Section titled “Integration tests fail with connection errors”

Verify that:

  1. The PostgreSQL 18 service container passed its health check (check job logs).
  2. Environment variables POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD match the service definition.
  3. The test fixture uses localhost:5432 — service containers map to the runner’s localhost.

Ensure the publish-github job declares permissions: packages: write. For organization repositories, verify that GitHub Actions is allowed to create packages in the organization settings (Settings › Actions › General).

Verify that CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are set in the repository secrets. The token needs the Cloudflare Pages: Edit permission scoped to the granit-docs project.