Compartir vía


Tutorial: Creación de una Acción de GitHub con .NET

Aprenda a crear una aplicación de .NET que se pueda usar como una acción de GitHub. Acciones de GitHub habilitar la automatización y composición del flujo de trabajo. Con Acciones de GitHub, puede compilar, probar e implementar el código de origen desde GitHub. Además, las acciones exponen la capacidad de interactuar mediante programación con problemas, crear solicitudes de incorporación de cambios, realizar revisiones de código y administrar ramas. Para obtener más información sobre la integración continua con Acciones de GitHub, consulte Creación y prueba de .NET.

En este tutorial, aprenderá a:

  • Preparación de una aplicación .NET para Acciones de GitHub
  • Definición de entradas y salidas de acción
  • Redacción de un flujo de trabajo

Requisitos previos

La intención de la aplicación

La aplicación de este tutorial realiza el análisis de métricas de código mediante:

  • Examinar y detectar archivos de proyecto *.csproj y *.vbproj .

  • Análisis del código fuente detectado en estos proyectos para:

    • Complejidad ciclomática
    • Índice de mantenimiento
    • Profundidad de herencia
    • Acoplamiento de clases
    • Número de líneas de código fuente
    • Líneas aproximadas de código ejecutable
  • Crear (o actualizar) un archivo CODE_METRICS.md.

La aplicación no es responsable de crear una solicitud de incorporación de cambios con los cambios realizados en el archivo CODE_METRICS.md. Estos cambios se administran como parte de la composición del flujo de trabajo.

Las referencias al código fuente de este tutorial tienen partes de la aplicación omitidas para mayor brevedad. El código de la aplicación completado está disponible en GitHub.

Exploración de la aplicación

La aplicación de consola de .NET usa el CommandLineParser paquete NuGet para analizar argumentos en el 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]);
        }
    }
}

La clase de entradas de acción anterior define varias entradas necesarias para que la aplicación se ejecute correctamente. El constructor escribirá el valor de la variable "GREETINGS" de entorno, si hay uno disponible en el entorno de ejecución actual. Las propiedades Name y Branch se analizan y asignan desde el último segmento de una cadena "/" delimitada.

Con la clase de entradas de acción definida, céntrese en el archivo 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);
}

El archivo Program se simplifica para mayor brevedad, para explorar el origen de ejemplo completo, consulte Program.cs. La mecánica en su lugar muestra el código reutilizable necesario para usar:

Se pueden usar referencias de paquetes o proyectos externos y registrarse con la inserción de dependencias. Get<TService> es una función local estática, que requiere la instancia IHost y se usa para resolver los servicios necesarios. Con el singleton CommandLine.Parser.Default, la aplicación obtiene una instancia parser de args. Cuando los argumentos no se pueden analizar, la aplicación se cierra con un código de salida distinto de cero. Para más información, vea Establecimiento de códigos de salida para acciones.

Cuando los argumentos se analizan correctamente, se llamó a la aplicación correctamente con las entradas necesarias. En este caso, se realiza una llamada a la funcionalidad StartAnalysisAsync principal.

Para escribir valores de salida, debe seguir el formato reconocido por Acciones de GitHub: Establecer un parámetro de salida.

Preparación de una aplicación .NET para Acciones de GitHub

Acciones de GitHub admite dos variaciones de desarrollo de aplicaciones, cualquiera de las dos

  • JavaScript (opcionalmente TypeScript)
  • Contenedor de Docker (cualquier aplicación que se ejecute en Docker)

El entorno virtual donde se hospeda la acción de GitHub puede o no tener instalado .NET. Para obtener información sobre lo que está preinstalado en el entorno de destino, consulte Acciones de GitHub en entornos virtuales. Aunque es posible ejecutar comandos de la CLI de .NET desde los flujos de trabajo de Acciones de GitHub, para un funcionamiento más completo. Acción de GitHub basada en NET, se recomienda incluir en contenedores la aplicación. Para obtener más información, vea Incluir una aplicación de .NET Core en un contenedor.

El archivo Dockerfile

