From 2d13c90fbde6c2f8347e8952a04ffd9e42a8eb71 Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:39:35 +0800 Subject: [PATCH 01/14] fix: use canonical bug label in issue form --- .github/ISSUE_TEMPLATE/bug.yml | 122 +++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..9ef4a6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,122 @@ +name: "Bug 报告" +description: "报告可以稳定复现的错误或非预期行为" +title: "[Bug]: " +labels: + - "type: bug" +assignees: [] + +body: + - type: markdown + attributes: + value: | + 感谢你帮助 MathArts 改进项目。 + + 提交前请先搜索已有 Issues,确认问题尚未被报告。 + + **安全漏洞不得在此公开提交。** 请按照 MathArts 的安全策略进行私密报告: + https://github.com/matharts/.github/blob/main/SECURITY.md + + - type: textarea + id: summary + attributes: + label: "问题摘要" + description: "请简要说明发生了什么,以及它为什么是一个问题。" + placeholder: "例如:在特定日期输入下,计算结果与项目文档定义不一致。" + validations: + required: true + + - type: input + id: version + attributes: + label: "受影响版本" + description: "请填写发布版本、软件包版本或 Commit SHA。" + placeholder: "例如:v0.4.1、1.2.0 或 abc1234" + validations: + required: true + + - type: textarea + id: environment + attributes: + label: "运行环境" + description: "请填写与复现有关的操作系统、运行时、浏览器、架构或依赖版本。" + placeholder: | + 操作系统: + 运行时及版本: + 浏览器及版本: + 其他相关依赖: + validations: + required: false + + - type: textarea + id: reproduction + attributes: + label: "复现步骤" + description: "请提供最小、完整且可以重复执行的步骤。" + placeholder: | + 1. 安装…… + 2. 使用输入…… + 3. 调用…… + 4. 观察到…… + validations: + required: true + + - type: textarea + id: minimal_example + attributes: + label: "最小复现示例" + description: "可以粘贴最小代码、配置或输入数据。请删除密钥和个人数据。" + render: "text" + validations: + required: false + + - type: textarea + id: expected + attributes: + label: "期望行为" + description: "请说明你期望得到的结果。" + validations: + required: true + + - type: textarea + id: actual + attributes: + label: "实际行为" + description: "请说明实际结果,包括完整错误信息或可观察到的差异。" + validations: + required: true + + - type: textarea + id: domain_context + attributes: + label: "规则、术语或资料依据" + description: "仅当问题涉及历法、术语、流派、映射关系或数术计算结果时填写。" + placeholder: | + 输入: + 当前结果: + 预期结果: + 采用的规则或流派: + 资料来源: + validations: + required: false + + - type: textarea + id: logs + attributes: + label: "日志或补充材料" + description: "可提供日志、截图、堆栈信息或相关链接。请删除敏感信息。" + validations: + required: false + + - type: checkboxes + id: confirmations + attributes: + label: "提交确认" + options: + - label: "我已搜索已有 Issues,并确认这不是重复报告。" + required: true + - label: "这不是需要私密报告的安全漏洞。" + required: true + - label: "我已删除密钥、凭证、个人数据和其他敏感信息。" + required: true + - label: "我愿意遵守 MathArts 社区行为准则。" + required: true From b364e52be7a5cc23c245ea17ee64e89391799fef Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:39:46 +0800 Subject: [PATCH 02/14] fix: use canonical feature label in issue form --- .github/ISSUE_TEMPLATE/feature.yml | 123 +++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature.yml diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..9ebf10d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,123 @@ +name: "功能或改进建议" +description: "提出新能力、工作流改进或用户可见行为变更" +title: "[Feature]: " +labels: + - "type: feature" +assignees: [] + +body: + - type: markdown + attributes: + value: | + 感谢你提出建议。 + + 请优先描述需要解决的问题、目标用户和使用场景。 + + 尚未形成明确需求的方向讨论、理论讨论或跨仓库想法,更适合先提交到 MathArts Discussions: + https://github.com/orgs/matharts/discussions + + - type: textarea + id: problem + attributes: + label: "需要解决的问题" + description: "当前有什么限制、困难或未满足的需求?请不要只描述希望增加的实现。" + placeholder: "例如:使用者无法在不修改内部代码的情况下选择不同的规则配置。" + validations: + required: true + + - type: textarea + id: users_and_scenario + attributes: + label: "目标用户和使用场景" + description: "谁会使用这项能力?会在什么情况下使用?" + placeholder: | + 目标用户: + 使用场景: + 当前工作方式: + validations: + required: true + + - type: textarea + id: desired_outcome + attributes: + label: "期望结果" + description: "请描述完成后的用户体验、行为或可验证结果。" + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: "建议方案" + description: "可以说明可能的接口、工作流或实现方向。维护者可能选择不同方案。" + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: "替代方案" + description: "请说明尝试过的变通方式,或考虑过但未采用的方案。" + validations: + required: false + + - type: dropdown + id: impact + attributes: + label: "可能影响" + description: "请选择最接近的影响范围。" + options: + - "不确定" + - "仅内部实现" + - "公共 API 或类型" + - "配置或数据格式" + - "用户可见行为或计算结果" + - "兼容性或 Breaking Change" + - "跨仓库或组织级流程" + validations: + required: true + + - type: textarea + id: domain_basis + attributes: + label: "术语、规则或资料依据" + description: "涉及数术术语、历法、流派、映射关系或计算规则时填写。" + placeholder: | + 术语或规则: + 适用范围: + 资料来源: + 已知差异或争议: + validations: + required: false + + - type: dropdown + id: contribution + attributes: + label: "贡献意愿" + description: "这不会影响建议是否被接受。" + options: + - "我可以提交 Pull Request" + - "我可以协助设计、测试或验证" + - "我目前只能提供需求和反馈" + validations: + required: true + + - type: textarea + id: additional_context + attributes: + label: "补充信息" + description: "可提供相关 Issue、Discussion、RFC、截图、示例或参考资料。" + validations: + required: false + + - type: checkboxes + id: confirmations + attributes: + label: "提交确认" + options: + - label: "我已搜索已有 Issues 和 Discussions,并确认这不是重复建议。" + required: true + - label: "我已区分事实、当前实现、个人建议和待验证内容。" + required: true + - label: "我愿意遵守 MathArts 社区行为准则。" + required: true From f4ea0873df94e993d8e19568526ac2819d1ea119 Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:40:02 +0800 Subject: [PATCH 03/14] docs: clarify managed label sync scope --- .github/actions/sync-labels/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/sync-labels/action.yml b/.github/actions/sync-labels/action.yml index 1375e23..33de43f 100644 --- a/.github/actions/sync-labels/action.yml +++ b/.github/actions/sync-labels/action.yml @@ -1,5 +1,5 @@ name: "MathArts Sync Labels" -description: "以 .github/labels.yml 为权威来源,严格同步组织内仓库标签" +description: "同步组织级受管标签,同时保留仓库自行维护的扩展标签" inputs: token: From 6697f700c4b3bbc83464e5893385c3b81e213dd1 Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:40:38 +0800 Subject: [PATCH 04/14] fix: preserve repository-specific labels during sync --- .github/actions/sync-labels/sync-labels.rb | 607 +++++++-------------- 1 file changed, 203 insertions(+), 404 deletions(-) diff --git a/.github/actions/sync-labels/sync-labels.rb b/.github/actions/sync-labels/sync-labels.rb index 58aabe0..9a07e9f 100644 --- a/.github/actions/sync-labels/sync-labels.rb +++ b/.github/actions/sync-labels/sync-labels.rb @@ -7,27 +7,28 @@ require "uri" require "yaml" -TOKEN = ENV.fetch("SYNC_LABELS_TOKEN") -OWNER = ENV.fetch("SYNC_LABELS_OWNER") - -CONFIG_FILE = ENV.fetch( - "SYNC_LABELS_CONFIG_FILE", - ".github/labels.yml" -) - -ONLY_REPOSITORY = ENV.fetch( - "SYNC_LABELS_REPOSITORY", - "" -).strip - -API_URL = ENV.fetch( - "SYNC_LABELS_API_URL", - "https://api.github.com" -).sub(%r{/\z}, "") - -DRY_RUN = %w[1 true yes on].include?( - ENV.fetch("SYNC_LABELS_DRY_RUN", "true").downcase -) +TOKEN = ENV.fetch("SYNC_LABELS_TOKEN", "") +OWNER = ENV.fetch("SYNC_LABELS_OWNER", "") +CONFIG_FILE = ENV.fetch("SYNC_LABELS_CONFIG_FILE", ".github/labels.yml") +ONLY_REPOSITORY = ENV.fetch("SYNC_LABELS_REPOSITORY", "").strip +API_URL = ENV.fetch("SYNC_LABELS_API_URL", "https://api.github.com").sub(%r{/\z}, "") +DRY_RUN = %w[1 true yes on].include?(ENV.fetch("SYNC_LABELS_DRY_RUN", "true").downcase) + +MANAGED_PREFIXES = %w[ + type: + status: + priority: + impact: + process: + resolution: +].freeze + +MANAGED_EXACT_NAMES = Set.new( + [ + "good first issue", + "help wanted" + ] +).freeze class GitHubApi def initialize(token:, base_url:) @@ -57,17 +58,10 @@ def paginate(path) loop do separator = path.include?("?") ? "&" : "?" - - batch = get( - "#{path}#{separator}per_page=100&page=#{page}" - ) - - unless batch.is_a?(Array) - raise "GitHub API 分页结果不是数组。" - end + batch = get("#{path}#{separator}per_page=100&page=#{page}") + raise "GitHub API 分页结果不是数组。" unless batch.is_a?(Array) results.concat(batch) - break if batch.length < 100 page += 1 @@ -81,7 +75,6 @@ def paginate(path) def request(request_class, path, body = nil) uri = URI.parse("#{@base_url}#{path}") request = request_class.new(uri) - request["Accept"] = "application/vnd.github+json" request["Authorization"] = "Bearer #{@token}" request["User-Agent"] = "matharts-sync-labels" @@ -103,19 +96,12 @@ def request(request_class, path, body = nil) end unless response.code.to_i.between?(200, 299) - message = - begin - parsed = JSON.parse(response.body.to_s) - parsed["message"] || response.body.to_s - rescue JSON::ParserError - response.body.to_s - end - - accepted_permissions = - response["x-accepted-github-permissions"].to_s - - oauth_scopes = - response["x-oauth-scopes"].to_s + message = begin + parsed = JSON.parse(response.body.to_s) + parsed["message"] || response.body.to_s + rescue JSON::ParserError + response.body.to_s + end details = [ "GitHub API request failed", @@ -125,13 +111,10 @@ def request(request_class, path, body = nil) "Message: #{message}" ] - unless accepted_permissions.empty? - details << "Accepted permissions: #{accepted_permissions}" - end - - unless oauth_scopes.empty? - details << "Token scopes: #{oauth_scopes}" - end + accepted_permissions = response["x-accepted-github-permissions"].to_s + oauth_scopes = response["x-oauth-scopes"].to_s + details << "Accepted permissions: #{accepted_permissions}" unless accepted_permissions.empty? + details << "Token scopes: #{oauth_scopes}" unless oauth_scopes.empty? raise details.join("\n") end @@ -143,26 +126,18 @@ def request(request_class, path, body = nil) end def label_key(value) - value - .to_s - .unicode_normalize(:nfc) - .downcase + value.to_s.unicode_normalize(:nfc).downcase rescue StandardError value.to_s.downcase end def escape_segment(value) - URI - .encode_www_form_component(value.to_s) - .gsub("+", "%20") + URI.encode_www_form_component(value.to_s).gsub("+", "%20") end def repository_path(full_name) owner, repository = full_name.split("/", 2) - - if owner.to_s.empty? || repository.to_s.empty? - raise "无效仓库名称:#{full_name.inspect}" - end + raise "无效仓库名称:#{full_name.inspect}" if owner.to_s.empty? || repository.to_s.empty? "#{escape_segment(owner)}/#{escape_segment(repository)}" end @@ -175,79 +150,29 @@ def load_labels(path) aliases: false ) - unless parsed.is_a?(Array) - raise "#{path} 的 YAML 根节点必须是数组。" - end + raise "#{path} 的 YAML 根节点必须是数组。" unless parsed.is_a?(Array) + raise "#{path} 不能为空。" if parsed.empty? - if parsed.empty? - raise( - "#{path} 不能为空;严格同步会删除仓库中的全部标签。" - ) - end - - allowed_keys = Set.new( - %w[ - name - color - description - aliases - ] - ) + allowed_keys = Set.new(%w[name color description aliases]) labels = parsed.map.with_index do |entry, index| - unless entry.is_a?(Hash) - raise "#{path} 第 #{index + 1} 项必须是对象。" - end - - unknown_keys = - entry.keys.map(&:to_s).to_set - allowed_keys + raise "#{path} 第 #{index + 1} 项必须是对象。" unless entry.is_a?(Hash) + unknown_keys = entry.keys.map(&:to_s).to_set - allowed_keys unless unknown_keys.empty? - raise( - "#{path} 第 #{index + 1} 项包含未知字段:" \ - "#{unknown_keys.to_a.sort.join(', ')}" - ) + raise "#{path} 第 #{index + 1} 项包含未知字段:#{unknown_keys.to_a.sort.join(', ')}" end name = entry["name"].to_s.strip + color = entry["color"].to_s.delete_prefix("#").upcase + description = entry.fetch("description", "").to_s.strip + aliases = Array(entry["aliases"]).map { |value| value.to_s.strip } - color = entry["color"] - .to_s - .delete_prefix("#") - .upcase - - description = entry - .fetch("description", "") - .to_s - .strip - - aliases = Array(entry["aliases"]).map do |value| - value.to_s.strip - end - - if name.empty? - raise "#{path} 第 #{index + 1} 项缺少 name。" - end - - if name.length > 50 - raise "标签名称超过 50 个字符:#{name}" - end - - unless color.match?(/\A[0-9A-F]{6}\z/) - raise( - "#{name} 的 color 必须是六位十六进制值。" - ) - end - - if description.length > 100 - raise( - "#{name} 的 description 超过 100 个字符。" - ) - end - - if aliases.any?(&:empty?) - raise "#{name} 的 aliases 不能包含空值。" - end + raise "#{path} 第 #{index + 1} 项缺少 name。" if name.empty? + raise "标签名称超过 50 个字符:#{name}" if name.length > 50 + raise "#{name} 的 color 必须是六位十六进制值。" unless color.match?(/\A[0-9A-F]{6}\z/) + raise "#{name} 的 description 超过 100 个字符。" if description.length > 100 + raise "#{name} 的 aliases 不能包含空值。" if aliases.any?(&:empty?) { "name" => name, @@ -257,42 +182,23 @@ def load_labels(path) } end - desired_names = labels.map do |label| - label_key(label["name"]) - end - - unless desired_names.uniq.length == desired_names.length - raise "#{path} 包含重复标签名称。" - end + desired_names = labels.map { |label| label_key(label["name"]) } + raise "#{path} 包含重复标签名称。" unless desired_names.uniq.length == desired_names.length desired_set = desired_names.to_set alias_owners = {} labels.each do |label| - normalized_aliases = label["aliases"].map do |name| - label_key(name) - end - - unless normalized_aliases.uniq.length == - normalized_aliases.length - raise "#{label['name']} 包含重复 aliases。" - end + normalized_aliases = label["aliases"].map { |name| label_key(name) } + raise "#{label['name']} 包含重复 aliases。" unless normalized_aliases.uniq.length == normalized_aliases.length label["aliases"].each do |alias_name| alias_key = label_key(alias_name) - if desired_set.include?(alias_key) - raise( - "#{label['name']} 的 alias " \ - "#{alias_name.inspect} 同时是正式标签名称。" - ) + raise "#{label['name']} 的 alias #{alias_name.inspect} 同时是正式标签名称。" end - if alias_owners.key?(alias_key) - raise( - "alias #{alias_name.inspect} 同时映射到 " \ - "#{alias_owners[alias_key]} 和 #{label['name']}。" - ) + raise "alias #{alias_name.inspect} 同时映射到 #{alias_owners[alias_key]} 和 #{label['name']}。" end alias_owners[alias_key] = label["name"] @@ -306,31 +212,32 @@ def load_labels(path) raise "标签配置 YAML 无效:#{error.message}" end -def sync_repository( - api, - full_name, - desired_labels, - dry_run: -) - path = repository_path(full_name) +def managed_label?(name, alias_keys) + key = label_key(name) + MANAGED_PREFIXES.any? { |prefix| key.start_with?(prefix) } || + MANAGED_EXACT_NAMES.include?(key) || + alias_keys.include?(key) +end - existing = api.paginate( - "/repos/#{path}/labels" - ) +def mutate(api, dry_run, method, *arguments) + api.public_send(method, *arguments) unless dry_run +end - labels_by_name = existing.to_h do |label| - [ - label_key(label["name"]), - label - ] - end +def sync_repository(api, full_name, desired_labels, dry_run:) + path = repository_path(full_name) + existing = api.paginate("/repos/#{path}/labels") + labels_by_name = existing.to_h { |label| [label_key(label["name"]), label] } + + desired_keys = desired_labels.map { |label| label_key(label["name"]) }.to_set + alias_keys = desired_labels.flat_map { |label| label["aliases"] }.map { |name| label_key(name) }.to_set counts = { created: 0, updated: 0, renamed: 0, deleted: 0, - unchanged: 0 + unchanged: 0, + preserved: 0 } puts "::group::#{full_name}" @@ -339,155 +246,106 @@ def sync_repository( desired_name = desired["name"] desired_key = label_key(desired_name) current = labels_by_name[desired_key] - - alias_matches = - desired["aliases"] - .filter_map do |alias_name| - labels_by_name[label_key(alias_name)] - end - .uniq do |label| - label_key(label["name"]) - end + alias_matches = desired["aliases"].filter_map { |alias_name| labels_by_name[label_key(alias_name)] } + .uniq { |label| label_key(label["name"]) } if current - changed = - current["name"] != desired_name || + changed = current["name"] != desired_name || current["color"].to_s.upcase != desired["color"] || current["description"].to_s != desired["description"] if changed - puts( - "#{dry_run ? 'WOULD UPDATE' : 'UPDATE'} " \ - "#{current['name']} -> #{desired_name}" - ) - - unless dry_run - api.patch( - "/repos/#{path}/labels/" \ - "#{escape_segment(current['name'])}", - { - new_name: desired_name, - color: desired["color"], - description: desired["description"] - } - ) - end - - labels_by_name.delete( - label_key(current["name"]) + puts "#{dry_run ? 'WOULD UPDATE' : 'UPDATE'} #{current['name']} -> #{desired_name}" + mutate( + api, + dry_run, + :patch, + "/repos/#{path}/labels/#{escape_segment(current['name'])}", + { + new_name: desired_name, + color: desired["color"], + description: desired["description"] + } ) - - labels_by_name[desired_key] = desired - counts[:updated] += 1 else puts "UNCHANGED #{desired_name}" counts[:unchanged] += 1 end - # 如果正式标签和旧 alias 同时存在, - # alias 会在后面的严格删除阶段被删除。 + labels_by_name.delete(label_key(current["name"])) + labels_by_name[desired_key] = desired + + alias_matches.each do |legacy| + legacy_name = legacy.fetch("name") + puts "#{dry_run ? 'WOULD DELETE' : 'DELETE'} legacy alias #{legacy_name}" + mutate(api, dry_run, :delete, "/repos/#{path}/labels/#{escape_segment(legacy_name)}") + labels_by_name.delete(label_key(legacy_name)) + counts[:deleted] += 1 + end + next end if alias_matches.length > 1 - names = alias_matches - .map { |label| label["name"] } - .join(", ") - - raise( - "多个旧标签同时映射到 #{desired_name}:#{names}" - ) + names = alias_matches.map { |label| label["name"] }.join(", ") + raise "多个旧标签同时映射到 #{desired_name}:#{names}" end if alias_matches.length == 1 old = alias_matches.first - - puts( - "#{dry_run ? 'WOULD RENAME' : 'RENAME'} " \ - "#{old['name']} -> #{desired_name}" - ) - - unless dry_run - api.patch( - "/repos/#{path}/labels/" \ - "#{escape_segment(old['name'])}", - { - new_name: desired_name, - color: desired["color"], - description: desired["description"] - } - ) - end - - labels_by_name.delete( - label_key(old["name"]) - ) - - labels_by_name[desired_key] = desired - - counts[:renamed] += 1 - next - end - - puts( - "#{dry_run ? 'WOULD CREATE' : 'CREATE'} " \ - "#{desired_name}" - ) - - unless dry_run - api.post( - "/repos/#{path}/labels", + puts "#{dry_run ? 'WOULD RENAME' : 'RENAME'} #{old['name']} -> #{desired_name}" + mutate( + api, + dry_run, + :patch, + "/repos/#{path}/labels/#{escape_segment(old['name'])}", { - name: desired_name, + new_name: desired_name, color: desired["color"], description: desired["description"] } ) + labels_by_name.delete(label_key(old["name"])) + labels_by_name[desired_key] = desired + counts[:renamed] += 1 + next end + puts "#{dry_run ? 'WOULD CREATE' : 'CREATE'} #{desired_name}" + mutate( + api, + dry_run, + :post, + "/repos/#{path}/labels", + { + name: desired_name, + color: desired["color"], + description: desired["description"] + } + ) labels_by_name[desired_key] = desired - counts[:created] += 1 end - desired_names = - desired_labels - .map { |label| label_key(label["name"]) } - .to_set - - extra_labels = - labels_by_name - .values - .reject do |label| - desired_names.include?( - label_key(label["name"]) - ) - end - .sort_by do |label| - label["name"].downcase - end + remaining = labels_by_name.values.reject { |label| desired_keys.include?(label_key(label["name"])) } + stale_managed, repository_specific = remaining.partition do |label| + managed_label?(label["name"], alias_keys) + end - extra_labels.each do |label| + stale_managed.sort_by { |label| label["name"].downcase }.each do |label| name = label.fetch("name") - - puts( - "#{dry_run ? 'WOULD DELETE' : 'DELETE'} " \ - "#{name}" - ) - - unless dry_run - api.delete( - "/repos/#{path}/labels/" \ - "#{escape_segment(name)}" - ) - end - + puts "#{dry_run ? 'WOULD DELETE' : 'DELETE'} stale organization label #{name}" + mutate(api, dry_run, :delete, "/repos/#{path}/labels/#{escape_segment(name)}") counts[:deleted] += 1 end - puts "::endgroup::" + repository_specific.sort_by { |label| label["name"].downcase }.each do |label| + puts "PRESERVE repository label #{label['name']}" + counts[:preserved] += 1 + end + puts "::endgroup::" counts rescue StandardError puts "::endgroup::" @@ -496,41 +354,24 @@ def sync_repository( def write_summary(results, failures) summary_path = ENV["GITHUB_STEP_SUMMARY"] - return if summary_path.to_s.empty? - File.open( - summary_path, - "a", - encoding: "UTF-8" - ) do |summary| + File.open(summary_path, "a", encoding: "UTF-8") do |summary| summary.puts "# 标签同步结果" summary.puts summary.puts "- 组织:`#{OWNER}`" summary.puts "- 配置:`#{CONFIG_FILE}`" summary.puts "- Dry Run:`#{DRY_RUN}`" - summary.puts "- 模式:严格镜像" + summary.puts "- 模式:组织级标签受管子集" summary.puts - - summary.puts( - "| 仓库 | 状态 | 新建 | 更新 | " \ - "重命名 | 删除 | 未变化 |" - ) - - summary.puts( - "| --- | --- | ---: | ---: | " \ - "---: | ---: | ---: |" - ) + summary.puts "| 仓库 | 状态 | 新建 | 更新 | 重命名 | 删除 | 未变化 | 保留扩展 |" + summary.puts "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |" results.each do |result| summary.puts( - "| `#{result[:repository]}` " \ - "| #{result[:status]} " \ - "| #{result[:created]} " \ - "| #{result[:updated]} " \ - "| #{result[:renamed]} " \ - "| #{result[:deleted]} " \ - "| #{result[:unchanged]} |" + "| `#{result[:repository]}` | #{result[:status]} | #{result[:created]} | " \ + "#{result[:updated]} | #{result[:renamed]} | #{result[:deleted]} | " \ + "#{result[:unchanged]} | #{result[:preserved]} |" ) end @@ -538,124 +379,82 @@ def write_summary(results, failures) summary.puts summary.puts "## 失败" summary.puts - failures.each do |failure| - message = failure[:error] - .gsub("\n", " ") - .gsub("|", "\\|") - - summary.puts( - "- `#{failure[:repository]}`:#{message}" - ) + message = failure[:error].gsub("\n", " ").gsub("|", "\\|") + summary.puts "- `#{failure[:repository]}`:#{message}" end end end end -desired_labels = load_labels(CONFIG_FILE) - -api = GitHubApi.new( - token: TOKEN, - base_url: API_URL -) +def run + raise "SYNC_LABELS_TOKEN 不能为空。" if TOKEN.empty? + raise "SYNC_LABELS_OWNER 不能为空。" if OWNER.empty? -repositories = - api.paginate( - "/orgs/#{escape_segment(OWNER)}/repos" \ - "?type=all&sort=full_name&direction=asc" + desired_labels = load_labels(CONFIG_FILE) + api = GitHubApi.new(token: TOKEN, base_url: API_URL) + repositories = api.paginate( + "/orgs/#{escape_segment(OWNER)}/repos?type=all&sort=full_name&direction=asc" ).select do |repository| - !repository["archived"] && - !repository["disabled"] && - !repository["fork"] + !repository["archived"] && !repository["disabled"] && !repository["fork"] end -unless ONLY_REPOSITORY.empty? - expected = - if ONLY_REPOSITORY.include?("/") - ONLY_REPOSITORY - else - "#{OWNER}/#{ONLY_REPOSITORY}" + unless ONLY_REPOSITORY.empty? + expected = ONLY_REPOSITORY.include?("/") ? ONLY_REPOSITORY : "#{OWNER}/#{ONLY_REPOSITORY}" + repositories.select! { |repository| repository["full_name"].casecmp(expected).zero? } + if repositories.empty? + raise "找不到可同步仓库 #{expected}。请检查名称、令牌访问范围和仓库状态。" end - - repositories.select! do |repository| - repository["full_name"] - .casecmp(expected) - .zero? end - if repositories.empty? - raise( - "找不到可同步仓库 #{expected}。" \ - "请检查名称、令牌访问范围和仓库状态。" - ) + puts "Owner: #{OWNER}" + puts "Config: #{CONFIG_FILE}" + puts "Dry run: #{DRY_RUN}" + puts "Repositories: #{repositories.length}" + puts + + results = [] + failures = [] + + repositories.each do |repository| + full_name = repository.fetch("full_name") + + begin + counts = sync_repository(api, full_name, desired_labels, dry_run: DRY_RUN) + results << { + repository: full_name, + status: DRY_RUN ? "预览完成" : "同步完成", + **counts + } + rescue StandardError => error + puts "::error title=标签同步失败::#{full_name}: #{error.message.lines.first.to_s.strip}" + puts + puts "Repository: #{full_name}" + puts error.message + puts + + results << { + repository: full_name, + status: "失败", + created: 0, + updated: 0, + renamed: 0, + deleted: 0, + unchanged: 0, + preserved: 0 + } + failures << { repository: full_name, error: error.message } + end end -end - -puts "Owner: #{OWNER}" -puts "Config: #{CONFIG_FILE}" -puts "Dry run: #{DRY_RUN}" -puts "Repositories: #{repositories.length}" -puts -results = [] -failures = [] - -repositories.each do |repository| - full_name = repository.fetch("full_name") - - begin - counts = sync_repository( - api, - full_name, - desired_labels, - dry_run: DRY_RUN - ) + write_summary(results, failures) - results << { - repository: full_name, - status: DRY_RUN ? "预览完成" : "同步完成", - **counts - } - rescue StandardError => error - puts( - "::error title=标签同步失败::" \ - "#{full_name}: #{error.message.lines.first.to_s.strip}" - ) - - puts - puts "Repository: #{full_name}" - puts error.message - puts - - results << { - repository: full_name, - status: "失败", - created: 0, - updated: 0, - renamed: 0, - deleted: 0, - unchanged: 0 - } - - failures << { - repository: full_name, - error: error.message - } + if failures.any? + warn "#{failures.length} 个仓库同步失败。" + exit 1 end -end - -write_summary( - results, - failures -) -if failures.any? - warn "#{failures.length} 个仓库同步失败。" - exit 1 + puts(DRY_RUN ? "Dry Run 完成。" : "标签同步完成。") end -puts( - DRY_RUN ? - "Dry Run 完成。" : - "标签同步完成。" -) +run if $PROGRAM_NAME == __FILE__ From 90c9a8a8717982250a825f15eee92f83a87a9789 Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:40:47 +0800 Subject: [PATCH 05/14] ci: require manual apply for label synchronization --- .github/workflows/sync-labels.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index 703e48c..326c244 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: dry_run: - description: "仅预览变更,不实际修改标签" + description: "仅预览变更;关闭后才会实际修改标签" required: true type: boolean default: true @@ -24,7 +24,7 @@ on: - ".github/workflows/sync-labels.yml" schedule: - # 每周一 UTC 03:17 检查并修复标签漂移。 + # 每周一 UTC 03:17 预览标签漂移,不自动写入仓库。 - cron: "17 3 * * 1" permissions: @@ -36,7 +36,7 @@ concurrency: jobs: sync-labels: - name: Synchronize organization labels + name: Preview or synchronize organization labels runs-on: ubuntu-latest timeout-minutes: 30 @@ -72,10 +72,10 @@ jobs: owner: ${{ github.repository_owner }} config_file: ".github/labels.yml" + # Push 和定时任务只做预览。只有手动触发并关闭 dry_run 才实际写入。 dry_run: >- - ${{ github.event_name == 'workflow_dispatch' - && inputs.dry_run - || false }} + ${{ github.event_name != 'workflow_dispatch' + || inputs.dry_run }} repository: >- ${{ github.event_name == 'workflow_dispatch' From 631c44a28ca8c8875ac16a0e45a90f1cc005d291 Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:41:00 +0800 Subject: [PATCH 06/14] test: cover managed and repository label behavior --- .../actions/sync-labels/test_sync_labels.rb | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/actions/sync-labels/test_sync_labels.rb diff --git a/.github/actions/sync-labels/test_sync_labels.rb b/.github/actions/sync-labels/test_sync_labels.rb new file mode 100644 index 0000000..92e1753 --- /dev/null +++ b/.github/actions/sync-labels/test_sync_labels.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require_relative "sync-labels" + +class FakeApi + attr_reader :calls + + def initialize(labels) + @labels = labels + @calls = [] + end + + def paginate(_path) + @labels.map(&:dup) + end + + def patch(path, body) + @calls << [:patch, path, body] + end + + def post(path, body) + @calls << [:post, path, body] + end + + def delete(path) + @calls << [:delete, path] + end +end + +class SyncLabelsTest < Minitest::Test + DESIRED = [ + { + "name" => "type: bug", + "color" => "D73A4A", + "description" => "已有行为出现错误、缺陷或回归", + "aliases" => ["bug"] + }, + { + "name" => "type: feature", + "color" => "A2EEEF", + "description" => "新增能力或改进现有功能", + "aliases" => ["enhancement"] + }, + { + "name" => "help wanted", + "color" => "008672", + "description" => "维护者明确欢迎并能够评审外部贡献", + "aliases" => [] + } + ].freeze + + def test_preserves_repository_specific_labels_and_removes_stale_managed_labels + api = FakeApi.new( + [ + { "name" => "bug", "color" => "FFFFFF", "description" => "legacy" }, + { "name" => "type: feature", "color" => "A2EEEF", "description" => "新增能力或改进现有功能" }, + { "name" => "enhancement", "color" => "FFFFFF", "description" => "legacy duplicate" }, + { "name" => "type: obsolete", "color" => "000000", "description" => "stale" }, + { "name" => "area: ephemeris", "color" => "123456", "description" => "repository extension" }, + { "name" => "package: core", "color" => "123456", "description" => "repository extension" }, + { "name" => "custom", "color" => "123456", "description" => "repository extension" } + ] + ) + + counts = sync_repository(api, "matharts/example", DESIRED, dry_run: false) + + assert_equal 1, counts[:created] + assert_equal 0, counts[:updated] + assert_equal 1, counts[:renamed] + assert_equal 2, counts[:deleted] + assert_equal 1, counts[:unchanged] + assert_equal 3, counts[:preserved] + + deleted_paths = api.calls.filter_map { |call| call[1] if call.first == :delete } + assert_includes deleted_paths, "/repos/matharts/example/labels/enhancement" + assert_includes deleted_paths, "/repos/matharts/example/labels/type%3A%20obsolete" + refute deleted_paths.any? { |path| path.include?("area%3A%20ephemeris") } + refute deleted_paths.any? { |path| path.include?("package%3A%20core") } + end + + def test_dry_run_does_not_mutate + api = FakeApi.new( + [ + { "name" => "bug", "color" => "FFFFFF", "description" => "legacy" }, + { "name" => "area: calendar", "color" => "123456", "description" => "repository extension" } + ] + ) + + counts = sync_repository(api, "matharts/example", DESIRED, dry_run: true) + + assert_empty api.calls + assert_equal 1, counts[:renamed] + assert_equal 2, counts[:created] + assert_equal 1, counts[:preserved] + end +end From 17c3bb0e741858ee8f83749f954d6bd59e72aced Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:41:06 +0800 Subject: [PATCH 07/14] ci: validate issue form labels against canonical config --- .github/scripts/validate-issue-labels.rb | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/scripts/validate-issue-labels.rb diff --git a/.github/scripts/validate-issue-labels.rb b/.github/scripts/validate-issue-labels.rb new file mode 100644 index 0000000..f0e753d --- /dev/null +++ b/.github/scripts/validate-issue-labels.rb @@ -0,0 +1,46 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "set" +require "yaml" + +LABEL_CONFIG = ".github/labels.yml" +ISSUE_TEMPLATE_GLOB = ".github/ISSUE_TEMPLATE/*.{yml,yaml}" +ERRORS = [] + +labels = YAML.safe_load( + File.read(LABEL_CONFIG, encoding: "UTF-8"), + permitted_classes: [], + permitted_symbols: [], + aliases: false +) + +unless labels.is_a?(Array) + warn "#{LABEL_CONFIG} 的根节点必须是数组。" + exit 1 +end + +canonical_names = labels.filter_map do |entry| + entry["name"].to_s.strip if entry.is_a?(Hash) +end.reject(&:empty?).to_set + +Dir.glob(ISSUE_TEMPLATE_GLOB).sort.each do |file| + document = YAML.safe_load( + File.read(file, encoding: "UTF-8"), + permitted_classes: [], + permitted_symbols: [], + aliases: false + ) + + next unless document.is_a?(Hash) + + Array(document["labels"]).each do |label| + name = label.to_s.strip + next if name.empty? || canonical_names.include?(name) + + puts "::error file=#{file}::Issue Form 使用了非正式标签:#{name}" + ERRORS << [file, name] + end +end + +exit ERRORS.empty? ? 0 : 1 From ec12e237dad83d2b34ef5302b8ce3f94dcea8d7e Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:41:17 +0800 Subject: [PATCH 08/14] ci: validate label templates and synchronization behavior --- .github/workflows/validate-documentation.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-documentation.yml b/.github/workflows/validate-documentation.yml index ba8dc4d..8cbd5b9 100644 --- a/.github/workflows/validate-documentation.yml +++ b/.github/workflows/validate-documentation.yml @@ -7,6 +7,7 @@ on: - ".github/**/*.yml" - ".github/**/*.yaml" - ".github/scripts/**" + - ".github/actions/sync-labels/**" - ".markdownlint-cli2.jsonc" - "lychee.toml" @@ -18,6 +19,7 @@ on: - ".github/**/*.yml" - ".github/**/*.yaml" - ".github/scripts/**" + - ".github/actions/sync-labels/**" - ".markdownlint-cli2.jsonc" - "lychee.toml" @@ -75,16 +77,23 @@ jobs: globs: "**/*.md" references: - name: Validate repository references + name: Validate repository references and governance scripts runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v7 with: persist-credentials: false + - name: Check repository references run: ruby .github/scripts/validate-document-references.rb + - name: Check Issue Form labels + run: ruby .github/scripts/validate-issue-labels.rb + + - name: Test label synchronization behavior + run: ruby .github/actions/sync-labels/test_sync_labels.rb + links: name: Validate documentation links runs-on: ubuntu-latest From c7858634d90cbdf280abf17ab21e819c67a16595 Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:41:35 +0800 Subject: [PATCH 09/14] docs: clarify canonical label configuration format --- .github/labels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/labels.yml b/.github/labels.yml index 0c63e37..bb19831 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -7,8 +7,8 @@ # - impact:*、process:* 可以组合使用; # - area:*、package:*、platform:* 由具体仓库自行维护。 # -# 本文件采用 EndBug/label-sync 的配置格式: -# https://github.com/EndBug/label-sync +# 本文件采用 MathArts 标签同步 Action 的配置格式。 +# aliases 仅用于将旧标签迁移到正式名称,不是可继续使用的标签名。 # ============================================================================= # Type From e5b000bdafd72fba02acacf00d8fc62025836cbe Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:41:46 +0800 Subject: [PATCH 10/14] docs: document safe label synchronization policy --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 391a708..996d42f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,9 @@ MathArts 的组织级标签由两个权威来源共同维护: 完整标签清单只在 `.github/labels.yml` 中维护,其他文档和模板不得复制第二份正式清单。 -项目仓库可以增加项目特定标签,但不得改变组织级标签的名称、含义或使用边界。 +标签同步只管理组织保留的 `type:*`、`status:*`、`priority:*`、`impact:*`、`process:*`、`resolution:*` 系列、贡献入口标签及其历史别名。项目仓库可以增加 `area:*`、`package:*`、`platform:*` 或其他项目特定标签,自动化会保留这些扩展标签。 + +推送和定时任务只预览标签漂移。实际修改必须通过手动触发标签同步工作流,并明确关闭 Dry Run。 ## 维护原则 From 87a8ce920c92f9971445b146354fff37f0f9a5f2 Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:44:23 +0800 Subject: [PATCH 11/14] fix: use canonical bug label in existing issue form --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 31e66c6..9ef4a6d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -2,7 +2,7 @@ name: "Bug 报告" description: "报告可以稳定复现的错误或非预期行为" title: "[Bug]: " labels: - - "bug" + - "type: bug" assignees: [] body: From 20cc36dd058de154e88bc49f37577e3cff67dfd3 Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:44:34 +0800 Subject: [PATCH 12/14] fix: use canonical feature label in existing issue form --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index dfe9fb3..9ebf10d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -2,7 +2,7 @@ name: "功能或改进建议" description: "提出新能力、工作流改进或用户可见行为变更" title: "[Feature]: " labels: - - "enhancement" + - "type: feature" assignees: [] body: From 730171cc50ed151d74c703eb12745aab82bb629b Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:44:41 +0800 Subject: [PATCH 13/14] chore: remove duplicate bug issue form --- .github/ISSUE_TEMPLATE/bug.yml | 122 --------------------------------- 1 file changed, 122 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug.yml diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml deleted file mode 100644 index 9ef4a6d..0000000 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ /dev/null @@ -1,122 +0,0 @@ -name: "Bug 报告" -description: "报告可以稳定复现的错误或非预期行为" -title: "[Bug]: " -labels: - - "type: bug" -assignees: [] - -body: - - type: markdown - attributes: - value: | - 感谢你帮助 MathArts 改进项目。 - - 提交前请先搜索已有 Issues,确认问题尚未被报告。 - - **安全漏洞不得在此公开提交。** 请按照 MathArts 的安全策略进行私密报告: - https://github.com/matharts/.github/blob/main/SECURITY.md - - - type: textarea - id: summary - attributes: - label: "问题摘要" - description: "请简要说明发生了什么,以及它为什么是一个问题。" - placeholder: "例如:在特定日期输入下,计算结果与项目文档定义不一致。" - validations: - required: true - - - type: input - id: version - attributes: - label: "受影响版本" - description: "请填写发布版本、软件包版本或 Commit SHA。" - placeholder: "例如:v0.4.1、1.2.0 或 abc1234" - validations: - required: true - - - type: textarea - id: environment - attributes: - label: "运行环境" - description: "请填写与复现有关的操作系统、运行时、浏览器、架构或依赖版本。" - placeholder: | - 操作系统: - 运行时及版本: - 浏览器及版本: - 其他相关依赖: - validations: - required: false - - - type: textarea - id: reproduction - attributes: - label: "复现步骤" - description: "请提供最小、完整且可以重复执行的步骤。" - placeholder: | - 1. 安装…… - 2. 使用输入…… - 3. 调用…… - 4. 观察到…… - validations: - required: true - - - type: textarea - id: minimal_example - attributes: - label: "最小复现示例" - description: "可以粘贴最小代码、配置或输入数据。请删除密钥和个人数据。" - render: "text" - validations: - required: false - - - type: textarea - id: expected - attributes: - label: "期望行为" - description: "请说明你期望得到的结果。" - validations: - required: true - - - type: textarea - id: actual - attributes: - label: "实际行为" - description: "请说明实际结果,包括完整错误信息或可观察到的差异。" - validations: - required: true - - - type: textarea - id: domain_context - attributes: - label: "规则、术语或资料依据" - description: "仅当问题涉及历法、术语、流派、映射关系或数术计算结果时填写。" - placeholder: | - 输入: - 当前结果: - 预期结果: - 采用的规则或流派: - 资料来源: - validations: - required: false - - - type: textarea - id: logs - attributes: - label: "日志或补充材料" - description: "可提供日志、截图、堆栈信息或相关链接。请删除敏感信息。" - validations: - required: false - - - type: checkboxes - id: confirmations - attributes: - label: "提交确认" - options: - - label: "我已搜索已有 Issues,并确认这不是重复报告。" - required: true - - label: "这不是需要私密报告的安全漏洞。" - required: true - - label: "我已删除密钥、凭证、个人数据和其他敏感信息。" - required: true - - label: "我愿意遵守 MathArts 社区行为准则。" - required: true From 6cb6d65201916f7fcfc3698be6d8d1e9c3765df6 Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 11 Jul 2026 07:44:48 +0800 Subject: [PATCH 14/14] chore: remove duplicate feature issue form --- .github/ISSUE_TEMPLATE/feature.yml | 123 ----------------------------- 1 file changed, 123 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature.yml diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml deleted file mode 100644 index 9ebf10d..0000000 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: "功能或改进建议" -description: "提出新能力、工作流改进或用户可见行为变更" -title: "[Feature]: " -labels: - - "type: feature" -assignees: [] - -body: - - type: markdown - attributes: - value: | - 感谢你提出建议。 - - 请优先描述需要解决的问题、目标用户和使用场景。 - - 尚未形成明确需求的方向讨论、理论讨论或跨仓库想法,更适合先提交到 MathArts Discussions: - https://github.com/orgs/matharts/discussions - - - type: textarea - id: problem - attributes: - label: "需要解决的问题" - description: "当前有什么限制、困难或未满足的需求?请不要只描述希望增加的实现。" - placeholder: "例如:使用者无法在不修改内部代码的情况下选择不同的规则配置。" - validations: - required: true - - - type: textarea - id: users_and_scenario - attributes: - label: "目标用户和使用场景" - description: "谁会使用这项能力?会在什么情况下使用?" - placeholder: | - 目标用户: - 使用场景: - 当前工作方式: - validations: - required: true - - - type: textarea - id: desired_outcome - attributes: - label: "期望结果" - description: "请描述完成后的用户体验、行为或可验证结果。" - validations: - required: true - - - type: textarea - id: proposal - attributes: - label: "建议方案" - description: "可以说明可能的接口、工作流或实现方向。维护者可能选择不同方案。" - validations: - required: false - - - type: textarea - id: alternatives - attributes: - label: "替代方案" - description: "请说明尝试过的变通方式,或考虑过但未采用的方案。" - validations: - required: false - - - type: dropdown - id: impact - attributes: - label: "可能影响" - description: "请选择最接近的影响范围。" - options: - - "不确定" - - "仅内部实现" - - "公共 API 或类型" - - "配置或数据格式" - - "用户可见行为或计算结果" - - "兼容性或 Breaking Change" - - "跨仓库或组织级流程" - validations: - required: true - - - type: textarea - id: domain_basis - attributes: - label: "术语、规则或资料依据" - description: "涉及数术术语、历法、流派、映射关系或计算规则时填写。" - placeholder: | - 术语或规则: - 适用范围: - 资料来源: - 已知差异或争议: - validations: - required: false - - - type: dropdown - id: contribution - attributes: - label: "贡献意愿" - description: "这不会影响建议是否被接受。" - options: - - "我可以提交 Pull Request" - - "我可以协助设计、测试或验证" - - "我目前只能提供需求和反馈" - validations: - required: true - - - type: textarea - id: additional_context - attributes: - label: "补充信息" - description: "可提供相关 Issue、Discussion、RFC、截图、示例或参考资料。" - validations: - required: false - - - type: checkboxes - id: confirmations - attributes: - label: "提交确认" - options: - - label: "我已搜索已有 Issues 和 Discussions,并确认这不是重复建议。" - required: true - - label: "我已区分事实、当前实现、个人建议和待验证内容。" - required: true - - label: "我愿意遵守 MathArts 社区行为准则。" - required: true