Delen via


Zelfstudie: Een GitHub-actie maken met .NET

Meer informatie over het maken van een .NET-app die kan worden gebruikt als een GitHub Action. GitHub Actions maken werkstroomautomatisering en -samenstelling mogelijk. Met GitHub Actions kunt u broncode bouwen, testen en implementeren vanuit GitHub. Daarnaast bieden acties de mogelijkheid om programmatisch te communiceren met issues, pull requests te maken, codebeoordelingen uit te voeren en branches te beheren. Zie .NET bouwen en testen voor meer informatie over continue integratie met GitHub Actions.

In deze handleiding leer je hoe je:

  • Een .NET-app voorbereiden voor GitHub Actions
  • Actie-invoer en -uitvoer definiëren
  • Een werkstroom opstellen

Vereiste voorwaarden

De intentie van de app

De app in deze zelfstudie voert metrische codeanalyse uit op:

  • Scannen en ontdekken van *.csproj en *.vbproj projectbestanden.

  • De gedetecteerde broncode in deze projecten analyseren voor:

    • Cyclomatische complexiteit
    • Index voor onderhoudbaarheid
    • Diepte van erfenis
    • Klassekoppeling
    • Aantal regels broncode
    • Geschatte aantal regels uitvoerbare code
  • Een CODE_METRICS.md-bestand maken (of bijwerken).

De app is niet verantwoordelijk voor het maken van een pull-aanvraag met de wijzigingen in het bestand CODE_METRICS.md . Deze wijzigingen worden beheerd als onderdeel van de werkstroomsamenstelling.

Verwijzingen naar de broncode in deze zelfstudie bevatten gedeelten van de app die ter beknoptheid zijn weggelaten. De volledige app-code is beschikbaar op GitHub.

De app verkennen

De .NET-console-app maakt gebruik van het CommandLineParser NuGet-pakket om argumenten in het ActionInputs object te parseren.

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

De voorgaande inputklasse definieert verschillende vereiste invoer voor de app om succesvol te draaien. De constructor schrijft de waarde van de "GREETINGS" omgevingsvariabele als deze beschikbaar is in de huidige uitvoeringsomgeving. De Name en Branch eigenschappen worden geparseerd en toegewezen vanuit het laatste segment van een "/" gescheiden tekenreeks.

Focus op het Program.cs-bestand met de gedefinieerde actie-invoerklasse.

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

Het Program bestand is vereenvoudigd voor beknoptheid, om de volledige voorbeeldbron te verkennen, zie Program.cs. De mechanismen laten de standaardcode zien die vereist is voor gebruik:

Externe verwijzingen naar projecten of pakketten kunnen worden gebruikt en geregistreerd bij dependency injection. Het Get<TService> is een statische lokale functie, waarvoor het IHost exemplaar is vereist en wordt gebruikt om vereiste services op te lossen. Met de CommandLine.Parser.Default singleton haalt de app een parser exemplaar op van de args. Wanneer de argumenten niet kunnen worden geparseerd, wordt de app afgesloten met een afsluitcode die niet nul is. Zie Afsluitcodes instellen voor acties voor meer informatie.

Wanneer de argumenten zijn verwerkt, wordt de app correct aangeroepen met de nodige gegevens. In dit geval wordt een aanroep naar de primaire functionaliteit StartAnalysisAsync uitgevoerd.

Als u uitvoerwaarden wilt schrijven, moet u de indeling volgen die wordt herkend door GitHub Actions: een uitvoerparameter instellen.

De .NET-app voorbereiden voor GitHub Actions

GitHub Actions ondersteunen twee variaties van app-ontwikkeling, ofwel

  • JavaScript (optioneel TypeScript)
  • Docker-container (elke app die wordt uitgevoerd in Docker)

De virtuele omgeving waarop de GitHub Action wordt gehost, heeft .NET al dan niet geïnstalleerd. Zie GitHub Actions Virtual Environments voor informatie over wat vooraf is geïnstalleerd in de doelomgeving. Hoewel het mogelijk is om .NET CLI-opdrachten uit te voeren vanuit de GitHub Actions-werkstromen, raden we voor een beter functionerende .NET-gebaseerde GitHub Action aan om de app te containeriseren. Zie Een .NET-app containeriseren voor meer informatie.

Het Dockerfile

Een Dockerfile is een set instructies voor het bouwen van een image. Voor .NET-toepassingen bevindt het Dockerfile zich meestal in de hoofdmap van de map naast een oplossingsbestand.

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

Opmerking

