Partilhar 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. As ações do GitHub permitem a automação e a composição do fluxo de trabalho. Com o GitHub Actions, você pode criar, testar e implantar código-fonte do GitHub. Além disso, as ações expõem a capacidade de interagir programaticamente com problemas, criar solicitações pull, executar revisões de código e gerenciar ramificações. Para obter mais informações sobre a integração contínua com as ações do GitHub, consulte Criando e testando o .NET.

Neste tutorial, irá aprender a:

  • Preparar um aplicativo .NET para ações do GitHub
  • Definir entradas e saídas de ação
  • Redigir um fluxo de trabalho

Pré-requisitos

A intenção do aplicativo

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

  • Digitalização e descoberta de arquivos de projeto *.csproj e *.vbproj.

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

    • Complexidade ciclomática
    • Índice de manutenibilidade
    • Profundidade da herança
    • Acoplamento de classe
    • Número de linhas do 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 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 por brevidade. O código completo do aplicativo está disponível no GitHub.

Explorar a aplicação

O aplicativo de console .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 estiver disponível no ambiente de execução atual. As Name propriedades e são analisadas e Branch atribuídas a partir do último segmento de uma "/" cadeia de caracteres delimitada.

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 brevidade, para explorar a fonte de exemplo completa, consulte Program.cs. Os mecânicos em vigor demonstram o código clichê necessário para usar:

Referências de projeto ou pacote externo podem ser usadas e registradas com injeção de dependência. O Get<TService> é uma função local estática, que requer a IHost instância e é usada para resolver os serviços necessários. Com o singleton, o CommandLine.Parser.Default aplicativo obtém uma parser instância 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 Definindo 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, é feita uma chamada para a funcionalidade StartAnalysisAsync principal.

Para escrever valores de saída, você deve seguir o formato reconhecido por GitHub Actions: Setting an output parameter.

Preparar o aplicativo .NET para ações do GitHub

As Ações do GitHub suportam duas variações de desenvolvimento de aplicativos, ou

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

O ambiente virtual onde a Ação do GitHub está hospedada pode ou não ter o .NET instalado. Para obter informações sobre o que está pré-instalado no ambiente de destino, consulte Ambientes virtuais de ações do GitHub. Embora seja possível executar comandos da CLI do .NET a partir dos fluxos de trabalho do GitHub Actions, para um funcionamento mais completo. Ação do GitHub baseada em NET, recomendamos que você coloque o aplicativo em contêineres. Para obter mais informações, consulte Containerize a .NET app.

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" ]

Nota

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. Ele começa do zero com a imagem do SDK e adiciona 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 confiar apenas no .NET Runtime. 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 particular, as últimas etapas variam por não especificar um novo WORKDIR que mudaria o caminho para o .ENTRYPOINT

As etapas anteriores do Dockerfile incluem:

  • Definindo a imagem base como mcr.microsoft.com/dotnet/sdk:7.0 o alias build-env.
  • Copiando o conteúdo e publicando o aplicativo .NET:
  • Aplicação de etiquetas ao recipiente.
  • Retransmitindo a imagem do SDK do .NET a partir de mcr.microsoft.com/dotnet/sdk:7.0
  • Copiando a saída de compilação publicada do build-env.
  • Definindo o ponto de entrada, que delega ao dotnet /DotNet.GitHubAction.dll.

Gorjeta

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

Atenção

Se você usar um arquivo global.json para fixar a versão do SDK, deverá consultar explicitamente essa versão no Dockerfile. Por exemplo, se você usou global.json para fixar a versão 5.0.300do SDK, seu Dockerfile deve usar mcr.microsoft.com/dotnet/sdk:5.0.300. Isso evita quebrar as ações do GitHub quando uma nova revisão secundária é lançada.

Definir entradas e saídas de ação

Na seção Explore o aplicativo, você aprendeu sobre a ActionInputs aula. Este 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:

  • O name e description da Ação 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 ActionInputs classe
  • 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 passar para ele

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

Variáveis de ambiente pré-definidas

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 tag que disparou a execução do fluxo de trabalho. GITHUB_REPOSITORY tem o proprietário e o nome do repositório, por exemplo, dotnet/docs.

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

Composição do fluxo de trabalho

Com o aplicativo .NET em contêiner e as entradas e saídas de ação definidas , você está pronto para consumir a ação. As Ações do GitHub não precisam ser publicadas no GitHub Marketplace para serem usadas. Os fluxos de trabalho são definidos no diretório .github/workflows 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, é necessário usar runs-on: ubuntu-latesto . Para obter mais informações, consulte Sintaxe jobs.<job_id>.runs-ondo fluxo de trabalho .

O arquivo YAML do fluxo de trabalho anterior define três nós principais:

  • O name do fluxo de trabalho. Esse nome também é usado ao criar um selo de status do fluxo de trabalho.
  • O on nó define quando e como a ação é acionada.
  • O jobs nó descreve os vários trabalhos e etapas dentro de cada trabalho. As etapas individuais consomem as ações do GitHub.

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. Os passos são orquestrados de tal forma que são sequenciais, comunicativos e compostos. 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. Foi feito check-out do repositório.

  2. Uma mensagem é impressa no log do 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êiner 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, denominada Create pull request é executada quando a dotnet-code-metrics etapa especifica um parâmetro de saída com um valor de updated-metrics 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 ${{ secrets.SECRET_NAME }} sintaxe. 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 Sintaxe de contexto e expressão para ações do GitHub.

Juntar tudo

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

O arquivo CODE_METRICS.md gerado é navegável. Este 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. À medida que você navega pelo arquivo, cada seção expõe oportunidades de detalhamento com um resumo de cada área. A marcação tem seções dobráveis como uma conveniência adicional.

A hierarquia progride a partir de:

  • Arquivo de projeto para montagem
  • Assembly para namespace
  • Namespace para named-type
  • 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 a para a ramificação, a push main ação é acionada para ser executada. Quando ele é 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 .NET code metrics execução:

.NET code metrics - GitHub Actions log

Melhoramentos de desempenho

Se você acompanhou o exemplo, deve ter notado que toda vez que essa ação for usada, ela fará uma compilação docker para essa imagem. Assim, cada gatilho é confrontado com algum tempo para construir o contêiner antes de executá-lo. Antes de lançar suas Ações do GitHub para o mercado, você deve:

  1. (automaticamente) Criar a imagem do Docker
  2. Envie a imagem do docker 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óximos passos