feat(workflows): add project bootstrap automation example
This commit is contained in:
parent
b45241dcda
commit
33ad8e62a0
|
|
@ -0,0 +1 @@
|
|||
outputs/
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# 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/architecture.md`:架构与流程说明
|
||||
- `docs/quickstart.md`:最短复现路径
|
||||
- `docs/runbook.md`:运行手册
|
||||
- `docs/verification.md`:验证记录
|
||||
- `docs/submission-checklist.md`:赛题要求映射
|
||||
- `scripts/bootstrap_project_test.go`:Go 单元测试
|
||||
|
||||
## 运行方式
|
||||
|
||||
进入本目录后执行 dry-run:
|
||||
|
||||
```powershell
|
||||
.\scripts\run_demo.ps1
|
||||
```
|
||||
|
||||
执行后会生成:
|
||||
|
||||
- `outputs/*_bootstrap_report.md`
|
||||
- `outputs/*_summary.md`
|
||||
- `outputs/*_manifest.json`
|
||||
- `outputs/*_files.json`
|
||||
- `outputs/command_log_*.json`
|
||||
|
||||
如需执行真实 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`。
|
||||
|
||||
## 场景价值
|
||||
|
||||
- 降低新开源项目启动成本,避免 README、License、CI、初始任务缺失。
|
||||
- 将项目初始化过程结构化,便于团队复用和审计。
|
||||
- 将 `gitlink-cli` 的仓库、分支、Issue 和评论能力串联为可复现方案。
|
||||
- 支持 dry-run 和 apply 两种模式,兼顾演示稳定性和真实落地。
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# 架构说明
|
||||
|
||||
本工作流采用“配置输入 -> 资产生成 -> CLI 编排 -> GitLink 落地 -> 结果归档”的五段式架构。
|
||||
|
||||
```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,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,26 @@
|
|||
# 提交核对清单
|
||||
|
||||
## 官方交付要求映射
|
||||
|
||||
| 要求 | 本项目对应内容 |
|
||||
| --- | --- |
|
||||
| 工作流串联不少于 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/quickstart.md`、`docs/runbook.md` |
|
||||
| 提供架构图 | `docs/architecture.md` |
|
||||
| 代码开源并托管到 GitLink | 放置于 `examples/workflows/project-bootstrap-automation/` |
|
||||
| 提供完整中文 README | `README.md` |
|
||||
|
||||
## 验证状态
|
||||
|
||||
- `go test ./examples/workflows/project-bootstrap-automation/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,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