From c305c3542082843e66129020cfc4b1e6dbee088b Mon Sep 17 00:00:00 2001 From: laomao-31day001 Date: Mon, 13 Jul 2026 16:35:02 +0800 Subject: [PATCH] feat(ci): add pr-contributor-trust workflow + Bot Killer policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves Open-Source-Bazaar/Open-Source-Bazaar.github.io#89 (mirrored as Vikingr2023/awesome-agent-bounties#165) Adds: - .github/workflows/pr-contributor-trust.yml — two-stage workflow: * preflight job: skip bot-authored PRs (type=Bot OR login ends [bot]) * trust-signals job: compute 3 cheap signals (account age, diff size, description length) and label PRs trust/medium / trust/high - .github/contributor-trust.yml — tunables file so maintainers can adjust thresholds without touching the workflow source - CONTRIBUTING.md §9 — documents the Bot Killer policy for humans/agents - AGENTS.md — updated reference card (pulled from docs/contributing-guide branch so both PRs share the same docs baseline) References the design from mengxi-ream/read-frog's pr-contributor-trust.yml, adapted to the smaller scale of 开源市集 (we deliberately drop the heavy 100-point scoring system). Co-authored-by: laomao-31day001 --- .github/contributor-trust.yml | 38 ++++ .github/workflows/pr-contributor-trust.yml | 190 ++++++++++++++++ AGENTS.md | 63 ++++++ CONTRIBUTING.md | 245 +++++++++++++++++++++ 4 files changed, 536 insertions(+) create mode 100644 .github/contributor-trust.yml create mode 100644 .github/workflows/pr-contributor-trust.yml create mode 100644 AGENTS.md create mode 100644 CONTRIBUTING.md diff --git a/.github/contributor-trust.yml b/.github/contributor-trust.yml new file mode 100644 index 0000000..2e80c28 --- /dev/null +++ b/.github/contributor-trust.yml @@ -0,0 +1,38 @@ +# Contributor Trust Policy — opensource-bazaar +# +# Tunables for .github/workflows/pr-contributor-trust.yml. +# Keep this file flat YAML so maintainers can tweak thresholds without +# touching the workflow source. + +version: 1 + +# Minimum acceptable GitHub account age for human PRs. +# Anything below this triggers the `trust/medium` label. +min_account_age_days: 7 + +# Maximum diff size (additions + deletions) before flagging as `trust/medium`. +# Set generously: 开源市集 accepts sizable refactors, only true drive-by +# mega-PRs should be flagged here. +max_diff_lines: 500 + +# Minimum PR description length (characters, after trim). +# Less than this is treated as a "low effort" signal. +min_description_chars: 20 + +# Risk levels → label mapping. Colors follow GitHub's standard palette: +# - low: 0e8a16 (green) → no label applied (skip) +# - medium: fbca04 (yellow) → trust/medium +# - high: d93f0b (red) → trust/high +risk_labels: + medium: trust/medium + high: trust/high + +# PRs authored by accounts whose `type === "Bot"` OR whose login ends +# with `[bot]` are unconditionally skipped (the `preflight` job early-exits). +# Add additional bot-patterns below if needed. +bot_patterns: + - "[bot]" # GitHub Apps convention + - "-bot" # some self-hosted bots + - "dependabot" + - "renovate" + - "github-actions" \ No newline at end of file diff --git a/.github/workflows/pr-contributor-trust.yml b/.github/workflows/pr-contributor-trust.yml new file mode 100644 index 0000000..6e62b99 --- /dev/null +++ b/.github/workflows/pr-contributor-trust.yml @@ -0,0 +1,190 @@ +name: PR - Contributor Trust (Bot Killer) + +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + - ready_for_review + workflow_dispatch: + inputs: + pr_number: + description: Pull request number to re-score + required: true + type: number + +# Cancel in-progress re-runs for the same PR +concurrency: + group: contributor-trust-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }} + cancel-in-progress: true + +jobs: + preflight: + name: Preflight — detect bot authors + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + author_login: ${{ steps.inspect.outputs.author_login }} + pr_number: ${{ steps.inspect.outputs.pr_number }} + should_skip: ${{ steps.inspect.outputs.should_skip }} + skip_reason: ${{ steps.inspect.outputs.skip_reason }} + steps: + - name: Resolve PR metadata + id: inspect + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.eventName === "pull_request_target" + ? context.payload.pull_request?.number + : Number(context.payload.inputs?.pr_number ?? ""); + + if (!Number.isInteger(prNumber) || prNumber <= 0) + throw new Error(`Unable to resolve a valid pull request number for event ${context.eventName}.`); + + const { data: pullRequest } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + const authorLogin = pullRequest.user?.login ?? ""; + const authorType = pullRequest.user?.type ?? ""; + // Skip if (a) GitHub recognises the author as a Bot account + // or (b) the username ends with `[bot]` (e.g. dependabot[bot]) + const shouldSkip = authorType === "Bot" || authorLogin.toLowerCase().endsWith("[bot]"); + + core.setOutput("author_login", authorLogin); + core.setOutput("pr_number", String(prNumber)); + core.setOutput("should_skip", shouldSkip ? "true" : "false"); + core.setOutput( + "skip_reason", + shouldSkip ? `bot-authored PR by @${authorLogin}` : "" + ); + + - name: Write skip summary + if: steps.inspect.outputs.should_skip == 'true' + run: | + { + echo "## Contributor trust automation" + echo "" + echo "- Event: ${{ github.event_name }}" + echo "- PR: #${{ steps.inspect.outputs.pr_number }}" + echo "- Author: @${{ steps.inspect.outputs.author_login }}" + echo "- Result: skipped — ${{ steps.inspect.outputs.skip_reason }}" + } >> "$GITHUB_STEP_SUMMARY" + + # ─────────────────────────────────────────────────────────────────────── + # Phase 2 — Lightweight trust signals for human authors + # + # We deliberately avoid the heavy 100-point scoring system used by read-frog + # (which weighs repo stars, followers, etc.) — 开源市集 is a small community + # repo, so over-scoring would create more friction than safety. + # + # Instead we apply three cheap signals that catch the common Bot / low-quality + # PR patterns observed in this repo: + # 1. Account age (< 7 days = suspicious) + # 2. PR diff size ( > 500 lines OR zero-line change = suspicious) + # 3. Description length ( < 20 chars = suspicious) + # + # Combined signal is surfaced as a sticky PR comment + labels. + # ─────────────────────────────────────────────────────────────────────── + trust-signals: + name: Compute trust signals + needs: preflight + if: needs.preflight.outputs.should_skip != 'true' + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + steps: + - name: Inspect PR (author age, diff size, description) + id: inspect + uses: actions/github-script@v7 + with: + script: | + const prNumber = Number("${{ needs.preflight.outputs.pr_number }}"); + + const [{ data: pr }, { data: files }] = await Promise.all([ + github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }), + github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + per_page: 100, + }), + ]); + + const authorLogin = pr.user?.login ?? ""; + const authorCreatedAt = pr.user?.created_at ?? pr.head?.user?.created_at; + const description = pr.body ?? ""; + const totalAdditions = files.reduce((sum, f) => sum + (f.additions ?? 0), 0); + const totalDeletions = files.reduce((sum, f) => sum + (f.deletions ?? 0), 0); + + const accountAgeDays = authorCreatedAt + ? Math.floor((Date.now() - new Date(authorCreatedAt).getTime()) / (1000 * 60 * 60 * 24)) + : null; + + const flags = []; + if (accountAgeDays !== null && accountAgeDays < 7) { + flags.push(`account age ${accountAgeDays}d < 7d`); + } + if (totalAdditions + totalDeletions === 0) { + flags.push("empty diff"); + } else if (totalAdditions + totalDeletions > 500) { + flags.push(`large diff (${totalAdditions + totalDeletions} lines)`); + } + if (description.trim().length < 20) { + flags.push(`short description (${description.trim().length} chars)`); + } + + const riskLevel = flags.length >= 2 ? "high" : flags.length === 1 ? "medium" : "low"; + + const summary = [ + `### PR contributor trust signals`, + ``, + `- **Author**: @${authorLogin}${accountAgeDays !== null ? ` (account age: ${accountAgeDays}d)` : ""}`, + `- **Diff size**: +${totalAdditions} / -${totalDeletions} across ${files.length} files`, + `- **Description length**: ${description.trim().length} chars`, + `- **Risk level**: **${riskLevel}**`, + ...(flags.length ? [`- **Flags**: ${flags.join(", ")}`] : ["- **Flags**: none"]), + ].join("\n"); + + core.setOutput("risk_level", riskLevel); + core.setOutput("flags", flags.join("|")); + core.setOutput("summary", summary); + core.setOutput("author_login", authorLogin); + core.setOutput("pr_number", String(prNumber)); + + - name: Label PR by risk level + env: + RISK: ${{ steps.inspect.outputs.risk_level }} + PR_NUMBER: ${{ steps.inspect.outputs.pr_number }} + run: | + LABEL="trust/$RISK" + # Create the label if it doesn't exist + gh label create "$LABEL" \ + --description "Auto-computed by pr-contributor-trust.yml" \ + --color "$( [ "$RISK" = "high" ] && echo "d93f0b" || ( [ "$RISK" = "medium" ] && echo "fbca04" || echo "0e8a16" ) )" \ + --force || true + gh issue edit "$PR_NUMBER" --add-label "$LABEL" + + - name: Post trust summary as PR comment + if: steps.inspect.outputs.risk_level != 'low' + uses: actions/github-script@v7 + with: + script: | + const summary = `${{ steps.inspect.outputs.summary }}`; + const prNumber = Number("${{ steps.inspect.outputs.pr_number }}"); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: summary + "\n\n— posted by `pr-contributor-trust.yml`", + }); \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..54ae017 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,63 @@ +# AGENTS.md — Open Source Bazaar Agent Quick Reference + +> 配套 [CONTRIBUTING.md](./CONTRIBUTING.md) § 7。本文件是给 AI Agent 的**精简指令卡**——可被 `read` 工具一次性加载。 + +## 项目一句话 + +Next.js v16 + TypeScript v5 + Bootstrap v5 多模块站点(开源市集官网),承载 `/finance` 等公益开源子项目。 + +## 命令速查 + +```bash +pnpm install # 装依赖(必须 pnpm) +pnpm dev # 本地起 http://localhost:3000 +pnpm lint # ESLint +pnpm build # Next.js build +``` + +## 文件结构 + +``` +components/ # 可复用 React 组件(PascalCase.tsx) +constants/ # 全局常量(SCREAMING_SNAKE_CASE) +lib/ # 工具函数 / Hooks(camelCase.ts) +models/ # 数据模型 / 类型定义 +pages/ # Next.js 路由(kebab-case.ts/tsx) +public/ # 静态资源 +styles/ # CSS Module + 全局样式 +translation/ # i18n 文案(禁止硬编码) +types/ # 全局 TypeScript 类型 +``` + +## 修改优先级 + +1. **i18n 必走** `translation/` —— 任何用户可见字符串必须走 t(key) +2. **不破坏 `master`** —— PR 必须从 feat/fix 分支提 +3. **类型严格** —— 禁 `any`,导出去必须显式 `export` +4. **CI 必过** —— lint + build 全绿才允许 merge + +## 任务接单 SOP(接 bounties 适用) + +1. 扫镜像:Vikingr2023/awesome-agent-bounties open issues +2. 评估能力:跳过 mobile/unity/bluetooth 类 +3. `/claim #` 在 issue 下评论 +4. fork → branch → 改 → lint/build → push → 提 PR → comment 链接 +5. 48h 内交付;超时 `/unclaim` + +## 禁止 + +- ❌ 任何凭据写入代码 +- ❌ 修改非关联文件 +- ❌ `[skip ci]` 未经批准 +- ❌ 直接 push master + +## 资源链接 + +- 上游:[idea2app/Lark-Next-Bootstrap-ts](https://github.com/idea2app/Lark-Next-Bootstrap-ts) +- CI:[GitHub Actions](https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io/actions) +- 部署:[Vercel](https://vercel.com) +- 镜像仓库:[Vikingr2023/awesome-agent-bounties](https://github.com/Vikingr2023/awesome-agent-bounties) + +--- + +由 **laomao-31day001**(31day.cloud agent)· 2026-07-13 贡献。 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a542f88 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,245 @@ +# Contributing to 开源市集 (Open Source Bazaar) + +欢迎来到**开源市集**——一个由社区驱动的开源项目孵化平台。本文是面向**人类贡献者**与 **AI Agent** 的统一贡献指南。 + +> **TL;DR** — 提交 PR 前请确保:① 通过 `pnpm lint && pnpm build`;② 在 PR 描述中关联对应 issue(`Fixes #`);③ 遵循本文 § 4「代码规范」与 § 5「提交规范」。AI Agent 额外阅读 § 7「Agent Integration Guide」。 + +--- + +## 1. 目录 + +1. [项目简介](#1-项目简介) +2. [快速开始](#2-快速开始) +3. [开发流程](#3-开发流程) +4. [代码规范](#4-代码规范) +5. [提交规范](#5-提交规范) +6. [Review 与合并](#6-review-与合并) +7. [Agent Integration Guide](#7-agent-integration-guide) +8. [社区沟通](#8-社区沟通) + +--- + +## 1. 项目简介 + +**开源市集** 是一个 Next.js + TypeScript 多模块站点,承载多种公益开源子项目(如 `/finance` 指数基金精选页)。技术栈: + +| 维度 | 选型 | +|---|---| +| 语言 | TypeScript v5 | +| 组件引擎 | Next.js v16 | +| UI 套件 | Bootstrap v5 | +| 国际化 | 自研 `translation/` 目录 | +| CI/CD | GitHub Actions + Vercel | + +上游参考项目:[idea2app/Lark-Next-Bootstrap-ts](https://github.com/idea2app/Lark-Next-Bootstrap-ts)。 + +--- + +## 2. 快速开始 + +```bash +# 1. 克隆(含子模块若有) +git clone https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io.git +cd Open-Source-Bazaar.github.io + +# 2. 安装依赖(必须使用 pnpm) +pnpm install + +# 3. 启动开发服务器 +pnpm dev # http://localhost:3000 + +# 4. 类型检查 / Lint / 构建 +pnpm lint +pnpm build +``` + +> **注意**:项目使用 `pnpm-workspace.yaml`,请勿切换到 npm/yarn——lockfile 不兼容。 + +--- + +## 3. 开发流程 + +1. **Fork** 本仓库到你的账号。 +2. 从 `master` 切特性分支:`git checkout -b feat/` 或 `fix/`。 +3. 修改代码 + **写测试**(如适用)。 +4. 本地通过 `pnpm lint && pnpm build`。 +5. Push 到你的 fork:`git push origin feat/`。 +6. 在本仓库提 PR,**PR 描述里关联 issue**:`Fixes #`。 +7. 等待 CI + Maintainer Review,循环迭代直到 ✅ merge。 + +--- + +## 4. 代码规范 + +### 4.1 命名 + +- **文件名**:`PascalCase.tsx`(组件)、`camelCase.ts`(工具/hook)、`kebab-case.ts`(路由/常量模块) +- **组件**:导出 `PascalCase`,文件名一致 +- **常量**:`SCREAMING_SNAKE_CASE`(在 `constants/` 目录) +- **Hook**:`useXxx`,导出函数(不要默认导出) + +### 4.2 TypeScript + +- **禁止** `any`(除非有详尽注释解释原因) +- 优先 `interface` 而非 `type`(除非需要 union/intersection) +- 导出类型需 `export type X = ...` 或 `export interface X {...}` + +### 4.3 React / Next.js + +- **组件**:函数组件 + hooks;禁止 class 组件 +- **State**:优先 `useState` / `useReducer`;跨组件共享用 Context 或全局 store +- **样式**:Bootstrap utility class + `styles/` 下的 module CSS;避免内联 style +- **i18n**:所有用户可见字符串必须走 `translation/` 目录,禁止硬编码 + +### 4.4 格式化 + +- 使用项目自带的 Prettier / ESLint 配置(已 commit `eslint.config.ts`) +- 单行最大 100 字符 +- 缩进 4 空格,尾随逗号 `none` + +--- + +## 5. 提交规范 + +采用 **Conventional Commits**: + +``` +(): + + + +