Compartilhar via


Tutorial: Criar uma ação do GitHub com o .NET

Saiba como criar um GitHub Action com um aplicativo .NET em contêineres. GitHub Actions permite a automação e a composição do fluxo de trabalho. Com o GitHub Actions, você pode criar, testar e implantar seu código-fonte do GitHub. Além disso, as ações mostram a capacidade de interagir de forma programática com problemas, criar solicitações de pull, executar revisões de código e gerenciar branches. Para obter mais informações sobre a integração contínua com GitHub Actions, consulte Compilar e testar o .NET.

Neste tutorial, você aprenderá a:

  • Preparar um aplicativo .NET para GitHub Actions
  • Definir entradas e saídas de ação
  • Criar um fluxo de trabalho

Pré-requisitos

A intenção do aplicativo

Neste tutorial, o aplicativo executa a análise de métrica de código ao:

  • analisar e encontrar arquivos de projeto *.csproj e *.vbproj.

  • analisar o código-fonte encontrado nesses projetos para:

    • Complexidade ciclomática
    • Índice de facilidade de manutenção
    • Profundidade da herança
    • Acoplamento de classes
    • Número de linhas de código-fonte
    • Linhas aproximadas de código executável
  • Criar (ou atualizar) um arquivo CODE_METRICS.md.

O aplicativo não é responsável por criar uma solicitação de pull com as alterações no arquivo CODE_METRICS.md. Essas alterações são gerenciadas como parte da composição do fluxo de trabalho.

Neste tutorial, as referências ao código-fonte têm partes do aplicativo omitidas para mais rapidez. O código de aplicativo completo está disponível no GitHub.

Explorar o aplicativo

O aplicativo de console .NET usa o pacote CommandLineParserNuGet para analisar argumentos no objeto ActionInputs.

using CommandLine;

namespace DotNet.GitHubAction;

public class ActionInputs
{
    string _repositoryName = null!;
    string _branchName = null!;

    public ActionInputs()
    {
        if (Environment.GetEnvironmentVariable("GREETINGS") is { Length: > 0 } greetings)
        {
            Console.WriteLine(greetings);
        }
    }

    [Option('o', "owner",
        Required = true,
        HelpText = "The owner, for example: \"dotnet\". Assign from `github.repository_owner`.")]
    public string Owner { get; set; } = null!;

    [Option('n', "name",
        Required = true,
        HelpText = "The repository name, for example: \"samples\". Assign from `github.repository`.")]
    public string Name
    {
        get => _repositoryName;
        set => ParseAndAssign(value, str => _repositoryName = str);
    }

    [Option('b', "branch",
        Required = true,
        HelpText = "The branch name, for example: \"refs/heads/main\". Assign from `github.ref`.")]
    public string Branch
    {
        get => _branchName;
        set => ParseAndAssign(value, str => _branchName = str);
    }

    [Option('d', "dir",
        Required = true,
        HelpText = "The root directory to start recursive searching from.")]
    public string Directory { get; set; } = null!;

    [Option('w', "workspace",
        Required = true,
        HelpText = "The workspace directory, or repository root directory.")]
    public string WorkspaceDirectory { get; set; } = null!;

    static void ParseAndAssign(string? value, Action<string> assign)
    {
        if (value is { Length: > 0 } && assign is not null)
        {
            assign(value.Split("/")[^1]);
        }
    }
}

A classe de entradas de ação anterior define várias entradas necessárias para que o aplicativo seja executado com êxito. O construtor grava o valor da variável de ambiente "GREETINGS", se estiver disponível no ambiente de execução atual. As propriedades Name e Branch são analisadas e atribuídas a partir do último segmento de uma cadeia de caracteres "/" delimitada.

Com a classe de entradas de ação definida,tenha como foco o arquivo Program.cs.

using System.Text;
using CommandLine;
using DotNet.GitHubAction;
using DotNet.GitHubAction.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using static CommandLine.Parser;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddGitHubActionServices();

using IHost host = builder.Build();

