Pipelines para .NET

2024-04-29 · 27 min read

Read in english

Pipelines

As esteiras automatizadas, também conhecidas como pipelines, são seqüências de comandos executados para garantir a integridade do seu código e para gerar um artefato final, como um programa executável ou uma biblioteca.

Ter uma pipeline significa ter um processo consistente que minimiza o risco de erros humanos no produto final e economiza tempo do programador, pois ele pode se ocupar de outras tarefas enquanto o código é compilado, verificado e empacotado.

Esteira de produção

Etapas

flowchart TD checkout --> clean --> restore --> audit --> sbom --> build --> unittests[unit tests] unittests --se for programa--> publish --com docker--> dockerk8s[docker / kubernetes] publish --sem docker--> zipinstaller[zip / instalador] unittests --se for pacote nuget--> pack --> nugetpush[nuget push]

checkout

Obtém o código da branch em questão, no Git. Se for um processo de integração (CI), pega o código da branch que pretende ser mergeada; se for um deploy (CD), pega o código da branch de publicação, como develop, master ou release_candidate.

Linha de comando: git clone

clean

Limpa as pastas bin e obj, para garantir que tudo vai começar do zero, sem interferências de compilações anteriores.

Linha de comando: dotnet clean

restore

Garante que as referências entre os projetos na solução estão corretas e faz o download dos pacotes NuGet necessários.

Linha de comando: dotnet restore

audit

Verifica se há algum pacote NuGet do projeto com problemas de segurança, conferindo nas listas de vulnerabilidades CVE (Common Vulnerabilities and Exposures) e GHSA (GitHub Advisory Database).

Linha de comando: dotnet list package --vulnerable --include-transitive

sbom

O SBOM, em inglês, software bill of materials, é um documento que informa quais componentes foram utilizados para a produção de um programa ou biblioteca.

Esse documento é de suma importância para softwares críticos, pois através dele, as organizações podem facilmente saber quais de suas aplicações estão em perigo quando uma vulnerabilidade em uma biblioteca é reportada. Após o ciberataque ao governo dos EUA em 2020, os SBOMs tornaram-se uma prática endossada pela Casa Branca.

Recomendo o formato CycloneDX, por ser mais sucinto e de leitura mais fácil.

Linha de comando:

  • Em formato CycloneDX: dotnet CycloneDX
  • Em formato SPDX: sbom-tool generate

build

Compila o código da solução.

Linha de comando: dotnet build

unit tests

Roda os testes unitários da solução para assegurar que estão passando.

Nessa etapa, podemos produzir um relatório que exibe o nível de cobertura dos testes unitários em relação ao código, mostrando quais classes, métodos e linhas foram abrangidos pelos testes. O ReportGenerator é a principal ferramenta para esses relatórios em projetos .NET.

Linha de comando:

  • Testes unitários: dotnet test
  • Relatório de cobertura: reportgenerator

publish

Gera o programa final, voltado para execução. Essa etapa difere do build porque nesta, pode-se especificar opções de compilação, como o runtime de destino, se é self-contained, se é single-file, entre outras.

Linha de comando: dotnet publish

pack

Produz um pacote NuGet, no caso de um código que é feito para ser uma biblioteca.

Linha de comando: dotnet pack

nuget push

Sobe o pacote para um servidor NuGet, privado ou público, para que possa ser utilizado por outras pessoas.

Linha de comando: dotnet nuget push

Motores de pipeline

Existem vários motores de pipeline disponíveis, como o GitHub Actions, GitLab CI, Jenkins, Azure Pipelines, CircleCI e diversos outros.

Além desses, você pode ter sua pipeline como um script para rodar localmente na sua máquina. Essa é uma boa prática por ser uma salvaguarda caso sua pipeline remota esteja fora do ar e porque permite testar modificações antes de commitá-las.

Recomendo pessoalmente usar scripts PowerShell para pipelines locais, pois é uma linguagem multiplataforma e amigável, com fácil interação com XML e JSON. Contudo, você pode usar outras linguagens de script, como Batch, Shell, Python e outras que você preferir.

Exemplo em GitHub Actions para programa .NET

name: Publicar console / API / programa desktop

on:
  workflow_dispatch: # acionamento manual
    inputs:
      version:
        required: true
        type: string
      rid:
        required: true
        default: linux-x64 # onde o programa vai rodar
        type: string
        # https://learn.microsoft.com/en-us/dotnet/core/rid-catalog