Un Dockerfile es un conjunto de instrucciones para compilar una imagen. En el caso de las aplicaciones .NET, el Dockerfile normalmente se encuentra en la raíz del directorio junto a un archivo de solución.

# Set the base image as the .NET 7.0 SDK (this includes the runtime)
FROM mcr.microsoft.com/dotnet/sdk:7.0 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
COPY --from=build-env /app/out .
ENTRYPOINT [ "dotnet", "/DotNet.GitHubAction.dll" ]

Nota

La aplicación .NET de este tutorial se basa en el SDK de .NET como parte de su funcionalidad. El Dockerfile crea un nuevo conjunto de capas de Docker, independientemente de los anteriores. Se inicia desde cero con la imagen del SDK y agrega la salida de compilación del conjunto anterior de capas. En el caso de las aplicaciones que no requieren el SDK de .NET como parte de su funcionalidad, deben basarse solo en el entorno de ejecución de .NET en su lugar. Esto reduce considerablemente el tamaño de la imagen.

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

Advertencia

Preste mucha atención a todos los pasos del Dockerfile, ya que difiere del Dockerfile estándar creado a partir de la funcionalidad "agregar compatibilidad con Docker". En concreto, los últimos pasos varían sin especificar un nuevo WORKDIR que cambiaría la ruta de acceso a la aplicación ENTRYPOINT.

Los pasos anteriores del Dockerfile incluyen:

  • Establecer la imagen base de mcr.microsoft.com/dotnet/sdk:7.0 como el alias build-env.
  • Copiar el contenido y publicar la aplicación .NET:
  • Aplicación de etiquetas al contenedor.
  • Retransmitir la imagen del SDK de .NET desde mcr.microsoft.com/dotnet/sdk:7.0
  • Copiar la salida de compilación publicada de build-env.
  • Definir el punto de entrada, que delega en dotnet /DotNet.GitHubAction.dll.

Sugerencia

MRC en mcr.microsoft.com significa "Microsoft Container Registry", y es el catálogo de contenedores sindicados de Microsoft del centro de Docker oficial. Para obtener más información, consulte Catálogo de contenedores de sindicatos de Microsoft.

Precaución

Si usa un archivo global.json para anclar la versión del SDK, debe hacer referencia explícita a esa versión en el Dockerfile. Por ejemplo, si ha usado global.json para anclar la versión 5.0.300del SDK, el Dockerfile debe usar mcr.microsoft.com/dotnet/sdk:5.0.300. Esto evita interrumpir el Acciones de GitHub cuando se libera una nueva revisión secundaria.

Definición de entradas y salidas de acción

En la sección Explorar la aplicación, ha aprendido sobre la clase ActionInputs. Este objeto representa las entradas de la acción de GitHub. Para que GitHub reconozca que el repositorio es una acción de GitHub, debe tener un archivo action.yml en la raíz del repositorio.

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 }}

El archivo action.yml anterior define:

  • name y description de la acción de GitHub
  • branding, que se usa en Marketplace de GitHub para ayudar a identificar de forma más única la acción
  • inputs, que asigna uno a uno con la clase ActionInputs
  • outputs, que se escribe en Program y se usa como parte de la composición del flujo de trabajo
  • El nodo runs, que indica a GitHub que la aplicación es una aplicación docker y qué argumentos se van a pasar a ella

Para más información, consulte Sintaxis de metadatos de trabajo para Acciones de GitHub.

Variables de entorno predefinidas

Con Acciones de GitHub, obtendrá una gran cantidad de variables de entorno de forma predeterminada. Por ejemplo, la variable GITHUB_REF siempre contendrá una referencia a la rama o etiqueta que desencadenó la ejecución del flujo de trabajo. GITHUB_REPOSITORY tiene el nombre del propietario y del repositorio, por ejemplo, dotnet/docs.

Debe explorar las variables de entorno predefinidas y usarlas en consecuencia.

Composición del flujo de trabajo

Con la aplicación .NET en contenedores y las entradas y salidas de acción definidas, está listo para consumir la acción. Acciones de GitHub no es necesario publicar en Marketplace de GitHub para su uso. Los flujos de trabajo se definen en el directorio .github/workflows de un repositorio como archivos 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

