Esercitazione: Creare un'azione GitHub con .NET

Informazioni su come creare un'app .NET che può essere usata come GitHub Action. GitHub Actions abilita l'automazione e la composizione del flusso di lavoro. Con GitHub Actions è possibile compilare, testare e distribuire il codice sorgente da GitHub. Inoltre, le azioni espongono la possibilità di interagire a livello di codice con i problemi, creare richieste pull, eseguire revisioni del codice e gestire rami. Per altre informazioni sull'integrazione continua con GitHub Actions, vedere Compilazione e test di .NET.

In questa esercitazione apprenderai a:

  • Preparare un'app .NET per GitHub Actions
  • Definire input e output dell'azione
  • Comporre un flusso di lavoro

Prerequisiti

Finalità dell'app

L'app in questa esercitazione esegue l'analisi delle metriche del codice in base a:

  • Analisi e individuazione dei file di progetto *.csproj e *.vbproj .

  • Analisi del codice sorgente individuato all'interno di questi progetti per:

    • Complessità ciclomatica
    • Indice di gestibilità
    • Profondità dell'ereditarietà
    • Accoppiamento tra classi
    • Numero di righe di codice sorgente
    • Righe approssimative di codice eseguibile
  • Creazione (o aggiornamento) di un file CODE_METRICS.md .

L'app non è responsabile della creazione di una richiesta pull con le modifiche apportate al file CODE_METRICS.md. Queste modifiche vengono gestite come parte della composizione del flusso di lavoro.

I riferimenti al codice sorgente in questa esercitazione hanno parti dell'app omesse per brevità. Il codice completo dell'app è disponibile in GitHub.

Esplora l'app

L'app console .NET usa il CommandLineParser pacchetto NuGet per analizzare gli argomenti nell'oggetto 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 di input dell'azione precedente definisce diversi input necessari per l'esecuzione corretta dell'app. Il costruttore scriverà il valore della "GREETINGS" variabile di ambiente, se disponibile nell'ambiente di esecuzione corrente. Le Name proprietà e Branch vengono analizzate e assegnate dall'ultimo segmento di una "/" stringa delimitata.

Con la classe di input dell'azione definita, concentrarsi sul file 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);
}

Il Program file è semplificato per brevità, per esplorare l'origine di esempio completa, vedere Program.cs. I meccanismi sul posto illustrano il codice boilerplate necessario per l'uso:

È possibile usare riferimenti a progetti o pacchetti esterni e registrarli con l'inserimento delle dipendenze. Get<TService> è una funzione locale statica, che richiede l'istanza IHost e viene usata per risolvere i servizi necessari. Con il CommandLine.Parser.Default singleton, l'app ottiene un'istanza argsparser da . Quando gli argomenti non possono essere analizzati, l'app viene chiusa con un codice di uscita diverso da zero. Per altre informazioni, vedere Impostazione dei codici di uscita per le azioni.

Quando gli argomenti vengono analizzati correttamente, l'app è stata chiamata correttamente con gli input necessari. In questo caso viene effettuata una chiamata alla funzionalità StartAnalysisAsync primaria.

Per scrivere valori di output, è necessario seguire il formato riconosciuto da GitHub Actions: Impostazione di un parametro di output.

Preparare l'app .NET per GitHub Actions

GitHub Actions supporta due varianti dello sviluppo di app, entrambe

  • JavaScript (facoltativamente TypeScript)
  • Contenitore Docker (qualsiasi app eseguita in Docker)

L'ambiente virtuale in cui è ospitato GitHub Action può avere o meno installato .NET. Per informazioni su ciò che è preinstallato nell'ambiente di destinazione, vedere Ambienti virtuali di GitHub Actions. Anche se è possibile eseguire i comandi dell'interfaccia della riga di comando di .NET dai flussi di lavoro di GitHub Actions, per un'interfaccia della riga di comando più funzionante. GitHub Action basato su NET, è consigliabile inserire in contenitori l'app. Per altre informazioni, vedere Containerize a .NET app (Containerize a .NET app).

Dockerfile