jobs:
  gerar_programa:

    runs-on: ubuntu-latest

    env:
      OUTPUT_FOLDER: ${{ format('./out/{0}/', inputs.rid) }}
      VERSION_NAME: ${{ inputs.version }}
      RID: ${{ inputs.rid }}

    steps:
    - name: Checkout
      uses: actions/checkout@v4
      with:
        fetch-depth: 1

    - name: Instalar .NET SDK
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: 8.x # versão do .NET aqui

    - name: Instalar CycloneDX .NET
      run: dotnet tool install --global CycloneDX

    - name: Limpar solução
      run: dotnet clean --nologo --verbosity quiet
    
    - name: Restaurar solução
      run: dotnet restore --nologo --verbosity quiet

    - name: Auditar solução
      shell: pwsh
      run: |
        $projectPath = "./src/MeuProjeto.Console/MeuProjeto.Console.csproj"
        $jsonObj = (dotnet list $projectPath package --vulnerable --include-transitive --format json) | ConvertFrom-Json;
        $hasAnyVulnerability = ($jsonObj.projects[0].frameworks -ne $null);
        if ($hasAnyVulnerability) {
          dotnet list package --vulnerable --include-transitive;
          exit 1;
        }

    - name: Compilar solução
      run: dotnet build --no-restore --configuration Release --nologo --verbosity quiet

    - name: Rodar testes unitários
      run: dotnet test --no-build --configuration Release --nologo --verbosity quiet --collect:"XPlat Code Coverage" --results-directory ./TestResults/

    - name: Relatório de cobertura de testes unitários
      uses: danielpalme/ReportGenerator-GitHub-Action@5.2.4
      with:
        reports: TestResults/**/coverage.cobertura.xml
        targetdir: TestResults
        reporttypes: JsonSummary;Html

    - name: Gerar SBOM
      shell: pwsh
      run: dotnet CycloneDX ./src/MeuProjeto.Console/MeuProjeto.Console.csproj -o $env:OUTPUT_FOLDER -f sbom.json -sv $env:VERSION_NAME --json

    - name: Publicar programa
      shell: pwsh
      run: |
        dotnet publish ./src/MeuProjeto.Console/MeuProjeto.Console.csproj `
        --verbosity quiet `
        --nologo `
        --configuration Release `
        -p:PublishSingleFile=true `
        -p:Version=${env:VERSION_NAME} `
        --self-contained true `
        --runtime ${env:RID} `
        --output ${env:OUTPUT_FOLDER};

    - name: Setar atributos de execução (UNIX apenas)
      if: ${{ startsWith(inputs.rid, 'linux') || startsWith(inputs.rid, 'osx') }}
      shell: pwsh
      run: chmod +x "${env:OUTPUT_FOLDER}/MeuProjeto.Console"

    - name: Empacotar programa
      shell: pwsh
      run: |
        $zipName = "MeuProjeto.Console_${env:VERSION_NAME}_${env:RID}.zip";
        # se for Linux ou MacOSX, devemos usar o zip ao invés do Compress-Archive,
        # para preservar os atributos de arquivos do Unix.
        if ($IsWindows) {
          Compress-Archive -CompressionLevel Optimal -Path $env:OUTPUT_FOLDER -DestinationPath "./out/${zipName}"
        } else {
          cd $env:OUTPUT_FOLDER
          zip -9 -r ../${zipName} *
          cd ../..
        }
        Remove-Item $env:OUTPUT_FOLDER -Force -Recurse -ErrorAction Ignore
        echo "OUTPUT_FILE_NAME=${zipName}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append

    - name: Subir programa para resultados do workflow
      uses: actions/upload-artifact@v4
      with:
        compression-level: 0 # não precisa comprimir pq passo anterior já comprime
        name: ${{ env.OUTPUT_FILE_NAME }}
        path: ${{ format('./out/{0}', env.OUTPUT_FILE_NAME) }}
    
    - name: Subir SBOM para resultados do workflow
      uses: actions/upload-artifact@v4
      with:
        name: sbom.json
        path: ./out/sbom.json
    
    - name: Subir relatório de cobertura para resultados do workflow
      uses: actions/upload-artifact@v4
      with:
        name: relatorio_de_cobertura
        path: TestResults
    
    # outras etapas subseqüentes podem ser adicionadas aqui,
    # como docker e kubernetes,
    # ou geração de instalador, no caso de programas desktop.