ParserResult<ActionInputs> parser = Default.ParseArguments<ActionInputs>(() => new(), args);
parser.WithNotParsed(
    errors =>
    {
        host.Services
            .GetRequiredService<ILoggerFactory>()
            .CreateLogger("DotNet.GitHubAction.Program")
            .LogError("{Errors}", string.Join(
                Environment.NewLine, errors.Select(error => error.ToString())));

        Environment.Exit(2);
    });

await parser.WithParsedAsync(
    async options => await StartAnalysisAsync(options, host));

await host.RunAsync();

static async ValueTask StartAnalysisAsync(ActionInputs inputs, IHost host)
{
    // Omitted for brevity, here is the pseudo code:
    // - Read projects
    // - Calculate code metric analytics
    // - Write the CODE_METRICS.md file
    // - Set the outputs

    var updatedMetrics = true;
    var title = "Updated 2 projects";
    var summary = "Calculated code metrics on two projects.";

    // Do the work here...

    // Write GitHub Action workflow outputs.
    var gitHubOutputFile = Environment.GetEnvironmentVariable("GITHUB_OUTPUT");
    if (!string.IsNullOrWhiteSpace(gitHubOutputFile))
    {
        using StreamWriter textWriter = new(gitHubOutputFile, true, Encoding.UTF8);
        textWriter.WriteLine($"updated-metrics={updatedMetrics}");
        textWriter.WriteLine($"summary-title={title}");
        textWriter.WriteLine($"summary-details={summary}");
    }

    await ValueTask.CompletedTask;

    Environment.Exit(0);
}

O arquivo Program é simplificado para maior rapidez, para explorar a fonte de exemplo completa, consulte Program.cs. O funcionamento atual mostra um código de texto clichê necessário para:

Referências externas de projeto ou pacote podem ser usadas e registradas com injeção de dependência. O Get<TService> é uma função local estática, que requer a instância IHost e é usada para resolver os serviços necessários. Com o singleton CommandLine.Parser.Default, o aplicativo obtém uma instância parser de args. Quando os argumentos não podem ser analisados, o aplicativo sai com um código de saída diferente de zero. Para obter mais informações, consulte "Como configurar códigos de saída para ações".

Quando os args são analisados com êxito, o aplicativo é chamado corretamente com as entradas necessárias. Nesse caso, uma chamada para a funcionalidade primária StartAnalysisAsync é feita.

Para gravar valores de saída, você deve seguir o formato reconhecido por GitHub Actions: configurando um parâmetro de saída.

Preparar um aplicativo .NET para GitHub Actions

GitHub Actions oferecem suporte a duas variações de desenvolvimento de aplicativos:

  • JavaScript (opcionalmente TypeScript)
  • Contêiner do Docker (qualquer aplicativo executado no Docker)

O ambiente virtual em que o GitHub Action está hospedado pode ou não ter o .NET instalado. Para obter informações sobre o que é pré-instalado no ambiente de destino, consulte Ambientes Virtuais do GitHub Actions. Embora seja possível executar comandos da CLI do .NET dos fluxos de trabalho do GitHub Actions, para um funcionamento mais completo do GitHub Action, baseado em NET, recomendamos que você conteinerize o aplicativo. Para obter mais informações, consulte Containerize um aplicativo .NET.

O Dockerfile

Um Dockerfile é um conjunto de instruções para criar uma imagem. Para aplicativos .NET, o Dockerfile geralmente fica na raiz do diretório ao lado de um arquivo de solução.

# Set the base image as the .NET 7.0 SDK (this includes the runtime)
FROM mcr.microsoft.com/dotnet/sdk:7.0@sha256:d32bd65cf5843f413e81f5d917057c82da99737cb1637e905a1a4bc2e7ec6c8d as build-env

# Copy everything and publish the release (publish implicitly restores and builds)
WORKDIR /app
COPY . ./
RUN dotnet publish ./DotNet.GitHubAction/DotNet.GitHubAction.csproj -c Release -o out --no-self-contained

# Label the container
LABEL maintainer="David Pine <david.pine@microsoft.com>"
LABEL repository="https://github.com/dotnet/samples"
LABEL homepage="https://github.com/dotnet/samples"