Un Dockerfile è un set di istruzioni per compilare un'immagine. Per le applicazioni .NET, il Dockerfile si trova in genere nella radice della directory accanto a un file di soluzione.

# 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

L'app .NET in questa esercitazione si basa su .NET SDK come parte delle funzionalità. Il Dockerfile crea un nuovo set di livelli Docker, indipendentemente da quelli precedenti. Inizia da zero con l'immagine DELL'SDK e aggiunge l'output di compilazione del set di livelli precedente. Per le applicazioni che non richiedono .NET SDK come parte della relativa funzionalità, devono invece basarsi solo sul runtime .NET. Ciò riduce notevolmente le dimensioni dell'immagine.

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

Avviso

Prestare particolare attenzione a ogni passaggio all'interno del Dockerfile, in quanto differisce dal Dockerfile standard creato dalla funzionalità "add docker support". In particolare, gli ultimi passaggi variano non specificando un nuovo WORKDIR che cambierebbe il percorso dell'app ENTRYPOINT.

I passaggi di Dockerfile precedenti includono:

  • Impostazione dell'immagine di base da mcr.microsoft.com/dotnet/sdk:7.0 come alias build-env.
  • Copia del contenuto e pubblicazione dell'app .NET:
  • Applicazione di etichette al contenitore.
  • Inoltro dell'immagine .NET SDK da mcr.microsoft.com/dotnet/sdk:7.0
  • Copia dell'output di compilazione pubblicato da build-env.
  • Definizione del punto di ingresso, che delega a dotnet /DotNet.GitHubAction.dll.

Suggerimento

McR è mcr.microsoft.com l'acronimo di "Microsoft Container Registry" ed è il catalogo dei contenitori diffuso di Microsoft dall'hub Docker ufficiale. Per altre informazioni, vedere Catalogo contenitori dei sindacati Microsoft.

Attenzione

Se si usa un file global.json per aggiungere la versione dell'SDK, è necessario fare riferimento in modo esplicito a tale versione nel Dockerfile. Ad esempio, se è stato usato global.json per aggiungere la versione 5.0.300dell'SDK, il Dockerfile deve usare mcr.microsoft.com/dotnet/sdk:5.0.300. Ciò impedisce l'interruzione di GitHub Actions quando viene rilasciata una nuova revisione secondaria.

Definire input e output dell'azione

Nella sezione Esplorare l'app si è appresa la ActionInputs classe . Questo oggetto rappresenta gli input per GitHub Action. Per consentire a GitHub di riconoscere che il repository è un'azione GitHub, è necessario avere un file action.yml nella radice del repository.

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

Il file action.yml precedente definisce:

  • e namedescription di GitHub Action
  • , brandingusato in GitHub Marketplace per identificare in modo più univoco l'azione
  • , inputsche esegue il mapping uno-a-uno con la ActionInputs classe
  • Oggetto outputs, scritto in Program e usato come parte della composizione del flusso di lavoro
  • Nodo runs , che indica a GitHub che l'app è un'applicazione docker e quali argomenti passare

Per altre informazioni, vedere Sintassi dei metadati per GitHub Actions.

Variabili di ambiente predefinite

Con GitHub Actions si otterranno molte variabili di ambiente per impostazione predefinita. Ad esempio, la variabile GITHUB_REF conterrà sempre un riferimento al ramo o al tag che ha attivato l'esecuzione del flusso di lavoro. GITHUB_REPOSITORY ha il nome del proprietario e del repository, dotnet/docsad esempio .

È consigliabile esplorare le variabili di ambiente predefinite e usarle di conseguenza.

Composizione del flusso di lavoro

Con l'app .NET in contenitori e gli input e gli output dell'azione definiti, è possibile usare l'azione. GitHub Actions non deve essere pubblicato in GitHub Marketplace per l'uso. I flussi di lavoro vengono definiti nella directory .github/workflows di un repository come file 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

Per GitHub Actions in contenitori, è necessario usare runs-on: ubuntu-latest. Per altre informazioni, vedere Sintassi jobs.<job_id>.runs-ondel flusso di lavoro.