Exemplo em GitHub Actions para pacote NuGet

name: Publicar pacote NuGet

on:
  workflow_dispatch: # acionamento manual

jobs:
  gerar_pacote_nuget:

    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v4
      with:
        fetch-depth: 1

    - name: Instalar .NET SDK
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: 8.x # versão do .NET aqui

    - name: Instalar CycloneDX .NET
      run: dotnet tool install --global CycloneDX

    - name: Limpar solução
      run: dotnet clean --nologo --verbosity quiet
    
    - name: Restaurar solução
      run: dotnet restore --nologo --verbosity quiet

    - name: Auditar solução
      shell: pwsh
      run: |
        $projectPath = "./src/MeuProjeto.Console/MeuProjeto.Console.csproj"
        $jsonObj = (dotnet list $projectPath package --vulnerable --include-transitive --format json) | ConvertFrom-Json;
        $hasAnyVulnerability = ($jsonObj.projects[0].frameworks -ne $null);
        if ($hasAnyVulnerability) {
          dotnet list package --vulnerable --include-transitive;
          exit 1;
        }

    - name: Compilar solução
      run: dotnet build --no-restore --configuration Release --nologo --verbosity quiet

    - name: Rodar testes unitários
      run: dotnet test --no-build --configuration Release --nologo --verbosity quiet --collect:"XPlat Code Coverage" --results-directory ./TestResults/

    - name: Relatório de cobertura de testes unitários
      uses: danielpalme/ReportGenerator-GitHub-Action@5.2.4
      with:
        reports: TestResults/**/coverage.cobertura.xml
        targetdir: TestResults
        reporttypes: JsonSummary;Html
    
    - name: Ler versão do pacote
      shell: pwsh
      run: |
        # o PackageVersion deve estar declarado no .csproj
        ([XML]$nugetCsprojXml = Get-Content ./src/MeuProjeto.Biblioteca/MeuProjeto.Biblioteca.csproj)
        $versionName = $nugetCsprojXml.Project.PropertyGroup.PackageVersion
        # adiciona às variáveis de ambiente do workflow
        echo "VERSION_NAME=${versionName}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append

    - name: Gerar SBOM
      shell: pwsh
      run: dotnet CycloneDX ./src/MeuProjeto.Biblioteca/MeuProjeto.Biblioteca.csproj -o ./out/ -f sbom_meuprojeto_biblioteca.json -sv $env:VERSION_NAME --json

    - name: Gerar pacote
      run: dotnet pack ./src/MeuProjeto.Biblioteca/MeuProjeto.Biblioteca.csproj --nologo --verbosity quiet --configuration Release
    
    - name: Subir pacote para servidor NuGet
      shell: pwsh
      run: |
        $filePath = "./src/MeuProjeto.Biblioteca/bin/Release/MeuProjeto.Biblioteca.${env:VERSION_NAME}.nupkg"
        dotnet nuget push $filePath --api-key $env:NUGET_API_KEY --source https://api.nuget.org/v3/index.json
        # se for um servidor NuGet privado, usar outro source.
        # portal web para testes do NuGet: https://int.nugettest.org
        # source para testes: https://apiint.nugettest.org/v3/index.json
      env:
        NUGET_API_KEY: ${{ secrets.MINHA_NUGET_API_KEY }}

    - name: Subir pacote para resultados do workflow
      uses: actions/upload-artifact@v4
      with:
        compression-level: 0 # não precisa comprimir pq .nupkg já é um zip
        name: ${{ format('MeuProjeto.Biblioteca.{0}.nupkg', env.VERSION_NAME) }}
        path: ${{ format('./src/MeuProjeto.Biblioteca/bin/Release/MeuProjeto.Biblioteca.{0}.nupkg', env.VERSION_NAME) }}

    - name: Subir SBOM para resultados do workflow
      uses: actions/upload-artifact@v4
      with:
        name: sbom_meuprojeto_biblioteca.json
        path: ./out/sbom_meuprojeto_biblioteca.json
    
    - name: Subir relatório de cobertura para resultados do workflow
      uses: actions/upload-artifact@v4
      with:
        name: relatorio_de_cobertura
        path: TestResults

Fonte da imagem

https://dyno.co.nz/products/telescopic-and-expandable-conveyors/telescopic-conveyor/

A
AlexandreHTRB

Campinas/SP,
Brasil