Compartilhar via


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

Saiba como criar um aplicativo .NET que pode ser usado como uma ação do GitHub. O GitHub Actions habilita a automação e a composição do fluxo de trabalho. Com o GitHub Actions, você pode criar, testar e implantar o código-fonte do GitHub. Além disso, as ações expõem a capacidade de interagir programaticamente 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 o GitHub Actions, consulte Compilar e testar o .NET.

Neste tutorial, você aprenderá como:

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

Pré-requisitos

A intenção do aplicativo

O aplicativo neste tutorial executa a análise de métricas de código:

  • Analisando e descobrindo arquivos *.csproj e *.vbproj.

  • Analisando o código-fonte descoberto nesses projetos para:

    • Complexidade ciclomática
    • Índice de manutenção
    • Profundidade da herança
    • Acoplamento de classe
    • Número de linhas do código-fonte
    • Linhas aproximadas do código executável
  • Criando (ou atualizando) 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.

As referências ao código-fonte neste tutorial têm partes do aplicativo omitidas para fins de brevidade. O código completo do aplicativo está disponível no GitHub.

Explorar o aplicativo

O aplicativo de console do .NET usa o CommandLineParser pacote NuGet para analisar argumentos no ActionInputs objeto.

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 gravará o valor da "GREETINGS" variável de ambiente se um 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 por "/".

Com a classe de entradas de ação definida, concentre-se no 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 Program arquivo é simplificado para fins de brevidade, para explorar a fonte de exemplo completa, consulte Program.cs. Os mecanismos em vigor demonstram o código padrão necessário para usar:

Referências externas de projeto ou pacote podem ser usadas e registradas com injeção de dependência. Get<TService> é uma função local estática, que requer a instância IHost e é usada para resolver serviços necessários. Com o CommandLine.Parser.Default Singleton, o aplicativo obtém uma instância parser do 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 Configurando códigos de saída para ações.

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

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

Preparar o aplicativo .NET para o GitHub Actions

O GitHub Actions dá 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 nos fluxos de trabalho do GitHub Actions, para um funcionamento mais completo de uma GitHub Action baseada 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, independentemente das anteriores. Começa do zero com a imagem do SDK e incorpora a saída de compilação do conjunto anterior de camadas. Para aplicativos que não exigem o SDK do .NET como parte de sua funcionalidade, eles devem depender apenas do Runtime do .NET. Isso reduz muito o tamanho da imagem.

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

Aviso

Preste muita atenção a cada etapa dentro do Dockerfile, pois ele difere do Dockerfile padrão criado a partir da funcionalidade "adicionar suporte ao Docker". Em especial, as últimas etapas variam por não especificarem um novo WORKDIR, o que alteraria o caminho para o ENTRYPOINT do aplicativo.

As etapas anteriores do Dockerfile incluem:

  • Definindo a imagem base de mcr.microsoft.com/dotnet/sdk:7.0 como o alias build-env.
  • Copiando o conteúdo e publicando o aplicativo .NET:
  • Aplicando rótulos ao contêiner.
  • Retransmissão da imagem do SDK do .NET mcr.microsoft.com/dotnet/sdk:7.0
  • Copiando o resultado de compilação publicado do build-env.
  • Definindo o ponto de entrada, que delega a dotnet /DotNet.GitHubAction.dll.

Dica

O MCR em mcr.microsoft.com significa "Microsoft Container Registry", e é o catálogo de contêineres sindicalizado da Microsoft do hub oficial do Docker. Para obter mais informações, consulte o catálogo de contêineres de sindicatos da Microsoft.

Cuidado

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

Definir entradas e saídas de ação

Na seção Explorar o aplicativo , você aprendeu sobre a ActionInputs classe. Esse objeto representa as entradas para a Ação do GitHub. Para que o GitHub reconheça que o repositório é uma Ação do GitHub, 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:

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

Para obter mais informações, consulte a sintaxe de metadados para o GitHub Actions.

Variáveis de ambiente predefinidas

Com o GitHub Actions, você obterá muitas variáveis de ambiente por padrão. Por exemplo, a variável GITHUB_REF sempre conterá uma referência à ramificação ou marca que disparou 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 de fluxo de trabalho

Com o aplicativo .NET conteinerizado e as entradas e saídas de ação definidas, você está pronto para utilizar a ação. O GitHub Actions não precisa ser publicado no GitHub Marketplace para ser usado. 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 ações do GitHub em contêineres, você precisa usar runs-on: ubuntu-latest. Para obter mais informações, consulte a sintaxe jobs.<job_id>.runs-ondo fluxo de trabalho.

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

  • O name do fluxo de trabalho. Esse nome também é usado ao criar um selo de status de fluxo de trabalho.
  • O on nó define quando e como a ação é disparada.
  • O jobs nó delimita os vários trabalhos e etapas em cada trabalho. As etapas individuais utilizam o GitHub Actions.

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

Focando no steps nó, 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 orquestradas de modo que sejam em sequência, comunicativas e componíveis. Com várias Ações do GitHub representando etapas, cada uma com entradas e saídas, os fluxos de trabalho podem ser compostos.

Nas etapas anteriores, você pode observar:

  1. O repositório está verificado.

  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 "GREETING"de ambiente, 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 updated-metrics com 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 uma Ação do GitHub, há um token do GitHub que é preenchido automaticamente por padrão: ${{ secrets.GITHUB_TOKEN }}. Para obter mais informações, consulte a sintaxe de contexto e expressão do GitHub Actions.

Coloque tudo 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 no nível mais alto e um emoji que representa o status geral do projeto em relação à complexidade ciclomática mais alta encontrada entre os objetos aninhados. À medida que você navega pelo arquivo, cada seção expõe oportunidades de detalhamento com um resumo de cada área. O markdown tem seções recolhiveis como uma conveniência adicional.

A hierarquia segue da seguinte forma:

  • Arquivo de projeto para montagem
  • 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 , push para o main branch, a ação é disparada para execução. Quando ele for executado, a guia Ações no GitHub relatará o fluxo de log ao vivo de sua execução. Aqui está um exemplo de log da execução .NET code metrics :

Métricas de código do .NET – log do GitHub Actions

Aprimoramentos de desempenho

Se você seguiu o exemplo, talvez tenha notado que sempre que essa ação for usada, ela fará um build do Docker para essa imagem. Portanto, cada gatilho enfrenta algum tempo para compilar o contêiner antes de executá-lo. Antes de lançar o GitHub Actions no marketplace, você deve:

  1. (automaticamente) Criar 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. Altere 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, consulte GitHub Docs: Trabalhando com o registro de contêiner.

Consulte também

Próximas etapas