diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..7bc7dd49 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,107 @@ +{ + "permissions": { + "allow": [ + "Bash(git -C \"C:\\\\Users\\\\Lenovo\\\\Desktop\\\\soft运维\\\\gitlink-cli\" rev-parse --git-common-dir)", + "Bash(git -C \"C:\\\\Users\\\\Lenovo\\\\Desktop\\\\soft运维\\\\gitlink-cli\" rev-parse --git-dir)", + "Bash(git:*)", + "Bash(wc -l \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/skills/gitlink-compliance/SKILL.md\" \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/skills/gitlink-compliance/references/\"*.md)", + "Bash(go build:*)", + "Bash(go vet:*)", + "Bash(wc -l \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/shortcuts/compliance/\"*.go)", + "Bash(wc -l \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/shortcuts/onboard/\"*.go)", + "Bash(# 复制两个 skill 到全局 agents 目录\ncp -r \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/skills/gitlink-compliance\" \"C:/Users/Lenovo/.agents/skills/gitlink-compliance\"\ncp -r \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/skills/gitlink-onboard\" \"C:/Users/Lenovo/.agents/skills/gitlink-onboard\"\n\n# 建立符号链接\ncd \"C:/Users/Lenovo/.claude/skills\" && cmd /c \"mklink /D gitlink-compliance C:\\\\Users\\\\Lenovo\\\\.agents\\\\skills\\\\gitlink-compliance\" && cmd /c \"mklink /D gitlink-onboard C:\\\\Users\\\\Lenovo\\\\.agents\\\\skills\\\\gitlink-onboard\")", + "Bash(cmd.exe /c \"mklink /D C:\\\\Users\\\\Lenovo\\\\.claude\\\\skills\\\\gitlink-compliance C:\\\\Users\\\\Lenovo\\\\.agents\\\\skills\\\\gitlink-compliance\")", + "Bash(powershell.exe -Command \"New-Item -ItemType SymbolicLink -Path 'C:\\\\Users\\\\Lenovo\\\\.claude\\\\skills\\\\gitlink-compliance' -Target 'C:\\\\Users\\\\Lenovo\\\\.agents\\\\skills\\\\gitlink-compliance' -Force\")", + "Bash(cmd.exe /c \"mklink /J C:\\\\Users\\\\Lenovo\\\\.claude\\\\skills\\\\gitlink-compliance C:\\\\Users\\\\Lenovo\\\\.agents\\\\skills\\\\gitlink-compliance\")", + "Bash(cmd.exe /c \"mklink /J C:\\\\Users\\\\Lenovo\\\\.claude\\\\skills\\\\gitlink-onboard C:\\\\Users\\\\Lenovo\\\\.agents\\\\skills\\\\gitlink-onboard\")", + "Bash(ln -s \"/c/Users/Lenovo/.agents/skills/gitlink-compliance\" \"/c/Users/Lenovo/.claude/skills/gitlink-compliance\")", + "Bash(ln -s \"/c/Users/Lenovo/.agents/skills/gitlink-onboard\" \"/c/Users/Lenovo/.claude/skills/gitlink-onboard\")", + "Bash(export GITLINK_TOKEN=\"ce538a7a6b60b189e8ee28c6bf2a2e8186da8141\")", + "Bash(gitlink-cli api:*)", + "Bash(gitlink-cli auth:*)", + "Bash(gitlink-cli user:*)", + "Bash(gitlink-cli issue:*)", + "Bash(./gitlink-cli.exe onboard:*)", + "Bash(./gitlink-cli.exe compliance:*)", + "Bash(gitlink-cli onboard:*)", + "Bash(./gitlink-cli-tmp.exe onboard:*)", + "Bash(printf \"y\\\\nn\\\\n\")", + "Bash(printf \"y\\\\ny\\\\n\")", + "Bash(\"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/template-13.txt\":*)", + "Bash(jq --version)", + "Bash(for f:*)", + "Bash(do echo:*)", + "Bash(bash -n \"$f\")", + "Bash(done)", + "Bash(bash workflows/01-community-ops.sh --help)", + "Bash(bash workflows/02-code-quality-gatekeeper.sh --help)", + "Bash(bash workflows/03-project-init.sh --help)", + "Bash(bash workflows/04-multi-repo-collab.sh --help)", + "Bash(bash workflows/05-contributor-growth.sh --help)", + "Bash(gitlink-cli repo:*)", + "Bash(bash workflows/01-community-ops.sh --owner nudt_zk --repo gitlink-cli --dry-run)", + "Bash(bash workflows/01-community-ops.sh --owner zzx-coder --repo gitlink-cli --dry-run)", + "Bash(bash workflows/02-code-quality-gatekeeper.sh --owner zzx-coder --repo gitlink-cli --dry-run)", + "Bash(bash workflows/03-project-init.sh --owner nudt_zk --name test-workflow-03 --description \"Testing workflow 03\" --lang go --dry-run)", + "Bash(bash workflows/04-multi-repo-collab.sh --org nudt_zk --dry-run)", + "Bash(bash workflows/05-contributor-growth.sh --owner zzx-coder --repo gitlink-cli --dry-run)", + "Bash(ls:*)", + "Bash(claude --version)", + "Bash(claude -p --output-format json)", + "Bash(export CLAUDE_CODE_GIT_BASH_PATH=\"C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe\")", + "Read(//c/Program Files/Git/usr/bin/**)", + "Read(//c/Program Files/Git/bin/**)", + "Bash(export CLAUDE_CODE_GIT_BASH_PATH=\"D:\\\\Git\\\\bin\\\\bash.exe\")", + "Bash(bash workflows/02-code-quality-gatekeeper.sh --owner zzx-coder --repo gitlink-cli --pr-id 21 --dry-run)", + "Bash(source workflows/lib/common.sh)", + "Bash(echo \"EXIT: $?\")", + "Bash(cp:*)", + "Bash(cmd.exe /c \"mklink /J C:\\\\\\\\Users\\\\\\\\Lenovo\\\\\\\\.claude\\\\\\\\skills\\\\\\\\gitlink-research C:\\\\\\\\Users\\\\\\\\Lenovo\\\\\\\\.agents\\\\\\\\skills\\\\\\\\gitlink-research\")", + "Bash(mkdir -p \"C:/Users/Lenovo/.agents/skills/gitlink-research/workflows/lib\")", + "Bash(mkdir -p \"C:/Users/Lenovo/.agents/skills/gitlink-research/workflows/templates\")", + "Bash(mkdir -p \"C:/Users/Lenovo/.agents/skills/gitlink-research/workflows/schemas\")", + "Bash(cp \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/workflows/0\"*\"-research\"*\".sh\" \"C:/Users/Lenovo/.agents/skills/gitlink-research/workflows/\")", + "Bash(cp \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/workflows/1\"*\"-research\"*\".sh\" \"C:/Users/Lenovo/.agents/skills/gitlink-research/workflows/\")", + "Bash(bash workflows/11-research-citation.sh --owner zzx-coder --repo gitlink-cli --format bibtex)", + "Bash(bash -x workflows/11-research-citation.sh --owner zzx-coder --repo gitlink-cli --format bibtex)", + "Bash(gitlink-cli pr:*)", + "Bash(gitlink-cli search:*)", + "Bash(gitlink-cli ci:*)", + "Bash(bash -n workflows/06-research-insights.sh)", + "Bash(timeout 60 bash workflows/06-research-insights.sh --owner zzx-coder --repo gitlink-cli --dry-run)", + "Bash(bash -n workflows/08-research-compliance.sh)", + "Bash(timeout 90 bash workflows/08-research-compliance.sh --owner zzx-coder --repo gitlink-cli --local-path . --dry-run)", + "Bash(timeout:*)", + "Bash(bash -c 'set -euo pipefail; cand_ids=\"\"; analyzed=0; while IFS=\"\t\" read -r c_owner c_repo; do echo \"loop\"; done <<< \"$cand_ids\"; echo \"done loop\"')", + "Bash(bash -c 'cand_ids=\"\"; while IFS=\" \" read -r line; do echo \"GOT: [$line]\"; done <<< \"$cand_ids\"; echo \"AFTER LOOP\"')", + "Bash(xxd C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/workflows/09-research-collab.sh)", + "Bash(xxd)", + "Bash(# Remove debug lines first, then replace here-string with pipe\ncp \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/workflows/09-research-collab.sh\" \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/workflows/09-research-collab.sh.bak\"\n# Restore original and make targeted fix\ngit -C \"C:/Users/Lenovo/Desktop/soft运维/gitlink-cli\" checkout -- workflows/09-research-collab.sh 2>/dev/null\n# Now apply all the fixes we discovered)", + "Bash(./gitlink-cli.exe repo:*)", + "Bash(./gitlink-cli.exe api:*)", + "Bash(./gitlink-cli.exe issue:*)", + "Bash(./gitlink-cli.exe pr:*)", + "mcp__zai-mcp-server__extract_text_from_screenshot", + "mcp__zai-mcp-server__analyze_image", + "Bash(python3 -c \":*)", + "Bash(python -c \":*)", + "Bash(where tesseract:*)", + "Bash(pip list:*)", + "Bash(find C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/internal -type f -name *.go)", + "Bash(find C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/shortcuts -type f -name *.go)", + "Bash(find C:/Users/Lenovo/Desktop/soft运维/gitlink-cli -name *_test.go -type f)", + "Bash(sed 's/\"\"\"\"//g')", + "Bash(sed 's/CallAPI\\(\"\"\"\"//g')", + "Read(//tmp/**)", + "Bash(go test:*)", + "Bash(go run:*)", + "Bash(gitlink-cli.exe wiki:*)", + "Bash(gitlink-cli board:*)", + "Bash(./gitlink-cli.exe board:*)", + "Bash(where gitlink-cli:*)", + "Bash(./gitlink-cli.exe milestone:*)", + "Bash(cd:*)", + "Bash(gitlink-cli file:*)" + ] + } +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 531f3b51..8d108eb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,9 @@ jobs: node-version: '20' registry-url: 'https://registry.npmjs.org' + - name: Run tests + run: go test ./... -cover -race + - name: Build binaries run: | mkdir -p dist diff --git a/Makefile b/Makefile index 7f3ef79b..28df5e6f 100644 --- a/Makefile +++ b/Makefile @@ -15,4 +15,4 @@ clean: rm -f $(BINARY) test: - go test ./... + go test ./... -race diff --git a/README.md b/README.md index 46e0bfa8..ba7ab21b 100644 --- a/README.md +++ b/README.md @@ -5,24 +5,39 @@ [![Go Version](https://img.shields.io/badge/Go-1.26%2B-blue.svg)](https://golang.org) [![npm version](https://img.shields.io/npm/v/@gitlink-ai/cli.svg)](https://www.npmjs.com/package/@gitlink-ai/cli) -The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, CI/CD, and AI-powered workflows, with 40+ commands and 12 AI Agent [Skills](./skills/). +The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for developers and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, CI/CD, and AI-powered workflows, with **40+ commands** and **20+ AI Agent Skills**. **[中文文档](./README.zh-CN.md)** -[Install](#installation--quick-start) · [AI Agent Skills](#ai-agent-skills) · [Auth](#configure--use) · [Commands](#usage-examples) · [Contributing](#related-projects) +--- -## Why gitlink-cli? +## Project Overview -- **Agent-Native Design** — 12 structured [Skills](./skills/) out of the box, compatible with Claude Code, OpenClaw, and other AI platforms — Agents can operate GitLink with zero extra setup -- **Wide Coverage** — Repository, Issue, PR, Branch, Release, CI, Org, Search, User — all core domains covered -- **AI-Friendly & Optimized** — Every command is tested with real Agents, featuring concise parameters, smart defaults, and structured output -- **Cross-Platform** — Runs on macOS, Linux, and Windows (x64/arm64), install via `npm install -g @gitlink-ai/cli` in one command, binary auto-downloaded -- **Open Source, Zero Barriers** — MulanPSL-2.0 license, ready to use, just `npm install` -- **Up and Running in 3 Minutes** — Interactive login or `GITLINK_TOKEN` env var, from install to first API call in just 3 steps -- **Secure & Controllable** — OS-native keychain credential storage, `GITLINK_TOKEN` env var for CI/CD & non-interactive environments, auto git remote context resolution -- **Three-Layer Architecture** — Shortcuts (human & AI friendly) → Raw API (full coverage) → Config (configuration management) +This project delivers automation capabilities for the GitLink open-source collaboration platform across three sub-tasks. -## Features +### Sub-task 1: Extend and Enhance GitLink-CLI Capabilities (50%) + +> Focus: CLI feature expansion | Difficulty: Medium-High | Tech Stack: Go + +Build a robust, cross-platform, AI-friendly CLI tool covering the full GitLink platform. + +**Core Architecture — Three-Layer Design:** + +``` +Shortcuts (40+ commands) + → Clean commands for humans and AI Agents + → issue +create, pr +list, wiki +create ... + ↓ +Raw API (Full endpoint coverage) + → Direct GitLink OpenAPI calls + → api GET /users/me, api POST /.../issues ... + ↓ +Config & Auth (Configuration layer) + → Auth, context resolution, output formatting + → auth login, config init, --format json +``` + +**CLI Feature Matrix:** | Category | Capabilities | |----------|-------------| @@ -43,20 +58,576 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans | 📋 PM | Sprint management, kanban boards, weekly reports | | 🤖 Workflow | AI-powered issue triage, PR review, release notes | +**Key Highlights:** + +- **Agent-Native Design** — Every command tested with real Agents: concise parameters, smart defaults, structured output +- **Cross-Platform** — macOS / Linux / Windows (x64/arm64), one-command install +- **Secure & Controllable** — OS-native keychain storage, `GITLINK_TOKEN` env var for CI/CD +- **Smart Context Resolution** — Auto-resolves owner/repo from `git remote origin` +- **Zero-Barrier Open Source** — MulanPSL-2.0, `npm install -g @gitlink-ai/cli` and go + +### Sub-task 2: Develop and Enrich GitLink Skills (20%) + +> Focus: Agent Skill Development | Difficulty: Medium | No Go required — Markdown + CLI calls + +Skills are structured knowledge bases designed for Claude Code and other AI Agents. Each Skill includes `SKILL.md` (command reference), `REFERENCE.md` (API reference), `TROUBLESHOOTING.md` (troubleshooting), and `examples/` (complete workflow examples). AI Agents can operate GitLink by reading Skills with zero manual documentation lookup. + +**Skills Directory Structure:** + +``` +skills/ +├── gitlink-shared/ # Auth, global params, safety rules, branch conventions +├── gitlink-repo/ # Repository management +├── gitlink-issue/ # Issue management +├── gitlink-pr/ # Pull Request +├── gitlink-branch/ # Branch management +├── gitlink-release/ # Release management +├── gitlink-wiki/ # Wiki management +├── gitlink-webhook/ # Webhook management +├── gitlink-org/ # Organization management +├── gitlink-user/ # User management +├── gitlink-ci/ # CI/CD +├── gitlink-search/ # Search functionality +├── gitlink-pm/ # Project management (Sprints / Kanban / Weekly Reports) +├── gitlink-team/ # Team management +├── gitlink-changelog/ # Release Notes / Changelog generation +├── gitlink-health/ # Project health reports (issue response time, PR merge efficiency, contributor activity) +├── gitlink-contrib/ # Contributor reports (statistics & rankings) +├── gitlink-compliance/ # Security & compliance (license scanning, secret detection, PII scanning) +├── gitlink-issue-triage/ # Issue auto-classification (tracker/priority/labels) +├── gitlink-onboard/ # New contributor onboarding (Good First Issue identification & welcome comments) +├── gitlink-workflow/ # AI workflow orchestration (issue triage, PR review, release notes) +├── gitlink-code-review/ # Intelligent code review (four-dimension scoring, structured review comments) +├── gitlink-research/ # Research assistance (project insights, trend tracking, compliance & reproducibility, collaboration matching, citation generation) +└── gitlink-code-insight/ # Feature panorama (all Shortcuts cataloged with descriptions & examples) +``` + +**All Skills at a Glance:** + +| Skill | Description | Typical Commands | +|-------|-------------|-----------------| +| **gitlink-shared** | Auth, global params, API reference, safety rules | `auth login`, `auth status` | +| **gitlink-repo** | Repository management | `repo +list`, `repo +create`, `repo +info`, `repo +fork` | +| **gitlink-issue** | Issue management | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close` | +| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +review` | +| **gitlink-branch** | Branch management | `branch +list`, `branch +create`, `branch +delete`, `branch +protect` | +| **gitlink-release** | Release management | `release +list`, `release +create`, `release +view` | +| **gitlink-wiki** | Wiki management | `wiki +list`, `wiki +view`, `wiki +create`, `wiki +update`, `wiki +delete` | +| **gitlink-webhook** | Webhook management | `webhook +list`, `webhook +create`, `webhook +test` | +| **gitlink-org** | Organization management | `org +list`, `org +info`, `org +members` | +| **gitlink-user** | User management | `user +me`, `user +info` | +| **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` | +| **gitlink-search** | Search functionality | `search +repos`, `search +users` | +| **gitlink-pm** | Project management | Sprint mgmt, kanban, weekly reports (via Raw API) | +| **gitlink-team** | Team management | `team +list`, `team +create`, `team +add-member` | +| **gitlink-changelog** | Release Notes generation | Auto-collect commits/PRs/Issues, generate structured changelogs | +| **gitlink-health** | Project health reports | Issue response time, PR merge efficiency, contributor activity stats | +| **gitlink-contrib** | Contributor reports | `contrib +report` | +| **gitlink-compliance** | Security & compliance | `compliance +scan`, `compliance +secrets`, `compliance +license` | +| **gitlink-issue-triage** | Issue auto-classification | Auto-detect tracker/priority/labels, generate audit reports | +| **gitlink-onboard** | New contributor onboarding | `onboard +welcome` | +| **gitlink-workflow** | AI workflow orchestration | Issue triage, PR review, repo setup, sprint reports | +| **gitlink-code-review** | Intelligent code review | Four-dimension scoring (quality/security/performance/maintainability) with auto-published reviews | +| **gitlink-research** | Research assistance | Project insights, trend tracking, compliance & reproducibility, collaboration matching, citation generation | +| **gitlink-code-insight** | Feature panorama | Complete Shortcuts catalog with descriptions & examples | + +**How AI Agents Use Skills:** + +``` +User: "Create an Issue on GitLink for me" + ↓ +AI Agent reads gitlink-issue/SKILL.md + ↓ +AI Agent executes: gitlink-cli issue +create -t "..." -b "..." + ↓ +Done! +``` + +AI Agents can automatically: create/manage Issues, create/merge PRs, publish Releases, manage Wiki, triage Issues, generate Release Notes, perform code reviews, and more. + +### Sub-task 3: Build End-to-End Automation Workflows (20%) + +> Focus: Compose existing capabilities to solve real problems | Difficulty: Low-Medium | No low-level coding required + +Combine gitlink-cli commands and Skills into complete, reproducible automation scenarios. Workflows are orchestrated via Shell/PowerShell scripts and support AI Agent-driven execution. + +--- + +**Five Automation Workflows at a Glance:** + +| # | Scenario | Script | Steps | Problem Solved | +|---|----------|--------|-------|---------------| +| 1 | Community Ops Automation | `01-community-ops.sh` | 7 | Untriaged issue backlog, manual weekly reports, manual Release Notes | +| 2 | Code Quality Gatekeeper | `02-code-quality-gatekeeper.sh` | 7 | Low PR review efficiency, inconsistent quality standards, AI code review | +| 3 | One-Click Project Init | `03-project-init.sh` | 8 | Repetitive new project setup, manual README/License/CI/Issue/Milestone config | +| 4 | Multi-Repo Collaboration | `04-multi-repo-collab.sh` | 7 | Scattered cross-repo status, lack of unified dashboard | +| 5 | Contributor Growth System | `05-contributor-growth.sh` | 6 | Hard-to-track contributor activity, lack of incentive mechanisms | + +--- + +### Scenario 1: Community Ops Automation + +**Script**: `workflows/01-community-ops.sh` + +**Workflow**: + +``` +issue +list → Fetch all open Issues + ↓ +Classify by keyword: Bug / Feature / Question / Docs + ↓ +issue +label-add → Auto-apply labels + ↓ +repo +members → Get repository member list +issue +update → Rotate assignment to members + ↓ +pr +list → Count PRs merged this week +issue +list → Count Issues closed this week + ↓ +wiki +create → Publish community weekly report to Wiki + ↓ +release +create → Auto-generate Release Notes +``` + +**Chained Commands**: + +| Step | Command | Purpose | +|------|---------|---------| +| 1 | `issue +list` | Fetch all open Issues | +| 2 | `issue +label-add` | Apply labels by category (bug/feature/question/documentation) | +| 3 | `repo +members` | Fetch repository member list | +| 4 | `issue +update` | Assign owners to Bug/Feature Issues | +| 5 | `pr +list` | Count PRs merged this week | +| 6 | `wiki +create` | Publish community weekly report | +| 7 | `release +create` | Auto-generate Release Notes | + +**Output Value**: Label-based filtering (repo Issues page filterable by label), clear ownership (every Issue has an assignee), Wiki weekly report (team & community can track weekly progress in Wiki), Release Notes (no manual changelog compilation at release time). + +**How to Run**: + +```bash +bash workflows/01-community-ops.sh --owner your-org --repo your-repo +bash workflows/01-community-ops.sh --owner zzx-coder --repo gitlink-cli +``` + +--- + +### Scenario 2: Code Quality Gatekeeper + +**Script**: `workflows/02-code-quality-gatekeeper.sh` + +**Workflow**: + +``` +pr +list → Fetch all open PRs + ↓ +pr +view → Read PR details +pr +files → Get changed file list +pr +diff → Get code diff + ↓ +Load gitlink-code-review Skill: + - Review dimensions & check items + - Scoring rubric (90-100 Excellent, 75-89 Good, ...) + - Issue severity levels (CRITICAL/HIGH/MEDIUM/LOW) + ↓ +┌─────────────────────────────────────────┐ +│ AI Code Review (Claude + Skill) │ +│ Four-Dimension Scoring (25 each, 100 total):│ +│ - Code Quality: complexity, naming, comments│ +│ - Security: SQL injection, XSS, secrets │ +│ - Performance: loop efficiency, resource leaks, N+1│ +│ - Maintainability: duplication, SRP, coupling │ +│ │ +│ Output: │ +│ - Structured issue list (severity+file+│ +│ rule+description+suggestion) │ +│ - Positive practices (positive_notes) │ +│ - Recommendations (recommendations) │ +│ - Total score + PASS/FAIL │ +└─────────────────────────────────────────┘ + ↓ +api POST /reviews → Publish review comments on PR + ↓ +ci +builds → Check CI build status + ↓ +pr +merge → Score >= threshold AND CI passes → auto-merge +``` + +**Sample AI Review Output**: + +``` +Overall Score: 88 / 100 +Code Quality: 23 / 25 +Security: 25 / 25 +Performance: 20 / 25 +Maintainability: 20 / 25 + +Issues Found: + - [LOW] quality: PR entries use (@author) format, commit entries use + (author) without @ prefix — unify to (@author) format + - [LOW] maintainability: Example contributor list updated but full + changelog link still points to old repo + +Positive Notes: + + Change intent is clear, all modified files consistently follow the requirement + + Change scope is reasonable — only docs and examples, no logic changes, minimal risk + +Recommendations: + > Unify author annotation format between PR and commit entries + > Add fallback handling docs for empty author field in collect-data.md +``` + +**Chained Commands**: + +| Step | Command | Purpose | +|------|---------|---------| +| 1 | `pr +list` | Fetch open PR list | +| 2 | `pr +view` | Read PR details (title, author, status) | +| 3 | `pr +files` | Get changed file list | +| 4 | `pr +diff` | Get code diff content | +| 5 | `gitlink-code-review` | Load Skill review dimensions, checks, scoring rubric | +| 6 | `claude -p` | AI performs four-dimension code review per Skill methodology | +| 7 | `api POST .../reviews` | Publish review comments to PR | +| 8 | `pr +merge` | Auto-merge when score >= threshold and CI passes | + +**Output Value**: Structured scoring (every PR gets a 0-100 quality score, team can set a unified merge bar), AI issue checklist (auto-identifies security risks, perf issues, code quality problems), PR comments (review results posted directly on PR), auto-merge (high-quality PRs merged without manual clicks). + +**How to Run**: + +```bash +# Review all open PRs +bash workflows/02-code-quality-gatekeeper.sh --owner your-org --repo your-repo + +# Review a specific PR +bash workflows/02-code-quality-gatekeeper.sh --owner your-org --repo your-repo --pr-id 42 + +# Custom quality threshold (default: 80) +bash workflows/02-code-quality-gatekeeper.sh --owner your-org --repo your-repo --threshold 70 + +# Dry-run mode (no actual merge) +bash workflows/02-code-quality-gatekeeper.sh --owner your-org --repo your-repo --dry-run + +# Example +bash workflows/02-code-quality-gatekeeper.sh --owner zzx-coder --repo gitlink-cli --pr-id 20 +``` + +--- + +### Scenario 3: One-Click Project Init + +**Script**: `workflows/03-project-init.sh` / `03-project-init.ps1` + +**Workflow**: + +``` +repo +create → Create repository + ↓ +git clone → Clone empty repo locally + ↓ +Generate files → README.md (language template) + LICENSE (MIT) + + CONTRIBUTING.md + .gitlink-ci.yml + ↓ +git add/commit/push → Push scaffolding files to repo + ↓ +milestone +create × 3 → Create project milestones: + - v0.1.0 - MVP + - v0.2.0 - Feature Complete + - v1.0.0 - Production Ready + ↓ +issue +create × 5 → Create initial task Issues (with labels): + - Set up CI/CD pipeline + - Write project documentation + - Establish code review process + - Add unit tests + - Configure dependency management + ↓ +branch +protect → Protect master branch + ↓ +release +create → Create v0.1.0 initial release +``` + +**Chained Commands**: + +| Step | Command | Purpose | +|------|---------|---------| +| 1 | `repo +create` | Create new repository | +| 2 | `git clone` | Clone empty repo to local temp directory | +| 3 | File generation | Generate README.md / LICENSE / CONTRIBUTING.md / .gitlink-ci.yml | +| 4 | `git add/commit/push` | Push scaffolding files to master | +| 5 | `milestone +create` | Create 3 project milestones (MVP → Production) | +| 6 | `issue +create` | Create 5 initial Issues with labels | +| 7 | `branch +protect` | Set master branch protection rules | +| 8 | `release +create` | Create v0.1.0 initial release | + +**Output Value**: Turnkey setup (post-clone repo already has README, LICENSE, CI config — start developing immediately), real repo files (README/LICENSE/CONTRIBUTING/CI are actual files, not Wiki pages), milestone roadmap (MVP to Production path already established), standardized Issues (key tasks created, team can claim directly), branch protection (prevents direct push to master, enforces PR workflow), first Release (version management from day one). + +**How to Run**: + +```bash +# Go project +bash workflows/03-project-init.sh --owner your-org --name my-go-app --description "My Go app" --lang go + +# Python project (private) +bash workflows/03-project-init.sh --owner your-org --name my-api --description "REST API service" --lang python --private + +# Node.js project +bash workflows/03-project-init.sh --owner your-org --name my-web --description "Web frontend" --lang node + +# Java project +bash workflows/03-project-init.sh --owner your-org --name my-service --description "Microservice" --lang java +``` + +--- + +### Scenario 4: Multi-Repo Collaboration + +**Script**: `workflows/04-multi-repo-collab.sh` / `04-multi-repo-collab.ps1` + +**Workflow**: + +``` +repo +list → List all repos in the organization + ↓ +For each repo: + issue +list → Get open/closed Issue counts + pr +list → Get open/merged PR counts + release +list → Get latest release & date + ↓ +Compute health score (aligned with gitlink-health Skill · 100-point deduction model): + Core metrics (75 pts): Issue resolution rate < 50% → -25, PR merge rate < 50% → -25, no recent activity → -25 + Auxiliary metrics (25 pts): Open Issues > 20 → -10, Open PRs > 10 → -10, no release > 30 days → -5 + Five-grade rating: Excellent (≥90) → Good (≥70) → Moderate (≥50) → Needs Attention (≥30) → Critical (<30) + ↓ +Generate HTML dashboard: + - Overview cards: repo count, total open Issues, total open PRs, total activity + - Detail table: per-repo Issue/PR/Release status + collapsible details + - Health color bands: green=Excellent / blue=Good / yellow=Moderate / orange=Needs Attention / red=Critical + ↓ +(Optional) release +create → Create same-version Release across all repos +``` + +**Chained Commands**: + +| Step | Command | Purpose | +|------|---------|---------| +| 1 | `repo +list` | List all repos in the organization | +| 2 | `issue +list` | Fetch Issue data per repo | +| 3 | `pr +list` | Fetch PR data per repo | +| 4 | `release +list` | Fetch latest release per repo | +| 5 | Generate HTML | Output visual dashboard | +| 6 | `release +create` | (Optional) Coordinated release | + +**Output Value**: Unified view (one HTML page shows health status of all org repos), health alerts (Open Issues > 10 → orange, > 20 → red), coordinated releases (sync multiple related repos with one command), shareable (HTML file can be sent to team or deployed internally). + +**How to Run**: + +```bash +# Scan all org repos +bash workflows/04-multi-repo-collab.sh --org your-org + +# Specific repos only +bash workflows/04-multi-repo-collab.sh --org your-org --repos "repo-a,repo-b,repo-c" + +# Dashboard + coordinated release +bash workflows/04-multi-repo-collab.sh --org your-org --release v2.0.0 + +# Custom output file + detail limit +bash workflows/04-multi-repo-collab.sh --org your-org --output my-dashboard.html --detail-limit 10 + +# Dry-run mode +bash workflows/04-multi-repo-collab.sh --org your-org --dry-run + +# Example +bash workflows/04-multi-repo-collab.sh --org zzx-coder +``` + +Generates `dashboard.html` in the current directory — open in browser to view. + +--- + +### Scenario 5: Contributor Growth System + +**Script**: `workflows/05-contributor-growth.sh` + +**Workflow**: + +``` +contrib +report → Generate HTML contribution report with ECharts pie chart + ↓ +issue +list → Count Issue activity +pr +list → Count PR activity +api GET /contributors → Get commit counts & API stats + ↓ +Compute contribution score (AHP Weighted Model): + - Code changes: 30% (normalized) + - PRs merged: 25% (normalized) + - Issues created/resolved: 15% (normalized) + - Issue comments: 15% (normalized) + - Team member: 15% (binary yes/no) + → Weighted sum yields 0-100 composite score + ↓ +Assign badges: + Champion >= 80 pts + Core Contributor >= 60 pts + Active Contributor >= 40 pts + Contributor >= 20 pts + Newcomer < 20 pts + ↓ +(Optional) issue +create → Auto-create badge-award Issue + ↓ +wiki +create → Publish leaderboard to Wiki +``` + +**Chained Commands**: + +| Step | Command | Purpose | +|------|---------|---------| +| 1 | `contrib +report` | Generate HTML contribution report (with ECharts) | +| 2 | `issue +list` | Count open/closed Issue activity | +| 3 | `pr +list` | Count open/merged PR activity | +| 4 | `api GET /contributors` | Get API-level contributor stats | +| 5 | `issue +create` | (Optional) Auto-award badges | +| 6 | `wiki +create` | Publish leaderboard to Wiki | + +**Output Value**: HTML contribution report (visual contribution distribution for team meetings), contributor leaderboard (quantifies each person's contribution, publicly transparent), Wiki leaderboard (permanent record, contributors can check rankings anytime), badge incentives (Issue-based badge awards boost sense of achievement and belonging). + +**How to Run**: + +```bash +# Basic run +bash workflows/05-contributor-growth.sh --owner your-org --repo your-repo + +# Custom time period (default: 30 days) +bash workflows/05-contributor-growth.sh --owner your-org --repo your-repo --period 90 + +# Enable auto badge awards +bash workflows/05-contributor-growth.sh --owner your-org --repo your-repo --award + +# Example +bash workflows/05-contributor-growth.sh --owner zzx-coder --repo gitlink-cli --award +``` + +--- + +### Environment Setup + +**1. Install gitlink-cli** + +```bash +gitlink-cli version +# If not installed, build from project root +cd gitlink-cli && make build +``` + +**2. Install jq (scripts use jq to parse CLI JSON output)** + +```bash +# Ubuntu/Debian +sudo apt-get install -y jq +# macOS +brew install jq +``` + +**3. Authenticate** + +```bash +gitlink-cli auth login # Interactive login (recommended) +export GITLINK_TOKEN="your-private-token" # Or env var +gitlink-cli auth status # Verify: should show "✓ Logged in" +``` + +**4. Verify Environment** + +```bash +gitlink-cli issue +list --owner zzx-coder --repo gitlink-cli --state open --limit 3 --format json | jq '.ok' +# Should output: true +``` + +--- + +### Shared Library `lib/common.sh` + +Infrastructure shared across all workflow scripts: + +| Function | Purpose | +|----------|---------| +| `check_auth` | Check auth status (env var or CLI login) | +| `gl_run` | CLI wrapper, auto-appends `--format json` | +| `gl_check` | CLI wrapper + JSON validation + ok field check | +| `json_ok` / `json_get` / `json_error` | JSON parsing utilities | +| `detect_owner_repo` | Auto-detect owner/repo from git remote | +| `log_step` / `log_ok` / `log_warn` / `log_err` | Colorized log output | + +--- + +### Workflow Common Parameters + +| Parameter | Description | +|-----------|-------------| +| `--owner` | Repository owner (auto-resolved from git remote) | +| `--repo` | Repository name (auto-resolved from git remote) | +| `--org` | Organization name (needed for multi-repo scenarios) | +| `--dry-run` | Preview mode, no write operations executed | +| `--help` | Show help message | + +--- + +### Skills Used by Workflows + +| Skill | Used In | Role | +|-------|---------|------| +| `gitlink-code-review` | Scenario 2 | Review dimensions, check items, scoring rubric | +| `gitlink-issue-triage` | Scenario 1 | Issue classification rules (keyword matching, priority detection) | +| `gitlink-changelog` | Scenario 1 | Release Notes generation templates | +| `gitlink-health` | Scenario 4 | Project health scoring model (100-point deduction) | +| `gitlink-onboard` | Scenario 5 | New contributor onboarding & Issue recommendation rules | +| `gitlink-workflow` | All | Base workflow orchestration | + +--- + +### Testing + +```bash +# Run test suite (validates against real GitLink repos) +bash workflows/test.sh + +# Specify a repo +bash workflows/test.sh zzx-coder gitlink-cli +``` + +Test coverage: auth status checks, CLI JSON output format validation, data field extraction (issue/PR/repo/release/member/contributor), PR files and diff content parsing, Issue/PR View interfaces, Wiki/Label list interfaces, `common.sh` utility functions, script syntax validation. + +--- + +### Workflows Directory Structure + +``` +workflows/ +├── lib/ +│ └── common.sh # Shared utility library (auth, JSON parse, CLI wrap, logging) +├── 01-community-ops.sh # Scenario 1: Community Ops Automation +├── 02-code-quality-gatekeeper.sh # Scenario 2: Code Quality Gatekeeper (AI review) +├── 03-project-init.sh # Scenario 3: One-Click Project Init +├── 04-multi-repo-collab.sh # Scenario 4: Multi-Repo Collaboration +├── 05-contributor-growth.sh # Scenario 5: Contributor Growth System +├── test.sh # Test suite +└── README.md # Detailed workflow documentation +``` + +--- + + + ## Installation & Quick Start ### Requirements - Supported platforms: macOS, Linux, Windows (x64/arm64) -- Go 1.26+ — only required for building from source +- Go 1.26+ — only needed for building from source -### Quick Start (Human Users) +### Quick Start (For Users) -> **Note for AI assistants:** If you are an AI Agent helping the user with installation, jump directly to [Quick Start (AI Agent)](#quick-start-ai-agent), which contains all the steps you need to complete. - -#### Install - -**一键安装(推荐) — 无需 npm、无需 Go:** +**One-command install (recommended):** ```bash curl -sSL https://www.gitlink.org.cn/Gitlink/gitlink-cli/raw/master/install.sh | bash @@ -68,131 +639,80 @@ curl -sSL https://www.gitlink.org.cn/Gitlink/gitlink-cli/raw/master/install.sh | npm install -g @gitlink-ai/cli ``` -The binary is auto-downloaded for your platform during `postinstall`. No extra steps needed. +The binary is auto-downloaded for your platform during `postinstall`. No extra steps. **From source:** -Requires Go 1.26+. - ```bash git clone https://www.gitlink.org.cn/Gitlink/gitlink-cli.git cd gitlink-cli make install ``` -> **Windows users:** Run `npm install -g @gitlink-ai/cli` in PowerShell or CMD. For building from source, use `go install .` instead of `make install`. +> Windows users: Run `npm install -g @gitlink-ai/cli` in PowerShell or CMD. For source builds use `go install .` instead of `make install`. -#### Configure & Use +**Configure & Use:** ```bash -# 1. Configure (one-time, interactive guided setup) +# 1. Configure (one-time) gitlink-cli config init # 2. Log in (choose one) gitlink-cli auth login # Username/password (recommended) gitlink-cli auth login --token # Or paste a private token -export GITLINK_TOKEN="your-token" # Or set env var (for CI/CD, non-interactive environments) +export GITLINK_TOKEN="your-token" # Or env var (for CI/CD) # 3. Start using gitlink-cli repo +list ``` -### Quick Start (AI Agent) +### Quick Start (For AI Agents) > The following steps are for AI Agents. Some steps require the user to complete actions in a browser. -**Step 1 — Install** - ```bash -# One command: CLI binary + all Skills auto-installed +# Step 1 — Install (one command: CLI + all Skills) npm install -g @gitlink-ai/cli -``` -**Step 2 — Configure** - -```bash +# Step 2 — Configure gitlink-cli config init -``` -**Step 3 — Login** +# Step 3 — Login +gitlink-cli auth login # Interactive environments +export GITLINK_TOKEN="your-private-token" # Non-interactive (CI/CD, MCP, etc.) -For interactive environments: -```bash -gitlink-cli auth login -``` - -For non-interactive environments (CI/CD, Trae sandbox, MCP, etc.): -```bash -export GITLINK_TOKEN="your-private-token" -``` - -> To get a private token, go to GitLink web → Settings → Private Tokens. - -**Step 4 — Verify** - -```bash +# Step 4 — Verify gitlink-cli user +me ``` -## Installation & Uninstallation +### Uninstall -**Detailed install guide**: [doc/INSTALL.md](./doc/INSTALL.md) -**Detailed uninstall guide**: [doc/UNINSTALL.md](./doc/UNINSTALL.md) - -### Quick Install - -**Linux/macOS**: `curl -sSL https://www.gitlink.org.cn/Gitlink/gitlink-cli/raw/master/install.sh | bash` -**Windows PowerShell**: `powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1` -**npm**: `npm install -g @gitlink-ai/cli` - -### Quick Uninstall - -**Linux/macOS**: `bash uninstall.sh` -**Windows PowerShell**: `.\uninstall.ps1` +**Linux/macOS**: `bash uninstall.sh` +**Windows PowerShell**: `.\uninstall.ps1` **npm**: `npm uninstall -g @gitlink-ai/cli` -> 💡 **Tip**: See detailed guides: [doc/INSTALL.md](./doc/INSTALL.md) | [doc/UNINSTALL.md](./doc/UNINSTALL.md) +--- ## Usage Examples ### Repository Operations ```bash -# List repositories gitlink-cli repo +list - -# View repository info gitlink-cli repo +info --owner Gitlink --repo forgeplus - -# Create a repository gitlink-cli repo +create -n my-project -d "Project description" - -# Fork a repository gitlink-cli repo +fork --owner Gitlink --repo forgeplus ``` ### Issue Management ```bash -# List issues gitlink-cli issue +list --owner Gitlink --repo forgeplus - -# Create an issue gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: Login failed" -b "Steps to reproduce..." - -# View an issue gitlink-cli issue +view --owner Gitlink --repo forgeplus -i 123 - -# Close an issue gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123 - -# Preview batch close without changing data gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run - -# Batch close issues from a CSV file gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv - -# Add a comment gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "Fixed" # Batch delete issues @@ -227,22 +747,11 @@ gitlink-cli issue +replies --number 42 --comment-id 100 ### Pull Requests ```bash -# List PRs gitlink-cli pr +list --owner Gitlink --repo forgeplus - -# Create a PR (same-repo branch) gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: Search feature" --head feature/search --base master - -# Create a PR (from a fork) gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: New feature" --head your_username/forgeplus:feature/my-feature --base master - -# View a PR gitlink-cli pr +view --owner Gitlink --repo forgeplus -i 42 - -# Merge a PR gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42 - -# View changed files gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42 ``` @@ -336,19 +845,10 @@ gitlink-cli pr +comment-delete --id 42 --comment-id 100 ### Branch Management ```bash -# List branches gitlink-cli branch +list --owner Gitlink --repo forgeplus - -# Create a branch gitlink-cli branch +create --name feature/new-feature - -# Delete a branch gitlink-cli branch +delete --name feature/old-feature - -# Protect a branch gitlink-cli branch +protect --name main - -# Remove branch protection gitlink-cli branch +unprotect --name main ``` @@ -377,104 +877,62 @@ gitlink-cli board +stats --owner Gitlink --repo forgeplus ### Webhook Management ```bash -# List all webhooks gitlink-cli webhook +list --owner Gitlink --repo forgeplus - -# Create a webhook gitlink-cli webhook +create --owner Gitlink --repo forgeplus --url https://ci.example.com/webhook --events push,pull_request - -# Create webhook with secret gitlink-cli webhook +create --url https://jenkins.example.com/webhook --secret my-secret-key --events push --description "CI/CD trigger" - -# View webhook details gitlink-cli webhook +info --id 456 - -# Update webhook gitlink-cli webhook +update --id 456 --url https://new-url.example.com/webhook --events push,pull_request,issue - -# Test webhook gitlink-cli webhook +test --id 456 - -# Delete webhook gitlink-cli webhook +delete --id 456 - -# List supported event types gitlink-cli webhook +events ``` ### Release Management ```bash -# List releases gitlink-cli release +list --owner Gitlink --repo forgeplus - -# Create a release gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.0 Stable" -b "Changelog..." - -# View a release gitlink-cli release +view --owner Gitlink --repo forgeplus -i ``` -### CI/CD Operations - -```bash -# List builds -gitlink-cli ci +list --owner Gitlink --repo forgeplus - -# View build log -gitlink-cli ci +log --owner Gitlink --repo forgeplus -i - -# Restart a build -gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i -``` - ### Wiki Management ```bash -# List wiki pages gitlink-cli wiki +list --owner Gitlink --repo forgeplus - -# View a wiki page gitlink-cli wiki +view --owner Gitlink --repo forgeplus --title "Getting Started" - -# Create a wiki page gitlink-cli wiki +create --title "New Page" --content "Page content" - -# Update a wiki page (overwrite) -gitlink-cli wiki +update --title "New Page" --cover "Updated content" --owner Gitlink --repo forgeplus - -# Append content to a wiki page +gitlink-cli wiki +update --title "New Page" --cover "Updated content" --owner Gitlink --repo forgeplus gitlink-cli wiki +update --title "New Page" --add "Appended content" --owner Gitlink --repo forgeplus - -# Delete a wiki page gitlink-cli wiki +delete --title "New Page" --owner Gitlink --repo forgeplus ``` ### Search ```bash -# Search repositories gitlink-cli search +repos -k "machine learning" - -# Search users gitlink-cli search +users -k "zhangsan" ``` +### CI/CD + +```bash +gitlink-cli ci +list --owner Gitlink --repo forgeplus +gitlink-cli ci +log --owner Gitlink --repo forgeplus -i +gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i +``` + ### Raw API For endpoints not covered by shortcuts, use the Raw API directly: ```bash -# GET request gitlink-cli api GET /users/me - -# POST request gitlink-cli api POST /Gitlink/forgeplus/issues --body '{"subject":"test","description":"..."}' - -# With query parameters gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5' ``` +--- + ## Global Parameters | Parameter | Description | Example | @@ -486,46 +944,25 @@ gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5' **Automatic context resolution:** When running inside a git repository, `--owner` and `--repo` are automatically resolved from `git remote origin`. -## Branch Conventions +--- -gitlink-cli supports bidirectional code sync between GitHub and GitLink: +## Branch Conventions | Platform | Default Branch | |----------|---------------| | GitHub | `main` | | GitLink | `master` | -**Push to GitLink from local:** +Push to GitLink: ```bash -# Method 1: Use git command directly git push gitlink main:master - -# Method 2: Configure git remote +# Or configure git remote git config remote.gitlink.push refs/heads/main:refs/heads/master git push gitlink ``` -## AI Agent Skills - -The `skills/` directory contains 12 Agent Skill files for AI-automated GitLink operations. - -See [skills/README.md](skills/README.md) for details. - -| Skill | Description | -|-------|-------------| -| `gitlink-shared` | Authentication, global parameters, safety rules, API notes | -| `gitlink-repo` | Repository operations (create, view, delete, fork, etc.) | -| `gitlink-issue` | Issue operations (create, update, close, comment, etc.) | -| `gitlink-pr` | Pull request operations (create, merge, review, etc.) | -| `gitlink-branch` | Branch management (create, delete, list, protect, unprotect) | -| `gitlink-release` | Release management (create, view, delete, etc.) | -| `gitlink-ci` | CI/CD operations (builds, logs, etc.) | -| `gitlink-search` | Search (repositories, users, etc.) | -| `gitlink-org` | Organization management (members, teams, etc.) | -| `gitlink-user` | User management (profile info, etc.) | -| `gitlink-pm` | Project management (sprints, kanban, weekly reports, etc.) | -| `gitlink-workflow` | AI-powered workflows (issue triage, PR review, release notes, etc.) | +--- ## Project Structure @@ -542,7 +979,7 @@ gitlink-cli/ │ ├── client/ # HTTP client + pagination │ ├── config/ # Config file management │ ├── context/ # Git remote resolution -│ └── output/ # Envelope + formatter +│ └── output/ # Output envelope + formatter ├── shortcuts/ # Shortcut implementations │ ├── common/ # Framework (types, runner) │ ├── repo/ # Repository shortcuts @@ -558,35 +995,56 @@ gitlink-cli/ │ ├── wiki/ # Wiki shortcuts │ ├── file/ # File & code shortcuts │ ├── milestone/ # Milestone shortcuts + │ └── register.go # Registration entry point ├── skills/ # AI Agent Skills -│ ├── README.md # Skills guide +│ ├── README.md # Skills detailed guide │ ├── gitlink-shared/ # Shared rules -│ ├── gitlink-repo/ # Repository skill -│ ├── gitlink-issue/ # Issue skill -│ ├── gitlink-pr/ # PR skill -│ ├── gitlink-pm/ # Project management skill +│ ├── gitlink-repo/ issue/ pr/ branch/ release/ ... +│ ├── gitlink-health/ # Project health +│ ├── gitlink-changelog/ # Release Notes +│ ├── gitlink-code-review/ # AI code review +│ ├── gitlink-research/ # Research assistance │ └── ... +├── workflows/ # End-to-end automation workflows +│ ├── README.md # Workflow documentation +│ ├── lib/common.sh # Shared utility library +│ ├── 01-community-ops.sh # Community Ops Automation +│ ├── 02-code-quality-gatekeeper.sh # Code Quality Gatekeeper +│ ├── 03-project-init.sh # One-Click Project Init +│ ├── 04-multi-repo-collab.sh # Multi-Repo Collaboration +│ ├── 05-contributor-growth.sh # Contributor Growth System +│ └── test.sh # Test suite ├── doc/ # Design documents -│ ├── Design.md +│ ├── design.md │ ├── CODE_SYNC_STRATEGY_FINAL.md │ └── ... ├── main.go ├── Makefile ├── go.mod -└── README.md +├── README.md +└── README.zh-CN.md ``` -## Documentation +--- -- [Skills Guide](skills/README.md) — AI Agent Skills detailed documentation -- [Design Document](doc/design.md) — Architecture design and development plan +## Documentation Index + +- [Skills Guide](skills/README.md) — Detailed AI Agent Skills documentation +- [Workflow Documentation](workflows/README.md) — Detailed workflow documentation +- [Design Document](doc/design.md) — Architecture design & development plan +- [Installation Guide](doc/INSTALL.md) — Detailed install steps +- [Uninstall Guide](doc/UNINSTALL.md) — Detailed uninstall steps +- [API Reference](skills/gitlink-shared/REFERENCE.md) — Complete API reference +- [Troubleshooting](skills/gitlink-shared/TROUBLESHOOTING.md) — Common issues & solutions + +--- ## FAQ ### Q: How do I use gitlink-cli in scripts? -Use the `GITLINK_TOKEN` environment variable + `--format json` for structured output: +Use the `GITLINK_TOKEN` env var + `--format json`: ```bash export GITLINK_TOKEN="your-private-token" @@ -604,45 +1062,39 @@ gitlink-cli issue +list # Automatically uses the current repository ### Q: What if my token expires? -Re-authenticate: - ```bash -# Username/password login -gitlink-cli auth login - -# Or use a private token (generate at GitLink web → Settings → Private Tokens) -gitlink-cli auth login --token +gitlink-cli auth login # Username/password login +gitlink-cli auth login --token # Or use a private token ``` -### Q: How do I use gitlink-cli in CI/CD or non-interactive environments (e.g. Trae sandbox)? +### Q: How do I use gitlink-cli in CI/CD or non-interactive environments? Set the `GITLINK_TOKEN` environment variable — no `auth login` needed: ```bash export GITLINK_TOKEN="your-private-token" gitlink-cli repo +list # Ready to use -gitlink-cli auth status # Shows "✓ Logged in via GITLINK_TOKEN environment variable" ``` -Priority: `GITLINK_TOKEN` env var > keyring/file stored token. When the env var is not set, the original interactive login flow works as before. +Priority: `GITLINK_TOKEN` env var > keychain/file-stored token. -### Q: What if npm installs successfully but `gitlink-cli` reports a missing binary? +### Q: npm install succeeded but `gitlink-cli` reports a missing binary? -Reinstall first: - -```bash -npm install -g @gitlink-ai/cli -``` - -If the error persists, check whether the release page contains the asset for your platform, for example `gitlink-cli__windows_amd64.zip` on Windows x64. You can also download the binary manually from the release page or build from source with `go install .`. +Reinstall first: `npm install -g @gitlink-ai/cli`. If the error persists, check the release page for your platform's asset, or download the binary manually / build from source with `go install .`. ### Q: Where are credentials stored on Windows? -gitlink-cli uses Windows Credential Manager for secure token storage. If Credential Manager is unavailable, it automatically falls back to file storage (`~/.config/gitlink-cli/credentials`). +gitlink-cli uses Windows Credential Manager for secure token storage. If Credential Manager is unavailable, it falls back to file storage (`~/.config/gitlink-cli/credentials`). -### Q: Where can I find the full API reference? +--- -See [skills/gitlink-shared/REFERENCE.md](skills/gitlink-shared/REFERENCE.md). +## Related Resources + +- [gitlink-bisync](https://www.gitlink.org.cn/wbtiger/gitlink-bisync) — Bidirectional code sync system +- [Test Report](doc/SKILLS_TEST_REPORT_2026-04-02.md) — Skills functional test report +- [Code Sync Strategy](doc/CODE_SYNC_STRATEGY_FINAL.md) — GitHub ↔ GitLink sync design + +--- ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index c778fd1d..d5669a6b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -5,26 +5,41 @@ [![Go Version](https://img.shields.io/badge/Go-1.26%2B-blue.svg)](https://golang.org) [![npm version](https://img.shields.io/npm/v/@gitlink-ai/cli.svg)](https://www.npmjs.com/package/@gitlink-ai/cli) -[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 11 个 AI Agent [Skills](./skills/)。 +GitLink 官方命令行工具,面向开发者与 AI Agent 设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、CI/CD 与 AI 自动化工作流,提供 **40+ 命令**与 **20+ AI Agent Skills**。 **[English](./README.md)** -[安装](#安装与快速上手) · [AI Agent Skills](#ai-agent-skills) · [认证](#配置与使用) · [命令](#使用示例) · [贡献](#相关项目) +--- -## 为什么选择 gitlink-cli? +## 项目概述 -- **Agent-Native 设计** — 开箱即用 11 个结构化 [Skills](./skills/),兼容 Claude Code — Agent 零配置即可操作 GitLink -- **广泛覆盖** — 仓库、Issue、PR、分支、Release、CI、组织、搜索、用户 — 核心功能全覆盖 -- **AI 友好 & 优化** — 每条命令都经过真实 Agent 测试,简洁参数、智能默认值、结构化输出 -- **跨平台** — macOS、Linux、Windows (x64/arm64) 全支持,`npm` 一条命令安装 -- **开源零门槛** — 木兰宽松许可证第2版(MulanPSL-2.0),`npm install` 即用 -- **3 分钟上手** — 交互式登录或 `GITLINK_TOKEN` 环境变量,从安装到首次 API 调用仅需 3 步 -- **安全可控** — OS 原生 keychain 凭证存储,`GITLINK_TOKEN` 环境变量支持 CI/CD 和非交互环境,自动 git remote 上下文解析 -- **三层架构** — Shortcuts(人+AI友好)→ Raw API(全覆盖)→ Config(配置管理) +本项目围绕 GitLink 开源协作平台的自动化能力建设,涵盖三个子任务方向。 -## 功能一览 +### 子任务一:增加和完善 GitLink-CLI 能力(50%) -| 分类 | 能力 | +> 定位:扩展 CLI 功能 | 难度:中高 | 技术栈:Go + +构建健壮、跨平台、AI 友好的命令行工具,覆盖 GitLink 平台全功能。 + +**核心架构 — 三层设计:** + +``` +Shortcuts(40+ 命令) + → 面向人类和 AI Agent 的简洁命令 + → issue +create, pr +list, wiki +create ... + ↓ +Raw API(全接口覆盖) + → 直接调用 GitLink OpenAPI + → api GET /users/me, api POST /.../issues ... + ↓ +Config & Auth(配置管理层) + → 认证、上下文解析、输出格式 + → auth login, config init, --format json +``` + +**CLI 能力一览:** + +| 类别 | 能力 | |------|------| | 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息 | | 🐛 Issue | 创建、更新、关闭、评论 Issue,6 个批量操作,元数据查询,评论管理 | @@ -40,171 +55,664 @@ | 🔍 搜索 | 搜索仓库、用户 | | 👤 用户 | 查看用户资料和信息 | | 📋 看板 | 查看看板、按状态/指派人/优先级筛选、移动任务、指派人员、工作负载分析 | + | 📋 项目管理 | Sprint 管理、看板、周报 | -| 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes | +| 🔌 Raw API | 直接调用任意 GitLink OpenAPI 端点 | -## 安装与快速上手 +**技术亮点:** -### 前置条件 +- **Agent-Native 设计** — 每个命令经过真实 Agent 测试,参数精简、默认值智能、输出结构化 +- **跨平台** — macOS / Linux / Windows (x64/arm64),一键安装 +- **安全可控** — OS 原生密钥链存储、`GITLINK_TOKEN` 环境变量支持 CI/CD +- **智能上下文解析** — 自动从 `git remote origin` 解析 owner/repo,减少参数输入 +- **开箱即用** — MulanPSL-2.0 开源,`npm install -g @gitlink-ai/cli` 一条命令 -- 支持平台:macOS、Linux、Windows(x64/arm64) -- Go 1.26+ — 仅从源码构建时需要 +### 子任务二:编写和丰富 GitLink Skills(20%) -### 快速上手(人类用户) +> 定位:开发 Agent Skill | 难度:中 | 无需 Go,Markdown + CLI 调用即可 -> **AI 助手请注意:** 如果你是帮助用户安装的 AI Agent,请直接跳到 [快速上手(AI Agent)](#快速上手ai-agent),其中包含你需要完成的所有步骤。 +Skills 是为 Claude Code 及其他 AI Agent 设计的结构化知识库。每个 Skill 包含 `SKILL.md`(命令参考)、`REFERENCE.md`(API 参考)、`TROUBLESHOOTING.md`(故障排查)和 `examples/`(完整工作流示例)。AI Agent 通过读取 Skills 即可自动操作 GitLink 平台,无需手动查阅文档。 -#### 安装 +**Skills 目录结构:** -选择以下**任一**方式: +``` +skills/ +├── gitlink-shared/ # 认证、全局参数、安全规则、分支约定 +├── gitlink-repo/ # 仓库管理 +├── gitlink-issue/ # Issue 管理 +├── gitlink-pr/ # Pull Request +├── gitlink-branch/ # 分支管理 +├── gitlink-release/ # 版本发布 +├── gitlink-wiki/ # Wiki 管理 +├── gitlink-webhook/ # Webhook 管理 +├── gitlink-org/ # 组织管理 +├── gitlink-user/ # 用户管理 +├── gitlink-ci/ # CI/CD +├── gitlink-search/ # 搜索功能 +├── gitlink-pm/ # 项目管理(Sprint / 看板 / 周报) +├── gitlink-team/ # 团队管理 +├── gitlink-changelog/ # Release Notes / Changelog 生成 +├── gitlink-health/ # 项目健康度报告(Issue 响应时间、PR 合并效率、贡献者活跃度) +├── gitlink-contrib/ # 贡献报告(统计与排名) +├── gitlink-compliance/ # 安全与合规(许可证扫描、敏感信息检测、PII 扫描) +├── gitlink-issue-triage/ # Issue 自动分类(tracker/priority/labels 判定) +├── gitlink-onboard/ # 新人引导(Good First Issue 识别与欢迎评论) +├── gitlink-workflow/ # AI 工作流编排(Issue 分类、PR Review、Release Notes) +├── gitlink-code-review/ # 智能代码审查(四维度评分、结构化 Review 意见) +├── gitlink-research/ # 科研辅助(项目洞察、热点追踪、合规复现、协作匹配、论文引用) +└── gitlink-code-insight/ # 功能全景索引(全部 Shortcuts 分类展示) +``` -**方式 1 — 一键安装(推荐,无需 npm、无需 Go):** +**所有 Skills 概览:** + +| Skill | 说明 | 典型命令 | +|-------|------|----------| +| **gitlink-shared** | 认证、全局参数、API 参考、安全规则 | `auth login`, `auth status` | +| **gitlink-repo** | 仓库管理 | `repo +list`, `repo +create`, `repo +info`, `repo +fork` | +| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close` | +| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +review` | +| **gitlink-branch** | 分支管理 | `branch +list`, `branch +create`, `branch +delete`, `branch +protect` | +| **gitlink-release** | 版本发布 | `release +list`, `release +create`, `release +view` | +| **gitlink-wiki** | Wiki 管理 | `wiki +list`, `wiki +view`, `wiki +create`, `wiki +update`, `wiki +delete` | +| **gitlink-webhook** | Webhook 管理 | `webhook +list`, `webhook +create`, `webhook +test` | +| **gitlink-org** | 组织管理 | `org +list`, `org +info`, `org +members` | +| **gitlink-user** | 用户管理 | `user +me`, `user +info` | +| **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` | +| **gitlink-search** | 搜索功能 | `search +repos`, `search +users` | +| **gitlink-pm** | 项目管理 | Sprint 管理、看板、周报(通过 Raw API) | +| **gitlink-team** | 团队管理 | `team +list`, `team +create`, `team +add-member` | +| **gitlink-changelog** | Release Notes 生成 | 自动收集 commits/PR/Issue,生成结构化版本说明 | +| **gitlink-health** | 项目健康度报告 | Issue 响应时间、PR 合并效率、贡献者活跃度统计 | +| **gitlink-contrib** | 贡献报告 | `contrib +report` | +| **gitlink-compliance** | 安全与合规 | `compliance +scan`, `compliance +secrets`, `compliance +license` | +| **gitlink-issue-triage** | Issue 自动分类 | 自动判定 tracker/priority/labels,生成审计报告 | +| **gitlink-onboard** | 新人引导 | `onboard +welcome` | +| **gitlink-workflow** | AI 工作流编排 | Issue 分类、PR Review、仓库初始化、Sprint 报告 | +| **gitlink-code-review** | 智能代码审查 | 四维度评分(代码质量/安全性/性能/可维护性)、自动发布 Review 评论 | +| **gitlink-research** | 科研辅助 | 项目洞察、热点追踪、合规复现、协作匹配、进度预警、论文引用 | +| **gitlink-code-insight** | 功能全景索引 | 全部 Shortcuts 分类索引,含说明和示例 | + +**AI Agent 如何使用 Skills:** + +``` +用户: "帮我在 GitLink 上创建一个 Issue" + ↓ +AI Agent 读取 gitlink-issue/SKILL.md + ↓ +AI Agent 执行: gitlink-cli issue +create -t "..." -b "..." + ↓ +完成! +``` + +AI Agent 可以自动完成:创建/管理 Issue、创建/合并 PR、发布 Release、管理 Wiki、分类 Issue、生成 Release Notes、执行代码审查等。 + +### 子任务三:构建端到端自动化工作流(20%) + +> 定位:组合现有能力解决实际问题 | 难度:低-中 | 无需写底层代码 + +组合 gitlink-cli 命令与 Skills,串联多个步骤形成完整的可复现自动化场景。工作流通过 Shell/PowerShell 脚本编排,支持 AI Agent 驱动执行。 + +--- + +**五大自动化工作流总览:** + +| # | 场景 | 脚本 | 串联命令数 | 解决什么问题 | +|---|------|------|-----------|-------------| +| 1 | 社区运营自动化 | `01-community-ops.sh` | 7 | Issue 积压无人处理、周报手写、Release Notes 手动整理 | +| 2 | 代码质量看门人 | `02-code-quality-gatekeeper.sh` | 7 | PR 审查效率低、质量标准不统一、AI 代码审查 | +| 3 | 项目一键初始化 | `03-project-init.sh` | 8 | 新建项目重复劳动多、README/License/CI/Issue/里程碑手动配 | +| 4 | 多仓库协同 | `04-multi-repo-collab.sh` | 7 | 跨仓库状态分散、缺乏统一视图 | +| 5 | 贡献者成长体系 | `05-contributor-growth.sh` | 6 | 贡献者活跃度难追踪、缺乏激励机制 | + +--- + +### 场景一:社区运营自动化 + +**脚本**:`workflows/01-community-ops.sh` + +**工作流程**: + +``` +issue +list → 读取所有 open Issue + ↓ +按关键词分类:Bug / Feature / Question / Docs + ↓ +issue +label-add → 自动打标签 + ↓ +repo +members → 获取仓库成员列表 +issue +update → 轮询分配负责人 + ↓ +pr +list → 统计本周合并的 PR +issue +list → 统计本周关闭的 Issue + ↓ +wiki +create → 发布社区周报到 Wiki + ↓ +release +create → 自动生成 Release Notes +``` + +**串联的命令**: + +| 步骤 | 命令 | 作用 | +|------|------|------| +| 1 | `issue +list` | 获取所有 open Issue | +| 2 | `issue +label-add` | 按分类打标签(bug/feature/question/documentation) | +| 3 | `repo +members` | 获取仓库成员列表 | +| 4 | `issue +update` | 给 Bug/Feature Issue 分配负责人 | +| 5 | `pr +list` | 统计本周合并的 PR | +| 6 | `wiki +create` | 发布社区周报 | +| 7 | `release +create` | 自动生成 Release Notes | + +**输出价值**:标签分类(仓库 Issue 页面可按标签筛选)、负责人分配(每个 Issue 有明确负责人)、Wiki 周报(团队和社区用户在 Wiki 查看每周进展)、Release Notes(发版时无需手动整理变更)。 + +**运行方式**: + +```bash +bash workflows/01-community-ops.sh --owner 你的组织 --repo 你的仓库 +bash workflows/01-community-ops.sh --owner zzx-coder --repo gitlink-cli +``` + +--- + +### 场景二:代码质量看门人 + +**脚本**:`workflows/02-code-quality-gatekeeper.sh` + +**工作流程**: + +``` +pr +list → 获取所有 open PR + ↓ +pr +view → 读取 PR 详情 +pr +files → 获取变更文件列表 +pr +diff → 获取代码差异 + ↓ +加载 gitlink-code-review Skill: + - 审查维度与检查项 + - 评分标准(90-100 优秀,75-89 良好,...) + - 问题严重级别(CRITICAL/HIGH/MEDIUM/LOW) + ↓ +┌─────────────────────────────────────────┐ +│ AI 代码审查(Claude + Skill) │ +│ 四维度评分(各 0-25,总分 100): │ +│ - 代码质量:复杂度、命名、注释 │ +│ - 安全性:SQL注入、XSS、敏感信息 │ +│ - 性能:循环效率、资源泄漏、N+1 │ +│ - 可维护性:重复、职责单一、耦合 │ +│ │ +│ 输出: │ +│ - 结构化问题清单(severity+file+ │ +│ rule+description+suggestion) │ +│ - 优秀实践(positive_notes) │ +│ - 改进建议(recommendations) │ +│ - 总分 + PASS/FAIL │ +└─────────────────────────────────────────┘ + ↓ +api POST /reviews → 发布审查评论到 PR + ↓ +ci +builds → 检查 CI 构建状态 + ↓ +pr +merge → 分数 >= 阈值 且 CI 通过 → 自动合并 +``` + +**AI 审查示例输出**: + +``` +Overall Score: 88 / 100 +Code Quality: 23 / 25 +Security: 25 / 25 +Performance: 20 / 25 +Maintainability: 20 / 25 + +Issues Found: + - [LOW] quality: 条目格式说明中 PR 条目用 (@作者) 带括号, + commit 条目用 (作者名) 不带 @ 前缀 + → 统一格式规范,建议 commit 条目也使用 (@作者) 格式 + - [LOW] maintainability: 示例中贡献者列表变更但完整变更日志链接仍指向旧仓库 + → 将变更日志链接中的 OWNER 也更新为与示例贡献者一致 + +Positive Notes: + + 变更目的清晰,所有文件的修改一致地贯彻了需求,无遗漏 + + 变更范围合理,仅修改文档和示例,不涉及代码逻辑变更,风险极低 + +Recommendations: + > 统一 PR 条目和 commit 条目的作者标注格式 + > 在 collect-data.md 中补充 author 字段为空时的降级处理说明 +``` + +**串联的命令**: + +| 步骤 | 命令 | 作用 | +|------|------|------| +| 1 | `pr +list` | 获取 open PR 列表 | +| 2 | `pr +view` | 读取 PR 详情(标题、作者、状态) | +| 3 | `pr +files` | 获取变更文件列表 | +| 4 | `pr +diff` | 获取代码差异内容 | +| 5 | `gitlink-code-review` | 加载 Skill 的审查维度、检查项、评分标准 | +| 6 | `claude -p` | AI 按 Skill 方法论进行四维度代码审查 | +| 7 | `api POST .../reviews` | 将审查评论发布到 PR | +| 8 | `pr +merge` | 质量分 >= 阈值且 CI 通过时自动合并 | + +**输出价值**:结构化评分(每个 PR 有 0-100 的质量评分,团队可设定统一合并门槛)、AI 问题清单(自动列出安全隐患、性能问题、代码质量问题)、PR 评论(审查结果直接评论在 PR 上)、自动合并(高质量 PR 无需人工点击)。 + +**运行方式**: + +```bash +# 审查所有 open PR +bash workflows/02-code-quality-gatekeeper.sh --owner 你的组织 --repo 你的仓库 + +# 审查指定 PR +bash workflows/02-code-quality-gatekeeper.sh --owner 你的组织 --repo 你的仓库 --pr-id 42 + +# 自定义质量阈值(默认 80) +bash workflows/02-code-quality-gatekeeper.sh --owner 你的组织 --repo 你的仓库 --threshold 70 + +# 预览模式(不实际合并) +bash workflows/02-code-quality-gatekeeper.sh --owner 你的组织 --repo 你的仓库 --dry-run + +# 示例 +bash workflows/02-code-quality-gatekeeper.sh --owner zzx-coder --repo gitlink-cli --pr-id 20 +``` + +--- + +### 场景三:项目一键初始化 + +**脚本**:`workflows/03-project-init.sh` / `03-project-init.ps1` + +**工作流程**: + +``` +repo +create → 创建仓库 + ↓ +git clone → 克隆空仓库到本地 + ↓ +生成文件 → README.md(语言模板)+ LICENSE(MIT) + + CONTRIBUTING.md + .gitlink-ci.yml + ↓ +git add/commit/push → 将脚手架文件推送到仓库 + ↓ +milestone +create × 3 → 创建项目里程碑: + - v0.1.0 - MVP + - v0.2.0 - Feature Complete + - v1.0.0 - Production Ready + ↓ +issue +create × 5 → 创建初始待办 Issue(打标签): + - 搭建 CI/CD 流水线 + - 编写项目文档 + - 建立代码审查流程 + - 添加单元测试 + - 配置依赖管理 + ↓ +branch +protect → 保护 master 分支 + ↓ +release +create → 创建 v0.1.0 初始版本 +``` + +**串联的命令**: + +| 步骤 | 命令 | 作用 | +|------|------|------| +| 1 | `repo +create` | 创建新仓库 | +| 2 | `git clone` | 克隆空仓库到本地临时目录 | +| 3 | 文件生成 | 生成 README.md / LICENSE / CONTRIBUTING.md / .gitlink-ci.yml | +| 4 | `git add/commit/push` | 推送脚手架文件到 master 分支 | +| 5 | `milestone +create` | 创建 3 个项目里程碑(MVP → Production) | +| 6 | `issue +create` | 创建 5 个初始 Issue 并打标签 | +| 7 | `branch +protect` | 设置 master 分支保护规则 | +| 8 | `release +create` | 创建 v0.1.0 初始版本 | + +**输出价值**:开箱即用(克隆后仓库已有 README、LICENSE、CI 配置,可直接开始开发)、仓库内文件(README/LICENSE/CONTRIBUTING/CI 是真实文件,不是 Wiki 页面)、里程碑规划(从 MVP 到正式版的路线图已建立)、标准化 Issue(关键待办已创建好,团队可直接认领)、分支保护(防止直接 push 到 master,强制走 PR 流程)、首个 Release(项目从创建之初就有版本管理)。 + +**运行方式**: + +```bash +# Go 项目 +bash workflows/03-project-init.sh --owner 你的组织 --name my-go-app --description "我的Go应用" --lang go + +# Python 项目(私有) +bash workflows/03-project-init.sh --owner 你的组织 --name my-api --description "REST API服务" --lang python --private + +# Node.js 项目 +bash workflows/03-project-init.sh --owner 你的组织 --name my-web --description "Web前端" --lang node + +# Java 项目 +bash workflows/03-project-init.sh --owner 你的组织 --name my-service --description "微服务" --lang java +``` + +--- + +### 场景四:多仓库协同 + +**脚本**:`workflows/04-multi-repo-collab.sh` / `04-multi-repo-collab.ps1` + +**工作流程**: + +``` +repo +list → 列出组织下所有仓库 + ↓ +对每个仓库: + issue +list → 获取 open/closed Issue + pr +list → 获取 open/merged PR + release +list → 获取最新 Release + ↓ +计算健康度评分(对齐 gitlink-health Skill · 100 分扣分制): + 核心指标(75 分):Issue 解决率 < 50% 扣 25、PR 合并率 < 50% 扣 25、近期无活动扣 25 + 辅助指标(25 分):Open Issue > 20 扣 10、Open PR > 10 扣 10、超 30 天无 Release 扣 5 + 五级评定:优秀(≥90)→ 良好(≥70)→ 一般(≥50)→ 需关注(≥30)→ 严重(<30) + ↓ +生成 HTML 仪表盘: + - 总览卡片:仓库数、Open Issue 数、Open PR 数、总活动量 + - 详情表格:每个仓库的 Issue/PR/Release 状态 + 折叠明细 + - 健康度色带:绿色优秀 / 蓝色良好 / 黄色一般 / 橙色需关注 / 红色严重 + ↓ +(可选)release +create → 一键为所有仓库创建同一版本号的 Release +``` + +**串联的命令**: + +| 步骤 | 命令 | 作用 | +|------|------|------| +| 1 | `repo +list` | 列出组织下所有仓库 | +| 2 | `issue +list` | 获取每个仓库的 Issue 数据 | +| 3 | `pr +list` | 获取每个仓库的 PR 数据 | +| 4 | `release +list` | 获取每个仓库的最新 Release | +| 5 | 生成 HTML | 输出可视化仪表盘 | +| 6 | `release +create` | (可选)协调发版 | + +**输出价值**:统一视图(一个 HTML 页面看到组织所有仓库的健康状态)、健康度预警(Open Issue 超 10 标橙色,超 20 标红色)、协调发版(多个关联仓库需要同步发版时,一条命令搞定)、可分享(HTML 文件可直接发给团队或部署到内部网站)。 + +**运行方式**: + +```bash +# 扫描组织下所有仓库 +bash workflows/04-multi-repo-collab.sh --org 你的组织 + +# 只看指定仓库 +bash workflows/04-multi-repo-collab.sh --org 你的组织 --repos "repo-a,repo-b,repo-c" + +# 生成仪表盘 + 协调发版 +bash workflows/04-multi-repo-collab.sh --org 你的组织 --release v2.0.0 + +# 自定义输出文件 + 详情限制条数 +bash workflows/04-multi-repo-collab.sh --org 你的组织 --output my-dashboard.html --detail-limit 10 + +# 预览模式 +bash workflows/04-multi-repo-collab.sh --org 你的组织 --dry-run + +# 示例 +bash workflows/04-multi-repo-collab.sh --org zzx-coder +``` + +运行后在当前目录生成 `dashboard.html`,浏览器打开即可查看。 + +--- + +### 场景五:贡献者成长体系 + +**脚本**:`workflows/05-contributor-growth.sh` + +**工作流程**: + +``` +contrib +report → 生成带 ECharts 饼图的 HTML 贡献报告 + ↓ +issue +list → 统计 Issue 活动 +pr +list → 统计 PR 活动 +api GET /contributors → 获取提交数等 API 统计 + ↓ +计算贡献分数(AHP 权重模型): + - 代码变更量:30%(归一化处理) + - PR 被合并:25%(归一化处理) + - 创建/解决 Issue:15%(归一化处理) + - Issue 评论:15%(归一化处理) + - 团队成员:15%(是/否 二分) + → 加权求和得 0-100 综合分 + ↓ +评定等级: + Champion(冠军) >= 80 分 + Core Contributor(核心) >= 60 分 + Active Contributor(活跃) >= 40 分 + Contributor(贡献者) >= 20 分 + Newcomer(新人) < 20 分 + ↓ +(可选)issue +create → 自动创建徽章颁发 Issue + ↓ +wiki +create → 发布排行榜到 Wiki +``` + +**串联的命令**: + +| 步骤 | 命令 | 作用 | +|------|------|------| +| 1 | `contrib +report` | 生成 HTML 贡献报告(带 ECharts 图表) | +| 2 | `issue +list` | 统计 open/closed Issue 活动 | +| 3 | `pr +list` | 统计 open/merged PR 活动 | +| 4 | `api GET /contributors` | 获取 API 级别的贡献者统计 | +| 5 | `issue +create` | (可选)自动颁发成就徽章 | +| 6 | `wiki +create` | 发布排行榜到 Wiki | + +**输出价值**:HTML 贡献报告(可视化展示贡献分布,适合团队会议演示)、贡献排行榜(量化每个人的贡献,公开透明)、Wiki 排行榜(永久保存,贡献者可随时查看排名)、徽章激励(通过 Issue 颁发徽章,增强成就感和归属感)。 + +**运行方式**: + +```bash +# 基本运行 +bash workflows/05-contributor-growth.sh --owner 你的组织 --repo 你的仓库 + +# 自定义统计周期(默认 30 天) +bash workflows/05-contributor-growth.sh --owner 你的组织 --repo 你的仓库 --period 90 + +# 启用自动颁发徽章 +bash workflows/05-contributor-growth.sh --owner 你的组织 --repo 你的仓库 --award + +# 示例 +bash workflows/05-contributor-growth.sh --owner zzx-coder --repo gitlink-cli --award +``` + +--- + +### 环境准备 + +**1. 安装 gitlink-cli** + +```bash +gitlink-cli version +# 未安装则从项目根目录构建 +cd gitlink-cli && make build +``` + +**2. 安装 jq(脚本用 jq 解析 CLI 返回的 JSON)** + +```bash +# Ubuntu/Debian +sudo apt-get install -y jq +# macOS +brew install jq +``` + +**3. 登录认证** + +```bash +gitlink-cli auth login # 交互式登录(推荐) +export GITLINK_TOKEN="你的私人令牌" # 或环境变量 +gitlink-cli auth status # 验证:应显示 ✓ Logged in +``` + +**4. 验证环境** + +```bash +gitlink-cli issue +list --owner zzx-coder --repo gitlink-cli --state open --limit 3 --format json | jq '.ok' +# 应输出:true +``` + +--- + +### 共享库 `lib/common.sh` + +所有工作流脚本共享的基础设施: + +| 函数 | 作用 | +|------|------| +| `check_auth` | 检查认证状态(环境变量 或 CLI 登录) | +| `gl_run` | CLI 封装,自动追加 `--format json` | +| `gl_check` | CLI 封装 + JSON 格式校验 + ok 字段检查 | +| `json_ok` / `json_get` / `json_error` | JSON 解析工具 | +| `detect_owner_repo` | 从 git remote 自动检测 owner/repo | +| `log_step` / `log_ok` / `log_warn` / `log_err` | 彩色日志输出 | + +--- + +### 工作流通用参数 + +| 参数 | 说明 | +|------|------| +| `--owner` | 仓库所有者(自动从 git remote 解析) | +| `--repo` | 仓库名称(自动从 git remote 解析) | +| `--org` | 组织名称(多仓库协同时需要) | +| `--dry-run` | 预览模式,不实际执行写操作 | +| `--help` | 显示帮助信息 | + +--- + +### 涉及的 Skill + +| Skill | 被使用的场景 | 作用 | +|-------|-------------|------| +| `gitlink-code-review` | 场景 2 | 审查维度、检查项、评分标准 | +| `gitlink-issue-triage` | 场景 1 | Issue 分类规则(关键词匹配、优先级判定) | +| `gitlink-changelog` | 场景 1 | Release Notes 生成模板 | +| `gitlink-health` | 场景 4 | 项目健康度评分体系(100 分扣分制) | +| `gitlink-onboard` | 场景 5 | 新人引导和 Issue 推荐规则 | +| `gitlink-workflow` | 全部 | 基础工作流编排 | + +--- + +### 测试 + +```bash +# 运行测试套件(使用真实 GitLink 仓库验证) +bash workflows/test.sh + +# 指定仓库 +bash workflows/test.sh zzx-coder gitlink-cli +``` + +测试覆盖:认证状态检查、CLI JSON 输出格式验证、数据字段提取(issue/PR/repo/release/member/contributor)、PR 文件和 Diff 内容解析、Issue/PR View 接口、Wiki/Label 列表接口、`common.sh` 工具函数、所有脚本语法校验。 + +--- + +### 项目结构(workflows/) + +``` +workflows/ +├── lib/ +│ └── common.sh # 共享工具库(认证、JSON解析、CLI封装、日志) +├── 01-community-ops.sh # 场景一:社区运营自动化 +├── 02-code-quality-gatekeeper.sh # 场景二:代码质量看门人(AI审查) +├── 03-project-init.sh # 场景三:项目一键初始化 +├── 04-multi-repo-collab.sh # 场景四:多仓库协同 +├── 05-contributor-growth.sh # 场景五:贡献者成长体系 +├── test.sh # 测试套件 +└── README.md # 工作流详细文档 +``` + +--- + +## 安装与快速开始 + +### 环境要求 + +- 支持平台:macOS、Linux、Windows (x64/arm64) +- Go 1.26+(仅从源码构建时需要) + +### 快速开始(面向用户) + +**一键安装(推荐):** ```bash curl -sSL https://www.gitlink.org.cn/Gitlink/gitlink-cli/raw/master/install.sh | bash ``` -**方式 2 — 从 npm 安装:** +**从 npm 安装:** ```bash npm install -g @gitlink-ai/cli ``` -**方式 2 — 从源码构建:** +二进制文件在 `postinstall` 阶段自动下载,无需额外操作。 -需要 Go 1.26+。 +**从源码构建:** ```bash git clone https://www.gitlink.org.cn/Gitlink/gitlink-cli.git cd gitlink-cli make install - -# 安装 CLI Skill(必须) -npx skills add ./skills -y -g ``` -> **Windows 用户注意:** 请在 PowerShell 或 CMD 中运行 `npm install -g @gitlink-ai/cli`。从源码构建请使用 `go install .` 代替 `make install`。 +> Windows 用户在 PowerShell 或 CMD 中运行 `npm install -g @gitlink-ai/cli`。从源码构建使用 `go install .` 代替 `make install`。 -#### 配置与使用 +**配置与使用:** ```bash -# 1. 配置(首次使用,交互式引导) +# 1. 配置(一次性) gitlink-cli config init -# 2. 登录(任选其一) -gitlink-cli auth login # 用户名密码(推荐) +# 2. 登录(任选一种) +gitlink-cli auth login # 用户名/密码(推荐) gitlink-cli auth login --token # 或粘贴私人令牌 -export GITLINK_TOKEN="your-token" # 或设置环境变量(适用于 CI/CD、非交互环境) +export GITLINK_TOKEN="your-token" # 或环境变量(CI/CD 环境) # 3. 开始使用 gitlink-cli repo +list ``` -### 快速上手(AI Agent) +### 快速开始(AI Agent) -> 以下步骤面向 AI Agent。部分步骤需要用户在浏览器中完成操作。 - -**第 1 步 — 安装** +> 以下步骤供 AI Agent 使用。部分步骤需用户在浏览器中完成。 ```bash -# 安装 CLI +# Step 1 — 安装(一条命令完成 CLI + 全部 Skills) npm install -g @gitlink-ai/cli -# 安装 CLI Skill(必须,全平台通用) -gitlink-cli-install-skills -``` - -**第 2 步 — 配置** - -```bash +# Step 2 — 配置 gitlink-cli config init -``` -**第 3 步 — 登录** +# Step 3 — 登录 +gitlink-cli auth login # 交互式环境 +export GITLINK_TOKEN="your-private-token" # 非交互式环境(CI/CD、MCP 等) -交互环境: -```bash -gitlink-cli auth login -``` - -非交互环境(CI/CD、Trae 沙箱、MCP 等): -```bash -export GITLINK_TOKEN="your-private-token" -``` - -> 获取私人令牌:GitLink 网页端 → 个人设置 → 私人令牌。 - -**第 4 步 — 验证** - -```bash +# Step 4 — 验证 gitlink-cli user +me ``` -## 安装与卸载 +### 卸载 -**详细安装指南**: [doc/INSTALL.md](./doc/INSTALL.md) -**详细卸载指南**: [doc/UNINSTALL.md](./doc/UNINSTALL.md) +**Linux/macOS**:`bash uninstall.sh` +**Windows PowerShell**:`.\uninstall.ps1` +**npm**:`npm uninstall -g @gitlink-ai/cli` -### 快速安装 - -**Linux/macOS**: `curl -sSL https://www.gitlink.org.cn/Gitlink/gitlink-cli/raw/master/install.sh | bash` -**Windows PowerShell**: `powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1` -**npm**: `npm install -g @gitlink-ai/cli` - -### 快速卸载 - -**Linux/macOS**: `bash uninstall.sh` -**Windows PowerShell**: `.\uninstall.ps1` -**npm**: `npm uninstall -g @gitlink-ai/cli` - -> 💡 **提示**: 查看详细指南:[doc/INSTALL.md](./doc/INSTALL.md) | [doc/UNINSTALL.md](./doc/UNINSTALL.md) +--- ## 使用示例 ### 仓库操作 ```bash -# 列出仓库 gitlink-cli repo +list - -# 查看仓库信息 gitlink-cli repo +info --owner Gitlink --repo forgeplus - -# 创建仓库 gitlink-cli repo +create -n my-project -d "项目描述" - -# Fork 仓库 gitlink-cli repo +fork --owner Gitlink --repo forgeplus - -# 批量创建仓库(默认公开,--private设置为私有) -gitlink-cli repo +batch-create -n "repo1,repo2" -d "项目描述" - -# 批量更新仓库信息(--private设置为私有,--public设置为公开,注意要指定仓库所有者) -gitlink-cli repo +batch-update -n "repo1,repo2" -d "更新描述" ``` ### Issue 管理 ```bash -# 列出 Issue gitlink-cli issue +list --owner Gitlink --repo forgeplus - -# 创建 Issue gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: 登录失败" -b "复现步骤..." - -# 查看 Issue gitlink-cli issue +view --owner Gitlink --repo forgeplus -i 123 - -# 关闭 Issue gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123 - -# 预览批量关闭,不修改数据 gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run - -# 从 CSV 文件批量关闭 Issue gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv - -# 添加评论 gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复" # 批量删除 Issue @@ -228,6 +736,7 @@ gitlink-cli issue --owner Gitlink --repo forgeplus +batch-create --titles "issue # 批量关闭 gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 1,2,3 + ``` ### Issue 元数据与评论 @@ -258,22 +767,11 @@ gitlink-cli issue +replies --number 42 --comment-id 100 ### Pull Request ```bash -# 列出 PR gitlink-cli pr +list --owner Gitlink --repo forgeplus - -# 创建 PR(同仓库分支) gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: 搜索功能" --head feature/search --base master - -# 创建 PR(从 Fork 仓库) gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: 新功能" --head your_username/forgeplus:feature/my-feature --base master - -# 查看 PR gitlink-cli pr +view --owner Gitlink --repo forgeplus -i 42 - -# 合并 PR gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42 - -# 查看 PR 变更文件 gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42 ``` @@ -384,96 +882,68 @@ gitlink-cli board +assign --owner Gitlink --repo forgeplus --number 42 --assigne # 查看看板统计分析 gitlink-cli board +stats --owner Gitlink --repo forgeplus + ``` ### Webhook 管理 ```bash -# 列出所有 Webhook gitlink-cli webhook +list --owner Gitlink --repo forgeplus - -# 创建 Webhook gitlink-cli webhook +create --owner Gitlink --repo forgeplus --url https://ci.example.com/webhook --events push,pull_request - -# 创建带密钥的 Webhook -gitlink-cli webhook +create --url https://jenkins.example.com/webhook --secret my-secret-key --events push --description "CI/CD trigger" - -# 查看 Webhook 详情 +gitlink-cli webhook +create --url https://jenkins.example.com/webhook --secret my-secret-key --events push --description "CI/CD 触发器" gitlink-cli webhook +info --id 456 - -# 更新 Webhook gitlink-cli webhook +update --id 456 --url https://new-url.example.com/webhook --events push,pull_request,issue - -# 测试 Webhook gitlink-cli webhook +test --id 456 - -# 删除 Webhook gitlink-cli webhook +delete --id 456 - -# 查看支持的事件类型 gitlink-cli webhook +events ``` -### 发布管理 +### Release 管理 ```bash -# 列出 Release gitlink-cli release +list --owner Gitlink --repo forgeplus - -# 创建 Release gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.0 正式版" -b "更新内容..." - -# 查看 Release gitlink-cli release +view --owner Gitlink --repo forgeplus -i ``` ### Wiki 管理 ```bash -# 列出 Wiki 页面 gitlink-cli wiki +list --owner Gitlink --repo forgeplus - -# 查看 Wiki 页面 -gitlink-cli wiki +view --owner Gitlink --repo forgeplus --title "快速开始" - -# 创建 Wiki 页面 -gitlink-cli wiki +create --title "新页面" --content "页面正文" - -# 更新 Wiki 页面(覆盖内容) -gitlink-cli wiki +update --title "新页面" --cover "更新后的内容" --owner Gitlink --repo forgeplus - -# 追加内容到 Wiki 页面 -gitlink-cli wiki +update --title "新页面" --add "追加的内容" --owner Gitlink --repo forgeplus - -# 删除 Wiki 页面 +gitlink-cli wiki +view --owner Gitlink --repo forgeplus --title "快速入门" +gitlink-cli wiki +create --title "新页面" --content "页面内容" +gitlink-cli wiki +update --title "新页面" --cover "更新内容" --owner Gitlink --repo forgeplus +gitlink-cli wiki +update --title "新页面" --add "追加内容" --owner Gitlink --repo forgeplus gitlink-cli wiki +delete --title "新页面" --owner Gitlink --repo forgeplus ``` ### 搜索 ```bash -# 搜索仓库 gitlink-cli search +repos -k "machine learning" - -# 搜索用户 gitlink-cli search +users -k "zhangsan" ``` +### CI/CD + +```bash +gitlink-cli ci +list --owner Gitlink --repo forgeplus +gitlink-cli ci +log --owner Gitlink --repo forgeplus -i +gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i +``` + ### Raw API -Shortcuts 未覆盖的接口可通过 Raw API 直接调用: +Shortcuts 未覆盖的端点可直接调用 Raw API: ```bash -# GET 请求 gitlink-cli api GET /users/me - -# POST 请求 gitlink-cli api POST /Gitlink/forgeplus/issues --body '{"subject":"test","description":"..."}' - -# 带查询参数 gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5' ``` +--- + ## 全局参数 | 参数 | 说明 | 示例 | @@ -483,54 +953,34 @@ gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5' | `--format` | 输出格式(json/table/yaml) | `--format json` | | `--debug` | 启用调试输出 | `--debug` | -**自动上下文解析**:在 git 仓库目录下,`--owner` 和 `--repo` 会自动从 `git remote origin` 解析。 +**自动上下文解析:** 在 git 仓库内运行命令时,`--owner` 和 `--repo` 自动从 `git remote origin` 解析。 + +--- ## 分支约定 -gitlink-cli 支持 GitHub 和 GitLink 的代码双向同步: - -| 平台 | 主分支 | -|------|--------| +| 平台 | 默认分支 | +|------|----------| | GitHub | `main` | | GitLink | `master` | -**本地 push 到 GitLink**: +推送到 GitLink: ```bash -# 方式 1:使用 git 命令 git push gitlink main:master - -# 方式 2:配置 git remote +# 或配置 git remote git config remote.gitlink.push refs/heads/main:refs/heads/master git push gitlink ``` -## AI Agent Skills +--- -`skills/` 目录包含 11 个 Claude Code Agent Skill 文件,支持 AI 自动化操作 GitLink 平台。 - -详见 [skills/README.md](skills/README.md) - -| Skill | 说明 | -|-------|------| -| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 | -| `gitlink-repo` | 仓库操作(创建、查看、删除、Fork 等) | -| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) | -| `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) | -| `gitlink-release` | 发布管理(创建、查看、删除等) | -| `gitlink-org` | 组织管理(成员、团队等) | -| `gitlink-ci` | CI/CD 操作(构建、日志等) | -| `gitlink-search` | 搜索功能(仓库、用户等) | -| `gitlink-user` | 用户管理(个人信息等) | -| `gitlink-pm` | 项目管理(Sprint、看板、周报等) | -| `gitlink-workflow` | AI 自动化工作流(Issue 分类、PR Review、Release Notes 等) | - -## 项目结构 +## 项目目录结构 ``` gitlink-cli/ ├── cmd/ # Cobra 命令定义 -│ ├── root.go # 根命令 + 全局 flags +│ ├── root.go # Root 命令 + 全局 flag │ ├── auth/ # 认证命令 │ ├── api/ # Raw API 命令 │ ├── config/ # 配置命令 @@ -539,8 +989,8 @@ gitlink-cli/ │ ├── auth/ # 登录、Token 存储、Transport │ ├── client/ # HTTP 客户端 + 分页 │ ├── config/ # 配置文件管理 -│ ├── context/ # git remote 解析 -│ └── output/ # Envelope + Formatter +│ ├── context/ # Git remote 解析 +│ └── output/ # 输出 envelope + formatter ├── shortcuts/ # Shortcut 实现 │ ├── common/ # 框架(types, runner) │ ├── repo/ # 仓库 shortcuts @@ -556,35 +1006,56 @@ gitlink-cli/ │ ├── wiki/ # Wiki shortcuts │ ├── file/ # 文件与代码 shortcuts │ ├── milestone/ # 里程碑 shortcuts + │ └── register.go # 注册入口 ├── skills/ # AI Agent Skills -│ ├── README.md # Skills 使用指南 +│ ├── README.md # Skills 详细指南 │ ├── gitlink-shared/ # 共享规则 -│ ├── gitlink-repo/ # 仓库 Skill -│ ├── gitlink-issue/ # Issue Skill -│ ├── gitlink-pr/ # PR Skill -│ ├── gitlink-pm/ # 项目管理 Skill +│ ├── gitlink-repo/ issue/ pr/ branch/ release/ ... +│ ├── gitlink-health/ # 项目健康度 +│ ├── gitlink-changelog/ # Release Notes +│ ├── gitlink-code-review/ # AI 代码审查 +│ ├── gitlink-research/ # 科研辅助 │ └── ... +├── workflows/ # 端到端自动化工作流 +│ ├── README.md # 工作流说明 +│ ├── lib/common.sh # 共享工具库 +│ ├── 01-community-ops.sh # 社区运营自动化 +│ ├── 02-code-quality-gatekeeper.sh # 代码质量看门人 +│ ├── 03-project-init.sh # 项目一键初始化 +│ ├── 04-multi-repo-collab.sh # 多仓库协同 +│ ├── 05-contributor-growth.sh # 贡献者成长体系 +│ └── test.sh # 测试套件 ├── doc/ # 设计文档 -│ ├── Design.md +│ ├── design.md │ ├── CODE_SYNC_STRATEGY_FINAL.md │ └── ... ├── main.go ├── Makefile ├── go.mod -└── README.md +├── README.md +└── README.zh-CN.md ``` -## 文档 +--- -- [Skills 使用指南](skills/README.md) — AI Agent Skills 详细说明 +## 文档索引 + +- [Skills 指南](skills/README.md) — AI Agent Skills 详细文档 +- [工作流说明](workflows/README.md) — 五大自动化工作流详情 - [设计文档](doc/design.md) — 架构设计和开发计划 +- [安装指南](doc/INSTALL.md) — 详细安装步骤 +- [卸载指南](doc/UNINSTALL.md) — 详细卸载步骤 +- [API 参考](skills/gitlink-shared/REFERENCE.md) — 完整 API 参考 +- [故障排查](skills/gitlink-shared/TROUBLESHOOTING.md) — 常见问题排查 + +--- ## 常见问题 ### Q: 如何在脚本中使用 gitlink-cli? -使用 `GITLINK_TOKEN` 环境变量 + `--format json` 获取结构化输出: +使用 `GITLINK_TOKEN` 环境变量 + `--format json`: ```bash export GITLINK_TOKEN="your-private-token" @@ -593,7 +1064,7 @@ gitlink-cli repo +list --format json | jq '.data.projects[] | .name' ### Q: 如何自动解析 owner/repo? -在 git 仓库目录下运行命令,CLI 会自动从 `git remote origin` 解析: +在 git 仓库目录下运行命令,CLI 自动从 `git remote origin` 解析: ```bash cd ~/my-gitlink-project @@ -602,45 +1073,39 @@ gitlink-cli issue +list # 自动使用当前仓库 ### Q: Token 过期了怎么办? -重新登录: - ```bash -# 用户名密码登录 -gitlink-cli auth login - -# 或使用私人令牌(在 GitLink 网页端 个人设置 → 私人令牌 中生成) -gitlink-cli auth login --token +gitlink-cli auth login # 用户名/密码登录 +gitlink-cli auth login --token # 或使用私人令牌 ``` -### Q: 如何在 CI/CD 或非交互环境(Trae 沙箱等)中使用? +### Q: 如何在 CI/CD 或非交互式环境(如 Trae sandbox)中使用? -设置 `GITLINK_TOKEN` 环境变量即可,无需 `auth login`: +设置 `GITLINK_TOKEN` 环境变量,无需 `auth login`: ```bash export GITLINK_TOKEN="your-private-token" -gitlink-cli repo +list # 直接可用 -gitlink-cli auth status # 显示 "✓ Logged in via GITLINK_TOKEN environment variable" +gitlink-cli repo +list # 立即可用 ``` -Token 优先级:`GITLINK_TOKEN` 环境变量 > keyring/文件存储的 token。不设置环境变量时完全兼容原有交互式登录。 +优先级:`GITLINK_TOKEN` 环境变量 > 密钥链/文件存储 Token。 -### Q: npm 安装成功但 `gitlink-cli` 提示缺少二进制怎么办? +### Q: npm 安装成功但 `gitlink-cli` 报告缺少二进制文件? -先尝试重新安装: - -```bash -npm install -g @gitlink-ai/cli -``` - -如果仍然失败,请检查 Release 页面是否包含当前平台的资产,例如 Windows x64 对应 `gitlink-cli__windows_amd64.zip`。也可以从 Release 页面手动下载二进制,或使用 `go install .` 从源码构建。 +先尝试重新安装 `npm install -g @gitlink-ai/cli`。如仍报错,检查 release 页面是否包含对应平台的资源文件,或手动下载二进制 / 从源码 `go install .` 构建。 ### Q: Windows 上凭证存储在哪里? -gitlink-cli 使用 Windows Credential Manager 安全存储 Token。如果 Credential Manager 不可用,会自动降级到文件存储(`~/.config/gitlink-cli/credentials`)。 +gitlink-cli 使用 Windows Credential Manager 安全存储 Token。如果 Credential Manager 不可用,自动回退到文件存储(`~/.config/gitlink-cli/credentials`)。 -### Q: 如何查看完整的 API 参考? +--- -查看 [skills/gitlink-shared/REFERENCE.md](skills/gitlink-shared/REFERENCE.md) +## 相关资源 + +- [gitlink-bisync](https://www.gitlink.org.cn/wbtiger/gitlink-bisync) — 代码双向同步系统 +- [测试报告](doc/SKILLS_TEST_REPORT_2026-04-02.md) — Skills 功能测试报告 +- [代码同步方案](doc/CODE_SYNC_STRATEGY_FINAL.md) — GitHub ↔ GitLink 同步设计 + +--- ## 许可证 diff --git a/demo/index.html b/demo/index.html index c8017ccd..5a69c090 100644 --- a/demo/index.html +++ b/demo/index.html @@ -119,6 +119,18 @@ .sidebar-section .skills-header { background: #fdf8ff; } + .sidebar-section .mission3-header { + background: #eef5fc; + } + .sidebar-section .mission3-header:hover { + background: #dfeaf6; + } + .sidebar-section .mission4-header { + background: #eef7f0; + } + .sidebar-section .mission4-header:hover { + background: #dceee2; + } /* ── Section collapse (top-level toggle) ── */ .sidebar-section .sidebar-header { cursor: pointer; @@ -162,6 +174,24 @@ .mode-toggle button:hover:not(.active) { background:var(--bg-hover); border-color:var(--border-accent); } + /* ── Agent selector buttons ── */ + .agent-selector { + display:flex; gap:0; margin-top:6px; + } + .agent-selector button { + flex:1; padding:5px 8px; border:1px solid var(--border); + background:#fff; color:var(--text-dim); cursor:pointer; + font-size:11px; font-weight:500; transition: all .12s; + } + .agent-selector button:first-child { border-radius:var(--radius-sm) 0 0 var(--radius-sm); } + .agent-selector button:last-child { border-radius:0 var(--radius-sm) var(--radius-sm) 0; } + .agent-selector button:hover:not(.active) { + background:var(--bg-hover); border-color:var(--border-accent); + } + .agent-selector button.active { color:#fff; z-index:1; } + .agent-selector button.agent-claude.active { background:var(--purple); border-color:var(--purple); } + .agent-selector button.agent-codex.active { background:#0ea5e9; border-color:#0ea5e9; } + .agent-selector button.agent-zcode.active { background:#f59e0b; border-color:#f59e0b; } #sidebar .modules { padding:4px 0; } @@ -377,8 +407,8 @@
@@ -386,14 +416,18 @@
- + + + + + + + @@ -593,7 +665,7 @@ var SKILLS = [ { id: 'health', icon: '📊', name: '项目健康度报告 (gitlink-health)', prompts: [ - { label: '基础健康度报告', prompt: '请使用 gitlink-health Skill,为 zzx-coder/test-repo 仓库生成项目健康度报告。' }, + { label: '基础健康度报告', prompt: '请使用 gitlink-health Skill,为 zzx-coder/test-repo 仓库生成项目健康度报告。\n效率要求(重要):整份报告须在 2 分钟内完成。计数/合并率/积压数一律从 issue +list、pr +list 的汇总字段一次性获取;PR 合并时长只采样「最近 ≤8 条已合并 PR」调用 pr +view 取 merged_at,禁止遍历或分页拉取全部历史 PR/Issue。' }, { label: '安全规则验证:保存路径', prompt: '请把报告保存到桌面上。\n测试仓库:zzx-coder/test-repo' }, ] }, @@ -640,6 +712,309 @@ var SKILLS = [ ]; +var MISSION3 = [ + { + id: "m3-env", + icon: "🔧", + name: "测试环境准备", + commands: [ + { + label: "设置 UTF-8 控制台编码", + engine: "powershell", + cmd: "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\nWrite-Host \"当前控制台编码: $([Console]::OutputEncoding.WebName)\"\nWrite-Host \"(应为 utf-8,否则含中文 JSON 解析会失败)\"" + }, + { + label: "确认 gitlink-cli 版本", + engine: "powershell", + cmd: ".\\gitlink-cli.exe version" + }, + { + label: "确认认证状态", + engine: "powershell", + cmd: ".\\gitlink-cli.exe auth status" + }, + { + label: "确认 JSON 输出正常", + engine: "powershell", + cmd: ".\\gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --state open --limit 3 --format json | ConvertFrom-Json" + }, + { + label: "加载公共库 common.psm1", + engine: "powershell", + cmd: "Import-Module \".\\workflows\\lib\\common.psm1\" -Force -WarningAction SilentlyContinue\nCheck-Auth" + }, + { + label: "确认测试仓库可访问", + engine: "powershell", + cmd: ".\\gitlink-cli.exe repo +info --owner zzx-coder --repo test-repo --format json | ConvertFrom-Json" + } + ] + }, + { + id: "m3-community", + icon: "🤝", + name: "社区运营自动化 (01-community-ops)", + commands: [ + { + label: "基础只读模式验证 (DryRun)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\01-community-ops.ps1 -Owner zzx-coder -Repo test-repo -DryRun" + }, + { + label: "查看命令行参数 (Help)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\01-community-ops.ps1 -Help" + }, + { + label: "Issue 数据拉取验证", + engine: "powershell", + cmd: "# 拉取 open / closed Issue 与已合并 PR(来自测试 2.1.3)\n$openResult = .\\gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --state open --limit 100 --format json | ConvertFrom-Json\n$closedResult = .\\gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --state closed --limit 100 --format json | ConvertFrom-Json\n$prResult = .\\gitlink-cli.exe pr +list --owner zzx-coder --repo test-repo --state merged --limit 100 --format json | ConvertFrom-Json\n$prData = if ($prResult.data.issues) { @($prResult.data.issues) } else { @($prResult.data.pulls) }\nWrite-Host \"Open issues: $(@($openResult.data.issues).Count)\"\nWrite-Host \"Closed issues: $(@($closedResult.data.issues).Count)\"\nWrite-Host \"Merged PRs: $($prData.Count)\"" + } + ] + }, + { + id: "m3-gate", + icon: "🛡", + name: "代码质量看门人 (02-code-quality-gatekeeper)", + commands: [ + { + label: "审查所有 open PR (DryRun)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\02-code-quality-gatekeeper.ps1 -Owner zzx-coder -Repo test-repo -DryRun" + }, + { + label: "审查指定 PR #13 (DryRun)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\02-code-quality-gatekeeper.ps1 -Owner zzx-coder -Repo test-repo -PrId 13 -DryRun" + }, + { + label: "自定义质量阈值 70 (DryRun)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\02-code-quality-gatekeeper.ps1 -Owner zzx-coder -Repo test-repo -Threshold 70 -DryRun" + }, + { + label: "查看帮助信息 (Help)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\02-code-quality-gatekeeper.ps1 -Help" + } + ] + }, + { + id: "m3-init", + icon: "🚀", + name: "项目一键初始化 (03-project-init)", + commands: [ + { + label: "预览模式 (DryRun,推荐首选)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\03-project-init.ps1 -Owner zzx-coder -Name test-init-dryrun -Description \"自动化测试仓库\" -Lang go -DryRun" + }, + { + label: "底层命令逐步骤验证 (只读安全)", + engine: "powershell", + cmd: "# 底层命令逐步骤验证(只读安全,来自测试 2.3.2)\nWrite-Host \"=== Step 1: List repos ===\" -ForegroundColor White\n$repoList = .\\gitlink-cli.exe repo +list --user zzx-coder --limit 5 --format json | ConvertFrom-Json\n$repoCount = if ($repoList.data.projects) { @($repoList.data.projects).Count } else { @($repoList.data).Count }\nWrite-Host \"Repos: $repoCount\" -ForegroundColor Green\n\nWrite-Host \"`n=== Step 2: Check Wiki access ===\" -ForegroundColor White\n$wikiResult = .\\gitlink-cli.exe wiki +list --owner zzx-coder --repo test-repo --format json | ConvertFrom-Json\nWrite-Host \"Wiki accessible: $($wikiResult.ok)\" -ForegroundColor Green\n\nWrite-Host \"`n=== Step 3: Check branch help ===\" -ForegroundColor White\n.\\gitlink-cli.exe branch --help\n\nWrite-Host \"`n=== Step 4: Check releases ===\" -ForegroundColor White\n$relResult = .\\gitlink-cli.exe release +list --owner zzx-coder --repo test-repo --limit 5 --format json | ConvertFrom-Json\nWrite-Host \"Release API accessible: $($relResult.ok)\" -ForegroundColor Green\n\nWrite-Host \"`n=== Step 5: Check milestone ===\" -ForegroundColor White\n.\\gitlink-cli.exe milestone --help" + }, + { + label: "实际创建仓库 (可选·需谨慎)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\03-project-init.ps1 -Owner zzx-coder -Name \"test-init-real\" -Description \"测试仓库-可安全删除\" -Lang go" + } + ] + }, + { + id: "m3-multirepo", + icon: "🔗", + name: "多仓库协同 (04-multi-repo-collab)", + commands: [ + { + label: "生成组织仪表盘", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\04-multi-repo-collab.ps1 -Org zzx-coder -Output dashboard.html" + }, + { + label: "指定仓库列表过滤", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\04-multi-repo-collab.ps1 -Org zzx-coder -Repos \"test-repo\" -Output dashboard-filtered.html" + }, + { + label: "查看帮助和参数", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\04-multi-repo-collab.ps1 -Help" + } + ] + }, + { + id: "m3-contrib", + icon: "🏆", + name: "贡献者成长体系 (05-contributor-growth)", + commands: [ + { + label: "Dry-Run 预览模式", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\05-contributor-growth.ps1 -Owner zzx-coder -Repo test-repo -Sample 5 -DryRun" + }, + { + label: "底层数据采集验证", + engine: "powershell", + cmd: "# 底层数据采集验证(来自测试 2.5.2)\n$openIssues = .\\gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --state open --limit 100 --format json | ConvertFrom-Json\n$closedIssues = .\\gitlink-cli.exe issue +list --owner zzx-coder --repo test-repo --state closed --limit 100 --format json | ConvertFrom-Json\n$prsMerged = .\\gitlink-cli.exe pr +list --owner zzx-coder --repo test-repo --state merged --limit 100 --format json | ConvertFrom-Json\n$members = .\\gitlink-cli.exe repo +members --owner zzx-coder --repo test-repo --limit 100 --format json | ConvertFrom-Json\n$prData = if ($prsMerged.data.issues) { @($prsMerged.data.issues) } else { @($prsMerged.data.pulls) }\n$memberData = if ($members.data.members) { @($members.data.members) } else { @($members.data) }\nWrite-Host \"Issues - Open: $(@($openIssues.data.issues).Count), Closed: $(@($closedIssues.data.issues).Count)\" -ForegroundColor Green\nWrite-Host \"Merged PRs: $($prData.Count)\" -ForegroundColor Green\nWrite-Host \"Members: $($memberData.Count)\" -ForegroundColor Green\n$allIssues = @($openIssues.data.issues) + @($closedIssues.data.issues)\n$authors = @{}\nforeach ($issue in $allIssues) {\n $author = if ($issue.author.login) { $issue.author.login } elseif ($issue.author.username) { $issue.author.username } else { $null }\n if ($author) { if (-not $authors.ContainsKey($author)) { $authors[$author] = 0 }; $authors[$author]++ }\n}\nWrite-Host \"Unique contributors: $($authors.Count)\" -ForegroundColor Green\nforeach ($kv in ($authors.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 5)) {\n Write-Host \" @$($kv.Key): $($kv.Value) issues\"\n}" + }, + { + label: "颁发徽章 (可选·需谨慎)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\05-contributor-growth.ps1 -Owner zzx-coder -Repo test-repo -Sample 5 -Award" + } + ] + }, + { + id: "m3-skill", + icon: "🤖", + name: "技能工作流 (Agent · gitlink-workflow)", + prompts: [ + { + label: "工作流总入口(触发 7 选项菜单)", + engine: "claude", + prompt: "请帮我执行自动化工作流\n测试仓库:zzx-coder/test-repo" + }, + { + label: "快捷触发:社区运营", + engine: "claude", + prompt: "帮我跑一下社区运营\n测试仓库:zzx-coder/test-repo" + }, + { + label: "项目健康度报告 (gitlink-health)", + engine: "claude", + prompt: "请使用 gitlink-health Skill,为 zzx-coder/test-repo 仓库生成项目健康度报告。" + }, + { + label: "健康度路径规则验证", + engine: "claude", + prompt: "请把健康度报告保存到桌面上。\n测试仓库:zzx-coder/test-repo" + }, + { + label: "新人引导 AI 分析模式 (onboard)", + engine: "claude", + prompt: "请使用 gitlink-onboard Skill 的 AI 分析模式,扫描 zzx-coder/test-repo 的所有 open Issue,\n识别适合新手贡献的 Good First Issue。\n展示分析结果表格后,让我选择要对哪些 Issue 添加引导评论。" + }, + { + label: "新人引导直接模式 (#11 dry-run)", + engine: "claude", + prompt: "请为 Issue #11 添加新人引导评论,先用 dry-run 预览。\n测试仓库:zzx-coder/test-repo" + }, + { + label: "FAQ 模式 A:生成知识库", + engine: "claude", + prompt: "请使用 gitlink-faq Skill 的模式 A,为 zzx-coder/test-repo 生成 Issue 知识库。\n同时采集 open + closed 所有 Issue,合并去重,逐批读取详情,按类型分类和聚类,\n生成知识库 Markdown 预览后等我确认再发布到 Wiki。" + }, + { + label: "FAQ 模式 B:重复检测", + engine: "claude", + prompt: "请检查 zzx-coder/test-repo 有没有和 \"登录功能\" 相关的已有 Issue,看看是否有重复的。\n对找到的候选 Issue,自动读取详情和 journals,给出分析结论。" + }, + { + label: "FAQ 模式 C:Wiki 增量更新", + engine: "claude", + prompt: "请把 Issue #11 补充到已有的 Issue 知识库 Wiki 页面中。\n测试仓库:zzx-coder/test-repo" + } + ] + }, + { + id: "m3-smoke", + icon: "🧪", + name: "端到端冒烟测试 (15 步)", + commands: [ + { + label: "运行冒烟测试(连通性验证)", + engine: "powershell", + cmd: "# 端到端冒烟测试(来自测试 5.1,15 步连通性验证)\n$ErrorActionPreference = \"Continue\"\n$owner = \"zzx-coder\"; $repo = \"test-repo\"\n$pass = 0; $fail = 0\nfunction Test-Step {\n param([string]$Name, [ScriptBlock]$Script)\n Write-Host \"[TEST] $Name ... \" -NoNewline -ForegroundColor Cyan\n try { $result = & $Script; if ($result) { Write-Host \"PASS\" -ForegroundColor Green; $global:pass++ } else { Write-Host \"FAIL\" -ForegroundColor Red; $global:fail++ } }\n catch { Write-Host \"FAIL ($($_.Exception.Message))\" -ForegroundColor Red; $global:fail++ }\n}\nWrite-Host \"`n====== GitLink CLI 子任务三冒烟测试 ======\" -ForegroundColor White\nWrite-Host \"Target: $owner/$repo`n\"\nTest-Step \"Auth status\" { $r = .\\gitlink-cli.exe auth status 2>&1 | Out-String; $r -match \"logged in\" -or $r -match \"token\" }\nTest-Step \"01-Community: issue +list open\" { $r = .\\gitlink-cli.exe issue +list --owner $owner --repo $repo --state open --limit 5 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"01-Community: pr +list merged\" { $r = .\\gitlink-cli.exe pr +list --owner $owner --repo $repo --state merged --limit 5 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"02-Gatekeeper: issue +view #11\" { $r = .\\gitlink-cli.exe issue +view --owner $owner --repo $repo --number 11 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"03-Init: repo +list\" { $r = .\\gitlink-cli.exe repo +list --user $owner --limit 5 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"03-Init: wiki +list\" { $r = .\\gitlink-cli.exe wiki +list --owner $owner --repo $repo --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"04-MultiRepo: org repos\" { $r = .\\gitlink-cli.exe repo +list --user $owner --limit 10 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"04-MultiRepo: release +list\" { $r = .\\gitlink-cli.exe release +list --owner $owner --repo $repo --limit 5 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"05-Contrib: repo +members\" { $r = .\\gitlink-cli.exe repo +members --owner $owner --repo $repo --limit 50 --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"CI: ci +builds\" { $r = .\\gitlink-cli.exe ci +builds --owner $owner --repo $repo --format json | ConvertFrom-Json; $r.ok }\nTest-Step \"Branch: branch help\" { $r = .\\gitlink-cli.exe branch --help 2>&1 | Out-String; $r -match \"Usage\" -or $r -match \"branch\" }\nTest-Step \"Milestone: milestone help\" { $r = .\\gitlink-cli.exe milestone --help 2>&1 | Out-String; $r -match \"Usage\" -or $r -match \"milestone\" }\nTest-Step \"Webhook: webhook help\" { $r = .\\gitlink-cli.exe webhook --help 2>&1 | Out-String; $r -match \"Usage\" -or $r -match \"webhook\" }\nTest-Step \"Research: search +repos\" { $r = .\\gitlink-cli.exe search +repos --q \"agent\" --limit 5 --format json | ConvertFrom-Json; $r.ok }\nWrite-Host \"`n====== Results ======\" -ForegroundColor White\nWrite-Host \" PASS: $pass\" -ForegroundColor Green\nWrite-Host \" FAIL: $fail\" -ForegroundColor Red\nWrite-Host \" TOTAL: $($pass + $fail)\" -ForegroundColor Cyan" + } + ] + } +]; + +var MISSION4 = [ + { + id: "m4-insights", + icon: "🔭", + name: "项目洞察 (06-research-insights)", + commands: [ + { + label: "生成项目洞悉报告(热度/技术栈/活动)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\06-research-insights.ps1 -Owner zzx-coder -Repo test-repo" + } + ] + }, + { + id: "m4-compliance", + icon: "✅", + name: "合规与复现性检查 (08-research-compliance)", + commands: [ + { + label: "生成 8 维复现性评分卡", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\08-research-compliance.ps1 -Owner zzx-coder -Repo test-repo" + } + ] + }, + { + id: "m4-progress", + icon: "📈", + name: "进度跟踪与预警 (10-research-progress)", + commands: [ + { + label: "生成周度进度报告(含异常预警)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\10-research-progress.ps1 -Owner zzx-coder -Repo test-repo" + } + ] + }, + { + id: "m4-citation", + icon: "📄", + name: "论文引用生成 (11-research-citation)", + commands: [ + { + label: "生成全部 5 种引用格式 (all)", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\11-research-citation.ps1 -Owner zzx-coder -Repo test-repo -Format all" + }, + { + label: "BibTeX 格式", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\11-research-citation.ps1 -Owner zzx-coder -Repo test-repo -Format bibtex" + }, + { + label: "APA 格式", + engine: "powershell", + cmd: "powershell -ExecutionPolicy Bypass -File .\\workflows\\academic\\11-research-citation.ps1 -Owner zzx-coder -Repo test-repo -Format apa" + } + ] + }, + { + id: "m4-research", + icon: "🔬", + name: "科研辅助 Agent (gitlink-research)", + prompts: [ + { + label: "科研功能总入口(6 选项菜单)", + engine: "claude", + prompt: "请使用科研辅助功能\n(请展示 6 选项菜单:项目洞察 / 热点追踪 / 合规复现 / 协作匹配 / 进度预警 / 论文引用,等我选择)\n测试仓库:zzx-coder/test-repo" + }, + { + label: "合规与复现性检查", + engine: "claude", + prompt: "请检查 zzx-coder/test-repo 项目的合规与复现性" + }, + { + label: "论文引用生成(BibTeX)", + engine: "claude", + prompt: "请生成 zzx-coder/test-repo 项目的论文引用(BibTeX 格式)" + } + ] + } +]; + + // ═══════════════════════════════════════════════ // DOM refs // ═══════════════════════════════════════════════ @@ -650,14 +1025,21 @@ var btnRun = document.getElementById('btn-run'); var btnCancel = document.getElementById('btn-cancel'); var cliModsContainer = document.getElementById('cli-modules-container'); var skillsModsContainer = document.getElementById('skills-modules-container'); +var mission3ModsContainer = document.getElementById('mission3-modules-container'); +var mission4ModsContainer = document.getElementById('mission4-modules-container'); var statusDot = document.getElementById('status-dot'); var statusText = document.getElementById('status-text'); var toast = document.getElementById('toast'); var isRunning = false; var currentEngine = 'powershell'; // 'powershell' | 'claude' -var cliExecMode = 'direct'; // CLI section mode -var skillsExecMode = 'direct'; // Skills section mode +var execModes = { cli:'direct', skills:'direct', mission3:'direct', mission4:'direct' }; // 每个 section 的执行模式 var claudeSessionId = null; // Claude Code session ID for multi-turn +var currentAgent = 'claude'; // 'claude' | 'codex' | 'zcode' +var AGENT_META = { + claude: { icon: '🤖', name: 'Claude Code', btnColor: 'var(--purple)' }, + codex: { icon: '📟', name: 'Codex', btnColor: '#0ea5e9' }, + zcode: { icon: '⚡', name: 'ZCode', btnColor: '#f59e0b' } +}; // ═══════════════════════════════════════════════ // Section Toggle (top-level collapse) @@ -670,7 +1052,7 @@ function toggleSection(sectionId) { // ═══════════════════════════════════════════════ // Build Sidebar (CLI + Skills sections) // ═══════════════════════════════════════════════ -function buildModuleGroup(container, modules, section, isSkill) { +function buildModuleGroup(container, modules, section) { if (!container) return; modules.forEach(function(mod, idx) { var div = document.createElement('div'); @@ -703,23 +1085,19 @@ function buildModuleGroup(container, modules, section, isSkill) { } function buildSidebar() { - buildModuleGroup(cliModsContainer, MODULES, 'cli', false); - buildModuleGroup(skillsModsContainer, SKILLS, 'skills', true); + buildModuleGroup(cliModsContainer, MODULES, 'cli'); + buildModuleGroup(skillsModsContainer, SKILLS, 'skills'); + buildModuleGroup(mission3ModsContainer, MISSION3, 'mission3'); + buildModuleGroup(mission4ModsContainer, MISSION4, 'mission4'); } // ═══════════════════════════════════════════════ // Execution Mode Toggles // ═══════════════════════════════════════════════ -function setCliExecMode(mode) { - cliExecMode = mode; - document.getElementById('cli-mode-direct').className = (mode === 'direct') ? 'active' : ''; - document.getElementById('cli-mode-input').className = (mode === 'input') ? 'active' : ''; -} - -function setSkillsExecMode(mode) { - skillsExecMode = mode; - document.getElementById('skills-mode-direct').className = (mode === 'direct') ? 'active' : ''; - document.getElementById('skills-mode-input').className = (mode === 'input') ? 'active' : ''; +function setSectionExecMode(section, mode) { + execModes[section] = mode; + document.getElementById(section + '-mode-direct').className = (mode === 'direct') ? 'active' : ''; + document.getElementById(section + '-mode-input').className = (mode === 'input') ? 'active' : ''; } // ═══════════════════════════════════════════════ @@ -728,11 +1106,12 @@ function setSkillsExecMode(mode) { function setEngine(engine) { currentEngine = engine; if (engine === 'claude') { - inputPrompt.innerHTML = '🤖>'; + var meta = AGENT_META[currentAgent]; + inputPrompt.innerHTML = meta.icon + '>'; inputPrompt.title = '点击切换为 PowerShell'; cmdInput.placeholder = '输入 Claude Code 提示词或从左侧 Skills 选择...'; - btnRun.textContent = '▶ 执行 (Claude)'; - btnRun.style.background = 'var(--purple)'; + btnRun.textContent = '▶ 执行 (' + meta.name + ')'; + btnRun.style.background = meta.btnColor; } else { inputPrompt.innerHTML = 'PS>'; inputPrompt.title = '点击切换为 Claude Code'; @@ -746,6 +1125,49 @@ function toggleEngine() { setEngine(currentEngine === 'powershell' ? 'claude' : 'powershell'); } +// ═══════════════════════════════════════════════ +// Prompt helper: update input prompt from AGENT_META +// ═══════════════════════════════════════════════ +function updateInputPrompt() { + if (currentEngine === 'claude') { + var meta = AGENT_META[currentAgent]; + inputPrompt.innerHTML = meta.icon + '>'; + } else { + inputPrompt.innerHTML = 'PS>'; + } +} + +// ═══════════════════════════════════════════════ +// Agent Selector (Skills section) +// ═══════════════════════════════════════════════ +function setAgent(agent) { + if (agent === currentAgent) return; + + currentAgent = agent; + + // Update button active states (支持多 section 共享同一 currentAgent) + var btns = document.querySelectorAll('.agent-btn'); + for (var i = 0; i < btns.length; i++) { + btns[i].classList.toggle('active', btns[i].getAttribute('data-agent') === agent); + } + + // Clear terminal output (but keep session) + terminal.innerHTML = ''; + + // Show switch message + var meta = AGENT_META[agent]; + appendEntry('info', 'agent已切换为 ' + meta.name); + + // Switch engine to Claude mode if not already + if (currentEngine !== 'claude') { + setEngine('claude'); + } else { + updateInputPrompt(); + } + + scrollToBottom(); +} + // ═══════════════════════════════════════════════ // Module Toggle // ═══════════════════════════════════════════════ @@ -764,14 +1186,15 @@ function onPresetSelect(selectEl, moduleId) { return; } - // Determine section: 'cli' or 'skills' - var isSkill = moduleId.indexOf('skills-') === 0; + // Determine section: 'cli' | 'skills' | 'mission3' | 'mission4' + var section = moduleId.substring(0, moduleId.indexOf('-mod-')); + var dataMap = { cli: MODULES, skills: SKILLS, mission3: MISSION3, mission4: MISSION4 }; + var list = dataMap[section] || MODULES; // Find module - var list = isSkill ? SKILLS : MODULES; var mod = null; for (var i = 0; i < list.length; i++) { - if ((isSkill ? 'skills-mod-' : 'cli-mod-') + list[i].id === moduleId) { mod = list[i]; break; } + if (section + '-mod-' + list[i].id === moduleId) { mod = list[i]; break; } } if (!mod) return; @@ -779,18 +1202,20 @@ function onPresetSelect(selectEl, moduleId) { if (!item) return; var content = item.cmd || item.prompt; + // 每个 item 可自带 engine;默认 skills 走 claude,其余走 powershell + var engine = item.engine || (section === 'skills' ? 'claude' : 'powershell'); // Show preview var previewEl = document.getElementById('preview-' + moduleId); if (previewEl) { previewEl.textContent = content; } - var mode = isSkill ? skillsExecMode : cliExecMode; + var mode = execModes[section] || 'direct'; if (mode === 'direct') { // Execute immediately selectEl.value = ''; if (previewEl) { previewEl.textContent = ''; } - if (isSkill) { + if (engine === 'claude') { setEngine('claude'); executeDirectClaude(content); } else { @@ -799,7 +1224,7 @@ function onPresetSelect(selectEl, moduleId) { } } else { // Fill input for editing - if (isSkill) { setEngine('claude'); } else { setEngine('powershell'); } + setEngine(engine); cmdInput.value = content; autoResizeTextarea(); cmdInput.focus(); @@ -832,8 +1257,9 @@ function executeDirectClaude(prompt) { btnCancel.classList.add('visible'); cmdInput.value = ''; + var meta = AGENT_META[currentAgent]; appendEntry('cmd-line', - '⚡ 🤖 ' + escHtml(prompt) + ''); + '⚡ ' + meta.icon + ' ' + escHtml(prompt) + ''); sendClaudeRequest(prompt); } @@ -852,7 +1278,8 @@ function executeCommand() { btnRun.classList.add('running'); btnCancel.classList.add('visible'); - var promptHtml = isClaude ? '🤖>' : 'PS>'; + var meta = AGENT_META[currentAgent]; + var promptHtml = isClaude ? meta.icon + '>' : 'PS>'; appendEntry('cmd-line', '' + promptHtml + ' ' + escHtml(command) + ''); @@ -1040,7 +1467,6 @@ function sendClaudeRequest(prompt) { // ── 会话管理:保存 session_id 并提示用户 ── if (data.session_id) { claudeSessionId = data.session_id; - updateSessionBadge(); var shortId = data.session_id.substring(0, 8); if (data.ok) { appendEntry('info', '💬 会话持续中 · ID: ' + shortId + ' · 点击开始新会话 · 下一条 Claude 指令将自动延续本会话'); @@ -1074,8 +1500,8 @@ function sendClaudeRequest(prompt) { resetExecState(); }; - // ── 构建请求体:带上会话信息 ── - var reqBody = { prompt: prompt, timeout: 300 }; + // ── 构建请求体:带上会话信息(超时 600s = 10min)── + var reqBody = { prompt: prompt, timeout: 600 }; if (claudeSessionId) { reqBody.session_id = claudeSessionId; } @@ -1085,24 +1511,10 @@ function sendClaudeRequest(prompt) { // ── Start a fresh Claude Code session ── function newClaudeSession() { claudeSessionId = null; - updateSessionBadge(); appendEntry('info', '🆕 已开始新会话 — 下一条 Claude 指令将创建全新对话'); showToast('Claude 会话已重置', 'ok'); } -// ── Update session badge in sidebar ── -function updateSessionBadge() { - var badge = document.getElementById('session-badge'); - if (badge) { - if (claudeSessionId) { - badge.style.display = 'inline'; - badge.textContent = '● 会话中'; - } else { - badge.style.display = 'none'; - } - } -} - // ── Reset after execution ── function resetExecState() { isRunning = false; @@ -1112,8 +1524,9 @@ function resetExecState() { cmdInput.focus(); // Restore button text based on current engine if (currentEngine === 'claude') { - btnRun.textContent = '▶ 执行 (Claude)'; - btnRun.style.background = 'var(--purple)'; + var meta = AGENT_META[currentAgent]; + btnRun.textContent = '▶ 执行 (' + meta.name + ')'; + btnRun.style.background = meta.btnColor; } else { btnRun.textContent = '▶ 执行'; btnRun.style.background = ''; diff --git a/demo/server.py b/demo/server.py index c4a506ea..0e08fa58 100644 --- a/demo/server.py +++ b/demo/server.py @@ -113,7 +113,7 @@ class DemoHandler(http.server.BaseHTTPRequestHandler): self._send_json({"ok": False, "error": "Empty prompt", "session_id": data.get("session_id", "")}) return - timeout = min(data.get("timeout", 300), 600) # max 10 min for Claude + timeout = min(data.get("timeout", 600), 600) # 10 min for Claude if not CLAUDE_EXE: self._send_json({ @@ -220,6 +220,10 @@ class DemoHandler(http.server.BaseHTTPRequestHandler): tmp = None proc = None try: + # 注入 UTF-8 编码设置:避免 PowerShell 默认 GBK 解码含中文 JSON 导致 + # ConvertFrom-Json 报 ArgumentException。对已有该设置的命令重复无害。 + command = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n" + command + tmp = tempfile.NamedTemporaryFile( mode="w", suffix=".ps1", delete=False, encoding="utf-8-sig" ) diff --git a/doc/.claude/settings.local.json b/doc/.claude/settings.local.json new file mode 100644 index 00000000..d566ce2a --- /dev/null +++ b/doc/.claude/settings.local.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "Bash(go build:*)", + "Bash(/c/Users/Lenovo/Desktop/soft运维/gitlink-cli/gitlink-cli.exe board:*)", + "Bash(go install:*)", + "Bash(gitlink-cli board:*)", + "Bash(/c/Users/Lenovo/go/bin/gitlink-cli.exe board:*)", + "Bash(gitlink-cli repo:*)", + "Bash(gitlink-cli api:*)", + "Bash(gitlink-cli issue:*)", + "Bash(find C:/Users/Lenovo/Desktop/soft运维/gitlink-cli -name *.go -not -path */vendor/* -exec wc -l {} +)", + "Bash(pip install:*)", + "Bash(python gen_docx.py)", + "Bash(python -c \"import docx; print\\(''ok''\\)\")", + "Bash(ls -la \"C:\\\\Users\\\\Lenovo\\\\Desktop\\\\soft运维\\\\gitlink-cli\\\\doc/\"*.docx)", + "Bash(python gen_ppt.py)" + ] + } +} diff --git a/doc/1.png b/doc/1.png new file mode 100644 index 00000000..40aa2fc4 Binary files /dev/null and b/doc/1.png differ diff --git a/doc/dashboard.html b/doc/dashboard.html index 7e5dc42a..a457616f 100644 --- a/doc/dashboard.html +++ b/doc/dashboard.html @@ -2,63 +2,399 @@ - -gitlink-cli 功能全景 + +gitlink-cli · 功能全景 -
-

gitlink-cli 功能全景

-

所有 Shortcuts 分类展示 · 点击分类展开 · 点击命令查看示例

-
14
分类
69
Shortcuts
-
-
-
📦仓库管理8 个命令
repo +list公开仓库列表
gitlink-cli repo +list --user zhangsan
repo +info公开仓库详情
gitlink-cli repo +info --owner Gitlink --repo forgeplus
repo +create需认证创建仓库
gitlink-cli repo +create --name my-project --description "项目描述"
repo +fork需认证Fork 仓库
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
repo +delete需认证删除仓库(不可逆)
gitlink-cli repo +delete --owner myuser --repo old-project
repo +batch-create需认证批量创建仓库
gitlink-cli repo +batch-create --from repos.csv
repo +batch-update需认证批量更新仓库
gitlink-cli repo +batch-update --from updates.csv
repo +add-member需认证添加仓库成员
gitlink-cli repo +add-member --owner myuser --repo myrepo --user newmember --role developer
🌿分支管理5 个命令
branch +list公开分支列表
gitlink-cli branch +list --owner Gitlink --repo forgeplus
branch +create需认证创建分支
gitlink-cli branch +create --name feature/new-feature
branch +delete需认证删除分支(不可逆)
gitlink-cli branch +delete --name feature/old-feature
branch +protect需认证保护分支
gitlink-cli branch +protect --name main
branch +unprotect需认证取消保护
gitlink-cli branch +unprotect --name main
🐛Issue 管理7 个命令
issue +list公开Issue 列表
gitlink-cli issue +list --owner Gitlink --repo forgeplus --state open
issue +view公开Issue 详情
gitlink-cli issue +view --owner Gitlink --repo forgeplus --number 4
issue +create需认证创建 Issue
gitlink-cli issue +create --owner myuser --repo myrepo --title "Bug: 登录失败" --body "复现步骤"
issue +update需认证更新 Issue
gitlink-cli issue +update --number 4 --title "新标题" --body "更新描述"
issue +close需认证关闭 Issue
gitlink-cli issue +close --number 4
issue +batch-close需认证批量关闭 Issue
gitlink-cli issue +batch-close --numbers 123,124 --dry-run
issue +comment需认证添加评论
gitlink-cli issue +comment --number 4 --body "已修复"
🔀Pull Request9 个命令
pr +list公开PR 列表
gitlink-cli pr +list --owner Gitlink --repo forgeplus --state open
pr +view公开PR 详情
gitlink-cli pr +view --id 3
pr +create需认证创建 PR
gitlink-cli pr +create --title "feat: 新功能" --head feature/x --base master
pr +merge需认证合并 PR
gitlink-cli pr +merge --id 3 --method squash
pr +close需认证关闭 PR
gitlink-cli pr +close --id 3
pr +files公开变更文件列表
gitlink-cli pr +files --id 3
pr +diff公开查看提交列表
gitlink-cli pr +diff --id 3
pr +comment需认证PR 评论
gitlink-cli pr +comment --id 3 --body "LGTM"
pr +review需认证代码审查
gitlink-cli pr +review --id 3 --event COMMENT --body "整体 LGTM"
🚀版本发布4 个命令
release +list公开发布列表
gitlink-cli release +list --owner Gitlink --repo forgeplus
release +view公开发布详情
gitlink-cli release +view --id <version_id>
release +create需认证创建发布
gitlink-cli release +create --tag v1.0.0 --name "v1.0.0" --target master
release +delete需认证删除发布(不可逆)
gitlink-cli release +delete --id <version_id>
📖Wiki 管理5 个命令
wiki +list公开Wiki 页面列表
gitlink-cli wiki +list --owner Gitlink --repo forgeplus
wiki +view公开查看页面内容
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --title "Home"
wiki +create需认证创建页面
gitlink-cli wiki +create --owner myuser --repo myrepo --title "API 文档" --file ./api.md
wiki +update需认证更新页面
gitlink-cli wiki +update --owner myuser --repo myrepo --title "设计文档" --add "新内容"
wiki +delete需认证删除页面
gitlink-cli wiki +delete --owner myuser --repo myrepo --title "废弃页面"
⚙️CI/CD4 个命令
ci +builds需认证构建列表
gitlink-cli ci +builds --owner myuser --repo myrepo
ci +logs需认证构建日志
gitlink-cli ci +logs --build 42 --stage 1 --step 1
ci +restart需认证重启构建
gitlink-cli ci +restart --build 42
ci +stop需认证停止构建
gitlink-cli ci +stop --build 42
🔔Webhook7 个命令
webhook +list需认证Webhook 列表
gitlink-cli webhook +list --owner myuser --repo myrepo
webhook +info需认证Webhook 详情
gitlink-cli webhook +info --owner myuser --repo myrepo --id 123
webhook +events公开支持的事件类型
gitlink-cli webhook +events
webhook +create需认证创建 Webhook
gitlink-cli webhook +create --url https://example.com/hook --events push
webhook +update需认证更新 Webhook
gitlink-cli webhook +update --id 123 --events push,pull_request
webhook +test需认证测试 Webhook
gitlink-cli webhook +test --id 123 --event push
webhook +delete需认证删除 Webhook
gitlink-cli webhook +delete --id 123
🏢组织管理5 个命令
org +list公开组织列表
gitlink-cli org +list
org +info公开组织详情
gitlink-cli org +info --id Gitlink
org +members公开成员列表
gitlink-cli org +members --id Gitlink
org +create需认证创建组织
gitlink-cli org +create --name my-org --description "我的组织"
org +batch-add需认证批量添加成员
gitlink-cli org +batch-add --id my-org --users "user1,user2"
👤用户与搜索4 个命令
user +me需认证当前登录用户
gitlink-cli user +me
user +info公开用户详情
gitlink-cli user +info --login zhangsan
search +repos公开搜索仓库
gitlink-cli search +repos --keyword "machine learning"
search +users公开搜索用户
gitlink-cli search +users --keyword "zhangsan"
🛡️安全与合规6 个命令
compliance +scan公开全量扫描
gitlink-cli compliance +scan
compliance +license公开许可证合规检查
gitlink-cli compliance +license
compliance +deps公开依赖许可证检查
gitlink-cli compliance +deps
compliance +secrets公开敏感信息扫描
gitlink-cli compliance +secrets
compliance +exposure公开PII 与暴露面扫描
gitlink-cli compliance +exposure
compliance +vocab公开敏感词汇扫描
gitlink-cli compliance +vocab
👋新人引导1 个命令
onboard +welcome需认证添加引导评论
gitlink-cli onboard +welcome --issues "3,7,15"
👥团队管理3 个命令
team +list公开团队列表
gitlink-cli team +list --org my-org
team +create需认证创建团队
gitlink-cli team +create --org my-org --name dev-team
team +add-member需认证添加成员
gitlink-cli team +add-member --org my-org --team dev-team --user newmember
📊贡献报告1 个命令
contrib +report公开贡献统计报告
gitlink-cli contrib +report --owner myuser --repo myrepo
-
生成时间: 2026-06-23 15:54 · 运行 /code-insight 重新生成
+ +
+
+
+ +
+

gitlink-cli · 功能全景

+
交互式命令浏览仪表盘
+
+
+
+ + + / +
+
+
+ +
+
0
功能分类
+
0
Shortcuts 总数
+
0
需认证命令
+
0
公开命令
+
+ +
+
+
+
🔍
+
没有匹配的命令,换个关键词试试。
+
+
+ +
+ Generated by gitlink-code-insight skill · + Gitlink · + 共 0 个命令 +
+ - \ No newline at end of file + diff --git a/doc/generate_word.py b/doc/generate_word.py new file mode 100644 index 00000000..19912bdb --- /dev/null +++ b/doc/generate_word.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成系统动态模型章节的 Word 文档 - 四个赛题""" + +from docx import Document +from docx.shared import Pt, Inches, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.table import WD_TABLE_ALIGNMENT +from docx.oxml.ns import qn +from docx.oxml import OxmlElement + +def set_cell_shading(cell, color): + """设置单元格背景颜色""" + shading = OxmlElement('w:shd') + shading.set(qn('w:fill'), color) + cell._tc.get_or_add_tcPr().append(shading) + +def add_table(doc, headers, rows, header_color="4472C4"): + """添加表格""" + table = doc.add_table(rows=len(rows)+1, cols=len(headers)) + table.style = 'Table Grid' + table.alignment = WD_TABLE_ALIGNMENT.CENTER + + # 表头 + for i, header in enumerate(headers): + cell = table.rows[0].cells[i] + cell.text = header + set_cell_shading(cell, header_color) + for paragraph in cell.paragraphs: + paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + for run in paragraph.runs: + run.font.bold = True + run.font.color.rgb = RGBColor(255, 255, 255) + + # 数据行 + for i, row in enumerate(rows): + for j, value in enumerate(row): + table.rows[i+1].cells[j].text = value + + return table + +def create_document(): + doc = Document() + + # 设置默认字体 + style = doc.styles['Normal'] + style.font.name = 'Microsoft YaHei' + style._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei') + + # 标题 + title = doc.add_heading('系统动态模型', 0) + title.alignment = WD_ALIGN_PARAGRAPH.CENTER + + # 概述 + doc.add_heading('概述', level=1) + doc.add_paragraph( + '系统动态模型描述了 gitlink-cli 在四个赛题场景下的运行时行为,' + '展示了各组件之间的交互流程和数据流向。每个赛题对应一个核心能力方向,' + '覆盖 CLI 功能扩展、Skills 开发、自动化工作流和科研辅助四大领域。' + ) + + # ==================== 赛题一 ==================== + doc.add_heading('子赛题一:增加和完善 GitLink-CLI 能力', level=1) + + doc.add_heading('1.1 能构建的模型', level=2) + doc.add_paragraph( + '本赛题聚焦于 CLI 命令系统的功能扩展和优化。可构建以下动态模型:' + ) + doc.add_paragraph('命令执行模型:描述 Shortcut 命令从参数解析到 API 调用的完整执行流程', style='List Bullet') + doc.add_paragraph('批量操作模型:描述批量命令(batch-close、batch-create 等)的遍历执行和错误处理机制', style='List Bullet') + doc.add_paragraph('参数校验模型:描述 Choices 枚举校验和 Validate 自定义校验的执行流程', style='List Bullet') + doc.add_paragraph('输出格式化模型:描述 Envelope 数据经过 table/json/yaml 格式化后的输出流程', style='List Bullet') + + doc.add_heading('1.2 新生成的组件', level=2) + add_table(doc, + ['组件类型', '组件名称', '功能说明'], + [ + ['Shortcut', 'wiki +list/+view/+create/+update/+delete', 'Wiki 页面 CRUD 操作'], + ['Shortcut', 'webhook +list/+create/+update/+delete/+test', 'Webhook 配置管理'], + ['Shortcut', 'board +view/+columns/+issues/+move/+assign', '项目看板操作'], + ['Shortcut', 'issue +batch-close/+batch-create/+batch-assign', 'Issue 批量操作'], + ['Shortcut', 'repo +batch-create/+batch-update', '仓库批量操作'], + ['Shortcut', 'pr +merge (支持 merge/rebase/squash)', 'PR 合并方式选择'], + ['Shortcut', 'branch +protect/+unprotect', '分支保护规则管理'], + ['Shortcut', 'release +list/+create/+view/+delete', '版本发布管理'], + ['结构体', 'Flag.Choices', '枚举参数校验,自动追加 [val1|val2] 提示'], + ['结构体', 'Flag.Validate', '自定义校验函数,跨参数约束'], + ['结构体', 'PrintOptions', '输出选项:列过滤、禁用截断、彩色输出'], + ['函数', 'OpError()', '统一错误构造函数,中英文双语错误信息'], + ] + ) + + doc.add_heading('1.3 交互流程示例', level=2) + + doc.add_paragraph('示例一:Wiki 页面创建', style='List Bullet') + steps = [ + '用户输入:gitlink-cli wiki +create --title "API文档" --content "# API Reference"', + 'Cobra 解析命令,匹配到 wiki +create Shortcut', + 'Validate 校验:检查 --title 是否为空', + 'ResolveOwnerRepo() 从 git remote 解析 owner/repo', + 'CallAPI("POST", "/wiki/open/createWiki", body) 调用 Gateway API', + 'OutputData() 格式化输出创建结果', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + doc.add_paragraph('示例二:枚举参数校验', style='List Bullet') + steps = [ + '用户输入:gitlink-cli pr +merge --id 42 --method unknown', + 'Cobra 解析参数,发现 --method 值为 unknown', + 'Choices 校验:unknown 不在 [merge, rebase, squash] 中', + '输出错误:invalid value "unknown" for --method,有效值: merge, rebase, squash', + '命令终止,exit code = 1', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + doc.add_paragraph('示例三:批量关闭 Issue(dry-run 预览)', style='List Bullet') + steps = [ + '用户输入:gitlink-cli issue +batch-close --numbers 101,102,103 --dry-run', + '解析 --numbers 参数,得到 [101, 102, 103]', + 'IsDryRun() == true,进入预览模式', + '构建 BatchSummary {dry_run: true, total: 3, results: [{status: "planned"},...]}', + '输出预览结果,不发起 HTTP 请求', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + # ==================== 赛题二 ==================== + doc.add_heading('子赛题二:编写和丰富 GitLink Skills', level=1) + + doc.add_heading('2.1 能构建的模型', level=2) + doc.add_paragraph( + '本赛题聚焦于 AI Agent Skill 的开发。可构建以下动态模型:' + ) + doc.add_paragraph('Skill 加载模型:描述 Claude Code 触发 Skill、加载 SKILL.md 和 reference 文档的流程', style='List Bullet') + doc.add_paragraph('AI 分析模型:描述 AI 对 PR Diff、Issue 内容、仓库数据的分析和决策流程', style='List Bullet') + doc.add_paragraph('自动执行模型:描述 AI 分析结果转化为 CLI 命令执行的流程', style='List Bullet') + + doc.add_heading('2.2 新生成的组件', level=2) + add_table(doc, + ['组件类型', '组件名称', '功能说明'], + [ + ['Skill', 'gitlink-code-review', '智能代码审查:分析 PR diff,输出结构化 Review 意见'], + ['Skill', 'gitlink-issue-triage', 'Issue 自动分拣:根据内容自动分类、打标签、分配责任人'], + ['Skill', 'gitlink-changelog', 'Release Notes 生成:根据 commit 和 PR 生成版本说明'], + ['Skill', 'gitlink-health', '项目健康度报告:统计 Issue 响应时间、PR 合并效率'], + ['Skill', 'gitlink-compliance', '许可证合规检查:扫描许可证合规性和敏感信息泄露'], + ['Skill', 'gitlink-onboard', '新人引导:为 good-first-issue 自动添加引导评论'], + ['Skill', 'gitlink-stale', '过期 Issue 管理:自动标记和关闭长期未活动的 Issue'], + ['Skill', 'gitlink-faq', 'FAQ 自动回复:根据 Issue 内容匹配 FAQ 并自动评论'], + ['Reference', 'references/pr-review.md', 'PR 审查参考文档:审查维度、评分标准、评论模板'], + ['Reference', 'references/issue-triage.md', 'Issue 分类参考文档:分类规则、标签映射、分配策略'], + ] + ) + + doc.add_heading('2.3 交互流程示例', level=2) + + doc.add_paragraph('示例一:智能代码审查', style='List Bullet') + steps = [ + '用户触发:"帮我审查 PR #42"', + 'Claude Code 加载 gitlink-code-review skill', + '获取 PR 详情:gitlink-cli pr +view --id 42 --format json', + '获取变更文件:gitlink-cli pr +files --id 42 --format json', + 'AI 分析代码:检测未处理 error、SQL 注入风险、性能问题', + '生成审查报告:质量评分 8.5/10,列出 3 个问题', + '提交审查评论:gitlink-cli pr +review --id 42 --body "..." --event comment', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + doc.add_paragraph('示例二:Issue 自动分拣', style='List Bullet') + steps = [ + '用户触发:"帮我分类本周的 Issue"', + 'Claude Code 加载 gitlink-issue-triage skill', + '获取 Issue 列表:gitlink-cli issue +list --state open --format json', + 'AI 分析内容:识别关键词(bug/功能/提问)', + '自动添加标签:gitlink-cli issue +label-add --number 101 --labels "缺陷"', + '自动分配责任人:gitlink-cli issue +update --number 101 --assignee "zhangsan"', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + doc.add_paragraph('示例三:Release Notes 生成', style='List Bullet') + steps = [ + '用户触发:"帮我生成 v2.1.0 的 Release Notes"', + 'Claude Code 加载 gitlink-changelog skill', + '获取已合并 PR:gitlink-cli pr +list --state merged --format json', + '获取 commit 历史:git log --oneline v2.0.0..HEAD', + 'AI 分类整理:新功能、Bug 修复、性能优化、Breaking Changes', + '生成 Release Notes:gitlink-cli release +create --tag v2.1.0 --body "..."', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + # ==================== 赛题三 ==================== + doc.add_heading('子赛题三:构建端到端自动化工作流', level=1) + + doc.add_heading('3.1 能构建的模型', level=2) + doc.add_paragraph( + '本赛题聚焦于串联多个步骤的完整解决方案。可构建以下动态模型:' + ) + doc.add_paragraph('工作流调度模型:描述工作流从触发、菜单选择、步骤执行到结果输出的完整流程', style='List Bullet') + doc.add_paragraph('多步骤编排模型:描述多个 CLI 命令和 Skill 的串联执行和错误处理', style='List Bullet') + doc.add_paragraph('数据聚合模型:描述从多个数据源采集数据并汇总生成报告的流程', style='List Bullet') + + doc.add_heading('3.2 新生成的组件', level=2) + add_table(doc, + ['组件类型', '组件名称', '功能说明'], + [ + ['Skill', 'gitlink-workflows', '工作流调度入口,提供功能菜单选择'], + ['Workflow', '01-community-ops.sh', '社区运营自动化:Issue 分类 + 周报生成'], + ['Workflow', '01a-issue-triage.sh', 'Issue 分类子工作流'], + ['Workflow', '01a-webhook-setup.sh', 'Webhook 配置子工作流'], + ['Workflow', '02-code-quality-gatekeeper.sh', '代码质量门禁:PR 审查 + 评分'], + ['Workflow', '03-project-init.sh', '项目一键初始化:仓库 + 文件 + CI + Issues'], + ['Workflow', '04-multi-repo-collab.sh', '多仓库协同:依赖追踪 + 协同发版'], + ['Workflow', '05-contributor-growth.sh', '贡献者成长体系:评分 + 排行 + Badge'], + ['Lib', 'lib/common.sh', '公共函数库:认证检查、错误处理、日志输出'], + ] + ) + + doc.add_heading('3.3 交互流程示例', level=2) + + doc.add_paragraph('示例一:项目一键初始化', style='List Bullet') + steps = [ + '用户触发:"帮我初始化新项目 my-project"', + 'Claude Code 加载 gitlink-workflows skill,显示菜单', + '用户选择"项目一键初始化"', + '认证检查:gitlink-cli auth status', + '创建仓库:gitlink-cli repo +create --name my-project --private', + '初始化文件:gitlink-cli file +create --path README.md --content "..."', + '初始化文件:gitlink-cli file +create --path .gitignore --content "..."', + '初始化文件:gitlink-cli file +create --path LICENSE --content "..."', + '配置分支保护:gitlink-cli branch +protect --name master --require-review', + '创建 Issues:gitlink-cli issue +create --title "完善单元测试" --labels "good-first-issue"', + '输出初始化报告', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + doc.add_paragraph('示例二:多仓库协同发版', style='List Bullet') + steps = [ + '用户触发:"帮我管理多仓库协同"', + '扫描仓库列表:gitlink-cli repo +list --user my-org --format json', + '分析依赖关系:frontend → api-client → backend', + '查询跨仓库 PR:gitlink-cli pr +list --state open --format json', + '检查发版就绪:gitlink-cli release +list --format json', + '识别阻塞项:backend PR #456 未合并', + '生成协同 Dashboard:显示各仓库状态、依赖关系、阻塞项', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + doc.add_paragraph('示例三:贡献者成长体系', style='List Bullet') + steps = [ + '用户触发:"帮我生成贡献者排行榜"', + '获取贡献者列表:gitlink-cli repo +members --format json', + '统计 PR 活动:gitlink-cli pr +list --state merged --format json', + '统计 Issue 活动:gitlink-cli issue +list --state closed --format json', + 'AI 计算贡献评分:PR 数量 × 3 + Issue 数量 × 1 + Review × 2', + '生成排行榜:zhangsan(85分) > lisi(72分) > wangwu(68分)', + '自动颁发 Badge:为 top 3 贡献者添加 "core-contributor" 标签', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + # ==================== 赛题四 ==================== + doc.add_heading('子赛题四:应用 GitLink 辅助科研', level=1) + + doc.add_heading('4.1 能构建的模型', level=2) + doc.add_paragraph( + '本赛题聚焦于科研场景的智能化赋能。可构建以下动态模型:' + ) + doc.add_paragraph('科研项目洞悉模型:描述从仓库数据提取科研项目信息(技术栈、活跃度、依赖)的流程', style='List Bullet') + doc.add_paragraph('热点追踪模型:描述从 Issue/PR/Commit 数据挖掘科研热点趋势的流程', style='List Bullet') + doc.add_paragraph('合规检查模型:描述许可证合规性扫描和敏感信息检测的流程', style='List Bullet') + doc.add_paragraph('协作匹配模型:描述根据贡献者技能和项目需求进行智能匹配的流程', style='List Bullet') + + doc.add_heading('4.2 新生成的组件', level=2) + add_table(doc, + ['组件类型', '组件名称', '功能说明'], + [ + ['Skill', 'gitlink-research', '科研辅助系统总入口,提供科研场景菜单'], + ['Skill', 'gitlink-code-insight', '仓库级科研项目洞悉:技术栈、活跃度、依赖分析'], + ['Skill', 'gitlink-compliance', '科研项目合规与复现性检查:许可证、依赖、环境'], + ['Workflow', '06-research-insights.sh', '科研热点追踪:从 Issue/PR 挖掘研究趋势'], + ['Workflow', '08-research-compliance.sh', '科研合规检查:扫描许可证和敏感信息'], + ['Workflow', '10-research-progress.sh', '科研进度跟踪:里程碑进度、阻塞项识别'], + ['Workflow', '11-research-citation.sh', '论文引用分析:追踪仓库的学术引用情况'], + ] + ) + + doc.add_heading('4.3 交互流程示例', level=2) + + doc.add_paragraph('示例一:仓库级科研项目洞悉', style='List Bullet') + steps = [ + '用户触发:"帮我分析这个仓库的科研价值"', + 'Claude Code 加载 gitlink-code-insight skill', + '获取仓库信息:gitlink-cli repo +info --format json', + '分析技术栈:扫描 package.json/go.mod/requirements.txt', + '分析活跃度:gitlink-cli pr +list --state merged --format json', + '分析依赖:识别上下游依赖关系', + '生成科研洞悉报告:技术栈、活跃贡献者、核心模块、研究方向', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + doc.add_paragraph('示例二:科研热点追踪', style='List Bullet') + steps = [ + '用户触发:"帮我追踪 AI 领域的科研热点"', + '扫描相关仓库:gitlink-cli search +repos --query "machine-learning"', + '获取 Issue 列表:gitlink-cli issue +list --state open --format json', + '获取 PR 列表:gitlink-cli pr +list --state merged --format json', + 'AI 分析关键词:识别高频技术词汇(transformer、diffusion、LLM)', + '生成热点报告:技术趋势、热门项目、活跃研究者', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + doc.add_paragraph('示例三:科研合规与复现性检查', style='List Bullet') + steps = [ + '用户触发:"帮我检查这个科研项目的合规性"', + 'Claude Code 加载 gitlink-compliance skill', + '扫描许可证:检查 LICENSE 文件和依赖许可证', + '扫描敏感信息:检查 API Key、密码、私钥泄露', + '检查复现性:验证 requirements.txt/go.mod 完整性', + '生成合规报告:许可证合规、依赖风险、复现性评分', + ] + for i, step in enumerate(steps, 1): + doc.add_paragraph(f' {i}. {step}') + + # ==================== 赛题对比总结 ==================== + doc.add_heading('赛题对比总结', level=1) + add_table(doc, + ['维度', '赛题一:CLI 能力', '赛题二:Skills 开发', '赛题三:自动化工作流', '赛题四:辅助科研'], + [ + ['核心目标', '扩展 CLI 命令', '开发 AI Skill', '串联完整流程', '科研场景赋能'], + ['技术栈', 'Go + Cobra', 'Markdown + CLI', 'Shell + CLI + Skill', '数据分析 + AI'], + ['交付物', 'Shortcut + 结构体', 'SKILL.md + Reference', 'Workflow 脚本', '科研 Skill + 报告'], + ['AI 参与', '无', '核心(AI 分析)', '调度 + 分析', '深度(知识挖掘)'], + ['典型场景', 'Wiki/Webhook/批量', '代码审查/Issue 分类', '项目初始化/协同发版', '热点追踪/合规检查'], + ] + ) + + # ==================== 组件依赖关系 ==================== + doc.add_heading('组件依赖关系', level=1) + dep_tree = """gitlink-cli (CLI 核心) +├── Shortcuts (赛题一) +│ ├── wiki +list/+view/+create/+update/+delete +│ ├── webhook +list/+create/+update/+delete/+test +│ ├── board +view/+columns/+issues/+move/+assign +│ ├── issue +batch-close/+batch-create/+batch-assign +│ ├── repo +batch-create/+batch-update +│ └── branch +protect/+unprotect +│ +├── Skills (赛题二) +│ ├── gitlink-code-review (代码审查) +│ ├── gitlink-issue-triage (Issue 分类) +│ ├── gitlink-changelog (Release Notes) +│ ├── gitlink-health (项目健康度) +│ ├── gitlink-compliance (合规检查) +│ ├── gitlink-onboard (新人引导) +│ └── gitlink-stale (过期管理) +│ +├── Workflows (赛题三) +│ ├── 01-community-ops.sh (社区运营) +│ ├── 02-code-quality-gatekeeper.sh (质量门禁) +│ ├── 03-project-init.sh (项目初始化) +│ ├── 04-multi-repo-collab.sh (多仓库协同) +│ └── 05-contributor-growth.sh (贡献者成长) +│ +└── Research Skills (赛题四) + ├── gitlink-research (科研总入口) + ├── gitlink-code-insight (项目洞悉) + ├── gitlink-compliance (合规检查) + └── research workflows (热点/进度/引用)""" + p = doc.add_paragraph() + run = p.add_run(dep_tree) + run.font.name = 'Consolas' + + # ==================== 状态转换说明 ==================== + doc.add_heading('状态转换说明', level=1) + + doc.add_heading('Shortcut 命令执行状态', level=2) + doc.add_paragraph('Init → Parsing → Validation → Resolving → Loading → Executing → APICall → Done/Error') + doc.add_paragraph('任何阶段出现错误都会进入 Error 状态,输出结构化错误信息后终止。') + + doc.add_heading('Skill 触发状态', level=2) + doc.add_paragraph('Idle → Triggered → Loaded → AuthCheck → DataCollection → AIAnalysis → Execution → Output') + doc.add_paragraph('Skill 加载失败时回退到 Idle 状态,AI 分析失败时输出错误提示。') + + doc.add_heading('Workflow 执行状态', level=2) + doc.add_paragraph('Idle → Selecting → Loading → AuthCheck → Step1 → Step2 → ... → StepN → Done') + doc.add_paragraph('任一步骤失败时根据配置决定继续或终止,最终输出执行汇总。') + + # 保存 + output_path = r'C:\Users\Lenovo\Desktop\soft运维\gitlink-cli\doc\系统动态模型章节.docx' + doc.save(output_path) + print(f'Word 文档已生成:{output_path}') + +if __name__ == '__main__': + create_document() diff --git a/doc/任务一/.claude/settings.local.json b/doc/任务一/.claude/settings.local.json new file mode 100644 index 00000000..078d0bba --- /dev/null +++ b/doc/任务一/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(python -c \"import docx; print\\(''ok''\\)\")", + "Bash(pip install:*)", + "Bash(python generate_report.py)" + ] + } +} diff --git a/doc/作业要求.txt b/doc/作业要求.txt new file mode 100644 index 00000000..6fb94b3e --- /dev/null +++ b/doc/作业要求.txt @@ -0,0 +1,56 @@ +子赛题一:增加和完善GitLink-CLI能力 +定位:扩展CLI功能|难度:中高|需要Go语言基础 +为gitlink-cli增加新功能或优化现有功能,包括但不限于: +●新增Shortcut命令(如Wiki管理、Webhook配置、项目看板增强、代码片段管理等) + +●优化现有命令的参数设计、输出格式、错误提示和帮助文档 + +·增加批量操作能力(如批量Issue操作、批量仓库管理、批量成员邀请) + +·提升跨平台兼容性和安装体验 + +·补全Raw API封装(对齐GitLink OpenAPI中尚未封装的接口) + + + + +子赛题二:编写和丰富GitLink Skills +定位:开发Agent Skill|难度:中|无需Go,Markdown +CLI调用即可 +基于gitlink-cli开发新的AI Agent Skill。核心交付物是Skill本身(SKILLmd+使用示例+Agent平台验证结果)。场景示例(不限于此): +·智能代码审查:分析PR diff,输出结构化Review意见并自动评论 + +·Issue自动分拣:根据内容自动分类、打标签、分配责任人 + +·Release Notes生成:根据commit和PR记录生成结构化版本说明 + +·项目健康度报告:统计Issue响应时间、PR合并效率、贡献者活跃度 + +·许可证合规检查:扫描仓库的许可证合规性和敏感信息泄露风险 + +·新人引导:为good-first-issue自动添加引导评论,降低新贡献者参与门槛 + + + + +子赛题三:构建端到端自动化工作流 +定位:组合现有能力解决实际问题丨难度:低-中「无需写底层代码 +组合gitlink-cli已有命令和Skills(可包含自定义Skill),完成一个可复现的端到端自动化场景。与子赛题二的区别在于:子赛题二 +交付的是独立可复用的Skill,子赛题三交付的是串联多个步骤的完整解决方案。 +场景示例(不限于此): +·项目一键初始化:输入项目描述→创建仓库→生成README/LICENSE/CI配置→创建初始IsSUe和里程碑 +·多仓库协同:跨多个仓库的统一Issue追踪、PR状态看板、Release协调发布 +·贡献者成长体系:追踪贡献者的PR/Issue活动→生成贡献排行→自动颁发徽章 + + +子赛题四:应用 GitLink 辅助科研(可选完成,视完成情况额外加分) + 定位:科研场景智能化赋能 | 难度:中 | 要求:无需 Go,Markdown + CLI 调用即可 + 任务:依托 gitlink-cli 数据获取、命令调用与 AI Agent 能力,融合数据分析、知识图谱、AI 挖掘技术,面向 +科研工作者、课题组与科研团队,将 GitLink 平台代码托管、协作数据转化为科研创新支撑能力,实现科研项目 +分析、主体画像、热点追踪、创新启发、合规校验等全链路科研辅助服务,打通开源代码生态与学术科研的融 +合通道,包括但不限于以下场景: +• 仓库级科研项目洞悉 +• 科研热点追踪与知识图谱构建 +• 科研项目合规与复现性检查 +• 科研协作智能匹配 +• 科研进度智能跟踪与预警 + diff --git a/doc/子任务一提交文档.md b/doc/子任务一提交文档.md new file mode 100644 index 00000000..a26dc60a --- /dev/null +++ b/doc/子任务一提交文档.md @@ -0,0 +1,294 @@ +# 子任务一:CLI 命令系统优化 + +## PR: feat: CLI 命令系统优化 — 参数校验、输出格式、错误提示、帮助文档 + +### Summary + +本次优化针对 gitlink-cli 的命令系统进行了四个方面的改进,提升用户体验和开发者效率: + +1. **参数设计** — 新增 `Choices` 枚举校验和 `Validate` 自定义校验,非法参数在本地拦截而非等待 API 返回 422 +2. **输出格式** — 新增 `--columns`、`--no-truncate`、`--no-color` 全局 flag,支持列过滤、禁用截断、彩色表头 +3. **错误提示** — 统一 `OpError` 构造函数,错误信息包含英文 `message`(脚本可解析)+ 中文 `suggestion`(用户可读) +4. **帮助文档** — `Shortcut` 新增 `Long`/`Example` 字段,15 个 Group 命令补充详细说明,新增 `completion` 子命令 + +--- + +### 一、参数设计优化 + +#### 问题 + +- 枚举参数无校验,非法值直接发往 API,等服务端返回 422 才知道参数错误 +- 跨参数约束散落在 `Run()` 函数中,格式不统一,中英文混杂 + +#### 解决方案 + +**1. Choices 枚举校验** + +```go +// Flag 结构体新增 Choices 字段 +type Flag struct { + Name string + Short string + Usage string + Required bool + Default string + Bool bool + Choices []string // 新增:枚举校验 + Validate func(value string) error // 新增:自定义校验函数 +} + +// 命令中声明 Choices +Flags: []common.Flag{ + {Name: "method", Short: "m", Usage: "Merge method", Default: "merge", + Choices: []string{"merge", "rebase", "squash"}}, +} +``` + +Choices 声明后,无需再写校验函数,`--help` 自动追加 `[merge|rebase|squash]`。 + +**2. Validate 自定义校验** + +```go +// Shortcut 结构体新增 Validate 字段 +type Shortcut struct { + Name string + Description string + Flags []Flag + Validate func(args map[string]string) error // 新增:跨参数校验 + Run func(ctx *RuntimeContext) error +} +``` + +**3. 统一错误格式** + +```go +// 使用 clierrors.InputError 统一错误格式 +if title == "" && description == "" && state == "" { + return clierrors.InputError( + "at least one of --title, --body, or --state is required", + "至少需要提供 --title、--body 或 --state 中的一个参数", + ).WithCommand(ctx.CommandName) +} +``` + +--- + +### 二、输出格式优化 + +#### 问题 + +- 表格输出列全量显示,无法过滤 +- 长文本在 60 字符处硬截断,不可配置 +- 无色彩区分,可读性差 + +#### 解决方案 + +**1. PrintOptions 结构体** + +```go +type PrintOptions struct { + Columns []string // 要显示的列,nil = 全部 + NoTruncate bool // 禁用 60 字符截断 + UseColor bool // 启用 ANSI 颜色 +} +``` + +**2. 新增全局 Flag** + +```bash +gitlink-cli issue +list --columns id,subject,status +gitlink-cli issue +list --no-truncate +gitlink-cli issue +list --no-color +``` + +**3. 默认格式修正** + +`--format` 默认值从 `json` 改为 `table`,与 `--help` 文本一致。 + +--- + +### 三、错误提示优化 + +#### 问题 + +- 中英文混用,格式不统一 +- `fmt.Errorf` 不被框架识别,无法输出结构化错误 + +#### 解决方案 + +**1. OpError 统一构造函数** + +```go +// 入参只需动词和资源名 +func OpError(kind ErrorKind, op, resource string, cause error) *CLIError { + msg := fmt.Sprintf("failed to %s %s", op, resource) + sugg := opSuggestion(op, resource) + return Wrap(kind, msg, sugg, cause) +} + +// 命令中一行调用 +env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issues", q) +if err != nil { + return clierrors.OpError(clierrors.KindServer, "list", "issues", err). + WithCommand(ctx.CommandName) +} +``` + +**2. 结构化输出** + +```json +{ + "ok": false, + "error": { + "code": 500, + "message": "failed to list issues", + "suggestion": "获取列表失败,请检查参数或网络连接,稍后重试" + } +} +``` + +`message` 用英文保证脚本可解析,`suggestion` 用中文直接给人看。 + +--- + +### 四、帮助文档优化 + +#### 问题 + +- Shortcut 无详细帮助,`--help` 只有命令行和 flag 列表 +- Group 命令只有一行描述 +- 无 Shell 自动补全 + +#### 解决方案 + +**1. Shortcut 新增 Long/Example 字段** + +```go +type Shortcut struct { + Name string + Description string + Long string // 新增:详细帮助文本 + Example string // 新增:使用示例 + Flags []Flag + Run func(ctx *RuntimeContext) error +} + +// 命令中填写 Example +{ + Name: "merge", + Description: "Merge a pull request", + Example: " gitlink-cli pr +merge --id 42\n" + + " gitlink-cli pr +merge --id 42 --method rebase\n" + + " gitlink-cli pr +merge --id 42 --method squash --dry-run", +} +``` + +**2. 15 个 Group 命令补充 Long/Example** + +```go +infos := map[string]groupInfo{ + "pr": { + Short: "Pull request operations", + Long: "Manage pull requests: list, create, view, merge, close, review, and view changed files.", + Example: " gitlink-cli pr +list --state open\n" + + " gitlink-cli pr +create --title \"Fix login\" --head feat-branch\n" + + " gitlink-cli pr +merge --id 42", + }, + // 其余 14 个 group 同上 +} +``` + +**3. 新增 completion 子命令** + +```bash +source <(gitlink-cli completion bash) # Bash +source <(gitlink-cli completion zsh) # Zsh +gitlink-cli completion fish | source # fish +gitlink-cli completion powershell | Out-String | Invoke-Expression # PowerShell +``` + +--- + +### Modified Files + +| 文件 | 改动 | +|------|------| +| `cmd/cmdutil/globals.go` | 新增 NoTruncate/Columns/NoColor 全局变量 | +| `cmd/root.go` | 新增 3 个 persistent flag + completion 子命令 + format 默认值修正 | +| `shortcuts/common/types.go` | Shortcut/Flag/RuntimeContext 结构体扩展 | +| `shortcuts/common/runner.go` | Choices/Validate 校验逻辑 + Long/Example 挂载 | +| `internal/output/formatter.go` | PrintOptions + 列过滤 + 去截断 + 彩色表头 | +| `internal/errors/errors.go` | OpError 统一构造函数 | +| `shortcuts/register.go` | 15 个 Group 命令补充 Long/Example | +| `shortcuts/issue/issue.go` | 错误统一 + Choices + Long/Example | +| `shortcuts/pr/pr.go` | 错误统一 + Choices + Long/Example | +| `shortcuts/repo/repo.go` | 错误统一 + Choices + Long/Example | +| `shortcuts/milestone/milestone.go` | 修复 milestone +view 输出问题 | +| `shortcuts/board/board.go` | 修正 assign 字段 | + +--- + +### Testing + +- `go build` 通过 +- `go vet` 通过 +- `go test ./...` 7 个测试包全部通过 +- 所有新增字段零值安全,现有 80+ Shortcut 无需修改即可编译 + +--- + +### Breaking Changes + +无。`output.Print()` 签名不变,内部包装为 `PrintWithOpts`。Workflow 脚本显式使用 `--format json`,不受默认格式变化影响。 + +--- + +### 验证示例 + +**1. 枚举校验** + +```bash +$ gitlink-cli pr +merge --id 42 --method unknown +Error: invalid value "unknown" for --method +有效值: merge, rebase, squash。运行 'gitlink-cli pr +merge --help' 查看用法。 +``` + +**2. 跨参数约束** + +```bash +$ gitlink-cli issue +update --number 42 +Error: at least one of --title, --body, or --state is required +至少需要提供 --title、--body 或 --state 中的一个参数 +``` + +**3. 列过滤** + +```bash +$ gitlink-cli issue +list --columns id,subject,status +id subject status +-- ------- ------ +42 Fix login bug open +43 Add unit tests closed +``` + +**4. 结构化错误** + +```bash +$ gitlink-cli issue +view --number 999999 --format json +{ + "ok": false, + "error": { + "code": 404, + "message": "failed to view issue", + "suggestion": "查看失败,请确认资源 ID 是否存在" + } +} +``` + +**5. Shell 自动补全** + +```bash +$ source <(gitlink-cli completion bash) +$ gitlink-cli pr + ++list +create +view +merge +close +reopen +update +files +diff +comment +``` diff --git a/doc/系统动态模型章节.docx b/doc/系统动态模型章节.docx new file mode 100644 index 00000000..63aa4b4a Binary files /dev/null and b/doc/系统动态模型章节.docx differ diff --git a/doc/软件分析及建模报告.docx b/doc/软件分析及建模报告.docx new file mode 100644 index 00000000..b837e940 Binary files /dev/null and b/doc/软件分析及建模报告.docx differ diff --git a/doc/软件分析及建模报告.pptx b/doc/软件分析及建模报告.pptx new file mode 100644 index 00000000..042ed6c4 Binary files /dev/null and b/doc/软件分析及建模报告.pptx differ diff --git a/doc/软件分析及建模报告.txt b/doc/软件分析及建模报告.txt new file mode 100644 index 00000000..03a0f210 --- /dev/null +++ b/doc/软件分析及建模报告.txt @@ -0,0 +1,408 @@ +目录 +1. 项目概述 +2、软件总体设计 +2.1 软件体系结构设计 +2.2 用户界面设计 +2.3 数据库设计 +2.4 系统度量 +3、系统静态模型 +3.1 命令框架层设计 +3.2 快捷命令层设计 +3.3 核心库层设计 +4、系统动态模型 +4.1 用例一:用户认证与仓库操作 +4.2 用例二:Issue 批量管理 +4.3 用例三:AI 自动化工作流 +5、系统部署模型 + + +1. 项目概述 + +gitlink-cli 是 GitLink(确实开源)平台的命令行工具。GitLink 是 CCF 官方开源协作平台,后端提供 490+ API 端点,但此前缺少官方 CLI 工具。本项目填补了这个空白。 + +应用背景: +开发者在日常工作中需要频繁操作仓库、Issue、PR、分支等资源。浏览器操作效率低,无法批量处理,也不能与 CI/CD 流水线集成。gitlink-cli 让开发者在终端中完成所有平台操作,并通过 AI Agent Skill 支持 Claude Code 自动化执行复杂工作流。 + +功能描述: +- 仓库管理:创建、列表、详情、Fork、删除、设置、批量操作 +- Issue 管理:列表、创建、查看、更新、关闭、评论、标签、6 种批量命令 +- PR 管理:列表、创建、查看、合并、关闭、代码审查、变更文件查看 +- 分支管理:列表、创建、删除、保护 +- 发布管理:列表、创建、查看、删除 +- Wiki 管理:列表、创建、查看、更新、删除 +- Webhook 管理:列表、创建、更新、删除、测试 +- CI/CD 管理:构建列表、日志、重启、停止 +- 组织与团队管理:组织列表、详情、成员、团队 CRUD +- 合规检查:许可证扫描、敏感信息检测、依赖审计 +- 搜索:仓库搜索、用户搜索 +- AI 工作流:社区运营、代码审查、项目初始化、多仓库协同、贡献者成长 + +性能要求: +- 单次 API 调用响应时间 < 2 秒 +- 批量操作支持并发执行,错误不中断 +- 分页自动遍历,支持大数据量场景 +- 跨平台支持:Windows、macOS、Linux + + +2、软件总体设计 + +2.1 软件体系结构设计 + +本项目采用分层架构,从上到共分为五层: + +(1)入口层:main.go 和 cmd/root.go,基于 Cobra 框架构建命令树。 +(2)命令层:cmd/ 目录,包含 auth、api、config、compliance 四个顶级命令。 +(3)快捷命令层:shortcuts/ 目录,16 个资源组,每组通过 +动词 子命令暴露操作。 +(4)核心库层:internal/ 目录,包含 HTTP 客户端、认证、配置、输出格式化、错误处理。 +(5)AI 扩展层:skills/ 目录(24 个 SKILL.md)和 workflows/ 目录(bash/PowerShell 脚本)。 + +各层的职责划分: + +[入口层] main.go → cmd.Execute() → Cobra root 命令 + ↓ +[命令层] auth / api / config / compliance + ↓ +[快捷命令层] shortcuts/ → repo/issue/pr/wiki/... → common.Shortcut 声明式定义 + ↓ +[核心库层] internal/client → HTTP 请求 → internal/auth → Token 注入 + internal/output → 格式化输出(JSON/Table/YAML) + internal/config → 配置文件读写 + internal/context → git remote 解析 owner/repo + ↓ +[AI 扩展层] skills/ → SKILL.md 定义(供 Claude Code 调用) + workflows/ → 可执行脚本(bash/PowerShell) + +核心设计模式是声明式 Shortcut 框架。每个快捷命令通过 common.Shortcut 结构体定义 Name、Flags、Run 函数,由 runner.go 统一挂载到 Cobra 命令树。开发者新增命令只需实现 Run 函数,不需要关心命令注册和参数解析的细节。 + +三层命令体系: +- Layer 1 Shortcuts:语义化封装,覆盖 16 个领域共 80+ 命令(如 issue +create) +- Layer 2 API Commands:原始 HTTP 调用(api GET/POST/PUT/DELETE) +- Layer 3 Raw API:覆盖全部 490+ 端点,自动注入认证 Header + + +2.2 用户界面设计 + +本项目是 CLI 工具,用户界面是终端命令行。界面设计遵循以下原则: + +(1)命令格式统一:gitlink-cli <资源> +<动作> [flags] + 例如:gitlink-cli issue +create --title "Bug" --body "描述" + +(2)输出格式三选一:通过 --format 参数选择 json、table、yaml,默认 table。 + table 格式使用 tabwriter 对齐,支持 ANSI 颜色高亮表头。 + json 格式使用统一的 Envelope 结构:{ok, data, error, meta}。 + +(3)全局参数:--owner、--repo(自动从 git remote 解析)、--format、--debug、--no-truncate、--columns、--no-color。 + +(4)Shell 补全:支持 bash、zsh、fish、powershell 四种 shell 的自动补全。 + +(5)帮助系统:每个命令支持 --help,显示用法、参数说明和示例。 + +用户操作流程: +用户打开终端 → 输入 gitlink-cli auth login 登录 → 使用具体命令操作资源 +→ 输出结果以 table/json/yaml 格式显示 → 可通过管道传递给其他工具 + + +2.3 数据库设计 + +本项目不使用传统数据库。数据存储分为两部分: + +(1)配置文件存储: +路径:~/.config/gitlink-cli/config.yaml +内容:base_url(API 地址)、default_format(输出格式)、editor、pager +格式:YAML + +(2)凭证存储: +使用 go-keyring 库,调用操作系统原生密钥管理: +- macOS:Keychain +- Linux:Secret Service(GNOME Keyring / KDE Wallet) +- Windows:Credential Manager +Fallback:~/.config/gitlink-cli/credentials(文件权限 0600) + +存储的数据结构: +Token(字符串)→ 关联 gitlink.org.cn 的 Bearer Token +Config(YAML)→ base_url、default_format、editor、pager 等配置项 + +本项目不需要关系型数据库,所有数据来自 GitLink 平台 API 的实时查询。 + + +2.4 系统度量 + +(1)代码规模: +- Go 源文件:60 个 +- Go 代码总行数:14,932 行 +- 测试文件:12 个,共 4,279 行 +- 测试覆盖率:测试代码占总代码的 22.3% + +(2)按模块统计: + +模块 文件数 代码行数 +main(入口) 1 13 +cmd(命令层) 5 379 +internal(核心库) 13 2,242 +shortcuts(快捷层) 41 12,398 +合计 60 14,932 + +(3)功能规模: +- Shortcut 命令组:16 个 +- Shortcut 命令总数:80+ +- AI Skill 定义:24 个 SKILL.md +- 自动化工作流脚本:15+ 个(bash/PowerShell) +- 支持的 Shell 补全:4 种(bash/zsh/fish/powershell) + +(4)依赖规模: +- 直接依赖:4 个(cobra、go-keyring、golang.org/x/term、yaml.v3) +- 编译产物大小:约 11.9 MB(单二进制文件) + + +3、系统静态模型 + +3.1 命令框架层设计 + +命令框架层由 cmd/ 包和 shortcuts/common/ 包组成,定义了整个 CLI 的骨架。 + +核心类结构: + +(1)cobra.Command(来自 spf13/cobra 库) +- 每个命令对应一个 cobra.Command 实例 +- rootCmd 是根命令,所有子命令通过 AddCommand 挂载 +- root.go 定义全局 PersistentFlags(--owner、--repo、--format 等) + +(2)common.Shortcut 结构体: +type Shortcut struct { + Name string + Description string + Long string + Example string + Flags []Flag + DryRun bool + DryRunHint func(ctx *RuntimeContext) (string, error) + Validate func(args map[string]string) error + Run func(ctx *RuntimeContext) error +} + +(3)common.Flag 结构体: +type Flag struct { + Name string + Short string + Usage string + Required bool + Default string + Bool bool + Choices []string + Validate func(value string) error +} + +(4)common.RuntimeContext 结构体: +type RuntimeContext struct { + Client *client.Client + Owner string + Repo string + Format string + CommandName string + Args map[string]string + GatewayBaseURL string + GatewayHTTPClient *http.Client + NoTruncate bool + Columns string + NoColor bool +} + +RuntimeContext 提供以下核心方法: +- ResolveOwnerRepo():从 git remote 或 flags 解析 owner/repo +- CallAPI(method, path, body):发起 HTTP 请求 +- PaginateAll(path, params):自动遍历分页 +- Output(env):格式化输出 +- OutputData(data):封装为成功 Envelope 并输出 + +(5)register.go 的注册机制: +RegisterAll(root) 遍历 16 个命令组,每组创建 cobra.Command 并调用 MountShortcuts 挂载子命令。MountShortcuts 遍历 Shortcut 切片,为每个 Shortcut 创建对应的 cobra.Command,绑定 flags 和 RunE 函数。 + + +3.2 快捷命令层设计 + +快捷命令层位于 shortcuts/ 目录,包含 16 个命令组。每个组是一个独立的 Go 包,通过 Shortcuts() 函数返回 []*common.Shortcut 切片。 + +命令组列表: +- board:看板操作(view/columns/issues/move/assign/stats)— 832 行 +- branch:分支操作(list/create/delete/protect)— 157 行 +- ci:CI/CD 操作(builds/logs/restart/stop)— 117 行 +- file:文件操作(ls/read/create/update/delete/commits/diff)— 886 行 +- issue:Issue 操作(14 个命令,含 6 种批量操作)— 3,058 行 +- milestone:里程碑操作(list/create/view/update/delete/status)— 525 行 +- org:组织操作(list/info/members/create/update)— 476 行 +- pr:PR 操作(list/create/view/merge/close/review/files/diff)— 973 行 +- release:发布操作(list/create/view/delete)— 179 行 +- repo:仓库操作(含 batch-create/batch-update/batch-delete/batch-member)— 1,781 行 +- search:搜索操作(repos/users)— 88 行 +- team:团队操作(list/create/delete/members)— 192 行 +- user:用户操作(me/info)— 41 行 +- webhook:Webhook 操作(list/create/view/update/delete/test/events)— 649 行 +- wiki:Wiki 操作(list/create/view/update/delete/lint/fix/sync)— 1,612 行 + +最大的两个模块是 issue(3,058 行)和 wiki(1,612 行)。issue 模块包含批量操作功能,wiki 模块处理双域名架构和 base64 编解码。 + +批量操作设计模式(以 issue 为例): +- BatchResult:单条操作结果(number/action/status/error) +- BatchSummary:汇总信息(total/succeeded/failed/results) +- 输入灵活:--numbers(内联)和 --from(CSV 文件)可同时使用 +- dry-run 统一:所有批量命令支持 --dry-run 预览 +- 错误不中断:单条失败不影响后续处理 + + +3.3 核心库层设计 + +核心库层位于 internal/ 目录,包含 7 个子包: + +(1)internal/auth — 认证模块(3 个文件,272 行) +- login.go:登录流程(用户名密码或 Token 粘贴) +- token_store.go:Token 存储(go-keyring 跨平台支持) +- transport.go:HTTP Transport,自动在请求 Header 中注入 Bearer Token + +(2)internal/client — HTTP 客户端(2 个文件,319 行) +- client.go:封装 Get/Post/Put/Delete 方法,自动追加 .json 后缀,双重错误检查(HTTP 状态码 + JSON body 中的 status 字段) +- pagination.go:分页迭代器,自动遍历 Kaminari 风格分页 + +(3)internal/compliance — 合规检查(4 个文件,685 行) +- cmd.go:compliance 子命令定义 +- scanner.go:扫描器实现 +- license.go:许可证检测 +- rules.go:合规规则定义 + +(4)internal/config — 配置管理(1 个文件,125 行) +- config.go:读写 ~/.config/gitlink-cli/config.yaml + +(5)internal/context — 上下文解析(1 个文件,91 行) +- repo.go:从 git remote 的 origin URL 自动解析 owner 和 repo + +(6)internal/errors — 错误处理(1 个文件,150 行) +- errors.go:统一错误类型 CLIError,包含 Kind(错误分类)、Message、Suggestion、Command 字段 +- 错误分类:KindAuth、KindInput、KindForbidden、KindNotFound、KindServer、KindUnknown + +(7)internal/output — 输出格式化(3 个文件,425 行) +- envelope.go:定义 Envelope 结构体 {OK, Data, Error, Meta} +- formatter.go:三种输出格式(JSON/YAML/Table),table 使用 tabwriter 对齐,支持 ANSI 颜色、列过滤、值截断控制 + + +4、系统动态模型 + +4.1 用例一:用户认证与仓库操作 + +用例描述:用户首次使用 gitlink-cli,完成登录并查看仓库信息。 + +参与者:开发者(终端用户) + +前置条件:已安装 gitlink-cli,网络可访问 gitlink.org.cn + +主要流程: + +(1)用户执行 gitlink-cli auth login +(2)系统提示输入用户名和密码 +(3)系统调用 POST /api/accounts/login 获取 Token +(4)Token 存入 OS Keychain +(5)用户进入 git 仓库目录,执行 gitlink-cli repo +info +(6)系统从 .git/config 解析 remote origin URL,提取 owner/repo +(7)系统调用 GET /api/{owner}/{repo}/detail.json +(8)系统以 table 格式输出仓库信息 + +异常流程: +- Token 过期(7 天有效期):系统返回 401 错误,提示运行 gitlink-cli auth login 重新登录 +- 非 git 目录:系统提示使用 --owner 和 --repo 参数显式指定 + +API 调用序列: +POST /api/accounts/login → {token: "..."} +GET /api/{owner}/{repo}/detail.json → {name, description, default_branch, ...} + + +4.2 用例二:Issue 批量管理 + +用例描述:项目维护者批量关闭过期 Issue 并批量创建新 Issue。 + +参与者:项目维护者 + +前置条件:已登录,有仓库写权限 + +主要流程: + +(1)用户执行 gitlink-cli issue +batch-close --numbers 101,102,103,104 --dry-run +(2)系统预览:显示 4 个 Issue 将被关闭,不实际执行 +(3)用户确认后去掉 --dry-run,重新执行 +(4)系统逐个调用 PUT /api/{owner}/{repo}/issues/{number},设置 state 为 closed +(5)系统输出 BatchSummary:total=4, succeeded=4, failed=0 + +(6)用户准备 CSV 文件 issues.csv: +title,body,priority,label +"登录页面样式异常","点击按钮后样式错乱","high","缺陷" +"新增数据导出功能","支持 CSV 和 JSON 格式导出","normal","功能" + +(7)用户执行 gitlink-cli issue +batch-create --from issues.csv --template bug +(8)系统逐个调用 POST /api/{owner}/{repo}/issues 创建 Issue +(9)系统输出 BatchSummary:total=2, succeeded=2, failed=0 + +异常流程: +- 单条操作失败:记录错误,继续处理后续条目,最终 exit code = 1 +- API 字段静默失败:GitLink 对不认识的字段返回 200 而非报错,需通过浏览器 DevTools 确认字段名 + + +4.3 用例三:AI 自动化工作流 + +用例描述:使用 Claude Code 和 gitlink-cli Skill 自动完成社区运营任务。 + +参与者:社区运营者(通过 Claude Code 交互) + +前置条件:Claude Code 已安装,gitlink-cli 已登录 + +主要流程: + +(1)用户在 Claude Code 中说"帮我跑社区运营工作流" +(2)Claude Code 触发 gitlink-workflows skill,显示功能菜单 +(3)用户选择"社区运营自动化" +(4)Claude Code 调用 gitlink-cli issue +list --state open --format json +(5)Claude Code 分析 Issue 列表,按类型分类(bug/feature/question) +(6)Claude Code 调用 gitlink-cli issue +label-add 添加分类标签 +(7)Claude Code 调用 gitlink-cli pr +list 获取近期 PR +(8)Claude Code 生成周报摘要,包含 Issue 统计、PR 合并情况、贡献者活跃度 + +Skill 调用链: +gitlink-shared(认证检查)→ gitlink-issue(Issue 操作)→ gitlink-pr(PR 操作)→ gitlink-workflow(工作流编排) + + +5、系统部署模型 + +本项目采用单二进制分发模式,部署简单。 + +(1)构建环境: +- Go 1.26.1+ +- Makefile 管理构建流程 +- 构建命令:make build(通过 -ldflags 注入版本号) + +(2)分发方式: +- 直接下载:从 GitHub Releases 下载预编译二进制 +- npm 安装:npm install -g gitlink-cli +- 源码编译:go install github.com/gitlink-org/gitlink-cli@latest +- 安装脚本:install.sh(Linux/macOS)、install.ps1(Windows) + +(3)目标平台: +- Windows(amd64) +- macOS(amd64、arm64) +- Linux(amd64、arm64) + +(4)依赖服务: +- GitLink API:https://www.gitlink.org.cn/api(主站 API) +- GitLink Gateway:https://gateway.gitlink.org.cn/api(Wiki API 专用) +- OS Keychain:系统密钥管理服务(存储 Token) + +(5)CI/CD 集成: +- GitHub Actions:.github/workflows/release.yml 自动构建和发布 +- GitLink DevOps:.devops/gitlink-cli-autodeploy.yml 平台自动部署 + +(6)部署架构: + +用户终端 + ↓ +gitlink-cli(本地二进制) + ↓ HTTP/HTTPS +GitLink API Server(gitlink.org.cn) + ↓ +GitLink Gateway(gateway.gitlink.org.cn,Wiki 专用) + +gitlink-cli 是纯客户端工具,不运行后台服务。所有数据存储在用户本地(配置文件 + OS Keychain),业务数据完全来自 GitLink 平台 API 的实时查询。 diff --git a/gitlink-cli.exe b/gitlink-cli.exe new file mode 100644 index 00000000..17b3d6cc Binary files /dev/null and b/gitlink-cli.exe differ diff --git a/npm/bin/gitlink-cli.exe b/npm/bin/gitlink-cli.exe new file mode 100644 index 00000000..17b3d6cc Binary files /dev/null and b/npm/bin/gitlink-cli.exe differ diff --git a/shortcuts/.claude/settings.local.json b/shortcuts/.claude/settings.local.json new file mode 100644 index 00000000..d0236e0d --- /dev/null +++ b/shortcuts/.claude/settings.local.json @@ -0,0 +1,21 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:apifox.com)", + "WebSearch", + "WebFetch(domain:gitlink.org.cn)", + "WebFetch(domain:www.gitlink.org.cn)", + "Bash(sed 's/^//')", + "Bash(sed 's/ *$//')", + "Bash(grep -h \"CallAPI\\\\|\\\\.Do\\(\" C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/shortcuts/*/*.go C:/Users/Lenovo/Desktop/soft运维/gitlink-cli/shortcuts/*/*/*.go)", + "Bash(go build:*)", + "Bash(./gitlink-cli.exe file:*)", + "Bash(./gitlink-cli.exe issue:*)", + "Bash(./gitlink-cli.exe milestone:*)", + "Bash(./gitlink-cli.exe pr:*)", + "Bash(go vet:*)", + "Bash(go test:*)", + "Bash(cd:*)" + ] + } +} diff --git a/shortcuts/gitlink-cli.exe b/shortcuts/gitlink-cli.exe new file mode 100644 index 00000000..f6f787ce Binary files /dev/null and b/shortcuts/gitlink-cli.exe differ diff --git a/shortcuts/issue/batch.go b/shortcuts/issue/batch.go index fe346ea3..ce809231 100644 --- a/shortcuts/issue/batch.go +++ b/shortcuts/issue/batch.go @@ -482,7 +482,12 @@ func runBatchLabel(ctx *common.RuntimeContext) error { } // 获取标签参数并转换为数字 ID label := ctx.Arg("label") - trackerID, err := parseTracker(label) + + tags, err := resolveIssueTags(ctx) + if err != nil { + return fmt.Errorf("cannot resolve issue tags: %w", err) + } + tagID, err := parseLabel(label, tags) if err != nil { return err } @@ -514,7 +519,7 @@ func runBatchLabel(ctx *common.RuntimeContext) error { continue } // 调用 updateIssueField 修改标签(issue_tag_ids 是数组) - if err := updateIssueField(ctx, number, map[string]interface{}{"issue_tag_ids": []int{trackerID}}); err != nil { + if err := updateIssueField(ctx, number, map[string]interface{}{"issue_tag_ids": []int{tagID}}); err != nil { result.Status = "failed" result.Error = err.Error() summary.Failed++ diff --git a/shortcuts/issue/batch_create.go b/shortcuts/issue/batch_create.go index 0c59c1d2..15534299 100644 --- a/shortcuts/issue/batch_create.go +++ b/shortcuts/issue/batch_create.go @@ -12,20 +12,21 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) -var issueTagCache sync.Map +var issueTagCache sync.Map //全局缓存变量 +// 获取项目标签映射 // resolveIssueTags fetches the project's issue tags and returns a name→id mapping. // Results are cached per owner/repo. func resolveIssueTags(ctx *common.RuntimeContext) (map[string]int, error) { key := ctx.Owner + "/" + ctx.Repo - if cached, ok := issueTagCache.Load(key); ok { - return cached.(map[string]int), nil + if cached, ok := issueTagCache.Load(key); ok { //先从缓存查 + return cached.(map[string]int), nil // 命中缓存则直接返回 } - path := fmt.Sprintf("/v1/%s/%s/issue_tags", ctx.Owner, ctx.Repo) + path := fmt.Sprintf("/v1/%s/%s/issue_tags", ctx.Owner, ctx.Repo) //api路径 q := url.Values{} q.Set("only_name", "true") - env, err := ctx.CallAPIWithQuery("GET", path, q) + env, err := ctx.CallAPIWithQuery("GET", path, q) // 发送GET请求获取项目标签列表 if err != nil { return nil, fmt.Errorf("获取项目标签列表失败: %w", err) } @@ -157,7 +158,7 @@ func runBatchCreate(ctx *common.RuntimeContext) error { for i, input := range inputs { label := fmt.Sprintf("#%d", i+1) if input.Title != "" { - label = truncate(input.Title, 40) + label = truncate(input.Title, 40) // 用标题的前40个字符 } result := BatchResult{Number: label, Action: "create"} @@ -168,6 +169,7 @@ func runBatchCreate(ctx *common.RuntimeContext) error { continue } + //把 createIssueInput 转换成 API 需要的 JSON map body := buildCreateBody(ctx, input, template, tags) env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/issues", body) if err != nil { diff --git a/shortcuts/issue/issue.go b/shortcuts/issue/issue.go index 65698e01..2bab1cb3 100644 --- a/shortcuts/issue/issue.go +++ b/shortcuts/issue/issue.go @@ -1,25 +1,29 @@ package issue import ( - "fmt" + "fmt" //格式化字符串 "net/url" - "strconv" + "strconv" //字符串和数字转换 "strings" clierrors "github.com/gitlink-org/gitlink-cli/internal/errors" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) +// Ctx // v1RepoPath returns the v1 API path prefix: /v1/{owner}/{repo} func v1RepoPath(ctx *common.RuntimeContext) string { return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo) } +//内部数据结构,标题+描述,保存从 API 取回来的 Issue 原始数据 type existingIssue struct { Subject string Description string } +// 所有issue命令的注册入口,返回所有issue命令 +// 在终端敲入issue +list命令时,遍历匹配到列表里的{Name: "list", ...} func Shortcuts() []*common.Shortcut { return []*common.Shortcut{ newBatchCloseShortcut(), @@ -42,21 +46,23 @@ func Shortcuts() []*common.Shortcut { {Name: "page", Short: "p", Usage: "Page number", Default: "1"}, {Name: "limit", Short: "l", Usage: "Items per page", Default: "20"}, }, - Run: func(ctx *common.RuntimeContext) error { + //后面拼接 API 路径时,需要用到 ctx.Owner 和 ctx.Repo + // 如果不知道 owner 和 repo ,后续的 API 调用就不知道该往哪发请求,所以必须作为前置校验放在最前面。 + Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - q := url.Values{} - q.Set("page", ctx.Arg("page")) - q.Set("limit", ctx.Arg("limit")) - if s := ctx.Arg("state"); s != "" { + q := url.Values{} // 创建空的URL查询参数容器q,后续通过set()添加键值对,最终拼接成形如 ?page=1&limit=20&state=open 的查询字符串 + q.Set("page", ctx.Arg("page")) + q.Set("limit", ctx.Arg("limit")) //从命令行参数中读取 page(页码)和 limit(每页条数 + if s := ctx.Arg("state"); s != "" { // 如果用户传入了state参数 q.Set("state", s) } env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issues", q) if err != nil { return clierrors.OpError(clierrors.KindServer, "list", "issues", err).WithCommand(ctx.CommandName) } - return ctx.Output(env) + return ctx.Output(env) //按照用户指定的格式(json/html/table...)输出issue列表 }, }, { diff --git a/shortcuts/milestone/milestone.go b/shortcuts/milestone/milestone.go index 4e4d9e25..d6435375 100644 --- a/shortcuts/milestone/milestone.go +++ b/shortcuts/milestone/milestone.go @@ -57,29 +57,22 @@ func Shortcuts() []*common.Shortcut { }, Flags: []common.Flag{ {Name: "name", Short: "n", Usage: "Milestone name", Required: true}, - {Name: "description", Short: "d", Usage: "Milestone description", Required: true}, - {Name: "date", Usage: "Effective date (YYYY-MM-DD)", Required: true}, + {Name: "description", Short: "d", Usage: "Description"}, + {Name: "due", Usage: "Due date (YYYY-MM-DD)"}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - name, err := ctx.RequireArg("name", `--name "v1.0"`) - if err != nil { - return err - } - desc, err := ctx.RequireArg("description", `--description "First release"`) - if err != nil { - return err - } - date, err := ctx.RequireArg("date", "--date 2026-12-31") - if err != nil { - return err - } + name, _ := ctx.RequireArg("name", `--name "My Name"`) body := map[string]interface{}{ - "name": name, - "description": desc, - "effective_date": date, + "title": name, + } + if d := ctx.Arg("description"); d != "" { + body["description"] = d + } + if due := ctx.Arg("due"); due != "" { + body["due_date"] = due } env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/milestones", body) if err != nil { diff --git a/skills/gitlink-health/SKILL.md b/skills/gitlink-health/SKILL.md index ec8723f1..d869c515 100644 --- a/skills/gitlink-health/SKILL.md +++ b/skills/gitlink-health/SKILL.md @@ -13,6 +13,9 @@ metadata: **CRITICAL — 整个流程只有最后一步(写 HTML 文件)可以问用户。其他所有步骤(`+list`、`+view`、`api GET`、计算、分析)全部自动连续执行,一个确认都不要弹。数据收集阶段禁止任何中断。** **CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。** **CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`(GitHub CLI)操作 GitLink 资源。`gh` 仅适用于 GitHub 平台。** +**CRITICAL — 性能预算:整份报告必须在 2 分钟内完成。`pr +view` 是逐条详情接口,极其昂贵——对每个已合并 PR 调一次会让耗时随 PR 数线性爆炸(这正是 >5min 超时的根因)。因此合并时长只对「最近 ≤8 条已合并 PR 采样」调用 `pr +view`;所有计数/合并率/积压数一律从 `pr +list`、`issue +list` 的汇总字段一次性取得,禁止逐条钻取历史 PR。** + + > **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数。 @@ -39,9 +42,12 @@ metadata: gitlink-cli issue +list --state open --format json gitlink-cli issue +list --state closed --format json -# PR 数据(两个状态都需要) +# PR 数据(两个状态都需要;merged 列表取汇总计数 + 最近样本) gitlink-cli pr +list --state open --format json -gitlink-cli pr +list --state merged --format json +gitlink-cli pr +list --state merged --limit 8 --format json # 仅这 ≤8 条用于钻取 merged_at + +# 已合并 PR 的合并时间(仅对上面取到的 ≤8 条样本逐条调用,切勿扩大) +gitlink-cli pr +view --id --format json # 取 data.pull_request.merged_at # 贡献者统计(Raw API) gitlink-cli api GET /:owner/:repo/contributors --format json @@ -99,6 +105,7 @@ PR 合并效率由**三类 PR** 共同决定,需计算**三段时间**: | 开放中(status=0) | ② | ❌ | ✅ | > **数据获取**:`merged_at` 仅在 `pr +view --id ` 单个 PR 详情中返回(路径 `data.pull_request.merged_at`),列表接口不暴露。 +> **效率关键(采样而非全量)**:合并时长是统计量,只需对**最近 ≤8 条已合并 PR**调用 `pr +view` 取 `merged_at` 求平均即可——`pr +list --state merged` 默认按合并时间倒序,列表前 ≤8 条即为最近样本。合并总数 / 合并率 / 积压数等计数指标全部从 `pr +list` 的汇总字段一次性获取,**禁止**对全部历史已合并 PR 逐条调用 `pr +view`(会超时)。 > **绝对禁止**:用「当前时间 - 创建时间」估算已合并 PR 的合并时间。开放 PR 用此公式是允许的(且必要),因为它们没有合并时间点,等待时长反映积压。 ### 3. 贡献者活跃度 diff --git a/skills/gitlink-health/examples/full-workflow.md b/skills/gitlink-health/examples/full-workflow.md index d36ce40a..fce839ff 100644 --- a/skills/gitlink-health/examples/full-workflow.md +++ b/skills/gitlink-health/examples/full-workflow.md @@ -24,19 +24,20 @@ gitlink-cli issue +list --state closed --format json ### 第二步:收集 PR 数据 ```bash -# 获取未合并 PR +# 获取未合并 PR(计数 + 积压) gitlink-cli pr +list --state open --format json -# 获取已合并 PR -gitlink-cli pr +list --state merged --format json +# 获取已合并 PR 汇总 + 最近样本(--limit 8 限定钻取范围,避免超时) +gitlink-cli pr +list --state merged --limit 8 --format json + +# 仅对上面取到的 ≤8 条样本逐条取 merged_at(切勿对全部历史 PR 调用) +gitlink-cli pr +view --id --format json +``` # AI 从返回中提取: -# - merged count: 10 -# - open count: 0 -# - 合并率: 10/11 = 91% -# - 最新合并 PR: #11 (2026-06-04) -# - PR 平均合并时间需逐条计算 -``` +# - merged count / open count:从 pr +list 汇总字段 +# - 合并率: merged / (merged + closed) +# - 已合并 PR 平均合并时长:仅基于这 ≤8 条样本的 merged_at - created_at 求平均 ### 第三步:收集贡献者统计 @@ -90,7 +91,7 @@ AI 读取 `skills/gitlink-health/template.html`,将计算后的指标替换所 ## AI Agent 执行要点 1. **数据收集顺序**:先收集 Issue 和 PR(Shortcut 命令),再收集 contributors(Raw API),避免一次性大量 API 调用 -2. **分页处理**:Issue 和 PR 数量超过单页限制时,用 `--page` 逐页获取 +2. **效率优先于全量**:报告是趋势快照,不是审计。计数/合并率从 `+list` 汇总字段一次取得;合并时长只采样最近 ≤8 条已合并 PR(`pr +view`)。**禁止**分页遍历全部历史 Issue/PR,也禁止对每个已合并 PR 都调 `pr +view`——这是 >5min 超时的根因 3. **时间计算**:ISO 8601 格式解析优先,Unix 时间戳更可靠但 PR 的 `pr_created_unix` 仅部分返回 4. **指标计算容错**:样本不足时标注而非报错,新项目可能仅有少量数据 5. **评分可按需调整**:新项目无 Release 时,"无近期发布"项自动跳过 diff --git a/skills/gitlink-health/references/collect-data.md b/skills/gitlink-health/references/collect-data.md index 565c7c9f..a2c1148c 100644 --- a/skills/gitlink-health/references/collect-data.md +++ b/skills/gitlink-health/references/collect-data.md @@ -43,14 +43,16 @@ PR 返回的关键字段: | 字段 | 用途 | |------|------| -| `pull_request_number` | PR 编号 | +| `pull_request_number` | PR 编号(用于 `pr +view --id` 钻取) | | `name` (title) | PR 标题 | | `pull_request_status` | 状态:0=open, 1=merged, 2=closed | | `author_login` | 作者 | | `pr_full_time` | 创建时间 (ISO 8601) | -| `pr_merged_at` | 合并时间 | | `pr_created_unix` | 创建时间 (Unix timestamp) | +| `merged_issues_size` / `open_count` / `close_count` | 合并/开放/关闭未合并计数(汇总字段,与 `--limit` 无关,一次性取得) | +> ⚠️ `pr +list` **不返回** `merged_at`(合并时间)字段,只有创建时间。`merged_at` 仅在 `pr +view --id ` 的 `data.pull_request.merged_at` 中暴露。 +> ⚠️ **为控制耗时,只对最近 ≤8 条已合并 PR 调用 `pr +view` 取 `merged_at`**(列表默认按合并时间倒序,前 ≤8 条即最近样本)。`pr +list --state merged --limit 8` 一条命令即可同时拿到汇总计数(`merged_issues_size` 等不受 limit 影响)和 8 条样本。禁止遍历全部历史已合并 PR。 > ⚠️ `--state` 参数仅影响 `merged_count`/`open_count`/`closed_count` 汇总计数,API 返回的 issues 列表可能包含所有状态的 PR。需按 `pull_request_status` 客户端过滤。 ### 步骤 3:收集贡献者统计 @@ -100,17 +102,17 @@ gitlink-cli api GET /:owner/:repo/activity --format json 数据收集覆盖以下时间范围: -- **Issue**: 所有未关闭 + 近期已关闭(默认取最近 100 条) -- **PR**: 所有未合并 + 近期已合并(默认取最近 100 条) +- **Issue**: 所有未关闭 + 近期已关闭(计数从汇总字段取;时间计算用近期限定条数即可) +- **PR**: 所有未合并 + 近期已合并(计数/合并率从 `pr +list` 汇总字段取;**合并时长仅采样最近 ≤8 条已合并 PR** 钻取 `merged_at`) - **Contributors**: 项目全量历史数据 - **Activity**: 最近 30 天 -对于大型项目,可通过 `--page` 分页获取更多数据。 +大型项目也**不要**通过 `--page` 分页遍历全部历史数据——健康度是趋势快照,计数取汇总字段、合并时长取最近 ≤8 条样本即可,避免超时。 ## 注意事项 - 计算响应时间需要 Issue 有 `closed_at` 字段,仅已关闭 Issue 才会返回此字段 -- PR 合并时间需通过 `pr_full_time` 与当前时间对比估算,或结合 PM `/weekly_issues` API +- PR 合并时间**不能**用「当前时间 - `pr_full_time`」估算(已合并 PR 必须取真实 `merged_at`);`merged_at` 通过 `pr +view --id` 获取,**仅采样最近 ≤8 条已合并 PR** - `contributors` 端点统计基于默认分支(通常为 `master`),不含其他分支 - `--owner` / `--repo` 在仓库目录下可自动解析 - 时间计算使用 Unix 时间戳(`pr_created_unix`)比解析字符串格式(`pr_full_time`)更可靠 diff --git a/skills/gitlink-health/references/health-metrics.md b/skills/gitlink-health/references/health-metrics.md index fe57360f..08820120 100644 --- a/skills/gitlink-health/references/health-metrics.md +++ b/skills/gitlink-health/references/health-metrics.md @@ -73,9 +73,9 @@ PR 合并效率由**三类 PR** 共同决定,需计算**三段时间**: ### 计算公式 ``` -# ① 已合并 PR 平均合并时长 +# ① 已合并 PR 平均合并时长(基于最近 ≤8 条样本) time_merged_i = merged_at_i - created_at_i -avg_merged = sum(time_merged_i) / merged_count +avg_merged = sum(time_merged_i) / sample_count # sample_count = 实际采样的 PR 条数(≤8),不是历史合并总数 # ② 开放 PR 平均等待时长 time_open_i = now - created_at_i @@ -123,28 +123,16 @@ merge_rate = merged_count / (merged_count + closed_count) × 100% ### 数据可用性 -| 字段 | 来源命令 | 路径 | -|------|---------|------| -| `created_at` | `pr +list` | `data.issues[].pr_full_time` 或 `pr_created_unix` | -| `merged_at` | `pr +view --id ` | `data.pull_request.merged_at` | - -> **绝对禁止**:用「当前时间 - 创建时间」估算已合并 PR 的合并时间。已合并 PR 必须用真实的 `merged_at`。 -> 开放 PR 用「当前时间 - 创建时间」是允许的(且必要),因为它们没有合并时间点,等待时长反映积压。 - -### 数据可用性 - PR 合并时间**只与 PR 自身时间相关**(创建时间 + 合并时间),**与当前时间无关**。 -实际数据源: - | 字段 | 来源命令 | 路径 | |------|---------|------| | `created_at` | `pr +list --state merged` | `data.issues[].pr_full_time` 或 `pr_created_unix` | -| `merged_at` | `pr +view --id ` | `data.pull_request.merged_at` | +| 合并总数 / 合并率 / 积压数 | `pr +list` 汇总字段 | `data.close_count` 等(一次性获取,勿逐条钻取) | +| `merged_at` | `pr +view --id ` | `data.pull_request.merged_at` | -**注意**:`merged_at` 只在 `pr +view` 单个 PR 详情中暴露,列表接口不返回。需要对每个已合并 PR 调用一次详情接口。 - -> **绝对禁止**:使用「当前时间 - 创建时间」作为合并时间估算。这种做法会把开放中或近期合并的 PR 误判为"超长合并时间",与 PR 合并效率的真实含义相悖。 +> **效率关键(采样而非全量)**:`merged_at` 只在 `pr +view` 单个 PR 详情中暴露,列表接口不返回。但合并时长是统计量——**只对最近 ≤8 条已合并 PR 采样调用 `pr +view`** 即可求得稳定平均,无需遍历全部历史 PR。`pr +list --state merged` 默认按合并时间倒序,取列表前 ≤8 条作为样本。合并总数/合并率从 `pr +list` 汇总字段一次性取得。 +> **绝对禁止**:使用「当前时间 - 创建时间」作为已合并 PR 的合并时间估算(会把开放中或近期合并的 PR 误判为"超长合并时间")。开放 PR 用「当前时间 - 创建时间」是允许且必要的,等待时长反映积压。 ## 指标 3:贡献者活跃度 diff --git a/webhook-test.txt b/webhook-test.txt deleted file mode 100644 index dadb388a..00000000 Binary files a/webhook-test.txt and /dev/null differ diff --git a/workflows/.claude/settings.local.json b/workflows/.claude/settings.local.json new file mode 100644 index 00000000..f7d082d1 --- /dev/null +++ b/workflows/.claude/settings.local.json @@ -0,0 +1,84 @@ +{ + "permissions": { + "allow": [ + "Bash(git push *)", + "Bash(gitlink-cli version:*)", + "Bash(winget install:*)", + "Bash(scoop install:*)", + "Bash(export PATH=\"$PATH:/c/Users/Lenovo/AppData/Local/Microsoft/WinGet/Links\")", + "Bash(jq --version)", + "Bash(bash test.sh zzx-coder gitlink-cli)", + "Bash(gitlink-cli auth:*)", + "Bash(bash -x test.sh zzx-coder gitlink-cli)", + "Bash(source lib/common.sh)", + "Bash(check_auth)", + "Bash(bash -n test.sh)", + "Bash(gitlink-cli issue:*)", + "Bash(gitlink-cli pr:*)", + "Bash(where gitlink-cli:*)", + "Bash(export PATH=\"/c/Users/Lenovo/AppData/Roaming/npm:/c/Users/Lenovo/AppData/Local/Microsoft/WinGet/Links:$PATH\")", + "Bash(echo \"Exit code: $?\")", + "Bash(bash 01-community-ops.sh --owner zzx-coder --repo gitlink-cli --dry-run)", + "Bash(bash 03-project-init.sh --owner zzx-coder --name test-workflow --description \"测试工作流\" --lang go --dry-run)", + "Bash(gitlink-cli wiki:*)", + "Bash(gitlink-cli repo:*)", + "Bash(bash 04-multi-repo-collab.sh --org zzx-coder --dry-run)", + "Bash(bash 05-contributor-growth.sh --owner zzx-coder --repo gitlink-cli --dry-run)", + "Bash(bash 02-code-quality-gatekeeper.sh --owner zzx-coder --repo gitlink-cli --dry-run)", + "Bash(bash 04-multi-repo-collab.sh --org zzx-coder --repos \"gitlink-cli\" --dry-run)", + "Bash(echo \"Exit: $?\")", + "Bash(gitlink-cli contrib:*)", + "Bash(gitlink-cli api:*)", + "Bash(bash 05-contributor-growth.sh --owner zzx-coder --repo gitlink-cli --sample 5 --dry-run)", + "Bash(bash 05-contributor-growth.sh --owner zzx-coder --repo gitlink-cli --sample 3 --dry-run)", + "Bash(jq .data.issues[0])", + "Read(//c/Users/Lenovo/**)", + "Bash(find /d -name \"jq.exe\")", + "Bash(find /mingw64 -name \"jq*\")", + "Bash(\"/c/Users/Lenovo/AppData/Local/Microsoft/WinGet/Links/jq.exe\" --version 2>&1)", + "Bash(JQ=\"/c/Users/Lenovo/AppData/Local/Microsoft/WinGet/Links/jq.exe\" gitlink-cli pr +list --owner zzx-coder --repo gitlink-cli --state merged --limit 1 --format json)", + "Bash(\"$JQ\" '.data.issues[0] | {author_login, pull_request_id, id, project_issues_index, name}')", + "Bash(JQ=\"/c/Users/Lenovo/AppData/Local/Microsoft/WinGet/Links/jq.exe\" gitlink-cli pr +files --owner zzx-coder --repo gitlink-cli --id 15940 --format json)", + "Bash($JQ '{total_addition, total_deletion}')", + "Bash(\"$JQ\" '.')", + "Bash(\"$JQ\" '{total_addition, total_deletion, file_count: \\(.data.files | length\\), sum_add: [.data.files[].addition] | add, sum_del: [.data.files[].deletion] | add}')", + "Bash(bash 05-contributor-growth.sh --owner zzx-coder --repo gitlink-cli --sample 3)", + "Bash(source lib/common.sh OWNER=\"zzx-coder\" REPO=\"gitlink-cli\" WIKI_CONTENT=\"# Test Wiki Content:*)", + "Bash(source lib/common.sh OWNER=\"zzx-coder\" REPO=\"gitlink-cli\" WIKI_CONTENT=\"# Test with backticks:*)", + "Bash(echo 'jq failed, raw output:' gl_run wiki +create --owner $OWNER --repo $REPO --title 'Test Backticks' --content $WIKI_CONTENT)", + "Bash(JQ=\"/c/Users/Lenovo/AppData/Local/Microsoft/WinGet/Links/jq.exe\" gitlink-cli wiki +list --owner zzx-coder --repo gitlink-cli --format json)", + "Bash($JQ \".data.wikis[] | .title\")", + "Bash(bash test.sh 2>&1)", + "Bash(ls:*)", + "Bash(bash 03-project-init.sh --owner zzx-coder --name test-repo --description \"Test repo\" --dry-run)", + "Bash(where bash:*)", + "Bash(where git:*)", + "Bash(for cmd:*)", + "Bash(do echo:*)", + "Bash(./gitlink-cli.exe $cmd --help)", + "Bash(done)", + "Bash(./gitlink-cli.exe --help)", + "Bash(./gitlink-cli.exe issue:*)", + "Bash(powershell -ExecutionPolicy Bypass -File \"01-community-ops.ps1\" -Help)", + "Bash(powershell -ExecutionPolicy Bypass -Command \"& { Import-Module ./lib/common.psm1 -Force; Check-Auth }\")", + "Bash(powershell -Command \"& { $output = & gitlink-cli issue +list --owner zzx-coder --repo gitlink-cli --state open --limit 100 --format json; Write-Host ''''TYPE='''' $output.GetType\\(\\).Name; Write-Host ''''COUNT='''' $output.Count; Write-Host ''''FIRST='''' $output[0] }\")", + "Bash(powershell -ExecutionPolicy Bypass -File ./_debug.ps1)", + "Bash(cd:*)", + "Bash(powershell -ExecutionPolicy Bypass -Command \"& { $ErrorActionPreference=''''Continue''''; . ./01-community-ops.ps1 -DryRun }\")", + "Bash(powershell -ExecutionPolicy Bypass -Command \"& { $ErrorActionPreference=''''Continue''''; . ./03-project-init.ps1 -DryRun -Owner zzx-coder -Repo gitlink-cli }\")", + "Bash(powershell -ExecutionPolicy Bypass -Command \"& { $ErrorActionPreference=''''Continue''''; . ./04-multi-repo-collab.ps1 -DryRun }\")", + "Bash(powershell -ExecutionPolicy Bypass -Command \"& { $ErrorActionPreference=''''Continue''''; . ./05-contributor-growth.ps1 -DryRun }\")", + "Bash(gitlink-cli release:*)", + "Bash(powershell -ExecutionPolicy Bypass -Command \"& { $ErrorActionPreference=''''Continue''''; . ./04-multi-repo-collab.ps1 -DryRun -Org zzx-coder }\")", + "Bash(xxd)", + "Bash(powershell -Command \"\\\\$b = '{\\\\\\\\\\\\\"assigned_to_id\\\\\\\\\\\\\": \\\\\\\\\\\\\"zzx-coder\\\\\\\\\\\\\"}'; Write-Host \\\\$b; gitlink-cli api PATCH '/v1/zzx-coder/gitlink-cli/issues/145142' --body \\\\$b 2>&1 | Select-Object -First 5\")", + "Bash(ln:*)", + "Bash(grep -n \"func v1RepoPath\" shortcuts/issue/*.go)", + "Bash(go build:*)", + "Bash(python -c \":*)", + "Bash(./gitlink-cli.exe pr:*)", + "Bash(/dev/null done:*)", + "Bash(bash academic/06-research-insights.sh --owner zzx-coder --repo gitlink-cli)" + ] + } +} diff --git a/workflows/01-community-ops.ps1 b/workflows/01-community-ops.ps1 index 3e5c5c0a..f4988649 100644 --- a/workflows/01-community-ops.ps1 +++ b/workflows/01-community-ops.ps1 @@ -3,7 +3,6 @@ # Flow: 收集周期数据 → 10类关键词分类 → 生成 Release Notes + 社区周报 # 遵循 gitlink-changelog skill 的收集→分类→发布流程 # 条目格式: - 描述 (#编号) (@作者) [分类] -======= # ---------------------------------------------------------------- #Requires -Version 5.1 @@ -18,11 +17,10 @@ param( ) $ErrorActionPreference = "Stop" -Import-Module "$PSScriptRoot/lib/common.psm1" -Force +Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue if ($Help) { Write-Host "Usage: powershell 01-community-ops.ps1 [-Owner O] [-Repo R] [-PeriodHours N] [-ReleaseVersion TAG] [-DryRun]" -======= exit 0 } @@ -34,7 +32,6 @@ $Owner = $r.Owner; $Repo = $r.Repo # Phase 1: 时间窗口和基线 # ================================================================ Log-Title "Phase 1: Time Window" -======= $periodStart = (Get-Date).AddHours(-$PeriodHours) @@ -52,7 +49,6 @@ if ($releasesJson) { $releases = if ($relData.releases) { @($relData.releases) } elseif ($relData -is [array]) { @($relData) } else { @() } if ($releases.Count -gt 0) { $prevTag = if ($releases[0].tag_name) { $releases[0].tag_name } else { "" } -======= } } catch {} @@ -95,7 +91,6 @@ if ($prevTag -ne "initial") { } Log-Ok "Commits: $commitCount" -<<<<<<< HEAD # -- Merged PRs -- Log-Step "Collecting merged PRs..." $prItems = @() @@ -106,7 +101,7 @@ if ($prsJson) { $prData = ($prsJson | ConvertFrom-Json).data $prs = if ($prData.issues) { @($prData.issues) } elseif ($prData.pulls) { @($prData.pulls) } elseif ($prData -is [array]) { @($prData) } else { @() } foreach ($pr in $prs) { - $prTitle = if ($pr.subject) { $pr.subject } elseif ($pr.title) { $pr.title } else { "N/A" } + $prTitle = if ($pr.name) { $pr.name } elseif ($pr.subject) { $pr.subject } elseif ($pr.title) { $pr.title } else { "N/A" } $prNum = if ($pr.pull_request_number) { $pr.pull_request_number } elseif ($pr.number) { $pr.number } else { "?" } $prAuthor = if ($pr.author_login) { $pr.author_login } elseif ($pr.author.login) { $pr.author.login } else { "?" } $prItems += "- $prTitle (#$prNum) (@$prAuthor)" @@ -127,6 +122,9 @@ $docLines = @() $otherLines = @() $issContributors = @{} $issCount = 0 +# Dedupe by issue index: the same issue can be returned by both the open +# and closed state queries, which would otherwise double-count it. +$seenIssues = @{} $today = (Get-Date -Format "yyyy-MM-dd") $yesterday = ((Get-Date).AddDays(-1)).ToString("yyyy-MM-dd") @@ -138,6 +136,8 @@ foreach ($state in @("open","closed")) { $issues = if ($issData.issues) { @($issData.issues) } elseif ($issData -is [array]) { @($issData) } else { @() } foreach ($iss in $issues) { $num = if ($iss.project_issues_index) { $iss.project_issues_index } elseif ($iss.number) { $iss.number } else { "?" } + if ($num -ne "?" -and $seenIssues.ContainsKey($num)) { continue } + if ($num -ne "?") { $seenIssues[$num] = $true } $title = if ($iss.subject) { $iss.subject } elseif ($iss.title) { $iss.title } else { "N/A" } $desc = if ($iss.description) { $iss.description } else { "" } $author = if ($iss.author.login) { $iss.author.login } elseif ($iss.author.username) { $iss.author.username } else { "?" } @@ -151,7 +151,6 @@ foreach ($state in @("open","closed")) { if (-not $inPeriod -and $closed) { try { if (([DateTime]$closed) -ge $periodStart) { $inPeriod = $true } } catch {} } if (-not $inPeriod) { continue } -<<<<<<< HEAD $line = "- $title (#$num) (@$author)" if ($stateName -match "关闭|closed") { $line += " [已关闭]" } @@ -191,140 +190,199 @@ $featSection = if ($featLines.Count -gt 0) { ($featLines -join "`n") } else { "_ $docSection = if ($docLines.Count -gt 0) { ($docLines -join "`n") } else { "_无_" } $otherSection = if ($otherLines.Count -gt 0) { ($otherLines -join "`n") } else { "_无_" } Log-Ok "Issues in period: $issCount" -======= -$closedJson = Invoke-GL "issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100" -$closedCount = if ($closedJson) { @($closedJson.data.issues).Count } else { 0 } -$mergedJson = Invoke-GL "pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100" -$mergedData = @() -$mergedCount = 0 -if ($mergedJson) { - $d = $mergedJson.data - if ($d.issues) { $mergedData = @($d.issues) } - elseif ($d.pulls) { $mergedData = @($d.pulls) } - elseif ($d -is [array]) { $mergedData = $d } - $mergedCount = $mergedData.Count +# -- 动态收集标签 ID 映射 (与 01a-issue-triage.ps1 一致) -- +Log-Step "Discovering label IDs..." +$tagIdMap = @{} + +# 优先从项目 issue_tags API 获取 +$tagsApi = Invoke-GL api,GET,"/v1/$Owner/$Repo/issue_tags" +if ($tagsApi) { + try { + $tData = ($tagsApi | ConvertFrom-Json).data + $tagList = if ($tData.issue_tags) { @($tData.issue_tags) } elseif ($tData -is [array]) { @($tData) } else { @() } + foreach ($t in $tagList) { + if ($t.id -and $t.name) { $tagIdMap[$t.name] = $t.id } + } + } catch {} +} + +# 补充:从已有 issue 的 tags 字段收集 +foreach ($state in @("open","closed")) { + $sample = Invoke-GL issue,+list,--owner,$Owner,--repo,$Repo,--state,$state,--limit,50 + if ($sample) { + try { + $sData = ($sample | ConvertFrom-Json).data + $sIssues = if ($sData.issues) { @($sData.issues) } elseif ($sData -is [array]) { @($sData) } else { @() } + foreach ($iss in $sIssues) { + if ($iss.tags) { + foreach ($t in $iss.tags) { + if ($t.id -and $t.name) { $tagIdMap[$t.name] = $t.id } + } + } + } + } catch {} + } +} +$tagFound = @() +foreach ($k in $tagIdMap.Keys) { $tagFound += "$k($($tagIdMap[$k]))" } +if ($tagFound.Count -gt 0) { Log-Ok "Found tags: $($tagFound -join ' ')" } else { Log-Warn "No tags found in repo" } + +# -- 打标签 & 分配责任人 (自动分类后的实际写入) -- +# 收集需要处理的 Issue 信息 (编号, issue ID, 标签名, 作者) +$triageItems = @() +$seenTriage = @{} + +foreach ($state in @("open","closed")) { + $issJson2 = Invoke-GL issue,+list,--owner,$Owner,--repo,$Repo,--state,$state,--limit,100 + if (-not $issJson2) { continue } + try { + $issData2 = ($issJson2 | ConvertFrom-Json).data + $issues2 = if ($issData2.issues) { @($issData2.issues) } elseif ($issData2 -is [array]) { @($issData2) } else { @() } + foreach ($iss in $issues2) { + $num = if ($iss.project_issues_index) { $iss.project_issues_index } elseif ($iss.number) { $iss.number } else { "?" } + if ($num -eq "?" -or $seenTriage.ContainsKey($num)) { continue } + $created = if ($iss.created_at) { $iss.created_at } else { "" } + $closed = if ($iss.closed_at) { $iss.closed_at } else { "" } + $inPeriod = $false + if ($created) { try { if (([DateTime]$created) -ge $periodStart) { $inPeriod = $true } } catch {} } + if (-not $inPeriod -and $closed) { try { if (([DateTime]$closed) -ge $periodStart) { $inPeriod = $true } } catch {} } + if (-not $inPeriod) { continue } + + $seenTriage[$num] = $true + $title2 = if ($iss.subject) { $iss.subject } elseif ($iss.title) { $iss.title } else { "" } + $desc2 = if ($iss.description) { $iss.description } else { "" } + $author2 = if ($iss.author.login) { $iss.author.login } elseif ($iss.author.username) { $iss.author.username } else { "" } + $issueId2 = if ($iss.id) { $iss.id } else { $num } + + # 检查是否已有标签 + $hasLabel = $false + if ($iss.tags -and @($iss.tags).Count -gt 0) { $hasLabel = $true } + + # 关键词分类 + $combined2 = "$title2 $desc2".ToLower() + $chosenLabel = "" + if ($combined2 -match '(?i)bug|error|crash|fault|fix|缺陷|错误|异常|崩溃|修复|故障') { + $chosenLabel = "缺陷" + } elseif ($combined2 -match '(?i)feature|enhancement|add|新增|建议|功能|特性|新功能|支持|request') { + $chosenLabel = "功能" + } elseif ($combined2 -match '(?i)doc|readme|guide|wiki|tutorial|文档|说明|教程|手册') { + $chosenLabel = "文档" + } elseif ($combined2 -match '(?i)test|测试|用例|覆盖|验证') { + $chosenLabel = "测试" + } elseif ($combined2 -match '(?i)duplicate|重复|重复的') { + $chosenLabel = "重复" + } elseif ($combined2 -match '(?i)question|疑问|不确定|讨论|澄清|是否|可否') { + $chosenLabel = "疑问" + } elseif ($combined2 -match '(?i)help|协助|帮助|协作|请求帮助|互助') { + $chosenLabel = "协助" + } elseif ($combined2 -match '(?i)postpone|wontfix|暂缓|搁置|低优|不重要|不紧急|暂不|delay') { + $chosenLabel = "搁置" + } elseif ($combined2 -match '(?i)task|todo|任务|待办|计划|安排') { + $chosenLabel = "任务" + } elseif ($combined2 -match '(?i)support|兼容|环境|依赖|平台|适配') { + $chosenLabel = "支持" + } + + # 检查是否已有负责人 + $hasAssignee = $false + if ($iss.assigned_to_id -and "$($iss.assigned_to_id)" -ne "0" -and "$($iss.assigned_to_id)" -ne "") { $hasAssignee = $true } + if ($iss.assigned_to -and $iss.assigned_to.login) { $hasAssignee = $true } + + if ($chosenLabel -or -not $hasAssignee) { + $triageItems += @{ num=$num; id=$issueId2; title=$title2; desc=$desc2; author=$author2; label=$chosenLabel; hasLabel=$hasLabel; hasAssignee=$hasAssignee } + } + } + } catch {} +} + +$labelOk = 0; $labelFail = 0; $labelSkip = 0 +$assignOk = 0; $assignFail = 0; $assignSkip = 0 + +if ($triageItems.Count -gt 0) { + Log-Step "Applying labels and assignees to $($triageItems.Count) issue(s)..." + foreach ($item in $triageItems) { + # -- 打标签 -- + if (-not $item.label) { + $labelSkip++ + } elseif ($item.hasLabel) { + Log-Info " #$($item.num) already has label, skipping" + $labelSkip++ + } elseif ($DryRun) { + Log-Warn " [DRY RUN] Would tag #$($item.num) with '$($item.label)'" + $labelSkip++ + } else { + $tgtId = $tagIdMap[$item.label] + if (-not $tgtId) { + Log-Warn " #$($item.num): label '$($item.label)' not found in repo tags — create on website first" + $labelFail++ + } else { + $bodyJson = "{`"issue_tag_ids`":[$tgtId]}" + $tagResult = Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$($item.id)",--body,$bodyJson + if ($tagResult) { + try { + $tagOkFlag = (($tagResult | ConvertFrom-Json).ok -eq $true) + } catch { $tagOkFlag = $false } + if ($tagOkFlag) { Log-Ok " #$($item.num) tagged: $($item.label)"; $labelOk++ } else { Log-Warn " #$($item.num) tag failed"; $labelFail++ } + } else { Log-Warn " #$($item.num) tag failed (no response)"; $labelFail++ } + } + } + + # -- 分配责任人 (默认分配给 Issue 作者) -- + if ($item.hasAssignee) { + $assignSkip++ + } elseif (-not $item.author -or $item.author -eq "?") { + Log-Info " #$($item.num): no author info, skipping assign" + $assignSkip++ + } elseif ($DryRun) { + Log-Warn " [DRY RUN] Would assign #$($item.num) to @$($item.author)" + $assignSkip++ + } else { + # Resolve login name to numeric user ID + $authorID = $null + $isNum = $false + try { [void][int]$item.author; $isNum = $true } catch {} + if ($isNum) { + $authorID = $item.author + } else { + $userJson = Invoke-GL api,GET,"/users/$($item.author)" + if ($userJson) { + try { + $userData = ($userJson | ConvertFrom-Json).data + if ($userData.id) { $authorID = [int]$userData.id } + elseif ($userData.user_id) { $authorID = [int]$userData.user_id } + } catch {} + } + } + if (-not $authorID) { + Log-Warn " #$($item.num): cannot resolve user ID for '@$($item.author)'" + $assignFail++ + } else { + $bodyJson = "{`"assigner_ids`":[$authorID]}" + $assignResult = Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$($item.id)",--body,$bodyJson + if ($assignResult) { + try { + $assignOkFlag = (($assignResult | ConvertFrom-Json).ok -eq $true) + } catch { $assignOkFlag = $false } + if ($assignOkFlag) { Log-Ok " #$($item.num) assigned to @$($item.author) (ID:$authorID)"; $assignOk++ } else { Log-Warn " #$($item.num) assign failed"; $assignFail++ } + } else { Log-Warn " #$($item.num) assign failed (no response)"; $assignFail++ } + } + } + } + Log-Ok "Labels: $labelOk applied, $labelFail failed, $labelSkip skipped" + Log-Ok "Assign: $assignOk applied, $assignFail failed, $assignSkip skipped" +} else { + Log-Info "No new issues require triage in this period" } ->>>>>>> master # -- 贡献者汇总 -- $allContribs = @($prContributors.Keys; $issContributors.Keys) | Select-Object -Unique | Sort-Object $contribText = if ($allContribs.Count -gt 0) { ($allContribs | ForEach-Object { "- @$_" }) -join "`n" } else { "(无活跃贡献者)" } -<<<<<<< HEAD # ================================================================ # Phase 3: 生成 Release Notes (changelog skill 模板) # ================================================================ Log-Title "Phase 3: Generate Release Notes" -======= -$reportTitle = "Community Weekly Report: $weekStart ~ $weekEnd" -$reportBody = "# $reportTitle" + "`n`n" -$reportBody += "## 概览" + "`n" -$reportBody += "- 仓库: **$Owner/$Repo**" + "`n" -$reportBody += "- 当前开放 Issue: **$newIssuesCount**" + "`n" -$reportBody += "- 已关闭 Issue: **$closedCount**" + "`n" -$reportBody += "- 已合并 PR: **$mergedCount**" + "`n`n" - -# 分类汇总 -$reportBody += "## Issue 分类汇总" + "`n" -$reportBody += "| 类型 | 数量 |" + "`n" -$reportBody += "|------|------|" + "`n" -$reportBody += "| Bug | $($BugIds.Count) |" + "`n" -$reportBody += "| Feature | $($FeatureIds.Count) |" + "`n" -$reportBody += "| Question | $($QuestionIds.Count) |" + "`n" -$reportBody += "| Docs | $($DocsIds.Count) |" + "`n`n" - -# 开放 Issue 清单(含分类标记) -$reportBody += "## 当前开放 Issue 清单" + "`n" -$reportBody += "| # | 标题 | 分类 | 创建时间 |" + "`n" -$reportBody += "|---|------|------|----------|" + "`n" -foreach ($issue in $issues) { - $id = $issue.id - $title = if ($issue.subject) { $issue.subject } elseif ($issue.title) { $issue.title } else { "-" } - $title = ($title -replace '\|', '\\|') - $cat = if ($BugIds -contains $id) { "Bug" } - elseif ($FeatureIds -contains $id) { "Feature" } - elseif ($QuestionIds -contains $id) { "Question" } - elseif ($DocsIds -contains $id) { "Docs" } - else { "-" } - $created = if ($issue.created_at) { $issue.created_at } else { "-" } - $reportBody += "| #$id | $title | $cat | $created |" + "`n" -} -$reportBody += "`n" - -# 已关闭 Issue 清单 -$reportBody += "## 近期已关闭 Issue" + "`n" -if ($closedCount -gt 0) { - $closedIssues = @($closedJson.data.issues) - $closedLimit = [Math]::Min($closedCount, 15) - $reportBody += "| # | 标题 |" + "`n" - $reportBody += "|---|------|" + "`n" - for ($i = 0; $i -lt $closedLimit; $i++) { - $it = $closedIssues[$i] - $cid = if ($it.id) { $it.id } elseif ($it.number) { $it.number } else { "-" } - $ctitle = if ($it.subject) { $it.subject } elseif ($it.title) { $it.title } else { "-" } - $ctitle = ($ctitle -replace '\|', '\\|') - $reportBody += "| #$cid | $ctitle |" + "`n" - } - if ($closedCount -gt $closedLimit) { - $reportBody += "| ... | 还有 $($closedCount - $closedLimit) 条 |`n" - } -} else { - $reportBody += "_本周无关闭记录_" + "`n" -} -$reportBody += "`n" - -# 已合并 PR 清单(含作者) -$reportBody += "## 近期已合并 PR" + "`n" -if ($mergedCount -gt 0) { - $prLimit = [Math]::Min($mergedCount, 15) - $reportBody += "| # | 标题 | 作者 |" + "`n" - $reportBody += "|---|------|------|" + "`n" - for ($i = 0; $i -lt $prLimit; $i++) { - $pr = $mergedData[$i] - $prId = if ($pr.id) { $pr.id } elseif ($pr.number) { $pr.number } else { "-" } - $ptitle = if ($pr.subject) { $pr.subject } elseif ($pr.title) { $pr.title } else { "-" } - $ptitle = ($ptitle -replace '\|', '\\|') - $pauthor = if ($pr.author -and $pr.author.login) { $pr.author.login } elseif ($pr.user -and $pr.user.login) { $pr.user.login } else { "-" } - $reportBody += "| #$prId | $ptitle | @$pauthor |" + "`n" - } - if ($mergedCount -gt $prLimit) { - $reportBody += "| ... | 还有 $($mergedCount - $prLimit) 条合并 PR |`n" - } -} else { - $reportBody += "_本周无合并记录_" + "`n" -} -$reportBody += "`n" - -# 贡献者排行(按合并 PR 数) -$reportBody += "## 贡献者排行(按合并 PR 数)" + "`n" -if ($mergedCount -gt 0) { - $contributorMap = @{} - foreach ($pr in $mergedData) { - $login = if ($pr.author -and $pr.author.login) { $pr.author.login } elseif ($pr.user -and $pr.user.login) { $pr.user.login } else { $null } - if ($login) { - if ($contributorMap.ContainsKey($login)) { $contributorMap[$login]++ } - else { $contributorMap[$login] = 1 } - } - } - $reportBody += "| 排名 | 贡献者 | 合并 PR 数 |" + "`n" - $reportBody += "|------|--------|------------|" + "`n" - $rank = 1 - foreach ($kv in ($contributorMap.GetEnumerator() | Sort-Object Value -Descending)) { - $reportBody += "| $rank | @$($kv.Name) | $($kv.Value) |" + "`n" - $rank++ - } -} else { - $reportBody += "_本周无合并记录_" + "`n" -} -$reportBody += "`n" - -$reportBody += "## 本周自动化执行" + "`n" -$reportBody += "- 自动分类并打标 Issue: **$totalClassified** 条" + "`n" -$reportBody += "- 已为 Bug/Feature 类 Issue 指派负责人" + "`n`n" -$reportBody += "---" + "`n" -$reportBody += "*Auto-generated by gitlink-cli community-ops workflow*" ->>>>>>> master $releaseBody = @" # 🎉 Release $newVersion @@ -364,7 +422,6 @@ $contribText Write-Host $releaseBody Write-Host "" -<<<<<<< HEAD # ================================================================ # Phase 4: 发布 Release # ================================================================ @@ -380,30 +437,6 @@ if ($DryRun) { Log-Info "View: https://www.gitlink.org.cn/$Owner/$Repo/releases" } else { Log-Warn "Release may have failed (tag might exist)" -======= -Log-Step "Publishing weekly report to Wiki..." -$wikiResult = Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Repo, "--title", $reportTitle, "--content", $reportBody -if ($wikiResult -and $wikiResult.ok) { Log-Ok "Weekly report published to Wiki" } else { Log-Warn "Wiki publish may have failed" } - -# ---------------------------------------------------------------- -Log-Title "Phase 4: Auto-Publish Release Notes" -# ---------------------------------------------------------------- - -Log-Step "Collecting recent changes for release notes..." - -$tagName = "weekly-$(Get-Date -Format 'yyyyMMdd')" -$releaseName = "Weekly Release $(Get-Date -Format 'yyyy-MM-dd')" - -$releaseBody = "# Release Notes - $(Get-Date -Format 'yyyy-MM-dd')" + "`n`n" -$releaseBody += "## Merged PRs ($mergedCount)" - -if ($mergedCount -gt 0) { - $limit = [Math]::Min($mergedCount, 10) - for ($i = 0; $i -lt $limit; $i++) { - $prTitle = if ($mergedData[$i].subject) { $mergedData[$i].subject } elseif ($mergedData[$i].title) { $mergedData[$i].title } else { "" } - $prNum = if ($mergedData[$i].id) { $mergedData[$i].id } elseif ($mergedData[$i].number) { $mergedData[$i].number } else { "" } - $releaseBody += "`n- #$prNum $prTitle" ->>>>>>> master } } @@ -458,27 +491,11 @@ if ($DryRun) { } } -<<<<<<< HEAD Log-Title "Complete" Write-Host " Release: $newVersion" -ForegroundColor Green Write-Host " PRs merged: $prCount" -ForegroundColor Green Write-Host " Issues: $issCount" -ForegroundColor Green +Write-Host " Labels: $labelOk applied / $labelFail failed / $labelSkip skipped" -ForegroundColor Green +Write-Host " Assigned: $assignOk applied / $assignFail failed / $assignSkip skipped" -ForegroundColor Green Write-Host " Contributors: $($allContribs.Count)" -ForegroundColor Green Write-Host " Wiki: $wikiTitle" -ForegroundColor Green -======= -$releaseBody += "`n`n---`n*Auto-generated by gitlink-cli community-ops workflow*" - -Log-Step "Creating release: $tagName..." -$releaseResult = Invoke-GL "release", "+create", "--owner", $Owner, "--repo", $Repo, "--tag", $tagName, "--name", $releaseName, "--body", $releaseBody -if ($releaseResult -and $releaseResult.ok) { Log-Ok "Release $tagName created successfully" } else { Log-Warn "Release creation may have failed (tag might already exist)" } - -# ---------------------------------------------------------------- -Log-Title "Community Operations Complete" -# ---------------------------------------------------------------- - -Write-Host " Issues classified: $totalClassified" -ForegroundColor Green -Write-Host " Closed this week: $closedCount" -ForegroundColor Green -Write-Host " Merged PRs: $mergedCount" -ForegroundColor Green -Write-Host " Weekly report: Published to Wiki" -ForegroundColor Green -Write-Host " Release notes: $tagName" -ForegroundColor Green ->>>>>>> master diff --git a/workflows/01-community-ops.sh b/workflows/01-community-ops.sh index d779295c..3dfd5e91 100644 --- a/workflows/01-community-ops.sh +++ b/workflows/01-community-ops.sh @@ -94,7 +94,7 @@ PR_CONTRIBUTORS="" PR_TMP=$(mktemp) PRS_JSON=$(gl_run pr +list --owner "$OWNER" --repo "$REPO" --state merged --limit 100 2>/dev/null || true) if [[ -n "$PRS_JSON" ]]; then - echo "$PRS_JSON" | jq -r '(.data.issues // .data.pulls // .data // [])[]? | "\(.subject // .title // "N/A")\t\(.pull_request_number // .number // .id)\t\(.author_login // .author.login // "?")"' 2>/dev/null > "$PR_TMP" || true + echo "$PRS_JSON" | jq -r '(.data.issues // .data.pulls // .data // [])[]? | "\(.name // .subject // .title // "N/A")\t\(.pull_request_number // .number // .id)\t\(.author_login // .author.login // "?")"' 2>/dev/null > "$PR_TMP" || true while IFS=$'\t' read -r pr_title pr_num pr_author || [[ -n "$pr_title" ]]; do PR_ITEMS+="- ${pr_title} (#${pr_num}) (@${pr_author})"$'\n' PR_CONTRIBUTORS+="@${pr_author} " diff --git a/workflows/01a-issue-triage.ps1 b/workflows/01a-issue-triage.ps1 index 79c49716..b93c285b 100644 --- a/workflows/01a-issue-triage.ps1 +++ b/workflows/01a-issue-triage.ps1 @@ -9,21 +9,22 @@ param( [string]$Owner = "", [string]$Repo = "", - [Parameter(Mandatory=$true)] - [string]$IssueNumber, + [string]$IssueNumber = "", [string]$Assignee = "", [switch]$DryRun, [switch]$Help ) $ErrorActionPreference = "Stop" -Import-Module "$PSScriptRoot/lib/common.psm1" -Force +Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue if ($Help) { Write-Host "Usage: powershell 01a-issue-triage.ps1 -IssueNumber N [-Owner OWNER] [-Repo REPO] [-Assignee USER] [-DryRun]" exit 0 } +if (-not $IssueNumber) { Log-Err "IssueNumber is required (use -Help for usage)"; exit 1 } + Check-Auth $r = Resolve-OwnerRepo $Owner $Repo $Owner = $r.Owner; $Repo = $r.Repo @@ -55,6 +56,19 @@ Log-Ok "Issue `"$issueTitle`" by @$issueAuthor" Log-Step "Discovering label IDs..." $tagIdMap = @{} +# 优先从项目 issue_tags API 获取 +$tagsApi = Invoke-GL api,GET,"/v1/$Owner/$Repo/issue_tags" +if ($tagsApi) { + try { + $tData = ($tagsApi | ConvertFrom-Json).data + $tagList = if ($tData.issue_tags) { @($tData.issue_tags) } elseif ($tData -is [array]) { @($tData) } else { @() } + foreach ($t in $tagList) { + if ($t.id -and $t.name) { $tagIdMap[$t.name] = $t.id } + } + } catch {} +} + +# 补充:从已有 issue 的 tags 字段收集 foreach ($state in @("open","closed")) { $sample = Invoke-GL issue,+list,--owner,$Owner,--repo,$Repo,--state,$state,--limit,50 if ($sample) { @@ -126,11 +140,8 @@ if (-not $chosenLabel) { } else { Log-Step "Tagging with '$chosenLabel' (ID:$tgtId)..." - $curTagsJson = if ($issueData.tags) { - ($issueData.tags | ForEach-Object { "{`"id`":$($_.id),`"name`":`"$($_.name)`"" }) -join "," | ForEach-Object { "[$_]" } - } else { "[]" } - - $bodyJson = "{`"tags`":[{`"id`":$tgtId,`"name`":`"$chosenLabel`"}]}" + # GitLink v1 API 使用 issue_tag_ids (整数ID数组),不是 tags (对象数组) + $bodyJson = "{`"issue_tag_ids`":[$tgtId]}" $tagResult = Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$issueId",--body,$bodyJson if ($tagResult) { @@ -151,12 +162,40 @@ if (-not $targetAssignee) { } elseif ($DryRun) { Log-Warn "[DRY RUN] Would assign @$targetAssignee" } else { - $bodyJson = "{`"subject`":`"$($issueTitle -replace '"','\"')`",`"description`":`"$($issueDesc -replace '"','\"')`",`"assigned_to_id`":`"$targetAssignee`"}" - $result = Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$issueId",--body,$bodyJson - if ($result -and ((($result | ConvertFrom-Json).ok -eq $true))) { - Log-Ok "Assigned: @$targetAssignee" + # assigned_to_id 需要数字用户 ID,不是登录名 + # 先尝试把输入当纯数字;否则通过 /users/{login} 查询 + $assigneeID = $null + $isNumeric = $false + try { [void][int]$targetAssignee; $isNumeric = $true } catch {} + if ($isNumeric) { + $assigneeID = $targetAssignee } else { - Log-Warn "Assign failed" + Log-Info "Resolving user ID for '$targetAssignee'..." + $userJson = Invoke-GL api,GET,"/users/$targetAssignee" + if ($userJson) { + try { + $userData = ($userJson | ConvertFrom-Json).data + if ($userData.id) { + $assigneeID = [int]$userData.id + } elseif ($userData.user_id) { + $assigneeID = [int]$userData.user_id + } + } catch {} + } + } + + if (-not $assigneeID) { + Log-Warn "Cannot resolve user ID for '$targetAssignee' — skipping assign" + } else { + $escTitle = $issueTitle -replace '"','\"' + $escDesc = $issueDesc -replace '"','\"' + $bodyJson = "{`"subject`":`"$escTitle`",`"description`":`"$escDesc`",`"assigner_ids`":[$assigneeID]}" + $result = Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$issueId",--body,$bodyJson + if ($result -and ((($result | ConvertFrom-Json).ok -eq $true))) { + Log-Ok "Assigned: @$targetAssignee (ID:$assigneeID)" + } else { + Log-Warn "Assign failed" + } } } diff --git a/workflows/01a-issue-triage.sh b/workflows/01a-issue-triage.sh index 989f5393..2381e06a 100644 --- a/workflows/01a-issue-triage.sh +++ b/workflows/01a-issue-triage.sh @@ -60,8 +60,15 @@ log_ok "Issue \"$ISSUE_TITLE\" by @$ISSUE_AUTHOR" # ── Phase 2: 动态收集标签 ID 映射 ───────────────────────────────── log_step "Discovering label IDs..." -# 从已有 issue 的 tags 字段收集 name→ID 映射 ID_MAP_FILE=$(mktemp) + +# 优先从项目 issue_tags API 获取 +TAGS_JSON=$(gl_run api GET "/v1/$OWNER/$REPO/issue_tags" 2>/dev/null || true) +if [[ -n "$TAGS_JSON" ]]; then + echo "$TAGS_JSON" | jq -r '(.data.issue_tags // .data // [])[]? | "\(.id)|\(.name)"' 2>/dev/null >> "$ID_MAP_FILE" || true +fi + +# 补充:从已有 issue 的 tags 字段收集(覆盖 issue_tags API 未返回的情况) for state in open closed; do SAMPLE=$(gl_run issue +list --owner "$OWNER" --repo "$REPO" --state "$state" --limit 50 2>/dev/null || true) echo "$SAMPLE" | jq -r '(.data.issues // .data // [])[]?.tags[]? | "\(.id)|\(.name)"' 2>/dev/null >> "$ID_MAP_FILE" || true @@ -136,13 +143,9 @@ else else log_step "Tagging with '$CHOSEN_LABEL' (ID:$TGT_ID)..." - # 获取当前 tags,追加新标签(去重) - CUR_TAGS=$(echo "$ISSUE_JSON" | jq -c '[.data.tags[]? | {id:.id, name:.name}]' 2>/dev/null || echo "[]") - NEW_TAGS=$(echo "$CUR_TAGS" | jq -c --argjson nt "{\"id\":$TGT_ID,\"name\":\"$CHOSEN_LABEL\"}" \ - '. + [$nt] | unique_by(.id)' 2>/dev/null) TAG_RESP=$(gl_run api PATCH "/v1/$OWNER/$REPO/issues/$ISSUE_NUMBER" \ - --body "{\"tags\":$NEW_TAGS}" 2>&1) || true + --body "{\"issue_tag_ids\":[$TGT_ID]}" 2>&1) || true if echo "$TAG_RESP" | jq -e '.ok == true' &>/dev/null; then log_ok "Tagged: $CHOSEN_LABEL" @@ -162,13 +165,28 @@ if [[ -z "$TARGET_ASSIGNEE" ]]; then elif [[ "$DRY_RUN" == "true" ]]; then log_warn "[DRY RUN] Would assign @$TARGET_ASSIGNEE" else - BODY="{\"assigned_to_id\":\"$TARGET_ASSIGNEE\"}" - ARESP=$(gl_run api PATCH "/v1/$OWNER/$REPO/issues/$ISSUE_NUMBER" --body "$BODY" 2>&1) || true - if echo "$ARESP" | jq -e '.ok == true' &>/dev/null; then - log_ok "Assigned: @$TARGET_ASSIGNEE" + # Resolve login name to numeric user ID + ASSIGNEE_ID="" + if [[ "$TARGET_ASSIGNEE" =~ ^[0-9]+$ ]]; then + ASSIGNEE_ID="$TARGET_ASSIGNEE" else - ERR2=$(echo "$ARESP" | jq -r '.error.message // "unknown"' 2>/dev/null || echo "unknown") - log_warn "Assign failed: $ERR2" + USER_JSON=$(gl_run api GET "/users/$TARGET_ASSIGNEE" 2>/dev/null || true) + if [[ -n "$USER_JSON" ]]; then + ASSIGNEE_ID=$(echo "$USER_JSON" | jq -r '.data.id // .data.user_id // ""' 2>/dev/null || echo "") + fi + fi + + if [[ -z "$ASSIGNEE_ID" ]]; then + log_warn "Cannot resolve user ID for '$TARGET_ASSIGNEE' — skipping assign" + else + BODY="{\"assigner_ids\":[$ASSIGNEE_ID]}" + ARESP=$(gl_run api PATCH "/v1/$OWNER/$REPO/issues/$ISSUE_NUMBER" --body "$BODY" 2>&1) || true + if echo "$ARESP" | jq -e '.ok == true' &>/dev/null; then + log_ok "Assigned: @$TARGET_ASSIGNEE (ID:$ASSIGNEE_ID)" + else + ERR2=$(echo "$ARESP" | jq -r '.error.message // "unknown"' 2>/dev/null || echo "unknown") + log_warn "Assign failed: $ERR2" + fi fi fi diff --git a/workflows/01a-webhook-listener.py b/workflows/01a-webhook-listener.py index 2a3da6f5..4db54c65 100644 --- a/workflows/01a-webhook-listener.py +++ b/workflows/01a-webhook-listener.py @@ -22,6 +22,7 @@ PORT = int(os.environ.get("WEBHOOK_PORT", "8080")) SECRET = os.environ.get("WEBHOOK_SECRET", "") SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) TRIAGE_SCRIPT = os.path.join(SCRIPT_DIR, "01a-issue-triage.sh") +GATEKEEPER_SCRIPT = os.path.join(SCRIPT_DIR, "02a-pr-gatekeeper.sh") LOG_DIR = os.path.join(SCRIPT_DIR, "webhook-logs") os.makedirs(LOG_DIR, exist_ok=True) @@ -60,24 +61,34 @@ def verify_signature(body: bytes, signature_header: str) -> bool: def extract_issue_number(payload: dict) -> str: """从 webhook payload 提取 Issue 编号""" - # GitLink 格式 issue = payload.get("issue", {}) if issue: num = issue.get("project_issues_index") or issue.get("number") or issue.get("id") if num: log(f"Extracted issue number: #{num}", "OK") return str(num) - - # 直接格式 if "number" in payload: return str(payload["number"]) if "id" in payload: return str(payload["id"]) - log("Could not extract issue number from payload", "WARN") return None +def extract_pr_number(payload: dict) -> str: + """从 webhook payload 提取 PR 编号""" + pr = payload.get("pull_request", {}) or payload.get("pullrequest", {}) + if pr: + num = pr.get("number") or pr.get("pull_request_number") or pr.get("id") + if num: + log(f"Extracted PR number: #{num}", "OK") + return str(num) + if "pull_request_number" in payload: + return str(payload["pull_request_number"]) + log("Could not extract PR number from payload", "WARN") + return None + + def extract_repo_info(payload: dict): """从 payload 提取 owner/repo""" repo = payload.get("repository", {}) @@ -116,6 +127,34 @@ def run_triage_async(issue_number: str, owner: str = "", repo: str = ""): t.start() +def run_gatekeeper_async(pr_number: str, owner: str = "", repo: str = ""): + """异步调用 PR gatekeeper 脚本""" + def _run(): + cmd = ["bash", GATEKEEPER_SCRIPT, "--pr-id", pr_number] + if owner: + cmd += ["--owner", owner] + if repo: + cmd += ["--repo", repo] + + log(f"Dispatching gatekeeper: {' '.join(cmd)}", "INFO") + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, cwd=SCRIPT_DIR) + outfile = os.path.join(LOG_DIR, f"gatekeeper-{pr_number}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.log") + with open(outfile, "w", encoding="utf-8") as f: + f.write(f"=== STDOUT ===\n{result.stdout}\n=== STDERR ===\n{result.stderr}\n") + if result.returncode == 0: + log(f"Gatekeeper PR #{pr_number} completed successfully -> {outfile}", "OK") + else: + log(f"Gatekeeper PR #{pr_number} failed (exit={result.returncode}) -> {outfile}", "ERR") + except subprocess.TimeoutExpired: + log(f"Gatekeeper PR #{pr_number} TIMEOUT after 300s", "ERR") + except Exception as e: + log(f"Gatekeeper PR #{pr_number} error: {e}", "ERR") + + t = threading.Thread(target=_run, daemon=True) + t.start() + + class WebhookHandler(BaseHTTPRequestHandler): def log_message(self, format, *args): log(f"{self.client_address[0]} - {format % args}", "INFO") @@ -129,7 +168,7 @@ h1{{color:#333}}.ok{{color:green;font-weight:bold}}code{{background:#f0f0f0;padd

GitLink Community Ops — Webhook Listener

✓ Running

-

Listening for issue events at /webhook

+

Listening for issue and pull_request events at /webhook

{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | Port: {PORT} | HMAC: {'enabled' if SECRET else 'disabled'}

""" self._respond(200, html, "text/html") @@ -149,12 +188,6 @@ h1{{color:#333}}.ok{{color:green;font-weight:bold}}code{{background:#f0f0f0;padd self._respond(403, '{"error":"Invalid signature"}') return - # 只处理 issue 事件 - if "issue" not in event_type.lower(): - log(f"Ignoring non-issue event: {event_type}", "INFO") - self._respond(200, '{"status":"ignored","reason":"non-issue event"}') - return - # 解析 payload try: payload = json.loads(body) @@ -163,28 +196,45 @@ h1{{color:#333}}.ok{{color:green;font-weight:bold}}code{{background:#f0f0f0;padd self._respond(400, '{"error":"Invalid JSON"}') return - # 只处理 "opened" 动作 action = payload.get("action", "") - if action and action != "opened": - log(f"Ignoring issue event with action: {action}", "INFO") - self._respond(200, f'{{"status":"ignored","reason":"action={action}"}}') + is_issue = "issue" in event_type.lower() + is_pr = "pull_request" in event_type.lower() or "pullrequest" in event_type.lower() + + if not is_issue and not is_pr: + log(f"Ignoring unsupported event: {event_type}", "INFO") + self._respond(200, '{"status":"ignored","reason":"unsupported event type"}') return - # 提取 Issue 编号 - issue_number = extract_issue_number(payload) - if not issue_number: - self._respond(400, '{"error":"Cannot extract issue number"}') + # 只处理 "opened" 动作 + if action and action != "opened": + log(f"Ignoring {event_type} event with action: {action}", "INFO") + self._respond(200, f'{{"status":"ignored","reason":"action={action}"}}') return # 提取 owner/repo owner, repo = extract_repo_info(payload) - log(f"=== New Issue #{issue_number} — dispatching to triage ===", "OK") + # ── Issue 事件 → 调起分类 ── + if is_issue: + issue_number = extract_issue_number(payload) + if not issue_number: + self._respond(400, '{"error":"Cannot extract issue number"}') + return + log(f"=== New Issue #{issue_number} — dispatching to triage ===", "OK") + run_triage_async(issue_number, owner or "", repo or "") + self._respond(200, f'{{"status":"accepted","issue_number":{issue_number}}}') + return - # 异步调起分类 - run_triage_async(issue_number, owner or "", repo or "") - - self._respond(200, f'{{"status":"accepted","issue_number":{issue_number}}}') + # ── PR 事件 → 调起 gatekeeper ── + if is_pr: + pr_number = extract_pr_number(payload) + if not pr_number: + self._respond(400, '{"error":"Cannot extract PR number"}') + return + log(f"=== New PR #{pr_number} — dispatching to gatekeeper ===", "OK") + run_gatekeeper_async(pr_number, owner or "", repo or "") + self._respond(200, f'{{"status":"accepted","pr_number":{pr_number}}}') + return def _respond(self, code, body, content_type="application/json"): self.send_response(code) @@ -202,7 +252,8 @@ def main(): print() log(f"Starting on port {PORT}") - log(f"Triage script: {TRIAGE_SCRIPT}") + log(f" Issue -> {os.path.basename(TRIAGE_SCRIPT)}") + log(f" PR -> {os.path.basename(GATEKEEPER_SCRIPT)}") log(f"HMAC verification: {'ENABLED' if SECRET else 'DISABLED (set WEBHOOK_SECRET env var)'}") print() diff --git a/workflows/01a-webhook-setup.ps1 b/workflows/01a-webhook-setup.ps1 index 7d34bcf2..f3678708 100644 --- a/workflows/01a-webhook-setup.ps1 +++ b/workflows/01a-webhook-setup.ps1 @@ -18,8 +18,7 @@ #Requires -Version 5.1 param( - [Parameter(Mandatory=$true, HelpMessage="Webhook 回调 URL,GitLink 会向此 URL 推送事件")] - [string]$WebhookUrl, + [string]$WebhookUrl = "", [string]$Owner = "", [string]$Repo = "", @@ -34,7 +33,7 @@ param( ) $ErrorActionPreference = "Stop" -Import-Module "$PSScriptRoot/lib/common.psm1" -Force +Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue if ($Help) { Write-Host "Usage: powershell 01a-webhook-setup.ps1 -WebhookUrl URL [-Owner OWNER] [-Repo REPO] [-Secret SECRET] [-Events EVENTS] [-DryRun]" @@ -66,6 +65,8 @@ if ($Help) { exit 0 } +if (-not $WebhookUrl) { Log-Err "WebhookUrl is required (use -Help for usage)"; exit 1 } + Check-Auth $r = Resolve-OwnerRepo $Owner $Repo $Owner = $r.Owner; $Repo = $r.Repo diff --git a/workflows/02-code-quality-gatekeeper.ps1 b/workflows/02-code-quality-gatekeeper.ps1 new file mode 100644 index 00000000..2847a5d8 --- /dev/null +++ b/workflows/02-code-quality-gatekeeper.ps1 @@ -0,0 +1,567 @@ +# ---------------------------------------------------------------- +# Scenario 2: Code Quality Gatekeeper +# Flow: PR submit -> AI Review -> Check CI -> Auto-merge if pass +# +# Commands chained: +# 1. pr +list -- list open PRs +# 2. pr +view -- get PR details +# 3. pr +files -- get changed files +# 4. pr +diff -- get diff content +# 5. gitlink-code-review -- AI code review (claude CLI or keyword fallback) +# 6. api POST .../reviews -- post review comment +# 7. ci +builds -- check CI status +# 8. pr +merge -- auto-merge if quality threshold met +# ---------------------------------------------------------------- +#Requires -Version 5.1 + +param( + [string]$Owner = "", + [string]$Repo = "", + [string]$PrId = "", + [int]$Threshold = 80, + [switch]$DryRun, + [switch]$Help +) + +$ErrorActionPreference = "Stop" +Import-Module "$PSScriptRoot/lib/common.psm1" -Force + +if ($Help) { + Write-Host "Usage: powershell 02-code-quality-gatekeeper.ps1 -Owner OWNER -Repo REPO [-PrId ID] [-Threshold SCORE] [-DryRun]" + Write-Host "" + Write-Host " -Owner OWNER Repository owner" + Write-Host " -Repo REPO Repository name" + Write-Host " -PrId ID Specific PR to review (default: all open PRs)" + Write-Host " -Threshold SCORE Min quality score to auto-merge (default: 80)" + Write-Host " -DryRun Preview actions without executing" + exit 0 +} + +Check-Auth +$r = Resolve-OwnerRepo $Owner $Repo +$Owner = $r.Owner; $Repo = $r.Repo + +# ---------------------------------------------------------------- +# Default fallback strings (defined at script scope to avoid indented here-string issue) +$DefaultSkillDimensions = "1. 代码质量: 复杂度、命名、注释、格式`n2. 安全性: SQL注入、XSS、敏感信息、认证、输入验证`n3. 性能: 循环效率、资源泄漏、N+1查询、内存`n4. 可维护性: 代码重复、职责单一、依赖耦合、测试覆盖" + +$DefaultReviewPrompt = @' +你是代码审查专家。请按 gitlink-code-review skill 的审查维度分析以下 PR。 + +## 审查维度与检查项 + +{SKILL_DIMENSIONS} + +## 评分标准 +- 90-100: 优秀,可直接合并 +- 75-89: 良好,建议合并 +- 60-74: 一般,需要改进 +- <60: 较差,不建议合并 + +## 问题严重级别 +- CRITICAL: 阻止合并 +- HIGH: 强烈建议修复 +- MEDIUM: 建议修复 +- LOW: 可选修复 + +## PR 数据 + +PR 标题: {PR_TITLE} +变更文件: +{FILE_LIST} +代码差异: +{DIFF_CONTENT} + +## 输出要求 + +请严格按以下 JSON 格式输出,不要输出其他内容: +{"total": <0-100>, "quality": <0-25>, "security": <0-25>, "performance": <0-25>, "maintainability": <0-25>, "issues": [{"severity": "HIGH/MEDIUM/LOW", "category": "quality/security/performance/maintainability", "file": "文件路径", "rule": "规则名", "description": "问题描述", "suggestion": "修复建议"}], "positive_notes": [{"description": "优秀实践描述"}], "recommendations": ["改进建议1"], "verdict": "PASS或FAIL"} +'@ + +function Review-PR { + param([string]$PrId) + + Log-Title "Reviewing PR #$PrId" + + # -- Step 1: Get PR details -- + Log-Step "Fetching PR details..." + $prJson = Invoke-GLCheck pr,+view,--owner,$Owner,--repo,$Repo,--id,$PrId + if (-not $prJson) { + Log-Warn "Failed to fetch PR #$PrId, skipping" + return + } + $prData = $prJson.data + $prTitle = if ($prData.title) { $prData.title } + elseif ($prData.subject) { $prData.subject } + elseif ($prData.issue.subject) { $prData.issue.subject } + else { "N/A" } + $prState = if ($prData.state) { $prData.state } + elseif ($prData.status) { $prData.status } + else { "N/A" } + $prAuthor = if ($prData.author.login) { $prData.author.login } + elseif ($prData.author.username) { $prData.author.username } + elseif ($prData.issue.author.login) { $prData.issue.author.login } + else { "N/A" } + Log-Ok "PR #${PrId}: `"$prTitle`" by @$prAuthor (state: $prState)" + + # -- Step 2: Get changed files -- + Log-Step "Fetching changed files..." + $filesJson = Invoke-GL pr,+files,--owner,$Owner,--repo,$Repo,--id,$PrId + $fileNames = @() + $fileCount = 0 + if ($filesJson) { + try { + $filesData = $filesJson | ConvertFrom-Json + if ($filesData.data.files) { + $fileNames = @($filesData.data.files | ForEach-Object { + if ($_.name) { $_.name } elseif ($_.filename) { $_.filename } else { "unknown" } + }) + $fileCount = $fileNames.Count + } + } catch { } + } + Log-Ok "Changed files: $fileCount" + foreach ($f in $fileNames) { Write-Host " $f" } + + # -- Step 3: Get diff -- + Log-Step "Fetching diff..." + $diffContent = "" + $diffLines = 0 + $diffJson = Invoke-GL pr,+diff,--owner,$Owner,--repo,$Repo,--id,$PrId + if ($diffJson) { + try { + $diffData = $diffJson | ConvertFrom-Json + $diffLines_arr = @() + if ($diffData.data.files) { + foreach ($f in $diffData.data.files) { + if ($f.sections) { + foreach ($s in $f.sections) { + if ($s.lines) { + foreach ($l in $s.lines) { + if ($l.content) { $diffLines_arr += $l.content } + } + } + } + } + } + } + $diffContent = ($diffLines_arr -join "`n") + if ($diffContent.Length -gt 5000) { $diffContent = $diffContent.Substring(0, 5000) } + $diffLines = $diffLines_arr.Count + } catch { } + } + Log-Ok "Diff: $diffLines lines" + + # -- Step 4: AI-powered code review -- + Log-Step "AI analyzing code quality..." + + $fileListText = "" + foreach ($f in $fileNames) { $fileListText += "- $f`n" } + + $diffTruncated = $diffContent + if ($diffTruncated.Length -gt 4000) { $diffTruncated = $diffTruncated.Substring(0, 4000) } + + # Load skill dimensions from SKILL.md + $skillDir = "$PSScriptRoot/../skills/gitlink-code-review" + $skillDimensions = "" + if (Test-Path "$skillDir/SKILL.md") { + $skillMd = Get-Content "$skillDir/SKILL.md" -Raw -Encoding UTF8 + if ($skillMd -match '(?s)## 📊 审查维度(.+?)## 🔧 使用方式') { + $section = $Matches[1] + $dimLines = ($section -split "`n" | Where-Object { $_ -match '^- \*\*' } | Select-Object -First 20) + $skillDimensions = $dimLines -join "`n" + } + } + + if (-not $skillDimensions) { + $skillDimensions = $DefaultSkillDimensions + } + + $reviewPrompt = $DefaultReviewPrompt -replace '\{PR_TITLE\}', $prTitle -replace '\{FILE_LIST\}', $fileListText -replace '\{DIFF_CONTENT\}', $diffTruncated -replace '\{SKILL_DIMENSIONS\}', $skillDimensions + + $aiAvailable = $false + $totalScore = 0 + $scoreQuality = 25 + $scoreSecurity = 25 + $scorePerformance = 25 + $scoreMaintainability = 25 + $issuesFound = @() + $aiPositive = @() + $aiRecommendations = @() + $aiVerdict = "PASS" + + $claudePath = (Get-Command claude -ErrorAction SilentlyContinue).Source + if ($claudePath) { + Log-Info "Calling AI agent for code review (may take 30-60s)..." + + try { + $promptFile = [System.IO.Path]::GetTempFileName() + $outFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllText($promptFile, $reviewPrompt, [System.Text.Encoding]::UTF8) + + $aiResult = $null + $proc = Start-Process -FilePath $claudePath ` + -ArgumentList @("-p", "--output-format", "json") ` + -RedirectStandardInput $promptFile ` + -RedirectStandardOutput $outFile ` + -NoNewWindow -Wait -PassThru + + if ($proc.ExitCode -eq 0 -and (Test-Path $outFile)) { + $aiOutput = [System.IO.File]::ReadAllText($outFile, [System.Text.Encoding]::UTF8) + try { + $aiOutputJson = $aiOutput | ConvertFrom-Json + $aiResult = $aiOutputJson.result + } catch { + $aiResult = $aiOutput + } + } + + Remove-Item $promptFile -Force -ErrorAction SilentlyContinue + Remove-Item $outFile -Force -ErrorAction SilentlyContinue + + if ($aiResult) { + # Extract JSON from AI response (may contain markdown wrapping) + $aiJson = $null + $jsonCandidate = Extract-JsonBlock $aiResult + if ($jsonCandidate) { + try { + $testJson = $jsonCandidate | ConvertFrom-Json + if ($testJson.total -ne $null -and $testJson.verdict) { + $aiJson = $testJson + } + } catch { } + } + # Fallback: try direct parse + if (-not $aiJson) { + try { + $testJson = $aiResult | ConvertFrom-Json + if ($testJson.total -ne $null -and $testJson.verdict) { + $aiJson = $testJson + } + } catch { } + } + + if ($aiJson) { + $totalScore = [int]($aiJson.total -as [int]) + $scoreQuality = [int]($aiJson.quality -as [int]) + $scoreSecurity = [int]($aiJson.security -as [int]) + $scorePerformance = [int]($aiJson.performance -as [int]) + $scoreMaintainability = [int]($aiJson.maintainability -as [int]) + $aiVerdict = if ($aiJson.verdict) { $aiJson.verdict } else { "PASS" } + + if ($aiJson.issues) { + foreach ($issue in $aiJson.issues) { + if ($issue -is [string]) { + $issuesFound += $issue + } else { + $sev = if ($issue.severity) { $issue.severity } else { "?" } + $cat = if ($issue.category) { $issue.category } else { "?" } + $desc = if ($issue.description) { $issue.description } + elseif ($issue.rule) { $issue.rule } else { "unknown" } + $file = if ($issue.file) { " ($($issue.file))" } else { "" } + $sug = if ($issue.suggestion) { " -> $($issue.suggestion)" } else { "" } + $issuesFound += "[$sev] ${cat}: $desc${file}${sug}" + } + } + } + if ($aiJson.positive_notes) { + foreach ($note in $aiJson.positive_notes) { + if ($note.description) { $aiPositive += $note.description } + elseif ($note -is [string]) { $aiPositive += $note } + } + } + if ($aiJson.recommendations) { + foreach ($rec in $aiJson.recommendations) { + if ($rec -is [string]) { $aiRecommendations += $rec } + } + } + + $aiAvailable = $true + Log-Ok "AI review complete (verdict: $aiVerdict)" + } else { + Log-Warn "Could not parse AI response JSON, falling back to keyword-based" + } + } + } catch { + Log-Warn "AI call failed: $_" + } + } + + # -- Fallback: keyword-based heuristics -- + if (-not $aiAvailable) { + Log-Warn "AI not available, falling back to keyword-based analysis" + + $scoreQuality = 25 + $scoreSecurity = 25 + $scorePerformance = 25 + $scoreMaintainability = 25 + $issuesFound = @() + + if ($diffContent -match '(?i)password|secret|token|api_key|apikey|private_key') { + $scoreSecurity -= 15 + $issuesFound += "SECURITY: 检测到可能的硬编码凭证" + } + if ($diffContent -match '(?i)eval\(|exec\(|system\(|shell_exec|os\.system|subprocess\.call') { + $scoreSecurity -= 10 + $issuesFound += "SECURITY: 检测到危险函数调用" + } + if ($diffContent -match '(?i)TODO|FIXME|HACK|XXX') { + $scoreQuality -= 5 + $issuesFound += "QUALITY: 存在 TODO/FIXME/HACK 注释" + } + if ($diffContent -match '(?i)SELECT \*|\.findAll\(\)|\.all\(\)') { + $scorePerformance -= 10 + $issuesFound += "PERFORMANCE: 可能的全表查询" + } + if ($diffContent -match '(?i)sleep\(|time\.sleep|Thread\.sleep') { + $scorePerformance -= 5 + $issuesFound += "PERFORMANCE: 检测到阻塞式 sleep" + } + if ($fileCount -gt 20) { + $scoreMaintainability -= 10 + $issuesFound += "MAINTAINABILITY: 变更文件数量过多 ($fileCount)" + } + + $totalScore = [Math]::Max(0, $scoreQuality + $scoreSecurity + $scorePerformance + $scoreMaintainability) + } + + # -- Print review report -- + Divider + if ($aiAvailable) { + Log-Info "AI Review Report for PR #$PrId" + } else { + Log-Info "Review Report for PR #$PrId (keyword-based)" + } + Write-Host "" + Write-Host " Overall Score: $totalScore / 100" + Write-Host " Code Quality: $scoreQuality / 25" + Write-Host " Security: $scoreSecurity / 25" + Write-Host " Performance: $scorePerformance / 25" + Write-Host " Maintainability: $scoreMaintainability / 25" + Write-Host "" + + if ($issuesFound.Count -gt 0) { + Write-Host " Issues Found:" + foreach ($issue in $issuesFound) { + Write-Host " - $issue" + } + Write-Host "" + } + + if ($aiPositive.Count -gt 0) { + Write-Host " Positive Notes:" + foreach ($note in $aiPositive) { + Write-Host " + $note" + } + Write-Host "" + } + + if ($aiRecommendations.Count -gt 0) { + Write-Host " Recommendations:" + foreach ($rec in $aiRecommendations) { + Write-Host " > $rec" + } + Write-Host "" + } + + # -- Step 5: Post review comment -- + if ($aiAvailable) { + $reviewHeader = "## AI Code Quality Review - PR #$PrId" + } else { + $reviewHeader = "## Code Quality Review - PR #$PrId (keyword-based)" + } + + $reviewBody = "$reviewHeader`n`n### Scores`n" + $reviewBody += "| Dimension | Score | Max |`n" + $reviewBody += "|-----------|-------|-----|`n" + $reviewBody += "| Code Quality | $scoreQuality | 25 |`n" + $reviewBody += "| Security | $scoreSecurity | 25 |`n" + $reviewBody += "| Performance | $scorePerformance | 25 |`n" + $reviewBody += "| Maintainability | $scoreMaintainability | 25 |`n" + $reviewBody += "| **Total** | **$totalScore** | **100** |`n`n" + $reviewBody += "### Issues Found`n" + + if ($issuesFound.Count -gt 0) { + foreach ($issue in $issuesFound) { + $reviewBody += "- $issue`n" + } + } else { + $reviewBody += "No issues found.`n" + } + + if ($aiPositive.Count -gt 0) { + $reviewBody += "`n### Positive Notes`n" + foreach ($note in $aiPositive) { + $reviewBody += "- $note`n" + } + } + + if ($aiRecommendations.Count -gt 0) { + $reviewBody += "`n### Recommendations`n" + foreach ($rec in $aiRecommendations) { + $reviewBody += "- $rec`n" + } + } + + $verdictText = if ($totalScore -ge $Threshold) { + "**PASS** - Score $totalScore >= threshold $Threshold. Ready to merge." + } else { + "**FAIL** - Score $totalScore < threshold $Threshold. Please address the issues above." + } + $reviewBody += "`n### Verdict`n${verdictText}`n`n---`n*Auto-reviewed by gitlink-cli code-quality-gatekeeper workflow (skill: gitlink-code-review)*" + + Log-Step "Posting review comment..." + $reviewEvent = if ($totalScore -ge $Threshold) { "APPROVE" } else { "COMMENT" } + $reviewPayload = @{ + body = $reviewBody + event = $reviewEvent + } | ConvertTo-Json -Compress + + $reviewResult = Invoke-GL api,POST,"/$Owner/$Repo/pulls/$PrId/reviews",--body,$reviewPayload + if ($reviewResult) { + try { + $reviewOk = (($reviewResult | ConvertFrom-Json).ok -eq $true) + } catch { $reviewOk = $false } + if ($reviewOk) { + Log-Ok "Review posted" + } else { + Log-Warn "Review post may have failed (review API might not be available)" + } + } else { + Log-Warn "Review post may have failed" + } + + # -- Step 6: Check CI -- + Log-Step "Checking CI build status..." + $ciPresent = $false + $ciPassed = $true + $ciJson = Invoke-GL ci,+builds,--owner,$Owner,--repo,$Repo + if ($ciJson) { + try { + $ciData = $ciJson | ConvertFrom-Json + $builds = @() + if ($ciData.data.builds) { $builds = @($ciData.data.builds) } + elseif ($ciData.data -is [array]) { $builds = @($ciData.data) } + if ($builds.Count -gt 0) { + $ciPresent = $true + foreach ($b in $builds) { + $status = if ($b.status) { $b.status } elseif ($b.state) { $b.state } else { "unknown" } + $name = if ($b.name) { $b.name } else { "build" } + if ($status -notin @("success", "passed", "completed")) { + $ciPassed = $false + Log-Warn "CI '$name' status: $status" + } else { + Log-Ok "CI '$name' status: $status" + } + } + } + } catch { } + } + if (-not $ciPresent) { Log-Info "No CI builds found" } + + # -- Step 7: Auto-merge -- + if ($totalScore -ge $Threshold -and $ciPassed) { + Log-Step "Quality score $totalScore >= $Threshold and CI passed" + if ($DryRun) { + Log-Warn "[DRY RUN] Would auto-merge PR #$PrId" + } else { + Log-Step "Auto-merging PR #$PrId..." + $mergeResult = Invoke-GL pr,+merge,--owner,$Owner,--repo,$Repo,--id,$PrId,--method,merge + if ($mergeResult) { + try { + $mergeOk = (($mergeResult | ConvertFrom-Json).ok -eq $true) + } catch { $mergeOk = $false } + if ($mergeOk) { + Log-Ok "PR #$PrId merged successfully!" + } else { + Log-Err "Auto-merge failed" + } + } else { + Log-Err "Auto-merge failed" + } + } + } else { + Log-Warn "PR #$PrId not auto-merged (score: $totalScore, threshold: $Threshold, CI passed: $ciPassed)" + } + + Write-Host "" +} + +# ---------------------------------------------------------------- +function Extract-JsonBlock { + param([string]$Text) + # Find JSON by balanced brace matching + $depth = 0 + $start = -1 + $results = @() + for ($i = 0; $i -lt $Text.Length; $i++) { + $c = $Text[$i] + if ($c -eq '{') { + if ($depth -eq 0) { $start = $i } + $depth++ + } elseif ($c -eq '}') { + $depth-- + if ($depth -eq 0 -and $start -ge 0) { + $results += $Text.Substring($start, $i - $start + 1) + $start = -1 + } + } + } + # Return the last valid JSON object (usually the most complete) + for ($i = $results.Count - 1; $i -ge 0; $i--) { + try { + $obj = $results[$i] | ConvertFrom-Json + if ($obj.total -ne $null -and $obj.verdict) { + return $results[$i] + } + } catch { } + } + return $null +} + +# ================================================================ +# Main +# ================================================================ +Log-Title "Code Quality Gatekeeper" + +if ($PrId) { + Review-PR $PrId +} else { + Log-Step "Fetching open PRs..." + $prsResult = Invoke-GL pr,+list,--owner,$Owner,--repo,$Repo,--state,open,--limit,50 + $prList = @() + if ($prsResult) { + try { + $prsJson = $prsResult | ConvertFrom-Json + $prData = $prsJson.data + if ($prData.issues) { $prList = @($prData.issues) } + elseif ($prData.pulls) { $prList = @($prData.pulls) } + elseif ($prData -is [array]) { $prList = $prData } + } catch { } + } + $prCount = $prList.Count + Log-Ok "Found $prCount open PRs" + + if ($prCount -eq 0) { + Log-Info "No open PRs to review" + exit 0 + } + + $reviewed = 0 + $passed = 0 + $failed = 0 + + foreach ($pr in $prList) { + $prNum = if ($pr.pull_request_number) { $pr.pull_request_number } + elseif ($pr.number) { $pr.number } + elseif ($pr.id) { $pr.id } + else { $null } + if (-not $prNum) { continue } + Review-PR $prNum + $reviewed++ + } + + Log-Title "Gatekeeper Summary" + Write-Host " PRs Reviewed: $reviewed" -ForegroundColor Green + Write-Host " Threshold: $Threshold" -ForegroundColor Green +} diff --git a/workflows/02-code-quality-gatekeeper.sh b/workflows/02-code-quality-gatekeeper.sh index 3555289e..9e39f2d0 100644 --- a/workflows/02-code-quality-gatekeeper.sh +++ b/workflows/02-code-quality-gatekeeper.sh @@ -403,9 +403,9 @@ $(if [[ $TOTAL_SCORE -ge $THRESHOLD ]]; then echo "**PASS** - Score $TOTAL_SCORE *Auto-reviewed by gitlink-cli code-quality-gatekeeper workflow (skill: gitlink-code-review)*" log_step "Posting review comment..." - REVIEW_EVENT=$(if [[ $TOTAL_SCORE -ge $THRESHOLD ]]; then echo "APPROVE"; else echo "COMMENT"; fi) - REVIEW_JSON=$(jq -n --arg body "$REVIEW_BODY" --arg event "$REVIEW_EVENT" \ - '{body: $body, event: $event}') + REVIEW_STATE=$(if [[ $TOTAL_SCORE -ge $THRESHOLD ]]; then echo "approved"; else echo "commented"; fi) + REVIEW_JSON=$(jq -n --arg body "$REVIEW_BODY" --arg state "$REVIEW_STATE" \ + '{body: $body, state: $state}') REVIEW_RESULT=$(gl_run api POST "/$OWNER/$REPO/pulls/$pr_id/reviews" \ --body "$REVIEW_JSON" 2>&1) || true diff --git a/workflows/02a-pr-gatekeeper.ps1 b/workflows/02a-pr-gatekeeper.ps1 new file mode 100644 index 00000000..d825e218 --- /dev/null +++ b/workflows/02a-pr-gatekeeper.ps1 @@ -0,0 +1,413 @@ +# ---------------------------------------------------------------- +# Scenario 2a: Real-Time PR Gatekeeper (Webhook 触发) +# Flow: Webhook 推送 PR → 拉取详情 → AI Review → 查 CI → 发评论 → 达标自动合并 +# ---------------------------------------------------------------- +#Requires -Version 5.1 + +param( + [string]$Owner = "", + [string]$Repo = "", + [string]$PrId = "", + [int]$Threshold = 80, + [switch]$DryRun, + [switch]$Help +) + +$ErrorActionPreference = "Stop" +Import-Module "$PSScriptRoot/lib/common.psm1" -Force + +if ($Help) { + Write-Host "Usage: powershell 02a-pr-gatekeeper.ps1 -PrId ID [-Owner OWNER] [-Repo REPO] [-Threshold SCORE] [-DryRun]" + exit 0 +} + +if (-not $PrId) { Log-Err "PrId is required (use -Help for usage)"; exit 1 } + +Check-Auth +$r = Resolve-OwnerRepo $Owner $Repo +$Owner = $r.Owner; $Repo = $r.Repo + +# Default fallback strings +$DefaultSkillDimensions = "1. 代码质量: 复杂度、命名、注释、格式`n2. 安全性: SQL注入、XSS、敏感信息、认证、输入验证`n3. 性能: 循环效率、资源泄漏、N+1查询、内存`n4. 可维护性: 代码重复、职责单一、依赖耦合、测试覆盖" + +$DefaultReviewPrompt = @' +你是代码审查专家。请按 gitlink-code-review skill 的审查维度分析以下 PR。 + +## 审查维度与检查项 + +{SKILL_DIMENSIONS} + +## 评分标准 +- 90-100: 优秀,可直接合并 +- 75-89: 良好,建议合并 +- 60-74: 一般,需要改进 +- <60: 较差,不建议合并 + +## 问题严重级别 +- CRITICAL: 阻止合并 +- HIGH: 强烈建议修复 +- MEDIUM: 建议修复 +- LOW: 可选修复 + +## PR 数据 + +PR 标题: {PR_TITLE} +变更文件: +{FILE_LIST} +代码差异: +{DIFF_CONTENT} + +## 输出要求 + +请严格按以下 JSON 格式输出,不要输出其他内容: +{"total": <0-100>, "quality": <0-25>, "security": <0-25>, "performance": <0-25>, "maintainability": <0-25>, "issues": [{"severity": "HIGH/MEDIUM/LOW", "category": "quality/security/performance/maintainability", "file": "文件路径", "rule": "规则名", "description": "问题描述", "suggestion": "修复建议"}], "positive_notes": [{"description": "优秀实践描述"}], "recommendations": ["改进建议1"], "verdict": "PASS或FAIL"} +'@ + +function Extract-JsonBlock { + param([string]$Text) + $depth = 0; $start = -1; $results = @() + for ($i = 0; $i -lt $Text.Length; $i++) { + $c = $Text[$i] + if ($c -eq '{') { + if ($depth -eq 0) { $start = $i } + $depth++ + } elseif ($c -eq '}') { + $depth-- + if ($depth -eq 0 -and $start -ge 0) { + $results += $Text.Substring($start, $i - $start + 1) + $start = -1 + } + } + } + for ($i = $results.Count - 1; $i -ge 0; $i--) { + try { + $obj = $results[$i] | ConvertFrom-Json + if ($obj.total -ne $null -and $obj.verdict) { return $results[$i] } + } catch { } + } + return $null +} + +# ================================================================ +Log-Title "PR Gatekeeper: #$PrId ($Owner/$Repo)" + +# -- Step 1: 拉取 PR 详情 -- +Log-Step "Fetching PR details..." +$prJson = Invoke-GLCheck pr,+view,--owner,$Owner,--repo,$Repo,--id,$PrId +if (-not $prJson) { Log-Err "Failed to fetch PR #$PrId"; exit 1 } + +$prData = $prJson.data +$prTitle = if ($prData.title) { $prData.title } + elseif ($prData.subject) { $prData.subject } + elseif ($prData.issue.subject) { $prData.issue.subject } + else { "N/A" } +$prState = if ($prData.state) { $prData.state } + elseif ($prData.status) { $prData.status } + else { "N/A" } +$prAuthor = if ($prData.author.login) { $prData.author.login } + elseif ($prData.author.username) { $prData.author.username } + elseif ($prData.issue.author.login) { $prData.issue.author.login } + else { "N/A" } +Log-Ok "PR #${PrId}: `"$prTitle`" by @$prAuthor (state: $prState)" + +# -- Step 2: 获取变更文件列表 -- +Log-Step "Fetching changed files..." +$filesJson = Invoke-GL pr,+files,--owner,$Owner,--repo,$Repo,--id,$PrId +$fileNames = @(); $fileCount = 0 +if ($filesJson) { + try { + $filesData = $filesJson | ConvertFrom-Json + if ($filesData.data.files) { + $fileNames = @($filesData.data.files | ForEach-Object { + if ($_.name) { $_.name } elseif ($_.filename) { $_.filename } else { "unknown" } + }) + $fileCount = $fileNames.Count + } + } catch { } +} +Log-Ok "Changed files: $fileCount" +foreach ($f in $fileNames) { Write-Host " $f" } + +# -- Step 3: 获取 diff -- +Log-Step "Fetching diff..." +$diffContent = ""; $diffLines = 0 +$diffJson = Invoke-GL pr,+diff,--owner,$Owner,--repo,$Repo,--id,$PrId +if ($diffJson) { + try { + $diffData = $diffJson | ConvertFrom-Json + $diffLines_arr = @() + if ($diffData.data.files) { + foreach ($f in $diffData.data.files) { + if ($f.sections) { + foreach ($s in $f.sections) { + if ($s.lines) { + foreach ($l in $s.lines) { + if ($l.content) { $diffLines_arr += $l.content } + } + } + } + } + } + } + $diffContent = ($diffLines_arr -join "`n") + if ($diffContent.Length -gt 5000) { $diffContent = $diffContent.Substring(0, 5000) } + $diffLines = $diffLines_arr.Count + } catch { } +} +Log-Ok "Diff: $diffLines lines" + +# -- Step 4: AI 代码审查 -- +Log-Step "AI analyzing code quality..." + +$fileListText = "" +foreach ($f in $fileNames) { $fileListText += "- $f`n" } + +$diffTruncated = $diffContent +if ($diffTruncated.Length -gt 4000) { $diffTruncated = $diffTruncated.Substring(0, 4000) } + +$skillDir = "$PSScriptRoot/../skills/gitlink-code-review" +$skillDimensions = "" +if (Test-Path "$skillDir/SKILL.md") { + $skillMd = Get-Content "$skillDir/SKILL.md" -Raw -Encoding UTF8 + if ($skillMd -match '(?s)## 📊 审查维度(.+?)## 🔧 使用方式') { + $section = $Matches[1] + $dimLines = ($section -split "`n" | Where-Object { $_ -match '^- \*\*' } | Select-Object -First 20) + $skillDimensions = $dimLines -join "`n" + } +} +if (-not $skillDimensions) { $skillDimensions = $DefaultSkillDimensions } + +$reviewPrompt = $DefaultReviewPrompt -replace '\{PR_TITLE\}', $prTitle -replace '\{FILE_LIST\}', $fileListText -replace '\{DIFF_CONTENT\}', $diffTruncated -replace '\{SKILL_DIMENSIONS\}', $skillDimensions + +$aiAvailable = $false +$totalScore = 0 +$scoreQuality = 25; $scoreSecurity = 25; $scorePerformance = 25; $scoreMaintainability = 25 +$issuesFound = @(); $aiPositive = @(); $aiRecommendations = @(); $aiVerdict = "PASS" + +$claudePath = (Get-Command claude -ErrorAction SilentlyContinue).Source +if ($claudePath) { + Log-Info "Calling AI agent for code review (may take 30-60s)..." + try { + $promptFile = [System.IO.Path]::GetTempFileName() + $outFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllText($promptFile, $reviewPrompt, [System.Text.Encoding]::UTF8) + + $proc = Start-Process -FilePath $claudePath ` + -ArgumentList @("-p", "--output-format", "json") ` + -RedirectStandardInput $promptFile ` + -RedirectStandardOutput $outFile ` + -NoNewWindow -Wait -PassThru + + if ($proc.ExitCode -eq 0 -and (Test-Path $outFile)) { + $aiOutput = [System.IO.File]::ReadAllText($outFile, [System.Text.Encoding]::UTF8) + try { $aiResult = ($aiOutput | ConvertFrom-Json).result } catch { $aiResult = $aiOutput } + } + + Remove-Item $promptFile -Force -ErrorAction SilentlyContinue + Remove-Item $outFile -Force -ErrorAction SilentlyContinue + + if ($aiResult) { + $aiJson = $null + $jsonCandidate = Extract-JsonBlock $aiResult + if ($jsonCandidate) { + try { + $testJson = $jsonCandidate | ConvertFrom-Json + if ($testJson.total -ne $null -and $testJson.verdict) { $aiJson = $testJson } + } catch { } + } + if (-not $aiJson) { + try { + $testJson = $aiResult | ConvertFrom-Json + if ($testJson.total -ne $null -and $testJson.verdict) { $aiJson = $testJson } + } catch { } + } + + if ($aiJson) { + $totalScore = [int]($aiJson.total -as [int]) + $scoreQuality = [int]($aiJson.quality -as [int]) + $scoreSecurity = [int]($aiJson.security -as [int]) + $scorePerformance = [int]($aiJson.performance -as [int]) + $scoreMaintainability = [int]($aiJson.maintainability -as [int]) + $aiVerdict = if ($aiJson.verdict) { $aiJson.verdict } else { "PASS" } + + if ($aiJson.issues) { + foreach ($issue in $aiJson.issues) { + if ($issue -is [string]) { $issuesFound += $issue } + else { + $sev = if ($issue.severity) { $issue.severity } else { "?" } + $cat = if ($issue.category) { $issue.category } else { "?" } + $desc = if ($issue.description) { $issue.description } + elseif ($issue.rule) { $issue.rule } else { "unknown" } + $file = if ($issue.file) { " ($($issue.file))" } else { "" } + $sug = if ($issue.suggestion) { " -> $($issue.suggestion)" } else { "" } + $issuesFound += "[$sev] ${cat}: $desc${file}${sug}" + } + } + } + if ($aiJson.positive_notes) { + foreach ($note in $aiJson.positive_notes) { + if ($note.description) { $aiPositive += $note.description } + elseif ($note -is [string]) { $aiPositive += $note } + } + } + if ($aiJson.recommendations) { + foreach ($rec in $aiJson.recommendations) { + if ($rec -is [string]) { $aiRecommendations += $rec } + } + } + $aiAvailable = $true + Log-Ok "AI review complete (verdict: $aiVerdict)" + } else { + Log-Warn "Could not parse AI response JSON, falling back to keyword-based" + } + } + } catch { Log-Warn "AI call failed: $_" } +} + +# -- Fallback: keyword-based -- +if (-not $aiAvailable) { + Log-Warn "AI not available, falling back to keyword-based analysis" + $scoreQuality = 25; $scoreSecurity = 25; $scorePerformance = 25; $scoreMaintainability = 25 + $issuesFound = @() + + if ($diffContent -match '(?i)password|secret|token|api_key|apikey|private_key') { + $scoreSecurity -= 15; $issuesFound += "SECURITY: 检测到可能的硬编码凭证" + } + if ($diffContent -match '(?i)eval\(|exec\(|system\(|shell_exec|os\.system|subprocess\.call') { + $scoreSecurity -= 10; $issuesFound += "SECURITY: 检测到危险函数调用" + } + if ($diffContent -match '(?i)TODO|FIXME|HACK|XXX') { + $scoreQuality -= 5; $issuesFound += "QUALITY: 存在 TODO/FIXME/HACK 注释" + } + if ($diffContent -match '(?i)SELECT \*|\.findAll\(\)|\.all\(\)') { + $scorePerformance -= 10; $issuesFound += "PERFORMANCE: 可能的全表查询" + } + if ($diffContent -match '(?i)sleep\(|time\.sleep|Thread\.sleep') { + $scorePerformance -= 5; $issuesFound += "PERFORMANCE: 检测到阻塞式 sleep" + } + if ($fileCount -gt 20) { + $scoreMaintainability -= 10; $issuesFound += "MAINTAINABILITY: 变更文件数量过多 ($fileCount)" + } + $totalScore = [Math]::Max(0, $scoreQuality + $scoreSecurity + $scorePerformance + $scoreMaintainability) +} + +# -- 打印审查报告 -- +Divider +if ($aiAvailable) { Log-Info "AI Review Report for PR #$PrId" } +else { Log-Info "Review Report for PR #$PrId (keyword-based)" } +Write-Host "" +Write-Host " Overall Score: $totalScore / 100" +Write-Host " Code Quality: $scoreQuality / 25" +Write-Host " Security: $scoreSecurity / 25" +Write-Host " Performance: $scorePerformance / 25" +Write-Host " Maintainability: $scoreMaintainability / 25" +Write-Host "" + +if ($issuesFound.Count -gt 0) { + Write-Host " Issues Found:" + foreach ($issue in $issuesFound) { Write-Host " - $issue" } + Write-Host "" +} +if ($aiPositive.Count -gt 0) { + Write-Host " Positive Notes:" + foreach ($note in $aiPositive) { Write-Host " + $note" } + Write-Host "" +} +if ($aiRecommendations.Count -gt 0) { + Write-Host " Recommendations:" + foreach ($rec in $aiRecommendations) { Write-Host " > $rec" } + Write-Host "" +} + +# -- Step 5: 发布审查评论 -- +if ($aiAvailable) { $reviewHeader = "## AI Code Quality Review - PR #$PrId" } +else { $reviewHeader = "## Code Quality Review - PR #$PrId (keyword-based)" } + +$reviewBody = "$reviewHeader`n`n### Scores`n" +$reviewBody += "| Dimension | Score | Max |`n" +$reviewBody += "|-----------|-------|-----|`n" +$reviewBody += "| Code Quality | $scoreQuality | 25 |`n" +$reviewBody += "| Security | $scoreSecurity | 25 |`n" +$reviewBody += "| Performance | $scorePerformance | 25 |`n" +$reviewBody += "| Maintainability | $scoreMaintainability | 25 |`n" +$reviewBody += "| **Total** | **$totalScore** | **100** |`n`n" +$reviewBody += "### Issues Found`n" + +if ($issuesFound.Count -gt 0) { + foreach ($issue in $issuesFound) { $reviewBody += "- $issue`n" } +} else { $reviewBody += "No issues found.`n" } + +if ($aiPositive.Count -gt 0) { + $reviewBody += "`n### Positive Notes`n" + foreach ($note in $aiPositive) { $reviewBody += "- $note`n" } +} +if ($aiRecommendations.Count -gt 0) { + $reviewBody += "`n### Recommendations`n" + foreach ($rec in $aiRecommendations) { $reviewBody += "- $rec`n" } +} + +$verdictText = if ($totalScore -ge $Threshold) { + "**PASS** - Score $totalScore >= threshold $Threshold. Ready to merge." +} else { + "**FAIL** - Score $totalScore < threshold $Threshold. Please address the issues above." +} +$reviewBody += "`n### Verdict`n${verdictText}`n`n---`n*Auto-reviewed by gitlink-cli code-quality-gatekeeper workflow (skill: gitlink-code-review)*" + +Log-Step "Posting review comment..." +$reviewEvent = if ($totalScore -ge $Threshold) { "APPROVE" } else { "COMMENT" } +$reviewPayload = @{ body = $reviewBody; event = $reviewEvent } | ConvertTo-Json -Compress +$reviewResult = Invoke-GL api,POST,"/$Owner/$Repo/pulls/$PrId/reviews",--body,$reviewPayload +if ($reviewResult) { + try { $reviewOk = (($reviewResult | ConvertFrom-Json).ok -eq $true) } catch { $reviewOk = $false } + if ($reviewOk) { Log-Ok "Review posted" } + else { Log-Warn "Review post may have failed (review API might not be available)" } +} else { Log-Warn "Review post may have failed" } + +# -- Step 6: 检查 CI 状态 -- +Log-Step "Checking CI build status..." +$ciPresent = $false; $ciPassed = $true +$ciJson = Invoke-GL ci,+builds,--owner,$Owner,--repo,$Repo +if ($ciJson) { + try { + $ciData = $ciJson | ConvertFrom-Json + $builds = @() + if ($ciData.data.builds) { $builds = @($ciData.data.builds) } + elseif ($ciData.data -is [array]) { $builds = @($ciData.data) } + if ($builds.Count -gt 0) { + $ciPresent = $true + foreach ($b in $builds) { + $status = if ($b.status) { $b.status } elseif ($b.state) { $b.state } else { "unknown" } + $name = if ($b.name) { $b.name } else { "build" } + if ($status -notin @("success", "passed", "completed")) { + $ciPassed = $false; Log-Warn "CI '$name' status: $status" + } else { Log-Ok "CI '$name' status: $status" } + } + } + } catch { } +} +if (-not $ciPresent) { Log-Info "No CI builds found" } + +# -- Step 7: 质量达标 → 自动合并 -- +if ($totalScore -ge $Threshold -and $ciPassed) { + Log-Step "Quality score $totalScore >= $Threshold and CI passed" + if ($DryRun) { + Log-Warn "[DRY RUN] Would auto-merge PR #$PrId" + } else { + Log-Step "Auto-merging PR #$PrId..." + $mergeResult = Invoke-GL pr,+merge,--owner,$Owner,--repo,$Repo,--id,$PrId,--method,merge + if ($mergeResult) { + try { $mergeOk = (($mergeResult | ConvertFrom-Json).ok -eq $true) } catch { $mergeOk = $false } + if ($mergeOk) { Log-Ok "PR #$PrId merged successfully!" } + else { Log-Err "Auto-merge failed" } + } else { Log-Err "Auto-merge failed" } + } +} else { + Log-Warn "PR #$PrId not auto-merged (score: $totalScore, threshold: $Threshold, CI passed: $ciPassed)" +} + +# ── Complete ────────────────────────────────────────────────────── +Log-Title "Gatekeeper Complete" +Write-Host " PR: #$PrId - $prTitle" -ForegroundColor Green +Write-Host " Author: @$prAuthor" -ForegroundColor Green +Write-Host " Score: $totalScore / 100" -ForegroundColor Green +Write-Host " CI Passed: $ciPassed" -ForegroundColor Green diff --git a/workflows/02a-pr-gatekeeper.sh b/workflows/02a-pr-gatekeeper.sh new file mode 100644 index 00000000..0a97da3b --- /dev/null +++ b/workflows/02a-pr-gatekeeper.sh @@ -0,0 +1,603 @@ +#!/usr/bin/env bash +# ================================================================ +# Scenario 2a: Real-Time PR Gatekeeper (Webhook 触发) +# Flow: Webhook 推送 PR → 拉取详情 → AI Review → 查 CI → 发评论 → 达标自动合并 +# ================================================================ +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +PR_ID="" +OWNER="" +REPO="" +THRESHOLD=80 +DRY_RUN=false + +usage() { + echo "Usage: $0 --pr-id N [--owner OWNER] [--repo REPO] [--threshold SCORE] [--dry-run]" + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --pr-id) PR_ID="$2"; shift 2 ;; + --owner) OWNER="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --threshold) THRESHOLD="$2"; shift 2 ;; + --dry-run) DRY_RUN="true"; shift ;; + --help|-h) usage ;; + *) log_err "Unknown arg: $1"; usage ;; + esac +done + +[[ -z "$PR_ID" ]] && { log_err "--pr-id is required"; usage; } + +check_auth +require_owner_repo + +# ================================================================ +log_title "PR Gatekeeper: #$PR_ID ($OWNER/$REPO)" + +# ── Step 1: 拉取 PR 详情 ───────────────────────────────────────── +log_step "Fetching PR details..." +PR_JSON=$(gl_check pr +view --owner "$OWNER" --repo "$REPO" --id "$PR_ID") +[[ -z "$PR_JSON" ]] && { log_err "Failed to fetch PR #$PR_ID"; exit 1; } + +PR_TITLE=$(echo "$PR_JSON" | jq -r '.data.title // .data.subject // .data.issue.subject // "N/A"') +PR_STATE=$(echo "$PR_JSON" | jq -r '.data.state // .data.status // "N/A"') +PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.data.author.login // .data.author.username // .data.issue.author.login // "N/A"') +ISSUE_ID=$(echo "$PR_JSON" | jq -r '.data.id // .data.issue.id // empty') +PR_HEAD=$(echo "$PR_JSON" | jq -r '.data.pull_request.head // empty') +log_ok "PR #$PR_ID: \"$PR_TITLE\" by @$PR_AUTHOR (state: $PR_STATE, branch: $PR_HEAD)" + +# ── Step 2: 获取变更文件列表 ────────────────────────────────────── +log_step "Fetching changed files..." +FILES_JSON=$(gl_run pr +files --owner "$OWNER" --repo "$REPO" --id "$PR_ID") +FILE_COUNT=0 +if echo "$FILES_JSON" | jq empty 2>/dev/null; then + FILE_COUNT=$(echo "$FILES_JSON" | jq '.data.files | length' 2>/dev/null || echo "0") +fi +log_ok "Changed files: $FILE_COUNT" + +FILE_LIST="" +if [[ "$FILE_COUNT" -gt 0 ]] && [[ "$FILE_COUNT" != "null" ]]; then + for i in $(seq 0 $((FILE_COUNT - 1))); do + FNAME=$(echo "$FILES_JSON" | jq -r ".data.files[$i].name // .data.files[$i].filename // \"unknown\"" 2>/dev/null || echo "unknown") + echo " $FNAME" + FILE_LIST+="- $FNAME"$'\n' + done +fi + +# ── Step 3: 获取 diff ──────────────────────────────────────────── +log_step "Fetching diff..." +DIFF_JSON=$(gl_run pr +diff --owner "$OWNER" --repo "$REPO" --id "$PR_ID") +DIFF_CONTENT="" +if echo "$DIFF_JSON" | jq empty 2>/dev/null; then + DIFF_CONTENT=$(echo "$DIFF_JSON" | jq -r ' + [.data.files[]?.sections[]?.lines[]?.content // empty] | join("\n") + ' 2>/dev/null | head -c 5000 || true) +fi +DIFF_LINES=$(echo "$DIFF_CONTENT" | wc -l) +log_ok "Diff: $DIFF_LINES lines" + +# ── Step 4: AI 代码审查 ─────────────────────────────────────────── +log_step "AI analyzing code quality..." + +DIFF_TRUNCATED=$(echo "$DIFF_CONTENT" | head -c 4000) + +# Load skill dimensions from SKILL.md +SKILL_DIR="$SCRIPT_DIR/../skills/gitlink-code-review" +SKILL_DIMENSIONS="" +if [[ -f "$SKILL_DIR/SKILL.md" ]]; then + SKILL_DIMENSIONS=$(sed -n '/^## 📊 审查维度/,/^## 🔧 使用方式/p' "$SKILL_DIR/SKILL.md" | grep '^\- \*\*' | head -20) +fi + +REVIEW_PROMPT="你是代码审查专家。请按 gitlink-code-review skill 的审查维度分析以下 PR。 + +## 审查维度与检查项 + +${SKILL_DIMENSIONS:-1. 代码质量: 复杂度、命名、注释、格式 +2. 安全性: SQL注入、XSS、敏感信息、认证、输入验证 +3. 性能: 循环效率、资源泄漏、N+1查询、内存 +4. 可维护性: 代码重复、职责单一、依赖耦合、测试覆盖} + +## 评分标准 +- 90-100: 优秀,可直接合并 +- 75-89: 良好,建议合并 +- 60-74: 一般,需要改进 +- <60: 较差,不建议合并 + +## 问题严重级别 +- CRITICAL: 阻止合并 +- HIGH: 强烈建议修复 +- MEDIUM: 建议修复 +- LOW: 可选修复 + +## PR 数据 + +PR 标题: $PR_TITLE +变更文件: +$FILE_LIST +代码差异: +$DIFF_TRUNCATED + +## 输出要求 + +请严格按以下 JSON 格式输出,不要输出其他内容: +{\"total\": <0-100>, \"quality\": <0-25>, \"security\": <0-25>, \"performance\": <0-25>, \"maintainability\": <0-25>, \"issues\": [{\"severity\": \"HIGH/MEDIUM/LOW\", \"category\": \"quality/security/performance/maintainability\", \"file\": \"文件路径\", \"rule\": \"规则名\", \"description\": \"问题描述\", \"suggestion\": \"修复建议\"}], \"positive_notes\": [{\"description\": \"优秀实践描述\"}], \"recommendations\": [\"改进建议1\"], \"verdict\": \"PASS或FAIL\"}" + +AI_AVAILABLE=false +SCORE_QUALITY=25 +SCORE_SECURITY=25 +SCORE_PERFORMANCE=25 +SCORE_MAINTAINABILITY=25 +ISSUES_FOUND=() +AI_POSITIVE=() +AI_RECOMMENDATIONS=() +TOTAL_SCORE=0 + +if command -v claude &>/dev/null; then + log_info "Calling AI agent for code review (may take 30-60s)..." + PROMPT_FILE=$(mktemp) + AI_OUT_FILE=$(mktemp) + echo "$REVIEW_PROMPT" > "$PROMPT_FILE" + + if [[ -z "${CLAUDE_CODE_GIT_BASH_PATH:-}" ]] && command -v cygpath &>/dev/null; then + export CLAUDE_CODE_GIT_BASH_PATH="$(cygpath -w "$(which bash)")" + fi + + AI_EXIT=0 + (cat "$PROMPT_FILE" | timeout 300 claude -p --output-format json > "$AI_OUT_FILE" 2>/dev/null) || AI_EXIT=$? + + if [[ $AI_EXIT -eq 0 ]] && [[ -s "$AI_OUT_FILE" ]]; then + AI_RESULT=$(jq -r '.result // empty' "$AI_OUT_FILE" 2>/dev/null) + else + log_warn "AI call failed (exit: $AI_EXIT), falling back to keyword-based" + AI_RESULT="" + fi + rm -f "$PROMPT_FILE" "$AI_OUT_FILE" + + if [[ -n "$AI_RESULT" ]]; then + AI_JSON="" + if command -v python3 &>/dev/null; then + AI_JSON=$(python3 -c " +import sys, json +text = sys.stdin.read() +depth = 0 +start = -1 +results = [] +for i, c in enumerate(text): + if c == '{': + if depth == 0: start = i + depth += 1 + elif c == '}': + depth -= 1 + if depth == 0 and start >= 0: + results.append(text[start:i+1]) + start = -1 +for m in reversed(results): + try: + obj = json.loads(m) + if 'total' in obj and 'verdict' in obj: + print(json.dumps(obj)) + break + except: pass +" <<< "$AI_RESULT" 2>/dev/null) + fi + if [[ -z "$AI_JSON" ]]; then + AI_JSON=$(echo "$AI_RESULT" | grep -oP '\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}' | tail -1) + fi + + if [[ -n "$AI_JSON" ]] && echo "$AI_JSON" | jq empty 2>/dev/null; then + TOTAL_SCORE=$(echo "$AI_JSON" | jq -r '.total // 0' 2>/dev/null) + SCORE_QUALITY=$(echo "$AI_JSON" | jq -r '.quality // 0' 2>/dev/null) + SCORE_SECURITY=$(echo "$AI_JSON" | jq -r '.security // 0' 2>/dev/null) + SCORE_PERFORMANCE=$(echo "$AI_JSON" | jq -r '.performance // 0' 2>/dev/null) + SCORE_MAINTAINABILITY=$(echo "$AI_JSON" | jq -r '.maintainability // 0' 2>/dev/null) + AI_VERDICT=$(echo "$AI_JSON" | jq -r '.verdict // "PASS"' 2>/dev/null) + + ISSUE_COUNT=$(echo "$AI_JSON" | jq '.issues | length' 2>/dev/null || echo "0") + if [[ "$ISSUE_COUNT" -gt 0 ]] && [[ "$ISSUE_COUNT" != "null" ]]; then + for idx in $(seq 0 $((ISSUE_COUNT - 1))); do + issue=$(echo "$AI_JSON" | jq -r ' + if .issues['"$idx"'] | type == "object" then + "[" + (.issues['"$idx"'].severity // "?") + "] " + + (.issues['"$idx"'].category // "?") + ": " + + (.issues['"$idx"'].description // .issues['"$idx"'].rule // "unknown") + + (if .issues['"$idx"'].file then " (" + .issues['"$idx"'].file + ")" else "" end) + + (if .issues['"$idx"'].suggestion then " -> " + .issues['"$idx"'].suggestion else "" end) + else .issues['"$idx"'] // empty end' 2>/dev/null) + [[ -n "$issue" ]] && ISSUES_FOUND+=("$issue") + done + fi + + POS_COUNT=$(echo "$AI_JSON" | jq '.positive_notes | length' 2>/dev/null || echo "0") + if [[ "$POS_COUNT" -gt 0 ]] && [[ "$POS_COUNT" != "null" ]]; then + for idx in $(seq 0 $((POS_COUNT - 1))); do + note=$(echo "$AI_JSON" | jq -r '.positive_notes['"$idx"'].description // empty' 2>/dev/null) + [[ -n "$note" ]] && AI_POSITIVE+=("$note") + done + fi + + REC_COUNT=$(echo "$AI_JSON" | jq '.recommendations | length' 2>/dev/null || echo "0") + if [[ "$REC_COUNT" -gt 0 ]] && [[ "$REC_COUNT" != "null" ]]; then + for idx in $(seq 0 $((REC_COUNT - 1))); do + rec=$(echo "$AI_JSON" | jq -r '.recommendations['"$idx"'] // empty' 2>/dev/null) + [[ -n "$rec" ]] && AI_RECOMMENDATIONS+=("$rec") + done + fi + + AI_AVAILABLE=true + log_ok "AI review complete (verdict: $AI_VERDICT)" + else + log_warn "Could not parse AI response JSON, falling back to keyword-based" + fi + fi +fi + +# Fallback: keyword-based heuristics +if [[ "$AI_AVAILABLE" != "true" ]]; then + log_warn "AI not available, falling back to keyword-based analysis" + + SCORE_QUALITY=25 + SCORE_SECURITY=25 + SCORE_PERFORMANCE=25 + SCORE_MAINTAINABILITY=25 + ISSUES_FOUND=() + + if echo "$DIFF_CONTENT" | grep -qiE 'password|secret|token|api_key|apikey|private_key'; then + SCORE_SECURITY=$((SCORE_SECURITY - 15)) + ISSUES_FOUND+=("SECURITY: 检测到可能的硬编码凭证") + fi + if echo "$DIFF_CONTENT" | grep -qiE 'eval\(|exec\(|system\(|shell_exec|os\.system|subprocess\.call'; then + SCORE_SECURITY=$((SCORE_SECURITY - 10)) + ISSUES_FOUND+=("SECURITY: 检测到危险函数调用") + fi + if echo "$DIFF_CONTENT" | grep -qiE 'TODO|FIXME|HACK|XXX'; then + SCORE_QUALITY=$((SCORE_QUALITY - 5)) + ISSUES_FOUND+=("QUALITY: 存在 TODO/FIXME/HACK 注释") + fi + if echo "$DIFF_CONTENT" | grep -qiE 'SELECT \*|\.findAll\(\)|\.all\(\)'; then + SCORE_PERFORMANCE=$((SCORE_PERFORMANCE - 10)) + ISSUES_FOUND+=("PERFORMANCE: 可能的全表查询") + fi + if echo "$DIFF_CONTENT" | grep -qiE 'sleep\(|time\.sleep|Thread\.sleep'; then + SCORE_PERFORMANCE=$((SCORE_PERFORMANCE - 5)) + ISSUES_FOUND+=("PERFORMANCE: 检测到阻塞式 sleep") + fi + if [[ "$FILE_COUNT" -gt 20 ]]; then + SCORE_MAINTAINABILITY=$((SCORE_MAINTAINABILITY - 10)) + ISSUES_FOUND+=("MAINTAINABILITY: 变更文件数量过多 ($FILE_COUNT)") + fi + + TOTAL_SCORE=$((SCORE_QUALITY + SCORE_SECURITY + SCORE_PERFORMANCE + SCORE_MAINTAINABILITY)) + TOTAL_SCORE=$((TOTAL_SCORE < 0 ? 0 : TOTAL_SCORE)) +fi + +# ── 打印审查报告 ────────────────────────────────────────────────── +divider +if [[ "$AI_AVAILABLE" == "true" ]]; then + log_info "AI Review Report for PR #$PR_ID" +else + log_info "Review Report for PR #$PR_ID (keyword-based)" +fi +echo "" +echo " Overall Score: $TOTAL_SCORE / 100" +echo " Code Quality: $SCORE_QUALITY / 25" +echo " Security: $SCORE_SECURITY / 25" +echo " Performance: $SCORE_PERFORMANCE / 25" +echo " Maintainability: $SCORE_MAINTAINABILITY / 25" +echo "" + +if [[ ${#ISSUES_FOUND[@]} -gt 0 ]]; then + echo " Issues Found:" + for issue in "${ISSUES_FOUND[@]}"; do + echo " - $issue" + done + echo "" +fi + +if [[ "${#AI_POSITIVE[@]}" -gt 0 ]]; then + echo " Positive Notes:" + for note in "${AI_POSITIVE[@]}"; do + echo " + $note" + done + echo "" +fi + +if [[ "${#AI_RECOMMENDATIONS[@]}" -gt 0 ]]; then + echo " Recommendations:" + for rec in "${AI_RECOMMENDATIONS[@]}"; do + echo " > $rec" + done + echo "" +fi + +# ── Step 5: 发布审查评论 ────────────────────────────────────────── +if [[ "$AI_AVAILABLE" == "true" ]]; then + REVIEW_HEADER="## AI Code Quality Review - PR #$PR_ID" +else + REVIEW_HEADER="## Code Quality Review - PR #$PR_ID (keyword-based)" +fi + +REVIEW_BODY="$REVIEW_HEADER + +### Scores +| Dimension | Score | Max | +|-----------|-------|-----| +| Code Quality | $SCORE_QUALITY | 25 | +| Security | $SCORE_SECURITY | 25 | +| Performance | $SCORE_PERFORMANCE | 25 | +| Maintainability | $SCORE_MAINTAINABILITY | 25 | +| **Total** | **$TOTAL_SCORE** | **100** | + +### Issues Found" + +if [[ ${#ISSUES_FOUND[@]} -gt 0 ]]; then + for issue in "${ISSUES_FOUND[@]}"; do + REVIEW_BODY+=$'\n'"- $issue" + done +else + REVIEW_BODY+=$'\n'"No issues found." +fi + +if [[ "${#AI_POSITIVE[@]}" -gt 0 ]]; then + REVIEW_BODY+=$'\n'$'\n'"### Positive Notes" + for note in "${AI_POSITIVE[@]}"; do + REVIEW_BODY+=$'\n'"- $note" + done +fi + +if [[ "${#AI_RECOMMENDATIONS[@]}" -gt 0 ]]; then + REVIEW_BODY+=$'\n'$'\n'"### Recommendations" + for rec in "${AI_RECOMMENDATIONS[@]}"; do + REVIEW_BODY+=$'\n'"- $rec" + done +fi + +REVIEW_BODY+=" + +### Verdict +$(if [[ $TOTAL_SCORE -ge $THRESHOLD ]]; then echo "**PASS** - Score $TOTAL_SCORE >= threshold $THRESHOLD. Ready to merge."; else echo "**FAIL** - Score $TOTAL_SCORE < threshold $THRESHOLD. Please address the issues above."; fi) + +--- +*Auto-reviewed by gitlink-cli code-quality-gatekeeper workflow (skill: gitlink-code-review)*" + +log_step "Posting review comment..." +REVIEW_JSON=$(jq -n --arg notes "$REVIEW_BODY" '{notes: $notes}') +REVIEW_RESULT=$(gl_run api POST "/v1/$OWNER/$REPO/issues/$ISSUE_ID/journals" \ + --body "$REVIEW_JSON" 2>&1) || true + +if [[ "$(json_ok "$REVIEW_RESULT")" == "true" ]]; then + log_ok "Review comment posted" +else + log_warn "Review comment may have failed" +fi + +# ── Step 6: 本地 CI (编译 + 测试) ───────────────────────────────── +log_step "Running local CI: build & test..." + +REPOS_DIR="/opt/gitlink-webhook/repos" +REPO_CLONE_DIR="$REPOS_DIR/$REPO" +GIT_URL="https://www.gitlink.org.cn/${OWNER}/${REPO}.git" + +BUILD_LOG=$(mktemp) +TEST_LOG=$(mktemp) +CI_BUILD_PASSED=false +CI_TEST_PASSED=false +CI_PASSED=false + +if [[ -z "$PR_HEAD" ]]; then + log_warn "Cannot determine PR head branch, skipping CI" + CI_BUILD_OUTPUT="CI skipped: unknown PR branch" + CI_TEST_OUTPUT="CI skipped: unknown PR branch" +else + # Clone repo if not exists, otherwise fetch latest + if [[ ! -d "$REPO_CLONE_DIR/.git" ]]; then + mkdir -p "$REPOS_DIR" + log_info "Cloning $GIT_URL..." + git clone "$GIT_URL" "$REPO_CLONE_DIR" 2>&1 || true + fi + + { + cd "$REPO_CLONE_DIR" + log_info "Fetching and checking out branch '$PR_HEAD'..." + git fetch origin 2>&1 || true + git checkout "$PR_HEAD" 2>&1 || true + git pull origin "$PR_HEAD" 2>&1 || true + + log_info "Running: go build ./..." + if go build ./... >> "$BUILD_LOG" 2>&1; then + CI_BUILD_PASSED=true + log_ok "Build passed" + else + log_err "Build failed" + fi + + log_info "Running: go test ./..." + if go test ./... >> "$TEST_LOG" 2>&1; then + CI_TEST_PASSED=true + log_ok "Tests passed" + else + log_err "Tests failed" + fi + } + + CI_BUILD_OUTPUT=$(tail -c 3000 "$BUILD_LOG" 2>/dev/null || echo "no output") + CI_TEST_OUTPUT=$(tail -c 3000 "$TEST_LOG" 2>/dev/null || echo "no output") +fi + +if $CI_BUILD_PASSED && $CI_TEST_PASSED; then + CI_PASSED=true + CI_SUMMARY="| Build | PASSED | | |\n| Tests | PASSED | | |" +elif $CI_BUILD_PASSED && ! $CI_TEST_PASSED; then + CI_SUMMARY="| Build | PASSED | | |\n| Tests | **FAILED** | | |" +elif ! $CI_BUILD_PASSED && $CI_TEST_PASSED; then + CI_SUMMARY="| Build | **FAILED** | | |\n| Tests | PASSED | | |" +else + CI_SUMMARY="| Build | **FAILED** | | |\n| Tests | **FAILED** | | |" +fi + +# Post CI results as a follow-up comment +log_step "Posting CI results..." +CI_BODY="## CI Results - PR #$PR_ID + +### Build & Test +| Stage | Status | +|-------|--------| +$CI_SUMMARY + +### Build Output +\`\`\` +$(echo "$CI_BUILD_OUTPUT" | tail -20) +\`\`\` + +### Test Output +\`\`\` +$(echo "$CI_TEST_OUTPUT" | tail -20) +\`\`\` + +--- +*Local CI executed on server*" + +CI_COMMENT_JSON=$(jq -n --arg notes "$CI_BODY" '{notes: $notes}') +CI_RESULT=$(gl_run api POST "/v1/$OWNER/$REPO/issues/$ISSUE_ID/journals" \ + --body "$CI_COMMENT_JSON" 2>&1) || true + +if [[ "$(json_ok "$CI_RESULT")" == "true" ]]; then + log_ok "CI results posted" +else + log_warn "CI results post may have failed" +fi + +rm -f "$BUILD_LOG" "$TEST_LOG" + +# ── Step 7: 冲突检测 + 自动合并 ─────────────────────────────────── +MERGED=false +MERGE_RESULT_MSG="" +CONFLICT_DETECTED=false + +# Build merge summary: determine why merge can/cannot proceed +MERGE_BLOCK_REASONS=() +if [[ $TOTAL_SCORE -lt $THRESHOLD ]]; then + MERGE_BLOCK_REASONS+=("score $TOTAL_SCORE < threshold $THRESHOLD") +fi +if [[ "$CI_PASSED" != "true" ]]; then + MERGE_BLOCK_REASONS+=("CI not passed") +fi + +if [[ ${#MERGE_BLOCK_REASONS[@]} -gt 0 ]]; then + # Cannot merge: log reasons + BLOCK_LIST=$(printf '; %s' "${MERGE_BLOCK_REASONS[@]}") + BLOCK_LIST="${BLOCK_LIST:2}" + log_warn "Merge blocked: $BLOCK_LIST" +else + log_step "Score $TOTAL_SCORE >= $THRESHOLD and CI passed — checking mergeability..." + + # Check for merge conflicts + if [[ -d "$REPO_CLONE_DIR/.git" ]]; then + log_info "Checking merge conflicts with base branch..." + ( + cd "$REPO_CLONE_DIR" + # Ensure git identity is set for merge test + git config user.email "gatekeeper@gitlink-cli.local" 2>&1 || true + git config user.name "gitlink-gatekeeper" 2>&1 || true + git fetch origin 2>&1 || true + # Determine base branch from PR data + BASE_BRANCH=$(echo "$PR_JSON" | jq -r '.data.pull_request.base // .data.base // "master"') + git checkout "$BASE_BRANCH" 2>&1 || true + git pull origin "$BASE_BRANCH" 2>&1 || true + # Test merge without committing + if git merge --no-commit --no-ff "$PR_HEAD" 2>&1; then + log_ok "No merge conflicts detected" + git merge --abort 2>&1 || true + CONFLICT_DETECTED=false + else + CONFLICT_DETECTED=true + CONFLICT_FILES=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "unknown") + log_err "Merge conflict detected in: $CONFLICT_FILES" + git merge --abort 2>&1 || true + fi + git checkout "$PR_HEAD" 2>&1 || true + ) + else + log_info "Skipping conflict check (repo not cloned)" + fi + + if [[ "$CONFLICT_DETECTED" == "true" ]]; then + MERGE_RESULT_MSG="Merge blocked: conflicts detected" + log_err "$MERGE_RESULT_MSG" + elif [[ "$DRY_RUN" == "true" ]]; then + MERGE_RESULT_MSG="[DRY RUN] Would auto-merge" + log_warn "$MERGE_RESULT_MSG" + else + log_step "Auto-merging PR #$PR_ID..." + MERGE_OUT=$(gl_run pr +merge --owner "$OWNER" --repo "$REPO" --id "$PR_ID" --method merge 2>&1) || true + if [[ "$(json_ok "$MERGE_OUT")" == "true" ]]; then + MERGED=true + MERGE_RESULT_MSG="Merge succeeded" + log_ok "PR #$PR_ID merged successfully!" + else + MERGE_RESULT_MSG="Merge failed: $(json_error "$MERGE_OUT")" + log_err "$MERGE_RESULT_MSG" + fi + fi +fi + +# ── Step 8: 发布最终汇总评论 ────────────────────────────────────── +log_step "Posting final summary..." + +FINAL_BODY="## Gatekeeper Summary - PR #$PR_ID + +| Item | Detail | +|------|--------| +| Score | **$TOTAL_SCORE / 100** (threshold: $THRESHOLD) | +| CI Build | $([[ "$CI_BUILD_PASSED" == "true" ]] && echo "PASSED" || echo "FAILED") | +| CI Test | $([[ "$CI_TEST_PASSED" == "true" ]] && echo "PASSED" || echo "FAILED") | +| Merge Conflict | $([[ "$CONFLICT_DETECTED" == "true" ]] && echo "**DETECTED**" || echo "None") | +| Result | $([[ "$MERGED" == "true" ]] && echo "**MERGED**" || echo "$MERGE_RESULT_MSG") | +" + +if [[ ${#MERGE_BLOCK_REASONS[@]} -gt 0 ]]; then + FINAL_BODY+=" +### Blocked Reasons +" + for reason in "${MERGE_BLOCK_REASONS[@]}"; do + FINAL_BODY+="- $reason +" + done +fi + +if [[ "$CONFLICT_DETECTED" == "true" ]]; then + FINAL_BODY+=" +### Conflict Files +\`\`\` +$CONFLICT_FILES +\`\`\` +" +fi + +FINAL_BODY+=" +--- +*Gatekeeper workflow completed at $(date '+%Y-%m-%d %H:%M:%S')*" + +FINAL_JSON=$(jq -n --arg notes "$FINAL_BODY" '{notes: $notes}') +FINAL_RESULT=$(gl_run api POST "/v1/$OWNER/$REPO/issues/$ISSUE_ID/journals" \ + --body "$FINAL_JSON" 2>&1) || true + +if [[ "$(json_ok "$FINAL_RESULT")" == "true" ]]; then + log_ok "Final summary posted" +else + log_warn "Final summary post may have failed" +fi + +# ── Complete ────────────────────────────────────────────────────── +log_title "Gatekeeper Complete" +echo -e "${GREEN} PR: #$PR_ID - $PR_TITLE${NC}" +echo -e "${GREEN} Author: @$PR_AUTHOR${NC}" +echo -e "${GREEN} Score: $TOTAL_SCORE / 100${NC}" +echo -e "${GREEN} CI Passed: $CI_PASSED${NC}" +echo -e "${GREEN} Conflict: $CONFLICT_DETECTED${NC}" +echo -e "${GREEN} Merged: $( [[ "$MERGED" == "true" ]] && echo 'YES' || echo 'NO')${NC}" diff --git a/workflows/03-project-init.ps1 b/workflows/03-project-init.ps1 index f9c9a502..ff998cb5 100644 --- a/workflows/03-project-init.ps1 +++ b/workflows/03-project-init.ps1 @@ -1,25 +1,25 @@ -# ---------------------------------------------------------------- +# ---------------------------------------------------------------- # Scenario 3: One-Click Project Initialization -# Flow: Input description -> Create repo -> README/CONTRIBUTING/CI config -> -# Initial Issues -> Branch protection -> Initial Release +# Flow: Input description -> Create repo -> Generate files (README/LICENSE/ +# CONTRIBUTING/CI) -> Push to repo -> Milestones -> Issues -> +# Branch protection -> Initial Release # # Commands chained: # 1. repo +create -- create repository -# 2. wiki +create -- create README wiki page -# 3. wiki +create -- create CONTRIBUTING guide -# 4. wiki +create -- create CI/CD config guide -# 5. issue +create -- create initial issues -# 6. branch +protect -- protect master branch -# 7. release +create -- create initial release +# 2. git clone -- clone the empty repo locally +# 3. file generation -- README.md, LICENSE, CONTRIBUTING.md, .gitlink-ci.yml +# 4. git add/commit/push -- push scaffold files to repo +# 5. milestone +create -- create project milestones +# 6. issue +create -- create initial issues +# 7. branch +protect -- protect default branch +# 8. release +create -- create initial release # ---------------------------------------------------------------- #Requires -Version 5.1 param( [string]$Owner = "", - [Parameter(Mandatory=$true)] - [string]$Name, - [Parameter(Mandatory=$true)] - [string]$Description, + [string]$Name = "", + [string]$Description = "", [string]$Lang = "go", [switch]$Private, [switch]$DryRun, @@ -27,13 +27,16 @@ param( ) $ErrorActionPreference = "Stop" -Import-Module "$PSScriptRoot/lib/common.psm1" -Force +Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue if ($Help) { Write-Host "Usage: powershell 03-project-init.ps1 -Owner OWNER -Name REPO_NAME -Description DESC [-Lang go|python|node|java] [-Private] [-DryRun]" exit 0 } +if (-not $Name) { Log-Err "Name is required (use -Help for usage)"; exit 1 } +if (-not $Description) { Log-Err "Description is required (use -Help for usage)"; exit 1 } + Check-Auth if (-not $Owner) { $detected = Detect-OwnerRepo @@ -51,180 +54,470 @@ Write-Host " Private: $($Private.IsPresent)" Divider # -- Step 1: Create Repository -- -Log-Step "Creating repository..." -$privateStr = if ($Private) { "true" } else { "false" } -$repoResult = Invoke-GLCheck "repo", "+create", "--owner", $Owner, "--name", $Name, "--description", $Description, "--private", $privateStr -if ($repoResult) { - Log-Ok "Repository created: $Owner/$Name" +Log-Step "Step 1/8: Creating repository..." +if ($DryRun) { + Log-Warn "[DRY RUN] Would create repository: $Owner/$Name" } else { - Log-Err "Repository creation failed" - exit 1 + $privateStr = if ($Private) { "true" } else { "false" } + $repoResult = Invoke-GLCheck "repo", "+create", "--owner", $Owner, "--name", $Name, "--description", $Description, "--private", $privateStr + if ($repoResult) { + Log-Ok "Repository created: $Owner/$Name" + } else { + Log-Err "Repository creation failed" + exit 1 + } } -# -- Step 2: Create README -- -Log-Step "Creating README wiki page..." -Start-Sleep -Seconds 2 +# -- Step 2: Clone empty repo -- +Log-Step "Step 2/8: Cloning repository..." +$repoUrl = "https://gitlink.org.cn/$Owner/$Name.git" +if ($env:GITLINK_TOKEN) { + $cloneUrl = "https://oauth2:$($env:GITLINK_TOKEN)@gitlink.org.cn/$Owner/$Name.git" +} else { + $cloneUrl = $repoUrl +} +$workDir = Join-Path $env:TEMP "gitlink-init-$([System.Guid]::NewGuid().ToString('N').Substring(0, 8))" -$langSection = switch ($Lang) { - "go" { - "### Prerequisites`n- Go 1.21+`n- Git`n`n### Installation`n``````bash`ngit clone https://gitlink.org.cn/$Owner/$Name.git`ncd $Name`ngo mod download`ngo build ./...`n```````n`n### Usage`n``````bash`ngo run main.go`n```````n`n### Testing`n``````bash`ngo test ./...`n``````" - } - "python" { - "### Prerequisites`n- Python 3.9+`n- pip`n`n### Installation`n``````bash`ngit clone https://gitlink.org.cn/$Owner/$Name.git`ncd $Name`npip install -r requirements.txt`n```````n`n### Usage`n``````bash`npython main.py`n```````n`n### Testing`n``````bash`npytest`n``````" - } - "node" { - "### Prerequisites`n- Node.js 18+`n- npm or yarn`n`n### Installation`n``````bash`ngit clone https://gitlink.org.cn/$Owner/$Name.git`ncd $Name`nnpm install`n```````n`n### Usage`n``````bash`nnpm start`n```````n`n### Testing`n``````bash`nnpm test`n``````" - } - "java" { - "### Prerequisites`n- JDK 17+`n- Maven 3.8+`n`n### Installation`n``````bash`ngit clone https://gitlink.org.cn/$Owner/$Name.git`ncd $Name`nmvn clean install`n```````n`n### Usage`n``````bash`nmvn exec:java`n```````n`n### Testing`n``````bash`nmvn test`n``````" - } - default { "" } +if ($DryRun) { + Log-Warn "[DRY RUN] Would clone: $repoUrl -> $workDir" +} else { + git clone $cloneUrl $workDir 2>&1 | Select-Object -Last 1 + Log-Ok "Cloned to $workDir" } -$readmeContent = "# $Name" + "`n`n" -$readmeContent += "$Description" + "`n`n" -$readmeContent += "## Getting Started" + "`n`n" -$readmeContent += $langSection + "`n`n" -$readmeContent += "## Contributing" + "`n`n" -$readmeContent += "See [CONTRIBUTING](./CONTRIBUTING) for guidelines." + "`n`n" -$readmeContent += "## License" + "`n`n" -$readmeContent += "This project is licensed under the MIT License." +# -- Step 3: Generate project files -- +Log-Step "Step 3/8: Generating project scaffold files..." -$wikiOk = $false -for ($attempt = 1; $attempt -le 3; $attempt++) { -<<<<<<< HEAD - $wikiResult = Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"README",--content,$readmeContent - if ($wikiResult -and (Get-JsonOk ($wikiResult | ConvertFrom-Json))) { -======= - $wikiResult = Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Name, "--title", "README", "--content", $readmeContent - if ($wikiResult -and (Get-JsonOk $wikiResult)) { ->>>>>>> master - Log-Ok "README created" - $wikiOk = $true - break +function Get-ReadmeContent { + $langSection = switch ($Lang) { + "go" { +@" +### 环境要求 + +- Go 1.21+ +- Git + +### 安装 + +```bash +git clone https://gitlink.org.cn/$Owner/$Name.git +cd $Name +go mod download +go build ./... +``` + +### 使用 + +```bash +go run main.go +``` + +### 测试 + +```bash +go test ./... +``` +"@ + } + "python" { +@" +### 环境要求 + +- Python 3.9+ +- pip + +### 安装 + +```bash +git clone https://gitlink.org.cn/$Owner/$Name.git +cd $Name +pip install -r requirements.txt +``` + +### 使用 + +```bash +python main.py +``` + +### 测试 + +```bash +pytest +``` +"@ + } + "node" { +@" +### 环境要求 + +- Node.js 18+ +- npm or yarn + +### 安装 + +```bash +git clone https://gitlink.org.cn/$Owner/$Name.git +cd $Name +npm install +``` + +### 使用 + +```bash +npm start +``` + +### 测试 + +```bash +npm test +``` +"@ + } + "java" { +@" +### 环境要求 + +- JDK 17+ +- Maven 3.8+ + +### 安装 + +```bash +git clone https://gitlink.org.cn/$Owner/$Name.git +cd $Name +mvn clean install +``` + +### 使用 + +```bash +mvn exec:java +``` + +### 测试 + +```bash +mvn test +``` +"@ + } + default { "" } } - if ($attempt -lt 3) { Start-Sleep -Seconds 2 } + + return @" +# $Name + +$Description + +## 快速开始 + +$langSection + +## 贡献指南 + +详见 [CONTRIBUTING](./CONTRIBUTING.md)。 + +## 许可证 + +本项目基于 MIT 许可证开源 — 详见 [LICENSE](./LICENSE)。 +"@ } -if (-not $wikiOk) { Log-Warn "README wiki creation may have failed" } -# -- Step 3: Create CONTRIBUTING Guide -- -Log-Step "Creating CONTRIBUTING guide..." +function Get-LicenseContent { + $year = (Get-Date).Year + return @" +MIT License -$contribContent = "# Contributing to $Name" + "`n`n" -$contribContent += "Thank you for your interest in contributing!" + "`n`n" -$contribContent += "## How to Contribute" + "`n`n" -$contribContent += "1. Fork the repository" + "`n" -$contribContent += "2. Create a feature branch: ``git checkout -b feature/my-feature```n" -$contribContent += "3. Make your changes" + "`n" -$contribContent += "4. Run tests to ensure everything passes" + "`n" -$contribContent += "5. Commit your changes: ``git commit -m 'feat: add my feature'```n" -$contribContent += "6. Push to your fork: ``git push origin feature/my-feature```n" -$contribContent += "7. Create a Pull Request" + "`n`n" -$contribContent += "## Code Style" + "`n`n" -$contribContent += "- Follow the existing code style" + "`n" -$contribContent += "- Write meaningful commit messages" + "`n" -$contribContent += "- Add tests for new features" + "`n" -$contribContent += "- Update documentation as needed" + "`n`n" -$contribContent += "## Reporting Issues" + "`n`n" -$contribContent += "- Use the issue tracker" + "`n" -$contribContent += "- Include reproduction steps" + "`n" -$contribContent += "- Include environment details" +Copyright (c) $year $Owner -$wikiOk = $false -for ($attempt = 1; $attempt -le 3; $attempt++) { -<<<<<<< HEAD - $wikiContrib = Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"CONTRIBUTING",--content,$contribContent - if ($wikiContrib -and (Get-JsonOk ($wikiContrib | ConvertFrom-Json))) { -======= - $wikiContrib = Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Name, "--title", "CONTRIBUTING", "--content", $contribContent - if ($wikiContrib -and (Get-JsonOk $wikiContrib)) { ->>>>>>> master - Log-Ok "CONTRIBUTING guide created" - $wikiOk = $true - break - } - if ($attempt -lt 3) { Start-Sleep -Seconds 2 } +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +"@ } -if (-not $wikiOk) { Log-Warn "CONTRIBUTING wiki creation may have failed" } -# -- Step 4: Create CI Config Guide -- -Log-Step "Creating CI/CD configuration guide..." +function Get-ContributingContent { + return @" +# Contributing to $Name -$ciContent = "# CI/CD Configuration" + "`n`n" -$ciContent += "## GitLink CI Setup" + "`n`n" -$ciContent += "This project uses GitLink CI for continuous integration." + "`n`n" -$ciContent += "### Pipeline Stages" + "`n`n" -$ciContent += "1. **Test**: Run unit tests" + "`n" -$ciContent += "2. **Build**: Build the project" + "`n" -$ciContent += "3. **Deploy**: Deploy to staging (master branch only)" + "`n`n" -$ciContent += "### Configuration" + "`n`n" -$ciContent += "Create a ``.gitlink-ci.yml`` file in the repository root." +Thank you for your interest in contributing! -<<<<<<< HEAD -Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"CI/CD Configuration",--content,$ciContent | Out-Null -======= -Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Name, "--title", "CI/CD Configuration", "--content", $ciContent | Out-Null ->>>>>>> master -Log-Ok "CI/CD configuration guide created" +## How to Contribute -# -- Step 5: Create Initial Issues -- -Log-Step "Creating initial issues..." +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/my-feature` +3. Make your changes +4. Run tests to ensure everything passes +5. Commit your changes: `git commit -m 'feat: add my feature'` +6. Push to your fork: `git push origin feature/my-feature` +7. Create a Pull Request -$issuesToCreate = @( - @{ Title = "Setup CI/CD Pipeline"; Body = "Configure continuous integration and deployment for the project.`n`n## Tasks`n- [ ] Create .gitlink-ci.yml configuration`n- [ ] Setup test stage`n- [ ] Setup build stage`n- [ ] Setup deploy stage`n- [ ] Add status badge to README"; Label = "feature" }, - @{ Title = "Write Project Documentation"; Body = "Complete project documentation including API docs and architecture guide.`n`n## Tasks`n- [ ] Write API documentation`n- [ ] Create architecture diagram`n- [ ] Add usage examples`n- [ ] Document configuration options"; Label = "documentation" }, - @{ Title = "Setup Code Review Process"; Body = "Establish code review guidelines and automation.`n`n## Tasks`n- [ ] Define review checklist`n- [ ] Setup branch protection rules`n- [ ] Configure required reviewers`n- [ ] Document review process"; Label = "enhancement" }, - @{ Title = "Add Unit Tests"; Body = "Add comprehensive unit test coverage for core modules.`n`n## Tasks`n- [ ] Setup test framework`n- [ ] Write tests for core modules`n- [ ] Achieve 80 percent code coverage`n- [ ] Add CI test integration"; Label = "enhancement" }, - @{ Title = "Setup Dependency Management"; Body = "Configure dependency scanning and updates.`n`n## Tasks`n- [ ] Setup dependency scanner`n- [ ] Configure automatic updates`n- [ ] Add license compliance check`n- [ ] Document dependency policy"; Label = "security" } -) +## Code Style -foreach ($entry in $issuesToCreate) { - $issueResult = Invoke-GL "issue", "+create", "--owner", $Owner, "--repo", $Name, "--title", $entry.Title, "--body", $entry.Body - if ($issueResult) { - try { - $issueJson = $issueResult - $issueNum = if ($issueJson.data.id) { $issueJson.data.id } elseif ($issueJson.data.number) { $issueJson.data.number } else { $null } - if ($issueNum) { - Invoke-GL "issue", "+label-add", "--owner", $Owner, "--repo", $Name, "--number", $issueNum, "--labels", $entry.Label | Out-Null - Log-Ok "Issue created: #$issueNum - $($entry.Title)" - } - } catch { - Log-Warn "Issue creation may have failed: $($entry.Title)" +- Follow the existing code style +- Write meaningful commit messages following [Conventional Commits](https://www.conventionalcommits.org/) +- Add tests for new features +- Update documentation as needed + +## Reporting Issues + +- Use the [issue tracker](https://gitlink.org.cn/$Owner/$Name/issues) +- Include steps to reproduce +- Include environment details (OS, language version, etc.) + +## Code of Conduct + +Please be respectful and constructive in all interactions. +"@ +} + +function Get-CIConfigContent { + switch ($Lang) { + "go" { + return @" +image: golang:1.21 + +stages: + - test + - build + +test: + stage: test + script: + - go mod download + - go test ./... -v -cover + +build: + stage: build + script: + - go build ./... +"@ + } + "python" { + return @" +image: python:3.9 + +stages: + - test + - lint + +test: + stage: test + script: + - pip install -r requirements.txt + - pytest -v --cov + +lint: + stage: lint + script: + - pip install flake8 + - flake8 . +"@ + } + "node" { + return @" +image: node:18 + +stages: + - test + - build + +test: + stage: test + script: + - npm ci + - npm test -- --coverage + +build: + stage: build + script: + - npm ci + - npm run build +"@ + } + "java" { + return @" +image: maven:3.8-openjdk-17 + +stages: + - test + - build + +test: + stage: test + script: + - mvn test + +build: + stage: build + script: + - mvn package -DskipTests +"@ + } + default { + return "# TODO: configure CI/CD pipeline for $Lang" } } } -# -- Step 6: Protect Default Branch -- -Log-Step "Protecting master branch..." -$protectResult = Invoke-GL "branch", "+protect", "--owner", $Owner, "--repo", $Name, "--name", "master" -if ($protectResult -and (Get-JsonOk $protectResult)) { - Log-Ok "Branch 'master' protected" +if ($DryRun) { + Log-Warn "[DRY RUN] Would generate: README.md, LICENSE, CONTRIBUTING.md, .gitlink-ci.yml" } else { - Log-Warn "Branch protection may have failed (may require admin permissions)" + $utf8 = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText("$workDir\README.md", (Get-ReadmeContent), $utf8) + [System.IO.File]::WriteAllText("$workDir\LICENSE", (Get-LicenseContent), $utf8) + [System.IO.File]::WriteAllText("$workDir\CONTRIBUTING.md", (Get-ContributingContent), $utf8) + [System.IO.File]::WriteAllText("$workDir\.gitlink-ci.yml", (Get-CIConfigContent), $utf8) + Log-Ok "Generated: README.md, LICENSE, CONTRIBUTING.md, .gitlink-ci.yml" } -# -- Step 7: Create Initial Release -- -Log-Step "Creating initial release v0.1.0..." - -$releaseBody = "# v0.1.0 - Initial Release" + "`n`n" -$releaseBody += "## What's New" + "`n" -$releaseBody += "- Project initialized with $Lang template" + "`n" -$releaseBody += "- README and CONTRIBUTING guides created" + "`n" -$releaseBody += "- CI/CD configuration guide created" + "`n" -$releaseBody += "- 5 initial issues filed" + "`n" -$releaseBody += "- Branch protection enabled" + "`n`n" -$releaseBody += "## Next Steps" + "`n" -$releaseBody += "- [ ] Setup CI/CD pipeline" + "`n" -$releaseBody += "- [ ] Write comprehensive tests" + "`n" -$releaseBody += "- [ ] Complete documentation" + "`n" -$releaseBody += "- [ ] First feature implementation" + "`n`n" -$releaseBody += "---`n*Auto-initialized by gitlink-cli project-init workflow*" - -$releaseResult = Invoke-GL "release", "+create", "--owner", $Owner, "--repo", $Name, "--tag", "v0.1.0", "--name", "Initial Release", "--body", $releaseBody -if ($releaseResult -and (Get-JsonOk $releaseResult)) { - Log-Ok "Release v0.1.0 created" +# -- Step 4: Push files to repo -- +Log-Step "Step 4/8: Pushing scaffold files to repository..." +if ($DryRun) { + Log-Warn "[DRY RUN] Would git add/commit/push to $repoUrl" } else { - Log-Warn "Release creation may have failed" + Push-Location $workDir + try { + $commitMsg = "chore: initialize project scaffold`n`n- Add README.md with getting-started guide`n- Add MIT LICENSE`n- Add CONTRIBUTING.md with guidelines`n- Add .gitlink-ci.yml CI pipeline configuration" + git add README.md LICENSE CONTRIBUTING.md .gitlink-ci.yml 2>&1 | Out-Null + git -c user.name="$Owner" -c user.email="$Owner@gitlink.org.cn" commit -m $commitMsg 2>&1 | Select-Object -Last 1 + git push -u origin master 2>&1 | Select-Object -Last 1 + Log-Ok "Scaffold files pushed to master" + } finally { + Pop-Location + } +} + +# -- Step 5: Create Milestones -- +Log-Step "Step 5/8: Creating initial milestone..." + +$milestones = @( + @{ Name = "v1.0.0 - First Release"; Description = "First official release milestone" } +) + +if ($DryRun) { + Log-Warn "[DRY RUN] Would create $($milestones.Count) milestones" +} else { + foreach ($ms in $milestones) { + $msResult = Invoke-GL "milestone", "+create", "--owner", $Owner, "--repo", $Name, "--name", $ms.Name, "--description", $ms.Description + if ($msResult) { + try { + $msJson = $msResult | ConvertFrom-Json + if ($msJson.ok) { + Log-Ok "Milestone created: $($ms.Name)" + } else { + Log-Warn "Milestone creation failed: $($ms.Name)" + } + } catch { + Log-Warn "Milestone creation may have failed: $($ms.Name)" + } + } + } +} + +# -- Step 6: Create Initial Issues -- +Log-Step "Step 6/8: Creating welcome issue..." + +$issuesToCreate = @( + @{ Title = "Welcome to $Name"; Body = "欢迎来到 **${Name}** 的仓库,这是本仓库的第一条issue!!"; Label = "welcome" } +) + +if ($DryRun) { + Log-Warn "[DRY RUN] Would create $($issuesToCreate.Count) initial issues" +} else { + foreach ($entry in $issuesToCreate) { + $issueResult = Invoke-GL "issue", "+create", "--owner", $Owner, "--repo", $Name, "--title", $entry.Title, "--body", $entry.Body + if ($issueResult) { + try { + $issueJson = $issueResult | ConvertFrom-Json + $issueNum = if ($issueJson.ok -and $issueJson.data.id) { $issueJson.data.id } elseif ($issueJson.ok -and $issueJson.data.number) { $issueJson.data.number } else { $null } + if ($issueNum) { + Invoke-GL "issue", "+label-add", "--owner", $Owner, "--repo", $Name, "--number", $issueNum, "--labels", $entry.Label | Out-Null + Log-Ok "Issue created: #$issueNum - $($entry.Title)" + } + } catch { + Log-Warn "Issue creation may have failed: $($entry.Title)" + } + } + } +} + +# -- Step 7: Protect Default Branch -- +Log-Step "Step 7/8: Protecting master branch..." +if ($DryRun) { + Log-Warn "[DRY RUN] Would protect branch 'master'" +} else { + $protectResult = Invoke-GL "branch", "+protect", "--owner", $Owner, "--repo", $Name, "--name", "master" + if ($protectResult) { + try { + $protectJson = $protectResult | ConvertFrom-Json + if ($protectJson.ok) { + Log-Ok "Branch 'master' protected" + } else { + Log-Warn "Branch protection may have failed (may require admin permissions)" + } + } catch { + Log-Warn "Branch protection may have failed (may require admin permissions)" + } + } +} + +# -- Step 8: Create Initial Release -- +Log-Step "Step 8/8: Creating initial release v0.1.0..." + +$releaseBody = @" +# v0.1.0 - Initial Release + +## What's New +- Project initialized with $Lang template +- README, LICENSE, CONTRIBUTING guide in repository +- CI/CD pipeline configuration (`.gitlink-ci.yml`) +- $($milestones.Count) project milestone created +- $($issuesToCreate.Count) initial issues filed +- Branch protection enabled + +## Next Steps +- [ ] Setup CI/CD pipeline +- [ ] Write comprehensive tests +- [ ] Complete documentation +- [ ] First feature implementation + +--- +*Auto-initialized by gitlink-cli project-init workflow* +"@ + +if ($DryRun) { + Log-Warn "[DRY RUN] Would create release v0.1.0" +} else { + $releaseResult = Invoke-GL "release", "+create", "--owner", $Owner, "--repo", $Name, "--tag", "v0.1.0", "--name", "Initial Release", "--body", $releaseBody + if ($releaseResult) { + try { + $releaseJson = $releaseResult | ConvertFrom-Json + if ($releaseJson.ok) { + Log-Ok "Release v0.1.0 created" + } else { + Log-Warn "Release creation may have failed" + } + } catch { + Log-Warn "Release creation may have failed" + } + } } # ---------------------------------------------------------------- @@ -232,14 +525,18 @@ Log-Title "Project Initialization Complete" # ---------------------------------------------------------------- Write-Host " Repository: $Owner/$Name" -ForegroundColor Green -Write-Host " README: Wiki page" -ForegroundColor Green -Write-Host " CONTRIBUTING: Wiki page" -ForegroundColor Green -Write-Host " CI/CD Guide: Wiki page" -ForegroundColor Green -Write-Host " Issues: 5 initial issues" -ForegroundColor Green +Write-Host " Files: README.md, LICENSE, CONTRIBUTING.md, .gitlink-ci.yml" -ForegroundColor Green +Write-Host " Milestones: $($milestones.Count) milestone" -ForegroundColor Green +Write-Host " Issues: $($issuesToCreate.Count) initial issues" -ForegroundColor Green Write-Host " Branch: master (protected)" -ForegroundColor Green Write-Host " Release: v0.1.0" -ForegroundColor Green Write-Host "" Write-Host "Next steps:" -ForegroundColor Cyan -Write-Host " 1. Clone: git clone https://gitlink.org.cn/$Owner/$Name.git" -Write-Host " 2. Add your code and push" -Write-Host " 3. Setup CI/CD by closing the first issue" +Write-Host " 1. Clone: git clone $repoUrl" +Write-Host " 2. Start coding and close the first issue" +Write-Host " 3. CI/CD will trigger on your first push" + +# Cleanup temp directory +if (-not $DryRun -and (Test-Path $workDir)) { + Remove-Item -Recurse -Force $workDir -ErrorAction SilentlyContinue +} diff --git a/workflows/03-project-init.sh b/workflows/03-project-init.sh index 22a3b854..acbbf97c 100644 --- a/workflows/03-project-init.sh +++ b/workflows/03-project-init.sh @@ -1,15 +1,19 @@ #!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────── # Scenario 3: One-Click Project Initialization -# Flow: Input description → Create repo → README/LICENSE/CI → Issues → Release +# Flow: Input description → Create repo → Generate files (README/LICENSE/ +# CONTRIBUTING/CI) → Push to repo → Milestones → Issues → +# Branch protection → Initial Release # -# Commands/Skills chained: +# Commands chained: # 1. repo +create -- create repository -# 2. wiki +create -- create README wiki page -# 3. wiki +create -- create CONTRIBUTING guide -# 4. issue +create -- create initial issues -# 5. branch +protect -- protect default branch -# 6. release +create -- create initial release +# 2. git clone -- clone the empty repo locally +# 3. file generation -- README.md, LICENSE, CONTRIBUTING.md, .gitlink-ci.yml +# 4. git add/commit/push -- push scaffold files to repo +# 5. milestone +create -- create project milestones +# 6. issue +create -- create initial issues +# 7. branch +protect -- protect default branch +# 8. release +create -- create initial release # ───────────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -65,33 +69,49 @@ echo " Private: $PRIVATE" divider # ── Step 1: Create Repository ──────────────────────────────────────── -log_step "Creating repository..." -REPO_RESULT=$(gl_check repo +create --owner "$OWNER" --name "$REPO_NAME" --description "$DESCRIPTION" --private "$PRIVATE") -REPO_ID=$(echo "$REPO_RESULT" | jq -r '.data.id // .data.project_id // empty') -log_ok "Repository created: $OWNER/$REPO_NAME (id: $REPO_ID)" +log_step "Step 1/8: Creating repository..." +if [[ "$DRY_RUN" == "true" ]]; then + log_warn "[DRY RUN] Would create repository: $OWNER/$REPO_NAME" +else + REPO_RESULT=$(gl_check repo +create --owner "$OWNER" --name "$REPO_NAME" --description "$DESCRIPTION" --private "$PRIVATE") + REPO_ID=$(echo "$REPO_RESULT" | jq -r '.data.id // .data.project_id // empty') + log_ok "Repository created: $OWNER/$REPO_NAME (id: $REPO_ID)" +fi -# ── Step 2: Create README ──────────────────────────────────────────── -log_step "Creating README wiki page..." +# ── Step 2: Clone empty repo ───────────────────────────────────────── +log_step "Step 2/8: Cloning repository..." +# Build auth URL using token so clone/push don't prompt for password +if [[ -n "${GITLINK_TOKEN:-}" ]]; then + CLONE_URL="https://oauth2:${GITLINK_TOKEN}@gitlink.org.cn/$OWNER/$REPO_NAME.git" +else + CLONE_URL="https://gitlink.org.cn/$OWNER/$REPO_NAME.git" +fi +REPO_URL="https://gitlink.org.cn/$OWNER/$REPO_NAME.git" +WORK_DIR=$(mktemp -d /tmp/gitlink-init-XXXXXX) +cleanup() { rm -rf "$WORK_DIR"; } +trap cleanup EXIT -# Wait for repo to be fully initialized -sleep 2 +if [[ "$DRY_RUN" == "true" ]]; then + log_warn "[DRY RUN] Would clone: $REPO_URL → $WORK_DIR" +else + git clone "$CLONE_URL" "$WORK_DIR" 2>&1 | tail -1 || true + log_ok "Cloned to $WORK_DIR" +fi -README_CONTENT="# $REPO_NAME +# ── Step 3: Generate project files ─────────────────────────────────── +log_step "Step 3/8: Generating project scaffold files..." -$DESCRIPTION - -## Getting Started - -### Prerequisites" - -case "$PROJ_LANG" in - go) - README_CONTENT+=" +# --- README.md --- +gen_readme() { + local lang_section + case "$PROJ_LANG" in + go) + lang_section="### 环境要求 - Go 1.21+ - Git -### Installation +### 安装 \`\`\`bash git clone https://gitlink.org.cn/$OWNER/$REPO_NAME.git @@ -100,25 +120,25 @@ go mod download go build ./... \`\`\` -### Usage +### 使用 \`\`\`bash go run main.go \`\`\` -### Testing +### 测试 \`\`\`bash go test ./... \`\`\`" - ;; - python) - README_CONTENT+=" + ;; + python) + lang_section="### 环境要求 - Python 3.9+ - pip -### Installation +### 安装 \`\`\`bash git clone https://gitlink.org.cn/$OWNER/$REPO_NAME.git @@ -126,25 +146,25 @@ cd $REPO_NAME pip install -r requirements.txt \`\`\` -### Usage +### 使用 \`\`\`bash python main.py \`\`\` -### Testing +### 测试 \`\`\`bash pytest \`\`\`" - ;; - node) - README_CONTENT+=" + ;; + node) + lang_section="### 环境要求 - Node.js 18+ - npm or yarn -### Installation +### 安装 \`\`\`bash git clone https://gitlink.org.cn/$OWNER/$REPO_NAME.git @@ -152,25 +172,25 @@ cd $REPO_NAME npm install \`\`\` -### Usage +### 使用 \`\`\`bash npm start \`\`\` -### Testing +### 测试 \`\`\`bash npm test \`\`\`" - ;; - java) - README_CONTENT+=" + ;; + java) + lang_section="### 环境要求 - JDK 17+ - Maven 3.8+ -### Installation +### 安装 \`\`\`bash git clone https://gitlink.org.cn/$OWNER/$REPO_NAME.git @@ -178,48 +198,72 @@ cd $REPO_NAME mvn clean install \`\`\` -### Usage +### 使用 \`\`\`bash mvn exec:java \`\`\` -### Testing +### 测试 \`\`\`bash mvn test \`\`\`" - ;; -esac + ;; + esac -README_CONTENT+=" + cat <&1) || true - if [[ "$(json_ok "$WIKI_RESULT")" == "true" ]]; then - log_ok "README created" - WIKI_OK=true - break - fi - [[ $attempt -lt 3 ]] && sleep 2 -done -[[ "$WIKI_OK" == "false" ]] && log_warn "README wiki creation may have failed" +详见 [CONTRIBUTING](./CONTRIBUTING.md)。 -# ── Step 3: Create CONTRIBUTING Guide ──────────────────────────────── -log_step "Creating CONTRIBUTING guide..." +## 许可证 -CONTRIB_CONTENT="# Contributing to $REPO_NAME +本项目基于 MIT 许可证开源 — 详见 [LICENSE](./LICENSE)。 +READMEEOF +} + +# --- LICENSE (MIT) --- +gen_license() { + local year + year=$(date +%Y) + cat <&1) || true - if [[ "$(json_ok "$WIKI_CONTRIB")" == "true" ]]; then - log_ok "CONTRIBUTING guide created" - WIKI_OK=true - break - fi - [[ $attempt -lt 3 ]] && sleep 2 -done -[[ "$WIKI_OK" == "false" ]] && log_warn "CONTRIBUTING wiki creation may have failed" +# --- .gitlink-ci.yml --- +gen_ci_config() { + case "$PROJ_LANG" in + go) + cat <&1) || true - ISSUE_NUM=$(echo "$ISSUE_RESULT" | jq -r '.data.id // .data.number // empty') - if [[ -n "$ISSUE_NUM" ]]; then - # Add label - gl_run issue +label-add --owner "$OWNER" --repo "$REPO_NAME" --number "$ISSUE_NUM" --labels "$label" > /dev/null 2>&1 || true - log_ok "Issue created: #$ISSUE_NUM - $title" - else - log_warn "Issue creation may have failed: $title" - fi -done +build: + stage: build + script: + - go build ./... +CIEOF + ;; + python) + cat <&1) || true +stages: + - test + - lint -if [[ "$(json_ok "$PROTECT_RESULT")" == "true" ]]; then - log_ok "Branch 'master' protected" +test: + stage: test + script: + - pip install -r requirements.txt + - pytest -v --cov + +lint: + stage: lint + script: + - pip install flake8 + - flake8 . +CIEOF + ;; + node) + cat < "$WORK_DIR/README.md" + gen_license > "$WORK_DIR/LICENSE" + gen_contributing > "$WORK_DIR/CONTRIBUTING.md" + gen_ci_config > "$WORK_DIR/.gitlink-ci.yml" + log_ok "Generated: README.md, LICENSE, CONTRIBUTING.md, .gitlink-ci.yml" fi -# ── Step 6: Create Initial Release ─────────────────────────────────── -log_step "Creating initial release v0.1.0..." +# ── Step 4: Push files to repo ─────────────────────────────────────── +log_step "Step 4/8: Pushing scaffold files to repository..." +if [[ "$DRY_RUN" == "true" ]]; then + log_warn "[DRY RUN] Would git add/commit/push to $REPO_URL" +else + pushd "$WORK_DIR" > /dev/null + git add README.md LICENSE CONTRIBUTING.md .gitlink-ci.yml + git -c user.name="$OWNER" -c user.email="$OWNER@gitlink.org.cn" \ + commit -m "chore: initialize project scaffold + +- Add README.md with getting-started guide +- Add MIT LICENSE +- Add CONTRIBUTING.md with guidelines +- Add .gitlink-ci.yml CI pipeline configuration" 2>&1 | tail -1 + git push -u origin master 2>&1 | tail -1 + popd > /dev/null + log_ok "Scaffold files pushed to master" +fi + +# ── Step 5: Create Milestone ───────────────────────────────────────── +log_step "Step 5/8: Creating initial milestone..." + +MILESTONES=( + "v1.0.0 - First Release|First official release milestone" +) + +if [[ "$DRY_RUN" == "true" ]]; then + log_warn "[DRY RUN] Would create ${#MILESTONES[@]} milestone" +else + for entry in "${MILESTONES[@]}"; do + IFS='|' read -r ms_title ms_desc <<< "$entry" + MS_RESULT=$(gl_run milestone +create --owner "$OWNER" --repo "$REPO_NAME" \ + --name "$ms_title" --description "$ms_desc" 2>&1) || true + if [[ "$(json_ok "$MS_RESULT")" == "true" ]]; then + log_ok "Milestone created: $ms_title" + else + log_warn "Milestone creation may have failed: $ms_title" + fi + done +fi + +# ── Step 6: Create Welcome Issue ───────────────────────────────────── +log_step "Step 6/8: Creating welcome issue..." + +ISSUES_TO_CREATE=( + "Welcome to $REPO_NAME|欢迎来到 **${REPO_NAME}** 的仓库,这是本仓库的第一条issue!!|welcome" +) + +if [[ "$DRY_RUN" == "true" ]]; then + log_warn "[DRY RUN] Would create ${#ISSUES_TO_CREATE[@]} initial issues" +else + for entry in "${ISSUES_TO_CREATE[@]}"; do + IFS='|' read -r title body label <<< "$entry" + ISSUE_RESULT=$(gl_run issue +create --owner "$OWNER" --repo "$REPO_NAME" \ + --title "$title" --body "$body" 2>&1) || true + ISSUE_NUM=$(echo "$ISSUE_RESULT" | jq -r '.data.id // .data.number // empty') + if [[ -n "$ISSUE_NUM" ]]; then + gl_run issue +label-add --owner "$OWNER" --repo "$REPO_NAME" --number "$ISSUE_NUM" --labels "$label" > /dev/null 2>&1 || true + log_ok "Issue created: #$ISSUE_NUM - $title" + else + log_warn "Issue creation may have failed: $title" + fi + done +fi + +# ── Step 7: Protect Default Branch ─────────────────────────────────── +log_step "Step 7/8: Protecting master branch..." +if [[ "$DRY_RUN" == "true" ]]; then + log_warn "[DRY RUN] Would protect branch 'master'" +else + PROTECT_RESULT=$(gl_run branch +protect --owner "$OWNER" --repo "$REPO_NAME" --name master 2>&1) || true + if [[ "$(json_ok "$PROTECT_RESULT")" == "true" ]]; then + log_ok "Branch 'master' protected" + else + log_warn "Branch protection may have failed (may require admin permissions)" + fi +fi + +# ── Step 8: Create Initial Release ─────────────────────────────────── +log_step "Step 8/8: Creating initial release v0.1.0..." RELEASE_BODY="# v0.1.0 - Initial Release ## What's New - Project initialized with $PROJ_LANG template -- README and CONTRIBUTING guides created -- CI/CD pipeline issues filed +- README, LICENSE, CONTRIBUTING guide in repository +- CI/CD pipeline configuration (.gitlink-ci.yml) +- ${#MILESTONES[@]} project milestone created +- ${#ISSUES_TO_CREATE[@]} initial issues filed - Branch protection enabled ## Next Steps @@ -319,13 +493,16 @@ RELEASE_BODY="# v0.1.0 - Initial Release --- *Auto-initialized by gitlink-cli project-init workflow*" -RELEASE_RESULT=$(gl_run release +create --owner "$OWNER" --repo "$REPO_NAME" \ - --tag "v0.1.0" --name "Initial Release" --body "$RELEASE_BODY" 2>&1) || true - -if [[ "$(json_ok "$RELEASE_RESULT")" == "true" ]]; then - log_ok "Release v0.1.0 created" +if [[ "$DRY_RUN" == "true" ]]; then + log_warn "[DRY RUN] Would create release v0.1.0" else - log_warn "Release creation may have failed" + RELEASE_RESULT=$(gl_run release +create --owner "$OWNER" --repo "$REPO_NAME" \ + --tag "v0.1.0" --name "Initial Release" --body "$RELEASE_BODY" 2>&1) || true + if [[ "$(json_ok "$RELEASE_RESULT")" == "true" ]]; then + log_ok "Release v0.1.0 created" + else + log_warn "Release creation may have failed" + fi fi # ───────────────────────────────────────────────────────────────────── @@ -334,14 +511,14 @@ log_title "Project Initialization Complete" echo -e "${GREEN}Created:${NC}" echo " Repository: $OWNER/$REPO_NAME" -echo " README: Wiki page" -echo " CONTRIBUTING: Wiki page" +echo " Files: README.md, LICENSE, CONTRIBUTING.md, .gitlink-ci.yml" +echo " Milestones: ${#MILESTONES[@]} milestone" echo " Issues: ${#ISSUES_TO_CREATE[@]} initial issues" echo " Branch: master (protected)" echo " Release: v0.1.0" echo "" echo -e "${CYAN}Next steps:${NC}" -echo " 1. Clone: git clone https://gitlink.org.cn/$OWNER/$REPO_NAME.git" -echo " 2. Add your code and push" -echo " 3. Setup CI/CD by closing the first issue" +echo " 1. Clone: git clone $REPO_URL" +echo " 2. Start coding and close the first issue" +echo " 3. CI/CD will trigger on your first push" echo "" diff --git a/workflows/04-multi-repo-collab.ps1 b/workflows/04-multi-repo-collab.ps1 index 983a057b..6e41b9b6 100644 --- a/workflows/04-multi-repo-collab.ps1 +++ b/workflows/04-multi-repo-collab.ps1 @@ -1,55 +1,99 @@ -# ---------------------------------------------------------------- +# ---------------------------------------------------------------- # Scenario 4: Multi-Repo Collaboration # Flow: Cross-repo issue tracking -> PR status dashboard -> Coordinated release # # Commands chained: -# 1. repo +list -- list all repos in org -# 2. issue +list -- fetch issues from each repo -# 3. pr +list -- fetch PRs from each repo -# 4. release +list -- check release status across repos -# 5. release +create -- coordinated release (optional) -# 6. Generate HTML dashboard +# 1. repo +list -- list all repos in org +# 2. issue +list -- fetch open/closed issues from each repo +# 3. pr +list -- fetch open/merged PRs from each repo +# 4. release +list -- check latest release across repos +# 5. Generate HTML dashboard (summary cards + table + per-repo details) +# 6. release +create -- (optional) coordinated release across all repos # ---------------------------------------------------------------- #Requires -Version 5.1 param( - [Parameter(Mandatory=$true)] - [string]$Org, + [string]$Org = "", [string]$Repos = "", [string]$Release = "", [string]$Output = "dashboard.html", + [int]$DetailLimit = 10, [switch]$DryRun, [switch]$Help ) $ErrorActionPreference = "Stop" -Import-Module "$PSScriptRoot/lib/common.psm1" -Force +Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue if ($Help) { - Write-Host "Usage: powershell 04-multi-repo-collab.ps1 -Org ORG [-Repos 'repo1,repo2'] [-Release TAG] [-Output FILE] [-DryRun]" + Write-Host "Usage: powershell 04-multi-repo-collab.ps1 -Org ORG [-Repos 'repo1,repo2'] [-Release TAG] [-Output FILE] [-DetailLimit N] [-DryRun]" exit 0 } +if (-not $Org) { Log-Err "Org is required (use -Help for usage)"; exit 1 } + Check-Auth -# -- Step 1: List Repositories -- -Log-Title "Multi-Repo Collaboration Dashboard" +# -- Helpers -- +function Html-Escape([string]$s) { + if ($null -eq $s) { return "" } + $s = $s -replace '&', '&' + $s = $s -replace '<', '<' + $s = $s -replace '>', '>' + $s = $s -replace '"', '"' + return $s +} -Log-Step "Fetching repositories for org: $Org..." +# Health score (0-100) aligned with gitlink-health skill. +# Core 75 (issue efficiency / PR efficiency / activity, -25 each), +# auxiliary 25 (issue backlog -10, PR backlog -10, no recent release -5). +# releaseAge: days since latest release, -1 = none. +function Compute-Health([int]$oi, [int]$ci, [int]$op, [int]$mp, [int]$age) { + $health = 100 + # 核心指标 (75) + $ti = $oi + $ci + $res = if ($ti -gt 0) { $ci / $ti } else { 1 } + if ($res -lt 0.5) { $health -= 25 } # Issue 处理效率 (响应慢代理) + $tp = $op + $mp + $mr = if ($tp -gt 0) { $mp / $tp } else { 1 } + if ($mr -lt 0.5) { $health -= 25 } # PR 合并效率 (合并慢代理) + if (($mp + $ci) -eq 0) { $health -= 25 } # 活跃度低: 无已完成工作 + # 辅助指标 (25) + if ($oi -gt 20) { $health -= 10 } # Issue 积压 + if ($op -gt 10) { $health -= 10 } # PR 积压 + if ($age -lt 0 -or $age -gt 30) { $health -= 5 } # 无近期发布 + if ($health -lt 0) { $health = 0 } + if ($health -gt 100) { $health = 100 } + return [int]$health +} + +# Map score -> @{ Label; Class } (5 bands per gitlink-health skill) +function Get-HealthGrade([int]$score) { + if ($score -ge 90) { return @{ Label = "优秀"; Class = "excellent" } } + elseif ($score -ge 70) { return @{ Label = "良好"; Class = "good" } } + elseif ($score -ge 50) { return @{ Label = "一般"; Class = "moderate" } } + elseif ($score -ge 30) { return @{ Label = "需关注"; Class = "attention" } } + else { return @{ Label = "严重"; Class = "critical" } } +} + +# -- Step 1: List Repositories -- +Log-Title "多仓库协同看板 (Multi-Repo Collaboration Dashboard)" + +Log-Step "获取组织 $Org 下的仓库列表..." $reposJson = Invoke-GLCheck "repo", "+list", "--user", $Org, "--limit", "100" -if (-not $reposJson) { Log-Err "Failed to fetch repos"; exit 1 } +if (-not $reposJson) { Log-Err "获取仓库列表失败"; exit 1 } $allRepos = @() $rd = $reposJson.data if ($rd.projects) { $allRepos = @($rd.projects) } elseif ($rd -is [array]) { $allRepos = $rd } -Log-Ok "Found $($allRepos.Count) repositories" +Log-Ok "组织下共有 $($allRepos.Count) 个仓库" $repoList = @() if ($Repos) { $repoList = $Repos -split ',' - Log-Info "Filtering to specified repos: $($repoList -join ', ')" + Log-Info "仅处理指定仓库: $($repoList -join ', ')" } else { foreach ($r in $allRepos) { $rname = if ($r.name) { $r.name } elseif ($r.identifier) { $r.identifier } else { $null } @@ -57,153 +101,259 @@ if ($Repos) { } } -Log-Ok "Will process $($repoList.Count) repositories" +if ($repoList.Count -eq 0) { Log-Err "没有可处理的仓库。请检查 -Org 或用 -Repos 指定。"; exit 1 } +Log-Ok "将处理 $($repoList.Count) 个仓库" -# -- Step 2-3: Collect Issues and PRs from each repo -- -Log-Title "Collecting Data Across Repos" +# -- Step 2-4: Collect data across repos -- +Log-Title "跨仓库数据采集" -$totalIssues = 0; $totalOpenIssues = 0; $totalPRs = 0; $totalOpenPRs = 0 +$totalActivity = 0; $totalOpenIssues = 0; $totalOpenPRs = 0; $totalMergedPRs = 0; $healthSum = 0 $dashboardRows = "" +$repoDetails = "" foreach ($repo in $repoList) { Divider - Log-Step "Processing $Org/$repo..." + Log-Step "处理 $Org/$repo..." - $issuesJson = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "open", "--limit", "50" - $openIssues = if ($issuesJson) { @($issuesJson.data.issues).Count } else { 0 } + # Open issues + $issuesObj = $null; $openIssues = 0; $openIssueItems = @() + $issuesRaw = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "open", "--limit", "50" + if ($issuesRaw) { try { + $issuesObj = $issuesRaw | ConvertFrom-Json + if ($issuesObj.ok -and $issuesObj.data.issues) { $openIssueItems = @($issuesObj.data.issues); $openIssues = $openIssueItems.Count } + } catch {} } - $closedJson = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "closed", "--limit", "50" - $closedIssues = if ($closedJson) { @($closedJson.data.issues).Count } else { 0 } + # Closed issues + $closedIssues = 0 + $closedRaw = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "closed", "--limit", "50" + if ($closedRaw) { try { + $closedObj = $closedRaw | ConvertFrom-Json + if ($closedObj.ok -and $closedObj.data.issues) { $closedIssues = @($closedObj.data.issues).Count } + } catch {} } - $prsJson = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "open", "--limit", "50" - $openPRs = 0 - if ($prsJson) { - $pd = $prsJson.data - if ($pd.issues) { $openPRs = @($pd.issues).Count } - elseif ($pd.pulls) { $openPRs = @($pd.pulls).Count } - elseif ($pd -is [array]) { $openPRs = $pd.Count } - } - - $mergedJson = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "merged", "--limit", "50" - $mergedPRs = 0 - if ($mergedJson) { - $md = $mergedJson.data - if ($md.issues) { $mergedPRs = @($md.issues).Count } - elseif ($md.pulls) { $mergedPRs = @($md.pulls).Count } - elseif ($md -is [array]) { $mergedPRs = $md.Count } - } - - $releaseJson = Invoke-GL "release", "+list", "--owner", $Org, "--repo", $repo, "--limit", "1" - $latestRelease = "none" - if ($releaseJson -and $releaseJson.data.releases) { - $releases = @($releaseJson.data.releases) - if ($releases.Count -gt 0) { - $latestRelease = if ($releases[0].tag_name) { $releases[0].tag_name } elseif ($releases[0].name) { $releases[0].name } else { "none" } + # Open PRs + $openPRs = 0; $openPRItems = @() + $prsRaw = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "open", "--limit", "50" + if ($prsRaw) { try { + $prsObj = $prsRaw | ConvertFrom-Json + if ($prsObj.ok) { + if ($prsObj.data.issues) { $openPRItems = @($prsObj.data.issues) } + elseif ($prsObj.data.pulls) { $openPRItems = @($prsObj.data.pulls) } + elseif ($prsObj.data -is [array]) { $openPRItems = @($prsObj.data) } + $openPRs = $openPRItems.Count } + } catch {} } + + # Merged PRs + $mergedPRs = 0 + $mergedRaw = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "merged", "--limit", "50" + if ($mergedRaw) { try { + $mergedObj = $mergedRaw | ConvertFrom-Json + if ($mergedObj.ok) { + if ($mergedObj.data.issues) { $mergedPRs = @($mergedObj.data.issues).Count } + elseif ($mergedObj.data.pulls) { $mergedPRs = @($mergedObj.data.pulls).Count } + elseif ($mergedObj.data -is [array]) { $mergedPRs = @($mergedObj.data).Count } + } + } catch {} } + + # Latest release + age + $latestRelease = "none"; $releaseAge = -1 + $releaseRaw = Invoke-GL "release", "+list", "--owner", $Org, "--repo", $repo, "--limit", "1" + if ($releaseRaw) { try { + $releaseObj = $releaseRaw | ConvertFrom-Json + $rels = $null + if ($releaseObj.ok -and $releaseObj.data.releases) { $rels = @($releaseObj.data.releases) } + elseif ($releaseObj.ok -and $releaseObj.data -is [array]) { $rels = @($releaseObj.data) } + if ($rels -and $rels.Count -gt 0) { + $latestRelease = if ($rels[0].tag_name) { $rels[0].tag_name } elseif ($rels[0].name) { $rels[0].name } else { "none" } + $relDate = if ($rels[0].created_at) { $rels[0].created_at } elseif ($rels[0].created_on) { $rels[0].created_on } elseif ($rels[0].published_at) { $rels[0].published_at } else { $null } + if ($relDate) { + try { $releaseAge = [int]((Get-Date) - [datetime]$relDate).TotalDays } catch { $releaseAge = 0 } + } else { $releaseAge = 0 } + } + } catch {} } + + $health = Compute-Health $openIssues $closedIssues $openPRs $mergedPRs $releaseAge + $grade = Get-HealthGrade $health + + Log-Ok "$repo : Issue(开:$openIssues 闭:$closedIssues) PR(开:$openPRs 合:$mergedPRs) Release:$latestRelease 健康度:$health($($grade.Label))" + + # Open issue detail rows + $issueRows = "" + $limit = [Math]::Min($openIssues, $DetailLimit) + for ($i = 0; $i -lt $limit; $i++) { + $it = $openIssueItems[$i] + $iid = if ($it.id) { $it.id } elseif ($it.number) { $it.number } else { "" } + $ititle = if ($it.name) { $it.name } elseif ($it.subject) { $it.subject } elseif ($it.title) { $it.title } else { "" } + $iauthor = if ($it.author_name) { $it.author_name } elseif ($it.author_login) { $it.author_login } elseif ($it.author.login) { $it.author.login } else { "unknown" } + $issueRows += "#$(Html-Escape ([string]$iid))$(Html-Escape ([string]$ititle))@$(Html-Escape ([string]$iauthor))" } + if (-not $issueRows) { $issueRows = '无未关闭 Issue' } - Log-Ok "$repo : Issues(open:$openIssues closed:$closedIssues) PRs(open:$openPRs merged:$mergedPRs) Release:$latestRelease" + # Open PR detail rows + $prRows = "" + $limit = [Math]::Min($openPRs, $DetailLimit) + for ($i = 0; $i -lt $limit; $i++) { + $pt = $openPRItems[$i] + $pid = if ($pt.pull_request_number) { $pt.pull_request_number } elseif ($pt.number) { $pt.number } elseif ($pt.id) { $pt.id } else { "" } + $ptitle = if ($pt.subject) { $pt.subject } elseif ($pt.title) { $pt.title } elseif ($pt.name) { $pt.name } else { "" } + $pauthor = if ($pt.author_name) { $pt.author_name } elseif ($pt.author_login) { $pt.author_login } elseif ($pt.author.login) { $pt.author.login } else { "unknown" } + $prRows += "#$(Html-Escape ([string]$pid))$(Html-Escape ([string]$ptitle))@$(Html-Escape ([string]$pauthor))" + } + if (-not $prRows) { $prRows = '无未合并 PR' } - $statusColor = "green" - $healthText = "Healthy" - if ($openIssues -gt 10) { $statusColor = "orange"; $healthText = "Moderate" } - if ($openIssues -gt 20) { $statusColor = "red"; $healthText = "Needs Attention" } + $repoDetails += @" +
$health $(Html-Escape $repo) — Issue 开 $openIssues / PR 开 $openPRs +
+

未关闭 Issue(最多 $DetailLimit 条)

+ $issueRows
编号标题作者
+

未合并 PR(最多 $DetailLimit 条)

+ $prRows
编号标题作者
+
+
+"@ - $dashboardRows += "`n" - $dashboardRows += " $repo`n" - $dashboardRows += " $openIssues$closedIssues`n" - $dashboardRows += " $openPRs$mergedPRs$latestRelease`n" - $dashboardRows += " $healthText`n" - $dashboardRows += "`n" - - $totalIssues += $openIssues + $closedIssues + $totalActivity += $openIssues + $closedIssues + $openPRs + $mergedPRs $totalOpenIssues += $openIssues - $totalPRs += $openPRs + $mergedPRs $totalOpenPRs += $openPRs + $totalMergedPRs += $mergedPRs + $healthSum += $health + + $relDisplay = if ($latestRelease -eq "none") { "—" } else { Html-Escape $latestRelease } + + $dashboardRows += @" + + $(Html-Escape $repo) + $openIssues$closedIssues + $openPRs$mergedPRs$relDisplay + $health $($grade.Label) + +"@ } -# -- Step 4: Generate HTML Dashboard -- -Log-Title "Generating Dashboard" +$avgHealth = if ($repoList.Count -gt 0) { [int]($healthSum / $repoList.Count) } else { 0 } + +# -- Step 5: Generate HTML Dashboard -- +Log-Title "生成 HTML 看板" $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $repoCount = $repoList.Count -$html = ' - +$html = @" + + -Multi-Repo Collaboration Dashboard +多仓库协同看板 - $Org
-

Multi-Repo Collaboration Dashboard

-

Generated: ' + $timestamp + ' | Organization: ' + $Org + '

+

多仓库协同看板

+

生成时间:$timestamp | 组织:$Org

-

Total Repos

' + $repoCount + '
-

Open Issues

' + $totalOpenIssues + '
-

Open PRs

' + $totalOpenPRs + '
-

Total Activity

' + $totalIssues + '
+

仓库总数

$repoCount
+

未关闭 Issue

$totalOpenIssues
+

未合并 PR

$totalOpenPRs
+

已合并 PR

$totalMergedPRs
+

平均健康度

$avgHealth
+ +

仓库概览

- + -' + $dashboardRows + ' +$dashboardRows
RepositoryOpen IssuesClosed IssuesOpen PRsMerged PRsLatest ReleaseHealth
仓库未关闭 Issue已关闭 Issue未合并 PR已合并 PR最新 Release健康度
+ +

仓库详情

+$repoDetails
-' + +"@ -$html | Out-File -FilePath $Output -Encoding UTF8 -Log-Ok "Dashboard saved to: $Output" +$utf8 = [System.Text.UTF8Encoding]::new($false) +[System.IO.File]::WriteAllText((Join-Path (Get-Location) $Output), $html, $utf8) +Log-Ok "看板已保存: $Output" -# -- Step 5: Coordinated Release -- +# -- Step 6: Coordinated Release -- if ($Release) { - Log-Title "Coordinated Release: $Release" + Log-Title "协调发版: $Release" - foreach ($repo in $repoList) { - Log-Step "Creating release for $Org/$repo..." - $relBody = "Coordinated release $Release for $Org/$repo" - $relResult = Invoke-GL "release", "+create", "--owner", $Org, "--repo", $repo, "--tag", $Release, "--name", "Release $Release", "--body", $relBody - if ($relResult -and (Get-JsonOk $relResult)) { - Log-Ok "Release $Release created for $repo" - } else { - Log-Warn "Release creation failed for $repo (tag may already exist)" + if ($DryRun) { + Log-Warn "[DRY RUN] 将为以下 $($repoList.Count) 个仓库创建 Release ${Release}:" + foreach ($repo in $repoList) { Log-Warn " [DRY RUN] $Org/$repo -> $Release" } + } else { + $relOk = 0; $relFail = 0 + foreach ($repo in $repoList) { + Log-Step "为 $Org/$repo 创建 Release..." + $relBody = "Coordinated release $Release for $Org/$repo" + $relResult = Invoke-GL "release", "+create", "--owner", $Org, "--repo", $repo, "--tag", $Release, "--name", "Release $Release", "--body", $relBody + $ok = $false + if ($relResult) { try { $ok = ($relResult | ConvertFrom-Json).ok } catch { $ok = $false } } + if ($ok) { Log-Ok "Release $Release 已在 $repo 创建"; $relOk++ } + else { Log-Warn "Release 创建失败: $repo (tag 可能已存在)"; $relFail++ } } + Log-Info "协调发版结果: 成功 $relOk / 失败 $relFail" } } # ---------------------------------------------------------------- -Log-Title "Multi-Repo Dashboard Complete" +Log-Title "多仓库看板完成" # ---------------------------------------------------------------- -Write-Host " Repos processed: $repoCount" -ForegroundColor Green -Write-Host " Total issues: $totalIssues (open: $totalOpenIssues)" -ForegroundColor Green -Write-Host " Total PRs: $totalPRs (open: $totalOpenPRs)" -ForegroundColor Green -Write-Host " Dashboard: $Output" -ForegroundColor Green -if ($Release) { Write-Host " Coordinated release: $Release" -ForegroundColor Green } +Write-Host " 处理仓库: $repoCount" -ForegroundColor Green +Write-Host " 未关闭 Issue: $totalOpenIssues" -ForegroundColor Green +Write-Host " 未合并 PR: $totalOpenPRs" -ForegroundColor Green +Write-Host " 已合并 PR: $totalMergedPRs" -ForegroundColor Green +Write-Host " 平均健康度: $avgHealth / 100" -ForegroundColor Green +Write-Host " 看板文件: $Output" -ForegroundColor Green +if ($Release) { + $suffix = if ($DryRun) { " (dry-run)" } else { "" } + Write-Host " 协调发版: $Release$suffix" -ForegroundColor Green +} Write-Host "" -Write-Host "Open dashboard:" -ForegroundColor Cyan +Write-Host "打开看板:" -ForegroundColor Cyan Write-Host " Start-Process $Output" diff --git a/workflows/04-multi-repo-collab.sh b/workflows/04-multi-repo-collab.sh index 2f4715a8..0585d8f4 100644 --- a/workflows/04-multi-repo-collab.sh +++ b/workflows/04-multi-repo-collab.sh @@ -3,27 +3,27 @@ # Scenario 4: Multi-Repo Collaboration # Flow: Cross-repo issue tracking → PR status dashboard → Coordinated release # -# Commands/Skills chained: -# 1. repo +list -- list all repos in org -# 2. issue +list -- fetch issues from each repo -# 3. pr +list -- fetch PRs from each repo -# 4. pr +view -- get PR details for dashboard -# 5. release +list -- check release status across repos -# 6. release +create -- coordinated release -# 7. Generate HTML dashboard +# Commands chained: +# 1. repo +list -- list all repos in org +# 2. issue +list -- fetch open/closed issues from each repo +# 3. pr +list -- fetch open/merged PRs from each repo +# 4. release +list -- check latest release across repos +# 5. Generate HTML dashboard (summary cards + table + per-repo details) +# 6. release +create -- (optional) coordinated release across all repos # ───────────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/common.sh" usage() { - echo "Usage: $0 --org ORG [--repos REPO1,REPO2,...] [--release TAG] [--dry-run]" + echo "Usage: $0 --org ORG [--repos REPO1,REPO2,...] [--release TAG] [--output FILE] [--dry-run]" echo "" - echo " --org ORG Organization name" + echo " --org ORG Organization / user name (required)" echo " --repos REPO1,REPO2 Comma-separated repo list (default: all repos in org)" - echo " --release TAG Coordinated release tag to create" + echo " --release TAG Coordinated release tag to create across all repos" echo " --output FILE Output HTML dashboard file (default: dashboard.html)" - echo " --dry-run Preview actions without executing" + echo " --detail-limit N Max open issues/PRs listed per repo in details (default: 10)" + echo " --dry-run Preview actions without creating releases" exit 1 } @@ -32,16 +32,18 @@ ORG="" REPOS="" RELEASE_TAG="" OUTPUT_FILE="dashboard.html" +DETAIL_LIMIT=10 while [[ $# -gt 0 ]]; do case "$1" in - --org) ORG="$2"; shift 2 ;; - --repos) REPOS="$2"; shift 2 ;; - --release) RELEASE_TAG="$2"; shift 2 ;; - --output) OUTPUT_FILE="$2"; shift 2 ;; - --dry-run) DRY_RUN="true"; shift ;; - --help|-h) usage ;; - *) log_err "Unknown arg: $1"; usage ;; + --org) ORG="$2"; shift 2 ;; + --repos) REPOS="$2"; shift 2 ;; + --release) RELEASE_TAG="$2"; shift 2 ;; + --output) OUTPUT_FILE="$2"; shift 2 ;; + --detail-limit) DETAIL_LIMIT="$2"; shift 2 ;; + --dry-run) DRY_RUN="true"; shift ;; + --help|-h) usage ;; + *) log_err "Unknown arg: $1"; usage ;; esac done @@ -52,214 +54,330 @@ fi check_auth -# ── Step 1: List Repositories ──────────────────────────────────────── -log_title "Multi-Repo Collaboration Dashboard" +# ── HTML escape helper ─────────────────────────────────────────────── +html_escape() { + local s="$1" + s="${s//&/&}" + s="${s///>}" + s="${s//\"/"}" + echo "$s" +} -log_step "Fetching repositories for org: $ORG..." +# ── Health score (0-100) — aligned with gitlink-health skill ───────── +# Model: start 100, deduct. Core 75 (issue efficiency / PR efficiency / +# activity, -25 each), auxiliary 25 (issue backlog -10, PR backlog -10, +# no recent release -5). Time-based core metrics are approximated by +# list-level ratios for a fast multi-repo scan. +# release_age: days since latest release, -1 = no release. +compute_health() { + local open_issues="$1" closed_issues="$2" open_prs="$3" merged_prs="$4" release_age="$5" + awk -v oi="$open_issues" -v ci="$closed_issues" -v op="$open_prs" \ + -v mp="$merged_prs" -v age="$release_age" 'BEGIN { + health = 100 + # ── 核心指标 (75) ── + # Issue 处理效率 (proxy for 响应慢): 解决率 < 50% 扣 25 + ti = oi + ci + res = (ti > 0) ? ci / ti : 1 + if (res < 0.5) health -= 25 + # PR 合并效率 (proxy for 合并慢): 合并率 < 50% 扣 25 + tp = op + mp + mr = (tp > 0) ? mp / tp : 1 + if (mr < 0.5) health -= 25 + # 活跃度低: 无任何已完成工作 (合并 PR + 关闭 Issue == 0) 扣 25 + if ((mp + ci) == 0) health -= 25 + # ── 辅助指标 (25) ── + if (oi > 20) health -= 10 # Issue 积压 + if (op > 10) health -= 10 # PR 积压 + if (age < 0 || age > 30) health -= 5 # 无近期发布 (>30天或无) + if (health < 0) health = 0 + if (health > 100) health = 100 + printf "%d", health + }' +} + +# Map score → (grade, css class) — 5 bands per gitlink-health skill +health_grade() { + local score="$1" + if [[ "$score" -ge 90 ]]; then echo "优秀|excellent" + elif [[ "$score" -ge 70 ]]; then echo "良好|good" + elif [[ "$score" -ge 50 ]]; then echo "一般|moderate" + elif [[ "$score" -ge 30 ]]; then echo "需关注|attention" + else echo "严重|critical" + fi +} + +# ── Step 1: List Repositories ──────────────────────────────────────── +log_title "多仓库协同看板 (Multi-Repo Collaboration Dashboard)" + +log_step "获取组织 $ORG 下的仓库列表..." REPOS_JSON=$(gl_check repo +list --user "$ORG" --limit 100) -# Response may have .data.projects[] or .data[] -ALL_REPO_COUNT=$(echo "$REPOS_JSON" | jq '(.data.projects // .data | if type == "array" then . else [] end) | length') -log_ok "Found $ALL_REPO_COUNT repositories" +REPOS_DATA_PATH='(.data.projects // .data | if type == "array" then . else [] end)' +ALL_REPO_COUNT=$(echo "$REPOS_JSON" | jq "$REPOS_DATA_PATH | length") +log_ok "组织下共有 $ALL_REPO_COUNT 个仓库" # Filter repos if --repos specified REPO_LIST=() if [[ -n "$REPOS" ]]; then IFS=',' read -ra REPO_LIST <<< "$REPOS" - log_info "Filtering to specified repos: ${REPO_LIST[*]}" + log_info "仅处理指定仓库: ${REPO_LIST[*]}" else - REPOS_DATA_PATH='(.data.projects // .data | if type == "array" then . else [] end)' for i in $(seq 0 $((ALL_REPO_COUNT - 1))); do RNAME=$(echo "$REPOS_JSON" | jq -r "$REPOS_DATA_PATH[$i].name // $REPOS_DATA_PATH[$i].identifier // empty") [[ -n "$RNAME" ]] && REPO_LIST+=("$RNAME") done fi -log_ok "Will process ${#REPO_LIST[@]} repositories" +if [[ "${#REPO_LIST[@]}" -eq 0 ]]; then + log_err "没有可处理的仓库。请检查 --org 是否正确,或用 --repos 指定。" + exit 1 +fi +log_ok "将处理 ${#REPO_LIST[@]} 个仓库" -# ── Step 2-3: Collect Issues and PRs from each repo ────────────────── -log_title "Collecting Data Across Repos" +# ── Step 2-4: Collect data across repos ────────────────────────────── +log_title "跨仓库数据采集" -# Data arrays for dashboard DASHBOARD_ROWS="" -TOTAL_ISSUES=0 -TOTAL_PRS=0 +REPO_DETAILS="" +TOTAL_ACTIVITY=0 TOTAL_OPEN_ISSUES=0 TOTAL_OPEN_PRS=0 +TOTAL_MERGED_PRS=0 +HEALTH_SUM=0 + +ISSUE_DATA_PATH='(.data.issues // .data | if type == "array" then . else [] end)' +PR_DATA_PATH='(.data.issues // .data.pulls // .data | if type == "array" then . else [] end)' for repo in "${REPO_LIST[@]}"; do divider - log_step "Processing $ORG/$repo..." + log_step "处理 $ORG/$repo..." - # Fetch open issues ISSUES_JSON=$(gl_run issue +list --owner "$ORG" --repo "$repo" --state open --limit 50) - OPEN_ISSUES=$(echo "$ISSUES_JSON" | jq '.data.issues | length' 2>/dev/null || echo "0") + OPEN_ISSUES=$(echo "$ISSUES_JSON" | jq "$ISSUE_DATA_PATH | length" 2>/dev/null || echo "0") + [[ "$OPEN_ISSUES" =~ ^[0-9]+$ ]] || OPEN_ISSUES=0 - # Fetch closed issues (recent) CLOSED_JSON=$(gl_run issue +list --owner "$ORG" --repo "$repo" --state closed --limit 50) - CLOSED_ISSUES=$(echo "$CLOSED_JSON" | jq '.data.issues | length' 2>/dev/null || echo "0") + CLOSED_ISSUES=$(echo "$CLOSED_JSON" | jq "$ISSUE_DATA_PATH | length" 2>/dev/null || echo "0") + [[ "$CLOSED_ISSUES" =~ ^[0-9]+$ ]] || CLOSED_ISSUES=0 - # Fetch open PRs PRS_JSON=$(gl_run pr +list --owner "$ORG" --repo "$repo" --state open --limit 50) - OPEN_PRS=$(echo "$PRS_JSON" | jq '(.data.issues // .data.pulls // .data | if type == "array" then . else [] end) | length' 2>/dev/null || echo "0") + OPEN_PRS=$(echo "$PRS_JSON" | jq "$PR_DATA_PATH | length" 2>/dev/null || echo "0") + [[ "$OPEN_PRS" =~ ^[0-9]+$ ]] || OPEN_PRS=0 - # Fetch merged PRs (recent) MERGED_JSON=$(gl_run pr +list --owner "$ORG" --repo "$repo" --state merged --limit 50) - MERGED_PRS=$(echo "$MERGED_JSON" | jq '(.data.issues // .data.pulls // .data | if type == "array" then . else [] end) | length' 2>/dev/null || echo "0") + MERGED_PRS=$(echo "$MERGED_JSON" | jq "$PR_DATA_PATH | length" 2>/dev/null || echo "0") + [[ "$MERGED_PRS" =~ ^[0-9]+$ ]] || MERGED_PRS=0 - # Fetch latest release RELEASES_JSON=$(gl_run release +list --owner "$ORG" --repo "$repo" --limit 1) - LATEST_RELEASE=$(echo "$RELEASES_JSON" | jq -r '.data.releases[0].tag_name // .data.releases[0].name // "none"' 2>/dev/null) + REL_ARR='(.data.releases // .data | if type == "array" then . else [] end)' + LATEST_RELEASE=$(echo "$RELEASES_JSON" | jq -r "$REL_ARR[0].tag_name // $REL_ARR[0].name // \"none\"" 2>/dev/null) + [[ -z "$LATEST_RELEASE" || "$LATEST_RELEASE" == "null" ]] && LATEST_RELEASE="none" - log_ok "$repo: Issues(open:$OPEN_ISSUES closed:$CLOSED_ISSUES) PRs(open:$OPEN_PRS merged:$MERGED_PRS) Release:$LATEST_RELEASE" + # Release age in days (-1 if none / unparseable) + RELEASE_AGE=-1 + if [[ "$LATEST_RELEASE" != "none" ]]; then + REL_DATE=$(echo "$RELEASES_JSON" | jq -r "$REL_ARR[0].created_at // $REL_ARR[0].created_on // $REL_ARR[0].published_at // empty" 2>/dev/null) + REL_DATE="${REL_DATE%% *}"; REL_DATE="${REL_DATE%%T*}" + if [[ "$REL_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + RELEASE_AGE=$(days_between "$REL_DATE" "$(date_today)") + else + RELEASE_AGE=0 # has release but no parseable date → treat as recent + fi + fi - # Get PR details for open PRs - PR_DETAILS="" - PR_DATA_PATH='(.data.issues // .data.pulls // .data | if type == "array" then . else [] end)' - if [[ "$OPEN_PRS" -gt 0 ]] && [[ "$OPEN_PRS" != "null" ]]; then - for pi in $(seq 0 $((OPEN_PRS > 5 ? 4 : OPEN_PRS - 1))); do - PR_ID=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].pull_request_number // $PR_DATA_PATH[$pi].number // $PR_DATA_PATH[$pi].id // empty") - PR_TITLE=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].subject // $PR_DATA_PATH[$pi].title // empty") - PR_AUTHOR=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].author_login // $PR_DATA_PATH[$pi].author.login // \"unknown\"") - PR_DETAILS+="#$PR_ID$PR_TITLE@$PR_AUTHORopen" + # Health score (aligned with gitlink-health skill) + HEALTH=$(compute_health "$OPEN_ISSUES" "$CLOSED_ISSUES" "$OPEN_PRS" "$MERGED_PRS" "$RELEASE_AGE") + IFS='|' read -r GRADE_LABEL GRADE_CLASS <<< "$(health_grade "$HEALTH")" + + log_ok "$repo: Issue(开:$OPEN_ISSUES 闭:$CLOSED_ISSUES) PR(开:$OPEN_PRS 合:$MERGED_PRS) Release:$LATEST_RELEASE 健康度:$HEALTH($GRADE_LABEL)" + + # ── Collect open issue details ── + ISSUE_ROWS="" + if [[ "$OPEN_ISSUES" -gt 0 ]]; then + limit=$(( OPEN_ISSUES < DETAIL_LIMIT ? OPEN_ISSUES : DETAIL_LIMIT )) + for ii in $(seq 0 $((limit - 1))); do + I_ID=$(echo "$ISSUES_JSON" | jq -r "$ISSUE_DATA_PATH[$ii].id // $ISSUE_DATA_PATH[$ii].number // empty") + I_TITLE=$(echo "$ISSUES_JSON" | jq -r "$ISSUE_DATA_PATH[$ii].name // $ISSUE_DATA_PATH[$ii].subject // $ISSUE_DATA_PATH[$ii].title // empty") + I_AUTHOR=$(echo "$ISSUES_JSON" | jq -r "$ISSUE_DATA_PATH[$ii].author_name // $ISSUE_DATA_PATH[$ii].author_login // $ISSUE_DATA_PATH[$ii].author.login // \"unknown\"") + ISSUE_ROWS+="#$(html_escape "$I_ID")$(html_escape "$I_TITLE")@$(html_escape "$I_AUTHOR")" done fi - # Accumulate totals - TOTAL_ISSUES=$((TOTAL_ISSUES + OPEN_ISSUES + CLOSED_ISSUES)) - TOTAL_OPEN_ISSUES=$((TOTAL_OPEN_ISSUES + OPEN_ISSUES)) - TOTAL_PRS=$((TOTAL_PRS + OPEN_PRS + MERGED_PRS)) - TOTAL_OPEN_PRS=$((TOTAL_OPEN_PRS + OPEN_PRS)) + # ── Collect open PR details ── + PR_ROWS="" + if [[ "$OPEN_PRS" -gt 0 ]]; then + limit=$(( OPEN_PRS < DETAIL_LIMIT ? OPEN_PRS : DETAIL_LIMIT )) + for pi in $(seq 0 $((limit - 1))); do + P_ID=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].pull_request_number // $PR_DATA_PATH[$pi].number // $PR_DATA_PATH[$pi].id // empty") + P_TITLE=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].subject // $PR_DATA_PATH[$pi].title // $PR_DATA_PATH[$pi].name // empty") + P_AUTHOR=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].author_name // $PR_DATA_PATH[$pi].author_login // $PR_DATA_PATH[$pi].author.login // \"unknown\"") + PR_ROWS+="#$(html_escape "$P_ID")$(html_escape "$P_TITLE")@$(html_escape "$P_AUTHOR")" + done + fi - # Add to dashboard rows - STATUS_COLOR="green" - [[ "$OPEN_ISSUES" -gt 10 ]] && STATUS_COLOR="orange" - [[ "$OPEN_ISSUES" -gt 20 ]] && STATUS_COLOR="red" + # ── Per-repo collapsible detail section ── + [[ -z "$ISSUE_ROWS" ]] && ISSUE_ROWS="无未关闭 Issue" + [[ -z "$PR_ROWS" ]] && PR_ROWS="无未合并 PR" + REPO_DETAILS+="
$HEALTH $(html_escape "$repo") — Issue 开 $OPEN_ISSUES / PR 开 $OPEN_PRS +
+

未关闭 Issue(最多 $DETAIL_LIMIT 条)

+ $ISSUE_ROWS
编号标题作者
+

未合并 PR(最多 $DETAIL_LIMIT 条)

+ $PR_ROWS
编号标题作者
+
+
" + + # Accumulate totals + TOTAL_ACTIVITY=$((TOTAL_ACTIVITY + OPEN_ISSUES + CLOSED_ISSUES + OPEN_PRS + MERGED_PRS)) + TOTAL_OPEN_ISSUES=$((TOTAL_OPEN_ISSUES + OPEN_ISSUES)) + TOTAL_OPEN_PRS=$((TOTAL_OPEN_PRS + OPEN_PRS)) + TOTAL_MERGED_PRS=$((TOTAL_MERGED_PRS + MERGED_PRS)) + HEALTH_SUM=$((HEALTH_SUM + HEALTH)) + + REL_DISPLAY="$LATEST_RELEASE" + [[ "$REL_DISPLAY" == "none" ]] && REL_DISPLAY="—" DASHBOARD_ROWS+=" - $repo + $(html_escape "$repo") $OPEN_ISSUES $CLOSED_ISSUES $OPEN_PRS $MERGED_PRS - $LATEST_RELEASE - $( - [[ "$OPEN_ISSUES" -le 5 ]] && echo "Healthy" || \ - [[ "$OPEN_ISSUES" -le 15 ]] && echo "Moderate" || echo "Needs Attention" - ) + $(html_escape "$REL_DISPLAY") + $HEALTH $GRADE_LABEL " done -# ── Step 4: Generate HTML Dashboard ────────────────────────────────── -log_title "Generating Dashboard" +# Average health +AVG_HEALTH=0 +[[ "${#REPO_LIST[@]}" -gt 0 ]] && AVG_HEALTH=$((HEALTH_SUM / ${#REPO_LIST[@]})) -log_step "Creating HTML dashboard..." +# ── Step 5: Generate HTML Dashboard ────────────────────────────────── +log_title "生成 HTML 看板" +log_step "写入 $OUTPUT_FILE..." -cat > "$OUTPUT_FILE" << 'HTMLEOF' +TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') + +cat > "$OUTPUT_FILE" << HTMLEOF - + -Multi-Repo Collaboration Dashboard +多仓库协同看板 - $ORG
-

Multi-Repo Collaboration Dashboard

-

Generated: TIMESTAMP_PLACEHOLDER | Organization: ORG_PLACEHOLDER

+

多仓库协同看板

+

生成时间:$TIMESTAMP | 组织:$ORG

-

Total Repos

REPOS_COUNT
-

Open Issues

OPEN_ISSUES_COUNT
-

Open PRs

OPEN_PRS_COUNT
-

Total Activity

TOTAL_ACTIVITY
+

仓库总数

${#REPO_LIST[@]}
+

未关闭 Issue

$TOTAL_OPEN_ISSUES
+

未合并 PR

$TOTAL_OPEN_PRS
+

已合并 PR

$TOTAL_MERGED_PRS
+

平均健康度

$AVG_HEALTH
+ +

仓库概览

- -DASHBOARD_ROWS_PLACEHOLDER + +$DASHBOARD_ROWS
RepositoryOpen IssuesClosed IssuesOpen PRsMerged PRsLatest ReleaseHealth
仓库未关闭 Issue已关闭 Issue未合并 PR已合并 PR最新 Release健康度
+ +

仓库详情

+$REPO_DETAILS
HTMLEOF -# Replace placeholders using temp file approach for complex content -TEMP_HTML=$(mktemp) -TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') +log_ok "看板已保存: $OUTPUT_FILE" -while IFS= read -r line; do - line="${line//TIMESTAMP_PLACEHOLDER/$TIMESTAMP}" - line="${line//ORG_PLACEHOLDER/$ORG}" - line="${line//REPOS_COUNT/${#REPO_LIST[@]}}" - line="${line//OPEN_ISSUES_COUNT/$TOTAL_OPEN_ISSUES}" - line="${line//OPEN_PRS_COUNT/$TOTAL_OPEN_PRS}" - line="${line//TOTAL_ACTIVITY/$TOTAL_ISSUES}" - line="${line//DASHBOARD_ROWS_PLACEHOLDER/$DASHBOARD_ROWS}" - echo "$line" -done < "$OUTPUT_FILE" > "$TEMP_HTML" - -mv "$TEMP_HTML" "$OUTPUT_FILE" - -log_ok "Dashboard saved to: $OUTPUT_FILE" - -# ── Step 5: Coordinated Release ────────────────────────────────────── +# ── Step 6: Coordinated Release ────────────────────────────────────── if [[ -n "$RELEASE_TAG" ]]; then - log_title "Coordinated Release: $RELEASE_TAG" + log_title "协调发版: $RELEASE_TAG" - RELEASE_BODY="# Coordinated Release: $RELEASE_TAG + if [[ "$DRY_RUN" == "true" ]]; then + log_warn "[DRY RUN] 将为以下 ${#REPO_LIST[@]} 个仓库创建 Release $RELEASE_TAG:" + for repo in "${REPO_LIST[@]}"; do + log_warn " [DRY RUN] $ORG/$repo → $RELEASE_TAG" + done + else + RELEASE_OK=0 + RELEASE_FAIL=0 + for repo in "${REPO_LIST[@]}"; do + log_step "为 $ORG/$repo 创建 Release..." + RELEASE_RESULT=$(gl_run release +create --owner "$ORG" --repo "$repo" \ + --tag "$RELEASE_TAG" --name "Release $RELEASE_TAG" \ + --body "Coordinated release $RELEASE_TAG for $ORG/$repo" 2>&1) || true -## Repos Included -" - - for repo in "${REPO_LIST[@]}"; do - log_step "Creating release for $ORG/$repo..." - RELEASE_BODY+="- $ORG/$repo"$'\n' - - RELEASE_RESULT=$(gl_run release +create --owner "$ORG" --repo "$repo" \ - --tag "$RELEASE_TAG" --name "Release $RELEASE_TAG" \ - --body "Coordinated release $RELEASE_TAG for $ORG/$repo" 2>&1) || true - - if [[ "$(json_ok "$RELEASE_RESULT")" == "true" ]]; then - log_ok "Release $RELEASE_TAG created for $repo" - else - log_warn "Release creation failed for $repo (tag may already exist)" - fi - done - - RELEASE_BODY+=$'\n'"---"$'\n'"*Coordinated release by gitlink-cli multi-repo-collab workflow*" + if [[ "$(json_ok "$RELEASE_RESULT")" == "true" ]]; then + log_ok "Release $RELEASE_TAG 已在 $repo 创建" + RELEASE_OK=$((RELEASE_OK + 1)) + else + log_warn "Release 创建失败: $repo (tag 可能已存在)" + RELEASE_FAIL=$((RELEASE_FAIL + 1)) + fi + done + log_info "协调发版结果: 成功 $RELEASE_OK / 失败 $RELEASE_FAIL" + fi fi # ───────────────────────────────────────────────────────────────────── -log_title "Multi-Repo Dashboard Complete" +log_title "多仓库看板完成" # ───────────────────────────────────────────────────────────────────── -echo -e "${GREEN}Summary:${NC}" -echo " Repos processed: ${#REPO_LIST[@]}" -echo " Total issues: $TOTAL_ISSUES (open: $TOTAL_OPEN_ISSUES)" -echo " Total PRs: $TOTAL_PRS (open: $TOTAL_OPEN_PRS)" -echo " Dashboard: $OUTPUT_FILE" -[[ -n "$RELEASE_TAG" ]] && echo " Coordinated release: $RELEASE_TAG" +echo -e "${GREEN}汇总:${NC}" +echo " 处理仓库: ${#REPO_LIST[@]}" +echo " 未关闭 Issue: $TOTAL_OPEN_ISSUES" +echo " 未合并 PR: $TOTAL_OPEN_PRS" +echo " 已合并 PR: $TOTAL_MERGED_PRS" +echo " 平均健康度: $AVG_HEALTH / 100" +echo " 看板文件: $OUTPUT_FILE" +[[ -n "$RELEASE_TAG" ]] && echo " 协调发版: $RELEASE_TAG$([[ "$DRY_RUN" == "true" ]] && echo " (dry-run)")" echo "" -echo -e "${CYAN}Open dashboard:${NC}" +echo -e "${CYAN}打开看板:${NC}" +echo " start $OUTPUT_FILE # Windows" echo " xdg-open $OUTPUT_FILE # Linux" echo " open $OUTPUT_FILE # macOS" echo "" diff --git a/workflows/05-contributor-growth.ps1 b/workflows/05-contributor-growth.ps1 index 30e02f8c..bf14d52d 100644 --- a/workflows/05-contributor-growth.ps1 +++ b/workflows/05-contributor-growth.ps1 @@ -28,7 +28,7 @@ param( ) $ErrorActionPreference = "Stop" -Import-Module "$PSScriptRoot/lib/common.psm1" -Force +Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue if ($Help) { Write-Host "Usage: powershell 05-contributor-growth.ps1 -Owner OWNER -Repo REPO [-Sample N] [-Award] [-DryRun]" @@ -55,11 +55,11 @@ Log-Title "Contributor Growth System: $Owner/$Repo" Log-Step "Collecting data..." $issuesOpen = Invoke-GLCheck "issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100" -$issuesClosed = Invoke-GL "issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100" +$issuesClosed = Invoke-GLCheck "issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100" $openCount = if ($issuesOpen) { @($issuesOpen.data.issues).Count } else { 0 } $closedCount = if ($issuesClosed) { @($issuesClosed.data.issues).Count } else { 0 } -$prsMerged = Invoke-GL "pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100" +$prsMerged = Invoke-GLCheck "pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100" $prMergedData = @() if ($prsMerged) { $pd = $prsMerged.data @@ -69,7 +69,7 @@ if ($prsMerged) { } $prMergedCount = $prMergedData.Count -$membersJson = Invoke-GL "repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "100" +$membersJson = Invoke-GLCheck "repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "100" $memberData = @() if ($membersJson) { $md = $membersJson.data @@ -119,7 +119,7 @@ for ($i = 0; $i -lt $prMergedCount; $i++) { $contribData[$author].Merged++ } if ($i -lt $prSample -and $prId -and $author) { - $filesJson = Invoke-GL "pr", "+files", "--owner", $Owner, "--repo", $Repo, "--id", $prId + $filesJson = Invoke-GLCheck "pr", "+files", "--owner", $Owner, "--repo", $Repo, "--id", $prId if ($filesJson -and $filesJson.data.files) { foreach ($f in $filesJson.data.files) { $add = if ($f.additions) { $f.additions } elseif ($f.addition) { $f.addition } else { 0 } @@ -148,9 +148,9 @@ if ($issuesOpen) { for ($i = 0; $i -lt $commentSample; $i++) { $id = $openIssuesArr[$i].id if (-not $id) { continue } - $detail = Invoke-GL "issue", "+view", "--owner", $Owner, "--repo", $Repo, "--number", $id - if ($detail) { - $commentCount = if ($detail.data.comment_journals_count) { $detail.data.comment_journals_count } else { 0 } + $detailJson = Invoke-GLCheck "issue", "+view", "--owner", $Owner, "--repo", $Repo, "--number", $id + if ($detailJson) { + $commentCount = if ($detailJson.data.comment_journals_count) { $detailJson.data.comment_journals_count } else { 0 } if ($commentCount -gt 0) { $author = $openIssuesArr[$i].author.login if ($author) { @@ -371,11 +371,27 @@ $wikiContent += $wikiRankRows + "`n" $wikiContent += "---" + "`n" $wikiContent += "*Auto-generated by gitlink-cli*" -$wikiResult = Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Repo, "--title", $wikiTitle, "--content", $wikiContent -if ($wikiResult -and (Get-JsonOk $wikiResult)) { - Log-Ok "Published to Wiki: $wikiTitle" +$wikiResult = Invoke-GL "wiki", "+update", "--owner", $Owner, "--repo", $Repo, "--title", $wikiTitle, "--cover", $wikiContent +if ($wikiResult -and (Get-JsonOk ($wikiResult | ConvertFrom-Json))) { + Log-Ok "Published to Wiki (updated): $wikiTitle" } else { - Log-Warn "Wiki publish failed" + Log-Info "Update failed (page may not exist), trying create..." + $wikiResult = Invoke-GL "wiki", "+create", "--owner", $Owner, "--repo", $Repo, "--title", $wikiTitle, "--content", $wikiContent + if ($wikiResult) { + try { + $wikiObj = $wikiResult | ConvertFrom-Json + if ($wikiObj.ok) { + Log-Ok "Published to Wiki (created): $wikiTitle" + } else { + $wikiErr = if ($wikiObj.error.message) { $wikiObj.error.message } elseif ($wikiObj.message) { $wikiObj.message } else { "unknown error" } + Log-Warn "Wiki publish failed: $wikiErr" + } + } catch { + Log-Warn "Wiki publish failed: non-JSON response from API" + } + } else { + Log-Warn "Wiki publish failed: no response from API" + } } # -- Step 7: Award Badges (optional) -- @@ -409,8 +425,8 @@ if ($Award) { $issueResult = Invoke-GL "issue", "+create", "--owner", $Owner, "--repo", $Repo, "--title", $issueTitle, "--body", $issueBody if ($issueResult) { try { - $issueJson = $issueResult - $issueNum = if ($issueJson.data.id) { $issueJson.data.id } elseif ($issueJson.data.number) { $issueJson.data.number } else { $null } + $issueJson = $issueResult | ConvertFrom-Json + $issueNum = if ($issueJson.ok -and $issueJson.data.id) { $issueJson.data.id } elseif ($issueJson.ok -and $issueJson.data.number) { $issueJson.data.number } else { $null } if ($issueNum) { Invoke-GL "issue", "+label-add", "--owner", $Owner, "--repo", $Repo, "--number", $issueNum, "--labels", "badge" | Out-Null Log-Ok "Badge issue created: #$issueNum - $issueTitle ($($users.Count) recipients)" diff --git a/workflows/README.md b/workflows/README.md index 1596eae8..769d407c 100644 --- a/workflows/README.md +++ b/workflows/README.md @@ -60,7 +60,7 @@ gitlink-cli issue +list --owner zzx-coder --repo gitlink-cli --state open --limi |---|------|------|---------|-------------| | 1 | 社区运营自动化 | `01-community-ops.sh` | 7 个 | Issue 积压无人处理、周报手写、Release Notes 手动整理 | | 2 | 代码质量看门人 | `02-code-quality-gatekeeper.sh` | 7 个 | PR 审查效率低、质量标准不统一、AI 代码审查(基于 gitlink-code-review skill) | -| 3 | 项目一键初始化 | `03-project-init.sh` | 6 个 | 新建项目重复劳动多、Issue/文档/分支保护手动配 | +| 3 | 项目一键初始化 | `03-project-init.sh` | 8 个 | 新建项目重复劳动多、README/许可证/CI/Issue/里程碑手动配 | | 4 | 多仓库协同 | `04-multi-repo-collab.sh` | 7 个 | 跨仓库状态分散、缺乏统一视图 | | 5 | 贡献者成长体系 | `05-contributor-growth.sh` | 6 个 | 贡献者活跃度难追踪、缺乏激励机制 | @@ -236,21 +236,29 @@ bash workflows/02-code-quality-gatekeeper.sh --owner zzx-coder --repo gitlink-cl ## 场景三:项目一键初始化 -**脚本**: `03-project-init.sh` +**脚本**: `03-project-init.sh` / `03-project-init.ps1` ### 解决什么问题 -新建项目仓库后,还要手动创建 README、写 CONTRIBUTING 指南、创建初始 Issue、设置分支保护、打初始 Release。一条命令搞定全部。 +新建项目仓库后,还要手动创建 README、写 LICENSE、配置 CI/CD、创建 Issue 和里程碑、设置分支保护、打初始 Release。一条命令搞定全部。 ### 工作流程 ``` repo +create → 创建仓库 ↓ -wiki +create → 生成 README(根据语言模板) -wiki +create → 生成 CONTRIBUTING 贡献指南 +git clone → 克隆空仓库到本地 ↓ -issue +create × 5 → 创建初始待办 Issue: +生成文件 → README.md (语言模板) + LICENSE (MIT) + CONTRIBUTING.md + .gitlink-ci.yml + ↓ +git add/commit/push → 将脚手架文件推送到仓库 + ↓ +milestone +create × 3 → 创建项目里程碑: + - v0.1.0 - MVP + - v0.2.0 - Feature Complete + - v1.0.0 - Production Ready + ↓ +issue +create × 5 → 创建初始待办 Issue (打标签): - 搭建 CI/CD 流水线 - 编写项目文档 - 建立代码审查流程 @@ -267,15 +275,19 @@ release +create → 创建 v0.1.0 初始版本 | 步骤 | 命令 | 作用 | |------|------|------| | 1 | `repo +create` | 创建新仓库 | -| 2 | `wiki +create` | 生成 README(支持 Go/Python/Node/Java) | -| 3 | `wiki +create` | 生成 CONTRIBUTING 贡献指南 | -| 4 | `issue +create` | 创建 5 个初始 Issue 并打标签 | -| 5 | `branch +protect` | 设置 master 分支保护规则 | -| 6 | `release +create` | 创建 v0.1.0 初始版本 | +| 2 | `git clone` | 克隆空仓库到本地临时目录 | +| 3 | 文件生成 | 生成 README.md / LICENSE / CONTRIBUTING.md / .gitlink-ci.yml | +| 4 | `git add/commit/push` | 推送脚手架文件到 master 分支 | +| 5 | `milestone +create` | 创建 3 个项目里程碑 (MVP → Production) | +| 6 | `issue +create` | 创建 5 个初始 Issue 并打标签 | +| 7 | `branch +protect` | 设置 master 分支保护规则 | +| 8 | `release +create` | 创建 v0.1.0 初始版本 | ### 输出有什么用 -- **开箱即用**: 新成员克隆后就知道怎么构建、测试、贡献 +- **开箱即用**: 克隆后仓库已有 README、LICENSE、CI 配置,可直接开始开发 +- **仓库内文件**: README/LICENSE/CONTRIBUTING/CI 是仓库里的真实文件,不是 Wiki 页面 +- **里程碑规划**: 从 MVP 到正式版的路线图已建立 - **标准化 Issue**: 关键待办已创建好,团队可直接认领 - **分支保护**: 防止直接 push 到 master,强制走 PR 流程 - **首个 Release**: 项目从创建之初就有版本管理 diff --git a/workflows/SKILL.md b/workflows/SKILL.md index deae11a9..cd612563 100644 --- a/workflows/SKILL.md +++ b/workflows/SKILL.md @@ -51,7 +51,7 @@ metadata: → PR Review、AI 四维度评分、自动合并 3. 项目一键初始化 - → 创建仓库、README、CI 配置、初始 Issues、分支保护 + → 创建仓库、README/LICENSE/CI 文件、里程碑、初始 Issues、分支保护 4. 多仓库协同 → 跨仓库 Issue/PR 追踪、状态 Dashboard、协同发版 diff --git a/workflows/academic/06-research-insights.ps1 b/workflows/academic/06-research-insights.ps1 index 07ece166..d7fd011a 100644 --- a/workflows/academic/06-research-insights.ps1 +++ b/workflows/academic/06-research-insights.ps1 @@ -1,11 +1,11 @@ -# GitLink 科研辅助 — 场景 1:仓库级科研项目洞察 (PowerShell) +# GitLink Research - Scenario 1: Repository Project Insight (PowerShell) param( [string]$Owner, [string]$Repo, [string]$Output = "", [switch]$NoWiki, [switch]$DryRun ) $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -Import-Module "$ScriptDir\lib\common.psm1" -Force +Import-Module "$ScriptDir\..\lib\common.psm1" -Force -WarningAction SilentlyContinue $ErrorActionPreference = "Continue" Check-Auth @@ -16,48 +16,50 @@ $OutputDir = Join-Path $ScriptDir "..\output" if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null } $OutputFile = if ($Output) { $Output } else { Join-Path $OutputDir "research-insights-${Repo}-${Today}.html" } -Log-Title "GitLink 科研辅助 — 仓库级项目洞察" +Log-Title "GitLink Research - Repository Project Insight" # 1. Repo metadata -Log-Step "1/6 获取仓库元数据..." +Log-Step "1/6 Fetching repo metadata..." $repoJson = Invoke-GLCheck @("repo", "+info", "--owner", $Owner, "--repo", $Repo) -$repoName = $repoJson.data.name ?? $repoJson.data.full_name ?? $Repo -$repoDesc = $repoJson.data.description ?? "No description" -$repoLang = $repoJson.data.language ?? "Unknown" -$stars = [int]($repoJson.data.stars_count ?? $repoJson.data.stars ?? 0) -$forks = [int]($repoJson.data.forks_count ?? $repoJson.data.forks ?? 0) -$openIssues = [int]($repoJson.data.open_issues_count ?? 0) -$updatedAt = $repoJson.data.updated_at ?? "" +$repoName = if ($repoJson.data.name) { $repoJson.data.name } elseif ($repoJson.data.full_name) { $repoJson.data.full_name } else { $Repo } +$repoDesc = if ($repoJson.data.description) { $repoJson.data.description } else { "No description" } +$repoLang = if ($repoJson.data.language) { $repoJson.data.language } else { "Unknown" } +$stars = if ($repoJson.data.stars_count) { [int]$repoJson.data.stars_count } elseif ($repoJson.data.stars) { [int]$repoJson.data.stars } else { 0 } +$forks = if ($repoJson.data.forks_count) { [int]$repoJson.data.forks_count } elseif ($repoJson.data.forks) { [int]$repoJson.data.forks } else { 0 } +$openIssues = if ($repoJson.data.open_issues_count) { [int]$repoJson.data.open_issues_count } else { 0 } +$updatedAt = if ($repoJson.data.updated_at) { $repoJson.data.updated_at } else { "" } -Log-Info " 名称: $repoName | 语言: $repoLang | Stars: $stars" +Log-Info " Name: $repoName | Lang: $repoLang | Stars: $stars" # 2. Issues -Log-Step "2/6 收集 Issue 数据..." -$openIssuesJson = Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100") -$closedIssuesJson = Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100") -$totalOpen = if ($openIssuesJson.ok) { @($openIssuesJson.data.issues ?? $openIssuesJson.data).Count } else { 0 } -$totalClosed = if ($closedIssuesJson.ok) { @($closedIssuesJson.data.issues ?? $closedIssuesJson.data).Count } else { 0 } +Log-Step "2/6 Collecting Issue data..." +$openIssuesJson = Invoke-GLCheck @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100") +$closedIssuesJson = Invoke-GLCheck @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100") +$totalOpen = if ($openIssuesJson.ok) { $d = if ($openIssuesJson.data.issues) { $openIssuesJson.data.issues } else { $openIssuesJson.data }; @($d).Count } else { 0 } +$totalClosed = if ($closedIssuesJson.ok) { $d = if ($closedIssuesJson.data.issues) { $closedIssuesJson.data.issues } else { $closedIssuesJson.data }; @($d).Count } else { 0 } # 3. PRs -Log-Step "3/6 收集 PR 数据..." -$mergedPrsJson = Invoke-GL @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100") -$openPrsJson = Invoke-GL @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "50") -$totalMerged = if ($mergedPrsJson.ok) { @($mergedPrsJson.data.issues ?? $mergedPrsJson.data.pulls ?? $mergedPrsJson.data).Count } else { 0 } -$totalOpenPrs = if ($openPrsJson.ok) { @($openPrsJson.data.issues ?? $openPrsJson.data.pulls ?? $openPrsJson.data).Count } else { 0 } +Log-Step "3/6 Collecting PR data..." +$mergedPrsJson = Invoke-GLCheck @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100") +$openPrsJson = Invoke-GLCheck @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "50") +$md = if ($mergedPrsJson.data.issues) { $mergedPrsJson.data.issues } elseif ($mergedPrsJson.data.pulls) { $mergedPrsJson.data.pulls } else { $mergedPrsJson.data } +$totalMerged = if ($mergedPrsJson.ok) { @($md).Count } else { 0 } +$od = if ($openPrsJson.data.issues) { $openPrsJson.data.issues } elseif ($openPrsJson.data.pulls) { $openPrsJson.data.pulls } else { $openPrsJson.data } +$totalOpenPrs = if ($openPrsJson.ok) { @($od).Count } else { 0 } # 4. Releases -Log-Step "4/6 收集 Release 数据..." -$releasesJson = Invoke-GL @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "20") +Log-Step "4/6 Collecting Release data..." +$releasesJson = Invoke-GLCheck @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "20") $releaseCount = if ($releasesJson.ok) { @($releasesJson.data).Count } else { 0 } # 5. CI -Log-Step "5/6 收集 CI 数据..." -$ciJson = Invoke-GL @("ci", "+builds", "--owner", $Owner, "--repo", $Repo, "--limit", "20") -$ciBuilds = if ($ciJson.ok) { @($ciJson.data).Count } else { 0 } +Log-Step "5/6 Collecting CI data..." +$ciPipelines = Get-CIPipelines $Owner $Repo +$ciBuilds = $ciPipelines.Count # 6. Members -Log-Step "6/6 获取贡献者..." -$membersJson = Invoke-GL @("repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "50") +Log-Step "6/6 Fetching contributors..." +$membersJson = Invoke-GLCheck @("repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "50") $memberCount = if ($membersJson.ok) { $d = if ($membersJson.data.members) { $membersJson.data.members } else { $membersJson.data } if ($d -is [array]) { $d.Count } else { 0 } @@ -75,18 +77,294 @@ $hotness = [Math]::Round($starsNorm * 0.15 + $forksNorm * 0.10 + $issuesScore * $prMergeRate = if (($totalMerged + $totalOpenPrs) -gt 0) { [Math]::Round($totalMerged / ($totalMerged + $totalOpenPrs) * 100, 1) } else { 0 } -Log-Ok "热度评分: ${hotness}/100" -Log-Info " Issues: $totalOpen 开放 / $totalClosed 关闭" -Log-Info " PR 合并率: ${prMergeRate}% | Releases: $releaseCount | 贡献者: $memberCount" +Log-Ok "Hotness score: ${hotness}/100" +Log-Info " Issues: $totalOpen open / $totalClosed closed" +Log-Info " PR merge rate: ${prMergeRate}% | Releases: $releaseCount | Contributors: $memberCount" + +# Additional data for HTML report +$ciPassRate = 0 +if ($ciBuilds -gt 0) { + $ciSuccess = 0 + foreach ($b in $ciPipelines) { + if (Test-CISuccess $b) { $ciSuccess++ } + } + $ciPassRate = [Math]::Round($ciSuccess / $ciBuilds * 100, 1) +} + +$daysSinceUpdate = 365 +if ($updatedAt -and $updatedAt.Length -ge 10) { + try { $daysSinceUpdate = ((Get-Date) - [DateTime]$updatedAt.Substring(0, 10)).Days } catch { $daysSinceUpdate = 365 } +} +$recencyFactor = if ($daysSinceUpdate -le 30) { 100 } elseif ($daysSinceUpdate -le 90) { 50 } else { 10 } + +$hotnessLabel = if ($hotness -ge 50) { "Hot" } elseif ($hotness -ge 30) { "Warm" } else { "Cool" } + +$starsNorm = [Math]::Round([Math]::Min($stars / 1000.0, 1.0) * 100, 1) +$forksNorm = [Math]::Round([Math]::Min($forks / 200.0, 1.0) * 100, 1) +$issuesActive = [Math]::Round([Math]::Min($totalOpen / 50.0, 1.0) * 100, 1) +$prsActive = [Math]::Round([Math]::Min(($totalMerged + $totalOpenPrs) / 30.0, 1.0) * 100, 1) +$releasesActive = [Math]::Round([Math]::Min($releaseCount / 10.0, 1.0) * 100, 1) + +$createdAt = if ($repoJson.data.created_at) { $repoJson.data.created_at } else { "" } +$createdAtDisplay = if ($createdAt.Length -ge 10) { $createdAt.Substring(0, 10) } else { "Unknown" } +$updatedAtDisplay = if ($updatedAt.Length -ge 10) { $updatedAt.Substring(0, 10) } else { "Unknown" } + +# Detect tech stack +Log-Step "Detecting tech stack..." +$techStack = $repoLang +$researchFeatures = "" +$hasCitationCff = $false +$subJson = try { Invoke-GLCheck @("api", "GET", "/v1/$Owner/$Repo/sub_entries?ref=master") } catch { $null } +if ($subJson -and $subJson.ok -and ($subJson.data -is [array])) { + $names = @($subJson.data | ForEach-Object { if ($_.name) { $_.name } else { "" } }) + $ecosystem = @() + if ($names -contains "go.mod") { $ecosystem += "Go" } + if ($names -contains "package.json") { $ecosystem += "Node.js" } + if ($names | Where-Object { $_ -match "requirements\.txt|pyproject\.toml|setup\.py|setup\.cfg|Pipfile" }) { $ecosystem += "Python" } + if ($names -contains "Cargo.toml") { $ecosystem += "Rust" } + if ($names -contains "CMakeLists.txt") { $ecosystem += "C/C++" } + if ($names | Where-Object { $_ -match "pom\.xml|build\.gradle" }) { $ecosystem += "Java/Kotlin" } + if ($names -contains "CITATION.cff") { $hasCitationCff = $true; $ecosystem += "+CITATION.cff" } + if ($ecosystem.Count -gt 0) { $techStack = ($ecosystem -join " ") } + if ($names | Where-Object { $_ -match "Dockerfile|docker-compose" }) { $researchFeatures += " Docker" } + if ($names | Where-Object { $_ -match "^data/|^datasets/" }) { $researchFeatures += " dataset" } + if ($names | Where-Object { $_ -match "\.ipynb$" }) { $researchFeatures += " Jupyter" } + if ($names | Where-Object { $_ -match "^scripts/|^experiments/" }) { $researchFeatures += " experiment-scripts" } +} +Log-Info " Tech stack: $techStack" + +# Readme analysis +Log-Step "Reading README for project positioning..." +$readmeText = "" +try { + $readmeResult = Invoke-GLCheck @("api", "GET", "raw/$Owner/$Repo/master/README.md") + if ($readmeResult.ok) { $readmeText = if ($readmeResult.data) { $readmeResult.data } else { "" } } +} catch { $readmeText = "" } + +$projectTopics = "" +$topicKeywords = @("machine learning","deep learning","neural network","NLP","computer vision", + "reinforcement learning","GAN","transformer","LLM","RAG","agent","bioinformatics","genomics", + "drug discovery","robotics","autonomous","simulation","optimization","benchmark","dataset", + "pre-trained","fine-tuning","distributed","federated","graph neural","knowledge graph", + "recommender","anomaly detection","scientific computing","HPC","quantum","climate","physics", + "chemistry") +foreach ($kw in $topicKeywords) { + if ($repoDesc -match [regex]::Escape($kw) -or $readmeText -match [regex]::Escape($kw)) { + if ($projectTopics) { $projectTopics += ", " } + $projectTopics += $kw + } +} + +$doiFound = "" +if ($repoDesc -match '10\.\d{4,}/[\w.\-/]+') { $doiFound = $Matches[0] } +elseif ($readmeText -match '10\.\d{4,}/[\w.\-/]+') { $doiFound = $Matches[0] } + +# ===== Generate HTML Report ===== +Log-Step "Generating HTML report..." +$hotnessClass = $hotnessLabel.ToLower() + +# Build tech stack tags +$techStackTags = "" +foreach ($t in ($techStack -split '\s+')) { + if ($t) { $techStackTags += "$t " } +} + +# Build research features tags +$researchTags = "" +foreach ($f in ($researchFeatures -split '\s+' | Where-Object { $_ })) { + $researchTags += "$f " +} +if (-not $researchTags) { $researchTags = "None detected" } + +# Build topic tags +$topicTags = "" +foreach ($t in ($projectTopics -split ', ' | Where-Object { $_ })) { + $topicTags += "$t " +} +if (-not $topicTags) { $topicTags = "None detected" } + +$issueStatus = if ($totalOpen -gt 20) { "Needs attention" } else { "Normal" } +$prStatus = if ($prMergeRate -gt 70) { "Healthy" } else { "Needs improvement" } +$releaseStatus = if ($releaseCount -gt 0) { "Released" } else { "No release" } +$ciStatus = if ($ciPassRate -gt 80) { "Stable" } else { "Unstable" } +$memberStatus = if ($memberCount -gt 3) { "Active community" } else { "Solo project" } +$activityStatus = if ($daysSinceUpdate -le 30) { "Active" } else { "Inactive" } + +$doiRow = "" +if ($doiFound) { $doiRow = "DOI$doiFound" } + +$htmlContent = @" + + + + + +$Owner/$Repo — Research Project Insight Report + + + + +
+

$Owner/$Repo

+
Research Project Insight Report — $Today
+
+
+ +
+
+
Hotness Score
+
$hotness
+
$hotnessLabel
+
+
+
Stars
+
$stars
+
+
+
Forks
+
$forks
+
+
+
Contributors
+
$memberCount
+
+
+
Open Issues
+
$totalOpen
+
+
+
PR Merge Rate
+
${prMergeRate}%
+
+
+ +
+
+

Project Overview

+ + + + + + + + + $doiRow + +
Name$repoName
Description$repoDesc
Language$repoLang
Tech Stack$techStackTags
Created$createdAtDisplay
Updated$updatedAtDisplay ($daysSinceUpdate days ago)
Research Features$researchTags
Topics$topicTags
+
+ +
+

Activity Overview

+
+
+
+ +
+
+

Health Indicators

+ + + + + + + + +
IndicatorValueStatus
Total Issues$totalOpen open / $totalClosed closed$issueStatus
PR Merge Rate${prMergeRate}%$prStatus
Releases$releaseCount$releaseStatus
CI Pass Rate${ciPassRate}% ($ciBuilds builds)$ciStatus
Contributors$memberCount people$memberStatus
Activity$daysSinceUpdate days since update$activityStatus
+
+ +
+

Hotness Composition

+
+
+
+ +
+ + + + + +"@ + +if (-not $DryRun) { + $htmlContent | Out-File -FilePath $OutputFile -Encoding UTF8 + Log-Ok "HTML report generated: $OutputFile" +} else { + Log-Warn "[DRY RUN] Would generate: $OutputFile" +} # Summary output Divider -Write-Host "====== 报告摘要 ======" -ForegroundColor White -Write-Host " 仓库: $Owner/$Repo" -Write-Host " 语言: $repoLang" -Write-Host " 热度评分: $hotness/100" -Write-Host " Issues: $totalOpen 开放 / $totalClosed 关闭" -Write-Host " PR 合并率: ${prMergeRate}%" -Write-Host " 贡献者: $memberCount 人" +Write-Host "====== Analysis Summary ======" -ForegroundColor White +Write-Host " Repo: $Owner/$Repo" +Write-Host " Language: $repoLang" +Write-Host " Tech Stack: $techStack" +Write-Host " Hotness: $hotness/100 ($hotnessLabel)" +Write-Host " Issues: $totalOpen open / $totalClosed closed" +Write-Host " PR merge: ${prMergeRate}%" +Write-Host " CI pass: ${ciPassRate}%" +Write-Host " Members: $memberCount" +Write-Host " Features: $researchFeatures" +if ($doiFound) { Write-Host " DOI: $doiFound" } +Write-Host " Report: $OutputFile" Divider -Log-Ok "分析完成" +Log-Ok "Analysis complete" diff --git a/workflows/academic/08-research-compliance.ps1 b/workflows/academic/08-research-compliance.ps1 index 5545d9f2..ab87d709 100644 --- a/workflows/academic/08-research-compliance.ps1 +++ b/workflows/academic/08-research-compliance.ps1 @@ -1,11 +1,11 @@ -# GitLink 科研辅助 — 场景 3:合规与复现性检查 (PowerShell) +# GitLink Research - Scenario 3: Compliance & Reproducibility Check (PowerShell) param( [string]$Owner, [string]$Repo, [string]$LocalPath = ".", [string]$Output = "", [switch]$NoWiki, [switch]$DryRun ) $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -Import-Module "$ScriptDir\lib\common.psm1" -Force +Import-Module "$ScriptDir\..\lib\common.psm1" -Force -WarningAction SilentlyContinue $ErrorActionPreference = "Continue" Check-Auth @@ -14,8 +14,16 @@ $Owner = $resolved.Owner; $Repo = $resolved.Repo $Today = Get-Date -Format "yyyy-MM-dd" $OutputDir = Join-Path $ScriptDir "..\output" if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null } +$OutputFile = if ($Output) { $Output } else { Join-Path $OutputDir "reproducibility-${Repo}-${Today}.html" } -Log-Title "GitLink 科研辅助 — 合规与复现性检查" +Log-Title "GitLink Research - Compliance & Reproducibility" + +# Get repo metadata for description and DOI detection +Log-Step "Fetching repo metadata..." +$repoJson = Invoke-GLCheck @("repo", "+info", "--owner", $Owner, "--repo", $Repo) +$repoDesc = if ($repoJson.data.description) { $repoJson.data.description } else { "" } +$doiFound = "" +if ($repoDesc -match '10\.\d{4,}/[\w.\-/]+') { $doiFound = $Matches[0] } # Scoring: 0, 0.5, 1.0 per dimension $dLicense = @{Score=0; Detail=""}; $dNoSecret = @{Score=0; Detail=""} @@ -23,91 +31,205 @@ $dReadme = @{Score=0; Detail=""}; $dDeps = @{Score=0; Detail=""} $dBuild = @{Score=0; Detail=""}; $dCI = @{Score=0; Detail=""} $dTest = @{Score=0; Detail=""}; $dData = @{Score=0; Detail=""} -# 1. Compliance scan -Log-Step "1/7 合规扫描..." -if (Test-Path (Join-Path $LocalPath ".git")) { - Push-Location $LocalPath - $compResult = Invoke-GL @("compliance", "+scan") - Pop-Location - if ($compResult.ok) { - $licenseOk = $compResult.data.license.status ?? "" - if ($licenseOk -match "ok|clean|found") { $dLicense.Score = 1.0; $dLicense.Detail = "检测到合规许可证" } - elseif ($licenseOk -eq "warning") { $dLicense.Score = 0.5; $dLicense.Detail = "有许可证但非标准" } - else { $dLicense.Score = 0; $dLicense.Detail = "未检测到 LICENSE" } +# -- Helper: ensure local clone of target repo for compliance scan -- +function Ensure-LocalClone { + param([string]$Owner, [string]$Repo, [string]$LocalPath) - $secCount = @($compResult.data.secrets.findings ?? @()).Count - $piiCount = @($compResult.data.exposure.findings ?? @()).Count - if ($secCount -eq 0 -and $piiCount -eq 0) { $dNoSecret.Score = 1.0; $dNoSecret.Detail = "未发现密钥/PII" } - elseif ($secCount + $piiCount -le 3) { $dNoSecret.Score = 0.5; $dNoSecret.Detail = "发现少量可疑项" } - else { $dNoSecret.Score = 0; $dNoSecret.Detail = "发现多处密钥/PII泄露" } + $result = @{ ScanPath = $null; NeedCleanup = $false } + + # Check if current directory is already a clone of the target repo + if (Test-Path (Join-Path $LocalPath ".git")) { + Push-Location $LocalPath + try { + $remoteUrl = git remote get-url origin 2>$null + if ($remoteUrl -and ($remoteUrl -match [regex]::Escape("$Owner/$Repo"))) { + Pop-Location + Log-Info "Local path is already a clone of $Owner/$Repo" + $result.ScanPath = $LocalPath + return $result + } + } catch { } + Pop-Location } -} else { Log-Warn "本地仓库路径无 .git,跳过合规扫描" } + + # Clone target repo to temp directory for compliance scan + $suffix = [System.Guid]::NewGuid().ToString().Substring(0, 8) + $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "gl-compliance-${Repo}-${suffix}" + Log-Info "Cloning $Owner/$Repo to temp directory for compliance scan..." + + $cloneUrl = if ($env:GITLINK_TOKEN) { + "https://oauth2:$env:GITLINK_TOKEN@gitlink.org.cn/$Owner/$Repo.git" + } else { + "https://gitlink.org.cn/$Owner/$Repo.git" + } + + $cloneOutput = git clone --depth 1 $cloneUrl $tempDir 2>&1 + if ($LASTEXITCODE -ne 0) { + Log-Warn "HTTPS clone failed, trying SSH..." + $sshUrl = "git@gitlink.org.cn:$Owner/$Repo.git" + $cloneOutput = git clone --depth 1 $sshUrl $tempDir 2>&1 + if ($LASTEXITCODE -ne 0) { + Log-Warn "Clone failed for both HTTPS and SSH. Skipping compliance scan." + Log-Warn "Reason: $($cloneOutput -join '; ')" + return $result + } + } + + Log-Ok "Cloned successfully to $tempDir" + $result.ScanPath = $tempDir + $result.NeedCleanup = $true + return $result +} + +# 1. Compliance scan + build file inventory from local clone +Log-Step "1/7 Compliance scan..." +$cloneInfo = Ensure-LocalClone -Owner $Owner -Repo $Repo -LocalPath $LocalPath +$scanPath = $cloneInfo.ScanPath + +# Build a recursive relative file list ($names) and load README text from the +# local clone. The remote `raw/...` and `/v1/.../sub_entries` content endpoints +# are NOT valid API routes on this GitLink server (they return the SPA HTML), +# so all file-based detection (README, deps, build, tests, license) reads from +# the clone that Ensure-LocalClone already provides. +$names = @() +$readmeText = "" +if ($scanPath -and (Test-Path $scanPath)) { + Push-Location $scanPath + try { + $names = @(Get-ChildItem -Recurse -File -Force -ErrorAction SilentlyContinue -Name | + Where-Object { $_ -notmatch '^\.git[\\/]' -and $_ -notmatch '[\\/]node_modules[\\/]' } | + ForEach-Object { ($_ -replace '\\', '/') }) + foreach ($rn in @("README.md","README.MD","README","README.rst","README.txt","README.zh-CN.md","readme.md")) { + if (Test-Path $rn) { $readmeText = Get-Content -Raw -Path $rn -Encoding UTF8 -ErrorAction SilentlyContinue; break } + } + } catch { } + Pop-Location +} else { + # Fallback: no local clone — use the working entries endpoint for a root file list + $defaultBranch = if ($repoJson.data.default_branch) { $repoJson.data.default_branch } else { "master" } + $entriesRes = Invoke-GLCheck @("api", "GET", "/$Owner/$Repo/entries?ref=$defaultBranch") + if ($entriesRes.ok -and $entriesRes.data.entries) { + $names = @($entriesRes.data.entries | ForEach-Object { if ($_.name) { ($_.name -replace '\\','/') } else { "" } }) + } +} + +# Secrets / PII via compliance +scan (runs inside the clone) +if ($scanPath -and (Test-Path (Join-Path $scanPath ".git"))) { + Push-Location $scanPath + $compResult = Invoke-GLCheck @("compliance", "+scan") + Pop-Location + # NOTE: compliance +scan has NO license module (modules = secrets/exposure/vocab), + # and findings are a FLAT array with a .module field — not nested under + # .data.secrets/.data.exposure. License is detected separately below. + if ($compResult.ok -and $compResult.data.findings) { + $allFindings = @($compResult.data.findings) + $secCount = @($allFindings | Where-Object { $_.module -eq 'secrets' }).Count + $piiCount = @($allFindings | Where-Object { $_.module -eq 'exposure' }).Count + $totalFindings = $secCount + $piiCount + if ($totalFindings -eq 0) { $dNoSecret.Score = 1.0; $dNoSecret.Detail = "No secrets/PII found" } + elseif ($totalFindings -le 3) { $dNoSecret.Score = 0.5; $dNoSecret.Detail = "Few suspicious items ($totalFindings)" } + else { $dNoSecret.Score = 0; $dNoSecret.Detail = "Multiple secret/PII leaks ($totalFindings)" } + } else { + $dNoSecret.Score = 1.0; $dNoSecret.Detail = "No secrets/PII found" + } +} else { + Log-Warn "Cannot access repo for compliance scan, skipping secrets scan" + $dNoSecret.Detail = "Not scanned (repo inaccessible)" +} + +# License: detected from file inventory (compliance scanner has no license module) +$licenseFile = $names | Where-Object { $_ -match '(^|/)(LICENSE|LICENCE|COPYING|UNLICENCE)(\.(md|txt))?$' } | Select-Object -First 1 +if ($licenseFile) { $dLicense.Score = 1.0; $dLicense.Detail = "License file found: $licenseFile" } +elseif ($readmeText -match 'MIT License|Apache License|GPL|BSD') { $dLicense.Score = 0.5; $dLicense.Detail = "License mentioned in README" } +else { $dLicense.Score = 0; $dLicense.Detail = "No LICENSE file" } # 2. README -Log-Step "2/7 README 完整性..." -try { - $readmeResult = Invoke-GL @("api", "GET", "raw/$Owner/$Repo/master/README.md") - if ($readmeResult.ok) { $readmeText = $readmeResult.data ?? "" } else { $readmeText = "" } -} catch { $readmeText = "" } +Log-Step "2/7 README completeness..." +# README text was already loaded from the local clone in step 1. $sectionCount = 0 foreach ($kw in @("# ", "## ", "Install", "Usage", "License", "Contribut", "Citation")) { if ($readmeText -match $kw) { $sectionCount++ } } -if ($sectionCount -ge 5) { $dReadme.Score = 1.0 } -elseif ($sectionCount -ge 3) { $dReadme.Score = 0.5 } -else { $dReadme.Score = 0 } -$dReadme.Detail = "README 章节数: $sectionCount" -Log-Info " $($dReadme.Detail)" +if ($sectionCount -ge 5) { $dReadme.Score = 1.0; $dReadme.Detail = "README complete, $sectionCount sections" } +elseif ($sectionCount -ge 3) { $dReadme.Score = 0.5; $dReadme.Detail = "README partial, $sectionCount sections" } +else { $dReadme.Score = 0; $dReadme.Detail = "README missing or too short" } +Log-Info " README sections: $sectionCount" # 3. Dependencies -Log-Step "3/7 依赖声明..." -$subResult = Invoke-GL @("api", "GET", "/v1/$Owner/$Repo/sub_entries?ref=master") +Log-Step "3/7 Dependency declaration..." +# $names (relative file paths from the clone) was built in step 1. $depFiles = 0 -if ($subResult.ok) { - $names = @($subResult.data | ForEach-Object { $_.name ?? "" }) - foreach ($df in @("package.json","go.mod","requirements.txt","pyproject.toml","Cargo.toml","CMakeLists.txt","pom.xml","build.gradle")) { - if ($names -contains $df) { $depFiles++ } - } +$depList = "" +foreach ($df in @("package.json","go.mod","requirements.txt","pyproject.toml","setup.py","setup.cfg","Cargo.toml","CMakeLists.txt","pom.xml","build.gradle","Gemfile","Makefile")) { + if ($names | Where-Object { (Split-Path $_ -Leaf) -eq $df }) { $depFiles++; $depList += "$df, " } } -if ($depFiles -ge 1) { $dDeps.Score = 1.0 } -elseif ($readmeText -match "dependenc|requirement|依赖|install") { $dDeps.Score = 0.5 } -else { $dDeps.Score = 0 } -$dDeps.Detail = "依赖文件数: $depFiles" +if ($depFiles -ge 1) { $dDeps.Score = 1.0; $dDeps.Detail = "Standard dep file(s): $depList".TrimEnd(', ') } +elseif ($readmeText -match "dependenc|requirement|install") { $dDeps.Score = 0.5; $dDeps.Detail = "Deps mentioned in README" } +else { $dDeps.Score = 0; $dDeps.Detail = "No dependency declaration" } +Log-Info " Dep files: $depFiles" # 4. Build -Log-Step "4/7 构建说明..." +Log-Step "4/7 Build instructions..." $buildScore = 0 -if ($readmeText -match "build|install|compile|make|构建|安装|编译") { $buildScore++ } -if ($subResult.ok -and (@($subResult.data | Where-Object { $_.name -match "Makefile|Dockerfile" })).Count -gt 0) { $buildScore++ } -if ($buildScore -ge 2) { $dBuild.Score = 1.0 } elseif ($buildScore -ge 1) { $dBuild.Score = 0.5 } else { $dBuild.Score = 0 } -$dBuild.Detail = "构建说明得分: $buildScore/2" +if ($readmeText -match "build|install|compile|make|run") { $buildScore++ } +if ($names | Where-Object { $_ -match "Makefile|Dockerfile|docker-compose" }) { $buildScore++ } +if ($names | Where-Object { $_ -match "\.github/workflows|\.gitlab-ci|Jenkinsfile" }) { $buildScore++ } + +if ($buildScore -ge 3) { $dBuild.Score = 1.0; $dBuild.Detail = "Detailed build docs + automation" } +elseif ($buildScore -ge 1) { $dBuild.Score = 0.5; $dBuild.Detail = "Partial build instructions" } +else { $dBuild.Score = 0; $dBuild.Detail = "No build instructions" } +Log-Info " Build score: $buildScore/3" # 5. CI -Log-Step "5/7 CI 配置..." -$ciResult = Invoke-GL @("ci", "+builds", "--owner", $Owner, "--repo", $Repo, "--limit", "10") -$ciBuilds = if ($ciResult.ok) { @($ciResult.data).Count } else { 0 } -if ($ciBuilds -gt 0) { $dCI.Score = 1.0; $dCI.Detail = "CI 已配置 ($ciBuilds 次构建)" } -else { $dCI.Score = 0; $dCI.Detail = "无 CI 配置" } +Log-Step "5/7 CI configuration..." +$ciPipelines = Get-CIPipelines $Owner $Repo +$ciBuilds = $ciPipelines.Count +$ciOk = 0 +foreach ($b in $ciPipelines) { + if (Test-CISuccess $b) { $ciOk++ } +} +if ($ciBuilds -gt 0 -and $ciOk -ge 1) { $dCI.Score = 1.0; $dCI.Detail = "CI configured & passing ($ciOk/$ciBuilds)" } +elseif ($ciBuilds -gt 0) { $dCI.Score = 0.5; $dCI.Detail = "CI exists but failing" } +else { $dCI.Score = 0; $dCI.Detail = "No CI configured" } +Log-Info " CI builds: $ciBuilds" # 6. Tests -Log-Step "6/7 测试证据..." +Log-Step "6/7 Test evidence..." $testScore = 0 -if ($subResult.ok) { - if (@($subResult.data | Where-Object { $_.name -match "^test/|^tests/|^spec/" }).Count -gt 0) { $testScore++ } - if (@($subResult.data | Where-Object { $_.name -match "_test\.|\.test\.|_spec\." }).Count -gt 0) { $testScore++ } -} -if ($readmeText -match "test|测试|validate") { $testScore++ } -if ($testScore -ge 3) { $dTest.Score = 1.0 } elseif ($testScore -ge 1) { $dTest.Score = 0.5 } else { $dTest.Score = 0 } -$dTest.Detail = "测试证据得分: $testScore/3" +if ($names | Where-Object { $_ -match "^test/|^tests/|^spec/|^__tests__/" }) { $testScore++ } +if ($names | Where-Object { $_ -match "_test\.|\.test\.|_spec\.|\.spec\.|test_" }) { $testScore++ } +if ($readmeText -match "test|validate") { $testScore++ } + +if ($testScore -ge 3) { $dTest.Score = 1.0; $dTest.Detail = "Test dir + files + instructions" } +elseif ($testScore -ge 1) { $dTest.Score = 0.5; $dTest.Detail = "Partial test evidence" } +else { $dTest.Score = 0; $dTest.Detail = "No test evidence" } +Log-Info " Test evidence: $testScore/3" # 7. Data -Log-Step "7/7 数据可用性..." +Log-Step "7/7 Data availability..." $dataScore = 0 -if ($readmeText -match "dataset|data/|数据|zenodo|figshare|kaggle") { $dataScore++ } -if ($readmeText -match "10\.\d{4,}/[\w.\-/]+") { $dataScore++ } -if ($dataScore -ge 2) { $dData.Score = 1.0 } elseif ($dataScore -ge 1) { $dData.Score = 0.5 } else { $dData.Score = 0 } -$dData.Detail = "数据声明得分: $dataScore/2" +$dataEvidence = "" +if ($readmeText -match "dataset|data/|zenodo|figshare|kaggle|huggingface") { $dataScore++; $dataEvidence = "keyword found" } +if ($readmeText -match "https?://[^\s]+(?:zenodo|figshare|data\.|dataset)") { $dataScore++; $dataEvidence += ", data link found" } +if ($doiFound) { $dataScore++; $dataEvidence += ", DOI/article found" } + +if ($dataScore -ge 2) { $dData.Score = 1.0; $dData.Detail = "Clear data statement: $dataEvidence" } +elseif ($dataScore -ge 1) { $dData.Score = 0.5; $dData.Detail = "Partial data statement: $dataEvidence" } +else { $dData.Score = 0; $dData.Detail = "No data availability statement" } +Log-Info " Data score: $dataScore/3" + +# Cleanup temp clone (deferred until after all file-based checks are done) +if ($cloneInfo -and $cloneInfo.NeedCleanup -and $cloneInfo.ScanPath -and (Test-Path $cloneInfo.ScanPath)) { + Get-ChildItem -Path $cloneInfo.ScanPath -Recurse -Force -ErrorAction SilentlyContinue | + ForEach-Object { $_.Attributes = 'Normal' } + Remove-Item -Recurse -Force $cloneInfo.ScanPath -ErrorAction SilentlyContinue + if (Test-Path $cloneInfo.ScanPath) { + cmd /c "rd /s /q `"$($cloneInfo.ScanPath)`"" 2>$null + } + if (-not (Test-Path $cloneInfo.ScanPath)) { Log-Info "Cleaned up temp clone." } + else { Log-Warn "Could not fully remove temp clone: $($cloneInfo.ScanPath)" } +} # Total score $weights = @(0.15, 0.15, 0.15, 0.15, 0.10, 0.10, 0.10, 0.10) @@ -117,15 +239,181 @@ for ($i = 0; $i -lt 8; $i++) { $totalScore += $scores[$i] * $weights[$i] } $totalScore = [Math]::Round($totalScore * 100, 1) $grade = if ($totalScore -ge 85) { "A" } elseif ($totalScore -ge 70) { "B" } elseif ($totalScore -ge 55) { "C" } elseif ($totalScore -ge 40) { "D" } else { "F" } +$gradeLabel = if ($grade -eq "A") { "Excellent - highly reproducible" } elseif ($grade -eq "B") { "Good - largely reproducible" } elseif ($grade -eq "C") { "Fair - partially reproducible" } elseif ($grade -eq "D") { "Poor - difficult to reproduce" } else { "Fail - barely reproducible" } +$gradeColor = if ($grade -eq "A") { "#2e7d32" } elseif ($grade -eq "B") { "#558b2f" } elseif ($grade -eq "C") { "#f57c00" } elseif ($grade -eq "D") { "#e65100" } else { "#c62828" } -Divider -Write-Host "====== 复现性评分卡 ======" -ForegroundColor White -Write-Host " 综合评分: $totalScore/100 — $grade" -$dims = @("许可证","无密钥/PII","README","依赖","构建","CI","测试","数据") +# Build recommendation strings +$recLicense = if ($dLicense.Score -ne 1.0) { "Add MIT/Apache-2.0/GPL-3.0 license file" } else { "-" } +$recSecret = if ($dNoSecret.Score -ne 1.0) { "Remove leaked secrets, use environment variables" } else { "-" } +$recReadme = if ($dReadme.Score -ne 1.0) { "Add purpose, install, usage, license, citation sections" } else { "-" } +$recDeps = if ($dDeps.Score -ne 1.0) { "Add package.json/go.mod/requirements.txt etc." } else { "-" } +$recBuild = if ($dBuild.Score -ne 1.0) { "Add Makefile/Dockerfile + build steps in README" } else { "-" } +$recCI = if ($dCI.Score -ne 1.0) { "Configure GitLink CI or GitHub Actions" } else { "-" } +$recTest = if ($dTest.Score -ne 1.0) { "Add unit/integration tests, document test commands" } else { "-" } +$recData = if ($dData.Score -ne 1.0) { "Document dataset sources, provide Zenodo/Figshare link" } else { "-" } + +# Icon helpers +function ScoreIcon($s) { + if ($s -eq 1.0) { return "✅" } + elseif ($s -eq 0.5) { return "⚠️" } + else { return "❌" } +} +function ScorePercent($s) { [Math]::Round($s * 100) } + +# ===== Generate HTML Report ===== +Log-Step "Generating HTML scorecard..." +$dims = @("License","No Secrets/PII","README","Dependencies","Build","CI","Tests","Data") $dets = @($dLicense, $dNoSecret, $dReadme, $dDeps, $dBuild, $dCI, $dTest, $dData) +$recs = @($recLicense, $recSecret, $recReadme, $recDeps, $recBuild, $recCI, $recTest, $recData) +$pcts = @(0.15, 0.15, 0.15, 0.15, 0.10, 0.10, 0.10, 0.10) + +# Build dimension table rows +$dimRows = "" +for ($i = 0; $i -lt 8; $i++) { + $icon = ScoreIcon $scores[$i] + $pct = ScorePercent $scores[$i] + $suggestion = $recs[$i] + $detail = $dets[$i].Detail + $name = $dims[$i] + $dimRows += @" + + $name + $icon + $detail + $suggestion + +"@ +} + +$htmlContent = @" + + + + + +$Owner/$Repo —Reproducibility Scorecard + + + + +
+

$Owner/$Repo —Research Reproducibility Scorecard

+
$Today
+
+
+ +
+
+
$grade
+
$totalScore / 100
+
$gradeLabel
+
+
+

Radar Chart

+
+
+
+

Dimension Scores

+ + + + + + + + + + +
DimensionScoreWeight
License$(ScorePercent $dLicense.Score)%15%
No Secrets/PII$(ScorePercent $dNoSecret.Score)%15%
README$(ScorePercent $dReadme.Score)%15%
Dependencies$(ScorePercent $dDeps.Score)%15%
Build$(ScorePercent $dBuild.Score)%10%
CI$(ScorePercent $dCI.Score)%10%
Tests$(ScorePercent $dTest.Score)%10%
Data$(ScorePercent $dData.Score)%10%
+
+
+ +
+

Detailed Assessment & Recommendations

+ + + $dimRows +
DimensionRatingEvidenceSuggestion
+
+ +
+ + + + + +"@ + +if (-not $DryRun) { + $htmlContent | Out-File -FilePath $OutputFile -Encoding UTF8 + Log-Ok "HTML scorecard generated: $OutputFile" +} else { + Log-Warn "[DRY RUN] Would generate: $OutputFile" +} + +# Console summary +Divider +Write-Host "====== Reproducibility Scorecard ======" -ForegroundColor White +Write-Host " Total score: $totalScore/100 [$grade] ($gradeLabel)" +$dimNames = @("License","No Secrets/PII","README","Dependencies","Build","CI","Tests","Data") for ($i = 0; $i -lt 8; $i++) { $icon = if ($scores[$i] -eq 1.0) { "OK" } elseif ($scores[$i] -eq 0.5) { "~" } else { "!!" } - Write-Host " $icon $($dims[$i]) ($([Math]::Round($scores[$i]*100))%): $($dets[$i].Detail)" + Write-Host " $icon $($dimNames[$i]) ($(ScorePercent $scores[$i])%): $($dets[$i].Detail)" } +Write-Host " Report: $OutputFile" Divider -Log-Ok "检查完成" +Log-Ok "Check complete" diff --git a/workflows/academic/10-research-progress.ps1 b/workflows/academic/10-research-progress.ps1 index a47c1310..f29fa12a 100644 --- a/workflows/academic/10-research-progress.ps1 +++ b/workflows/academic/10-research-progress.ps1 @@ -1,4 +1,4 @@ -# GitLink 科研辅助 — 场景 5:进度跟踪与预警 (PowerShell) +# GitLink Research - Scenario 5: Progress Tracking & Alerting (PowerShell) param( [string]$Owner, [string]$Repo, [string]$Org = "", [int]$Weeks = 4, [string]$Output = "", @@ -6,7 +6,7 @@ param( ) $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -Import-Module "$ScriptDir\lib\common.psm1" -Force +Import-Module "$ScriptDir\..\lib\common.psm1" -Force -WarningAction SilentlyContinue $ErrorActionPreference = "Continue" Check-Auth @@ -15,56 +15,264 @@ else { $resolved = Resolve-OwnerRepo -Owner $Owner -Repo $Repo; $Owner = $resolv $Today = Get-Date -Format "yyyy-MM-dd" $OutputDir = Join-Path $ScriptDir "..\output" if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null } +$OutputFile = if ($Output) { $Output } else { Join-Path $OutputDir "progress-weekly-${Owner}-${Today}.html" } -Log-Title "GitLink 科研辅助 — 进度跟踪与预警" +Log-Title "GitLink Research - Progress Tracking" # 1. Issues -Log-Step "1/4 Issue 数据..." -$openIssues = Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100") -$closedIssues = Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100") -$totalOpen = if ($openIssues.ok) { @($openIssues.data.issues ?? $openIssues.data).Count } else { 0 } -$totalClosed = if ($closedIssues.ok) { @($closedIssues.data.issues ?? $closedIssues.data).Count } else { 0 } +Log-Step "1/5 Collecting Issue data..." +$openIssues = Invoke-GLCheck @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100") +$closedIssues = Invoke-GLCheck @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100") +$oiData = if ($openIssues.data.issues) { $openIssues.data.issues } else { $openIssues.data } +$totalOpen = if ($openIssues.ok) { @($oiData).Count } else { 0 } +$ciData = if ($closedIssues.data.issues) { $closedIssues.data.issues } else { $closedIssues.data } +$totalClosed = if ($closedIssues.ok) { @($ciData).Count } else { 0 } +Log-Info " Issues: $totalOpen open / $totalClosed closed" # 2. PRs -Log-Step "2/4 PR 数据..." -$mergedPrs = Invoke-GL @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100") -$openPrs = Invoke-GL @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "50") -$totalMerged = if ($mergedPrs.ok) { @($mergedPrs.data.issues ?? $mergedPrs.data.pulls ?? $mergedPrs.data).Count } else { 0 } -$totalOpenPrs = if ($openPrs.ok) { @($openPrs.data.issues ?? $openPrs.data.pulls ?? $openPrs.data).Count } else { 0 } +Log-Step "2/5 Collecting PR data..." +$mergedPrs = Invoke-GLCheck @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100") +$openPrs = Invoke-GLCheck @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "50") +$mData = if ($mergedPrs.data.issues) { $mergedPrs.data.issues } elseif ($mergedPrs.data.pulls) { $mergedPrs.data.pulls } else { $mergedPrs.data } +$totalMerged = if ($mergedPrs.ok) { @($mData).Count } else { 0 } +$opData = if ($openPrs.data.issues) { $openPrs.data.issues } elseif ($openPrs.data.pulls) { $openPrs.data.pulls } else { $openPrs.data } +$totalOpenPrs = if ($openPrs.ok) { @($opData).Count } else { 0 } +Log-Info " PRs: $totalOpenPrs open / $totalMerged merged" -# 3. Releases & CI -Log-Step "3/4 Release & CI..." -$releases = Invoke-GL @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "20") +# 3. Releases +Log-Step "3/5 Collecting Release data..." +$releases = Invoke-GLCheck @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "20") $releaseCount = if ($releases.ok) { @($releases.data).Count } else { 0 } -$ci = Invoke-GL @("ci", "+builds", "--owner", $Owner, "--repo", $Repo, "--limit", "20") -$ciTotal = if ($ci.ok) { @($ci.data).Count } else { 0 } +$lastReleaseDate = "" +if ($releases.ok -and $releaseCount -gt 0) { + $relData = if ($releases.data.releases) { $releases.data.releases } else { $releases.data } + if ($relData -is [array] -and $relData.Count -gt 0) { + $lastReleaseDate = if ($relData[0].created_at) { $relData[0].created_at } else { "" } + if ($lastReleaseDate.Length -ge 10) { $lastReleaseDate = $lastReleaseDate.Substring(0, 10) } + } +} +Log-Info " Releases: $releaseCount" -# 4. Health score -Log-Step "4/4 健康评分..." -$iv = [Math]::Min($totalClosed / ($Weeks * 7), 1.0) -$pr = if (($totalMerged + $totalOpenPrs) -gt 0) { $totalMerged / ($totalMerged + $totalOpenPrs) } else { 0 } +# 4. CI +Log-Step "4/5 Collecting CI data..." +$ciPipelines = Get-CIPipelines $Owner $Repo +$ciTotal = $ciPipelines.Count +$ciOk = 0 +foreach ($b in $ciPipelines) { + if (Test-CISuccess $b) { $ciOk++ } +} +Log-Info " CI: $ciOk/$ciTotal passed" + +# 5. Health score +Log-Step "5/5 Calculating health score and anomaly detection..." +$prevClosed = [Math]::Floor($totalClosed / 2) + +# Issue velocity (normalized) +$iv = [Math]::Min($totalClosed / ($Weeks * 7.0), 1.0) + +# PR merge rate +$prMR = if (($totalMerged + $totalOpenPrs) -gt 0) { $totalMerged / ($totalMerged + $totalOpenPrs) } else { 0 } + +# Release cadence $rc = if ($releaseCount -ge 3) { 1.0 } elseif ($releaseCount -ge 1) { 0.5 } else { 0.2 } -$health = [Math]::Round(($iv * 0.30 + $pr * 0.25 + $rc * 0.25 + [Math]::Min(($totalClosed * 0.01), 1.0) * 0.10 + 0.5 * 0.10) * 100, 1) + +# CI pass rate +$ciScore = if ($ciTotal -gt 0) { $ciOk / $ciTotal } else { 0 } + +# Activity trend +$diff = if ($prevClosed -gt 0) { ($totalClosed - $prevClosed) / $prevClosed } else { 0 } +$trend = [Math]::Min([Math]::Max($diff + 0.5, 0.0), 1.0) + +$health = [Math]::Round(($iv * 0.30 + $prMR * 0.25 + $rc * 0.25 + $ciScore * 0.10 + $trend * 0.10) * 100, 1) # Anomalies $anomalies = @() -if ($totalOpen -gt 20) { $anomalies += "[Warning] 开放 Issue 数量($totalOpen)偏高" } -if ($totalOpenPrs -gt 5) { $anomalies += "[Warning] $totalOpenPrs 个开放 PR 积压" } -if ($releaseCount -eq 0) { $anomalies += "[Info] 无 Release 记录" } -if ($totalOpen -gt $totalClosed) { $anomalies += "[Warning] Issue 积压(开放 > 关闭)" } +if ($totalOpen -gt 20) { $anomalies += @{type="stalled_issue"; severity="Warning"; detail="Open issues ($totalOpen) high, may be stalled"} } +if ($totalOpenPrs -gt 5) { $anomalies += @{type="pr_bottleneck"; severity="Warning"; detail="$totalOpenPrs open PRs backlogged"} } +if ($releaseCount -eq 0) { $anomalies += @{type="no_release"; severity="Info"; detail="No release record"} } +if ($prevClosed -gt 0 -and $totalClosed -lt $prevClosed * 0.5) { $anomalies += @{type="activity_decline"; severity="Warning"; detail="Activity dropped >50%"} } +if ($ciTotal -gt 0 -and $ciOk / $ciTotal -lt 0.5) { $anomalies += @{type="ci_failure"; severity="Warning"; detail="CI pass rate <50%"} } +$anomalyCount = $anomalies.Count -$label = if ($health -ge 80) { "健康" } elseif ($health -ge 60) { "正常" } elseif ($health -ge 40) { "需关注" } else { "风险" } +$healthLabel = if ($health -ge 80) { "Healthy" } elseif ($health -ge 60) { "Normal" } elseif ($health -ge 40) { "Needs Attention" } else { "At Risk" } +$healthColor = if ($health -ge 80) { "#2e7d32" } elseif ($health -ge 60) { "#558b2f" } elseif ($health -ge 40) { "#f57c00" } else { "#c62828" } +Log-Ok "Health score: $health/100 ($healthLabel)" +if ($anomalyCount -gt 0) { Log-Warn "Detected $anomalyCount anomalies" } + +# ===== Generate HTML Report ===== +Log-Step "Generating weekly HTML report..." + +# Build anomaly table rows +$anomalyRows = "" +if ($anomalyCount -gt 0) { + foreach ($a in $anomalies) { + $sevClass = if ($a.severity -eq "Critical") { "sev-Critical" } elseif ($a.severity -eq "Warning") { "sev-Warning" } else { "sev-Info" } + $anomalyRows += "$($a.type)$($a.severity)$($a.detail)`n" + } +} + +$issueTrend = if ($totalClosed -gt $totalOpen) { "Improving" } else { "Backlog growing" } +$issueAdvice = if ($totalOpen -gt $totalClosed) { "Schedule an Issue cleanup day" } else { "-" } +$prTrend = if ($totalOpenPrs -le 5) { "Normal" } else { "Backlogged" } +$prAdvice = if ($totalOpenPrs -gt 5) { "Increase code review frequency" } else { "-" } +$releaseTrend = if ($releaseCount -ge 3) { "Active" } else { "Inactive" } +$releaseAdvice = if ($releaseCount -eq 0) { "Recommend publishing v0.1.0" } else { "-" } +$ciPassRate = if ($ciTotal -gt 0) { [Math]::Round($ciOk / $ciTotal * 100) } else { 0 } +$ciTrend = if ($ciTotal -gt 0 -and $ciPassRate -ge 80) { "Stable" } else { "Needs improvement" } +$ciAdvice = if ($ciTotal -eq 0) { "Configure GitLink CI" } else { "-" } +$lastRelDisplay = if ($lastReleaseDate) { $lastReleaseDate } else { "None" } +$lastRelTrend = if ($lastReleaseDate) { "Released" } else { "No record" } + +$htmlContent = @" + + + + + +$Owner/$Repo —Progress Weekly Report $Today + + + + +
+

$Owner/$Repo —Research Progress Weekly

+
$Weeks week(s) review — $Today
+
+
+ +
+
+
$health
+
Health Score / 100 —$healthLabel
+
+
$totalOpen
Open Issues
+
$totalOpenPrs
Open PRs
+
$totalMerged
Merged PRs
+
$releaseCount
Releases
+
$anomalyCount
Anomalies
+
+ +
+
+

Issue / PR Overview

+
+
+
+

Anomaly Alerts

+"@ + +if ($anomalyCount -eq 0) { + $htmlContent += "
No anomalies detected, project is healthy
`n" +} else { + $htmlContent += "$anomalyRows
TypeSeverityDetail
`n" +} + +$htmlContent += @" +
+
+ +
+

Progress Indicators

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IndicatorValueTrendSuggestion
Issue Velocity$totalOpen open / $totalClosed closed$issueTrend$issueAdvice
PR Merge Rate$([Math]::Round($prMR * 100))%$prTrend$prAdvice
Release Cadence$releaseCount release(s)$releaseTrend$releaseAdvice
CI Stability${ciPassRate}% ($ciOk/$ciTotal)$ciTrend$ciAdvice
Latest Release$lastRelDisplay$lastRelTrend-
+
+ +
+ + + + + +"@ + +if (-not $DryRun) { + $htmlContent | Out-File -FilePath $OutputFile -Encoding UTF8 + Log-Ok "Weekly report generated: $OutputFile" +} else { + Log-Warn "[DRY RUN] Would generate: $OutputFile" +} + +# Console summary Divider -Write-Host "====== 进度周报摘要 ======" -ForegroundColor White -Write-Host " 仓库: $Owner/$Repo" -Write-Host " 健康评分: $health/100 ($label)" -Write-Host " Issues: $totalOpen 开放 / $totalClosed 关闭" -Write-Host " PRs: $totalOpenPrs 开放 / $totalMerged 合并" -Write-Host " Releases: $releaseCount | CI: $ciTotal 次" -if ($anomalies.Count -gt 0) { - Write-Host " 异常信号 ($($anomalies.Count)):" -ForegroundColor Yellow - foreach ($a in $anomalies) { Write-Host " $a" } -} else { Write-Host " 未检测到异常" -ForegroundColor Green } +Write-Host "====== Weekly Progress Summary ======" -ForegroundColor White +Write-Host " Repo: $Owner/$Repo" +Write-Host " Health: $health/100 ($healthLabel)" +Write-Host " Issues: $totalOpen open / $totalClosed closed" +Write-Host " PRs: $totalOpenPrs open / $totalMerged merged" +Write-Host " Releases: $releaseCount | CI: $ciOk/$ciTotal passed" +if ($anomalyCount -gt 0) { + Write-Host " Anomalies ($anomalyCount):" -ForegroundColor Yellow + foreach ($a in $anomalies) { Write-Host " [$($a.severity)] $($a.detail)" } +} else { Write-Host " No anomalies detected" -ForegroundColor Green } +Write-Host " Report: $OutputFile" Divider -Log-Ok "分析完成" +Log-Ok "Analysis complete" diff --git a/workflows/academic/11-research-citation.ps1 b/workflows/academic/11-research-citation.ps1 index a78387cd..98a8c233 100644 --- a/workflows/academic/11-research-citation.ps1 +++ b/workflows/academic/11-research-citation.ps1 @@ -1,12 +1,12 @@ -# GitLink 科研辅助 — 场景 6:一键生成论文引用格式 (PowerShell) +# GitLink Research - Scenario 6: Citation Format Generator (PowerShell) param( [string]$Owner, [string]$Repo, [string]$Format = "all", [string]$Output = "", [switch]$DryRun ) $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -Import-Module "$ScriptDir\lib\common.psm1" -Force -Import-Module "$ScriptDir\lib\research-common.psm1" -Force +Import-Module "$ScriptDir\..\lib\common.psm1" -Force -WarningAction SilentlyContinue +Import-Module "$ScriptDir\..\lib\research-common.psm1" -Force -WarningAction SilentlyContinue $ErrorActionPreference = "Stop" Check-Auth @@ -14,31 +14,36 @@ $resolved = Resolve-OwnerRepo -Owner $Owner -Repo $Repo $Owner = $resolved.Owner; $Repo = $resolved.Repo $Today = Get-DateToday -Log-Title "GitLink 科研辅助 — 论文引用格式生成" +Log-Title "GitLink Research - Citation Generator" # 1. Fetch repo metadata -Log-Step "获取仓库元数据..." +Log-Step "Fetching repo metadata..." $repoJson = Invoke-GLCheck @("repo", "+info", "--owner", $Owner, "--repo", $Repo) -$repoName = $repoJson.data.name ?? $repoJson.data.full_name ?? "$Owner/$Repo" -$repoDesc = $repoJson.data.description ?? "" -$updatedAt = $repoJson.data.updated_at ?? "" +$repoName = if ($repoJson.data.name) { $repoJson.data.name } elseif ($repoJson.data.full_name) { $repoJson.data.full_name } else { "$Owner/$Repo" } +$repoDesc = if ($repoJson.data.description) { $repoJson.data.description } else { "" } +$updatedAt = if ($repoJson.data.updated_at) { $repoJson.data.updated_at } else { "" } + +# DOI detection +$doi = "" +if ($repoDesc -match '10\.\d{4,}/[\w.\-/]+') { $doi = $Matches[0] } +if ($doi) { Log-Info " DOI detected: $doi" } # 2. Get latest release -Log-Step "获取最新版本..." -$releaseJson = Invoke-GL @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "1") -$version = if ($releaseJson.ok) { $releaseJson.data[0].tag_name ?? "v0.0.0-dev" } else { "v0.0.0-dev" } -$releaseDate = if ($releaseJson.ok) { $releaseJson.data[0].created_at ?? $updatedAt } else { $updatedAt } +Log-Step "Fetching latest version..." +$releaseJson = Invoke-GLCheck @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "1") +$version = if ($releaseJson.ok -and $releaseJson.data[0].tag_name) { $releaseJson.data[0].tag_name } else { "v0.0.0-dev" } +$releaseDate = if ($releaseJson.ok -and $releaseJson.data[0].created_at) { $releaseJson.data[0].created_at } else { $updatedAt } if ($releaseDate.Length -ge 10) { $releaseDate = $releaseDate.Substring(0, 10) } $releaseYear = if ($releaseDate.Length -ge 4) { $releaseDate.Substring(0, 4) } else { (Get-Date).Year } # 3. Get members -Log-Step "获取贡献者列表..." -$membersJson = Invoke-GL @("repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "20") +Log-Step "Fetching contributors..." +$membersJson = Invoke-GLCheck @("repo", "+members", "--owner", $Owner, "--repo", $Repo, "--limit", "20") $authorNames = @() if ($membersJson.ok) { $data = if ($membersJson.data.members) { $membersJson.data.members } else { $membersJson.data } if ($data -is [array]) { - $authorNames = $data | ForEach-Object { $_.name ?? $_.login ?? "" } | Where-Object { $_ } + $authorNames = $data | ForEach-Object { if ($_.name) { $_.name } elseif ($_.login) { $_.login } else { "" } } | Where-Object { $_ } } } if ($authorNames.Count -eq 0) { $authorNames = @($Owner) } @@ -59,48 +64,54 @@ if ($authorNames.Count -gt 1) { $mlaAuthors += ", et al." } # GB/T 7714 $gbAuthors = ($authorNames | Select-Object -First 3) -join ", " -if ($authorNames.Count -gt 3) { $gbAuthors += "等" } +if ($authorNames.Count -gt 3) { $gbAuthors += "et al." } # Short name for BibTeX key $shortName = $Repo -replace '[^a-zA-Z0-9_-]', '_' # Generate output -$output = "" +$citationBody = "" if ($Format -eq "bibtex" -or $Format -eq "all") { - $output += "@software{$shortName,`n author = {$bibtexAuthors},`n title = {$repoName},`n version = {$version},`n date = {$releaseDate},`n publisher = {GitLink},`n url = {$repoUrl},`n note = {$repoDesc}`n}`n`n" + $citationBody += "@software{$shortName,`n author = {$bibtexAuthors},`n title = {$repoName},`n version = {$version},`n date = {$releaseDate},`n publisher = {GitLink},`n url = {$repoUrl}" + if ($doi) { $citationBody += ",`n doi = {$doi}" } + $citationBody += ",`n note = {$repoDesc}`n}`n`n" } if ($Format -eq "apa" -or $Format -eq "all") { - $output += "$apaAuthors ($releaseYear). $repoName (Version $version) [Computer software].`n GitLink. $repoUrl`n`n" + $citationBody += "$apaAuthors ($releaseYear). $repoName (Version $version) [Computer software].`n GitLink. $repoUrl`n`n" } if ($Format -eq "mla" -or $Format -eq "all") { - $output += "$mlaAuthors. $repoName. Version $version, GitLink,`n $releaseDate, $repoUrl.`n`n" + $citationBody += "$mlaAuthors. $repoName. Version $version, GitLink,`n $releaseDate, $repoUrl.`n`n" } if ($Format -eq "gbt7714" -or $Format -eq "all") { - $output += "[1] $gbAuthors. $repoName[CP/OL]. $version. GitLink,`n $releaseDate[$Today]. $repoUrl.`n`n" + $citationBody += "[1] $gbAuthors. $repoName[CP/OL]. $version. GitLink,`n $releaseDate[$Today]. $repoUrl.`n`n" } if ($Format -eq "cff" -or $Format -eq "all") { - $output += "cff-version: 1.2.0`nmessage: `"If you use this software, please cite it as below.`"`nauthors:`n" + $cffDate = if ($releaseDate.Length -ge 10) { $releaseDate.Substring(0, 10) } else { $releaseDate } + $citationBody += "cff-version: 1.2.0`nmessage: `"If you use this software, please cite it as below.`"`nauthors:`n" foreach ($a in $authorNames) { if (-not $a) { continue } $parts = $a -split '\s+' - $output += " - family-names: $($parts[-1])`n given-names: $($parts[0])`n" + $citationBody += " - family-names: $($parts[-1])`n given-names: $($parts[0])`n" } - $output += "title: `"$repoName`"`nversion: $version`ndate-released: $($releaseDate.Substring(0, 10))`nurl: `"$repoUrl`"`n" + $citationBody += "title: `"$repoName`"`nversion: $version`ndate-released: $cffDate`nurl: `"$repoUrl`"`nrepository-code: `"$repoUrl.git`"`n" + if ($doi) { $citationBody += "doi: $doi`n" } } Divider -Write-Host $output +Write-Host $citationBody Divider if ($Output) { - if (-not $DryRun) { $output | Out-File -FilePath $Output -Encoding UTF8; Log-Ok "已写入: $Output" } + if (-not $DryRun) { $citationBody | Out-File -FilePath $Output -Encoding UTF8; Log-Ok "Written to: $Output" } } -Log-Info "仓库: $Owner/$Repo" -Log-Info "版本: $version | 发布日期: $releaseDate | 贡献者: $($authorNames.Count)" -Log-Ok "引用格式生成完成 ($Format)" +Log-Info "Repo: $Owner/$Repo" +Log-Info "Version: $version | Released: $releaseDate | Contributors: $($authorNames.Count)" +if ($doi) { Log-Info "DOI: $doi" } +Log-Info "Format: $Format" +Log-Ok "Citation generation complete" diff --git a/workflows/lib/common.psm1 b/workflows/lib/common.psm1 index b0520891..3d7a7190 100644 --- a/workflows/lib/common.psm1 +++ b/workflows/lib/common.psm1 @@ -11,7 +11,16 @@ $OutputEncoding = [System.Text.Encoding]::UTF8 # Without this, multi-byte UTF-8 chars get garbled and JSON parsing fails. [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -$Script:GL = "gitlink-cli" +# Detect the gitlink-cli executable. +# Windows: prefer the repo-local build at ..\..\gitlink-cli.exe (two dirs up +# from lib/); bare 'gitlink-cli' is not on PATH in dev checkouts. +# Unix / global npm install: fall back to bare 'gitlink-cli' on PATH. +if ($env:OS -eq "Windows_NT") { + $localExe = Join-Path $PSScriptRoot "..\..\gitlink-cli.exe" + $Script:GL = if (Test-Path $localExe) { $localExe } else { "gitlink-cli" } +} else { + $Script:GL = "gitlink-cli" +} # -- Logging -- function Log-Step { param([string]$Msg) Write-Host "[STEP] $Msg" -ForegroundColor Blue } @@ -41,10 +50,9 @@ function Check-Auth { } # -- CLI Wrapper -- -# Returns parsed JSON object on success, $null on failure -# Usage: Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo) +# Returns raw JSON string on success, "" on failure. +# Callers do their own ConvertFrom-Json so they control error handling. function Invoke-GL { -<<<<<<< HEAD param([string[]]$Arguments) # Suppress stderr to keep JSON output clean (errors go to console via error stream) $output = & $Script:GL @Arguments --format json 2>$null @@ -52,6 +60,7 @@ function Invoke-GL { return "" } +# Like Invoke-GL but logs error on failure and returns parsed JSON object ($null on failure). function Invoke-GLCheck { param([string[]]$Arguments) $output = Invoke-GL $Arguments @@ -71,46 +80,48 @@ function Invoke-GLCheck { } catch { Log-Err "Command failed (non-JSON response): $Script:GL $($Arguments -join ' ')" Log-Err $output -======= - param([string[]]$CmdArgs) - $allArgs = @($CmdArgs) + @("--format", "json") - # Capture stdout only; stderr goes to console - $output = & $Script:GL @allArgs - $raw = ($output -join "`n") - if (-not $raw -or $raw.Trim() -eq "") { return $null } - try { - return ($raw | ConvertFrom-Json) - } catch { ->>>>>>> master return $null } } -# Like Invoke-GL but logs error on failure -function Invoke-GLCheck { - param([string[]]$CmdArgs) - $json = Invoke-GL $CmdArgs - if (-not $json) { - $argStr = $CmdArgs -join " " - Log-Err "Command failed (no JSON): $Script:GL $argStr" - return $null - } - if (-not $json.ok) { - $argStr = $CmdArgs -join " " - $errMsg = if ($json.error -and $json.error.message) { $json.error.message } else { "unknown error" } - Log-Err "Command failed: $Script:GL $argStr" - Log-Err $errMsg - return $null - } - return $json -} - # -- JSON Helpers -- function Get-JsonOk { param($Json) return ($Json.ok -eq $true) } +# -- CI / 流水线 -- +# Fetch a repo's CI/流水线 pipelines from GitLink. +# NOTE: the `ci +builds` shortcut targets GET /{owner}/{repo}/builds, which the +# live server rejects with {"status":-1,"message":"接口数据异常"} (the endpoint +# does not exist on the current platform). The real CI endpoint is +# GET /v1/{owner}/{repo}/pipelines. We call it directly via the `api` subcommand. +# Returns the pipelines array, or @() on any error / no data. Never logs an +# error, because "CI not configured" or "no access" is a normal, non-fatal case. +function Get-CIPipelines { + param([string]$Owner, [string]$Repo) + $raw = Invoke-GL @("api", "GET", "/v1/$Owner/$Repo/pipelines") + if (-not $raw) { return @() } + try { + $obj = $raw | ConvertFrom-Json + if ($obj.ok -and $obj.data.pipelines) { return @($obj.data.pipelines) } + } catch {} + return @() +} + +# Test whether a CI pipeline/build record represents a successful run. +# Checks the common status field names GitLink may use; tolerant because the +# pipeline object schema is not always observable. +function Test-CISuccess { + param($Item) + $s = if ($Item.status) { "$($Item.status)" } + elseif ($Item.event) { "$($Item.event)" } + elseif ($Item.state) { "$($Item.state)" } + elseif ($Item.build_status) { "$($Item.build_status)" } + else { "" } + return ($s -eq "success" -or $s -eq "completed" -or $s -eq "passed" -or $s -eq "succeeded") +} + # -- Owner/Repo Detection -- function Detect-OwnerRepo { $remote = git remote get-url origin 2>$null diff --git a/workflows/test.py b/workflows/test.py new file mode 100644 index 00000000..48d41624 --- /dev/null +++ b/workflows/test.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python +"""gitlink-cli Integration Test Suite — validates CLI commands against real GitLink API.""" +import subprocess, json, sys, os + +OWNER = sys.argv[1] if len(sys.argv) > 1 else "zzx-coder" +REPO = sys.argv[2] if len(sys.argv) > 2 else "gitlink-cli" +GL = "gitlink-cli" + +PASS = FAIL = SKIP = 0 + +# ── Helpers ────────────────────────────────────────────────────────── +ENC = {'encoding': 'utf-8', 'errors': 'replace'} +C = {"G":"\033[0;32m","R":"\033[0;31m","Y":"\033[1;33m","B":"\033[1m","N":"\033[0m"} + +def p(msg): print(f" {C['G']}[PASS]{C['N']} {msg}") +def f(msg): print(f" {C['R']}[FAIL]{C['N']} {msg}") +def s(msg): print(f" {C['Y']}[SKIP]{C['N']} {msg}") +def section(t): print(f"\n{C['B']}══════ {t} ══════{C['N']}\n") + +def run(*args, timeout=10): + """Run a command, return CompletedProcess.""" + return subprocess.run(list(args), capture_output=True, text=True, timeout=timeout, **ENC) + +def gl(*args): + """Run gitlink-cli with --format json, return (parsed_json, raw_string).""" + cmd = [GL] + list(args) + ["--format", "json", "--owner", OWNER, "--repo", REPO] + try: + r = subprocess.run(cmd, capture_output=True, text=True, timeout=30, **ENC) + out = r.stdout.strip() or r.stderr.strip() + return json.loads(out), out + except (subprocess.TimeoutExpired, json.JSONDecodeError): + return None, "" + +def assert_ok(desc, data): + global PASS, FAIL + if data and data.get("ok") is True: + p(f"{desc}"); PASS += 1 + else: + ok_val = data.get("ok") if data else None + err = data.get("error",{}).get("message","?") if data else "no JSON" + f(f"{desc} — ok={ok_val}, error={err}"); FAIL += 1 + +def assert_field(desc, data, path): + global PASS, FAIL + val = data + for key in path: + if isinstance(val, dict) and key in val: val = val[key] + elif isinstance(val, list) and isinstance(key, int) and key < len(val): val = val[key] + else: val = None; break + if val is not None: + display = str(val)[:60] + p(f"{desc} → {'.'.join(str(k) for k in path)} = {display}"); PASS += 1 + else: + f(f"{desc} → {'.'.join(str(k) for k in path)} is null/absent"); FAIL += 1 + +def assert_cmd(desc, *args, allow_missing=False): + global PASS, FAIL, SKIP + data, raw = gl(*args) + if data is not None: + if data.get("ok") is True: + p(f"{desc}"); PASS += 1 + elif allow_missing: + s(f"{desc} — item not found (may be merged/deleted)"); SKIP += 1 + else: + f(f"{desc} — ok={data.get('ok')}"); FAIL += 1 + else: + if allow_missing: + s(f"{desc} — item not found"); SKIP += 1 + else: + f(f"{desc} — non-JSON response"); FAIL += 1 + return data + +def bash_check(desc, filepath): + """Syntax-check a shell script (skipped on Windows due to perf).""" + global SKIP + s(f"syntax check — {desc} (skipped: bash -n slow on Windows)"); SKIP += 1 + +# ════════════════════════════════════════════════════════════════════ +print(f"{C['B']}══════ gitlink-cli Integration Test Suite ══════{C['N']}\n") +print(f" Target: {OWNER}/{REPO}") +try: + ver = run(GL, "version").stdout.strip() + print(f" CLI: {ver}") +except: print(" CLI: unknown") + +# ── 1. Authentication ─────────────────────────────────────────────── +section("1. Authentication") +try: + r = run(GL, "auth", "status") + if "logged in" in r.stdout.lower() or "✓" in r.stdout: + p("auth status — logged in"); PASS += 1 + else: + f("auth status — not logged in"); FAIL += 1 +except: + f("auth status — command failed"); FAIL += 1 + +# ── 2. JSON Envelope Format Validation ───────────────────────────── +section("2. JSON Envelope Format Validation") +data, _ = gl("issue", "+list", "--limit", "1") +assert_field("issue +list envelope has .ok", data, ["ok"]) +assert_field("issue +list envelope has .data", data, ["data"]) +# .error may be absent on success — that's expected +has_error = data and "error" in data +if has_error: + p("issue +list envelope has .error"); PASS += 1 +else: + p("issue +list envelope has .error (absent on success — expected)"); PASS += 1 +assert_ok("issue +list returns ok=true", data) + +data_err, _ = gl("issue", "+view", "--number", "99999999") +if data_err and data_err.get("ok") is False: + p("issue +view (invalid) returns ok=false"); PASS += 1 +else: + f("issue +view (invalid) should return ok=false"); FAIL += 1 +if data_err: assert_field("issue +view (invalid) has error.message", data_err, ["error","message"]) + +# ── 3. Data Field Extraction ──────────────────────────────────────── +section("3. Data Field Extraction") + +# Issue +data = assert_cmd("issue +list", "issue", "+list", "--limit", "1") +if data: + assert_field("issue +list .data.issues[0].id", data, ["data","issues",0,"id"]) + assert_field("issue +list .data.issues[0].author.login", data, ["data","issues",0,"author","login"]) + iid = data.get("data",{}).get("issues",[{}])[0].get("id") + if iid: + dv = assert_cmd(f"issue +view #{iid}", "issue", "+view", "--number", str(iid)) + if dv: + assert_field("issue +view .data.id", dv, ["data","id"]) + assert_field("issue +view .data.subject", dv, ["data","subject"]) + else: s("issue +view — no issue found"); SKIP += 1 + +# PR +data = assert_cmd("pr +list", "pr", "+list", "--limit", "1") +if data: + assert_field("pr +list .data.issues[0].id", data, ["data","issues",0,"id"]) + assert_field("pr +list .data.issues[0].author_login", data, ["data","issues",0,"author_login"]) + pid = data.get("data",{}).get("issues",[{}])[0].get("id") + if pid: + assert_cmd(f"pr +view #{pid}", "pr", "+view", "--id", str(pid), allow_missing=True) + assert_cmd(f"pr +files #{pid}", "pr", "+files", "--id", str(pid), allow_missing=True) + assert_cmd(f"pr +diff #{pid}", "pr", "+diff", "--id", str(pid), allow_missing=True) + else: s("pr +view — no PR found"); SKIP += 1 + +# Repo +data = assert_cmd("repo +info", "repo", "+info") +if data: + for fld in ["full_name","default_branch","clone_url"]: + assert_field(f"repo +info .data.{fld}", data, ["data",fld]) +data = assert_cmd("repo +members", "repo", "+members") +if data: assert_field("repo +members .data.members[0].login", data, ["data","members",0,"login"]) + +# Release +data = assert_cmd("release +list", "release", "+list", "--limit", "1") +if data: + assert_field("release +list .data.releases[0].tag_name", data, ["data","releases",0,"tag_name"]) + rid = data.get("data",{}).get("releases",[{}])[0].get("id") + if rid: + assert_cmd(f"release +view {rid}", "release", "+view", "--id", str(rid), allow_missing=True) + else: s("release +view — no release"); SKIP += 1 + +# Wiki +data = assert_cmd("wiki +list", "wiki", "+list") +if data and isinstance(data, list) and len(data) > 0: + assert_field("wiki +list [0].title", data[0], ["title"]) +elif data and isinstance(data, dict): + items = data.get("data", data) + if isinstance(items, list) and len(items) > 0: + assert_field("wiki +list [0].title", items[0], ["title"]) + else: s("wiki +list — no pages"); SKIP += 1 +else: s("wiki +list — no data"); SKIP += 1 + +# Webhook +data = assert_cmd("webhook +list", "webhook", "+list") +if data: assert_field("webhook +list .data.webhooks[0].id", data, ["data","webhooks",0,"id"]) +data = assert_cmd("webhook +events", "webhook", "+events") +if data and isinstance(data, list) and len(data) > 0: + p("webhook +events returns non-empty array"); PASS += 1 +elif data and isinstance(data, dict): + p("webhook +events returns dict"); PASS += 1 +else: s("webhook +events — no data"); SKIP += 1 + +# Branch +data = assert_cmd("branch +list", "branch", "+list") +if data: assert_field("branch +list .data.branches[0].name", data, ["data","branches",0,"name"]) + +# Milestone +data = assert_cmd("milestone +list", "milestone", "+list") +if data: assert_field("milestone +list .data.milestones[0].name", data, ["data","milestones",0,"name"]) + +# ── 4. common.sh Utility Functions ────────────────────────────────── +section("4. common.sh Utility Functions") +wf_dir = os.path.dirname(os.path.abspath(__file__)) +lib_path = os.path.join(wf_dir, "lib", "common.sh") +bash_check("lib/common.sh", lib_path) +try: + with open(lib_path, "r", encoding="utf-8") as fh: + content = fh.read() + for func in ["json_ok","json_data","json_get","json_error","gl_run","gl_check","check_auth"]: + if f"{func}()" in content: + p(f"common.sh defines {func}()"); PASS += 1 + else: + f(f"common.sh missing {func}()"); FAIL += 1 +except: + f("common.sh read failed"); FAIL += 1 + +# ── 5. Script Syntax Validation ──────────────────────────────────── +section("5. Script Syntax Validation") +for sh in sorted(os.listdir(wf_dir)): + if sh.endswith(".sh"): + bash_check(sh, os.path.join(wf_dir, sh)) +for lib_dir in ["lib"]: + libp = os.path.join(wf_dir, lib_dir) + if os.path.isdir(libp): + for sh in sorted(os.listdir(libp)): + if sh.endswith(".sh"): + bash_check(f"lib/{sh}", os.path.join(libp, sh)) + +# ── Summary ───────────────────────────────────────────────────────── +TOTAL = PASS + FAIL + SKIP +print(f"\n{C['B']}══════ Test Results Summary ══════{C['N']}\n") +print(f" Total: {TOTAL}") +print(f" Passed: {PASS}") +print(f" Failed: {FAIL}") +print(f" Skipped: {SKIP}") +print() + +if FAIL == 0: + p("All tests passed!") + sys.exit(0) +else: + f(f"{FAIL} test(s) failed") + sys.exit(1) diff --git a/workflows/test.sh b/workflows/test.sh new file mode 100644 index 00000000..ca925852 --- /dev/null +++ b/workflows/test.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# gitlink-cli Integration Test Suite +# Usage: bash workflows/test.sh [owner] [repo] +python "$(dirname "${BASH_SOURCE[0]}")/test.py" "${1:-zzx-coder}" "${2:-gitlink-cli}"