ci: gate merges on all platforms and structure issue intake
Pull requests: `lint (fmt, clippy)` and `pr-size` join the required checks together with all five `Tests (<os>)` jobs and both `Release workspace` jobs, so a PR can no longer merge with a red platform. Draft PRs run the three fast platforms on every push; marking ready for review adds Intel Mac and Windows ARM64. `pr-size` labels size/S-XL and fails above 1500 changed source lines unless a maintainer adds `size-exempt`. Issues: the bug form now asks for OS version, shell, program in the pane, connection type, non-default settings and mandatory logs/screenshots, with log paths per platform. `issue-labels` turns the dropdowns into os:/area: labels, flags regressions, and parks reports without usable steps, evidence or a version as needs-info with a checklist comment; an edit that fills the gaps clears the label. config.yml pointed at the old nebula repository.
This commit is contained in:
1 parent
0110c7c139
commit
b0caec9dcb
7 files changed
+510
-84
No files matched your search
@@ -1,114 +1,215 @@
|
||||
name: Bug Report
|
||||
description: Report a reproducible problem in Nebula
|
||||
name: Bug Report / 问题反馈
|
||||
description: Report a reproducible problem in Pebrel / 反馈一个可复现的问题
|
||||
title: "[Bug] "
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report a bug. Please search existing issues first and include enough detail for someone else to reproduce the problem.
|
||||
Thanks for taking the time to report a bug. A report we can reproduce gets fixed in the next release; one we cannot reproduce is asked for details and waits.
|
||||
感谢反馈。**能复现的问题会进下个版本;说不清的问题只能先挂起等补充。** 下拉选项会自动转成标签。
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Operating system
|
||||
label: Operating system / 操作系统
|
||||
options:
|
||||
- Windows
|
||||
- Windows (WSL)
|
||||
- macOS
|
||||
- Linux
|
||||
- Other
|
||||
- Other / 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os_version
|
||||
attributes:
|
||||
label: OS version and display / 系统版本与显示
|
||||
description: Windows 11 23H2 / macOS 26.0 / Ubuntu 24.04; DPI scaling and monitor count if display-related. / 系统版本;渲染问题请附缩放比例与显示器数量。
|
||||
placeholder: "Windows 11 23H2, 150% scaling, 2 monitors"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Nebula version
|
||||
description: Find it in Settings > Home, or on the latest release page.
|
||||
placeholder: "e.g. 1.0.0"
|
||||
label: Pebrel version / 版本号
|
||||
description: Settings > Home, or the release page. / 设置 > 主页,或发布页。
|
||||
placeholder: "e.g. 1.9.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: install_source
|
||||
attributes:
|
||||
label: How did you install Nebula?
|
||||
label: How did you install Pebrel? / 安装方式
|
||||
options:
|
||||
- GitHub Release (.msi / .exe / portable archive)
|
||||
- Built from source (cargo build / cargo run)
|
||||
- Other
|
||||
- GitHub Release (.exe / .zip / .dmg / .AppImage / .deb)
|
||||
- Built from source / 源码构建 (cargo build)
|
||||
- Other / 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: shell
|
||||
attributes:
|
||||
label: Shell in the affected pane / 出问题的面板用的 Shell
|
||||
multiple: true
|
||||
options:
|
||||
- PowerShell 7 (pwsh)
|
||||
- Windows PowerShell 5.1
|
||||
- cmd
|
||||
- WSL (bash / zsh inside WSL)
|
||||
- Git Bash / MSYS2
|
||||
- zsh
|
||||
- bash
|
||||
- fish
|
||||
- nushell
|
||||
- Not sure / 不确定
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: program
|
||||
attributes:
|
||||
label: Program running in the pane / 面板里正在跑的程序
|
||||
description: What was running when it happened? / 出问题时面板里跑着什么?
|
||||
multiple: true
|
||||
options:
|
||||
- Nothing, plain shell prompt / 什么都没跑
|
||||
- Claude Code
|
||||
- Codex CLI
|
||||
- Pi
|
||||
- Gemini CLI / other AI agent — 其他 AI agent
|
||||
- Terminal multiplexer (screen etc.) — 终端复用器
|
||||
- vim / nvim / other full-screen TUI — 全屏 TUI
|
||||
- ssh client / remote shell — 远程 shell
|
||||
- A long-running command (build, ping, adb…) — 长时间运行的命令
|
||||
- Other / 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: connection
|
||||
attributes:
|
||||
label: Connection type / 连接方式
|
||||
options:
|
||||
- Local terminal / 本地终端
|
||||
- SSH via Pebrel host list / Pebrel 内 SSH 主机
|
||||
- SSH via command line (ssh user@host) / 手动 ssh 命令
|
||||
- WSL
|
||||
- Other / 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Affected area
|
||||
label: Affected area / 影响范围
|
||||
description: Pick every area that applies. / 可多选。
|
||||
multiple: true
|
||||
options:
|
||||
- Local terminal / shell
|
||||
- SSH connection / authentication
|
||||
- SFTP / file tree
|
||||
- Tabs / splits / sessions
|
||||
- AI assistant / providers
|
||||
- Settings / backup / sync
|
||||
- UI / layout / rendering
|
||||
- Startup / crash
|
||||
- Other
|
||||
- Terminal rendering / scrolling / selection / garbled text — 终端渲染、滚动、选区、乱码
|
||||
- Shell startup / PATH / start directory / context menu — Shell 启动、环境变量、起始目录、右键菜单
|
||||
- SSH / proxy / jump host — SSH、代理、跳板
|
||||
- File tree / drag-and-drop upload — 文件树、拖拽上传
|
||||
- Tabs / splits / session restore / window size — 标签页、分屏、会话恢复、窗口尺寸
|
||||
- AI agent (Claude Code / Codex / Pi / OMP) — AI Agent 集成与状态
|
||||
- Markdown / LaTeX / image reader — 阅读页与内联渲染
|
||||
- Settings / theme / layout / blur — 设置页、主题、布局、模糊背景
|
||||
- Keyboard / IME / mouse / shortcuts / clipboard — 键盘、输入法、鼠标、快捷键、剪贴板
|
||||
- Git / SVN
|
||||
- Startup crash / hang / cannot exit — 启动崩溃、卡死、无法退出
|
||||
- Performance / lag — 性能、卡顿
|
||||
- Installer / update / packaging — 安装包、更新、打包
|
||||
- Other / 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: regression
|
||||
attributes:
|
||||
label: Did it work in a previous version? / 旧版本是否正常?
|
||||
options:
|
||||
- Not sure / 不确定
|
||||
- Yes, it worked before / 旧版本正常,新版本坏了
|
||||
- No, it never worked / 一直如此
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: reproducibility
|
||||
attributes:
|
||||
label: Can you reproduce it?
|
||||
label: Can you reproduce it? / 复现概率
|
||||
options:
|
||||
- Always (100%)
|
||||
- Often (>50%)
|
||||
- Sometimes
|
||||
- Once / not sure
|
||||
- Always (100%) / 必现
|
||||
- Often (>50%) / 经常
|
||||
- Sometimes / 偶尔
|
||||
- Once / not sure / 仅一次
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Use numbered steps and include the shell or connection type when relevant.
|
||||
label: Steps to reproduce / 复现步骤
|
||||
description: |
|
||||
Numbered, from a fresh launch, ending at the exact moment it goes wrong. "It's laggy" or "it doesn't work" cannot be acted on.
|
||||
从启动开始按序编号,写到出错的那一步为止。"很卡"、"不好用"这种一句话我们没法处理。
|
||||
placeholder: |
|
||||
1. Open Nebula
|
||||
2. Create or connect to ...
|
||||
3. ...
|
||||
1. Launch Pebrel, open a new PowerShell 7 tab
|
||||
2. Run `codex` and ask it to ...
|
||||
3. Split the pane with Ctrl+Shift+D
|
||||
4. Output overlaps the input box at the bottom (see screenshot)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
label: Expected behavior / 期望结果
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
label: Actual behavior / 实际结果
|
||||
description: What you see instead, including any error text verbatim. / 实际看到什么,错误文字原样贴。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs / screenshots
|
||||
description: Include relevant lines or screenshots. Remove passwords, private keys, tokens, host addresses, and other secrets.
|
||||
label: Logs / screenshots / 日志或截图
|
||||
description: |
|
||||
Required. Screenshot or short recording for anything visual; log lines for crashes, hangs and connection problems. Remove passwords, keys, tokens and host addresses first.
|
||||
必填。显示类问题贴截图或短录屏;崩溃、卡死、连接类问题贴日志。请先删掉密码、私钥、token、主机地址。
|
||||
Logs / 日志位置:
|
||||
- Windows: `%APPDATA%\Pebrel\pebrel_debug.log` and `%TEMP%\Pebrel-<pid>.log`
|
||||
- macOS: `~/Library/Application Support/Pebrel/pebrel_debug.log` and `$TMPDIR/Pebrel-<pid>.log`
|
||||
- Linux: `~/.config/pebrel/pebrel_debug.log` and `/tmp/Pebrel-<pid>.log`
|
||||
placeholder: Paste redacted logs or drag screenshots here.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: settings
|
||||
attributes:
|
||||
label: Non-default settings / 改过的设置
|
||||
description: Theme, font, transparency/blur, GPU, shell profile, hooks — anything changed from defaults. Attach settings.txt if unsure. / 改过的主题、字体、透明/模糊、GPU、Shell 配置、hooks;不确定就贴 settings.txt。
|
||||
placeholder: "Acrylic blur on, opacity 85%, font JetBrains Mono 13"
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Before submitting
|
||||
label: Before submitting / 提交前确认
|
||||
options:
|
||||
- label: I searched existing issues and did not find a duplicate
|
||||
- label: I searched existing issues and did not find a duplicate / 已搜索,无重复
|
||||
required: true
|
||||
- label: I removed passwords, private keys, tokens, and other secrets from this report
|
||||
- label: I tested with the latest release / 已用最新版本试过
|
||||
required: true
|
||||
- label: I removed passwords, private keys, tokens and other secrets / 已删除敏感信息
|
||||
required: true
|
||||
@@ -1,8 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Existing issues
|
||||
url: https://github.com/Kuddev/nebula/issues
|
||||
about: Search existing reports before opening a new issue.
|
||||
- name: Latest release
|
||||
url: https://github.com/Kuddev/nebula/releases/latest
|
||||
about: Check whether the problem is already fixed in the latest version.
|
||||
- name: Existing issues / 已有 issue
|
||||
url: https://github.com/Kuddev/pebrel/issues
|
||||
about: Search existing reports before opening a new issue. / 先搜一下是否已有人反馈。
|
||||
- name: Latest release / 最新版本
|
||||
url: https://github.com/Kuddev/pebrel/releases/latest
|
||||
about: Check whether the problem is already fixed in the latest version. / 看看最新版是否已修复。
|
||||
- name: Discord
|
||||
url: https://discord.gg/VFn4rcxmhn
|
||||
about: Quick questions and discussion. / 快速提问与交流。
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Feature Request
|
||||
description: Suggest an improvement or new capability for Nebula
|
||||
name: Feature Request / 功能建议
|
||||
description: Suggest an improvement or new capability for Pebrel / 提出改进或新功能
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement", "triage"]
|
||||
body:
|
||||
@@ -7,12 +7,13 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Describe the workflow problem first, then the change you want. Concrete examples make the request easier to evaluate.
|
||||
先说现在哪里不顺手,再说希望怎么改。具体的使用场景最有说服力。
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem / pain point
|
||||
description: What is hard, missing, or repetitive today?
|
||||
label: Problem / pain point — 现在的痛点
|
||||
description: What is hard, missing, or repetitive today? / 什么事难做、缺失、或者每天重复?
|
||||
placeholder: When I work with multiple terminals or hosts, I cannot ...
|
||||
validations:
|
||||
required: true
|
||||
@@ -20,50 +21,66 @@ body:
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: What would you like Nebula to do?
|
||||
label: Proposed solution — 希望的做法
|
||||
description: What would you like Pebrel to do? / 希望 Pebrel 怎么做?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: List workarounds, other tools, or designs you considered.
|
||||
validations:
|
||||
required: true
|
||||
label: Alternatives considered — 现在的替代办法
|
||||
description: Workarounds, other tools, or designs you considered. / 目前怎么绕过去的,别的终端怎么做的。
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform / 平台
|
||||
description: Leave empty if it applies everywhere. / 全平台通用可不选。
|
||||
multiple: true
|
||||
options:
|
||||
- Windows
|
||||
- Windows (WSL)
|
||||
- macOS
|
||||
- Linux
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Related area
|
||||
label: Related area / 相关范围
|
||||
multiple: true
|
||||
options:
|
||||
- Local terminal / shell
|
||||
- SSH / SFTP
|
||||
- Tabs / splits / sessions
|
||||
- AI assistant / providers
|
||||
- Settings / backup / sync
|
||||
- UI / UX
|
||||
- Other
|
||||
- Terminal rendering / scrolling / selection — 终端渲染、滚动、选区
|
||||
- Shell startup / PATH / start directory / context menu — Shell 启动、环境变量、起始目录、右键菜单
|
||||
- SSH / proxy / jump host — SSH、代理、跳板
|
||||
- File tree / drag-and-drop upload — 文件树、拖拽上传
|
||||
- Tabs / splits / session restore / window size — 标签页、分屏、会话恢复、窗口尺寸
|
||||
- AI agent (Claude Code / Codex / Pi / OMP) — AI Agent 集成与状态
|
||||
- Markdown / LaTeX / image reader — 阅读页与内联渲染
|
||||
- Settings / theme / layout / blur — 设置页、主题、布局、模糊背景
|
||||
- Keyboard / IME / mouse / shortcuts / clipboard — 键盘、输入法、鼠标、快捷键、剪贴板
|
||||
- Git / SVN
|
||||
- Performance / lag — 性能、卡顿
|
||||
- Installer / update / packaging — 安装包、更新、打包
|
||||
- Other / 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: How important is this to you?
|
||||
label: How important is this to you? / 对你有多重要
|
||||
options:
|
||||
- Nice to have
|
||||
- Would improve my daily workflow
|
||||
- Blocking / critical for my use case
|
||||
- Nice to have / 有更好
|
||||
- Would improve my daily workflow / 每天都会用到
|
||||
- Blocking / critical for my use case / 没有就没法用
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Before submitting
|
||||
label: Before submitting / 提交前确认
|
||||
options:
|
||||
- label: I searched existing issues for similar requests
|
||||
- label: I searched existing issues for similar requests / 已搜索,无重复
|
||||
required: true
|
||||
@@ -0,0 +1,154 @@
|
||||
name: Issue labels
|
||||
|
||||
# Turns the dropdown answers of the issue forms (.github/ISSUE_TEMPLATE/*.yml)
|
||||
# into labels and flags bug reports that are too thin to act on. Labels are
|
||||
# only added; maintainers' manual labels are never removed.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: issue-labels-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
label-from-form:
|
||||
name: label-from-form
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Map form answers to labels
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const body = issue.body || "";
|
||||
|
||||
// Issue forms render every field as "### <label>\n\n<value>";
|
||||
// multi-select dropdowns join their values with ", ".
|
||||
const fields = {};
|
||||
const re = /^### (.+?)\s*\n+([\s\S]*?)(?=^### |\s*$(?![\s\S]))/gm;
|
||||
for (const m of body.matchAll(re)) {
|
||||
const value = m[2].trim();
|
||||
if (value && value !== "_No response_") fields[m[1].trim()] = value;
|
||||
}
|
||||
const field = (prefix) => {
|
||||
const key = Object.keys(fields).find((k) => k.startsWith(prefix));
|
||||
return key ? fields[key] : "";
|
||||
};
|
||||
const answers = (prefix) => {
|
||||
const value = field(prefix);
|
||||
return value ? value.split(", ").map((s) => s.trim()) : [];
|
||||
};
|
||||
|
||||
// Match on the English lead of each option so wording tweaks in
|
||||
// the Chinese half never silently break the mapping.
|
||||
const OS = [
|
||||
["Windows (WSL)", ["os: windows", "os: wsl"]],
|
||||
["Windows", ["os: windows"]],
|
||||
["macOS", ["os: macos"]],
|
||||
["Linux", ["os: linux"]],
|
||||
];
|
||||
const AREA = [
|
||||
["Terminal rendering", ["area: terminal"]],
|
||||
["Shell startup", ["area: shell"]],
|
||||
["SSH", ["area: ssh"]],
|
||||
["File tree", ["area: files"]],
|
||||
["Tabs / splits", ["area: session"]],
|
||||
["AI agent", ["area: agents"]],
|
||||
["Markdown", ["area: reader"]],
|
||||
["Settings", ["area: ui"]],
|
||||
["Keyboard", ["area: input"]],
|
||||
["Git", ["area: git"]],
|
||||
["Startup crash", ["crash"]],
|
||||
["Performance", ["performance"]],
|
||||
["Installer", ["area: ci-release"]],
|
||||
];
|
||||
const PROGRAM = [
|
||||
["Claude Code", ["area: agents"]],
|
||||
["Codex", ["area: agents"]],
|
||||
["Pi", ["area: agents"]],
|
||||
["Gemini", ["area: agents"]],
|
||||
];
|
||||
const CONNECTION = [
|
||||
["SSH", ["area: ssh"]],
|
||||
["WSL", ["os: wsl"]],
|
||||
];
|
||||
const SHELL = [["WSL", ["os: wsl"]]];
|
||||
const pick = (values, table) => {
|
||||
const out = [];
|
||||
for (const v of values) {
|
||||
const hit = table.find(([lead]) => v.startsWith(lead));
|
||||
if (hit) out.push(...hit[1]);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const labels = new Set([
|
||||
...pick([...answers("Operating system"), ...answers("Platform")], OS),
|
||||
...pick([...answers("Affected area"), ...answers("Related area")], AREA),
|
||||
...pick(answers("Program running in the pane"), PROGRAM),
|
||||
...pick(answers("Connection type"), CONNECTION),
|
||||
...pick(answers("Shell in the affected pane"), SHELL),
|
||||
]);
|
||||
if (answers("Did it work in a previous version").some((v) => v.startsWith("Yes"))) {
|
||||
labels.add("regression");
|
||||
}
|
||||
|
||||
// Thin bug reports: no usable steps or no evidence. Ask once, on
|
||||
// open; an edit that fills the gaps clears the label.
|
||||
const existing = new Set(issue.labels.map((l) => l.name));
|
||||
const isBug = existing.has("bug");
|
||||
const steps = field("Steps to reproduce");
|
||||
const logs = field("Logs / screenshots");
|
||||
const missing = [];
|
||||
// CJK carries more per character; weigh it double. Numbered
|
||||
// multi-line steps count as an attempt regardless of length.
|
||||
const compact = steps.replace(/\s+/g, "");
|
||||
const weight = compact.length + (compact.match(/[ -鿿豈-]/g) || []).length;
|
||||
const numbered = (steps.match(/^\s*\d+[.、)]/gm) || []).length >= 2;
|
||||
if (isBug && weight < 60 && !numbered) {
|
||||
missing.push("**Steps to reproduce / 复现步骤**: numbered steps from launch to the failing moment / 从启动开始按序写到出错那一步");
|
||||
}
|
||||
if (isBug && !logs) {
|
||||
missing.push("**Logs / screenshots / 日志或截图**: a screenshot for visual issues, `pebrel_debug.log` for crashes, hangs and connection problems / 显示类问题贴截图,崩溃卡死连接类贴日志");
|
||||
}
|
||||
if (isBug && !/\d+\.\d+/.test(field("Pebrel version"))) {
|
||||
missing.push("**Pebrel version / 版本号**: e.g. `1.9.0` (Settings > Home)");
|
||||
}
|
||||
|
||||
const toAdd = [...labels].filter((l) => !existing.has(l));
|
||||
if (missing.length) {
|
||||
if (!existing.has("needs-info")) toAdd.push("needs-info");
|
||||
if (context.payload.action === "opened") {
|
||||
await github.rest.issues.createComment({
|
||||
...context.repo,
|
||||
issue_number: issue.number,
|
||||
body: [
|
||||
"Thanks for the report. It is missing details we need before anyone can reproduce it, so it is parked as `needs-info`. Please edit the issue and fill in:",
|
||||
"感谢反馈。目前缺少复现所需的信息,已标记 `needs-info`。请**编辑 issue** 补齐以下内容,补齐后标签会自动移除:",
|
||||
"",
|
||||
...missing.map((m) => `- ${m}`),
|
||||
"",
|
||||
"Reports without these are closed after 14 days of silence. / 14 天无补充将关闭。",
|
||||
].join("\n"),
|
||||
});
|
||||
}
|
||||
} else if (isBug && existing.has("needs-info") && context.payload.action === "edited") {
|
||||
await github.rest.issues.removeLabel({
|
||||
...context.repo, issue_number: issue.number, name: "needs-info",
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
core.info(`form labels: ${[...labels].join(", ") || "(none)"}; adding: ${toAdd.join(", ") || "(none)"}; missing: ${missing.length}`);
|
||||
if (toAdd.length) {
|
||||
await github.rest.issues.addLabels({
|
||||
...context.repo,
|
||||
issue_number: issue.number,
|
||||
labels: toAdd,
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,9 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
# Include docs, catalogs, assets and workflow edits: every PR gets test results.
|
||||
# ready_for_review starts the scarce-runner jobs that draft pushes skip.
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
@@ -29,13 +31,61 @@ jobs:
|
||||
with:
|
||||
runtime_run: ${{ inputs.windows_diagnostics_run }}
|
||||
|
||||
lint:
|
||||
if: ${{ inputs.windows_diagnostics_run == '' }}
|
||||
name: lint (fmt, clippy)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install native dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends build-essential cmake pkg-config libasound2-dev libdbus-1-dev libudev-dev libfontconfig1-dev libfreetype6-dev libegl1-mesa-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxcb-xinput-dev libxcb-xkb-dev libxi-dev
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: "1.97.1"
|
||||
components: rustfmt, clippy
|
||||
- name: rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
- uses: ./.github/actions/rust-cache
|
||||
id: rust-cache
|
||||
with:
|
||||
key: clippy
|
||||
target: target/ci
|
||||
- name: clippy
|
||||
# Advisory until the existing warning backlog is cleared; then drop
|
||||
# continue-on-error and add -D warnings.
|
||||
continue-on-error: true
|
||||
run: cargo clippy --locked --config .github/ci-profile.toml --profile ci --workspace --all-targets
|
||||
- name: Save compiled workload
|
||||
if: ${{ always() && steps.rust-cache.outcome == 'success' && steps.rust-cache.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: target/ci
|
||||
key: ${{ steps.rust-cache.outputs.cache-key }}
|
||||
|
||||
# Draft pull requests run the three fast platforms (about 20 minutes) on
|
||||
# every push. The scarce runners (Intel Mac, Windows ARM64) join once the
|
||||
# pull request is marked ready for review and on main, so every merge is
|
||||
# verified on all five platforms without paying their queue time while a
|
||||
# change is still being iterated. Skipped jobs report success, which keeps
|
||||
# all five names usable as required checks; a ready PR cannot merge red.
|
||||
native-tests:
|
||||
if: ${{ inputs.windows_diagnostics_run == '' }}
|
||||
name: Tests (${{ matrix.os }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-24.04, windows-2022, windows-11-arm, macos-26, macos-26-intel]
|
||||
os: [ubuntu-24.04, windows-2022, macos-26]
|
||||
tier: [core]
|
||||
include:
|
||||
- os: windows-11-arm
|
||||
tier: full
|
||||
- os: macos-26-intel
|
||||
tier: full
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
@@ -45,13 +95,15 @@ jobs:
|
||||
RUST_TOOLCHAIN: "1.97.1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v5
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install native dependencies
|
||||
if: runner.os == 'Linux'
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && (runner.os == 'Linux') }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
@@ -62,15 +114,16 @@ jobs:
|
||||
libx11-xcb-dev libxcb1-dev libxcb-render0-dev \
|
||||
libxcb-shape0-dev libxcb-xfixes0-dev libxcb-xinput-dev libxcb-xkb-dev libxi-dev
|
||||
- name: Require the AppKit SDK used by this PR
|
||||
if: runner.os == 'macOS'
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && (runner.os == 'macOS') }}
|
||||
run: |
|
||||
sdk_version="$(xcrun --sdk macosx --show-sdk-version)"
|
||||
test "${sdk_version%%.*}" -ge 26
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
with:
|
||||
toolchain: "1.97.1"
|
||||
- name: Require a native Windows ARM64 compiler
|
||||
if: matrix.os == 'windows-11-arm'
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && (matrix.os == 'windows-11-arm') }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -ne 'Arm64') {
|
||||
@@ -81,6 +134,7 @@ jobs:
|
||||
throw 'The Windows ARM job must compile and run ARM64 tests.'
|
||||
}
|
||||
- uses: ./.github/actions/rust-cache
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
id: rust-cache
|
||||
with:
|
||||
key: native
|
||||
@@ -90,25 +144,26 @@ jobs:
|
||||
${{ runner.os != 'macOS' && 'target/release' || '' }}
|
||||
legacy-key: ${{ runner.os == 'Windows' && 'windows' || runner.os == 'Linux' && 'linux' || 'macos' }}
|
||||
- name: Prepare pinned Windows console runtime
|
||||
if: runner.os == 'Windows'
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && (runner.os == 'Windows') }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$architecture = if ('${{ runner.arch }}' -eq 'ARM64') { 'arm64' } else { 'x64' }
|
||||
./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost -Architecture $architecture
|
||||
./scripts/tests/windows-runtime.tests.ps1 -Architecture $architecture
|
||||
- name: Test complete workspace and native harness
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
run: python scripts/ci_native_tests.py
|
||||
- name: Check source architecture
|
||||
if: ${{ !cancelled() && runner.os == 'Linux' }}
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && (!cancelled() && runner.os == 'Linux') }}
|
||||
run: python scripts/check_architecture.py
|
||||
- name: Test translation and allocation contracts
|
||||
if: ${{ !cancelled() && runner.os == 'Linux' }}
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && (!cancelled() && runner.os == 'Linux') }}
|
||||
run: cargo test --locked --manifest-path tools/i18n-contract/Cargo.toml
|
||||
- name: Check the release workspace
|
||||
if: ${{ !cancelled() && runner.os != 'macOS' }}
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && (!cancelled() && runner.os != 'macOS') }}
|
||||
run: cargo check --locked --workspace --release
|
||||
- name: Save compiled workload
|
||||
if: ${{ always() && steps.rust-cache.outcome == 'success' && steps.rust-cache.outputs.cache-hit != 'true' }}
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && (always() && steps.rust-cache.outcome == 'success' && steps.rust-cache.outputs.cache-hit != 'true') }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
@@ -116,7 +171,7 @@ jobs:
|
||||
${{ runner.os != 'macOS' && 'target/release' || '' }}
|
||||
key: ${{ steps.rust-cache.outputs.cache-key }}
|
||||
- name: Upload native compilation timings
|
||||
if: always()
|
||||
if: ${{ (matrix.tier != 'full' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && (always()) }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: native-timings-${{ matrix.os }}
|
||||
@@ -145,32 +200,38 @@ jobs:
|
||||
RUST_TOOLCHAIN: "1.97.1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ (matrix.os != 'macos-26-intel' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
- uses: actions/setup-python@v5
|
||||
if: ${{ (matrix.os != 'macos-26-intel' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Require the AppKit SDK used by this PR
|
||||
if: ${{ (matrix.os != 'macos-26-intel' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
run: |
|
||||
sdk_version="$(xcrun --sdk macosx --show-sdk-version)"
|
||||
test "${sdk_version%%.*}" -ge 26
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
if: ${{ (matrix.os != 'macos-26-intel' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
with:
|
||||
toolchain: "1.97.1"
|
||||
- uses: ./.github/actions/rust-cache
|
||||
if: ${{ (matrix.os != 'macos-26-intel' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
id: rust-cache
|
||||
with:
|
||||
key: native-release-check
|
||||
target: target/release
|
||||
legacy-key: macos
|
||||
- name: Check the release workspace
|
||||
if: ${{ (matrix.os != 'macos-26-intel' || github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
run: cargo check --locked --workspace --release --timings
|
||||
- name: Save compiled workload
|
||||
if: ${{ always() && steps.rust-cache.outcome == 'success' && steps.rust-cache.outputs.cache-hit != 'true' }}
|
||||
if: ${{ (matrix.os != 'macos-26-intel' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && always() && steps.rust-cache.outcome == 'success' && steps.rust-cache.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: target/release
|
||||
key: ${{ steps.rust-cache.outputs.cache-key }}
|
||||
- name: Upload release check compilation timings
|
||||
if: always()
|
||||
if: ${{ (matrix.os != 'macos-26-intel' || github.event_name != 'pull_request' || !github.event.pull_request.draft) && always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-check-timings-${{ matrix.os }}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
name: PR size
|
||||
|
||||
# Labels every pull request by the number of changed source lines and fails
|
||||
# oversized ones so they get split before review. Generated and documentation
|
||||
# files do not count. Maintainers bypass the limit with the `size-exempt`
|
||||
# label (a release integration branch is the usual case).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
# Required checks must also report inside the merge queue; the size was
|
||||
# already enforced on the pull request, so this is a pass-through.
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: pr-size-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pr-size:
|
||||
name: pr-size
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const LIMIT = 1500;
|
||||
const IGNORED = [
|
||||
/^Cargo\.lock$/, /(^|\/)package-lock\.json$/, /(^|\/)yarn\.lock$/,
|
||||
/^docs\//, /\.md$/, /\.snap$/, /\.svg$/, /\.png$/, /\.ico$/,
|
||||
/^assets\//, /^locales\//, /\.lock$/,
|
||||
];
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
...context.repo, pull_number: pr.number, per_page: 100,
|
||||
});
|
||||
let counted = 0, ignored = 0;
|
||||
for (const f of files) {
|
||||
const lines = f.additions + f.deletions;
|
||||
if (IGNORED.some((re) => re.test(f.filename))) ignored += lines; else counted += lines;
|
||||
}
|
||||
|
||||
const size = counted <= 100 ? "S" : counted <= 500 ? "M" : counted <= LIMIT ? "L" : "XL";
|
||||
const labels = new Set(pr.labels.map((l) => l.name));
|
||||
const wanted = `size/${size}`;
|
||||
for (const l of labels) {
|
||||
if (l.startsWith("size/") && l !== wanted) {
|
||||
await github.rest.issues.removeLabel({ ...context.repo, issue_number: pr.number, name: l }).catch(() => {});
|
||||
}
|
||||
}
|
||||
if (!labels.has(wanted)) {
|
||||
await github.rest.issues.addLabels({ ...context.repo, issue_number: pr.number, labels: [wanted] });
|
||||
}
|
||||
|
||||
core.info(`counted ${counted} lines (${ignored} ignored) in ${files.length} files -> ${wanted}`);
|
||||
if (size !== "XL" || labels.has("size-exempt")) return;
|
||||
|
||||
const marker = "<!-- pr-size-guard -->";
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
...context.repo, issue_number: pr.number, per_page: 100,
|
||||
});
|
||||
if (!comments.some((c) => c.body && c.body.includes(marker))) {
|
||||
await github.rest.issues.createComment({
|
||||
...context.repo, issue_number: pr.number,
|
||||
body: [
|
||||
marker,
|
||||
`This pull request changes **${counted}** source lines (limit ${LIMIT}; docs, lockfiles and assets excluded). Changes this large cannot be reviewed reliably, so please split it into independent pull requests — one behaviour change each, with its own tests.`,
|
||||
"",
|
||||
`本 PR 改动 **${counted}** 行源码(上限 ${LIMIT},不计文档、lockfile、资源)。这么大的改动无法可靠审查,请拆成多个相互独立的 PR,每个只做一件事并附测试。`,
|
||||
"",
|
||||
"Maintainers can add the `size-exempt` label for integration branches. / 维护者可对集成分支加 `size-exempt` 放行。",
|
||||
].join("\n"),
|
||||
});
|
||||
}
|
||||
core.setFailed(`${counted} changed source lines exceed the ${LIMIT}-line limit; split the pull request or add size-exempt.`);
|
||||
+12
-3
@@ -74,6 +74,8 @@ raise any existing tracked private artifact for an explicit maintainer decision.
|
||||
`architecture/notes/`; ordinary fixes do not create a note.
|
||||
2. Keep one conceptual change per PR. A necessary extraction and its behavior
|
||||
tests may accompany the feature; unrelated rewrites and formatting may not.
|
||||
The `pr-size` check fails above 1500 changed source lines (docs, lockfiles
|
||||
and assets excluded); split instead of asking for an exemption.
|
||||
3. Put shared rules in their existing authority. UI modules adapt those rules;
|
||||
they must not fork persistence, state transitions or domain behavior.
|
||||
4. Add regression tests that fail for the defect, and test error/cancellation
|
||||
@@ -122,9 +124,14 @@ compile check into a claim that UI tests or a packaged application were run.
|
||||
|
||||
## Review and enforcement
|
||||
|
||||
`architecture-contracts` is the stable PR job name. Maintainers must enable it as
|
||||
a required check and require Code Owner approval in the target branch ruleset;
|
||||
see the [activation checklist](docs/project-constraints.md#server-side-activation).
|
||||
`architecture-contracts`, `lint (fmt, clippy)`, `pr-size`, the five
|
||||
`Tests (<os>)` jobs and both `Release workspace (<os>)` jobs are required checks
|
||||
on `main`, together with Code Owner approval; see the
|
||||
[activation checklist](docs/project-constraints.md#server-side-activation).
|
||||
Open pull requests as drafts while iterating: drafts run Linux, Windows x64 and
|
||||
Apple Silicon on every push (about 20 minutes). Marking the PR ready for review
|
||||
adds Intel Mac and Windows ARM64, so every merge is verified on all five
|
||||
platforms without paying for the scarce runners on each push.
|
||||
Local hooks are convenient, but bypassable; they are not the enforcement boundary.
|
||||
Submitting a workflow or `CODEOWNERS` file does not configure server-side rules.
|
||||
|
||||
@@ -137,6 +144,8 @@ feature work; there is no routine `--skip-architecture` option.
|
||||
## 中文摘要
|
||||
|
||||
- 先读架构图、工程合同和决策记录;按职责拆分,不按行号切片。
|
||||
- 一个 PR 只做一件事;改动超过 1500 行源码(不计文档、lockfile、资源)`pr-size` 会失败,请拆分。
|
||||
- 迭代期间用 Draft PR:每次 push 跑 Linux / Windows x64 / Apple Silicon(约 20 分钟);标记 Ready for review 后补跑 Intel Mac 与 Windows ARM64,十项必需检查全绿才能合并。
|
||||
- 2000 行是现有仓库的防灾上限,800 行只提示审查,不是“大厂标准”。
|
||||
- 普通功能 PR 不得增加存量债务;有问题的规则可以修订,但要有反例、测试和维护者审批。
|
||||
- 新增核心抽象、依赖方向、持久化或线程模型改变要先说明设计,不强迫每个小修复写 ADR。
|
||||
|
||||
Reference in new issue
Block a user