Tutoriel : Créer une action GitHub avec .NET

Découvrez comment créer une application .NET qui peut être utilisée comme action GitHub. GitHub Actions permet l’automatisation et la composition des flux de travail. Avec GitHub Actions, vous pouvez générer, tester et déployer un code source directement à partir de GitHub. De plus, les actions exposent la possibilité d’interagir par programmation avec les problèmes, de créer des demandes de tirage, d’effectuer des révisions de code et de gérer des branches. Pour plus d’informations sur l’intégration continue avec GitHub Actions, consultez Création et test de code .NET.

Dans ce tutoriel, vous allez apprendre à :

  • Préparer une application .NET pour GitHub Actions
  • Définir des entrées et des sorties d’action
  • Composer un flux de travail

Prérequis

Intention de l’application

L’application de ce didacticiel effectue une analyse de la métrique de code en procédant en :

  • Analysant et découvrant des fichiers projet *.csproj et *.vbproj .

  • Analysant du code source découvert dans ces projets pour :

    • Complexité cyclomatique
    • L’Indice de maintenabilité
    • Profondeur de l’héritage
    • Couplage de classes
    • Le nombre de lignes de code source
    • Les lignes approximatives de code exécutable
  • Créant (ou mettant à jour) un fichier CODE_METRICS.md .

L’application n’est pas responsable de la création d’une requête de tirage avec les modifications apportées au fichier CODE_METRICS.md. Ces modifications sont gérées dans le cadre de la composition du flux de travail.

Des parties de l’application ont été omises dans les références au code source de ce didacticiel par souci de concision. Le code de l’application terminé est disponible sur GitHub.

Explorer l’application

L’application console .NET utilise le package CommandLineParserNuGet pour analyser les arguments dans l’objet 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 classe d’entrées d’action précédente définit plusieurs entrées obligatoires pour que l’application s’exécute correctement. Le constructeur écrit la valeur de la variable d’environnement "GREETINGS", si elle est disponible dans l’environnement d’exécution actuel. Les propriétés Name et Branch sont analysées et affectées à partir du dernier segment d’une chaîne délimitée "/".

Avec la classe d’entrées d’action définie, concentrez-vous sur le fichier 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);
}

Le fichier Program est simplifié par souci de concision. Pour explorer l’exemple de source complet, consultez Program.cs. Les mécanismes en place illustrent le code réutilisable requis pour utiliser :

Les références du projet ou du package externes peuvent être utilisées et inscrites avec l’injection de dépendances. Get<TService> est une fonction locale statique, qui nécessite l’instance IHost et est utilisée pour résoudre les services requis. Avec le singleton CommandLine.Parser.Default, l’application obtient une instance parser du args. Lorsqu’il est impossible d’analyser les arguments, l’application se ferme avec un code de sortie différent de zéro. Pour plus d’informations, consultez Définition de codes de sortie pour les actions.

Lorsque les arguments sont correctement analysés, l’application a été appelée correctement avec les entrées requises. Dans ce cas, un appel à la fonctionnalité principale StartAnalysisAsync est effectué.

Pour écrire des valeurs de sortie, vous devez suivre le format reconnu par GitHub Actions : Définition d’un paramètre de sortie.

Préparer l’application .NET pour GitHub Actions

GitHub Actions prend en charge deux variantes du développement d’applications, soit

  • JavaScript (éventuellement TypeScript)
  • Conteneur Docker (toute application qui s’exécute sur Docker)

L’environnement virtuel où l’action GitHub est hébergée peut avoir installé .NET ou non. Pour plus d’informations sur ce qui est préinstallé dans l’environnement cible, consultez GitHub Actions Virtual Environments. Bien qu’il soit possible d’exécuter des commandes CLI .NET à partir des flux de travail GitHub Actions, pour un fonctionnement plus complet d’un action GitHub basée sur .NET, nous vous recommandons de conteneuriser l’application. Pour plus d’informations, consultez Conteneuriser une application .NET.

Le fichier Dockerfile

Un Dockerfile est un ensemble d’instructions pour générer une image. Pour les applications .NET, le fichier Dockerfile se trouve généralement à la racine du répertoire à côté d’un fichier solution.

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

