教程:通过 .NET 创建 GitHub 操作

了解如何创建可用作 GitHub Action 的 .NET 应用程序。 GitHub Actions 支持工作流自动化和组合。 使用 GitHub Actions,可以从 GitHub 生成、测试和部署源代码。 此外,还公开了以编程方式与议题交互、创建拉取请求、进行代码评审和管理分支的能力。 有关与 GitHub Actions 的持续集成的详细信息,请参阅 生成和测试 .NET

本教程中,您将学习如何:

  • 为 GitHub Actions 准备 .NET 应用
  • 定义动作的输入和输出
  • 撰写工作流

先决条件

应用的意图

本教程中的应用通过以下方式执行代码指标分析:

  • 扫描和发现 *.csproj*.vbproj 项目文件。

  • 在这些项目中分析发现的源代码,以便:

    • 气旋复杂性
    • 可维护性索引
    • 继承深度
    • 类耦合
    • 源代码行数
    • 可执行代码的近似行
  • 创建(或更新) CODE_METRICS.md 文件。

该应用 负责创建拉取请求,其中包含 对 CODE_METRICS.md 文件的更改。 这些更改作为 工作流组合的一部分进行管理。

本教程中对源代码的引用为简明起见省略了一些应用程序的部分内容。 GitHub 上提供了完整的应用代码。

浏览应用

.NET 控制台应用使用 CommandLineParser NuGet 包来解析参数到 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]);
        }
    }
}

前面的作输入类定义了多个必需的输入,使应用能够成功运行。 如果当前执行环境中有环境变量值,构造函数将写入 "GREETINGS" 环境变量值。 NameBranch 属性被解析并从 "/" 分隔字符串的最后一段中分配。

使用定义的动作输入类,将焦点放在 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);
}

为了简洁起见,文件 Program 被简化。如需探索完整的示例源,请参阅 Program.cs。 现有的机制展示了使用所需的样板代码:

可以使用外部项目或包引用,并在依赖注入中注册。 Get<TService>这是一个静态本地函数,它需要IHost实例,并用于解析所需的服务。 CommandLine.Parser.Default单例模式时,应用从args获取一个parser实例。 当参数无法分析时,应用会退出并显示非零退出代码。 有关详细信息,请参阅 为操作设置退出代码

成功解析了参数后,应用已被正确地调用,并使用了所需的输入。 在这种情况下,将调用主要功能 StartAnalysisAsync

若要编写输出值,必须遵循 GitHub Actions 识别的格式:设置输出参数

为 GitHub Actions 准备 .NET 应用

GitHub Actions 支持应用开发的两种变体

托管 GitHub Action 的虚拟环境可能已安装或未安装 .NET。 有关目标环境中预安装的内容的信息,请参阅 GitHub Actions 虚拟环境。 虽然可以通过 GitHub Actions 工作流运行 .NET CLI 命令,但为了实现更完善的基于 .NET 的 GitHub Action,我们建议对应用进行容器化。 有关详细信息,请参阅 容器化 .NET 应用

Dockerfile

Dockerfile 是构建镜像的一组指令。 对于 .NET 应用程序, Dockerfile 通常位于解决方案文件旁边的目录的根目录中。

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

注释

本教程中的 .NET 应用依赖于 .NET SDK 作为其功能的一部分。 Dockerfile 创建一组新的 Docker 层,与前面的层无关。 它从头开始使用 SDK 映像,并添加上一组层的生成输出。 对于 不需要 .NET SDK 作为其功能的一部分的应用程序,它们应仅依赖于 .NET 运行时。 这大大减少了图像的大小。

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

警告

请密切关注 Dockerfile 中的每个步骤,因为它与从“添加 docker 支持”功能创建的标准 Dockerfile 不同。 具体而言,由于最后几个步骤未指定新的WORKDIR来更改应用的ENTRYPOINT路径,因此这些步骤有所不同。

前面的 Dockerfile 步骤包括:

  • 将基础映像 mcr.microsoft.com/dotnet/sdk:7.0 设置为别名 build-env
  • 复制内容并发布 .NET 应用:
  • 将标签应用于容器。
  • 从中中继 .NET SDK 映像 mcr.microsoft.com/dotnet/sdk:7.0
  • build-env复制已发布的生成输出。
  • 定义一个入口点,该入口点委托给 dotnet /DotNet.GitHubAction.dll

小窍门

MCR 代表mcr.microsoft.com“Microsoft 容器注册表”,这是来自官方 Docker 中心的 Microsoft 共享容器目录。 有关详细信息,请参阅 Microsoft 发布的容器目录

注意

如果使用 global.json 文件固定 SDK 版本,则应在 Dockerfile 中显式引用该版本。 使用 global.json 固定 SDK 版本5.0.300时,Dockerfile 应使用 mcr.microsoft.com/dotnet/sdk:5.0.300。 这可以防止在发布新的次要修订时中断 GitHub Actions。