De .NET-app in deze zelfstudie is afhankelijk van de .NET SDK als onderdeel van de functionaliteit. Het Dockerfile maakt een nieuwe set Docker-lagen, onafhankelijk van de vorige lagen. Het begint vanaf nul met de SDK-afbeelding en voegt de build-uitvoer van de vorige reeks lagen toe. Voor toepassingen die de .NET SDK niet nodig hebben als onderdeel van hun functionaliteit, moeten ze alleen afhankelijk zijn van de .NET Runtime. Dit vermindert de grootte van de afbeelding aanzienlijk.

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

Waarschuwing

Let goed op elke stap in het Dockerfile, omdat deze verschilt van de standaard Dockerfile die is gemaakt met de functionaliteit Docker-ondersteuning toevoegen. In het bijzonder variëren de laatste stappen door geen nieuwe WORKDIR op te geven die het pad naar de app ENTRYPOINTzou wijzigen.

De voorgaande Dockerfile-stappen omvatten:

  • De basisafbeelding mcr.microsoft.com/dotnet/sdk:7.0 instellen als de alias build-env.
  • De inhoud kopiëren en de .NET-app publiceren:
    • De app wordt gepubliceerd met behulp van de dotnet publish opdracht.
  • Labels toepassen op de container.
  • Het herstructureren van het .NET SDK-beeld van mcr.microsoft.com/dotnet/sdk:7.0
  • Het kopiëren van de gepubliceerde build-uitvoer van de build-env.
  • Het beginpunt definiëren, dat door dotnet /DotNet.GitHubAction.dll wordt gedelegeerd.

Aanbeveling

De MCR in mcr.microsoft.com staat voor 'Microsoft Container Registry' en is de gesyndiceerde containercatalogus van Microsoft van de officiële Docker-hub. Zie de containercatalogus van Microsoft syndicates voor meer informatie.

Waarschuwing

Als u een global.json-bestand gebruikt om de SDK-versie vast te maken, moet u expliciet naar die versie verwijzen in uw Dockerfile. Als u bijvoorbeeld global.json hebt gebruikt om de SDK-versie 5.0.300 te vergrendelen, moet uw Dockerfilemcr.microsoft.com/dotnet/sdk:5.0.300 gebruiken. Dit voorkomt dat de GitHub Actions worden onderbroken wanneer er een nieuwe kleine revisie wordt uitgebracht.

Actie-invoer en -uitvoer definiëren

In de sectie De app verkennen hebt u meer geleerd over de ActionInputs klas. Dit object vertegenwoordigt de invoer voor de GitHub-actie. GitHub moet een action.yml bestand in de hoofdmap van de opslagplaats hebben om te kunnen herkennen dat de opslagplaats een GitHub Action is.

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

In het voorgaande action.yml bestand wordt het volgende gedefinieerd:

  • De name en description van de GitHub Action
  • De branding, die wordt gebruikt in de GitHub Marketplace om uw actie unieker te identificeren
  • De inputs, die een-op-een overeenkomt met de ActionInputs klasse
  • De outputs, waarnaar wordt geschreven in de Program en wordt gebruikt als onderdeel van werkstroomsamenstelling
  • Het runs knooppunt, dat GitHub vertelt dat de app een docker toepassing is en welke argumenten aan de app moeten worden doorgegeven

Zie De syntaxis van metagegevens voor GitHub Actions voor meer informatie.

Vooraf gedefinieerde omgevingsvariabelen

Met GitHub Actions krijgt u standaard veel omgevingsvariabelen . De variabele GITHUB_REF bevat bijvoorbeeld altijd een verwijzing naar de vertakking of tag die de werkstroom heeft geactiveerd. GITHUB_REPOSITORY heeft de naam van de eigenaar en de opslagplaats, dotnet/docsbijvoorbeeld.

U moet de vooraf gedefinieerde omgevingsvariabelen verkennen en dienovereenkomstig gebruiken.

Werkstroomsamenstelling

Nu de .NET-app is gecontaineriseerd en de actie-invoer en -uitvoer zijn gedefinieerd, kunt u de actie gebruiken. GitHub Actions hoeven niet te worden gepubliceerd in de GitHub Marketplace om te worden gebruikt. Werkstromen worden gedefinieerd in de map .github/workflows van een opslagplaats als YAML-bestanden.

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

Belangrijk

Voor in containers geplaatste GitHub Actions moet u gebruiken runs-on: ubuntu-latest. Zie Werkstroomsyntaxis jobs.<job_id>.runs-onvoor meer informatie.

