diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yaml b/.github/ISSUE_TEMPLATE/01_bug_report.yaml index f5fadcab1..9f9020f2d 100644 --- a/.github/ISSUE_TEMPLATE/01_bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yaml @@ -8,42 +8,17 @@ body: - type: markdown attributes: value: | - 感谢反馈。请尽量填写完整,尤其是 **ScriptCat 版本、浏览器版本、复现步骤、日志、相关脚本链接/导出文件**。 - 如果是某个用户脚本在 ScriptCat 不兼容,但在 Tampermonkey/Violentmonkey 正常,请改用 “Userscript Compatibility” 模板。 + 感谢反馈。请尽量写清复现步骤和版本信息。 + + 如果是某个用户脚本在 ScriptCat 不兼容、但在 Tampermonkey/Violentmonkey 正常,请改用 “Userscript 兼容性问题” 模板。 - type: checkboxes id: precheck attributes: label: 提交前检查 options: - - label: 我已经搜索过现有 issues,确认不是重复问题 - required: true - - label: 我已经尽量使用最新 stable 或 beta 版本测试 + - label: 我已经搜索过现有 issues,并尽量用最新 stable 或 beta 版本确认问题仍然存在 required: true - - label: 这不是安全漏洞;安全漏洞会通过 Security Advisory 私密提交 - required: true - - - type: dropdown - id: bug-area - attributes: - label: 问题类型 - multiple: true - options: - - 扩展安装 / 更新 / 卸载 - - 脚本安装 / 更新 / 导入 / 导出 - - 脚本执行 / 注入 / 匹配规则 - - GM API / CAT API - - 后台脚本 / 定时脚本 / offscreen - - Cloud Sync / 备份 / 恢复 / WebDAV / Google Drive / OneDrive - - Popup / Options 管理页 / UI - - 内置编辑器 / Monaco / ESLint - - 日志 / 调试 / 错误提示 - - 移动端 / 小屏适配 - - 隐身模式 / 多窗口 / 多标签页 - - 性能 / 内存 / 存储 - - 其他 - validations: - required: true - type: textarea id: summary @@ -52,14 +27,17 @@ body: description: 实际发生了什么?你原本期望发生什么? placeholder: | 例:在 Edge Android 上打开 ScriptCat popup 后点击“导入文件”,文件选择器出现但选择后没有任何反应。 + + 期望: + 实际: validations: required: true - type: textarea id: steps attributes: - label: 最小复现步骤 - description: 请写成别人可以照着操作的步骤。不要只写“1”“见图”“如题”。 + label: 复现步骤 + description: 请写成别人可以照着操作的步骤。 placeholder: | 1. 安装 ScriptCat v... 2. 打开 ... @@ -68,16 +46,6 @@ body: validations: required: true - - type: textarea - id: expected-actual - attributes: - label: 期望行为 vs 实际行为 - placeholder: | - 期望: - 实际: - validations: - required: true - - type: input id: scriptcat-version attributes: @@ -86,114 +54,22 @@ body: validations: required: true - - type: dropdown - id: release-channel - attributes: - label: ScriptCat 渠道 - options: - - Chrome Stable - - Chrome Beta - - Edge Stable - - Edge Beta - - Firefox Stable / MV2 - - Firefox Beta / MV2 - - GitHub Release 手动安装 - - 自行构建 / dev - - VSCode 插件 - - 不确定 - validations: - required: true - - - type: dropdown - id: manifest-mode - attributes: - label: Manifest / 扩展模式 - options: - - MV3 - - MV2 / Firefox - - 不确定 - validations: - required: true - - - type: input - id: os - attributes: - label: 操作系统 - placeholder: "例如:Windows 11 24H2 / macOS 15 / Android 16 / HarmonyOS 4.2 / iOS" - validations: - required: true - - type: input id: browser attributes: - label: 浏览器及版本 - placeholder: "例如:Chrome 143.0.7499.41 / Edge 146.0.3802.0 / Firefox 151.0b7 / Brave 1.92.120 / Cromite 142..." - validations: - required: true - - - type: dropdown - id: device-type - attributes: - label: 设备类型 - options: - - 桌面端 - - Android 手机/平板 - - iOS/iPadOS - - 其他 + label: 系统 / 浏览器及版本 + placeholder: "例如:Windows 11 + Chrome 143 / Android 16 + Edge 146 / macOS + Firefox 151" validations: required: true - - type: checkboxes - id: browser-flags - attributes: - label: 浏览器/扩展状态 - options: - - label: 开启了隐身模式 / InPrivate / Private Window - - label: 使用移动端浏览器扩展支持 - - label: 使用第三方 Chromium 浏览器 - - label: 使用第三方 Firefox/Gecko 浏览器 - - label: 已授予 ScriptCat 对目标网站的站点访问权限 - - label: 开启了开发者模式 - - label: 安装了其他可能影响页面/请求/脚本的扩展 - - - type: textarea - id: related-script - attributes: - label: 相关用户脚本 / 配置 - description: 如果问题和某个脚本有关,请提供安装链接、脚本头部 metadata、或 ScriptCat 导出的 zip。敏感信息请先脱敏。 - placeholder: | - 脚本链接: - 目标网站: - 关键 metadata: - ```js - // ==UserScript== - // @name ... - // @match ... - // @grant ... - // @connect ... - // ==/UserScript== - ``` - validations: - required: false - - - type: textarea - id: logs - attributes: - label: 日志 / 错误信息 / 截图 - description: | - 请说明日志来自哪里:页面 DevTools、扩展 Service Worker、offscreen.html、popup、options 页面、定时脚本日志等。 - placeholder: | - 日志来源: - 错误信息: - 截图/视频: - validations: - required: false - - type: textarea - id: workaround + id: details attributes: - label: 是否有临时解决办法 + label: 相关脚本 / 日志 / 截图 + description: 相关脚本的安装链接或 metadata、控制台或 Service Worker 日志、截图。敏感信息请先脱敏。 placeholder: | - 例:刷新页面后恢复;换成 Tampermonkey 正常;关闭隐身模式后正常;重新安装脚本后正常。 + 脚本链接 / 目标网站: + 日志(来源:页面 DevTools / Service Worker / offscreen / popup): + 截图: validations: required: false diff --git a/.github/ISSUE_TEMPLATE/02_userscript_compatibility.yaml b/.github/ISSUE_TEMPLATE/02_userscript_compatibility.yaml index 2d5c15db5..45bce4e08 100644 --- a/.github/ISSUE_TEMPLATE/02_userscript_compatibility.yaml +++ b/.github/ISSUE_TEMPLATE/02_userscript_compatibility.yaml @@ -9,19 +9,15 @@ body: attributes: value: | 这个模板专门用于 **用户脚本兼容性**。 - 请尽量提供脚本链接、目标网站、metadata、对比结果和最小复现脚本。 - 如果只是 ScriptCat 界面或同步功能异常,请使用 Bug 反馈模板。 + + 如果只是 ScriptCat 界面、同步、安装等本体功能异常,请改用 Bug 反馈模板。 - type: checkboxes id: precheck attributes: label: 提交前检查 options: - - label: 我已经搜索过现有 issues - required: true - - label: 我确认该问题与某个用户脚本或 userscript API 兼容性有关 - required: true - - label: 我已经尽量提供脚本链接或最小复现脚本 + - label: 我已经搜索过现有 issues,并确认问题与某个用户脚本或 userscript API 有关 required: true - type: input @@ -36,29 +32,17 @@ body: id: target-url attributes: label: 触发问题的目标网站 - placeholder: "例如: " validations: required: true - type: textarea id: problem attributes: - label: 在 ScriptCat 中的异常表现 + label: 问题表现与对比 + description: 在 ScriptCat 中是什么表现?在其他脚本管理器中又是什么表现?请注明对比用的管理器和版本。 placeholder: | - 例:按钮不出现 / 脚本不执行 / GM_xmlhttpRequest 报错 / 菜单项无效 / 页面无限刷新 - validations: - required: true - - - type: textarea - id: comparison - attributes: - label: 与其他用户脚本管理器的对比 - description: 请写清楚 Tampermonkey / Violentmonkey / FireMonkey / Greasemonkey 的结果。 - placeholder: | - Tampermonkey 版本: - Violentmonkey 版本: - FireMonkey/Greasemonkey 版本: - 在这些管理器中的表现: + 在 ScriptCat 中:按钮不出现 / 脚本不执行 / GM_xmlhttpRequest 报错 / 页面无限刷新 + 在其他管理器中(如 Tampermonkey 5.x):正常 validations: required: true @@ -78,41 +62,6 @@ body: validations: required: true - - type: textarea - id: minimal-reproduction - attributes: - label: 最小复现脚本 - description: 如果原脚本太大,请尽量删减成最小可复现版本。 - render: javascript - placeholder: | - // 尽量提供能单独复现问题的最小脚本 - validations: - required: false - - - type: dropdown - id: api-area - attributes: - label: 可能相关的 API / 机制 - multiple: true - options: - - GM_xmlhttpRequest / GM.xmlHttpRequest - - GM_getValue / GM.setValue / GM storage - - GM_registerMenuCommand - - GM_download - - GM_cookie / Cookie API - - "@match / @include / @exclude" - - "@connect" - - "@require / @resource" - - "@run-at" - - "@inject-into" - - sandbox / unsafeWindow / page context - - iframe / allFrames / noframes - - CSP / Trusted Types - - service worker / offscreen / background - - 不确定 - validations: - required: true - - type: input id: scriptcat-version attributes: @@ -124,19 +73,19 @@ body: - type: input id: browser attributes: - label: 操作系统和浏览器 - placeholder: "例如:Windows 11 Chrome 143 / macOS Firefox 151 / Android WebLibre 0.20" + label: 系统 / 浏览器及版本 + placeholder: "例如:Windows 11 + Chrome 143 / macOS + Firefox 151 / Android + WebLibre 0.20" validations: required: true - type: textarea - id: logs + id: details attributes: - label: 控制台日志 / 网络请求 / 截图 - description: 请说明日志来源:页面 console、content script、service worker、offscreen、网络面板等。 + label: 最小复现脚本 / 日志 / 截图 + description: 如果原脚本太大,请尽量删减成最小可复现版本。日志请注明来源:页面 console、content script、Service Worker、offscreen、网络面板等。 placeholder: | - 日志来源: - 错误: - 截图/视频: + 最小复现脚本: + 日志来源与错误信息: + 截图 / 视频: validations: required: false diff --git a/.github/ISSUE_TEMPLATE/03_feature_request.yaml b/.github/ISSUE_TEMPLATE/03_feature_request.yaml index 738e12d6d..62f7d4893 100644 --- a/.github/ISSUE_TEMPLATE/03_feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/03_feature_request.yaml @@ -9,45 +9,22 @@ body: attributes: value: | 请描述你真正想解决的问题,而不只是描述一个按钮或 API 名称。 - 如果是 GM/CAT API 设计,建议给出 API 草案、兼容对象和安全影响。 + + 如果某个功能失效、或者以前能用现在不行,请改用 Bug 反馈模板。 - type: checkboxes id: precheck attributes: label: 提交前检查 options: - - label: 我已经搜索过现有 issues,确认不是重复功能请求 - required: true - - label: 我已经确认这不是 bug;如果是异常行为,应使用 Bug 模板 + - label: 我已经搜索过现有 issues,确认不是重复的功能请求 required: true - - type: dropdown - id: feature-area - attributes: - label: 功能领域 - multiple: true - options: - - 用户脚本管理 / 列表 / 分组 / 搜索 - - 脚本安装 / 更新 / 订阅 - - 脚本匹配 / 排除 / 权限管理 - - GM API / CAT API - - 后台脚本 / 定时脚本 - - Cloud Sync / 备份 / 恢复 / 迁移 - - Popup / Options / UI - - 内置编辑器 / Monaco / ESLint / VSCode 插件 - - 日志 / 调试 / 开发者体验 - - 移动端适配 - - 国际化 / 翻译 - - 性能 / 存储 / 安全 - - 其他 - validations: - required: true - - type: textarea id: problem attributes: - label: 你遇到的实际问题 - description: 没有这个功能时,用户或脚本作者会遇到什么痛点? + label: 你想解决的问题 + description: 没有这个功能时,你或脚本作者会遇到什么麻烦? placeholder: | 例:我有很多定时签到脚本,目前必须逐个打开日志才能确认运行结果。 validations: @@ -63,35 +40,11 @@ body: required: true - type: textarea - id: api-design - attributes: - label: API / UI 草案(如适用) - description: 如果涉及 GM/CAT API、配置结构、同步格式、UI 流程,请给出草案。 - placeholder: | - ```ts - // API 草案 - ``` - validations: - required: false - - - type: textarea - id: alternatives - attributes: - label: 备选方案 - description: 是否已有 workaround?其他扩展是如何处理的? - placeholder: | - Tampermonkey / Violentmonkey / FireMonkey / Soulsign / Automa 等参考: - validations: - required: false - - - type: textarea - id: compatibility + id: additional-info attributes: - label: 兼容性和安全影响 - description: 是否影响 MV2/MV3、Firefox、移动端、隐身模式、站点权限、用户脚本权限? + label: 补充信息 + description: 可以补充 API 草案、其他脚本管理器的做法、截图或参考链接。 placeholder: | - 可能影响: - 需要新增权限: - 是否可能被恶意脚本滥用: + API 草案 / 参考实现 / 截图: validations: required: false diff --git a/.github/ISSUE_TEMPLATE/04_technical_proposal.yaml b/.github/ISSUE_TEMPLATE/04_technical_proposal.yaml index 0e128e53a..d0ab30bb8 100644 --- a/.github/ISSUE_TEMPLATE/04_technical_proposal.yaml +++ b/.github/ISSUE_TEMPLATE/04_technical_proposal.yaml @@ -9,29 +9,8 @@ body: attributes: value: | 这个模板用于技术设计、重构、性能优化、存储结构、运行时架构等提案。 - 普通用户 bug 或功能请求请使用其他模板。 - - type: dropdown - id: proposal-area - attributes: - label: 涉及模块 - multiple: true - options: - - Service Worker - - offscreen - - sandbox / inject / content script - - script matching / registry - - GM/CAT API runtime - - storage / Dexie / IndexedDB / OPFS - - sync / backup / import/export - - resource loader / @require / @resource - - editor / Monaco / ESLint - - popup / options UI - - build / packaging / manifest - - tests / e2e / CI - - docs / i18n - validations: - required: true + 普通用户 bug 或功能请求请使用其他模板。 - type: textarea id: background @@ -50,25 +29,13 @@ body: required: true - type: textarea - id: compatibility + id: impact attributes: - label: 兼容性 / 迁移影响 + label: 兼容性 / 迁移 / 测试 placeholder: | 对旧版本数据的影响: - 对 MV2/MV3 的影响: - 对 Firefox/Chrome/Edge/移动端的影响: + 对 MV2/MV3、Firefox/Chrome/Edge/移动端的影响: 是否需要 migration: - validations: - required: true - - - type: textarea - id: test-plan - attributes: - label: 测试计划 - placeholder: | - 单元测试: - e2e: - 手动测试浏览器: - 回归风险: + 测试与回归风险: validations: required: false diff --git a/.github/ISSUE_TEMPLATE/05_documentation.yaml b/.github/ISSUE_TEMPLATE/05_documentation.yaml index b5b712395..60a660e78 100644 --- a/.github/ISSUE_TEMPLATE/05_documentation.yaml +++ b/.github/ISSUE_TEMPLATE/05_documentation.yaml @@ -9,9 +9,8 @@ body: attributes: value: | 感谢帮助改进 ScriptCat 文档。 - 请尽量提供具体页面、章节、当前问题和建议修改内容。 - 如果是界面翻译、语言包、本地化措辞问题,请使用 “Translation / i18n” 模板。 + 如果是界面翻译、语言包、本地化措辞问题,请改用 “翻译 / 本地化” 模板。 - type: checkboxes id: precheck @@ -20,73 +19,12 @@ body: options: - label: 我已经搜索过现有 issues,确认不是重复问题 required: true - - label: 我确认这是文档问题,而不是 ScriptCat 本体 bug - required: true - type: input id: doc-url attributes: label: 相关文档页面 description: 请提供 docs.scriptcat.org、learn.scriptcat.org、README、CONTRIBUTING 或其他相关页面。 - placeholder: "例如:" - validations: - required: true - - - type: dropdown - id: doc-language - attributes: - label: 文档语言 - options: - - 简体中文 / zh-CN - - 繁體中文 / zh-TW - - English / en - - 日本語 / ja - - Русский / ru - - Deutsch / de - - Tiếng Việt / vi - - 其他 / Other - - 不确定 - validations: - required: true - - - type: dropdown - id: doc-area - attributes: - label: 文档领域 - multiple: true - options: - - 安装 / 更新 / 浏览器支持 - - 用户脚本安装 / 更新 / 订阅 - - 用户脚本开发指南 - - GM API / CAT API - - 后台脚本 / 定时脚本 - - Cloud Sync / 备份 / 恢复 / 迁移 - - 权限 / 安全 / 隐身模式 - - Popup / Options / UI - - 内置编辑器 / VSCode 插件 - - 调试 / 日志 / 开发者工具 - - 构建 / 贡献指南 - - README / Release Notes - - 其他 - validations: - required: true - - - type: dropdown - id: doc-issue-type - attributes: - label: 文档问题类型 - multiple: true - options: - - 内容错误 - - 内容过期 - - 缺少说明 - - 表达不清楚 - - 示例代码无法运行 - - 链接失效 - - 图片 / 截图过期 - - 页面结构或导航问题 - - 与实际 UI 或行为不一致 - - 其他 validations: required: true @@ -107,27 +45,13 @@ body: description: 请说明你希望如何修改文档。可以直接贴出建议文案。 placeholder: | 建议改为: - ... validations: required: true - - type: textarea - id: affected-version - attributes: - label: 相关版本 / 环境(如适用) - description: 如果文档问题和特定 ScriptCat 版本、浏览器或平台有关,请填写。 - placeholder: | - ScriptCat 版本: - 浏览器 / 平台: - validations: - required: false - - type: textarea id: additional-info attributes: label: 补充信息 description: 可以补充截图、相关 issue、PR、代码位置或参考资料。 - placeholder: | - 截图 / 参考资料 / 相关链接: validations: required: false diff --git a/.github/ISSUE_TEMPLATE/06_translation.yaml b/.github/ISSUE_TEMPLATE/06_translation.yaml index d879f0388..c13ceb51d 100644 --- a/.github/ISSUE_TEMPLATE/06_translation.yaml +++ b/.github/ISSUE_TEMPLATE/06_translation.yaml @@ -9,9 +9,8 @@ body: attributes: value: | 感谢帮助改进 ScriptCat 的本地化。 - 请尽量提供原文、当前译文、建议译文和出现位置。 - 如果是文档内容本身错误、过期或缺失,请使用 “Documentation” 模板。 + 如果是文档内容本身错误、过期或缺失,请改用 “文档问题 / 改进” 模板。 - type: checkboxes id: precheck @@ -20,27 +19,6 @@ body: options: - label: 我已经搜索过现有 issues,确认不是重复问题 required: true - - label: 我已经确认这是翻译 / 本地化问题,而不是功能 bug - required: true - - - type: dropdown - id: i18n-area - attributes: - label: 问题范围 - multiple: true - options: - - 扩展界面 / Popup - - Options 管理页 - - 安装 / 更新页面 - - 内置编辑器 / Monaco / ESLint 提示 - - 日志 / 错误提示 / 通知 - - 文档 - - README / CONTRIBUTING / SECURITY - - VSCode 插件 - - ScriptCat 网站 / 脚本市场 - - 其他 - validations: - required: true - type: dropdown id: language @@ -59,28 +37,11 @@ body: validations: required: true - - type: dropdown - id: issue-type - attributes: - label: 翻译问题类型 - multiple: true - options: - - 翻译错误 - - 术语不一致 - - 机器翻译不自然 - - 缺少翻译 / fallback 到其他语言 - - 文案太长导致 UI 溢出 - - 标点 / 大小写 / 格式问题 - - 新增语言支持 - - 其他 - validations: - required: true - - type: input id: location attributes: label: 出现位置 - description: 请提供页面、菜单路径、截图位置、文件路径或文档页面。 + description: 请提供页面、菜单路径、文件路径或文档页面。 placeholder: "例如:Options > Settings > Script Sync / src/locales//translation.json" validations: required: true @@ -88,10 +49,8 @@ body: - type: textarea id: source-text attributes: - label: 原文 / 当前文案 - description: 请贴出看到的原文或当前翻译。 - placeholder: | - 当前文案: + label: 当前文案 + description: 请贴出你看到的原文或当前翻译。 validations: required: true @@ -100,35 +59,15 @@ body: attributes: label: 建议译文 description: 请提供你建议使用的翻译;如果是新增语言请求,请说明目标语言和是否愿意贡献。 - placeholder: | - 建议改为: validations: required: true - type: textarea - id: reason + id: additional-info attributes: - label: 修改理由 - description: 为什么当前翻译不合适?是否有术语、上下文或地区用语考虑? + label: 修改理由 / 截图 + description: 为什么当前翻译不合适?是否涉及术语、上下文或地区用语?如果涉及 UI 溢出或截断,请上传截图。 placeholder: | 例:这里的 “sync” 在产品中统一译为 “同步”,不应译为 “备份”。 validations: required: false - - - type: input - id: scriptcat-version - attributes: - label: ScriptCat 版本(如适用) - placeholder: "例如:v1.4.0 / v1.4.0-beta / Firefox MV2 v0.16.15" - validations: - required: false - - - type: textarea - id: screenshots - attributes: - label: 截图 / 补充信息 - description: 如果涉及 UI 溢出、截断、换行或上下文,请上传截图。 - placeholder: | - 截图 / 相关链接: - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/11_bug_report_en.yaml b/.github/ISSUE_TEMPLATE/11_bug_report_en.yaml index 909e9e504..de9a9274a 100644 --- a/.github/ISSUE_TEMPLATE/11_bug_report_en.yaml +++ b/.github/ISSUE_TEMPLATE/11_bug_report_en.yaml @@ -8,66 +8,36 @@ body: - type: markdown attributes: value: | - Thanks for reporting a bug. + Thanks for reporting a bug. Please describe the reproduction steps and version information as clearly as you can. - Please provide as much detail as possible, especially: - - ScriptCat version - - Browser and OS version - - Reproduction steps - - Logs or screenshots - - Related userscript link or exported script package, if applicable - - If the issue is that a specific userscript works in Tampermonkey / Violentmonkey but not in ScriptCat, please use the **Userscript Compatibility** template instead. + If a specific userscript works in Tampermonkey / Violentmonkey but not in ScriptCat, please use the **Userscript Compatibility** template instead. - type: checkboxes id: precheck attributes: label: Pre-check options: - - label: I have searched existing issues and confirmed this is not a duplicate. - required: true - - label: I have tested with the latest stable or beta version where possible. + - label: I have searched existing issues, and confirmed the problem still happens on the latest stable or beta version where possible. required: true - - label: This is not a security vulnerability. Security issues should be reported privately through Security Advisory. - required: true - - - type: dropdown - id: bug-area - attributes: - label: Issue Area - multiple: true - options: - - Extension installation / update / uninstall - - Userscript installation / update / import / export - - Script execution / injection / matching rules - - GM API / CAT API - - Background scripts / scheduled scripts / offscreen - - Cloud Sync / backup / restore / WebDAV / Google Drive / OneDrive - - Popup / Options page / UI - - Built-in editor / Monaco / ESLint - - Logs / debugging / error messages - - Mobile / small-screen support - - Incognito mode / multiple windows / multiple tabs - - Performance / memory / storage - - Other - validations: - required: true - type: textarea id: summary attributes: - label: Problem Description - description: What happened? What did you expect to happen? + label: Description + description: What actually happened, and what did you expect to happen? placeholder: | - Example: On Edge for Android, after opening the ScriptCat popup and selecting "Import File", the file picker opens, but nothing happens after choosing a file. + Example: On Edge for Android, opening the ScriptCat popup and tapping "Import file" shows the file picker, but nothing happens after selecting a file. + + Expected: + Actual: validations: required: true - type: textarea id: steps attributes: - label: Minimal Reproduction Steps - description: Please provide steps that others can follow. Do not write only "1", "see screenshot", or "as title". + label: Steps to Reproduce + description: Please write steps someone else can follow. placeholder: | 1. Install ScriptCat v... 2. Open ... @@ -76,17 +46,6 @@ body: validations: required: true - - type: textarea - id: expected-actual - attributes: - label: Expected Behavior vs Actual Behavior - placeholder: | - Expected: - - Actual: - validations: - required: true - - type: input id: scriptcat-version attributes: @@ -95,115 +54,22 @@ body: validations: required: true - - type: dropdown - id: release-channel - attributes: - label: ScriptCat Channel - options: - - Chrome Stable - - Chrome Beta - - Edge Stable - - Edge Beta - - Firefox Stable / MV2 - - Firefox Beta / MV2 - - GitHub Release manual installation - - Self-built / dev - - VSCode extension - - Not sure - validations: - required: true - - - type: dropdown - id: manifest-mode - attributes: - label: Manifest / Extension Mode - options: - - MV3 - - MV2 / Firefox - - Not sure - validations: - required: true - - - type: input - id: os - attributes: - label: Operating System - placeholder: "Example: Windows 11 24H2 / macOS 15 / Android 16 / HarmonyOS 4.2 / iOS" - validations: - required: true - - type: input id: browser attributes: - label: Browser and Version - placeholder: "Example: Chrome 143.0.7499.41 / Edge 146.0.3802.0 / Firefox 151.0b7 / Brave 1.92.120 / Cromite 142..." + label: OS / Browser and Version + placeholder: "Example: Windows 11 + Chrome 143 / Android 16 + Edge 146 / macOS + Firefox 151" validations: required: true - - type: dropdown - id: device-type - attributes: - label: Device Type - options: - - Desktop - - Android phone / tablet - - iOS / iPadOS - - Other - validations: - required: true - - - type: checkboxes - id: browser-flags - attributes: - label: Browser / Extension State - options: - - label: Incognito / InPrivate / Private Window is enabled - - label: Mobile browser extension support is being used - - label: Third-party Chromium-based browser is being used - - label: Third-party Firefox / Gecko-based browser is being used - - label: ScriptCat has been granted site access permission for the target website - - label: Developer mode is enabled - - label: Other extensions may affect pages, requests, or userscripts - - - type: textarea - id: related-script - attributes: - label: Related Userscript / Configuration - description: If the issue is related to a specific userscript, please provide its install URL, metadata block, or an exported ScriptCat zip package. Please remove sensitive information first. - placeholder: | - Userscript URL: - Target website: - Relevant metadata: - ```js - // ==UserScript== - // @name ... - // @match ... - // @grant ... - // @connect ... - // ==/UserScript== - ``` - validations: - required: false - - - type: textarea - id: logs - attributes: - label: Logs / Error Messages / Screenshots - description: | - Please also mention where the logs come from: - page DevTools, extension Service Worker, offscreen.html, popup, options page, scheduled script logs, etc. - placeholder: | - Log source: - Error message: - Screenshots / video: - validations: - required: false - - type: textarea - id: workaround + id: details attributes: - label: Temporary Workaround + label: Related Script / Logs / Screenshots + description: Installation URL or metadata of the related script, console or Service Worker logs, and screenshots. Please redact sensitive information first. placeholder: | - Example: Refreshing the page fixes it; it works in Tampermonkey; it works after disabling incognito mode; reinstalling the userscript fixes it. + Script URL / target website: + Logs (source: page DevTools / Service Worker / offscreen / popup): + Screenshots: validations: required: false diff --git a/.github/ISSUE_TEMPLATE/12_userscript_compatibility_en.yaml b/.github/ISSUE_TEMPLATE/12_userscript_compatibility_en.yaml index 37ed9be35..fecfc2373 100644 --- a/.github/ISSUE_TEMPLATE/12_userscript_compatibility_en.yaml +++ b/.github/ISSUE_TEMPLATE/12_userscript_compatibility_en.yaml @@ -10,13 +10,6 @@ body: value: | This template is specifically for **userscript compatibility issues**. - Please provide: - - Userscript URL - - Target website - - Metadata block - - Comparison with Tampermonkey / Violentmonkey / other managers - - Minimal reproduction script if possible - If the issue is about ScriptCat UI, sync, backup, installation, or general extension behavior, please use the Bug Report template instead. - type: checkboxes @@ -24,11 +17,7 @@ body: attributes: label: Pre-check options: - - label: I have searched existing issues. - required: true - - label: I confirm this issue is related to a userscript or userscript API compatibility. - required: true - - label: I have provided a userscript link or a minimal reproduction script where possible. + - label: I have searched existing issues, and confirmed this is related to a userscript or userscript API. required: true - type: input @@ -42,30 +31,18 @@ body: - type: input id: target-url attributes: - label: Target Website - placeholder: "Example: " + label: Target Website Where the Issue Occurs validations: required: true - type: textarea id: problem attributes: - label: What Fails in ScriptCat? + label: Behavior and Comparison + description: What happens in ScriptCat, and what happens in other userscript managers? Please name the manager and version you compared against. placeholder: | - Example: Button does not appear / script does not run / GM_xmlhttpRequest fails / menu command does not work / page refreshes infinitely. - validations: - required: true - - - type: textarea - id: comparison - attributes: - label: Comparison with Other Userscript Managers - description: Please describe the behavior in Tampermonkey, Violentmonkey, FireMonkey, Greasemonkey, etc. - placeholder: | - Tampermonkey version: - Violentmonkey version: - FireMonkey / Greasemonkey version: - Behavior in those managers: + In ScriptCat: button never appears / script does not run / GM_xmlhttpRequest throws / page reloads endlessly + In another manager (e.g. Tampermonkey 5.x): works fine validations: required: true @@ -73,7 +50,7 @@ body: id: metadata attributes: label: Userscript Metadata - description: Please include relevant fields such as `@match`, `@grant`, `@connect`, `@require`, `@resource`, `@run-at`, `@inject-into`, etc. + description: Please include at least the relevant `@match`, `@grant`, `@connect`, `@require`, `@resource`, `@run-at`, and `@inject-into` entries. render: javascript placeholder: | // ==UserScript== @@ -85,41 +62,6 @@ body: validations: required: true - - type: textarea - id: minimal-reproduction - attributes: - label: Minimal Reproduction Script - description: If the original userscript is large, please try to reduce it to the smallest script that can reproduce the problem. - render: javascript - placeholder: | - // Please provide the smallest standalone script that reproduces the issue. - validations: - required: false - - - type: dropdown - id: api-area - attributes: - label: Related API / Mechanism - multiple: true - options: - - GM_xmlhttpRequest / GM.xmlHttpRequest - - GM_getValue / GM.setValue / GM storage - - GM_registerMenuCommand - - GM_download - - GM_cookie / Cookie API - - "@match / @include / @exclude" - - "@connect" - - "@require / @resource" - - "@run-at" - - "@inject-into" - - sandbox / unsafeWindow / page context - - iframe / allFrames / noframes - - CSP / Trusted Types - - service worker / offscreen / background - - Not sure - validations: - required: true - - type: input id: scriptcat-version attributes: @@ -131,19 +73,19 @@ body: - type: input id: browser attributes: - label: Operating System and Browser - placeholder: "Example: Windows 11 Chrome 143 / macOS Firefox 151 / Android WebLibre 0.20" + label: OS / Browser and Version + placeholder: "Example: Windows 11 + Chrome 143 / macOS + Firefox 151 / Android + WebLibre 0.20" validations: required: true - type: textarea - id: logs + id: details attributes: - label: Console Logs / Network Requests / Screenshots - description: Please mention the log source, such as page console, content script, service worker, offscreen, network panel, etc. + label: Minimal Reproduction Script / Logs / Screenshots + description: If the original script is large, please reduce it to a minimal reproduction. For logs, note where they came from — page console, content script, Service Worker, offscreen, network panel, etc. placeholder: | - Log source: - Error: + Minimal reproduction script: + Log source and error message: Screenshots / video: validations: required: false diff --git a/.github/ISSUE_TEMPLATE/13_feature_request_en.yaml b/.github/ISSUE_TEMPLATE/13_feature_request_en.yaml index b3fe5bf5f..7393ad3b2 100644 --- a/.github/ISSUE_TEMPLATE/13_feature_request_en.yaml +++ b/.github/ISSUE_TEMPLATE/13_feature_request_en.yaml @@ -10,11 +10,7 @@ body: value: | Please describe the real problem you want to solve, not just a button name or an API name. - If this is a GM/CAT API design request, please provide: - - Proposed API shape - - Compatibility target - - Security impact - - Possible abuse cases + If something is broken, or used to work and no longer does, please use the Bug Report template instead. - type: checkboxes id: precheck @@ -23,38 +19,14 @@ body: options: - label: I have searched existing issues and confirmed this is not a duplicate feature request. required: true - - label: I confirm this is not a bug. If this is unexpected behavior, I should use the Bug Report template. - required: true - - - type: dropdown - id: feature-area - attributes: - label: Feature Area - multiple: true - options: - - Userscript management / list / group / search - - Script installation / update / subscription - - Script matching / exclusion / permission management - - GM API / CAT API - - Background scripts / scheduled scripts - - Cloud Sync / backup / restore / migration - - Popup / Options / UI - - Built-in editor / Monaco / ESLint / VSCode extension - - Logs / debugging / developer experience - - Mobile support - - Internationalization / translation - - Performance / storage / security - - Other - validations: - required: true - type: textarea id: problem attributes: - label: Problem to Solve - description: What pain point do users or userscript authors experience without this feature? + label: The Problem You Want to Solve + description: What is painful for you or for script authors without this feature? placeholder: | - Example: I have many scheduled check-in scripts. Currently I must open logs one by one to confirm whether they ran successfully. + Example: I have many scheduled check-in scripts, and I currently have to open each script's log to find out whether it ran successfully. validations: required: true @@ -63,40 +35,16 @@ body: attributes: label: Proposed Solution placeholder: | - Example: Add "Last Run Time" and "Last Result" columns to the script list. If a scheduled script fails, show the failure count on the extension icon. + Example: Add "last run time" and "last run result" columns to the script list, and show a failure count badge on the extension icon. validations: required: true - type: textarea - id: api-design - attributes: - label: API / UI Draft, if Applicable - description: If this involves GM/CAT APIs, configuration structure, sync format, or UI flow, please provide a draft. - placeholder: | - ```ts - // Proposed API - ``` - validations: - required: false - - - type: textarea - id: alternatives - attributes: - label: Alternatives - description: Is there any workaround? How do other extensions handle this? - placeholder: | - References from Tampermonkey / Violentmonkey / FireMonkey / Soulsign / Automa / others: - validations: - required: false - - - type: textarea - id: compatibility + id: additional-info attributes: - label: Compatibility and Security Impact - description: Does this affect MV2/MV3, Firefox, mobile browsers, incognito mode, site permissions, or userscript permissions? + label: Additional Information + description: Optionally add a proposed API shape, how other userscript managers handle this, screenshots, or reference links. placeholder: | - Possible impact: - New permissions required: - Possible abuse cases: + API draft / reference implementation / screenshots: validations: required: false diff --git a/.github/ISSUE_TEMPLATE/14_technical_proposal_en.yaml b/.github/ISSUE_TEMPLATE/14_technical_proposal_en.yaml index 0c97b3954..e3bbced37 100644 --- a/.github/ISSUE_TEMPLATE/14_technical_proposal_en.yaml +++ b/.github/ISSUE_TEMPLATE/14_technical_proposal_en.yaml @@ -12,28 +12,6 @@ body: For regular user bugs or feature requests, please use the other templates. - - type: dropdown - id: proposal-area - attributes: - label: Affected Module - multiple: true - options: - - Service Worker - - offscreen - - sandbox / inject / content script - - script matching / registry - - GM/CAT API runtime - - storage / Dexie / IndexedDB / OPFS - - sync / backup / import / export - - resource loader / @require / @resource - - editor / Monaco / ESLint - - popup / options UI - - build / packaging / manifest - - tests / e2e / CI - - docs / i18n - validations: - required: true - - type: textarea id: background attributes: @@ -51,25 +29,13 @@ body: required: true - type: textarea - id: compatibility + id: impact attributes: - label: Compatibility / Migration Impact + label: Compatibility / Migration / Testing placeholder: | Impact on existing data: - Impact on MV2/MV3: - Impact on Firefox / Chrome / Edge / mobile: + Impact on MV2/MV3 and Firefox / Chrome / Edge / mobile: Migration required: - validations: - required: true - - - type: textarea - id: test-plan - attributes: - label: Test Plan - placeholder: | - Unit tests: - E2E tests: - Manual browsers to test: - Regression risks: + Test plan and regression risks: validations: required: false diff --git a/.github/ISSUE_TEMPLATE/15_documentation_en.yaml b/.github/ISSUE_TEMPLATE/15_documentation_en.yaml index 0d9124465..f719aa0a6 100644 --- a/.github/ISSUE_TEMPLATE/15_documentation_en.yaml +++ b/.github/ISSUE_TEMPLATE/15_documentation_en.yaml @@ -10,8 +10,6 @@ body: value: | Thanks for helping improve ScriptCat documentation. - Please provide the exact page, section, current problem, and suggested change where possible. - If this is about UI translation, language packs, or localization wording, please use the **Translation / i18n** template instead. - type: checkboxes @@ -21,73 +19,12 @@ body: options: - label: I have searched existing issues and confirmed this is not a duplicate. required: true - - label: I confirm this is a documentation issue, not a ScriptCat product bug. - required: true - type: input id: doc-url attributes: label: Related Documentation Page description: Please provide the related docs.scriptcat.org, learn.scriptcat.org, README, CONTRIBUTING, or other page. - placeholder: "Example: " - validations: - required: true - - - type: dropdown - id: doc-language - attributes: - label: Documentation Language - options: - - Simplified Chinese / zh-CN - - Traditional Chinese / zh-TW - - English / en - - Japanese / ja - - Russian / ru - - German / de - - Vietnamese / vi - - Other - - Not sure - validations: - required: true - - - type: dropdown - id: doc-area - attributes: - label: Documentation Area - multiple: true - options: - - Installation / update / browser support - - Userscript installation / update / subscription - - Userscript development guide - - GM API / CAT API - - Background scripts / scheduled scripts - - Cloud Sync / backup / restore / migration - - Permissions / security / incognito mode - - Popup / Options / UI - - Built-in editor / VSCode extension - - Debugging / logs / developer tools - - Build / contributing guide - - README / Release Notes - - Other - validations: - required: true - - - type: dropdown - id: doc-issue-type - attributes: - label: Documentation Issue Type - multiple: true - options: - - Incorrect content - - Outdated content - - Missing explanation - - Unclear wording - - Code example does not work - - Broken link - - Outdated image / screenshot - - Page structure or navigation issue - - Inconsistent with actual UI or behavior - - Other validations: required: true @@ -108,27 +45,13 @@ body: description: Please describe how the documentation should be changed. You may paste suggested wording directly. placeholder: | Suggested wording: - ... validations: required: true - - type: textarea - id: affected-version - attributes: - label: Related Version / Environment, if Applicable - description: If the documentation issue is related to a specific ScriptCat version, browser, or platform, please provide it. - placeholder: | - ScriptCat version: - Browser / platform: - validations: - required: false - - type: textarea id: additional-info attributes: label: Additional Information description: Screenshots, related issues, PRs, code locations, or references can be added here. - placeholder: | - Screenshots / references / related links: validations: required: false diff --git a/.github/ISSUE_TEMPLATE/16_translation_en.yaml b/.github/ISSUE_TEMPLATE/16_translation_en.yaml index 510b93324..385ad18ba 100644 --- a/.github/ISSUE_TEMPLATE/16_translation_en.yaml +++ b/.github/ISSUE_TEMPLATE/16_translation_en.yaml @@ -10,8 +10,6 @@ body: value: | Thanks for helping improve ScriptCat localization. - Please provide the source text, current translation, suggested translation, and where it appears. - If the documentation content itself is incorrect, outdated, or missing, please use the **Documentation** template instead. - type: checkboxes @@ -21,27 +19,6 @@ body: options: - label: I have searched existing issues and confirmed this is not a duplicate. required: true - - label: I confirm this is a translation / localization issue, not a feature bug. - required: true - - - type: dropdown - id: i18n-area - attributes: - label: Affected Area - multiple: true - options: - - Extension UI / Popup - - Options page - - Installation / update page - - Built-in editor / Monaco / ESLint messages - - Logs / error messages / notifications - - Documentation - - README / CONTRIBUTING / SECURITY - - VSCode extension - - ScriptCat website / script marketplace - - Other - validations: - required: true - type: dropdown id: language @@ -60,28 +37,11 @@ body: validations: required: true - - type: dropdown - id: issue-type - attributes: - label: Translation Issue Type - multiple: true - options: - - Incorrect translation - - Inconsistent terminology - - Unnatural machine translation - - Missing translation / fallback to another language - - Text too long and breaks UI - - Punctuation / capitalization / formatting issue - - New language support - - Other - validations: - required: true - - type: input id: location attributes: label: Location - description: Please provide the page, menu path, screenshot location, file path, or documentation page. + description: Please provide the page, menu path, file path, or documentation page. placeholder: "Example: Options > Settings > Script Sync / src/locales//translation.json" validations: required: true @@ -89,10 +49,8 @@ body: - type: textarea id: source-text attributes: - label: Source Text / Current Text - description: Please paste the current source text or translation you see. - placeholder: | - Current text: + label: Current Text + description: Please paste the source text or current translation you see. validations: required: true @@ -101,35 +59,15 @@ body: attributes: label: Suggested Translation description: Please provide the wording you suggest. For new language requests, mention the target language and whether you are willing to contribute. - placeholder: | - Suggested wording: validations: required: true - type: textarea - id: reason - attributes: - label: Reason - description: Why is the current translation unsuitable? Are there terminology, context, or regional-language considerations? - placeholder: | - Example: In this product, "sync" is consistently translated as "...", so this string should not use "...". - validations: - required: false - - - type: input - id: scriptcat-version - attributes: - label: ScriptCat Version, if Applicable - placeholder: "Example: v1.4.0 / v1.4.0-beta / Firefox MV2 v0.16.15" - validations: - required: false - - - type: textarea - id: screenshots + id: additional-info attributes: - label: Screenshots / Additional Information - description: If this involves UI overflow, truncation, line wrapping, or context, please upload screenshots. + label: Reason / Screenshots + description: Why is the current translation unsuitable? Does it involve terminology, context, or regional wording? If it involves UI overflow or truncation, please attach screenshots. placeholder: | - Screenshots / related links: + Example: In this product, "sync" is consistently translated one way, so this string should not use a different term. validations: required: false diff --git a/.husky/pre-commit b/.husky/pre-commit index ba726b54f..6a1d34393 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -49,6 +49,24 @@ else rm -f "$i18n_files" fi +# issue 模板或构造 issues/new 预填链接的源码有改动时,跑模板机械检查(YAML 合法性、 +# issue-form schema、中英文结构对齐、预填参数 id 是否仍然存在)。同样检查暂存快照而非工作区。 +issue_template_files=$(mktemp) +git diff --cached --name-only -z --diff-filter=ACMRD -- \ + ".github/ISSUE_TEMPLATE/*" "src/*.ts" "src/*.tsx" > "$issue_template_files" + +if [ -s "$issue_template_files" ]; then + rm -f "$issue_template_files" + snapshot_dir=$(mktemp -d) + node scripts/git-staged-snapshot.mjs "$(pwd)" "$snapshot_dir" \ + && node scripts/check-issue-templates.mjs "--root=$snapshot_dir" + status=$? + rm -rf "$snapshot_dir" + [ $status -eq 0 ] || exit 1 +else + rm -f "$issue_template_files" +fi + # 在 main 或 release/* 分支上提交时额外跑测试 branch=$(git rev-parse --abbrev-ref HEAD) if [ "$branch" = "main" ] || echo "$branch" | grep -q "^release/"; then diff --git a/docs/develop.md b/docs/develop.md index f4eb30c2d..691760815 100644 --- a/docs/develop.md +++ b/docs/develop.md @@ -22,13 +22,21 @@ pnpm run typecheck # tsc --noEmit pnpm run test:e2e:install # install Playwright Chromium (first run only) pnpm run test:e2e # Playwright (e2e/*.spec.ts, 1 worker) -pnpm run lint # tsc --noEmit + eslint +pnpm run lint # prettier --check + tsc --noEmit + check:i18n + check:issue-templates, then eslint pnpm run lint-fix # prettier --write + tsc --noEmit + eslint --fix + +pnpm run check:i18n # translation key parity (see docs/translation.md) +pnpm run check:issue-templates # .github/ISSUE_TEMPLATE schema, zh/en parity, issues/new prefill ids ``` No standalone `format` script — formatting is part of `lint-fix` and runs through `prettier --write`. Husky -pre-commit runs `prettier --check` and `pnpm run typecheck` plus ESLint for staged JS/TS files, and also runs -`pnpm run test:ci` when committing on `main` or `release/*`. +pre-commit runs `prettier --check` and `pnpm run typecheck` plus ESLint for staged JS/TS files, runs +`check:i18n` when locale files are staged and `check:issue-templates` when issue templates or `src/` TypeScript +are staged, and also runs `pnpm run test:ci` when committing on `main` or `release/*`. + +`check:issue-templates` guards a contract that is invisible in review: GitHub prefills an issue form from +`issues/new?...` query params keyed by **field id**, so renaming or deleting an id silently breaks every link +using it — including links already shipped in installed builds, which keep sending the old param name. After `pnpm run dev`, load `dist/ext` as an unpacked extension. The browser hot-reloads page changes, but edits to `manifest.json`, `service_worker`, `offscreen`, or `sandbox` require reloading the extension. diff --git a/package.json b/package.json index 16f331519..371daac99 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "build": "cross-env NODE_ENV=production rspack build", "pack": "node ./scripts/pack.js", "typecheck": "tsc --noEmit", - "lint": "concurrently -g \"prettier --check --cache \\\"**/*.{ts,tsx,js,jsx,mjs}\\\"\" \"tsc --noEmit\" \"pnpm run check:i18n\" && eslint . --cache --cache-location .eslintcache", - "lint:ci": "concurrently -g \"prettier --check --cache \\\"**/*.{ts,tsx,js,jsx,mjs}\\\"\" \"tsc --noEmit\" \"pnpm run check:i18n\" && eslint . --cache --cache-location .eslintcache", + "lint": "concurrently -g \"prettier --check --cache \\\"**/*.{ts,tsx,js,jsx,mjs}\\\"\" \"tsc --noEmit\" \"pnpm run check:i18n\" \"pnpm run check:issue-templates\" && eslint . --cache --cache-location .eslintcache", + "lint:ci": "concurrently -g \"prettier --check --cache \\\"**/*.{ts,tsx,js,jsx,mjs}\\\"\" \"tsc --noEmit\" \"pnpm run check:i18n\" \"pnpm run check:issue-templates\" && eslint . --cache --cache-location .eslintcache", "lint-fix": "concurrently -g \"prettier --write --cache \\\"**/*.{ts,tsx,js,jsx,mjs}\\\"\" \"tsc --noEmit\" && eslint --fix . --cache --cache-location .eslintcache", "test": "vitest --no-coverage --reporter=verbose", "test:ci": "vitest run --no-coverage --reporter=default --reporter.summary=false", @@ -26,7 +26,8 @@ "test:e2e:ui": "pnpm exec playwright test --ui", "validate:yaml": "node ./scripts/validate-yaml.mjs", "validate:yaml:all": "node ./scripts/validate-yaml.mjs --all", - "check:i18n": "node ./scripts/check-i18n.mjs" + "check:i18n": "node ./scripts/check-i18n.mjs", + "check:issue-templates": "node ./scripts/check-issue-templates.mjs" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/scripts/check-issue-templates.mjs b/scripts/check-issue-templates.mjs new file mode 100644 index 000000000..94833ad7b --- /dev/null +++ b/scripts/check-issue-templates.mjs @@ -0,0 +1,350 @@ +#!/usr/bin/env node + +// Mechanical check for .github/ISSUE_TEMPLATE/*.yaml. +// +// Fail-closed by design: anything the checker cannot statically resolve is reported as an error +// asking a human to extend the check, never passed through. A silently-broken issue form is +// invisible in review — GitHub simply refuses to render it, or drops a prefilled value, and +// nobody notices until a user hits it. +// +// Checks: +// 1. Every template parses as YAML and satisfies GitHub's issue-form schema: known top-level +// keys, `name`/`description`/`body` present, known element types, unique non-empty ids, +// `markdown` blocks carry neither id nor validations, dropdown/checkboxes have options, +// and checkboxes use per-option `required` rather than `validations.required`. +// 2. zh/en parity — templates pair by numeric prefix (0N ↔ 1N). The pair must expose the same +// element sequence (type, id, required, render, multiple) and the same title/type/labels, +// so a field added to one language cannot silently go missing in the other. +// 3. The `issues/new?...` prefill contract. GitHub prefills an issue form from query params +// keyed by *field id*, so renaming or dropping an id silently breaks every link that used +// it — including links baked into already-installed extension builds, which keep sending +// the old param forever. Every such URL built in src/ is parsed from the TypeScript AST +// (not string-matched) and each of its params must resolve to a real id in every template +// the URL can select. +// +// Run with `--root=` to check a tree other than this file's repo checkout. + +import process from "node:process"; +import { readdirSync, readFileSync, existsSync, statSync, realpathSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import YAML from "yaml"; +import ts from "typescript"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const TEMPLATE_DIR = ".github/ISSUE_TEMPLATE"; +const TOP_LEVEL_KEYS = new Set(["name", "description", "title", "labels", "assignees", "body", "type", "projects"]); +const ELEMENT_TYPES = new Set(["markdown", "textarea", "input", "dropdown", "checkboxes"]); + +function walkFiles(dir, out = []) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === "dist") continue; + walkFiles(full, out); + } else if (/\.(ts|tsx)$/.test(entry.name)) { + out.push(full); + } + } + return out; +} + +// Collapse a `a + b + c` chain of string/template literals into one static skeleton, replacing +// each `${...}` with "\0" so the result stays parseable as a URL shape while marking the spots +// whose value is only known at runtime. +function staticText(node) { + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text; + if (ts.isTemplateExpression(node)) { + return node.head.text + node.templateSpans.map((span) => "\0" + span.literal.text).join(""); + } + if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) { + const left = staticText(node.left); + const right = staticText(node.right); + return left === null || right === null ? null : left + right; + } + if (ts.isParenthesizedExpression(node)) return staticText(node.expression); + return null; +} + +// ts.forEachChild aborts as soon as its callback returns a truthy value, so the visitor must +// return undefined — returning the accumulator here silently stops after the first child. +function collectStringLiterals(node, out = []) { + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) out.push(node.text); + node.forEachChild((child) => { + collectStringLiterals(child, out); + }); + return out; +} + +// The whole expression a URL literal belongs to, so `"...?" + \`template=${a ? "x" : "y"}\`` is +// reconstructed as one unit rather than three unrelated fragments. +function outermostConcat(node) { + let current = node; + while ( + current.parent && + ((ts.isBinaryExpression(current.parent) && current.parent.operatorToken.kind === ts.SyntaxKind.PlusToken) || + ts.isParenthesizedExpression(current.parent)) + ) { + current = current.parent; + } + return current; +} + +function readTemplates(root, problems) { + const dir = path.join(root, TEMPLATE_DIR); + if (!existsSync(dir) || !statSync(dir).isDirectory()) { + problems.push(`${TEMPLATE_DIR}/ not found — issue templates must live there.`); + return {}; + } + + const templates = {}; + for (const file of readdirSync(dir) + .filter((f) => /\.ya?ml$/.test(f) && f !== "config.yml") + .sort()) { + const source = readFileSync(path.join(dir, file), "utf8"); + const doc = YAML.parseDocument(source, { prettyErrors: true, strict: true }); + if (doc.errors.length > 0) { + for (const error of doc.errors) problems.push(`${TEMPLATE_DIR}/${file}: invalid YAML — ${error.message}`); + continue; + } + templates[file] = doc.toJS(); + } + return templates; +} + +function checkSchema(file, doc, problems) { + const where = `${TEMPLATE_DIR}/${file}`; + + if (doc === null || typeof doc !== "object" || Array.isArray(doc)) { + problems.push(`${where}: top level must be a mapping.`); + return; + } + for (const key of Object.keys(doc)) { + if (!TOP_LEVEL_KEYS.has(key)) problems.push(`${where}: unknown top-level key "${key}".`); + } + for (const key of ["name", "description", "body"]) { + if (!doc[key]) problems.push(`${where}: missing required top-level "${key}".`); + } + if (!Array.isArray(doc.body)) { + if (doc.body) problems.push(`${where}: "body" must be a list.`); + return; + } + + const seen = new Set(); + doc.body.forEach((element, index) => { + const at = `${where} body[${index}]`; + if (element === null || typeof element !== "object") { + problems.push(`${at}: element must be a mapping.`); + return; + } + const attributes = element.attributes ?? {}; + + if (!ELEMENT_TYPES.has(element.type)) { + problems.push(`${at}: unknown element type "${element.type}".`); + return; + } + + if (element.type === "markdown") { + if (element.id) problems.push(`${at}: markdown blocks must not declare an id.`); + if (element.validations) problems.push(`${at}: markdown blocks must not declare validations.`); + if (!attributes.value) problems.push(`${at}: markdown block missing attributes.value.`); + return; + } + + if (!element.id) { + problems.push(`${at}: missing id — prefill links address fields by id.`); + } else if (seen.has(element.id)) { + problems.push(`${at}: duplicate id "${element.id}".`); + } + seen.add(element.id); + + if (!attributes.label) problems.push(`${at}: missing attributes.label.`); + + if (element.type === "dropdown" || element.type === "checkboxes") { + if (!Array.isArray(attributes.options) || attributes.options.length === 0) { + problems.push(`${at}: ${element.type} must declare a non-empty options list.`); + } + } + if (element.type === "checkboxes" && element.validations) { + problems.push(`${at}: checkboxes mark required per option, not via validations.`); + } + }); +} + +function elementShape(doc) { + return (doc.body ?? []) + .filter((element) => element && typeof element === "object") + .map((element) => { + const attributes = element.attributes ?? {}; + return [ + element.type, + element.id ?? "-", + element.validations?.required ? "required" : "optional", + attributes.render ? `render=${attributes.render}` : "", + attributes.multiple ? "multiple" : "", + ] + .filter(Boolean) + .join(":"); + }); +} + +// zh templates are 0N_*, their en mirrors 1N_*_en. +function checkLanguageParity(templates, problems) { + for (const file of Object.keys(templates)) { + const match = /^0(\d)_(.+)\.yaml$/.exec(file); + if (!match) continue; + const [, digit, slug] = match; + const mirror = `1${digit}_${slug}_en.yaml`; + + if (!templates[mirror]) { + problems.push(`${TEMPLATE_DIR}/${file}: missing English mirror ${mirror}.`); + continue; + } + + const zh = templates[file]; + const en = templates[mirror]; + + const zhShape = elementShape(zh); + const enShape = elementShape(en); + if (JSON.stringify(zhShape) !== JSON.stringify(enShape)) { + problems.push( + `${file} / ${mirror}: element structure differs — the two languages must expose the same fields.\n` + + ` zh: ${JSON.stringify(zhShape)}\n` + + ` en: ${JSON.stringify(enShape)}` + ); + } + for (const key of ["title", "type"]) { + if (zh[key] !== en[key]) problems.push(`${file} / ${mirror}: top-level "${key}" differs.`); + } + if (JSON.stringify(zh.labels) !== JSON.stringify(en.labels)) { + problems.push(`${file} / ${mirror}: "labels" differ.`); + } + + for (const type of ["dropdown", "checkboxes"]) { + const zhElements = (zh.body ?? []).filter((element) => element?.type === type); + const enElements = (en.body ?? []).filter((element) => element?.type === type); + zhElements.forEach((element, index) => { + const zhCount = element.attributes?.options?.length ?? 0; + const enCount = enElements[index]?.attributes?.options?.length ?? 0; + if (zhCount !== enCount) { + problems.push( + `${file} / ${mirror}: ${type} "${element.id}" has ${zhCount} options in zh but ${enCount} in en.` + ); + } + }); + } + } +} + +function checkPrefillContract(root, templates, problems) { + const srcDir = path.join(root, "src"); + if (!existsSync(srcDir)) return; + + const ids = Object.fromEntries( + Object.entries(templates).map(([file, doc]) => [ + file, + new Set((doc.body ?? []).map((element) => element?.id).filter(Boolean)), + ]) + ); + + for (const file of walkFiles(srcDir)) { + const source = readFileSync(file, "utf8"); + if (!source.includes("issues/new")) continue; + + const relative = path.relative(root, file); + const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true); + const expressions = new Set(); + + const visit = (node) => { + if ( + (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node)) && + (staticText(node) ?? "").includes("issues/new") + ) { + expressions.add(outermostConcat(node)); + } + node.forEachChild(visit); + }; + visit(sourceFile); + + for (const expression of expressions) { + const url = staticText(expression); + if (url === null) { + problems.push( + `${relative}: found an issues/new URL this check cannot statically resolve. ` + + `Extend scripts/check-issue-templates.mjs rather than leaving the prefill contract unverified.` + ); + continue; + } + + const params = [...url.matchAll(/[?&\0]([a-zA-Z][\w-]*)=/g)].map((m) => m[1]).filter((p) => p !== "template"); + if (params.length === 0) continue; + + // `template=` is either spelled out inline (`template=01_bug_report.yaml&...`, so the name + // sits in the surrounding literal text) or chosen at runtime between locale variants + // (`template=${cond ? "01_bug_report" : "11_bug_report_en"}.yaml`, so the candidates are + // string literals in the expression). Collect both, and treat every candidate as reachable. + const targets = new Set(); + const addCandidate = (value) => { + const name = /^(\d\d_\w+?)(\.yaml)?$/.exec(value)?.[1]; + if (name && ids[`${name}.yaml`]) targets.add(`${name}.yaml`); + }; + addCandidate(/[?&\0]template=([^&\0]+)/.exec(url)?.[1] ?? ""); + for (const literal of collectStringLiterals(expression)) addCandidate(literal); + + if (targets.size === 0) { + problems.push( + `${relative}: issues/new URL prefills ${params.map((p) => `"${p}"`).join(", ")} ` + + `but no known template file could be resolved from it — the prefill target is unverifiable.` + ); + continue; + } + + for (const target of targets) { + for (const param of params) { + if (!ids[target].has(param)) { + problems.push( + `${relative}: prefills "${param}=", but ${TEMPLATE_DIR}/${target} has no field with that id. ` + + `GitHub silently drops unknown prefill params, and installed builds keep sending the old name.` + ); + } + } + } + } + } +} + +function runCheck(root) { + const problems = []; + const templates = readTemplates(root, problems); + + for (const [file, doc] of Object.entries(templates)) checkSchema(file, doc, problems); + checkLanguageParity(templates, problems); + checkPrefillContract(root, templates, problems); + + return { problems, templateCount: Object.keys(templates).length }; +} + +function main() { + const rootArg = process.argv.find((arg) => arg.startsWith("--root=")); + const root = rootArg ? path.resolve(rootArg.slice("--root=".length)) : path.resolve(__dirname, ".."); + + const { problems, templateCount } = runCheck(root); + + if (problems.length === 0) { + console.log(`✅ issue template check passed: ${templateCount} templates valid, mirrored, prefill ids intact.`); + process.exit(0); + } + + for (const problem of problems) console.error(`\n❌ ${problem}`); + console.error("\nIssue template check failed. Fix the problems above before submitting."); + process.exit(1); +} + +// Same argv/import.meta.url normalization rationale as scripts/check-i18n.mjs: compare real paths +// on both sides, or the check silently no-ops under symlinked or non-ASCII repo paths. +if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) { + main(); +} + +export { runCheck }; diff --git a/scripts/check-issue-templates.test.mjs b/scripts/check-issue-templates.test.mjs new file mode 100644 index 000000000..b2b1f5145 --- /dev/null +++ b/scripts/check-issue-templates.test.mjs @@ -0,0 +1,226 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { mkdirSync, writeFileSync, rmSync } from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import { fileURLToPath } from "node:url"; +import { runCheck } from "./check-issue-templates.mjs"; + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +// 用最小 fixture 仓库树(而非真实仓库)锁定两类真实回归: +// - 英文模板里未加引号的 `: ` 让 YAML 解析失败,中文模板因为用全角「:」而侥幸通过, +// 单看 diff 完全看不出来,GitHub 则直接拒绝渲染该表单; +// - 字段 id 被改名后,popup「报告问题」链接的 &browser= 预填静默失效, +// 且已安装的旧版本会一直沿用旧参数名。 + +const tmpDirs = []; + +const TEMPLATE_DIR = ".github/ISSUE_TEMPLATE"; + +function template({ name, title = "[BUG] ", labels = ["bug"], fields }) { + const body = fields + .map((field) => { + if (field.type === "markdown") { + return ` - type: markdown\n attributes:\n value: |\n ${field.value ?? "hello"}\n`; + } + const lines = [` - type: ${field.type}`]; + if (field.id !== undefined) lines.push(` id: ${field.id}`); + lines.push(` attributes:`); + lines.push(` label: ${field.label ?? field.id}`); + if (field.options) { + lines.push(` options:`); + for (const option of field.options) { + lines.push(typeof option === "string" ? ` - ${option}` : ` - label: ${option.label}`); + } + } + if (field.validations !== false) lines.push(` validations:\n required: ${field.required ?? true}`); + return lines.join("\n") + "\n"; + }) + .join("\n"); + return `name: ${name}\ndescription: ${name} description\ntitle: "${title}"\nlabels: ${JSON.stringify(labels)}\n\nbody:\n${body}`; +} + +const DEFAULT_FIELDS = [ + { type: "markdown" }, + { type: "textarea", id: "summary" }, + { type: "input", id: "scriptcat-version" }, + { type: "input", id: "browser" }, +]; + +function makeFixtureRoot({ zh, en, source } = {}) { + const dir = path.join(os.tmpdir(), `check-issue-templates-${Math.random().toString(36).slice(2)}`); + tmpDirs.push(dir); + mkdirSync(path.join(dir, TEMPLATE_DIR), { recursive: true }); + + writeFileSync( + path.join(dir, TEMPLATE_DIR, "01_bug_report.yaml"), + zh ?? template({ name: "Bug 反馈", fields: DEFAULT_FIELDS }) + ); + writeFileSync( + path.join(dir, TEMPLATE_DIR, "11_bug_report_en.yaml"), + en ?? template({ name: "Bug Report", fields: DEFAULT_FIELDS }) + ); + + if (source !== undefined) { + mkdirSync(path.join(dir, "src/pages/popup"), { recursive: true }); + writeFileSync(path.join(dir, "src/pages/popup/App.tsx"), source); + } + return dir; +} + +function problemsOf(root) { + return runCheck(root).problems; +} + +afterEach(() => { + while (tmpDirs.length) rmSync(tmpDirs.pop(), { recursive: true, force: true }); +}); + +describe("issue 模板机械检查", () => { + it("结构合法且中英文对齐的模板集合应通过", () => { + expect(problemsOf(makeFixtureRoot())).toEqual([]); + }); + + it("仓库现有的 issue 模板应全部通过检查", () => { + expect(problemsOf(REPO_ROOT)).toEqual([]); + }); + + describe("YAML 与 issue-form schema", () => { + it("未加引号导致的 YAML 解析失败应报错,而不是被跳过", () => { + // description 里裸露的 `: ` 会让 YAML 认为这里又开了一个映射键。 + const broken = + `name: Bug Report\ndescription: d\ntitle: "[BUG] "\nlabels: ["bug"]\n\nbody:\n` + + ` - type: input\n id: summary\n attributes:\n label: L\n` + + ` description: For logs, note the source: page console, service worker\n`; + const problems = problemsOf(makeFixtureRoot({ en: broken })); + expect(problems.join("\n")).toMatch(/invalid YAML/); + }); + + it("markdown 块声明 id 应报错", () => { + const zh = template({ + name: "Bug 反馈", + fields: [{ type: "markdown" }, { type: "textarea", id: "summary" }], + }).replace(" - type: markdown\n", " - type: markdown\n id: intro\n"); + expect(problemsOf(makeFixtureRoot({ zh })).join("\n")).toMatch(/markdown blocks must not declare an id/); + }); + + it("重复的字段 id 应报错", () => { + const fields = [ + { type: "textarea", id: "summary" }, + { type: "input", id: "summary" }, + ]; + const zh = template({ name: "Bug 反馈", fields }); + const en = template({ name: "Bug Report", fields }); + expect(problemsOf(makeFixtureRoot({ zh, en })).join("\n")).toMatch(/duplicate id "summary"/); + }); + + it("非 markdown 字段缺少 id 应报错", () => { + const fields = [{ type: "textarea", label: "无 id" }]; + const zh = template({ name: "Bug 反馈", fields }); + const en = template({ name: "Bug Report", fields }); + expect(problemsOf(makeFixtureRoot({ zh, en })).join("\n")).toMatch(/missing id/); + }); + + it("dropdown 没有 options 应报错", () => { + const fields = [{ type: "dropdown", id: "area" }]; + const zh = template({ name: "Bug 反馈", fields }); + const en = template({ name: "Bug Report", fields }); + expect(problemsOf(makeFixtureRoot({ zh, en })).join("\n")).toMatch(/must declare a non-empty options list/); + }); + + it("checkboxes 用 validations 而非逐项 required 应报错", () => { + const fields = [{ type: "checkboxes", id: "precheck", options: [{ label: "已搜索" }] }]; + const zh = template({ name: "Bug 反馈", fields }); + const en = template({ name: "Bug Report", fields }); + expect(problemsOf(makeFixtureRoot({ zh, en })).join("\n")).toMatch(/mark required per option/); + }); + + it("未知的顶层键应报错", () => { + const zh = template({ name: "Bug 反馈", fields: DEFAULT_FIELDS }) + "\nunknown_key: x\n"; + expect(problemsOf(makeFixtureRoot({ zh })).join("\n")).toMatch(/unknown top-level key "unknown_key"/); + }); + }); + + describe("中英文模板对齐", () => { + it("英文镜像缺少某个字段时应报错", () => { + const en = template({ + name: "Bug Report", + fields: DEFAULT_FIELDS.filter((field) => field.id !== "browser"), + }); + expect(problemsOf(makeFixtureRoot({ en })).join("\n")).toMatch(/element structure differs/); + }); + + it("同一字段中英文必填性不一致时应报错", () => { + const en = template({ + name: "Bug Report", + fields: DEFAULT_FIELDS.map((field) => (field.id === "browser" ? { ...field, required: false } : field)), + }); + expect(problemsOf(makeFixtureRoot({ en })).join("\n")).toMatch(/element structure differs/); + }); + + it("labels 不一致时应报错", () => { + const en = template({ name: "Bug Report", labels: ["bug", "extra"], fields: DEFAULT_FIELDS }); + expect(problemsOf(makeFixtureRoot({ en })).join("\n")).toMatch(/"labels" differ/); + }); + + it("缺少英文镜像文件应报错", () => { + const root = makeFixtureRoot(); + rmSync(path.join(root, TEMPLATE_DIR, "11_bug_report_en.yaml")); + expect(problemsOf(root).join("\n")).toMatch(/missing English mirror/); + }); + }); + + describe("issues/new 预填契约", () => { + const linkSource = (params) => ` + export function App() { + const onClick = () => { + const issueUrl = + \`https://github.com/scriptscat/scriptcat/issues/new?\` + + \`template=\${isChineseUser() ? "01_bug_report" : "11_bug_report_en"}.yaml&${params}\`; + window.open(issueUrl, "_blank"); + }; + return onClick; + } + `; + + it("预填参数都能对应到字段 id 时应通过", () => { + const source = linkSource("scriptcat-version=${ExtVersion}&browser=${encodeURIComponent(ua)}"); + expect(problemsOf(makeFixtureRoot({ source }))).toEqual([]); + }); + + it("字段 id 被改名导致预填参数失效时应报错", () => { + // browser -> environment:模板本身完全合法,只有链接会静默失效。 + const renamed = DEFAULT_FIELDS.map((field) => (field.id === "browser" ? { ...field, id: "environment" } : field)); + const zh = template({ name: "Bug 反馈", fields: renamed }); + const en = template({ name: "Bug Report", fields: renamed }); + const source = linkSource("scriptcat-version=${ExtVersion}&browser=${encodeURIComponent(ua)}"); + const problems = problemsOf(makeFixtureRoot({ zh, en, source })); + expect(problems.join("\n")).toMatch(/prefills "browser="/); + expect(problems.join("\n")).toMatch(/01_bug_report\.yaml has no field with that id/); + expect(problems.join("\n")).toMatch(/11_bug_report_en\.yaml has no field with that id/); + }); + + it("跨越模板字面量拼接的第二段参数同样要被检查", () => { + // browser= 出现在新的一段模板字面量开头,而不是紧跟 ? 或 &。 + const source = ` + const issueUrl = + \`https://github.com/scriptscat/scriptcat/issues/new?\` + + \`template=01_bug_report.yaml&scriptcat-version=\${ExtVersion}&\` + + \`nonexistent-field=\${x}\`; + `; + expect(problemsOf(makeFixtureRoot({ source })).join("\n")).toMatch(/prefills "nonexistent-field="/); + }); + + it("无法静态解析出模板名时应报错而不是放行", () => { + const source = ` + const issueUrl = \`https://github.com/scriptscat/scriptcat/issues/new?template=\${pickTemplate()}&browser=\${ua}\`; + \`;`; + expect(problemsOf(makeFixtureRoot({ source })).join("\n")).toMatch(/prefill target is unverifiable/); + }); + + it("不带预填参数的 issues/new 链接不应报错", () => { + const source = `const url = "https://github.com/scriptscat/scriptcat/issues/new";`; + expect(problemsOf(makeFixtureRoot({ source }))).toEqual([]); + }); + }); +});