Freigeben über


Tutorial: Erstellen einer GitHub-Aktion mit .NET

Erfahren Sie, wie Sie eine .NET-App erstellen, die als GitHub-Aktion verwendet werden kann. GitHub Actions Workflowautomatisierung und -komposition aktivieren. Mit GitHub Actions können Sie Quellcode in GitHub erstellen, testen und bereitstellen. Darüber hinaus bietet Actions die Möglichkeit, programmgesteuert mit Problemen zu interagieren, Pull Requests zu erstellen, Code Reviews durchzuführen und Branches zu verwalten. Weitere Informationen zur Continuous Integration mit GitHub Actions finden Sie unter Erstellen und Testen von .NET.

In diesem Tutorial lernen Sie Folgendes:

  • Vorbereiten einer .NET-App für GitHub Actions
  • Definieren von Aktionseingaben und -ausgaben
  • Erstellen eines Workflows

Voraussetzungen

Der Zweck der App

Die App in diesem Tutorial führt codemetrische Analysen durch:

  • Überprüfen und Ermitteln von *.csproj- und *.vbproj-Projektdateien.

  • Analysieren des ermittelten Quellcodes in diesen Projekten für Folgendes:

    • Zyklomatische Komplexität
    • Wartbarkeitsindex
    • Vererbungstiefe
    • Kopplung zwischen Klassen
    • Anzahl der Zeilen des Quellcodes
    • Ungefähre Zeilen des ausführbaren Codes
  • Erstellen (oder Aktualisieren) einer CODE_METRICS.md-Datei.

Die App ist nicht für das Erstellen eines Pull Request mit den Änderungen an der Datei CODE_METRICS.md verantwortlich. Diese Änderungen werden im Rahmen der Workflowkomposition verwaltet.

Bei Verweisen auf den Quellcode in diesem Tutorial wurden Teile der App aus Gründen der Kürze nicht angegeben. Der vollständige Code der App ist auf GitHub verfügbar.

App erkunden

Die .NET-Konsolen-App verwendet das CommandLineParser NuGet-Paket, um Argumente in das ActionInputs-Objekt zu parsen.

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]);
        }
    }
}

Die oben abgebildete Aktionseingabeklasse definiert mehrere erforderliche Eingaben, damit die App erfolgreich ausgeführt werden kann. Der Konstruktor schreibt den Wert der "GREETINGS"-Umgebungsvariable, wenn in der aktuellen Ausführungsumgebung ein Wert verfügbar ist. Die Eigenschaften Name und Branch werden geparst und aus dem letzten Segment einer "/" durch Trennzeichen getrennten Zeichenfolge zugewiesen.

Konzentrieren Sie sich mit der definierten Aktionseingabeklasse auf die Datei 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);
}

Die Datei Program ist aus Gründen der Kürze vereinfacht. Die vollständige Beispielquelle können Sie unter Program.cs anzeigen. Die vorhandenen Mechanismen zeigen den erforderlichen Codebaustein:

Externe Projekt- oder Paketverweise können verwendet und mit Abhängigkeitsinjektion registriert werden. Get<TService> ist eine statische lokale Funktion, die die IHost-Instanz erfordert und zum Auflösen erforderlicher Dienste verwendet wird. Mit dem Singleton CommandLine.Parser.Default ruft die App eine parser-Instanz von args ab. Wenn die Argumente nicht analysiert werden können, wird die App mit einem Exitcode ohne Null beendet. Weitere Informationen finden Sie unter Festlegen von Exitcodes für Aktionen.

Wenn die Argumente erfolgreich geparst wurden, wurde die App ordnungsgemäß mit den erforderlichen Eingaben aufgerufen. In diesem Fall wird die primäre Funktionalität StartAnalysisAsync aufgerufen.

Zum Schreiben von Ausgabewerten müssen Sie das von GitHub Actions: Festlegen eines Ausgabeparameters erkannte Format verwenden.

Vorbereiten der .NET-App für GitHub Actions

GitHub Actions unterstützt zwei Varianten der App-Entwicklung:

  • JavaScript (optional TypeScript)
  • Docker-Container (jede App, die unter Docker ausgeführt wird)

In der virtuellen Umgebung, in der GitHub Action gehostet wird, kann .NET installiert sein oder nicht. Informationen dazu, was in der Zielumgebung vorinstalliert ist, finden Sie unter GitHub Actions: Virtuelle Umgebungen. Es ist zwar möglich, .NET CLI-Befehle aus den GitHub Actions-Workflows auszuführen, für eine besser funktionierende .NET-basierte GitHub-Aktion empfehlen wir Ihnen, die Anwendung zu containerisieren. Weitere Informationen finden Sie unter Containerisieren einer .NET-App.

Die Dockerfile-Datei