Il file YAML del flusso di lavoro precedente definisce tre nodi primari:

  • name del flusso di lavoro. Questo nome viene usato anche durante la creazione di una notifica di stato del flusso di lavoro.
  • Il on nodo definisce quando e come viene attivata l'azione.
  • Il jobs nodo descrive i vari processi e passaggi all'interno di ogni processo. I singoli passaggi usano GitHub Actions.

Per altre informazioni, vedere Creazione del primo flusso di lavoro.

Concentrandosi sul steps nodo, la composizione è più ovvia:

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

Rappresenta jobs.steps la composizione del flusso di lavoro. I passaggi vengono orchestrati in modo che siano sequenziali, comunicativi e componibili. Con varie azioni di GitHub Actions che rappresentano i passaggi, ognuno con input e output, i flussi di lavoro possono essere composti.

Nei passaggi precedenti è possibile osservare:

  1. Il repository è estratto.

  2. Un messaggio viene stampato nel log del flusso di lavoro, quando viene eseguito manualmente.

  3. Un passaggio identificato come dotnet-code-metrics:

    • uses: dotnet/samples/github-actions/DotNet.GitHubAction@main è il percorso dell'app .NET in contenitori in questa esercitazione.
    • env crea una variabile "GREETING"di ambiente , che viene stampata nell'esecuzione dell'app.
    • with specifica ogni input dell'azione richiesta.
  4. Un passaggio condizionale, denominato viene eseguito Create pull request quando il dotnet-code-metrics passaggio specifica un parametro di output di updated-metrics con un valore .true

Importante

GitHub consente la creazione di segreti crittografati. I segreti possono essere usati all'interno della composizione del flusso di lavoro, usando la ${{ secrets.SECRET_NAME }} sintassi . Nel contesto di un'azione GitHub è presente un token GitHub popolato automaticamente per impostazione predefinita: ${{ secrets.GITHUB_TOKEN }}. Per altre informazioni, vedere Sintassi di contesto ed espressione per GitHub Actions.

Combinare tutti gli elementi

Il repository GitHub dotnet/samples ospita molti progetti di codice sorgente di esempio .NET, inclusa l'app in questa esercitazione.

Il file CODE_METRICS.md generato è esplorabile. Questo file rappresenta la gerarchia dei progetti analizzati. Ogni progetto ha una sezione di primo livello e un'emoji che rappresenta lo stato complessivo della maggiore complessità ciclomatica per gli oggetti annidati. Mentre si esplora il file, ogni sezione espone opportunità di drill-down con un riepilogo di ogni area. Il markdown include sezioni collapible come ulteriore praticità.

La gerarchia procede da:

  • File di progetto nell'assembly
  • Assembly in spazio dei nomi
  • Spazio dei nomi a tipo denominato
  • Ogni tipo denominato ha una tabella e ogni tabella include:
    • Collegamenti a numeri di riga per campi, metodi e proprietà
    • Classificazioni individuali per le metriche del codice

In azione

Il flusso di lavoro specifica che on un push oggetto nel main ramo , l'azione viene attivata per l'esecuzione. Quando viene eseguita, la scheda Actions in GitHub invierà il flusso di log live dell'esecuzione. Di seguito è riportato un esempio di log dell'esecuzione .NET code metrics :

.NET code metrics - GitHub Actions log

Miglioramenti delle prestazioni

Se è stato seguito l'esempio, si potrebbe aver notato che ogni volta che viene usata questa azione, verrà eseguita una compilazione Docker per tale immagine. Di conseguenza, ogni trigger deve affrontare un po' di tempo per compilare il contenitore prima di eseguirlo. Prima di rilasciare GitHub Actions nel marketplace, è necessario:

  1. (automaticamente) Compilare l'immagine Docker
  2. Eseguire il push dell'immagine Docker nel Registro Contenitori GitHub (o in qualsiasi altro registro contenitori pubblico)
  3. Modificare l'azione per non compilare l'immagine, ma per usare un'immagine da un registro pubblico.
# 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!!

Per altre informazioni, vedere GitHub Docs: Uso del registro contenitori.

Vedi anche

Passaggi successivi