# Label as GitHub action
LABEL com.github.actions.name="The name of your GitHub Action"
# Limit to 160 characters
LABEL com.github.actions.description="The description of your GitHub Action."
# See branding:
# https://docs.github.com/actions/creating-actions/metadata-syntax-for-github-actions#branding
LABEL com.github.actions.icon="activity"
LABEL com.github.actions.color="orange"

# Relayer the .NET SDK, anew with the build output
FROM mcr.microsoft.com/dotnet/sdk:7.0@sha256:d32bd65cf5843f413e81f5d917057c82da99737cb1637e905a1a4bc2e7ec6c8d
COPY --from=build-env /app/out .
ENTRYPOINT [ "dotnet", "/DotNet.GitHubAction.dll" ]

Observação

O aplicativo .NET neste tutorial depende do SDK do .NET como parte de sua funcionalidade. O Dockerfile cria um novo conjunto de camadas do Docker, independente das anteriores. O Dockerfile começa do zero com a imagem do SDK e adiciona a saída de build do conjunto anterior de camadas. Para aplicativos que não exigem o SDK do .NET como parte de sua funcionalidade, devem depender apenas do Runtime do .NET. Isso reduz consideravelmente o tamanho da imagem.

FROM mcr.microsoft.com/dotnet/runtime:7.0

Aviso

Preste muita atenção a cada etapa dentro do Dockerfile, pois ela difere do Dockerfile padrão criado a partir da funcionalidade "adicionar suporte ao Docker". Em particular, as últimas etapas variam não especificando uma nova WORKDIR que alteraria o caminho para o ENTRYPOINT do aplicativo.

As etapas anteriores do Dockerfile incluem:

  • Definindo a imagem base como mcr.microsoft.com/dotnet/sdk:7.0 o alias build-env.
  • Copiar conteúdos e publicar o aplicativo .NET:
  • Aplicando rótulos ao contêiner.
  • Retransmissão da imagem do SDK do .NET a partir de mcr.microsoft.com/dotnet/sdk:7.0
  • Copiando a saída de build publicada do build-env.
  • Definindo o ponto de entrada, que atribui para dotnet /DotNet.GitHubAction.dll.

Dica

O MCR no mcr.microsoft.com significa"Registro de Contêiner da Microsoft" e é o catálogo de contêineres sindicalizado da Microsoft a partir do hub oficial do Docker. Para obter mais informações, consulte Microsoft analisa catálogo de contêiner.

Cuidado

Se você usar um arquivo global.json para fixar a versão do SDK, consulte explicitamente essa versão em seu Dockerfile. Por exemplo, se você tiver usado global.json para fixar a versão do SDK5.0.300, seu Dockerfile deve usar mcr.microsoft.com/dotnet/sdk:5.0.300. Isso impede a interrupção do GitHub Actions quando uma nova revisão secundária é liberada.

Definir entradas e saídas de ação

Na seção Explorar o aplicativo, você aprendeu sobre a classe ActionInputs. Esse objeto representa as entradas para o GitHub Action. Para que o GitHub reconheça que o repositório é um GitHub Action, você precisa ter um arquivo action.yml na raiz do repositório.

name: 'The title of your GitHub Action'
description: 'The description of your GitHub Action'
branding:
  icon: activity
  color: orange
inputs:
  owner:
    description:
      'The owner of the repo. Assign from github.repository_owner. Example, "dotnet".'
    required: true
  name:
    description:
      'The repository name. Example, "samples".'
    required: true
  branch:
    description:
      'The branch name. Assign from github.ref. Example, "refs/heads/main".'
    required: true
  dir:
    description:
      'The root directory to work from. Examples, "path/to/code".'
    required: false
    default: '/github/workspace'
outputs:
  summary-title:
    description:
      'The title of the code metrics action.'
  summary-details:
    description:
      'A detailed summary of all the projects that were flagged.'
  updated-metrics:
    description:
      'A boolean value, indicating whether or not the action updated metrics.'
runs:
  using: 'docker'
  image: 'Dockerfile'
  args:
  - '-o'
  - ${{ inputs.owner }}
  - '-n'
  - ${{ inputs.name }}
  - '-b'
  - ${{ inputs.branch }}
  - '-d'
  - ${{ inputs.dir }}