En el caso de Acciones de GitHub en contenedor, debe usar runs-on: ubuntu-latest. Para más información, consulte Sintaxis de flujo de trabajojobs.<job_id>.runs-on.

El archivo YAML de flujo de trabajo anterior define tres nodos principales:

  • name del flujo de trabajo. Este nombre también es lo que se usa al crear un distintivo de estado de flujo de trabajo.
  • El nodo on define cuándo y cómo se desencadena la acción.
  • El nodo jobs describe los distintos trabajos y pasos dentro de cada trabajo. Los pasos individuales consumen Acciones de GitHub.

Para obtener más información, consulte Creación del primer flujo de trabajo.

Centrarse en el nodo steps, la composición es más obvia:

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.'

jobs.steps representa la composición del flujo de trabajo. Los pasos se orquestan de forma que sean secuenciales, comunicativas y composables. Con varias Acciones de GitHub que representan los pasos, cada uno tiene entradas y salidas, se pueden componer flujos de trabajo.

En los pasos anteriores, puede observar:

  1. El repositorio está desprotegido.

  2. Cuando se ejecuta manualmente, se imprime un mensaje en el registro de flujo de trabajo.

  3. Un paso identificado como dotnet-code-metrics:

    • uses: dotnet/samples/github-actions/DotNet.GitHubAction@main es la ubicación de la aplicación .NET en contenedor en este tutorial.
    • env crea una variable de entorno "GREETING", que se imprime en la ejecución de la aplicación.
    • with especifica cada una de las entradas de acción necesarias.
  4. Un paso condicional, denominado Create pull request, se ejecuta cuando el paso dotnet-code-metrics especifica un parámetro de salida de updated-metrics con un valor de true.

Importante

GitHub permite la creación de secretos cifrados. Los secretos se pueden usar en la composición del flujo de trabajo mediante la sintaxis ${{ secrets.SECRET_NAME }}. En el contexto de una acción de GitHub, hay un token de GitHub que se rellena automáticamente de forma predeterminada: ${{ secrets.GITHUB_TOKEN }}. Para obtener más información, consulte Sintaxis de contexto y expresión para Acciones de GitHub.

Colocación de todo junto

El repositorio dotnet/samples de GitHub es el hogar de muchos proyectos de código fuente de ejemplo de .NET, incluida la aplicación de este tutorial.

El archivo CODE_METRICS.md generado es navegable. Este archivo representa la jerarquía de los proyectos analizados. Cada proyecto tiene una sección de nivel superior y un emoji que representa el estado general de la complejidad ciclomática más alta para los objetos anidados. A medida que navega por el archivo, cada sección expone oportunidades de exploración en profundidad con un resumen de cada área. Markdown tiene secciones contraíbles como una comodidad adicional.

La jerarquía progresa de:

  • Archivo de proyecto para ensamblado
  • Ensamblado para espacio de nombres
  • Espacio de nombres para tipo con nombre
  • Cada tipo con nombre tiene una tabla y cada tabla tiene:
    • Vínculos a números de línea para campos, métodos y propiedades
    • Clasificaciones individuales para métricas de código

En acción

El flujo de trabajo especifica que on se desencadena una push en la rama main, la acción se desencadena para ejecutarse. Cuando se ejecuta, la pestaña Acciones de GitHub notificará la secuencia de registro en directo de su ejecución. Un registro de ejemplo de la ejecución .NET code metrics:

.NET code metrics - GitHub Actions log

Mejoras en el rendimiento

Si ha seguido el ejemplo, es posible que haya observado que cada vez que se usa esta acción, realizará una compilación de Docker para esa imagen. Por lo tanto, cada desencadenador se enfrenta con algún tiempo para compilar el contenedor antes de ejecutarlo. Antes de publicar el Acciones de GitHub en Marketplace, debe hacer lo siguiente:

  1. (automáticamente) Compilación de la imagen de Docker
  2. Inserción de la imagen de Docker en GitHub Container Registry (o en cualquier otro registro de contenedor público)
  3. Cambie la acción para no compilar la imagen, pero para usar una imagen de un 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 obtener más información, consulta Trabajo con el registro de contenedor.

Consulte también

Pasos siguientes