diff --git a/examples/workflows/project-bootstrap-automation/.gitignore b/examples/workflows/project-bootstrap-automation/.gitignore new file mode 100644 index 0000000..17aa483 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/.gitignore @@ -0,0 +1 @@ +outputs/ diff --git a/examples/workflows/project-bootstrap-automation/README.md b/examples/workflows/project-bootstrap-automation/README.md new file mode 100644 index 0000000..70da85e --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/README.md @@ -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 两种模式,兼顾演示稳定性和真实落地。 diff --git a/examples/workflows/project-bootstrap-automation/docs/architecture.md b/examples/workflows/project-bootstrap-automation/docs/architecture.md new file mode 100644 index 0000000..164be7a --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/docs/architecture.md @@ -0,0 +1,34 @@ +# 架构说明 + +本工作流采用“配置输入 -> 资产生成 -> CLI 编排 -> GitLink 落地 -> 结果归档”的五段式架构。正式架构图见 `docs/assets/bootstrap-architecture.svg`。 + +![GitLink 项目一键初始化工作流架构](assets/bootstrap-architecture.svg) + +```mermaid +flowchart LR + A["项目配置
sample_project.json"] --> B["资产生成
README / LICENSE / CI / 协作文档"] + B --> C["CLI 编排
repo / branch / issue / comment"] + C --> D["GitLink 项目空间
仓库 / 分支 / Issue"] + D --> E["结果归档
报告 / 摘要 / 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. 生成报告与命令日志,支撑复现和审计。 diff --git a/examples/workflows/project-bootstrap-automation/docs/assets/bootstrap-architecture.svg b/examples/workflows/project-bootstrap-automation/docs/assets/bootstrap-architecture.svg new file mode 100644 index 0000000..5701d99 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/docs/assets/bootstrap-architecture.svg @@ -0,0 +1,169 @@ + + GitLink Project Bootstrap Automation + A vector architecture diagram for a configuration-driven GitLink project bootstrap workflow. + + + + + + + + + GitLink Project Bootstrap Automation + Configuration-driven project initialization, CLI orchestration, GitLink execution, and reproducible evidence + + + + + + + + { } + + + + Input + + Project metadata + Repository target + Branches + Initial issues + sample_project.json + + + + + + + + + + + + + + + + Assets + + README + LICENSE + Go CI + CONTRIBUTING + ROADMAP + + + + + + + + + + + + + + + CLI Orchestration + + repo +info + branch +list + branch +create + issue +create + + + + + + + + + + + + + + + GitLink + + Repository state + Collaboration branches + Bootstrap issues + Issue comment + Apply mode writesremotely + + + + + + + + + + + + + Evidence + + Markdown report + Issue summary + manifest.json + files.json + command log + + + + + + + + + + + + + + Go implementation + + + + + + 7 default CLI calls + + + + + + Reproducible dry-run + + + + + + Validated on real GitLink repo + + + diff --git a/examples/workflows/project-bootstrap-automation/docs/quickstart.md b/examples/workflows/project-bootstrap-automation/docs/quickstart.md new file mode 100644 index 0000000..b194178 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/docs/quickstart.md @@ -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 +``` diff --git a/examples/workflows/project-bootstrap-automation/docs/runbook.md b/examples/workflows/project-bootstrap-automation/docs/runbook.md new file mode 100644 index 0000000..73d9020 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/docs/runbook.md @@ -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`,便于复盘和审计。 diff --git a/examples/workflows/project-bootstrap-automation/docs/submission-checklist.md b/examples/workflows/project-bootstrap-automation/docs/submission-checklist.md new file mode 100644 index 0000000..9019003 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/docs/submission-checklist.md @@ -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/` 用于保存固定示例产物。 diff --git a/examples/workflows/project-bootstrap-automation/docs/verification.md b/examples/workflows/project-bootstrap-automation/docs/verification.md new file mode 100644 index 0000000..465a702 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/docs/verification.md @@ -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 `。 + +## 真实仓库验证结果 + +目标仓库: + +```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。 diff --git a/examples/workflows/project-bootstrap-automation/docs/workflow-spec.md b/examples/workflows/project-bootstrap-automation/docs/workflow-spec.md new file mode 100644 index 0000000..d688a78 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/docs/workflow-spec.md @@ -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 调用”“真实项目运行展示”“工作流说明文档和架构图”的要求。 diff --git a/examples/workflows/project-bootstrap-automation/examples/demo_outputs/README.md b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/README.md new file mode 100644 index 0000000..441df6c --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/README.md @@ -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 命令计划 diff --git a/examples/workflows/project-bootstrap-automation/examples/demo_outputs/command_log_20260524_080000.json b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/command_log_20260524_080000.json new file mode 100644 index 0000000..7380c91 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/command_log_20260524_080000.json @@ -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": "" + } + ] +} \ No newline at end of file diff --git a/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_bootstrap_report.md b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_bootstrap_report.md new file mode 100644 index 0000000..d84edd3 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_bootstrap_report.md @@ -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。 diff --git a/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_files.json b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_files.json new file mode 100644 index 0000000..7b65eb6 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_files.json @@ -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" +} \ No newline at end of file diff --git a/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_manifest.json b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_manifest.json new file mode 100644 index 0000000..02e80ff --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_manifest.json @@ -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" +} \ No newline at end of file diff --git a/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_summary.md b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_summary.md new file mode 100644 index 0000000..edd7318 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/examples/demo_outputs/puygob236_gitlink-bootstrap-demo_20260524_080000_summary.md @@ -0,0 +1,8 @@ +# GitLink 项目初始化摘要 + +- 目标仓库: `puygob236/gitlink-bootstrap-demo` +- 项目名称: Open Research Toolkit +- 生成时间: 2026-05-24T08:00:00Z +- 初始化文件: 5 个 +- 初始 Issue: 3 个 +- 分支动作: 2 个 diff --git a/examples/workflows/project-bootstrap-automation/examples/sample_project.json b/examples/workflows/project-bootstrap-automation/examples/sample_project.json new file mode 100644 index 0000000..93d90d3 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/examples/sample_project.json @@ -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 + } +} diff --git a/examples/workflows/project-bootstrap-automation/examples/verification_comment_config.json b/examples/workflows/project-bootstrap-automation/examples/verification_comment_config.json new file mode 100644 index 0000000..042741e --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/examples/verification_comment_config.json @@ -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 + } +} diff --git a/examples/workflows/project-bootstrap-automation/scripts/bootstrap_project.go b/examples/workflows/project-bootstrap-automation/scripts/bootstrap_project.go new file mode 100644 index 0000000..cdf0144 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/scripts/bootstrap_project.go @@ -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) + } +} diff --git a/examples/workflows/project-bootstrap-automation/scripts/bootstrap_project_test.go b/examples/workflows/project-bootstrap-automation/scripts/bootstrap_project_test.go new file mode 100644 index 0000000..406ae13 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/scripts/bootstrap_project_test.go @@ -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 +} diff --git a/examples/workflows/project-bootstrap-automation/scripts/run_demo.ps1 b/examples/workflows/project-bootstrap-automation/scripts/run_demo.ps1 new file mode 100644 index 0000000..57345e1 --- /dev/null +++ b/examples/workflows/project-bootstrap-automation/scripts/run_demo.ps1 @@ -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