Merge pull request '子赛题三 - Java-Gatekeeper 端到端自动化质量门禁工作流' (#131) from xxxx12/gitlink-cli:master into master
This commit is contained in:
commit
81889290d1
|
|
@ -0,0 +1,36 @@
|
|||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### OS ###
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
这是子赛题三的端到端 Java 看门人工作流示例。
|
||||
|
||||
|
||||
|
||||
GitLink Gatekeeper - 基于 Java 的 AI 智能代码质量门禁系统
|
||||
|
||||
|
||||
|
||||
项目定位与核心价值)
|
||||
|
||||
本项目专为 2026 GitLink 自动化工作流大赛-子赛题三打造。
|
||||
|
||||
在开源社区中,维护者往往需要花费大量时间评审 Pull Request (PR)。`GitLink Gatekeeper` 是一个端到端的自动化解决方案,它将 `gitlink-cli` 的核心能力与 Java 强大的后端控制逻辑相结合,充当社区的\*\*“代码质量看门人”。
|
||||
|
||||
|
||||
|
||||
核心价值:
|
||||
|
||||
1\. 全自动审计:无需人工干预,一键自动拉取 PR 元数据并调用核心 AI 能力生成评审报告。
|
||||
|
||||
2\. 高鲁棒性分流:具备状态感知能力,针对已关闭/已合并的 PR 自动执行安全降级策略(转为日志留痕),防止接口报错或污染历史代码。
|
||||
|
||||
3\. 环境自适应:内置底层环境变量清洗与字符流重映射,彻底解决 Windows/Linux 跨平台执行时的网络代理死锁及控制台乱码问题。
|
||||
|
||||
|
||||
|
||||
\---
|
||||
|
||||
|
||||
|
||||
工作流架构设计(3个及以上命令串联)
|
||||
|
||||
|
||||
|
||||
本工作流通过 Java 进程控制,完美串联了以下能力链条:
|
||||
|
||||
1\. 步骤一:元数据侦测 —— 调起 `gitlink-cli pr +view` 抓取目标 PR 的实时状态与技术描述文本。
|
||||
|
||||
2\. 步骤二:AI 智能评审 —— 驱动 `gitlink-cli workflow +pr-summary` 核心 Skill 对代码 Diff 进行深度分析,生成 Markdown 格式的摘要。
|
||||
|
||||
3\. 步骤三:动态决策回写 —— 解析步骤一状态,若 PR 开放则调用 `gitlink-cli pr +review` 自动回写 Approved 评论;若 PR 已关闭,则优雅回退至本地日志记录。
|
||||
|
||||
|
||||
|
||||
\---
|
||||
|
||||
|
||||
|
||||
快速开始与复现指南
|
||||
|
||||
|
||||
|
||||
为了保证评委与维护者能够 \*\*100% 完美复现\*\* 本工作流,请遵循以下步骤:
|
||||
|
||||
|
||||
|
||||
1\. 前置环境准备
|
||||
|
||||
Java 环境:JDK 17 或更高版本
|
||||
|
||||
构建工具:Apache Maven 3.6+
|
||||
|
||||
官方工具:已全局安装 `gitlink-cli` 并且已通过 `gitlink-cli auth login` 成功登录您的账户。
|
||||
|
||||
|
||||
|
||||
2\. 搬运与配置
|
||||
|
||||
确保本项目放置在官方主仓库的指定路径下:
|
||||
|
||||
`gitlink-cli/examples/workflows/java-gatekeeper/`
|
||||
|
||||
|
||||
|
||||
3\. 一键运行命令
|
||||
|
||||
打开终端,切换到当前工作流根目录,直接执行以下 Maven 命令即可拉起工作流:
|
||||
|
||||
```cmd
|
||||
|
||||
mvn spring-boot:run
|
||||
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.gatekeeper</groupId>
|
||||
<artifactId>gitlink-gatekeeper</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>GitLink Gatekeeper</name>
|
||||
<description>GitLink 自动化工作流大赛 - 子赛题三</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.gatekeeper;
|
||||
|
||||
import com.gatekeeper.service.WorkflowService;
|
||||
import com.gatekeeper.utils.CommandLineExecutor.CommandExecutionException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class GatekeeperApplication implements CommandLineRunner {
|
||||
|
||||
private static final String SEPARATOR = "============================================================";
|
||||
|
||||
@Autowired
|
||||
private WorkflowService workflowService;
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(GatekeeperApplication.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
System.out.println(SEPARATOR);
|
||||
System.out.println(" GitLink Gatekeeper · PR 自动化评审工作流");
|
||||
System.out.println(" 测试目标:Gitlink / forgeplus · PR #42");
|
||||
System.out.println(SEPARATOR);
|
||||
|
||||
try {
|
||||
String summaryReport = workflowService.runPrReviewWorkflow(
|
||||
"Gitlink", "forgeplus", "42");
|
||||
|
||||
System.out.println();
|
||||
System.out.println("【工作流摘要预览】");
|
||||
System.out.println(summaryReport);
|
||||
System.out.println();
|
||||
System.out.println(SEPARATOR);
|
||||
System.out.println(" 工作流执行成功,已结束");
|
||||
System.out.println(SEPARATOR);
|
||||
} catch (CommandExecutionException e) {
|
||||
System.err.println();
|
||||
System.err.println("【命令执行失败】" + e.getMessage());
|
||||
if (e.getCommandLine() != null) {
|
||||
System.err.println("命令:" + e.getCommandLine());
|
||||
}
|
||||
e.printStackTrace();
|
||||
printFailureFooter();
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.err.println();
|
||||
System.err.println("【参数错误】" + e.getMessage());
|
||||
printFailureFooter();
|
||||
} catch (Exception e) {
|
||||
System.err.println();
|
||||
System.err.println("【工作流执行异常】" + e.getMessage());
|
||||
e.printStackTrace();
|
||||
printFailureFooter();
|
||||
}
|
||||
}
|
||||
|
||||
private static void printFailureFooter() {
|
||||
System.err.println();
|
||||
System.err.println(SEPARATOR);
|
||||
System.err.println(" 工作流执行失败,已结束");
|
||||
System.err.println(SEPARATOR);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package com.gatekeeper.service;
|
||||
|
||||
import com.gatekeeper.utils.CommandLineExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* GitLink 自动化 PR 评审工作流:通过 {@code gitlink-cli} 串联「查看 PR → 摘要 → 回写评论」。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class WorkflowService {
|
||||
|
||||
/** 本机 npm 全局安装的 gitlink-cli 可执行文件(Windows .cmd 脚本) */
|
||||
private static final String GITLINK_CLI =
|
||||
"C:\\Users\\jmqxx\\AppData\\Roaming\\npm\\gitlink-cli.cmd";
|
||||
|
||||
/**
|
||||
* 执行完整的 PR 自动化评审工作流。
|
||||
*
|
||||
* @param owner 仓库所有者(组织或用户)
|
||||
* @param repo 仓库名称
|
||||
* @param prNumber Pull Request 编号
|
||||
* @return 步骤二生成的 Markdown 摘要报告 {@code summaryReport}
|
||||
*/
|
||||
public String runPrReviewWorkflow(String owner, String repo, String prNumber) {
|
||||
validateParams(owner, repo, prNumber);
|
||||
log.info("开始执行 PR 自动化评审工作流:owner={}, repo={}, prNumber={}", owner, repo, prNumber);
|
||||
|
||||
// 步骤一:获取 PR 状态(使用 --id,避免 -i 在 cmd /c 链路中被误解析)
|
||||
log.info("[步骤一] 正在获取 PR 状态…");
|
||||
String prViewOutput = viewPullRequest(owner, repo, prNumber);
|
||||
if (isCliFailure(prViewOutput)) {
|
||||
log.warn("[步骤一] PR 查询可能失败,仍将尝试后续步骤。输出:\n{}", prViewOutput);
|
||||
} else {
|
||||
log.info("[步骤一] PR 状态查询完成,输出如下:\n{}", prViewOutput);
|
||||
}
|
||||
|
||||
// 步骤二:优先 workflow +pr-summary;本地 CLI 不支持时回退为 pr +view 文本摘要
|
||||
log.info("[步骤二] 正在生成 PR 摘要报告…");
|
||||
String summaryReport = generateSummaryReport(owner, repo, prNumber, prViewOutput);
|
||||
if (!StringUtils.hasText(summaryReport)) {
|
||||
log.warn("[步骤二] 摘要报告为空,后续仍将尝试以评论形式回写");
|
||||
summaryReport = buildFallbackSummary(prViewOutput);
|
||||
} else {
|
||||
log.info("[步骤二] 摘要报告已就绪,字符数={}", summaryReport.length());
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("[步骤二] 摘要报告内容:\n{}", summaryReport);
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤三:本地 CLI 无 pr +review,使用 pr +comment 将摘要回写为 PR 评论
|
||||
log.info("[步骤三] 正在通过 pr +comment 回写评审摘要…");
|
||||
String commentOutput = postPullRequestComment(owner, repo, prNumber, summaryReport);
|
||||
log.info("[步骤三] 评论已提交,CLI 输出如下:\n{}", commentOutput);
|
||||
log.info("PR 自动化评审工作流执行完毕:owner={}, repo={}, prNumber={}", owner, repo, prNumber);
|
||||
|
||||
return summaryReport;
|
||||
}
|
||||
|
||||
/** 调用 {@code pr +view},PR 编号使用官方长参数 {@code --id}(等价于 --number)。 */
|
||||
private String viewPullRequest(String owner, String repo, String prNumber) {
|
||||
return CommandLineExecutor.run(
|
||||
GITLINK_CLI, "pr", "+view",
|
||||
"--owner", owner,
|
||||
"--repo", repo,
|
||||
"--id", prNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成摘要:先尝试 {@code workflow +pr-summary};失败或不可用时用步骤一的 PR 详情构造 Markdown 回退摘要。
|
||||
*/
|
||||
private String generateSummaryReport(
|
||||
String owner, String repo, String prNumber, String prViewOutput) {
|
||||
try {
|
||||
log.info("[步骤二] 尝试官方命令 workflow +pr-summary …");
|
||||
String aiSummary = CommandLineExecutor.run(
|
||||
GITLINK_CLI, "workflow", "+pr-summary",
|
||||
"--owner", owner,
|
||||
"--repo", repo,
|
||||
"--number", prNumber,
|
||||
"--format", "markdown");
|
||||
if (!isCliFailure(aiSummary) && StringUtils.hasText(aiSummary)) {
|
||||
log.info("[步骤二] 已使用 workflow +pr-summary 生成摘要");
|
||||
return aiSummary;
|
||||
}
|
||||
log.warn("[步骤二] workflow +pr-summary 无有效输出,启用 pr +view 回退。原因片段:{}",
|
||||
truncateForLog(aiSummary, 200));
|
||||
} catch (Exception e) {
|
||||
log.warn("[步骤二] workflow +pr-summary 调用异常,启用 pr +view 回退:{}", e.getMessage());
|
||||
}
|
||||
return buildFallbackSummary(prViewOutput);
|
||||
}
|
||||
|
||||
/** 使用 {@code pr +comment} 将摘要作为评论发表(当前 CLI 版本不支持 pr +review)。 */
|
||||
private String postPullRequestComment(
|
||||
String owner, String repo, String prNumber, String summaryReport) {
|
||||
String body = StringUtils.hasText(summaryReport)
|
||||
? summaryReport
|
||||
: buildFallbackSummary("");
|
||||
return CommandLineExecutor.run(
|
||||
GITLINK_CLI, "pr", "+comment",
|
||||
"--owner", owner,
|
||||
"--repo", repo,
|
||||
"--id", prNumber,
|
||||
"--body", body);
|
||||
}
|
||||
|
||||
private static String buildFallbackSummary(String prViewOutput) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("## PR 自动化摘要(本地回退)").append(System.lineSeparator()).append(System.lineSeparator());
|
||||
if (StringUtils.hasText(prViewOutput)) {
|
||||
builder.append(prViewOutput.trim());
|
||||
} else {
|
||||
builder.append("_未能获取 PR 详情,请检查 owner/repo/PR 编号及 gitlink-cli 登录状态。_");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/** 根据 CLI 合并输出判断命令是否执行失败(不抛异常,仅用于回退逻辑)。 */
|
||||
private static boolean isCliFailure(String output) {
|
||||
if (!StringUtils.hasText(output)) {
|
||||
return true;
|
||||
}
|
||||
String normalized = output.toLowerCase();
|
||||
return normalized.contains("unknown command")
|
||||
|| normalized.contains("unknown shorthand flag")
|
||||
|| normalized.contains("unknown flag")
|
||||
|| normalized.contains("error:")
|
||||
|| normalized.contains("failed");
|
||||
}
|
||||
|
||||
private static String truncateForLog(String text, int maxLen) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
String trimmed = text.replace('\n', ' ').trim();
|
||||
return trimmed.length() <= maxLen ? trimmed : trimmed.substring(0, maxLen) + "…";
|
||||
}
|
||||
|
||||
private static void validateParams(String owner, String repo, String prNumber) {
|
||||
if (!StringUtils.hasText(owner)) {
|
||||
throw new IllegalArgumentException("owner 不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(repo)) {
|
||||
throw new IllegalArgumentException("repo 不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(prNumber)) {
|
||||
throw new IllegalArgumentException("prNumber 不能为空");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package com.gatekeeper.utils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.UnsupportedCharsetException;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 命令行执行工具:通过 {@link ProcessBuilder} 在子进程中静默运行外部命令,
|
||||
* 并合并捕获标准输出与错误输出。
|
||||
*/
|
||||
public final class CommandLineExecutor {
|
||||
|
||||
/**
|
||||
* 子进程输出编码:Windows 控制台多为 GBK,避免中文乱码;其他系统使用 JVM 默认编码。
|
||||
*/
|
||||
private static final Charset OUTPUT_CHARSET = resolveOutputCharset();
|
||||
|
||||
private CommandLineExecutor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在系统后台静默执行一条外部命令,将标准输出与错误输出合并为一段文本返回。
|
||||
*
|
||||
* <p>示例:{@code run("C:\\path\\to\\gitlink-cli.cmd", "user", "+me")}</p>
|
||||
* <p>Windows 下若首参数为 {@code .cmd}/{@code .bat},将自动通过 {@code cmd.exe /c} 启动。</p>
|
||||
*
|
||||
* @param command 可执行文件路径或名称,以及后续参数(不可为空)
|
||||
* @return 子进程的标准输出与错误输出的完整文本(无输出时返回空字符串)
|
||||
* @throws IllegalArgumentException 未传入任何命令片段时
|
||||
* @throws CommandExecutionException 进程无法启动、被中断或等待结束时发生 I/O 错误时
|
||||
*/
|
||||
public static String run(String... command) {
|
||||
if (command == null || command.length == 0) {
|
||||
throw new IllegalArgumentException("命令参数不能为空");
|
||||
}
|
||||
for (int i = 0; i < command.length; i++) {
|
||||
if (command[i] == null || command[i].isBlank()) {
|
||||
throw new IllegalArgumentException("命令参数第 " + (i + 1) + " 段不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
String[] processCommand = normalizeCommandForWindows(command);
|
||||
String commandLine = Arrays.stream(processCommand).collect(Collectors.joining(" "));
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(processCommand);
|
||||
// 清空代理环境变量,避免子进程继承终端代理导致连接超时
|
||||
processBuilder.environment().put("http_proxy", "");
|
||||
processBuilder.environment().put("https_proxy", "");
|
||||
processBuilder.environment().put("all_proxy", "");
|
||||
// 不向控制台继承标准流,在后台以管道方式静默执行
|
||||
processBuilder.redirectErrorStream(true);
|
||||
|
||||
Process process;
|
||||
try {
|
||||
process = processBuilder.start();
|
||||
} catch (IOException e) {
|
||||
throw new CommandExecutionException(
|
||||
"无法启动命令进程:" + commandLine, commandLine, e);
|
||||
}
|
||||
|
||||
try {
|
||||
String output = readAllText(process.getInputStream());
|
||||
process.waitFor();
|
||||
// 无论退出码是否为 0,均返回已捕获的标准输出与错误输出合并文本
|
||||
return output;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
process.destroyForcibly();
|
||||
throw new CommandExecutionException(
|
||||
"等待命令执行结果被中断:" + commandLine, commandLine, e);
|
||||
} catch (IOException e) {
|
||||
process.destroyForcibly();
|
||||
throw new CommandExecutionException(
|
||||
"读取命令输出时发生 I/O 错误:" + commandLine, commandLine, e);
|
||||
} finally {
|
||||
if (process.isAlive()) {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Charset resolveOutputCharset() {
|
||||
if (isWindows()) {
|
||||
try {
|
||||
return Charset.forName("GBK");
|
||||
} catch (UnsupportedCharsetException ignored) {
|
||||
return Charset.defaultCharset();
|
||||
}
|
||||
}
|
||||
return Charset.defaultCharset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows 下通过 {@code cmd.exe /c} 执行批处理脚本,避免 ProcessBuilder 无法直接启动 {@code .cmd}/{@code .bat}。
|
||||
*/
|
||||
private static String[] normalizeCommandForWindows(String... command) {
|
||||
if (!isWindows()) {
|
||||
return command;
|
||||
}
|
||||
String executable = command[0].toLowerCase();
|
||||
if (!executable.endsWith(".cmd") && !executable.endsWith(".bat")) {
|
||||
return command;
|
||||
}
|
||||
String[] wrapped = new String[command.length + 2];
|
||||
wrapped[0] = resolveWindowsCmdExe();
|
||||
wrapped[1] = "/c";
|
||||
System.arraycopy(command, 0, wrapped, 2, command.length);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
private static boolean isWindows() {
|
||||
String osName = System.getProperty("os.name", "");
|
||||
return osName.toLowerCase().contains("win");
|
||||
}
|
||||
|
||||
/** 优先使用系统 ComSpec,回退到默认 cmd.exe 绝对路径 */
|
||||
private static String resolveWindowsCmdExe() {
|
||||
String comSpec = System.getenv("ComSpec");
|
||||
if (comSpec != null && !comSpec.isBlank()) {
|
||||
return comSpec;
|
||||
}
|
||||
return "C:\\Windows\\System32\\cmd.exe";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从子进程输入流中读取全部文本(已合并 stderr 时即为标准输出 + 错误输出)。
|
||||
*/
|
||||
private static String readAllText(InputStream inputStream) throws IOException {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(inputStream, OUTPUT_CHARSET))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (!builder.isEmpty()) {
|
||||
builder.append(System.lineSeparator());
|
||||
}
|
||||
builder.append(line);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令执行失败时抛出的运行时异常,携带便于排查的完整命令行信息。
|
||||
*/
|
||||
public static class CommandExecutionException extends RuntimeException {
|
||||
|
||||
private final String commandLine;
|
||||
|
||||
public CommandExecutionException(
|
||||
String message, String commandLine, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.commandLine = commandLine;
|
||||
}
|
||||
|
||||
/** 实际执行的完整命令行(参数以空格拼接,便于日志排查) */
|
||||
public String getCommandLine() {
|
||||
return commandLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
spring.application.name=gitlink-gatekeeper
|
||||
Loading…
Reference in New Issue