Notes

L’application .NET de ce didacticiel s’appuie sur le Kit de développement logiciel (SDK) .NET dans le cadre de ses fonctionnalités. Le fichier Dockerfile crée un nouvel ensemble de couches Docker, indépendamment des précédentes. Il démarre à partir de zéro avec l’image du kit de développement logiciel (SDK) et ajoute la sortie de build de l’ensemble de couches précédent. Les applications qui ne nécessitent pas le kit de développement logiciel (SDK) .NET dans le cadre de leurs fonctionnalités, doivent s’appuyer uniquement sur le runtime .NET à la place. Cela réduit considérablement la taille de l’image.

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

Avertissement

Faites attention à chaque étape du Dockerfile, car il diffère du Dockerfile standard créé à partir de la fonctionnalité « Ajouter la prise en charge de Docker ». Les dernières étapes notamment varient en ne spécifiant pas de nouveau WORKDIR qui modifierait le chemin d’accès à l’application ENTRYPOINT.

Les étapes dockerfile précédentes sont les suivantes :

  • La définition de l’image de base à partir de mcr.microsoft.com/dotnet/sdk:7.0 comme alias build-env.
  • La copie des contenus et la publication de l’application .NET :
    • L’application est publiée à l’aide de la commande dotnet publish.
  • Application d’étiquettes au conteneur.
  • Relai de l’image du kit de développement logiciel (SDK) .NET à partir de mcr.microsoft.com/dotnet/sdk:7.0
  • Copie de la sortie du build publiée à partir de build-env.
  • Définition du point d’entrée, qui délègue à dotnet /DotNet.GitHubAction.dll.

Conseil

McR dans mcr.microsoft.com signifie « Microsoft Container Registry » et est le catalogue de conteneurs syndiqué de Microsoft à partir du hub Docker officiel. Pour plus d’informations, consultez Microsoft syndicates container catalog.

Attention

Si vous utilisez un fichier global.json pour épingler la version du kit de développement logiciel (SDK), vous devez explicitement faire référence à cette version dans votre fichier Dockerfile. Par exemple, si vous avez utilisé un fichier global.json pour épingler la version 5.0.300du kit de développement logiciel (SDK), votre fichier Dockerfile doit utiliser mcr.microsoft.com/dotnet/sdk:5.0.300. Cela empêche la rupture de GitHub Actions lors de la publication d’une nouvelle révision mineure.

Définir des entrées et des sorties d’action

Dans la section Explorer l’application, vous avez découvert la classe ActionInputs. Cet objet représente les entrées pour l’action GitHub. Pour que GitHub reconnaisse que le référentiel est une action GitHub, vous devez disposer d’un fichier action.yml à la racine du référentiel.

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

Le fichier action.yml précédent définit :

  • La name et le description de l’action GitHub
  • Le brandingqui est utilisé dans la Place de marché GitHub pour vous aider à identifier de manière plus unique votre action
  • Les inputsqui mappent un-à-un avec la classe ActionInputs
  • Les outputsqui sont écrits dans le Programet utilisés dans la composition de flux de travail
  • Le nœud runs, qui indique à GitHub que l’application est une application docker et quels arguments lui passer

Pour plus d’informations, consultez Metadata syntax for GitHub Actions.

Variables d’environnement prédéfinies

Avec GitHub Actions, vous obtenez un grand nombre de variables d’environnement par défaut. Par exemple, la variable GITHUB_REF contient toujours une référence à la branche ou à la balise qui a déclenché l’exécution du flux de travail. GITHUB_REPOSITORYa le nom du propriétaire et du dépôt, par exemple, dotnet/docs.

Vous devez analyser les variables d’environnement prédéfinies et les utiliser en conséquence.

Composition du flux de travail

Une fois l’application .NET conteneurisée et les entrées et les sorties d’action définies, vous pouvez utiliser l’action. Les GitHub Actions ne doivent pas être publiées sur la Place de marché GitHub pour être utilisé. Les flux de travail sont définis dans le répertoire .github/workflows d’un référentiel en tant que fichiers 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.'

Important

