3 minute read

GitHub Actions enable automated workflows for building, testing, and deploying software. This post walks through a sample YAML workflow for publishing NuGet packages, explaining each part to help you implement similar CI/CD pipelines.

Workflow Triggers

The workflow named “publish” activates on:

  • Manual dispatch (workflow_dispatch).
  • Pushes to the master branch.
  • Pull requests to any branch (*).
  • Releases when published.

This ensures builds on code changes, PRs, and releases.

Environment and Defaults

  • Environment variables: Skip .NET first-time experience, suppress logos, set directories for NuGet and coverage.
  • Defaults: Use PowerShell shell for cross-platform consistency.

Jobs Overview

The workflow has four jobs: create_nuget, validate_nuget, run_test, and deploy. They run on Ubuntu-latest for efficiency.

Job: create_nuget

  • Checks out code with full history.
  • Sets up .NET 8.0.x and 9.0.x.
  • Installs MinVer for versioning.
  • Builds and packs two projects: UniversalReportCore and UniversalReportCore.Ui.
  • Uploads NuGet artifacts.

Job: validate_nuget

  • Depends on create_nuget.
  • Downloads artifacts.
  • Installs NuGet validator.
  • Validates each package for compliance.

Job: run_test

  • Checks out code.
  • Sets up .NET.
  • Installs Coverlet for coverage.
  • Runs tests with coverage collection.
  • Generates reports and uploads artifacts.

Job: deploy

  • Runs only on release events.
  • Depends on validation and tests.
  • Downloads artifacts.
  • Publishes packages to NuGet.org using API key, skipping duplicates.

Why This Workflow Matters

  1. Automation: Streamlines building, testing, and deploying, reducing manual errors.
  2. Validation: Ensures packages meet standards before release.
  3. Testing: Integrates coverage to maintain code quality.
  4. Conditional Deployment: Publishes only on releases for controlled rollout.
  5. Efficiency: Parallel jobs speed up processes.
  6. Security: Uses secrets for API keys.

Common Pitfalls and Fixes

  • Missing Dependencies: Ensure .NET versions match project requirements. Fix: Specify exact versions in setup.
  • Artifact Issues: Files not found. Fix: Verify paths in upload/download steps.
  • Secret Mismanagement: Exposed keys. Fix: Use GitHub secrets.
  • Version Conflicts: Inconsistent builds. Fix: Use tools like MinVer.
  • Test Failures: Ignored coverage. Fix: Integrate reporting tools.

Practical Tips for Implementation

  • Modularize Jobs: Break into independent tasks for parallelism.
  • Use Artifacts: Pass data between jobs securely.
  • Add Notifications: Include Slack/email on failures.
  • Monitor Runs: Review logs for optimizations.
  • Learn Actions: Explore marketplace for reusable steps.

Adopting such workflows enhances DevOps practices, ensuring reliable software delivery.

Code Listing

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

name: publish

on:
  workflow_dispatch:
  push:
    branches:
      - 'master'
  pull_request:
    branches:
      - '*'
  release:
    types:
      - published

env:
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
  DOTNET_NOLOGO: true
  NuGetDirectory: $/nuget
  CoverageReport: $/coverage

defaults:
  run:
    shell: pwsh

jobs:
  create_nuget:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0 

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

    - name: Install MinVer
      run: dotnet tool install --global minver-cli

    - name: Run MinVer to determine package version
      run: minver

    # Build & Pack UniversalReportCore
    - name: Build UniversalReportCore
      run: dotnet build UniversalReportCore --configuration Release

    - name: Pack UniversalReportCore
      run: dotnet pack UniversalReportCore --configuration Release --output $

    # Build & Pack UniversalReportCore.Ui
    - name: Build UniversalReportCore.Ui
      run: dotnet build UniversalReportCore.Ui --configuration Release

    - name: Pack UniversalReportCore.Ui
      run: dotnet pack UniversalReportCore.Ui --configuration Release --output $

    # Debug: List NuGet Packages
    - name: Debug - List NuGet Files
      run: ls -la $

    - uses: actions/upload-artifact@v4
      with:
        name: nuget
        if-no-files-found: error
        retention-days: 7
        path: $/*.nupkg

  validate_nuget:
    runs-on: ubuntu-latest
    needs: [ create_nuget ]
    steps:
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: |
            9.0.x
            8.0.x

      - uses: actions/download-artifact@v4
        with:
          name: nuget
          path: $

      - name: Install NuGet validator
        run: dotnet tool update Meziantou.Framework.NuGetPackageValidation.Tool --global

      - name: Validate package
        run: |
          Get-ChildItem "$/*.nupkg" | ForEach-Object { meziantou.validate-nuget-package $_.FullName }

  run_test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

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

    - name: Install Coverlet for Code Coverage
      run: dotnet tool install --global coverlet.console

    - name: Run tests with code coverage
      run: |
        dotnet test UniversalReportCore.Tests --configuration Release --collect:"XPlat Code Coverage" --results-directory $ 

    - name: Convert Coverage to Cobertura Format
      run: |
        dotnet tool install -g dotnet-reportgenerator-globaltool
        reportgenerator -reports:$/**/coverage.cobertura.xml -targetdir:$/html -reporttypes:Cobertura

    - name: Upload Coverage Report
      uses: actions/upload-artifact@v4
      with:
        name: coverage-report
        path: $/html

  deploy:
    if: github.event_name == 'release'
    runs-on: ubuntu-latest
    needs: [ validate_nuget, run_test ]
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: nuget
          path: $

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

      # Find and Push
      - name: Publish NuGet package
        run: |
          foreach($file in (Get-ChildItem "$" -Recurse -Include *.nupkg)) {
              dotnet nuget push $file --api-key "$" --source https://api.nuget.org/v3/index.json --skip-duplicate
          }
        shell: pwsh