O arquivo action.yml anterior define:

  • name e description do GitHub Action
  • branding, que é usado no GitHub Marketplace para ajudar a identificar sua ação de forma mais exclusiva
  • inputs, que mapeia um a um com a classe ActionInputs
  • outputs, que é gravado no Program e usado como parte da Composição do fluxo de trabalho
  • O nó runs, que informa ao GitHub que o aplicativo é um aplicativo docker e os argumentos para passar para ele

Para obter mais informações, consulte Sintaxe de metadados para GitHub Actions.

Variáveis de ambiente predefinidas

Com GitHub Actions, você terá muitas variáveis de ambiente por padrão. Por exemplo, a variável GITHUB_REF sempre contém uma referência à branch ou marca que iniciou a execução do fluxo de trabalho. GITHUB_REPOSITORY tem o nome do proprietário e do repositório, por exemplo, dotnet/docs.

Você deve explorar as variáveis de ambiente predefinidas e usá-las adequadamente.

Composição do fluxo de trabalho

Com o aplicativo .NET em contêineres e as entradas e saídas de ação definidas, você está pronto para realizar a ação. GitHub Actions não precisam ser publicados no GitHub Marketplace a serem usados. Os fluxos de trabalho são definidos no diretório .github/fluxos de trabalho de um repositório como arquivos YAML.

# The name of the work flow. Badges will use this name
name: '.NET code metrics'

on:
  push:
    branches: [ main ]
    paths:
    - 'github-actions/DotNet.GitHubAction/**'               # run on all changes to this dir
    - '!github-actions/DotNet.GitHubAction/CODE_METRICS.md' # ignore this file
  workflow_dispatch:
    inputs:
      reason:
        description: 'The reason for running the workflow'
        required: true
        default: 'Manual run'

jobs:
  analysis:

    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write

    steps:
    - uses: actions/checkout@v3

    - name: 'Print manual run reason'
      if: ${{ github.event_name == 'workflow_dispatch' }}
      run: |
        echo 'Reason: ${{ github.event.inputs.reason }}'

    - name: .NET code metrics
      id: dotnet-code-metrics
      uses: dotnet/samples/github-actions/DotNet.GitHubAction@main
      env:
        GREETINGS: 'Hello, .NET developers!' # ${{ secrets.GITHUB_TOKEN }}
      with:
        owner: ${{ github.repository_owner }}
        name: ${{ github.repository }}
        branch: ${{ github.ref }}
        dir: ${{ './github-actions/DotNet.GitHubAction' }}
      
    - name: Create pull request
      uses: peter-evans/create-pull-request@v4
      if: ${{ steps.dotnet-code-metrics.outputs.updated-metrics }} == 'true'
      with:
        title: '${{ steps.dotnet-code-metrics.outputs.summary-title }}'
        body: '${{ steps.dotnet-code-metrics.outputs.summary-details }}'
        commit-message: '.NET code metrics, automated pull request.'

Importante

Para GitHub Actions em contêineres, você precisa usar runs-on: ubuntu-latest. Para obter mais informações, consulte Sintaxe do fluxo de trabalhojobs.<job_id>.runs-on.

O arquivo YAML do fluxo de trabalho anterior define três nós primários:

  • O name do fluxo de trabalho. Esse nome também é usado ao criar uma notificação de status de fluxo de trabalho.
  • O nó on define quando e como a ação é iniciada.
  • O nó jobs descreve os diversos trabalhos e etapas de cada trabalho. Etapas individuais produzem GitHub Actions.

Para obter mais informações, consulte Criando seu primeiro fluxo de trabalho.

Com foco no nó steps, a composição é mais óbvia:

steps:
- uses: actions/checkout@v3

- name: 'Print manual run reason'
  if: ${{ github.event_name == 'workflow_dispatch' }}
  run: |
    echo 'Reason: ${{ github.event.inputs.reason }}'

- name: .NET code metrics
  id: dotnet-code-metrics
  uses: dotnet/samples/github-actions/DotNet.GitHubAction@main
  env:
    GREETINGS: 'Hello, .NET developers!' # ${{ secrets.GITHUB_TOKEN }}
  with:
    owner: ${{ github.repository_owner }}
    name: ${{ github.repository }}
    branch: ${{ github.ref }}
    dir: ${{ './github-actions/DotNet.GitHubAction' }}
  
