了解如何创建可用作 GitHub 操作的 .NET 应用。 使用 GitHub Actions 可以自动化和组合工作流。 借助 GitHub Actions,可以从 GitHub 生成、测试和部署源代码。 此外,操作还公开以编程方式与问题交互、创建拉取请求、执行代码评审和管理分支的功能。 有关使用 GitHub Actions 实现持续集成的详细信息,请参阅生成和测试 .NET。
在本教程中,你将了解如何执行以下操作:
- 为 GitHub Actions 准备 .NET 应用
- 定义操作输入和输出
- 组合工作流
先决条件
- 一个 GitHub 帐户
- .NET 6 SDK 或更高版本
- .NET 集成开发环境 (IDE)
- 任意使用 Visual Studio IDE
应用的目的
本教程中的应用通过以下方式执行代码指标分析:
扫描和发现 *.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" 环境变量值(如果在当前执行环境中可用)。 Name 和 Branch 属性从 "/" 分隔字符串的最后一段进行分析和分配。
定义操作输入类后,请将注意力放在 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@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 应用:
- 应用使用
dotnet publish命令发布。
- 应用使用
- 将标签应用于容器。
- 将
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 操作的
name和description 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 表示步骤,每个步骤都有输入和输出,可以组合工作流。
在前面的步骤中,可以看到:
存储库已签出。
手动运行时,会在工作流日志中输出一条消息。
标识为
dotnet-code-metrics的步骤:uses: dotnet/samples/github-actions/DotNet.GitHubAction@main是本教程中容器化 .NET 应用的位置。env创建环境变量"GREETING",该变量在应用的执行中输出。with指定每个必需的操作输入。
当
dotnet-code-metrics步骤指定值为true的updated-metrics输出参数时,将运行名为Create pull request的条件步骤。
重要
GitHub 允许创建加密的机密。 可以使用 ${{ secrets.SECRET_NAME }} 语法在工作流组合中使用机密。 在 GitHub 操作上下文中,默认情况下会自动填充一个 GitHub 标记:${{ secrets.GITHUB_TOKEN }}。 有关详细信息,请参阅GitHub Actions 的上下文和表达式语法。
将其放在一起
dotnet/samples GitHub 存储库包含许多 .NET 示例源代码项目,其中包括本教程中的应用。
生成的 CODE_METRICS.md 文件可导航。 此文件表示它分析的项目的层次结构。 每个项目都有一个顶级节和一个表情符号,该符号表示嵌套对象最高圈复杂度的整体状态。 在该文件中导航时,每个节会显示下钻控件以及每个区域的摘要。 markdown 包含可折叠的节,使操作更方便。
层次结构如下:
- 项目文件的上一级是程序集
- 程序集的上一级是命名空间
- 命名空间的上一级是命名类型
- 每个命名类型包含一个表,每个表包含:
- 字段、方法和属性的行号的链接
- 代码指标的单独分级
操作过程
工作流将 push 到 main 分支的操作指定为 on,即触发操作的运行。 运行该操作时,GitHub 中的“操作”选项卡将报告其执行的实时日志流。 下面是 .NET code metrics 运行的示例日志:
性能改进
如果你按照示例进行操作,则可能已注意到,每次使用此操作时,它都会为该映像执行 docker build。 因此,在运行该操作之前,每个触发器都需要花费一些时间来生成容器。 将 GitHub Actions 发布到市场之前,应该:
- (自动)生成 Docker 映像
- 将 Docker 映像推送到 GitHub 容器注册表(或任何其他公共容器注册表)
- 将操作更改为不生成映像,而是使用公共注册表中的映像。
# 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 文档:使用容器注册表。
另请参阅
- .NET 通用主机
- .NET 中的依赖关系注入
- 代码度量值
- 使用可自动生成和推送 Docker 映像的工作流,在 .NET 中生成开源 GitHub 操作。
