公司动态

PR变动态架构图:让代码评审一眼看懂变更影响

📅 2026/8/30 16:49:35
PR变动态架构图:让代码评审一眼看懂变更影响
许多开发团队都会遇到一个很常见的场景PR 越堆越多评审人点开一看几十个文件、几百行改动根本看不出这次变更到底动了哪些模块、影响了哪条调用链。代码评审逐渐变成了“看 diff 有没有明显问题直接 Approve”。最近看到一个很有意思的开源方向——把每一个 PR 自动转化成动态架构图animated architecture diagrams让评审人能直观看到系统结构在本次合并前后发生了什么变化。这篇文章围绕这个思路拆解实现原理、完整落地步骤和常见坑点包含可直接改造使用的示例代码。需要提前说明一点这里说的 PR 是 Pull Request合并请求是 Git 协作里发起代码变更评审的单位不是视频剪辑软件 Premiere Pro。如果搜索 PR 时看到大量“PR 下载”“PR 控制”“PR 音频淡出”那是另一款软件的内容不要混淆。1. 背景与核心概念1.1 代码评审为什么需要架构图每次 PR 都代表一次代码变更可能是新增一个接口、拆分一个服务、修改一张表结构也可能是删掉一个没人知道的老模块。问题是diff 只能告诉你“哪一行变了”却很难回答“这个变更会影响哪些系统边界”。比如一个 PR 只在OrderService.java里改了一个方法签名看起来人畜无害但它可能影响 API 网关、前端页面、另一个微服务的 Feign Client。评审人在没有全局视角的情况下很难判断风险。这正是动态架构图要解决的问题把“代码级差异”翻译成“架构级差异”让风险一眼可见。所谓“动态架构图”不只是静态地画几个服务方块和连线而是用动画表达变化过程新增组件用绿色浮现出来删除组件用红色淡出被修改的组件用黄色闪烁新增的调用边线用流动效果表示。这样评审人不用对比两张图也能在几秒内理解本次变更“新增了什么、删除了什么、修改了什么”。1.2 开源思路的价值把这个过程自动化并开源意味着任何团队都可以把它接进自己的 CI/CD 流程而不需要购买商业架构治理平台。它的核心价值有三点第一降低评审成本。动态架构图把人工脑补的“全局影响分析”变成了自动生成的辅助材料。第二减少架构漂移。当架构图跟随 PR 自动更新团队能及时发现“计划里的模块边界”和“代码里的真实依赖”之间的偏差。第三可沉淀、可追溯。每次 PR 生成一张架构变化快照长期积累后就是一份可回放的系统演进历史。这类工具通常涉及三个技术点PR 变更解析、架构信息提取、图表动画渲染。下面逐一展开。2. 整体方案设计2.1 技术链路总览一个完整的“PR 转动画架构图”工具推荐按下面这条链路设计PR 触发阶段通过 GitHub Actions 监听pull_request事件变更采集阶段调用 GitHub REST API 获取本次 PR 涉及的文件、增删行数和原始地址架构分析阶段解析文件路径、依赖关系和关键配置提取受影响的架构组件差异计算阶段对比目标分支比如 main的基线快照得到新增、修改、删除清单图片生成阶段生成 Graphviz DOT 描述渲染成 SVG动画增强阶段在 SVG 中嵌入动画或逐帧渲染成 GIF内容回贴阶段把图片和文字说明作为评论发布到 PR 下。整体并不复杂难点在于第二步和第三步的规则要结合团队自己的技术栈定制。2.2 技术栈选型下面示例采用的组合是当前开源社区比较常见的轻量方案能力推荐方案说明CI 编排GitHub Actions与 PR 事件天然集成变更获取GitHub REST API GITHUB_TOKEN无需单独申请高权限密钥运行环境Node.js 18fetch原生可用脚本简洁依赖分析自定义解析脚本先按文件路径和关键配置做规则化分析图表渲染Graphvizdot成熟稳定支持 SVG 输出动画生成SVGanimate或 ImageMagick轻量场景用 SVG需要 GIF 时用逐帧这些工具都是开源免费、跨平台可用的适合作为基础依赖。3. 环境准备与版本说明3.1 本地开发环境建议先在本地跑通脚本再迁移到 GitHub Actions。需要准备以下环境Node.js 18 或更高版本脚本会使用原生fetchGit并且能访问目标仓库Graphviz用于渲染 DOT 文件为 SVG文本编辑器推荐 VS CodeGitHub Token本地调试时建议使用repo权限的临时 Token注意不要泄漏。安装命令以 Ubuntu 为例sudo apt update sudo apt install -y graphviz nodejs npm node -v dot -V如果使用 macOS可以用 Homebrewbrew install graphviz node node -v dot -VGraphviz 安装成功后会输出类似dot - graphviz version 2.43.0 (0)的版本信息。版本不同不影响本文示例的主流程。3.2 示例项目结构为了让读者能照着复现本文设计一个名为pr-architecture-diagram的最小项目目录结构如下pr-architecture-diagram/ ├── .github/ │ ├── workflows/ │ │ └── architecture-diagram.yml │ └── scripts/ │ └── comment.js ├── scripts/ │ ├── fetch-pr-files.js │ ├── analyze-architecture.js │ └── render-diagram.js ├── rules/ │ └── architecture-rules.json ├── package.json └── README.mdscripts目录放核心脚本rules目录放架构分析规则.github/workflows放 CI 编排。4. 核心原理拆解4.1 从 PR 到变更文件列表GitHub 提供了GET /repos/{owner}/{repo}/pulls/{pull_number}/files接口可以拿到一个 PR 修改过的所有文件信息包括文件名、状态、增删行数、原始文件地址等。下面用 Node.js 实现一个拉取变更文件的脚本。// scripts/fetch-pr-files.js const fs require(node:fs); const path require(node:path); const owner process.env.GITHUB_REPOSITORY?.split(/)[0]; const repo process.env.GITHUB_REPOSITORY?.split(/)[1]; const prNumber process.env.PR_NUMBER; const token process.env.GITHUB_TOKEN; if (!owner || !repo || !prNumber || !token) { console.error(缺少必要环境变量GITHUB_REPOSITORY、PR_NUMBER、GITHUB_TOKEN); process.exit(1); } async function getChangedFiles() { const response await fetch( https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files, { headers: { Authorization: token ${token}, User-Agent: pr-architecture-diagram, }, } ); if (!response.ok) { const body await response.text(); throw new Error(GitHub API 请求失败${response.status} ${response.statusText}\n${body}); } const files await response.json(); return files.map((file) ({ filename: file.filename, status: file.status, additions: file.additions, deletions: file.deletions, rawUrl: file.raw_url, })); } async function main() { const files await getChangedFiles(); fs.mkdirSync(artifacts, { recursive: true }); fs.writeFileSync( path.join(artifacts, changed-files.json), JSON.stringify(files, null, 2) ); console.log(共发现 ${files.length} 个变更文件); for (const file of files) { console.log(${file.status}: ${file.filename} (${file.additions}/-${file.deletions})); } } main().catch((error) { console.error(error); process.exit(1); });注意脚本一开始就校验环境变量避免缺少 Token 时发出无意义的请求。GITHUB_REPOSITORY是 GitHub Actions 自动注入的格式为owner/repo。4.2 从文件变更到架构组件拿到文件清单后需要把文件归类到架构组件。这是整个流程中最依赖团队规范的部分不可能有万能方案。通常可以从这几个维度提取一级目录常见微服务项目里services/order、services/user分别代表不同服务关键配置pom.xml、package.json、go.mod等文件位置标识模块边界文件类型*Controller.java、*Api.java、*Mapper.java标识组件在架构中的角色数据库脚本db/migration/*.sql标识数据层变更。先做一个小而美的规则文件让不同团队可以自行维护// rules/architecture-rules.json { serviceRoots: [services], roles: { controller: API 入口, service: 业务服务, mapper: 数据访问, client: 外部调用, entity: 数据模型 }, keyFiles: [ package.json, pom.xml, build.gradle, go.mod, docker-compose.yml ] }4.3 架构差异计算与可视化描述为了让最终输出可控我们先把架构组件变更整理成“基线对比”的结构化数据{ branch: main, added: [ { name: PaymentService, type: service, path: services/payment } ], modified: [ { name: OrderService, type: service, path: services/order } ], removed: [ { name: LegacyReportModule, type: module, path: modules/legacy-report } ], dependencies: [ { from: CheckoutBFF, to: PaymentService, action: add } ] }拿到结构化数据后就可以生成 Graphviz 的 DOT 描述文件。下面是一个核心片段完整脚本可以放在scripts/render-diagram.js中。// scripts/render-diagram.js 核心片段 function buildDot(architecture) { const lines []; lines.push(digraph architecture {); lines.push( rankdirLR;); lines.push( node [shapebox, stylefilled, fontnameMicrosoft YaHei];); lines.push( edge [fontnameMicrosoft YaHei];); for (const component of architecture.added) { lines.push( ${component.name} [fillcolor#d4f7dc label${component.name}\n${component.type}];); } for (const component of architecture.modified) { lines.push( ${component.name} [fillcolor#fff3bf label${component.name}\n${component.type}];); } for (const component of architecture.removed) { lines.push( ${component.name} [fillcolor#ffd4d4 label${component.name}\n${component.type}];); } for (const relation of architecture.dependencies) { lines.push( ${relation.from} - ${relation.to} [label${relation.action add ? : ~}];); } lines.push(}); return lines.join(\n); }将 DOT 内容写入artifacts/architecture.dot后执行dot -Tsvg artifacts/architecture.dot -o artifacts/architecture.svg如果成功会在artifacts目录下生成一个 SVG 架构图。5. 完整实战做一个 PR 架构图机器人5.1 初始化项目与依赖在空目录中初始化 Node.js 项目mkdir pr-architecture-diagram cd pr-architecture-diagram npm init -y npm install octokit/rest --save-dev这里引入octokit/rest是为了在评论步骤中更方便地调用 GitHub API。如果希望减少依赖也可以继续使用原生fetch。5.2 编写分析脚本继续完善scripts/analyze-architecture.js。它读取第一步生成的artifacts/changed-files.json根据规则文件把文件归类为架构组件。// scripts/analyze-architecture.js const fs require(node:fs); const path require(node:path); const changedFiles JSON.parse( fs.readFileSync(path.join(artifacts, changed-files.json), utf-8) ); const rules JSON.parse( fs.readFileSync(path.join(rules, architecture-rules.json), utf-8) ); const added []; const modified []; const removed []; function classifyFile(file) { const parts file.filename.split(/); const serviceRoot rules.serviceRoots.find((root) parts[0] root); if (serviceRoot parts.length 2) { return { name: parts[1], type: service, path: parts.slice(0, 2).join(/), }; } if (/controller/i.test(file.filename)) { return { name: parts.at(-1), type: controller, path: file.filename }; } if (/mapper|repository|dao/i.test(file.filename)) { return { name: parts.at(-1), type: mapper, path: file.filename }; } if (/\.sql$/i.test(file.filename)) { return { name: parts.at(-1), type: database migration, path: file.filename }; } return { name: parts.at(-1), type: file, path: file.filename }; } for (const file of changedFiles) { const component classifyFile(file); if (file.status added) { added.push(component); } else if (file.status removed) { removed.push(component); } else { modified.push(component); } } const architecture { branch: process.env.GITHUB_BASE_REF || main, added, modified, removed, dependencies: [], }; fs.mkdirSync(artifacts, { recursive: true }); fs.writeFileSync( path.join(artifacts, architecture.json), JSON.stringify(architecture, null, 2) ); console.log(新增组件${added.length}修改组件${modified.length}删除组件${removed.length});再次说明这里的归类规则是演示思路实际项目要根据自己的目录规范调整。5.3 生成动画 SVG 的两种思路SVG 本身支持动画标签不需要额外工具。我们可以让新增节点在渲染后出现闪烁效果让边线出现“流动”效果。在render-diagram.js中生成节点后可以附加animatecircle cx40 cy40 r8 fill#00aa55 animate attributeNameopacity values1;0.2;1 dur2s repeatCountindefinite / /circle如果需要生成 GIF更简单的方式是使用 ImageMagick 将几帧静态图拼接convert -delay 60 artifacts/frame-1.png artifacts/frame-2.png artifacts/frame-3.png artifacts/architecture.gif两种方案各有优劣SVG 动画体积小、清晰、浏览器原生支持但部分代码托管平台的评论区显示可能受限制GIF 动画兼容性最好几乎所有 Markdown 预览都支持但体积更大设计起来更繁琐。工程上建议优先出 SVG如果评论区不支持再退化渲染成 GIF。5.4 编写 GitHub Actions 工作流把整套流程接入 GitHub Actions核心是给作业配置足够的 token 权限。# .github/workflows/architecture-diagram.yml name: architecture-diagram on: pull_request: types: [opened, synchronize] permissions: contents: read pull-requests: write jobs: build-diagram: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkoutv4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-nodev4 with: node-version: 20 cache: npm - name: Install dependencies run: npm ci - name: Fetch PR changed files run: node scripts/fetch-pr-files.js env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} - name: Analyze architecture run: node scripts/analyze-architecture.js env: GITHUB_BASE_REF: ${{ github.base_ref }} - name: Render diagram run: | node scripts/render-diagram.js dot -Tsvg artifacts/architecture.dot -o artifacts/architecture.svg - name: Comment architecture diagram on PR uses: actions/github-scriptv7 with: script: | const fs require(fs); const diagram fs.readFileSync(artifacts/architecture.svg, utf-8); const summary JSON.parse( fs.readFileSync(artifacts/architecture.json, utf-8) ); const lines [ ## ️ 架构变更总览, , 本次 PR 涉及 **${summary.added.length}** 个新增组件、**${summary.modified.length}** 个修改组件、**${summary.removed.length}** 个删除组件。, , ### 组件签名, , - 新增${summary.added.map((item) item.name).join(, ) || 无}, - 修改${summary.modified.map((item) item.name).join(, ) || 无}, - 删除${summary.removed.map((item) item.name).join(, ) || 无}, , details, summary点击查看架构图/summary, , diagram, , /details, ]; await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: lines.join(\n), });这里有几个关键点需要解释permissions配置成pull-requests: write是最小权限机器人只需要写评论的能力不需要contents: write。actions/github-script的使用让评论脚本可以放在 workflow 里不必单独维护一个发布到 npm 的包。注意 SVG 内容里如果包含特殊字符可以放进details块中避免直接撑爆 Markdown 渲染。5.5 本地运行与验证本地调试时先设置环境变量再依次执行脚本export GITHUB_REPOSITORYyour-org/your-repo export PR_NUMBER123 export GITHUB_TOKENghp_xxx export GITHUB_BASE_REFmain node scripts/fetch-pr-files.js node scripts/analyze-architecture.js node scripts/render-diagram.js dot -Tsvg artifacts/architecture.dot -o artifacts/architecture.svg执行成功后打开artifacts/architecture.svg应该能看到架构图。新增组件为绿色、修改组件为黄色、删除组件为红色。如果artifacts/changed-files.json是空的说明 PR 没有实际文件变更脚本会正常退出但不会生成有意义的图。建议在工作流里增加一个判断if (changedFiles.length 0) { console.log(没有文件变更跳过渲染。); process.exit(0); }6. 常见问题与排查思路把这类工具接入团队仓库后最常见的问题集中在权限、环境变量和渲染依赖上。问题现象常见原因解决思路PR 评论一直不出现Actions 权限不足或脚本报错检查 workflow 日志确认secrets.GITHUB_TOKEN存在且pull-requests: write已配置403 错误Token 权限不够本地调试使用带repo权限的 TokenCI 中确认 job 的 permissions422 错误评论内容过长或包含不兼容字符将 SVG 折叠进details并限制生成内容大小Graphviz 渲染失败Actions 运行环境缺少dot工作流中增加apt-get install -y graphviz或使用官方 setup 步骤API 二次限流PR 文件过多多次请求同一接口一次只拉一页控制并发必要时使用 GraphQL API生成的图与期望不符解析规则不够完善先整理团队常用项目结构再逐步补充规则评论被机器人连刷工作流没有做幂等处理评论前查找是否已有机器人评论存在则删除旧评论或回复新评论其中一个值得展开的是评论重复问题。pull_request的synchronize事件在每次 push 新 commit 时都会触发如果工作流不判断旧评论PR 下面会堆一长串架构图评论。可以改用issues.listComments找到机器人创建的评论然后issues.updateComment更新它而不是每次新建。const comments await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, }); const botComment comments.data.find( (comment) comment.user.login github-actions[bot] comment.body.includes(架构变更总览) ); if (botComment) { await github.rest.issues.updateComment({ comment_id: botComment.id, owner: context.repo.owner, repo: context.repo.repo, body: newBody, }); } else { await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: newBody, }); }7. 最佳实践与工程建议7.1 最小权限与安全边界架构图机器人必须遵守最小权限原则。在 GitHub Actions 里只需要pull-requests: write和contents: read不要把actions: write、security-events: write等无关权限直接放开。如果团队使用的是 GitHub App 而不是原生 Token建议使用 OIDC 认证避免把长期有效的密钥写在 secrets 里。生成的内容要经过 Markdown 转义防止评论里被注入恶意链接。7.2 优先做增量分析不做全量重扫大型仓库里的全量架构扫描非常慢而且容易超时。更合理的做法是缓存 main 分支上一次的架构快照本次 PR 只分析有变更的文件用缓存快照参与差异计算。这样可以显著缩短执行时间尤其是 monorepo 场景。- name: Cache architecture baseline uses: actions/cachev4 with: path: artifacts/baseline.json key: architecture-baseline-${{ github.base_ref }}-${{ github.base_sha }}当然缓存命中率取决于提交频率。如果频繁变动建议退化为“拉取 main 分支后重新生成基线”但限制扫描范围到本次涉及的模块。7.3 失败不阻塞主流程架构图是辅助评审的工具不应该成为合并的阻塞项。建议工作流中允许失败时只输出 warning而不是 fail- name: Render diagram run: | node scripts/render-diagram.js || echo render failed, skip如果渲染失败PR 评论里可以给出一个降级提示但不影响贡献者继续提交代码。7.4 规则文件必须版本化架构规则是团队知识的一部分必须跟代码一起提交而不是放在某个运维人员的本地文件夹里。每次更新规则也走 PR 评审这样规则本身也是可追溯的。7.5 控制评论长度评论太长会拖慢页面加载也容易触发平台的评论长度限制。建议只列出 Top 10 的组件变化SVG 折叠在details中完整 JSON 作为 artifact 上传不在评论里展开。7.6 从最小闭环开始第一次实现时不要一开始就追求复杂的 AST 解析和完整依赖图。先做一个“文件路径级别的架构图”让团队看到价值。等大家都依赖这个机器人了再逐步加入接口签名分析、数据库变更分析和跨服务调用链分析。8. 总结与下一步本文从 PR 评审的痛点出发梳理了“把 PR 自动转成动画架构图”的完整实现思路从 GitHub API 获取变更文件、规则化提取架构组件、生成 Graphviz SVG到 GitHub Actions 自动回贴评论每一步都给出了可直接改造的代码片段。核心收获可以归纳为三条第一PR 级架构图的关键不是画得有多精美而是解释“这次变更影响了哪些边界”因此增量差异计算比全量架构可视化更重要。第二解析规则必须和团队实际项目结构绑定先小范围验证再逐步补充规则才能真正提高代码评审效率。第三自动化工具要设计成“非阻塞、低噪音、可追溯”的辅助角色避免因为评论刷屏和频繁失败消耗掉团队的信任。下一步可以继续探索的方向包括用 AST 解析接口签名变化、接入依赖图工具如 Dependency-Check 或 OWASP Dependency-Track做安全影响分析、把架构图历史变成可视化演进时间线。最开始的实现也不一定非要用 GitHub ActionsGitLab CI、Jenkins 甚至本地命令行都可以跑通同一套脚本掌握核心思路后迁移到其他 CI 平台并不困难。建议先从一个小仓库开始实践用真实 PR 去测试规则文件是否合理跑通后再推广到核心项目。架构图机器人的价值只有在团队真正需要“快速判断一次合并的影响范围”时才会充分体现出来。