教程:使用 .NET 创建 GitHub 操作

了解如何创建可用作 GitHub 操作的 .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 支持两种应用开发形式:

  • JavaScript(可以选择使用 TypeScript
  • Docker 容器(在 Docker 上运行的任何应用)

托管 GitHub 操作的虚拟环境中不一定安装了 .NET。 有关目标环境中预装的组件的信息,请参阅 GitHub Actions 虚拟环境。 虽然可以从 GitHub Actions 工作流运行 .NET CLI 命令,但为了进一步确保基于 .NET 的 GitHub 操作完全正常运行,我们建议将应用容器化。 有关详细信息,请参阅容器化 .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 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" ]

注意

本教程中的 .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 应用:
  • 将标签应用于容器。
  • mcr.microsoft.com/dotnet/sdk:7.0 中的 .NET SDK 映像重新分层
  • build-env 复制已发布的生成输出。
  • 定义委托给 dotnet /DotNet.GitHubAction.dll 的入口点。

提示

mcr.microsoft.com 中的 MCR 代表“Microsoft Container Registry”,是 Microsoft 官方 Docker 中心的联合容器目录。 有关详细信息,请参阅 Microsoft 联合容器目录

注意

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

定义操作输入和输出

浏览应用部分,你已了解 ActionInputs 类。 此对象表示 GitHub 操作的输入。 要使 GitHub 能够识别存储库是 GitHub 操作,需要将一个 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 文件定义:

  • GitHub 操作的 namedescription
  • branding,在 GitHub 市场中使用,用于帮助以更高的唯一性标识操作
  • inputs,与 ActionInputs 类存在一对一的映射
  • outputs,写入到 Program,用作工作流组合的一部分
  • 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 步骤指定值为 trueupdated-metrics 输出参数时,将运行名为 Create pull request 的条件步骤。

重要

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

将其放在一起

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

生成的 CODE_METRICS.md 文件可导航。 此文件表示它分析的项目的层次结构。 每个项目都有一个顶级节和一个表情符号,该符号表示嵌套对象最高圈复杂度的整体状态。 在该文件中导航时,每个节会显示下钻控件以及每个区域的摘要。 markdown 包含可折叠的节,使操作更方便。

层次结构如下:

  • 项目文件的上一级是程序集
  • 程序集的上一级是命名空间
  • 命名空间的上一级是命名类型
  • 每个命名类型包含一个表,每个表包含:
    • 字段、方法和属性的行号的链接
    • 代码指标的单独分级

操作过程

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

.NET code metrics - GitHub Actions log

性能改进

如果你按照示例进行操作,则可能已注意到,每次使用此操作时,它都会为该映像执行 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 文档:使用容器注册表

另请参阅

后续步骤