Eine Dockerfile-Datei ist eine Reihe von Anweisungen zum Erstellen eines Images. Bei .NET-Apps befindet sich die Dockerfile-Datei in der Regel im Stamm des Verzeichnisses neben einer Projektmappendatei.

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

Hinweis

Die .NET-App in diesem Tutorial basiert auf dem .NET SDK als Teil der Funktionalität. Die Dockerfile-Datei erstellt einen neuen Satz von Docker-Ebenen, unabhängig von den vorherigen Ebenen. Sie beginnt von Grund auf mit dem SDK-Image und fügt die Buildausgabe aus dem vorherigen Satz von Ebenen hinzu. Für Anwendungen, die das .NET SDK nicht als Teil ihrer Funktionalität benötigen, sollten sie stattdessen nur auf die .NET Runtime zurückgreifen. Dadurch wird die Größe des Bilds erheblich reduziert.

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

Warnung

Achten Sie auf jeden Schritt innerhalb der Dockerfile-Datei, da sie sich von der Dockerfile-Standarddatei unterscheidet, die mit der Funktion "Docker-Unterstützung hinzufügen" erstellt wurde. Insbesondere unterscheiden sich die letzten Schritte, indem sie keinen neuen WORKDIR angeben, der den Pfad zur ENTRYPOINT der App ändern würde.

Die obigen Dockerfile-Schritte umfassen Folgendes:

  • Festlegen des Basisimages von mcr.microsoft.com/dotnet/sdk:7.0 als Alias build-env.
  • Kopieren des Inhalts und Veröffentlichen der .NET-App:
  • Anwenden von Bezeichnungen auf den Container.
  • Neuschichten des .NET SDK-Image aus mcr.microsoft.com/dotnet/sdk:7.0
  • Kopieren der veröffentlichten Buildausgabe aus build-env.
  • Definieren des Einstiegspunkts, der an dotnet /DotNet.GitHubAction.dll delegiert wird.

Tipp

MCR in mcr.microsoft.com steht für „Microsoft Container Registry“ und ist der syndizierte Containerkatalog von Microsoft aus dem offiziellen Docker-Hub. Weitere Informationen finden Sie unter Containerkatalog für Microsoft-Syndikate.

Achtung

Wenn Sie eine global.json-Datei verwenden, um die SDK-Version anzuheften, sollten Sie explizit auf diese Version in Ihrer Dockerfile-Datei verweisen. Wenn Sie beispielsweise global.json zum Anheften der SDK-Version 5.0.300 verwendet haben, sollte Ihre Dockerfile-Datei mcr.microsoft.com/dotnet/sdk:5.0.300 verwenden. Dadurch wird verhindert, dass GitHub Actions bei der Veröffentlichung einer neuen Nebenrevision abgebrochen wird.

Definieren von Aktionseingaben und -ausgaben

Im Abschnitt Erkunden der App haben Sie mehr über die ActionInputs-Klasse erfahren. Dieses Objekt stellt die Eingaben für die GitHub-Aktion dar. Damit GitHub erkennt, dass es sich bei dem Repository um eine GitHub-Aktion handelt, müssen Sie eine action.yml-Datei im Stammverzeichnis des Repositorys haben.

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

Die oben abgebildete action.yml-Datei definiert Folgendes:

  • name und description der GitHub-Aktion
  • das branding, das im GitHub Marketplace verwendet wird, um Ihre Aktion eindeutig zu identifizieren
  • die inputs, die eins zu eins der ActionInputs-Klasse zuordnet werden
  • die outputs, die in das Program geschrieben und als Teil der Workflowkomposition verwendet werden
  • der runs-Knoten, der GitHub mitteilt, dass es sich bei der App um eine docker-Anwendung handelt und welche Argumente an sie übergeben werden sollen

Weitere Informationen finden Sie unter Metadatensyntax für GitHub Actions.

Vordefinierte Umgebungsvariablen

Mit GitHub Actions erhalten Sie standardmäßig viele Umgebungsvariablen. Beispielsweise enthält die Variable GITHUB_REF immer einen Verweis auf den Branch oder das Tag, das die Workflowausführung ausgelöst hat. GITHUB_REPOSITORY bezeichnet den Besitzer und den Repositorynamen, z. B dotnet/docs.

Wir empfehlen Ihnen, die vordefinierten Umgebungsvariablen zu ergründen und entsprechend zu verwenden.

Workflowkomposition

Wenn die .NET-App containerisiert ist und die Aktionsein- und -ausgaben definiert sind, können Sie die Aktion nutzen. GitHub-Aktionen müssen nicht im GitHub Marketplace veröffentlicht werden, um verwendet zu werden. Workflows werden im Verzeichnis .github/workflows eines Repositorys als YAML-Dateien definiert.

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

Wichtig

Für containerisierte GitHub-Aktionen müssen Sie runs-on: ubuntu-latest verwenden. Weitere Informationen finden Sie unter Workflowsyntax jobs.<job_id>.runs-on.

