Merge pull request 'feat(workflows): add project bootstrap automation example' (#112) from puygob236/gitlink-cli:project-bootstrap-automation-fork into master
This commit is contained in:
commit
220aa70e51
|
|
@ -0,0 +1 @@
|
|||
outputs/
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
# GitLink 项目一键初始化与协作启动工作流
|
||||
|
||||
面向 GitLink 竞赛子赛题三的端到端自动化工作流示例。
|
||||
|
||||
本项目聚焦开源项目从 0 到可协作状态的启动过程,使用 `gitlink-cli` 串联仓库检查、分支规划、初始 Issue 创建和结果回写等能力,自动生成 README、LICENSE、CI 配置、协作文档、初始化报告和结构化清单。该流程覆盖“项目配置 -> 初始化文件生成 -> GitLink 命令编排 -> 任务落地 -> 报告归档”的完整闭环。
|
||||
|
||||
## 交付物
|
||||
|
||||
- `scripts/bootstrap_project.go`:主工作流入口
|
||||
- `scripts/run_demo.ps1`:一键复现脚本
|
||||
- `examples/sample_project.json`:示例项目配置
|
||||
- `examples/verification_comment_config.json`:真实回写验证配置
|
||||
- `examples/demo_outputs/`:固定示例输出
|
||||
- `docs/workflow-spec.md`:工作流说明文档
|
||||
- `docs/architecture.md`:架构与流程说明
|
||||
- `docs/assets/bootstrap-architecture.svg`:架构图
|
||||
- `docs/quickstart.md`:最短复现路径
|
||||
- `docs/runbook.md`:运行手册
|
||||
- `docs/verification.md`:验证记录
|
||||
- `docs/submission-checklist.md`:赛题要求映射
|
||||
- `scripts/bootstrap_project_test.go`:Go 单元测试
|
||||
|
||||
## 实现语言
|
||||
|
||||
本工作流主实现采用 Go,主要考虑如下:
|
||||
|
||||
- 与 `gitlink-cli` 主仓库技术栈一致,便于维护者阅读、测试和后续集成。
|
||||
- 可直接复用 Go 标准库完成 JSON 配置解析、文件生成、命令编排和单元测试,不引入额外运行时依赖。
|
||||
- Windows、Linux 和 macOS 均可通过 `go run` 复现,便于评审在不同环境中执行。
|
||||
- 对命令执行结果、退出码和结构化日志的处理更接近 `gitlink-cli` 自身工程风格。
|
||||
|
||||
## 运行方式
|
||||
|
||||
进入本目录后执行 dry-run:
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1
|
||||
```
|
||||
|
||||
执行后会生成:
|
||||
|
||||
- `outputs/*_bootstrap_report.md`
|
||||
- `outputs/*_summary.md`
|
||||
- `outputs/*_manifest.json`
|
||||
- `outputs/*_files.json`
|
||||
- `outputs/command_log_*.json`
|
||||
|
||||
输出文件名包含目标仓库和生成时间,格式如下:
|
||||
|
||||
- `{owner}_{repo}_{YYYYMMDD_HHMMSS}_bootstrap_report.md`
|
||||
- `{owner}_{repo}_{YYYYMMDD_HHMMSS}_summary.md`
|
||||
- `{owner}_{repo}_{YYYYMMDD_HHMMSS}_manifest.json`
|
||||
- `{owner}_{repo}_{YYYYMMDD_HHMMSS}_files.json`
|
||||
- `command_log_{YYYYMMDD_HHMMSS}.json`
|
||||
|
||||
例如 `puygob236_gitlink-bootstrap-demo_20260524_080000_bootstrap_report.md`。实际运行时会按当前时间生成新文件名,`examples/demo_outputs/` 中的固定时间戳文件仅作为示例产物。
|
||||
|
||||
如需执行真实 GitLink 写操作,在完成 GitLink 认证并核对目标仓库后使用:
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1 -Apply
|
||||
```
|
||||
|
||||
如需连同仓库创建一起执行:
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1 -Apply -CreateRepo
|
||||
```
|
||||
|
||||
如需把初始化摘要发布到指定 Issue:
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1 -Apply -PublishIssueNumber 1
|
||||
```
|
||||
|
||||
## 工作流串联的 gitlink-cli 调用
|
||||
|
||||
默认配置会规划 7 个 `gitlink-cli` 调用:
|
||||
|
||||
1. `repo +info`
|
||||
2. `branch +list`
|
||||
3. `branch +create`
|
||||
4. `branch +create`
|
||||
5. `issue +create`
|
||||
6. `issue +create`
|
||||
7. `issue +create`
|
||||
|
||||
当指定 `-PublishIssueNumber` 时,会额外追加 `issue +comment`,用于把初始化摘要回写到 GitLink Issue。
|
||||
当指定 `-CreateRepo` 时,会在检查仓库前追加 `repo +create`。
|
||||
|
||||
## 文档索引
|
||||
|
||||
- 工作流说明:`docs/workflow-spec.md`
|
||||
- 架构说明与架构图:`docs/architecture.md`
|
||||
- 复现指南:`docs/quickstart.md`
|
||||
- 运行手册:`docs/runbook.md`
|
||||
- 验证记录:`docs/verification.md`
|
||||
- 提交核对清单:`docs/submission-checklist.md`
|
||||
|
||||
## 场景价值
|
||||
|
||||
- 降低新开源项目启动成本,避免 README、License、CI、初始任务缺失。
|
||||
- 将项目初始化过程结构化,便于团队复用和审计。
|
||||
- 将 `gitlink-cli` 的仓库、分支、Issue 和评论能力串联为可复现方案。
|
||||
- 支持 dry-run 和 apply 两种模式,兼顾演示稳定性和真实落地。
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# 架构说明
|
||||
|
||||
本工作流采用“配置输入 -> 资产生成 -> CLI 编排 -> GitLink 落地 -> 结果归档”的五段式架构。正式架构图见 `docs/assets/bootstrap-architecture.svg`。
|
||||
|
||||

|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["项目配置<br/>sample_project.json"] --> B["资产生成<br/>README / LICENSE / CI / 协作文档"]
|
||||
B --> C["CLI 编排<br/>repo / branch / issue / comment"]
|
||||
C --> D["GitLink 项目空间<br/>仓库 / 分支 / Issue"]
|
||||
D --> E["结果归档<br/>报告 / 摘要 / manifest / 命令日志"]
|
||||
C --> E
|
||||
```
|
||||
|
||||
## 模块职责
|
||||
|
||||
| 模块 | 职责 |
|
||||
| --- | --- |
|
||||
| 配置输入 | 描述项目名称、目标仓库、初始化分支和初始 Issue |
|
||||
| 资产生成 | 生成 README、LICENSE、CI 配置、贡献指南和路线图 |
|
||||
| CLI 编排 | 规划或执行 `gitlink-cli` 命令,串联仓库、分支、Issue 和评论能力 |
|
||||
| GitLink 落地 | 在真实 GitLink 仓库中创建分支、Issue,并可回写摘要 |
|
||||
| 结果归档 | 输出 Markdown 报告、摘要、JSON manifest 和命令日志 |
|
||||
|
||||
## 端到端链路
|
||||
|
||||
1. 读取 `examples/sample_project.json`。
|
||||
2. 生成初始化文件包。
|
||||
3. 规划 `repo +info` 和 `branch +list` 检查目标状态。
|
||||
4. 规划或执行 `branch +create` 创建协作分支。
|
||||
5. 规划或执行 `issue +create` 创建初始任务。
|
||||
6. 可选执行 `issue +comment` 发布初始化摘要。
|
||||
7. 生成报告与命令日志,支撑复现和审计。
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1672" height="941" viewBox="0 0 1672 941" role="img" aria-labelledby="title desc">
|
||||
<title id="title">GitLink Project Bootstrap Automation</title>
|
||||
<desc id="desc">A vector architecture diagram for a configuration-driven GitLink project bootstrap workflow.</desc>
|
||||
<defs>
|
||||
<filter id="cardShadow" x="-16%" y="-16%" width="132%" height="132%">
|
||||
<feDropShadow dx="0" dy="12" stdDeviation="11" flood-color="#0a1b35" flood-opacity="0.11"/>
|
||||
</filter>
|
||||
<style>
|
||||
text { font-family: Arial, "Microsoft YaHei", sans-serif; fill: #0b1736; }
|
||||
.title { font-size: 72px; font-weight: 800; letter-spacing: 0; }
|
||||
.subtitle { font-size: 27px; fill: #46556b; }
|
||||
.card-title { font-size: 34px; font-weight: 800; }
|
||||
.cli-title { font-size: 30px; font-weight: 800; }
|
||||
.title-navy { fill: #071449; }
|
||||
.title-teal { fill: #075e70; }
|
||||
.title-blue { fill: #14579f; }
|
||||
.title-slate { fill: #334155; }
|
||||
.item { font-size: 23px; fill: #101a2f; }
|
||||
.item-condensed { font-size: 22px; fill: #101a2f; }
|
||||
.small { font-size: 20px; fill: #123e21; }
|
||||
.mono { font-family: Consolas, "Courier New", monospace; font-size: 19px; fill: #101a2f; }
|
||||
.hairline { stroke: #a8b5c4; stroke-width: 1.6; }
|
||||
.icon-line { fill: none; stroke-width: 3.5; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.bullet-navy { fill: #071449; }
|
||||
.bullet-teal { fill: #075e70; }
|
||||
.bullet-blue { fill: #14579f; }
|
||||
.bullet-slate { fill: #334155; }
|
||||
.valid { fill: #32833f; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect width="1672" height="941" fill="#fbfcfe"/>
|
||||
<text x="836" y="93" text-anchor="middle" class="title">GitLink Project Bootstrap Automation</text>
|
||||
<text x="836" y="151" text-anchor="middle" class="subtitle">Configuration-driven project initialization, CLI orchestration, GitLink execution, and reproducible evidence</text>
|
||||
|
||||
<!-- Input -->
|
||||
<g transform="translate(48 208)">
|
||||
<rect width="255" height="505" rx="18" fill="#ffffff" stroke="#071449" stroke-width="2.2" filter="url(#cardShadow)"/>
|
||||
<g transform="translate(72 36)" class="icon-line" stroke="#071449">
|
||||
<path d="M10 0h55l28 28v102H10z"/>
|
||||
<path d="M65 0v28h28"/>
|
||||
<text x="50" y="78" text-anchor="middle" font-family="Consolas, 'Courier New', monospace" font-size="34" font-weight="700" fill="#071449" stroke="none">{ }</text>
|
||||
<path d="M32 92h38"/>
|
||||
<path d="M32 111h29"/>
|
||||
</g>
|
||||
<text x="128" y="196" text-anchor="middle" class="card-title title-navy">Input</text>
|
||||
<line x1="23" y1="221" x2="232" y2="221" class="hairline"/>
|
||||
<circle cx="24" cy="259" r="4.5" class="bullet-navy"/><text x="43" y="267" class="item">Project metadata</text>
|
||||
<circle cx="24" cy="305" r="4.5" class="bullet-navy"/><text x="43" y="313" class="item">Repository target</text>
|
||||
<circle cx="24" cy="351" r="4.5" class="bullet-navy"/><text x="43" y="359" class="item">Branches</text>
|
||||
<circle cx="24" cy="397" r="4.5" class="bullet-navy"/><text x="43" y="405" class="item">Initial issues</text>
|
||||
<circle cx="24" cy="443" r="4.5" class="bullet-navy"/><text x="43" y="451" class="mono">sample_project.json</text>
|
||||
</g>
|
||||
|
||||
<!-- Assets -->
|
||||
<g transform="translate(377 208)">
|
||||
<rect width="255" height="505" rx="18" fill="#ffffff" stroke="#075e70" stroke-width="2.2" filter="url(#cardShadow)"/>
|
||||
<g transform="translate(64 35)" class="icon-line" stroke="#075e70">
|
||||
<path d="M2 45h17v84H2z"/>
|
||||
<path d="M22 25h58l23 23v84H22z"/>
|
||||
<path d="M80 25v23h23"/>
|
||||
<path d="M43 6h59l23 23v84h-22"/>
|
||||
<path d="M102 6v23h23"/>
|
||||
<path d="M43 58h44"/>
|
||||
<path d="M43 80h48"/>
|
||||
<path d="M43 102h40"/>
|
||||
</g>
|
||||
<text x="128" y="196" text-anchor="middle" class="card-title title-teal">Assets</text>
|
||||
<line x1="23" y1="221" x2="232" y2="221" class="hairline"/>
|
||||
<circle cx="24" cy="259" r="4.5" class="bullet-teal"/><text x="43" y="267" class="item">README</text>
|
||||
<circle cx="24" cy="305" r="4.5" class="bullet-teal"/><text x="43" y="313" class="item">LICENSE</text>
|
||||
<circle cx="24" cy="351" r="4.5" class="bullet-teal"/><text x="43" y="359" class="item">Go CI</text>
|
||||
<circle cx="24" cy="397" r="4.5" class="bullet-teal"/><text x="43" y="405" class="item">CONTRIBUTING</text>
|
||||
<circle cx="24" cy="443" r="4.5" class="bullet-teal"/><text x="43" y="451" class="item">ROADMAP</text>
|
||||
</g>
|
||||
|
||||
<!-- CLI -->
|
||||
<g transform="translate(705 208)">
|
||||
<rect width="255" height="505" rx="18" fill="#ffffff" stroke="#14579f" stroke-width="2.2" filter="url(#cardShadow)"/>
|
||||
<g transform="translate(79 41)" class="icon-line" stroke="#14579f">
|
||||
<rect x="0" y="0" width="98" height="94" rx="6"/>
|
||||
<path d="M0 29h98"/>
|
||||
<circle cx="17" cy="14" r="3" fill="#14579f" stroke="none"/>
|
||||
<circle cx="33" cy="14" r="3" fill="#14579f" stroke="none"/>
|
||||
<circle cx="49" cy="14" r="3" fill="#14579f" stroke="none"/>
|
||||
<path d="M31 56l18 17-18 18"/>
|
||||
<path d="M61 86h22"/>
|
||||
</g>
|
||||
<text x="128" y="196" text-anchor="middle" class="cli-title title-blue" textLength="218" lengthAdjust="spacingAndGlyphs">CLI Orchestration</text>
|
||||
<line x1="23" y1="221" x2="232" y2="221" class="hairline"/>
|
||||
<circle cx="24" cy="259" r="4.5" class="bullet-blue"/><text x="52" y="267" class="mono">repo +info</text>
|
||||
<circle cx="24" cy="305" r="4.5" class="bullet-blue"/><text x="52" y="313" class="mono">branch +list</text>
|
||||
<circle cx="24" cy="351" r="4.5" class="bullet-blue"/><text x="52" y="359" class="mono">branch +create</text>
|
||||
<circle cx="24" cy="397" r="4.5" class="bullet-blue"/><text x="52" y="405" class="mono">issue +create</text>
|
||||
</g>
|
||||
|
||||
<!-- GitLink -->
|
||||
<g transform="translate(1032 208)">
|
||||
<rect width="255" height="505" rx="18" fill="#ffffff" stroke="#334155" stroke-width="2.2" filter="url(#cardShadow)"/>
|
||||
<g transform="translate(51 40)" class="icon-line" stroke="#334155">
|
||||
<path d="M44 106h68c25 0 42-17 42-41 0-23-16-40-40-42C109 9 93 0 75 0 54 0 37 12 29 31 11 36 0 50 0 68c0 22 18 38 43 38"/>
|
||||
<circle cx="76" cy="36" r="8"/>
|
||||
<circle cx="51" cy="70" r="8"/>
|
||||
<circle cx="102" cy="70" r="8"/>
|
||||
<path d="M72 44L56 63"/>
|
||||
<path d="M80 44l17 20"/>
|
||||
<path d="M76 44v21"/>
|
||||
</g>
|
||||
<text x="128" y="196" text-anchor="middle" class="card-title title-slate">GitLink</text>
|
||||
<line x1="23" y1="221" x2="232" y2="221" class="hairline"/>
|
||||
<circle cx="24" cy="259" r="4.5" class="bullet-slate"/><text x="43" y="267" class="item">Repository state</text>
|
||||
<circle cx="24" cy="305" r="4.5" class="bullet-slate"/><text x="43" y="313" class="item-condensed" textLength="188" lengthAdjust="spacingAndGlyphs">Collaboration branches</text>
|
||||
<circle cx="24" cy="351" r="4.5" class="bullet-slate"/><text x="43" y="359" class="item">Bootstrap issues</text>
|
||||
<circle cx="24" cy="397" r="4.5" class="bullet-slate"/><text x="43" y="405" class="item">Issue comment</text>
|
||||
<circle cx="24" cy="443" r="4.5" class="bullet-slate"/><text x="43" y="445" class="item">Apply mode writes</text><text x="43" y="475" class="item">remotely</text>
|
||||
</g>
|
||||
|
||||
<!-- Evidence -->
|
||||
<g transform="translate(1364 208)">
|
||||
<rect width="255" height="505" rx="18" fill="#ffffff" stroke="#14579f" stroke-width="2.2" filter="url(#cardShadow)"/>
|
||||
<g transform="translate(72 36)" class="icon-line" stroke="#14579f">
|
||||
<path d="M10 0h55l28 28v102H10z"/>
|
||||
<path d="M65 0v28h28"/>
|
||||
<path d="M31 56h43"/>
|
||||
<path d="M31 79h43"/>
|
||||
<path d="M31 102h34"/>
|
||||
</g>
|
||||
<text x="128" y="196" text-anchor="middle" class="card-title title-blue">Evidence</text>
|
||||
<line x1="23" y1="221" x2="232" y2="221" class="hairline"/>
|
||||
<circle cx="24" cy="259" r="4.5" class="bullet-blue"/><text x="43" y="267" class="item">Markdown report</text>
|
||||
<circle cx="24" cy="305" r="4.5" class="bullet-blue"/><text x="43" y="313" class="item">Issue summary</text>
|
||||
<circle cx="24" cy="351" r="4.5" class="bullet-blue"/><text x="43" y="359" class="mono">manifest.json</text>
|
||||
<circle cx="24" cy="397" r="4.5" class="bullet-blue"/><text x="43" y="405" class="mono">files.json</text>
|
||||
<circle cx="24" cy="443" r="4.5" class="bullet-blue"/><text x="43" y="451" class="item">command log</text>
|
||||
</g>
|
||||
|
||||
<polygon points="316,404 343,404 343,386 371,411 343,436 343,418 316,418" fill="#06133a"/>
|
||||
<polygon points="644,404 671,404 671,386 699,411 671,436 671,418 644,418" fill="#06133a"/>
|
||||
<polygon points="972,404 999,404 999,386 1027,411 999,436 999,418 972,418" fill="#06133a"/>
|
||||
<polygon points="1302,404 1329,404 1329,386 1357,411 1329,436 1329,418 1302,418" fill="#06133a"/>
|
||||
|
||||
<g transform="translate(55 777)">
|
||||
<rect width="1562" height="122" rx="14" fill="#f8fbf8" stroke="#a8b8aa" stroke-width="1.8"/>
|
||||
<g transform="translate(32 22)">
|
||||
<rect width="330" height="78" rx="12" fill="#ffffff" stroke="#4f9a57" stroke-width="2"/>
|
||||
<circle cx="49" cy="39" r="23" class="valid"/>
|
||||
<path d="M38 38l8 9 18-20" fill="none" stroke="#ffffff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<text x="96" y="48" class="small" fill="#0f4d22">Go implementation</text>
|
||||
</g>
|
||||
<g transform="translate(400 22)">
|
||||
<rect width="330" height="78" rx="12" fill="#ffffff" stroke="#4f9a57" stroke-width="2"/>
|
||||
<circle cx="49" cy="39" r="23" class="valid"/>
|
||||
<path d="M38 38l8 9 18-20" fill="none" stroke="#ffffff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<text x="96" y="48" class="small" fill="#0f4d22">7 default CLI calls</text>
|
||||
</g>
|
||||
<g transform="translate(768 22)">
|
||||
<rect width="330" height="78" rx="12" fill="#ffffff" stroke="#4f9a57" stroke-width="2"/>
|
||||
<circle cx="49" cy="39" r="23" class="valid"/>
|
||||
<path d="M38 38l8 9 18-20" fill="none" stroke="#ffffff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<text x="96" y="48" class="small" fill="#0f4d22">Reproducible dry-run</text>
|
||||
</g>
|
||||
<g transform="translate(1136 22)">
|
||||
<rect width="370" height="78" rx="12" fill="#ffffff" stroke="#4f9a57" stroke-width="2"/>
|
||||
<circle cx="49" cy="39" r="23" class="valid"/>
|
||||
<path d="M38 38l8 9 18-20" fill="none" stroke="#ffffff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<text x="96" y="48" class="small" fill="#0f4d22">Validated on real GitLink repo</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -0,0 +1,56 @@
|
|||
# 快速开始
|
||||
|
||||
## 1. 进入目录
|
||||
|
||||
```powershell
|
||||
cd examples\workflows\project-bootstrap-automation
|
||||
```
|
||||
|
||||
## 2. 运行 dry-run
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1
|
||||
```
|
||||
|
||||
该命令不会写入 GitLink,只生成初始化材料和命令计划。
|
||||
|
||||
## 3. 查看输出
|
||||
|
||||
```powershell
|
||||
Get-ChildItem outputs
|
||||
```
|
||||
|
||||
重点查看:
|
||||
|
||||
- `*_bootstrap_report.md`
|
||||
- `*_summary.md`
|
||||
- `*_manifest.json`
|
||||
- `command_log_*.json`
|
||||
|
||||
## 4. 执行单元测试
|
||||
|
||||
```powershell
|
||||
go test ./scripts
|
||||
```
|
||||
|
||||
## 5. 执行真实写入
|
||||
|
||||
确认目标仓库和认证状态后执行:
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1 -Apply
|
||||
```
|
||||
|
||||
执行真实写入前,应先通过 `gitlink-cli auth login` 或当前环境已配置的认证方式完成 GitLink 登录。
|
||||
|
||||
如需创建目标仓库:
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1 -Apply -CreateRepo
|
||||
```
|
||||
|
||||
如需把摘要发布到指定 Issue:
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1 -Apply -PublishIssueNumber 1
|
||||
```
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# 运行手册
|
||||
|
||||
## 模式说明
|
||||
|
||||
| 模式 | 命令 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| dry-run | `.\scripts\run_demo.ps1` | 只生成材料和命令计划,不写入 GitLink |
|
||||
| apply | `.\scripts\run_demo.ps1 -Apply` | 执行真实 `gitlink-cli` 命令 |
|
||||
| apply + create repo | `.\scripts\run_demo.ps1 -Apply -CreateRepo` | 先创建仓库,再执行初始化命令 |
|
||||
| apply + comment | `.\scripts\run_demo.ps1 -Apply -PublishIssueNumber 1` | 执行真实命令,并将摘要评论到指定 Issue |
|
||||
|
||||
## 配置文件
|
||||
|
||||
默认配置位于:
|
||||
|
||||
```text
|
||||
examples/sample_project.json
|
||||
```
|
||||
|
||||
主要字段:
|
||||
|
||||
- `project`:项目名称、描述、语言、许可证
|
||||
- `repository`:目标 GitLink 仓库 owner/name
|
||||
- `branches`:需要创建的协作分支
|
||||
- `issues`:初始化 Issue 列表
|
||||
- `publish.issue_number`:可选的摘要发布 Issue 编号
|
||||
|
||||
## 输出文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
| --- | --- |
|
||||
| `*_bootstrap_report.md` | 初始化报告 |
|
||||
| `*_summary.md` | 可发布到 Issue 的摘要 |
|
||||
| `*_manifest.json` | 结构化初始化清单 |
|
||||
| `*_files.json` | 生成文件内容包 |
|
||||
| `command_log_*.json` | gitlink-cli 命令计划或执行结果 |
|
||||
|
||||
## 安全边界
|
||||
|
||||
- 默认 dry-run,不进行远端写操作。
|
||||
- 只有显式传入 `-Apply` 才执行真实 GitLink 命令。
|
||||
- `-PublishIssueNumber` 只在明确指定 Issue 编号时追加评论命令。
|
||||
- 所有命令会写入 `command_log_*.json`,便于复盘和审计。
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# 提交核对清单
|
||||
|
||||
## 官方交付要求映射
|
||||
|
||||
| 要求 | 本项目对应内容 |
|
||||
| --- | --- |
|
||||
| 工作流串联不少于 3 个 CLI 命令或 Skill 调用 | `scripts/bootstrap_project.go` 规划或执行 `repo +info`、`branch +list`、`branch +create`、`issue +create`、`issue +comment` |
|
||||
| 提供可复现执行脚本或 Agent 对话记录 | `scripts/run_demo.ps1` |
|
||||
| 在至少一个真实 GitLink 项目上运行并展示效果 | 已在 `puygob236/gitlink-bootstrap-demo` 完成仓库读取、分支读取、Issue 创建和 Issue 摘要回写验证 |
|
||||
| 提供工作流说明文档 | `README.md`、`docs/workflow-spec.md`、`docs/quickstart.md`、`docs/runbook.md` |
|
||||
| 提供架构图 | `docs/architecture.md`、`docs/assets/bootstrap-architecture.svg` |
|
||||
| 提供演示材料 | 演示视频作为比赛平台附件提交;仓库内保留 `scripts/run_demo.ps1`、`docs/verification.md` 和 `examples/demo_outputs/` 作为可复现证据 |
|
||||
| 代码开源并托管到 GitLink | 放置于 `examples/workflows/project-bootstrap-automation/` |
|
||||
| 提供完整中文 README | `README.md` |
|
||||
|
||||
## 验证状态
|
||||
|
||||
- `go test ./scripts`:通过
|
||||
- `.\scripts\run_demo.ps1`:通过
|
||||
- dry-run 生成 7 个 gitlink-cli 调用计划,满足赛题要求
|
||||
- `.\scripts\run_demo.ps1 -Config examples\verification_comment_config.json -Apply -PublishIssueNumber 4`:通过,3 个真实 gitlink-cli 调用状态均为 `ok`
|
||||
|
||||
## 交付内容
|
||||
|
||||
- `README.md`、`docs/`、`scripts/`、`examples/` 均位于本目录。
|
||||
- `outputs/` 为运行时生成目录,评审可通过复现脚本重新生成。
|
||||
- `examples/demo_outputs/` 用于保存固定示例产物。
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
# 验证记录
|
||||
|
||||
## 本地验证
|
||||
|
||||
执行目录:
|
||||
|
||||
```text
|
||||
examples/workflows/project-bootstrap-automation
|
||||
```
|
||||
|
||||
单元测试:
|
||||
|
||||
```powershell
|
||||
go test ./scripts
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
ok github.com/gitlink-org/gitlink-cli/examples/workflows/project-bootstrap-automation/scripts
|
||||
```
|
||||
|
||||
dry-run 复现:
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
已生成初始化报告: outputs\puygob236_gitlink-bootstrap-demo_20260524_072107_bootstrap_report.md
|
||||
已生成初始化摘要: outputs\puygob236_gitlink-bootstrap-demo_20260524_072107_summary.md
|
||||
已生成文件清单: outputs\puygob236_gitlink-bootstrap-demo_20260524_072107_manifest.json
|
||||
已生成命令日志: outputs\command_log_20260524_072107.json
|
||||
模式: dry-run
|
||||
计划/执行 gitlink-cli 调用: 7 个
|
||||
```
|
||||
|
||||
## 真实仓库验证计划
|
||||
|
||||
目标仓库:
|
||||
|
||||
```text
|
||||
puygob236/gitlink-bootstrap-demo
|
||||
```
|
||||
|
||||
验证步骤:
|
||||
|
||||
1. 确认 GitLink 认证可用。
|
||||
2. 创建或确认目标仓库存在。
|
||||
3. 执行 `.\scripts\run_demo.ps1 -Apply`。
|
||||
4. 检查分支、Issue 和输出报告。
|
||||
5. 如需展示回写能力,执行 `.\scripts\run_demo.ps1 -Apply -PublishIssueNumber <number>`。
|
||||
|
||||
## 真实仓库验证结果
|
||||
|
||||
目标仓库:
|
||||
|
||||
```text
|
||||
https://gitlink.org.cn/puygob236/gitlink-bootstrap-demo
|
||||
```
|
||||
|
||||
已完成验证:
|
||||
|
||||
- `repo +info`:成功读取 `puygob236/gitlink-bootstrap-demo` 仓库信息。
|
||||
- `branch +list`:成功读取 `master`、`develop`、`release/v0.1` 分支。
|
||||
- `issue +create`:成功创建初始化 Issue,生成项目任务清单。
|
||||
- `issue +comment`:成功将初始化摘要回写到 Issue。
|
||||
|
||||
回写验证命令:
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1 -Config examples\verification_comment_config.json -Apply -PublishIssueNumber 4
|
||||
```
|
||||
|
||||
回写验证结果:
|
||||
|
||||
```text
|
||||
模式: apply
|
||||
计划/执行 gitlink-cli 调用: 3 个
|
||||
```
|
||||
|
||||
命令日志中 3 条调用状态均为 `ok`,无 stderr。
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# 工作流说明
|
||||
|
||||
## 场景定位
|
||||
|
||||
本工作流面向 GitLink 子赛题三“构建端到端自动化工作流”,选择“项目一键初始化”作为应用场景。目标是在新开源项目创建初期,将项目配置、初始化文件、协作分支、初始 Issue 和执行报告统一串联,形成可复现、可审计的启动流程。
|
||||
|
||||
该场景覆盖开源项目常见的启动缺口:
|
||||
|
||||
- README、License、CI 配置和协作文档不完整。
|
||||
- 初始任务缺少统一模板,Issue 粒度和验收标准不一致。
|
||||
- 分支、Issue、报告产物分散,难以复盘初始化过程。
|
||||
- 真实写入和演示复现之间缺少安全边界。
|
||||
|
||||
## 端到端流程
|
||||
|
||||
工作流由 `scripts/bootstrap_project.go` 实现,默认读取 `examples/sample_project.json`,并按以下顺序执行:
|
||||
|
||||
1. 解析项目配置,读取项目名称、仓库 owner/name、许可证、初始化分支和初始 Issue。
|
||||
2. 生成初始化文件包,包括 README、LICENSE、CI 配置、贡献指南和路线图。
|
||||
3. 规划或执行 `repo +info`,检查目标 GitLink 仓库状态。
|
||||
4. 规划或执行 `branch +list`,读取分支状态。
|
||||
5. 规划或执行 `branch +create`,创建协作分支。
|
||||
6. 规划或执行 `issue +create`,创建初始化任务。
|
||||
7. 可选执行 `issue +comment`,将初始化摘要回写到指定 Issue。
|
||||
8. 生成 Markdown 报告、摘要、manifest、文件包和命令日志。
|
||||
|
||||
## 串联的 GitLink CLI 能力
|
||||
|
||||
默认 dry-run 配置会生成 7 个 `gitlink-cli` 调用计划:
|
||||
|
||||
| 顺序 | CLI 能力 | 用途 |
|
||||
| ---: | --- | --- |
|
||||
| 1 | `repo +info` | 检查目标仓库信息 |
|
||||
| 2 | `branch +list` | 读取当前分支列表 |
|
||||
| 3 | `branch +create` | 创建 `develop` 协作分支 |
|
||||
| 4 | `branch +create` | 创建 `release/v0.1` 发布分支 |
|
||||
| 5 | `issue +create` | 创建 README 与快速开始任务 |
|
||||
| 6 | `issue +create` | 创建 CI 检查任务 |
|
||||
| 7 | `issue +create` | 创建 v0.1 里程碑任务 |
|
||||
|
||||
当传入 `-PublishIssueNumber` 时,会追加 `issue +comment`,用于把初始化摘要发布到指定 GitLink Issue。
|
||||
|
||||
## 运行模式
|
||||
|
||||
| 模式 | 命令 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| dry-run | `.\scripts\run_demo.ps1` | 生成材料和命令计划,不写入 GitLink |
|
||||
| apply | `.\scripts\run_demo.ps1 -Apply` | 执行真实 GitLink CLI 命令 |
|
||||
| apply + create repo | `.\scripts\run_demo.ps1 -Apply -CreateRepo` | 先创建仓库,再执行初始化流程 |
|
||||
| apply + comment | `.\scripts\run_demo.ps1 -Apply -PublishIssueNumber 1` | 执行真实命令并回写摘要 |
|
||||
|
||||
## 输出产物
|
||||
|
||||
运行后会生成以下文件:
|
||||
|
||||
| 文件 | 说明 |
|
||||
| --- | --- |
|
||||
| `*_bootstrap_report.md` | 初始化报告,展示目标项目、生成文件、分支计划和 Issue 计划 |
|
||||
| `*_summary.md` | 可发布到 Issue 的初始化摘要 |
|
||||
| `*_manifest.json` | 结构化初始化清单 |
|
||||
| `*_files.json` | 生成文件内容包 |
|
||||
| `command_log_*.json` | gitlink-cli 命令计划或执行结果 |
|
||||
|
||||
固定示例输出保存在 `examples/demo_outputs/`,用于评审快速查看产物格式。`outputs/` 是运行时目录,可通过脚本重新生成。
|
||||
|
||||
## 工程边界
|
||||
|
||||
- 主实现使用 Go,便于与 `gitlink-cli` 主仓库技术栈保持一致。
|
||||
- 默认 dry-run,避免演示阶段误写远端仓库。
|
||||
- 真实写入必须显式传入 `-Apply`。
|
||||
- 命令日志记录每个 CLI 调用的状态,便于复盘和排查。
|
||||
- 测试覆盖文件生成、CLI 编排、Issue 内容生成、幂等跳过判断和输出 manifest。
|
||||
|
||||
## 赛题价值
|
||||
|
||||
该工作流不是单个命令封装,而是面向真实开源项目启动流程的组合式方案。它把 `gitlink-cli` 的仓库、分支、Issue 和评论能力整合为一个可复现闭环,符合子赛题三对“串联多个 CLI 命令或 Skill 调用”“真实项目运行展示”“工作流说明文档和架构图”的要求。
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# 示例输出
|
||||
|
||||
本目录保存 `scripts/bootstrap_project.go` 在 dry-run 模式下生成的固定示例产物,便于快速查看工作流输出格式。
|
||||
|
||||
生成命令:
|
||||
|
||||
```powershell
|
||||
go run scripts\bootstrap_project.go --config examples\sample_project.json --output-dir examples\demo_outputs --now 2026-05-24T08:00:00Z
|
||||
```
|
||||
|
||||
产物说明:
|
||||
|
||||
- `*_bootstrap_report.md`:项目初始化报告
|
||||
- `*_summary.md`:可发布到 Issue 的初始化摘要
|
||||
- `*_manifest.json`:结构化初始化清单
|
||||
- `*_files.json`:生成文件内容包
|
||||
- `command_log_*.json`:gitlink-cli 命令计划
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
{
|
||||
"mode": "dry-run",
|
||||
"commands": [
|
||||
{
|
||||
"command": [
|
||||
"gitlink-cli",
|
||||
"repo",
|
||||
"+info",
|
||||
"--owner",
|
||||
"puygob236",
|
||||
"--repo",
|
||||
"gitlink-bootstrap-demo",
|
||||
"--format",
|
||||
"json"
|
||||
],
|
||||
"status": "planned",
|
||||
"returncode": null,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"command": [
|
||||
"gitlink-cli",
|
||||
"branch",
|
||||
"+list",
|
||||
"--owner",
|
||||
"puygob236",
|
||||
"--repo",
|
||||
"gitlink-bootstrap-demo",
|
||||
"--format",
|
||||
"json"
|
||||
],
|
||||
"status": "planned",
|
||||
"returncode": null,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"command": [
|
||||
"gitlink-cli",
|
||||
"branch",
|
||||
"+create",
|
||||
"--owner",
|
||||
"puygob236",
|
||||
"--repo",
|
||||
"gitlink-bootstrap-demo",
|
||||
"--name",
|
||||
"develop",
|
||||
"--from",
|
||||
"master",
|
||||
"--format",
|
||||
"json"
|
||||
],
|
||||
"status": "planned",
|
||||
"returncode": null,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"command": [
|
||||
"gitlink-cli",
|
||||
"branch",
|
||||
"+create",
|
||||
"--owner",
|
||||
"puygob236",
|
||||
"--repo",
|
||||
"gitlink-bootstrap-demo",
|
||||
"--name",
|
||||
"release/v0.1",
|
||||
"--from",
|
||||
"master",
|
||||
"--format",
|
||||
"json"
|
||||
],
|
||||
"status": "planned",
|
||||
"returncode": null,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"command": [
|
||||
"gitlink-cli",
|
||||
"issue",
|
||||
"+create",
|
||||
"--owner",
|
||||
"puygob236",
|
||||
"--repo",
|
||||
"gitlink-bootstrap-demo",
|
||||
"--title",
|
||||
"完善项目 README 与快速开始文档",
|
||||
"--body",
|
||||
"仓库: `puygob236/gitlink-bootstrap-demo`\n\n类型: documentation\n优先级: normal\n\n## 任务清单\n\n- [ ] 补充项目背景和目标用户\n- [ ] 补充安装与运行步骤\n- [ ] 补充最小示例\n\n## 验收标准\n\nREADME 能支撑新贡献者在 10 分钟内完成本地启动。\n",
|
||||
"--format",
|
||||
"json"
|
||||
],
|
||||
"status": "planned",
|
||||
"returncode": null,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"command": [
|
||||
"gitlink-cli",
|
||||
"issue",
|
||||
"+create",
|
||||
"--owner",
|
||||
"puygob236",
|
||||
"--repo",
|
||||
"gitlink-bootstrap-demo",
|
||||
"--title",
|
||||
"建立基础 CI 检查",
|
||||
"--body",
|
||||
"仓库: `puygob236/gitlink-bootstrap-demo`\n\n类型: ci\n优先级: high\n\n## 任务清单\n\n- [ ] 添加测试命令\n- [ ] 添加 lint 或格式检查\n- [ ] 在 PR 中展示检查结果\n\n## 验收标准\n\n每次 push 和 PR 均能触发基础检查。\n",
|
||||
"--format",
|
||||
"json"
|
||||
],
|
||||
"status": "planned",
|
||||
"returncode": null,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"command": [
|
||||
"gitlink-cli",
|
||||
"issue",
|
||||
"+create",
|
||||
"--owner",
|
||||
"puygob236",
|
||||
"--repo",
|
||||
"gitlink-bootstrap-demo",
|
||||
"--title",
|
||||
"规划 v0.1 版本里程碑",
|
||||
"--body",
|
||||
"仓库: `puygob236/gitlink-bootstrap-demo`\n\n类型: release\n优先级: normal\n\n## 任务清单\n\n- [ ] 整理 v0.1 范围\n- [ ] 确定验收标准\n- [ ] 准备 Release Notes 模板\n\n## 验收标准\n\n形成可执行的 v0.1 版本任务列表。\n",
|
||||
"--format",
|
||||
"json"
|
||||
],
|
||||
"status": "planned",
|
||||
"returncode": null,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# GitLink 项目初始化工作流报告
|
||||
|
||||
## 目标项目
|
||||
|
||||
- 仓库: `puygob236/gitlink-bootstrap-demo`
|
||||
- 项目名称: Open Research Toolkit
|
||||
- 描述: A reproducible GitLink project initialized by an end-to-end automation workflow.
|
||||
- 生成时间: 2026-05-24T08:00:00Z
|
||||
|
||||
## 初始化文件
|
||||
|
||||
| 文件 | 字节数 |
|
||||
| --- | ---: |
|
||||
| `README.md` | 545 |
|
||||
| `LICENSE` | 179 |
|
||||
| `.github/workflows/ci.yml` | 232 |
|
||||
| `docs/CONTRIBUTING.md` | 83 |
|
||||
| `docs/ROADMAP.md` | 92 |
|
||||
|
||||
## 分支计划
|
||||
|
||||
| 分支 | 来源 | 保护 |
|
||||
| --- | --- | --- |
|
||||
| `develop` | `master` | false |
|
||||
| `release/v0.1` | `master` | false |
|
||||
|
||||
## 初始 Issue 计划
|
||||
|
||||
| 序号 | 标题 | 优先级 |
|
||||
| ---: | --- | --- |
|
||||
| 1 | 完善项目 README 与快速开始文档 | normal |
|
||||
| 2 | 建立基础 CI 检查 | high |
|
||||
| 3 | 规划 v0.1 版本里程碑 | normal |
|
||||
|
||||
## 工作流闭环
|
||||
|
||||
1. 读取项目配置。
|
||||
2. 生成 README、LICENSE、CI 和协作文档。
|
||||
3. 调用 gitlink-cli 检查仓库和分支状态。
|
||||
4. 调用 gitlink-cli 创建初始化 Issue。
|
||||
5. 输出报告、摘要和结构化 manifest,必要时回写到 GitLink Issue。
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
".github/workflows/ci.yml": "name: Go CI\n\non:\n push:\n pull_request:\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-go@v5\n with:\n go-version: \"1.23\"\n - run: go test ./...\n",
|
||||
"LICENSE": "# License\n\nThis project is initialized with the `MulanPSL-2.0` license.\n\nThe final repository should keep the complete license text that matches the selected open-source license.\n",
|
||||
"README.md": "# Open Research Toolkit\n\nA reproducible GitLink project initialized by an end-to-end automation workflow.\n\n## 项目信息\n\n- GitLink 仓库: `puygob236/gitlink-bootstrap-demo`\n- 技术方向: Go\n- 初始化来源: GitLink 项目一键初始化工作流\n\n## 快速开始\n\n```bash\ngit clone https://gitlink.org.cn/puygob236/gitlink-bootstrap-demo.git\ncd gitlink-bootstrap-demo\n```\n\n## 协作约定\n\n- 使用 Issue 跟踪需求、缺陷和文档任务。\n- 使用 Pull Request 合并代码变更。\n- 重要里程碑通过 Release Notes 记录。\n",
|
||||
"docs/CONTRIBUTING.md": "# 贡献指南\n\n请通过 Issue 讨论需求,通过 Pull Request 提交变更。\n",
|
||||
"docs/ROADMAP.md": "# Roadmap\n\n- [ ] 完成项目初始化\n- [ ] 建立基础测试\n- [ ] 发布第一个版本\n"
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"repository": "puygob236/gitlink-bootstrap-demo",
|
||||
"project": {
|
||||
"name": "Open Research Toolkit",
|
||||
"description": "A reproducible GitLink project initialized by an end-to-end automation workflow.",
|
||||
"language": "Go",
|
||||
"license": "MulanPSL-2.0"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"bytes": 545
|
||||
},
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"bytes": 179
|
||||
},
|
||||
{
|
||||
"path": ".github/workflows/ci.yml",
|
||||
"bytes": 232
|
||||
},
|
||||
{
|
||||
"path": "docs/CONTRIBUTING.md",
|
||||
"bytes": 83
|
||||
},
|
||||
{
|
||||
"path": "docs/ROADMAP.md",
|
||||
"bytes": 92
|
||||
}
|
||||
],
|
||||
"branches": [
|
||||
{
|
||||
"name": "develop",
|
||||
"from": "master",
|
||||
"create": true,
|
||||
"protect": false
|
||||
},
|
||||
{
|
||||
"name": "release/v0.1",
|
||||
"from": "master",
|
||||
"create": true,
|
||||
"protect": false
|
||||
}
|
||||
],
|
||||
"issues": [
|
||||
{
|
||||
"title": "完善项目 README 与快速开始文档",
|
||||
"type": "documentation",
|
||||
"priority": "normal",
|
||||
"tasks": [
|
||||
"补充项目背景和目标用户",
|
||||
"补充安装与运行步骤",
|
||||
"补充最小示例"
|
||||
],
|
||||
"acceptance": "README 能支撑新贡献者在 10 分钟内完成本地启动。"
|
||||
},
|
||||
{
|
||||
"title": "建立基础 CI 检查",
|
||||
"type": "ci",
|
||||
"priority": "high",
|
||||
"tasks": [
|
||||
"添加测试命令",
|
||||
"添加 lint 或格式检查",
|
||||
"在 PR 中展示检查结果"
|
||||
],
|
||||
"acceptance": "每次 push 和 PR 均能触发基础检查。"
|
||||
},
|
||||
{
|
||||
"title": "规划 v0.1 版本里程碑",
|
||||
"type": "release",
|
||||
"priority": "normal",
|
||||
"tasks": [
|
||||
"整理 v0.1 范围",
|
||||
"确定验收标准",
|
||||
"准备 Release Notes 模板"
|
||||
],
|
||||
"acceptance": "形成可执行的 v0.1 版本任务列表。"
|
||||
}
|
||||
],
|
||||
"generated_at": "2026-05-24T08:00:00Z"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# GitLink 项目初始化摘要
|
||||
|
||||
- 目标仓库: `puygob236/gitlink-bootstrap-demo`
|
||||
- 项目名称: Open Research Toolkit
|
||||
- 生成时间: 2026-05-24T08:00:00Z
|
||||
- 初始化文件: 5 个
|
||||
- 初始 Issue: 3 个
|
||||
- 分支动作: 2 个
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"project": {
|
||||
"name": "Open Research Toolkit",
|
||||
"description": "A reproducible GitLink project initialized by an end-to-end automation workflow.",
|
||||
"language": "Go",
|
||||
"license": "MulanPSL-2.0"
|
||||
},
|
||||
"repository": {
|
||||
"owner": "puygob236",
|
||||
"name": "gitlink-bootstrap-demo"
|
||||
},
|
||||
"branches": [
|
||||
{
|
||||
"name": "develop",
|
||||
"from": "master",
|
||||
"create": true,
|
||||
"protect": false
|
||||
},
|
||||
{
|
||||
"name": "release/v0.1",
|
||||
"from": "master",
|
||||
"create": true,
|
||||
"protect": false
|
||||
}
|
||||
],
|
||||
"issues": [
|
||||
{
|
||||
"title": "完善项目 README 与快速开始文档",
|
||||
"type": "documentation",
|
||||
"priority": "normal",
|
||||
"tasks": [
|
||||
"补充项目背景和目标用户",
|
||||
"补充安装与运行步骤",
|
||||
"补充最小示例"
|
||||
],
|
||||
"acceptance": "README 能支撑新贡献者在 10 分钟内完成本地启动。"
|
||||
},
|
||||
{
|
||||
"title": "建立基础 CI 检查",
|
||||
"type": "ci",
|
||||
"priority": "high",
|
||||
"tasks": [
|
||||
"添加测试命令",
|
||||
"添加 lint 或格式检查",
|
||||
"在 PR 中展示检查结果"
|
||||
],
|
||||
"acceptance": "每次 push 和 PR 均能触发基础检查。"
|
||||
},
|
||||
{
|
||||
"title": "规划 v0.1 版本里程碑",
|
||||
"type": "release",
|
||||
"priority": "normal",
|
||||
"tasks": [
|
||||
"整理 v0.1 范围",
|
||||
"确定验收标准",
|
||||
"准备 Release Notes 模板"
|
||||
],
|
||||
"acceptance": "形成可执行的 v0.1 版本任务列表。"
|
||||
}
|
||||
],
|
||||
"publish": {
|
||||
"issue_number": 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"project": {
|
||||
"name": "Open Research Toolkit",
|
||||
"description": "A reproducible GitLink project initialized by an end-to-end automation workflow.",
|
||||
"language": "Go",
|
||||
"license": "MulanPSL-2.0"
|
||||
},
|
||||
"repository": {
|
||||
"owner": "puygob236",
|
||||
"name": "gitlink-bootstrap-demo"
|
||||
},
|
||||
"branches": [],
|
||||
"issues": [],
|
||||
"publish": {
|
||||
"issue_number": 4
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,478 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProjectConfig struct {
|
||||
Project ProjectInfo `json:"project"`
|
||||
Repository RepositoryInfo `json:"repository"`
|
||||
Branches []BranchPlan `json:"branches"`
|
||||
Issues []IssuePlan `json:"issues"`
|
||||
Publish PublishConfig `json:"publish"`
|
||||
}
|
||||
|
||||
type ProjectInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Language string `json:"language"`
|
||||
License string `json:"license"`
|
||||
}
|
||||
|
||||
type RepositoryInfo struct {
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type BranchPlan struct {
|
||||
Name string `json:"name"`
|
||||
From string `json:"from"`
|
||||
Create *bool `json:"create"`
|
||||
Protect bool `json:"protect"`
|
||||
}
|
||||
|
||||
type IssuePlan struct {
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Priority string `json:"priority"`
|
||||
Tasks []string `json:"tasks"`
|
||||
Acceptance string `json:"acceptance"`
|
||||
}
|
||||
|
||||
type PublishConfig struct {
|
||||
IssueNumber int `json:"issue_number"`
|
||||
}
|
||||
|
||||
type FileManifestItem struct {
|
||||
Path string `json:"path"`
|
||||
Bytes int `json:"bytes"`
|
||||
}
|
||||
|
||||
type OutputManifest struct {
|
||||
Repository string `json:"repository"`
|
||||
Project ProjectInfo `json:"project"`
|
||||
Files []FileManifestItem `json:"files"`
|
||||
Branches []BranchPlan `json:"branches"`
|
||||
Issues []IssuePlan `json:"issues"`
|
||||
GeneratedAt string `json:"generated_at"`
|
||||
}
|
||||
|
||||
type CommandResult struct {
|
||||
Command []string `json:"command"`
|
||||
Status string `json:"status"`
|
||||
ReturnCode *int `json:"returncode"`
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
}
|
||||
|
||||
type CommandLog struct {
|
||||
Mode string `json:"mode"`
|
||||
Commands []CommandResult `json:"commands"`
|
||||
}
|
||||
|
||||
type OutputPaths struct {
|
||||
Report string
|
||||
Summary string
|
||||
Manifest string
|
||||
Files string
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
ConfigPath string
|
||||
OutputDir string
|
||||
CLIBin string
|
||||
Apply bool
|
||||
CreateRepo bool
|
||||
PublishIssueNumber int
|
||||
Now string
|
||||
}
|
||||
|
||||
func parseFlags(args []string) Options {
|
||||
var opts Options
|
||||
fs := flag.NewFlagSet("bootstrap-project", flag.ExitOnError)
|
||||
fs.StringVar(&opts.ConfigPath, "config", filepath.FromSlash("examples/sample_project.json"), "配置文件路径")
|
||||
fs.StringVar(&opts.OutputDir, "output-dir", "outputs", "输出目录")
|
||||
fs.StringVar(&opts.CLIBin, "cli-bin", firstNonEmpty(os.Getenv("GITLINK_CLI_BIN"), "gitlink-cli"), "gitlink-cli 可执行文件路径")
|
||||
fs.BoolVar(&opts.Apply, "apply", false, "执行真实 GitLink 写操作")
|
||||
fs.BoolVar(&opts.CreateRepo, "create-repo", false, "仓库不存在时创建仓库")
|
||||
fs.IntVar(&opts.PublishIssueNumber, "publish-issue-number", 0, "把初始化摘要评论到指定 Issue")
|
||||
fs.StringVar(&opts.Now, "now", "", "固定当前时间,ISO8601 格式")
|
||||
_ = fs.Parse(args)
|
||||
return opts
|
||||
}
|
||||
|
||||
func loadConfig(path string) (ProjectConfig, error) {
|
||||
var config ProjectConfig
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("配置文件不存在: %s", path)
|
||||
}
|
||||
data = bytes.TrimPrefix(data, []byte{0xef, 0xbb, 0xbf})
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return config, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func parseNow(value string) (time.Time, error) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return time.Now().UTC(), nil
|
||||
}
|
||||
text := strings.ReplaceAll(strings.TrimSpace(value), "Z", "+00:00")
|
||||
dt, err := time.Parse(time.RFC3339, text)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return dt.UTC(), nil
|
||||
}
|
||||
|
||||
func isoTime(value time.Time) string {
|
||||
return value.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func safeName(value string) string {
|
||||
replacer := strings.NewReplacer("/", "_", "\\", "_", " ", "_")
|
||||
return replacer.Replace(value)
|
||||
}
|
||||
|
||||
func renderReadme(config ProjectConfig) string {
|
||||
language := firstNonEmpty(config.Project.Language, "未指定")
|
||||
return fmt.Sprintf("# %s\n\n%s\n\n## 项目信息\n\n- GitLink 仓库: `%s/%s`\n- 技术方向: %s\n- 初始化来源: GitLink 项目一键初始化工作流\n\n## 快速开始\n\n```bash\ngit clone https://gitlink.org.cn/%s/%s.git\ncd %s\n```\n\n## 协作约定\n\n- 使用 Issue 跟踪需求、缺陷和文档任务。\n- 使用 Pull Request 合并代码变更。\n- 重要里程碑通过 Release Notes 记录。\n",
|
||||
config.Project.Name,
|
||||
config.Project.Description,
|
||||
config.Repository.Owner,
|
||||
config.Repository.Name,
|
||||
language,
|
||||
config.Repository.Owner,
|
||||
config.Repository.Name,
|
||||
config.Repository.Name,
|
||||
)
|
||||
}
|
||||
|
||||
func renderLicense(config ProjectConfig) string {
|
||||
licenseName := firstNonEmpty(config.Project.License, "MulanPSL-2.0")
|
||||
return fmt.Sprintf("# License\n\nThis project is initialized with the `%s` license.\n\nThe final repository should keep the complete license text that matches the selected open-source license.\n", licenseName)
|
||||
}
|
||||
|
||||
func renderCI(config ProjectConfig) string {
|
||||
if strings.Contains(strings.ToLower(config.Project.Language), "go") {
|
||||
return `name: Go CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23"
|
||||
- run: go test ./...
|
||||
`
|
||||
}
|
||||
return `name: Basic CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: echo "Add project-specific checks here."
|
||||
`
|
||||
}
|
||||
|
||||
func plannedFiles(config ProjectConfig) map[string]string {
|
||||
return map[string]string{
|
||||
"README.md": renderReadme(config),
|
||||
"LICENSE": renderLicense(config),
|
||||
".github/workflows/ci.yml": renderCI(config),
|
||||
"docs/CONTRIBUTING.md": "# 贡献指南\n\n请通过 Issue 讨论需求,通过 Pull Request 提交变更。\n",
|
||||
"docs/ROADMAP.md": "# Roadmap\n\n- [ ] 完成项目初始化\n- [ ] 建立基础测试\n- [ ] 发布第一个版本\n",
|
||||
}
|
||||
}
|
||||
|
||||
func issueBody(item IssuePlan, config ProjectConfig) string {
|
||||
repo := fmt.Sprintf("%s/%s", config.Repository.Owner, config.Repository.Name)
|
||||
tasks := "- [ ] 待补充"
|
||||
if len(item.Tasks) > 0 {
|
||||
lines := make([]string, 0, len(item.Tasks))
|
||||
for _, task := range item.Tasks {
|
||||
lines = append(lines, "- [ ] "+task)
|
||||
}
|
||||
tasks = strings.Join(lines, "\n")
|
||||
}
|
||||
return fmt.Sprintf("仓库: `%s`\n\n类型: %s\n优先级: %s\n\n## 任务清单\n\n%s\n\n## 验收标准\n\n%s\n",
|
||||
repo,
|
||||
firstNonEmpty(item.Type, "task"),
|
||||
firstNonEmpty(item.Priority, "normal"),
|
||||
tasks,
|
||||
firstNonEmpty(item.Acceptance, "完成后在本 Issue 中说明验证结果。"),
|
||||
)
|
||||
}
|
||||
|
||||
func shouldCreateBranch(branch BranchPlan) bool {
|
||||
return branch.Create == nil || *branch.Create
|
||||
}
|
||||
|
||||
func branchFrom(branch BranchPlan) string {
|
||||
return firstNonEmpty(branch.From, "master")
|
||||
}
|
||||
|
||||
func buildCLIPlan(config ProjectConfig, summary string, createRepo bool) [][]string {
|
||||
owner := config.Repository.Owner
|
||||
repo := config.Repository.Name
|
||||
commands := [][]string{}
|
||||
if createRepo {
|
||||
commands = append(commands, []string{"repo", "+create", "--name", repo, "--description", config.Project.Description, "--format", "json"})
|
||||
}
|
||||
commands = append(commands,
|
||||
[]string{"repo", "+info", "--owner", owner, "--repo", repo, "--format", "json"},
|
||||
[]string{"branch", "+list", "--owner", owner, "--repo", repo, "--format", "json"},
|
||||
)
|
||||
for _, branch := range config.Branches {
|
||||
if shouldCreateBranch(branch) {
|
||||
commands = append(commands, []string{"branch", "+create", "--owner", owner, "--repo", repo, "--name", branch.Name, "--from", branchFrom(branch), "--format", "json"})
|
||||
}
|
||||
if branch.Protect {
|
||||
commands = append(commands, []string{"branch", "+protect", "--owner", owner, "--repo", repo, "--name", branch.Name, "--format", "json"})
|
||||
}
|
||||
}
|
||||
for _, issue := range config.Issues {
|
||||
commands = append(commands, []string{"issue", "+create", "--owner", owner, "--repo", repo, "--title", issue.Title, "--body", issueBody(issue, config), "--format", "json"})
|
||||
}
|
||||
if summary != "" && config.Publish.IssueNumber > 0 {
|
||||
commands = append(commands, []string{"issue", "+comment", "--owner", owner, "--repo", repo, "--number", strconv.Itoa(config.Publish.IssueNumber), "--body", summary, "--format", "json"})
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func runCommand(cliBin string, args []string, apply bool) CommandResult {
|
||||
command := append([]string{cliBin}, args...)
|
||||
if !apply {
|
||||
return CommandResult{Command: command, Status: "planned"}
|
||||
}
|
||||
cmd := exec.Command(cliBin, args...)
|
||||
if strings.HasSuffix(strings.ToLower(cliBin), ".cmd") || strings.HasSuffix(strings.ToLower(cliBin), ".bat") {
|
||||
cmd = exec.Command("cmd", append([]string{"/c", cliBin}, args...)...)
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
returnCode := 0
|
||||
status := "ok"
|
||||
if err != nil {
|
||||
status = "failed"
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
returnCode = exitErr.ExitCode()
|
||||
} else {
|
||||
returnCode = 1
|
||||
}
|
||||
if isIdempotentSkip(stderr.String()) {
|
||||
status = "skipped"
|
||||
}
|
||||
}
|
||||
return CommandResult{
|
||||
Command: command,
|
||||
Status: status,
|
||||
ReturnCode: &returnCode,
|
||||
Stdout: strings.TrimSpace(stdout.String()),
|
||||
Stderr: strings.TrimSpace(stderr.String()),
|
||||
}
|
||||
}
|
||||
|
||||
func isIdempotentSkip(stderr string) bool {
|
||||
knownMessages := []string{
|
||||
"新分支已存在",
|
||||
"branch already exists",
|
||||
"repository already exists",
|
||||
"仓库已存在",
|
||||
}
|
||||
for _, message := range knownMessages {
|
||||
if strings.Contains(stderr, message) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func writeOutputs(config ProjectConfig, outputDir string, now time.Time) (OutputPaths, error) {
|
||||
owner := config.Repository.Owner
|
||||
repo := config.Repository.Name
|
||||
prefix := fmt.Sprintf("%s_%s_%s", safeName(owner), safeName(repo), now.UTC().Format("20060102_150405"))
|
||||
if err := os.MkdirAll(outputDir, 0o755); err != nil {
|
||||
return OutputPaths{}, err
|
||||
}
|
||||
files := plannedFiles(config)
|
||||
fileManifest := make([]FileManifestItem, 0, len(files))
|
||||
for _, path := range []string{"README.md", "LICENSE", ".github/workflows/ci.yml", "docs/CONTRIBUTING.md", "docs/ROADMAP.md"} {
|
||||
if content, ok := files[path]; ok {
|
||||
fileManifest = append(fileManifest, FileManifestItem{Path: path, Bytes: len([]byte(content))})
|
||||
}
|
||||
}
|
||||
summary := fmt.Sprintf("# GitLink 项目初始化摘要\n\n- 目标仓库: `%s/%s`\n- 项目名称: %s\n- 生成时间: %s\n- 初始化文件: %d 个\n- 初始 Issue: %d 个\n- 分支动作: %d 个\n",
|
||||
owner,
|
||||
repo,
|
||||
config.Project.Name,
|
||||
isoTime(now),
|
||||
len(files),
|
||||
len(config.Issues),
|
||||
len(config.Branches),
|
||||
)
|
||||
report := renderReport(config, fileManifest, now)
|
||||
manifest := OutputManifest{
|
||||
Repository: fmt.Sprintf("%s/%s", owner, repo),
|
||||
Project: config.Project,
|
||||
Files: fileManifest,
|
||||
Branches: config.Branches,
|
||||
Issues: config.Issues,
|
||||
GeneratedAt: isoTime(now),
|
||||
}
|
||||
manifestJSON, err := json.MarshalIndent(manifest, "", " ")
|
||||
if err != nil {
|
||||
return OutputPaths{}, err
|
||||
}
|
||||
filesJSON, err := json.MarshalIndent(files, "", " ")
|
||||
if err != nil {
|
||||
return OutputPaths{}, err
|
||||
}
|
||||
paths := OutputPaths{
|
||||
Report: filepath.Join(outputDir, prefix+"_bootstrap_report.md"),
|
||||
Summary: filepath.Join(outputDir, prefix+"_summary.md"),
|
||||
Manifest: filepath.Join(outputDir, prefix+"_manifest.json"),
|
||||
Files: filepath.Join(outputDir, prefix+"_files.json"),
|
||||
}
|
||||
writes := map[string][]byte{
|
||||
paths.Report: []byte(report),
|
||||
paths.Summary: []byte(summary),
|
||||
paths.Manifest: manifestJSON,
|
||||
paths.Files: filesJSON,
|
||||
}
|
||||
for path, data := range writes {
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
return OutputPaths{}, err
|
||||
}
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func renderReport(config ProjectConfig, fileManifest []FileManifestItem, now time.Time) string {
|
||||
fileRows := []string{}
|
||||
for _, item := range fileManifest {
|
||||
fileRows = append(fileRows, fmt.Sprintf("| `%s` | %d |", item.Path, item.Bytes))
|
||||
}
|
||||
branchRows := []string{}
|
||||
for _, item := range config.Branches {
|
||||
branchRows = append(branchRows, fmt.Sprintf("| `%s` | `%s` | %t |", item.Name, branchFrom(item), item.Protect))
|
||||
}
|
||||
if len(branchRows) == 0 {
|
||||
branchRows = append(branchRows, "| 无 | 无 | false |")
|
||||
}
|
||||
issueRows := []string{}
|
||||
for idx, item := range config.Issues {
|
||||
issueRows = append(issueRows, fmt.Sprintf("| %d | %s | %s |", idx+1, item.Title, firstNonEmpty(item.Priority, "normal")))
|
||||
}
|
||||
if len(issueRows) == 0 {
|
||||
issueRows = append(issueRows, "| 0 | 无 | normal |")
|
||||
}
|
||||
return fmt.Sprintf("# GitLink 项目初始化工作流报告\n\n## 目标项目\n\n- 仓库: `%s/%s`\n- 项目名称: %s\n- 描述: %s\n- 生成时间: %s\n\n## 初始化文件\n\n| 文件 | 字节数 |\n| --- | ---: |\n%s\n\n## 分支计划\n\n| 分支 | 来源 | 保护 |\n| --- | --- | --- |\n%s\n\n## 初始 Issue 计划\n\n| 序号 | 标题 | 优先级 |\n| ---: | --- | --- |\n%s\n\n## 工作流闭环\n\n1. 读取项目配置。\n2. 生成 README、LICENSE、CI 和协作文档。\n3. 调用 gitlink-cli 检查仓库和分支状态。\n4. 调用 gitlink-cli 创建初始化 Issue。\n5. 输出报告、摘要和结构化 manifest,必要时回写到 GitLink Issue。\n",
|
||||
config.Repository.Owner,
|
||||
config.Repository.Name,
|
||||
config.Project.Name,
|
||||
config.Project.Description,
|
||||
isoTime(now),
|
||||
strings.Join(fileRows, "\n"),
|
||||
strings.Join(branchRows, "\n"),
|
||||
strings.Join(issueRows, "\n"),
|
||||
)
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func main() {
|
||||
opts := parseFlags(os.Args[1:])
|
||||
config, err := loadConfig(opts.ConfigPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
now, err := parseNow(opts.Now)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "无法解析 --now 的值: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if opts.PublishIssueNumber > 0 {
|
||||
config.Publish.IssueNumber = opts.PublishIssueNumber
|
||||
}
|
||||
outputPaths, err := writeOutputs(config, opts.OutputDir, now)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "写入输出失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
summaryBytes, err := os.ReadFile(outputPaths.Summary)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "读取摘要失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
plan := buildCLIPlan(config, string(summaryBytes), opts.CreateRepo)
|
||||
results := make([]CommandResult, 0, len(plan))
|
||||
for _, command := range plan {
|
||||
results = append(results, runCommand(opts.CLIBin, command, opts.Apply))
|
||||
}
|
||||
mode := "dry-run"
|
||||
if opts.Apply {
|
||||
mode = "apply"
|
||||
}
|
||||
commandLog := CommandLog{Mode: mode, Commands: results}
|
||||
commandLogJSON, err := json.MarshalIndent(commandLog, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "生成命令日志失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
commandLogPath := filepath.Join(opts.OutputDir, fmt.Sprintf("command_log_%s.json", now.UTC().Format("20060102_150405")))
|
||||
if err := os.WriteFile(commandLogPath, commandLogJSON, 0o644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "写入命令日志失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("已生成初始化报告: %s\n", outputPaths.Report)
|
||||
fmt.Printf("已生成初始化摘要: %s\n", outputPaths.Summary)
|
||||
fmt.Printf("已生成文件清单: %s\n", outputPaths.Manifest)
|
||||
fmt.Printf("已生成命令日志: %s\n", commandLogPath)
|
||||
fmt.Printf("模式: %s\n", mode)
|
||||
fmt.Printf("计划/执行 gitlink-cli 调用: %d 个\n", len(results))
|
||||
failed := 0
|
||||
for _, result := range results {
|
||||
if result.Status == "failed" {
|
||||
failed++
|
||||
}
|
||||
}
|
||||
if failed > 0 {
|
||||
fmt.Printf("失败命令: %d 个\n", failed)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func sampleConfig() ProjectConfig {
|
||||
create := true
|
||||
return ProjectConfig{
|
||||
Project: ProjectInfo{
|
||||
Name: "Demo Project",
|
||||
Description: "Demo description",
|
||||
Language: "Go",
|
||||
License: "MulanPSL-2.0",
|
||||
},
|
||||
Repository: RepositoryInfo{Owner: "alice", Name: "demo"},
|
||||
Branches: []BranchPlan{{Name: "develop", From: "master", Create: &create}},
|
||||
Issues: []IssuePlan{
|
||||
{
|
||||
Title: "Write README",
|
||||
Type: "documentation",
|
||||
Priority: "normal",
|
||||
Tasks: []string{"Add quickstart", "Add license"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlannedFilesIncludeRequiredProjectArtifacts(t *testing.T) {
|
||||
files := plannedFiles(sampleConfig())
|
||||
for _, path := range []string{"README.md", "LICENSE", ".github/workflows/ci.yml", "docs/CONTRIBUTING.md"} {
|
||||
if _, ok := files[path]; !ok {
|
||||
t.Fatalf("expected planned file %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCLIPlanChainsMoreThanThreeGitlinkCommands(t *testing.T) {
|
||||
plan := buildCLIPlan(sampleConfig(), "", false)
|
||||
if len(plan) < 4 {
|
||||
t.Fatalf("expected at least 4 commands, got %d", len(plan))
|
||||
}
|
||||
if strings.Join(plan[0][:2], " ") != "repo +info" {
|
||||
t.Fatalf("unexpected first command: %#v", plan[0])
|
||||
}
|
||||
if !containsCommand(plan, "branch +list") {
|
||||
t.Fatalf("branch +list command missing: %#v", plan)
|
||||
}
|
||||
if !containsCommand(plan, "issue +create") {
|
||||
t.Fatalf("issue +create command missing: %#v", plan)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCLIPlanCanCreateRepositoryFirst(t *testing.T) {
|
||||
plan := buildCLIPlan(sampleConfig(), "", true)
|
||||
if strings.Join(plan[0][:2], " ") != "repo +create" {
|
||||
t.Fatalf("unexpected first command: %#v", plan[0])
|
||||
}
|
||||
if strings.Join(plan[1][:2], " ") != "repo +info" {
|
||||
t.Fatalf("unexpected second command: %#v", plan[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueBodyContainsChecklistAndRepository(t *testing.T) {
|
||||
config := sampleConfig()
|
||||
body := issueBody(config.Issues[0], config)
|
||||
if !strings.Contains(body, "`alice/demo`") {
|
||||
t.Fatalf("repository missing from body: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, "- [ ] Add quickstart") {
|
||||
t.Fatalf("checklist missing from body: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteOutputsCreatesReportManifestAndSummary(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
paths, err := writeOutputs(sampleConfig(), tmp, time.Date(2026, 5, 24, 0, 0, 0, 0, time.UTC))
|
||||
if err != nil {
|
||||
t.Fatalf("writeOutputs returned error: %v", err)
|
||||
}
|
||||
for _, path := range []string{paths.Report, paths.Summary, paths.Manifest, paths.Files} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("expected output %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
data, err := os.ReadFile(paths.Manifest)
|
||||
if err != nil {
|
||||
t.Fatalf("read manifest: %v", err)
|
||||
}
|
||||
var manifest OutputManifest
|
||||
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||
t.Fatalf("unmarshal manifest: %v", err)
|
||||
}
|
||||
if manifest.Repository != "alice/demo" {
|
||||
t.Fatalf("unexpected repository: %s", manifest.Repository)
|
||||
}
|
||||
if len(manifest.Files) < 4 {
|
||||
t.Fatalf("expected at least 4 files, got %d", len(manifest.Files))
|
||||
}
|
||||
if filepath.Base(paths.Report) == "" {
|
||||
t.Fatal("report path should include filename")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchAlreadyExistsIsIdempotentSkip(t *testing.T) {
|
||||
if !isIdempotentSkip("[-1] 新分支已存在!") {
|
||||
t.Fatal("expected existing branch error to be skipped")
|
||||
}
|
||||
if isIdempotentSkip("[401] 请登录后再操作") {
|
||||
t.Fatal("auth error should not be skipped")
|
||||
}
|
||||
}
|
||||
|
||||
func containsCommand(plan [][]string, command string) bool {
|
||||
for _, item := range plan {
|
||||
if len(item) >= 2 && strings.Join(item[:2], " ") == command {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
param(
|
||||
[string]$Config = "examples/sample_project.json",
|
||||
[string]$OutputDir = "outputs",
|
||||
[switch]$Apply,
|
||||
[switch]$CreateRepo,
|
||||
[int]$PublishIssueNumber = 0
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$args = @(
|
||||
"run",
|
||||
"scripts\bootstrap_project.go",
|
||||
"--config", $Config,
|
||||
"--output-dir", $OutputDir
|
||||
)
|
||||
|
||||
if ($Apply.IsPresent) {
|
||||
$cliCandidates = npm.cmd exec --yes --package=@gitlink-ai/cli -- cmd /c where gitlink-cli 2>$null
|
||||
$cliPath = $cliCandidates | Where-Object { $_ -match 'gitlink-cli\.cmd$' } | Select-Object -First 1
|
||||
if (-not $cliPath) {
|
||||
$cliPath = $cliCandidates | Select-Object -First 1
|
||||
}
|
||||
if (-not $cliPath) {
|
||||
throw "未能通过 npm exec 找到 gitlink-cli"
|
||||
}
|
||||
$args += @("--cli-bin", $cliPath, "--apply")
|
||||
}
|
||||
|
||||
if ($CreateRepo.IsPresent) {
|
||||
$args += "--create-repo"
|
||||
}
|
||||
|
||||
if ($PublishIssueNumber -gt 0) {
|
||||
$args += @("--publish-issue-number", "$PublishIssueNumber")
|
||||
}
|
||||
|
||||
go @args
|
||||
Loading…
Reference in New Issue