- name: Create pull request
  uses: peter-evans/create-pull-request@v4
  if: ${{ steps.dotnet-code-metrics.outputs.updated-metrics }} == 'true'
  with:
    title: '${{ steps.dotnet-code-metrics.outputs.summary-title }}'
    body: '${{ steps.dotnet-code-metrics.outputs.summary-details }}'
    commit-message: '.NET code metrics, automated pull request.'

O jobs.steps representa a composição do fluxo de trabalho. As etapas são administradas de modo que sejam sequenciais, comunicativas e combinável. Com vários GitHub Actions representando etapas, cada uma com entradas e saídas, os fluxos de trabalho podem ser combinados.

Nas etapas anteriores, você pode observar:

  1. O repositório está em check-out.

  2. Uma mensagem é impressa no log de fluxo de trabalho, quando executada manualmente.

  3. Uma etapa identificada como dotnet-code-metrics:

    • uses: dotnet/samples/github-actions/DotNet.GitHubAction@main é o local do aplicativo .NET em contêineres neste tutorial.
    • env cria uma variável de ambiente "GREETING", que é impressa na execução do aplicativo.
    • with especifica cada uma das entradas de ação necessárias.
  4. Uma etapa condicional, nomeada Create pull request, é executada quando a etapa dotnet-code-metrics especifica um parâmetro de saída com updated-metrics um valor de true.

Importante

O GitHub permite a criação de segredos criptografados. Os segredos podem ser usados na composição do fluxo de trabalho usando a sintaxe ${{ secrets.SECRET_NAME }}. No contexto de um GitHub Action, há um token GitHub que é preenchido automaticamente por padrão: ${{ secrets.GITHUB_TOKEN }}. Para obter mais informações, consulte Sintaxe de contexto e de expressão do GitHub Actions.

Colocar tudo isso junto

O repositório GitHub dotnet/samples abriga muitos projetos de código-fonte de exemplo do .NET, incluindo o aplicativo neste tutorial.

O arquivo CODE_METRICS.md gerado é navegável. Esse arquivo representa a hierarquia dos projetos analisados. Cada projeto tem uma seção de nível superior e um emoji que representa o status geral da maior complexidade ciclomática para objetos aninhados. Conforme você navega pelo arquivo, cada seção mostra oportunidades de detalhamento com um resumo de cada área. O markdown tem seções recolhiveis como uma conveniência a mais.

A hierarquia progride de:

  • Arquivo de projeto para assembly
  • Assembly para namespace
  • Namespace para tipo nomeado
  • Cada tipo nomeado tem uma tabela e cada tabela tem:
    • Links para números de linha para campos, métodos e propriedades
    • Classificações individuais para métricas de código

Em ação

O fluxo de trabalho especifica que on e push para o branch main, a ação deve iniciar a execução. Quando for executado, a guia Ações no GitHub informa o fluxo de log ao vivo de sua execução. Veja aqui um log de exemplo a partir da execução .NET code metrics:

.NET code metrics - GitHub Actions log

Aprimoramentos de desempenho

Se você seguiu o exemplo, talvez tenha notado que toda vez que essa ação é usada, ela fará um build do Docker para essa imagem. Portanto, cada gatilho é confrontado com algum tempo para compilar o contêiner antes de executá-lo. Antes de liberar seu GitHub Actions para o marketplace, você deve:

  1. Criar (automaticamente) a imagem do Docker
  2. Enviar a imagem do docker por push para o Registro de Contêiner do GitHub (ou qualquer outro registro de contêiner público)
  3. Alterar a ação para não criar a imagem, mas para usar uma imagem de um registro público.
# Rest of action.yml content removed for readability
# using Dockerfile
runs:
  using: 'docker'
  image: 'Dockerfile' # Change this line
# using container image from public registry
runs:
  using: 'docker'
  image: 'docker://ghcr.io/some-user/some-registry' # Starting with docker:// is important!!

Para obter mais informações, confira GitHub Docs: como trabalhar com o Registro de contêiner.

Confira também

Próximas etapas