In het voorgaande YAML-werkstroombestand worden drie primaire knooppunten gedefinieerd:

  • De name werkstroom. Deze naam wordt ook gebruikt bij het maken van een werkstroomstatusbadge.
  • Het on knooppunt definieert wanneer en hoe de actie wordt geactiveerd.
  • Het jobs knooppunt bevat een overzicht van de verschillende taken en stappen binnen elke taak. Afzonderlijke stappen gebruiken GitHub Actions.

Zie Uw eerste werkstroom maken voor meer informatie.

Focus op het steps knooppunt, de samenstelling is duidelijker:

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

De jobs.steps vertegenwoordigt de werkstroomsamenstelling. Stappen worden zodanig ingedeeld dat ze opeenvolgend, communicatief en samenstelbaar zijn. Met verschillende GitHub Actions die stappen vertegenwoordigen, elk met invoer en uitvoer, kunnen werkstromen worden samengesteld.

In de voorgaande stappen kunt u het volgende bekijken:

  1. De opslagplaats is uitgecheckt.

  2. Er wordt een bericht weergegeven in het workflowlogboek wanneer het handmatig wordt uitgevoerd.

  3. Een stap die is geïdentificeerd als dotnet-code-metrics:

    • uses: dotnet/samples/github-actions/DotNet.GitHubAction@main is de locatie van de gecontaineriseerde .NET-app in deze instructie.
    • env maakt een omgevingsvariabele "GREETING"die wordt afgedrukt in de uitvoering van de app.
    • with specificeert elk van de vereiste invoer voor acties.
  4. Een voorwaardelijke stap, genaamd Create pull request, wordt uitgevoerd wanneer de stap dotnet-code-metrics een uitvoerparameter updated-metrics specificeert met een waarde van true.

Belangrijk

Met GitHub kunt u versleutelde geheimen maken. Geheimen kunnen worden gebruikt in werkstroomsamenstelling, met behulp van de ${{ secrets.SECRET_NAME }} syntaxis. In de context van een GitHub Action is er een GitHub-token dat standaard automatisch wordt ingevuld: ${{ secrets.GITHUB_TOKEN }} Zie context- en expressiesyntaxis voor GitHub Actions voor meer informatie.

Alles samenvoegen

De GitHub-opslagplaats dotnet/samples is de thuisbasis van veel .NET-voorbeeldbroncodeprojecten, waaronder de app in deze zelfstudie.

Het gegenereerde bestand CODE_METRICS.md is bevaarbaar. Dit bestand vertegenwoordigt de hiërarchie van de projecten die worden geanalyseerd. Elk project heeft een sectie op het hoogste niveau en een emoji die de algehele status van de hoogste cyclomatische complexiteit voor geneste objecten vertegenwoordigt. Wanneer u door het bestand navigeert, worden in elke sectie inzoommogelijkheden weergegeven met een samenvatting van elk gebied. Markdown heeft samenvouwbare secties als een extra handige functie.

De hiërarchie loopt van:

  • Projectbestand naar assemblage
  • Assembly naar naamruimte
  • Naamruimte naar benoemd type
  • Elk benoemd type heeft een tabel en elke tabel heeft:
    • Koppelingen naar regelnummers voor velden, methoden en eigenschappen
    • Afzonderlijke classificaties voor metrische codegegevens

In actie

De werkstroom geeft aan dat on een push naar de main vertakking, de actie wordt geactiveerd om uit te voeren. Wanneer het wordt uitgevoerd, rapporteert het tabblad Acties in GitHub de livelogboekstream van de uitvoering. Hier volgt een voorbeeldlogboek van de .NET code metrics uitvoering:

Metrische .NET-codegegevens - GitHub Actions-logboek

Prestatieverbeteringen

Als u het voorbeeld hebt gevolgd, hebt u misschien gemerkt dat elke keer dat deze actie wordt gebruikt, er een docker build voor dat image wordt uitgevoerd. Dus, voor elke trigger is enige tijd nodig om de container op te bouwen voordat deze wordt uitgevoerd. Voordat u uw GitHub Actions publiceert in de marketplace, moet u het volgende doen:

  1. Bouw automatisch de Docker-image
  2. Push het Docker-image naar het GitHub-containerregister (of een ander openbaar containerregister)
  3. Wijzig de actie om de image niet te bouwen, maar om een image uit een openbare registry te gebruiken.
# 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!!

Zie GitHub Docs voor meer informatie: Werken met het containerregister.

Zie ook

Volgende stappen