Die oben abgebildete WORKFLOW-YAML-Datei definiert drei primäre Knoten:

  • Der name des Workflows. Dieser Name wird auch beim Erstellen eines Workflowstatus-Badges verwendet.
  • Der on-Knoten definiert, wann und wie die Aktion ausgelöst wird.
  • Der jobs-Knoten beschreibt die verschiedenen Einzelvorgänge und Schritte innerhalb der einzelnen Aufträge. Jeder Schritt nutzt eine GitHub-Aktion.

Weitere Informationen finden Sie unter Erstellen Ihres ersten Workflows.

Beim steps-Knoten ist die Komposition offensichtlicher:

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 stellt die Workflowkomposition dar. Die Schritte sind so orchestriert, dass sie sequenziell, kommunikativ und komponierbar sind. Mit verschiedenen GitHub-Aktionen, die Schritte darstellen, die jeweils Ein- und Ausgaben enthalten, können Workflows zusammengestellt werden.

In den oben dargestellten Schritten können Sie beobachten:

  1. Das Repository ist ausgecheckt.

  2. Bei manueller Ausführung wird eine Nachricht in das Workflowprotokoll gedruckt.

  3. Ein als dotnet-code-metrics identifizierter Schritt:

    • uses: dotnet/samples/github-actions/DotNet.GitHubAction@main ist der Speicherort der containerisierten .NET-App in diesem Tutorial.
    • env erstellt eine Umgebungsvariable "GREETING", die bei der Ausführung der App ausgegeben wird.
    • with gibt jede der erforderlichen Aktionseingaben an.
  4. Ein bedingter Schritt mit dem Namen Create pull request wird ausgeführt, wenn der Schritt dotnet-code-metrics einen Ausgabeparameter von updated-metrics mit dem Wert true angibt.

Wichtig

GitHub ermöglicht die Erstellung verschlüsselter Geheimnisse. Geheimnisse können in der Workflowkomposition mithilfe der Syntax ${{ secrets.SECRET_NAME }} verwendet werden. Im Kontext einer GitHub-Aktion gibt es ein GitHub-Token, das standardmäßig automatisch aufgefüllt wird: ${{ secrets.GITHUB_TOKEN }}. Weitere Informationen finden Sie unter Syntax für Kontexte und Ausdrücke für GitHub Actions.

Korrektes Zusammenfügen

Im GitHub-Repository dotnet/samples befinden sich viele .NET-Beispielquellcodeprojekte, einschließlich der App in diesem Tutorial.

Die generierte CODE_METRICS.md-Datei ist navigierbar. Diese Datei stellt die Hierarchie der analysierten Projekte dar. Jedes Projekt verfügt über einen Abschnitt auf oberster Ebene und ein Emoji, das den Gesamtstatus der höchsten zyklomatischen Komplexität für geschachtelte Objekte darstellt. Beim Navigieren durch die Datei bietet jeder Abschnitt Drill-Down-Möglichkeiten mit einer Zusammenfassung der einzelnen Bereiche. Das Markdown verfügt über reduzierbare Abschnitte.

Die Hierarchie sieht wie folgt aus:

  • Projektdatei zu Assembly
  • Assembly zum Namespace
  • Namespace für benannten Typ
  • Jeder benannte Typ verfügt über eine Tabelle, und jede Tabelle beinhaltet Folgendes:
    • Links zu Zeilennummern für Felder, Methoden und Eigenschaften
    • Individuelle Bewertungen für Codemetriken

In Aktion

Der Workflow sieht vor, dass die Aktion bei on in Bezug auf einen push auf den main-Branch ausgelöst wird. Wenn sie ausgeführt wird, meldet die Registerkarte Aktionen in GitHub den Liveprotokollstream seiner Ausführung. Hier sehen Sie ein Beispielprotokoll aus der .NET code metrics-Ausführung:

.NET code metrics - GitHub Actions log

Leistungsverbesserungen

Wenn Sie das Beispiel befolgt haben, haben Sie möglicherweise bemerkt, dass bei jeder Verwendung dieser Aktion ein Docker-Build für dieses Image ausgeführt wird. Jeder Trigger benötigt also eine gewisse Zeit, um den Container zu erstellen, bevor er ausgeführt wird. Bevor Sie Ihre GitHub-Aktion auf dem Marketplace freigeben, sollten Sie Folgendes ausführen:

  1. (automatisches) Erstellen des Docker-Images
  2. Pushen des Docker-Images an die GitHub Container Registry (oder eine andere öffentliche Containerregistrierung)
  3. Ändern der Aktion so, dass das Image nicht erstellt wird, sondern ein Image aus einer öffentlichen Registrierung verwendet wird.
# 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!!

Weitere Informationen finden Sie unter GitHub Docs: Arbeiten mit der Containerregistrierung.

Siehe auch

Nächste Schritte