Pour les GitHub Actions en conteneur, vous devez utiliser runs-on: ubuntu-latest. Pour plus d’informations, consultez la syntaxe de flux de travailjobs.<job_id>.runs-on.

Le fichier YAML de flux de travail précédent définit trois nœuds principaux :

  • name du workflow. Ce nom est également utilisé lors de la création d’un badge d’état du flux de travail.
  • Le nœud on définit quand et comment l’action est déclenchée.
  • Le nœud jobs décrit les différents travaux et étapes de chaque travail. Les étapes individuelles consomment GitHub Actions.

Pour plus d’informations, consultez Création de votre premier workflow.

En se concentrant sur le nœud steps, la composition est plus évidente :

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 représente la composition du flux de travail. Les étapes sont organisées de telle sorte qu’elles soient séquentielles, communicatives et composables. Avec différentes GitHub Actions représentant des étapes, chacune ayant des entrées et des sorties, des flux de travail peuvent être composés.

Dans les étapes précédentes, vous pouvez observer :

  1. Le référentiel est extrait.

  2. Un message est imprimé dans le journal de flux de travail lorsqu’il est exécuté manuellement.

  3. Étape identifiée comme dotnet-code-metrics :

    • uses: dotnet/samples/github-actions/DotNet.GitHubAction@main est l’emplacement de l’application .NET conteneurisée dans ce didacticiel.
    • env crée une variable d’environnement"GREETING", qui est imprimée lors de l’exécution de l’application.
    • with spécifie chacune des entrées d’action requises.
  4. Une étape conditionnelle nommée Create pull request s’exécute lorsque l’étape dotnet-code-metrics spécifie un paramètre de sortie de updated-metrics avec la valeur true.

Important

GitHub permet de créer des secrets chiffrés. Les secrets peuvent être utilisés dans la composition du flux de travail, à l’aide de la syntaxe ${{ secrets.SECRET_NAME }}. Dans le contexte d’une action GitHub, il existe un jeton GitHub qui est rempli par défaut automatiquement : ${{ secrets.GITHUB_TOKEN }}. Pour plus d’informations, consultez Syntaxe de contexte et d’expression pour GitHub Actions.

Assemblage

Le référentiel GitHub dotnet/samples contient de nombreux exemples de projets de code source .NET, y compris l’application dans ce tutoriel.

Le fichier CODE_METRICS.md généré est explorable. Ce fichier représente la hiérarchie des projets analysés. Chaque projet a une section de niveau supérieur et un emoji qui représente l’état général de la complexité cyclomatique la plus élevée pour les objets imbriqués. Lorsque vous parcourez le fichier, chaque section expose des opportunités d’exploration hiérarchique avec un résumé de chaque zone. Le markdown a des sections réductibles pour plus de commodité.

La hiérarchie progresse à partir du :

  • Fichier projet à assembly
  • Assembly à espace de noms
  • Espace de noms à type nommé
  • Chaque type nommé a une table, et chaque table a :
    • Les liens vers des numéros de ligne pour les champs, méthodes et propriétés
    • Les évaluations individuelles pour les métriques de code

En action

Le flux de travail spécifie que on a push dans la branche main, l’action est déclenchée pour s’exécuter. Lorsqu’elle s’exécute, l’onglet Actions dans GitHub signale le flux de journal en direct de son exécution. Voici un exemple de journal de l’exécution .NET code metrics :

.NET code metrics - GitHub Actions log

Optimisation des performances

Si vous avez suivi l’exemple, vous avez peut-être remarqué que chaque fois que cette action est utilisée, elle effectue un build Docker pour cette image. Par conséquent, chaque déclencheur dispose d’un certain temps pour générer le conteneur avant de l’exécuter. Avant de publier votre GitHub Actions sur la place de marché, vous devez :

  1. (automatiquement) Générer l’image Docker
  2. Envoyer l’image Docker vers GitHub Container Registry (ou tout autre registre de conteneurs public)
  3. Modifier l’action pour ne pas générer l’image, mais pour utiliser une image d’un registre public.
# 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!!

Pour plus d’informations, consultez GitHub Docs : Utilisation du registre de conteneurs.

Voir aussi

Étapes suivantes