定义动作的输入和输出

“探索应用 ”部分中,你了解了该 ActionInputs 类。 此对象表示 GitHub Action 的输入。 要使 GitHub 能够识别存储库是 GitHub Action,需要在存储库的根目录中有action.yml文件。

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

上述 action.yml 文件定义:

  • namedescription 的 GitHub 操作
  • GitHub 市场中branding 用于帮助更加独特地标识你的操作。
  • inputsActionInputs类一对一映射
  • Program中写入的outputs,并用作工作流组合的一部分。
  • runs 节点告知 GitHub 该应用是 docker 应用程序,以及传递给它的参数是什么。

有关详细信息,请参阅 GitHub Actions 的元数据语法

预定义的环境变量

使用 GitHub Actions 时,默认情况下会获得大量 环境变量 。 例如,变量 GITHUB_REF 将始终包含对触发工作流运行的分支或标记的引用。 GITHUB_REPOSITORY 具有所有者和存储库名称,例如 dotnet/docs

应浏览预定义的环境变量并相应地使用它们。

工作流组合

随着 .NET 应用程序容器化,并且已定义操作输入和输出,您已准备好调用该操作。 不需要在 GitHub 市场中发布 GitHub Actions 即可使用。 工作流在存储库的 .github/workflows 目录中定义为 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.'

重要

对于容器化 GitHub Actions,需要使用 runs-on: ubuntu-latest。 有关详细信息,请参阅 工作流语法 jobs.<job_id>.runs-on

前面的工作流 YAML 文件定义了三个主要节点:

  • name的工作流。 此名称也是创建 工作流状态徽章时使用的名称。
  • 节点 on 定义何时以及如何触发动作。
  • jobs 节点概述了每个作业中的各种作业和步骤。 各个步骤都会消耗 GitHub Actions。

有关详细信息,请参阅 创建第一个工作流

专注于 steps 节点,组合更为明显:

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表示工作流组合。 步骤被协调,以使其具备顺序性、可传递性和可组合性。 通过各种 GitHub Actions 表示步骤,每个步骤都有输入和输出,工作流可以组合在一起。

在前面的步骤中,可以观察到:

  1. 存储库 已签出

  2. 手动运行时,会将一条消息打印到工作流日志。

  3. 此步骤标识为 dotnet-code-metrics:

    • uses: dotnet/samples/github-actions/DotNet.GitHubAction@main 是本教程中容器化 .NET 应用的位置。
    • env 创建一个环境变量,该变量 "GREETING"在应用执行中打印。
    • with 指定每个必需的动作输入。
  4. 当步骤dotnet-code-metrics指定输出参数updated-metrics的值为true时,将运行命名为Create pull request的条件步骤。

重要

GitHub 允许创建 加密的机密。 可以通过${{ secrets.SECRET_NAME }}语法在工作流组合中使用密钥。 在 GitHub Action 的上下文中,默认会自动填充一个 GitHub 令牌:${{ secrets.GITHUB_TOKEN }}。 有关详细信息,请参阅 GitHub Actions 的上下文和表达式语法

把所有的东西放在一起

dotnet/samples GitHub 存储库是许多 .NET 示例源代码项目的所在地,包括本教程中的应用

生成的 CODE_METRICS.md 文件可导航。 此文件表示分析的项目的层次结构。 每个项目都有一个顶级部分和一个表情符号,表示嵌套对象的最高气旋复杂性的总体状态。 导航文件时,每个部分都提供深入分析的机会,并包含该区域的简要概述。 Markdown 具有可折叠部分,提供了额外的便利。

层次结构的进展顺序为:

  • 项目文件转化为程序集
  • 程序集转命名空间
  • 命名空间到命名类型
  • 每个命名类型都有一个表,每个表都有:
    • 字段、方法和属性的行号链接
    • 代码指标的单独评分

正在操作中

工作流指定,当on分支有一个pushmain时,将触发该操作运行。 运行时,GitHub 中的 “操作”选项卡将报告其执行过程中的实时日志流。 下面是运行中的 .NET code metrics 示例日志:

.NET 代码指标 - GitHub Actions 日志

性能改进

如果按照示例进行操作,你可能已注意到每次使用此操作时,都会为该镜像执行 docker build 。 因此,在运行之前,每个触发器需要一些时间来构建容器。 将 GitHub Actions 发布到市场之前,应:

  1. (自动)生成 Docker 映像
  2. 将 docker 映像推送到 GitHub 容器注册表(或任何其他公共容器注册表)
  3. 将操作更改为不构建镜像,而是使用公共注册表中的镜像。
# 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!!

有关详细信息,请参阅 GitHub Docs:使用容器注册表

另请参阅

后续步骤