Merge branch 'main' into fix/feedback-terminal-color-20260923

This commit is contained in:
Kuddev authored and GitHub committed 2026-09-23 18:51:24 +08:00
commit e13e92b761
14 files changed
+1009 -106

No files matched your search

+140 -39
View File
@@ -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
+9 -6
View File
@@ -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. / 快速提问与交流。
+41 -24
View File
@@ -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
+154
View File
@@ -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,
});
}
+51 -8
View File
@@ -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,50 @@ jobs:
with:
runtime_run: ${{ inputs.windows_diagnostics_run }}
# rustfmt only for now. clippy joins once the existing warning backlog is
# cleared; several crates deny lints, so it cannot run advisory.
lint:
if: ${{ inputs.windows_diagnostics_run == '' }}
name: lint
outputs:
native_matrix: ${{ steps.matrix.outputs.native_matrix }}
release_matrix: ${{ steps.matrix.outputs.release_matrix }}
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.97.1"
components: rustfmt
- name: rustfmt
run: cargo fmt --all -- --check
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Plan native validation
id: matrix
shell: bash
run: |
python scripts/ci_plan.py \
--event-name "$GITHUB_EVENT_NAME" \
--event-path "$GITHUB_EVENT_PATH" \
--output "$GITHUB_OUTPUT"
# The required lint job validates the matrix before heavy runners are
# requested. Draft PRs omit deferred platforms entirely; ready PRs and
# release callers require the full matrix. Deferred checks remain missing
# on drafts instead of being reported as successful tests.
native-tests:
needs: lint
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]
include: ${{ fromJSON(needs.lint.outputs.native_matrix) }}
runs-on: ${{ matrix.os }}
timeout-minutes: 120
env:
@@ -46,12 +85,13 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install native dependencies
if: runner.os == 'Linux'
if: ${{ runner.os == 'Linux' }}
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
@@ -62,7 +102,7 @@ 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: ${{ runner.os == 'macOS' }}
run: |
sdk_version="$(xcrun --sdk macosx --show-sdk-version)"
test "${sdk_version%%.*}" -ge 26
@@ -70,7 +110,7 @@ jobs:
with:
toolchain: "1.97.1"
- name: Require a native Windows ARM64 compiler
if: matrix.os == 'windows-11-arm'
if: ${{ matrix.os == 'windows-11-arm' }}
shell: pwsh
run: |
if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -ne 'Arm64') {
@@ -90,7 +130,7 @@ 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: ${{ runner.os == 'Windows' }}
shell: pwsh
run: |
$architecture = if ('${{ runner.arch }}' -eq 'ARM64') { 'arm64' } else { 'x64' }
@@ -116,7 +156,7 @@ jobs:
${{ runner.os != 'macOS' && 'target/release' || '' }}
key: ${{ steps.rust-cache.outputs.cache-key }}
- name: Upload native compilation timings
if: always()
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: native-timings-${{ matrix.os }}
@@ -130,12 +170,13 @@ jobs:
# 34856491552. Both jobs remain part of this reusable workflow's result, so
# the stable release aggregation still requires every check to succeed.
macos-release-check:
needs: lint
if: ${{ inputs.windows_diagnostics_run == '' }}
name: Release workspace (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [macos-26, macos-26-intel]
include: ${{ fromJSON(needs.lint.outputs.release_matrix) }}
runs-on: ${{ matrix.os }}
timeout-minutes: 120
env:
@@ -145,6 +186,8 @@ jobs:
RUST_TOOLCHAIN: "1.97.1"
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.13"
@@ -170,7 +213,7 @@ jobs:
path: target/release
key: ${{ steps.rust-cache.outputs.cache-key }}
- name: Upload release check compilation timings
if: always()
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: release-check-timings-${{ matrix.os }}
+81
View File
@@ -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.`);
+1 -20
View File
@@ -4,26 +4,7 @@ run-name: >-
Cross-platform Preview (${{ inputs.preview_id || format('run-{0}', github.run_number) }})
on:
pull_request:
# Native tests cover every PR; package only when package inputs change.
paths:
- "**/Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- "nebula_app/build.rs"
- "nebula_app/build/**"
- "nebula_app/res/**"
- "nebula_app/windows/**"
- "assets/**"
- "packaging/**"
- "scripts/package*"
- "scripts/build-*"
- "scripts/installer*"
- "scripts/prepare-windows-runtime.ps1"
- "scripts/preview_release.py"
- "scripts/conformance/**"
- "scripts/tests/test_preview_release.py"
- ".github/workflows/preview-packages.yml"
# Pull requests use Full native tests; packages are built after merge or on request.
push:
branches: [main]
# Native tests cover every PR; package only when package inputs change.
+20 -6
View File
@@ -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
@@ -105,9 +107,11 @@ cargo check -p nebula --bin pebrel --features gpui-shell --tests --locked
`Full native tests` runs on every PR and merge-group update, and on `main` pushes.
It tests the full workspace on Linux, Windows x64 / ARM64 and both macOS
architectures. New commits cancel obsolete PR runs. Package validation has its own
input filters and can also be dispatched manually; a package job does not replace
the native test suite. These triggers do not configure required status checks.
architectures. New commits cancel obsolete PR runs. Pull requests run tests and
compile checks without building distribution packages. Package validation runs
after matching changes reach `main`, or by explicit manual dispatch; a package
job does not replace the native test suite. These triggers do not configure
required status checks.
GitHub may show **Waiting for approval** for a first-time fork contributor. A
maintainer must inspect the submitted changes and approve that workflow run from
@@ -122,9 +126,17 @@ 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`, `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 tests plus the Apple Silicon release-profile compile check. The
required lint job validates the event and selects the matrix before requesting
platform runners. Drafts omit Intel Mac and Windows ARM64 jobs entirely, so their
required contexts may be missing until the PR is marked ready. Ready PRs run the
full matrix; every required check must succeed before merging. The ready event
still reruns core validation rather than reusing an earlier check conclusion.
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 +149,8 @@ feature work; there is no routine `--skip-architecture` option.
## 中文摘要
- 先读架构图、工程合同和决策记录;按职责拆分,不按行号切片。
- 一个 PR 只做一件事;改动超过 1500 行源码(不计文档、lockfile、资源)`pr-size` 会失败,请拆分。
- Draft PR 先运行必需的格式检查和矩阵规划,再创建 Linux / Windows x64 / Apple Silicon 测试及 Apple Silicon release 编译检查;不创建 Intel Mac / Windows ARM64 任务。Ready 后运行完整矩阵,十项必需检查全绿才能合并。
- 2000 行是现有仓库的防灾上限,800 行只提示审查,不是“大厂标准”。
- 普通功能 PR 不得增加存量债务;有问题的规则可以修订,但要有反例、测试和维护者审批。
- 新增核心抽象、依赖方向、持久化或线程模型改变要先说明设计,不强迫每个小修复写 ADR。
@@ -0,0 +1,67 @@
# Keep automatic package builds out of pull-request checks
## Status
Accepted by the maintainer on 2026-09-23: pull requests run tests and compile
checks; distribution packages are built after merge or on explicit request.
## Context
Preview packaging subscribed to pull requests that changed package inputs.
A Windows installer-menu change therefore started Linux and both macOS package
builds alongside the independent native test matrix. These workflows requested
the same macOS runner types while required checks were waiting for runners.
## Evidence
- [PR #264](https://github.com/Kuddev/pebrel/pull/264) changed Windows installer
scripts and triggered [Preview run 35825643107](https://github.com/Kuddev/pebrel/actions/runs/35825643107).
- The Preview macOS jobs ran workspace tests, dialog tests and an optimized
product build before creating DMGs. The native workflow independently ran
workspace tests and checked the production feature graph.
- The required `Release workspace` jobs execute `cargo check --release`; they
do not generate distribution packages and remain required compile checks.
- Runner waiting is observable. The exact hosted-runner quota or scheduling
policy responsible for a particular delay is not established by these logs.
## Decision
Remove the `pull_request` trigger from `preview-packages.yml`. Keep its filtered
`main` push trigger and explicit `workflow_dispatch`, including all package and
runtime conformance validation. Public Preview publication still requires an
explicit dispatch with publication enabled. Stable release triggers are retained.
The native PR and merge-group workflows retain their test coverage. This change
does not alter required-check names or server-side protection rules.
## Rejected alternatives
- Cancel required macOS checks to merge sooner: loses required validation.
- Treat the release-profile compile checks as package creation: these commands
validate a different build profile without creating installers or archives.
- Remove package conformance tests: package behavior still needs verification
when a package is actually requested.
## Consequences
Pull requests no longer automatically provide Preview installation artifacts.
Maintainers can explicitly dispatch package validation for a candidate branch.
Already queued workflows must be handled separately; changing a trigger cannot
retroactively cancel them. This change does not remove duplicate native runs
when a draft becomes ready, or guarantee a fixed runner wait time.
## Validation
The native-suite workflow contract verifies that Preview has only filtered main
push and manual triggers, that stable release remains tag/manual, and that native
PR and merge-group checks remain unfiltered. Restoring the previous PR package
trigger must fail the new regression.
## Supersedes
The previous automatic Preview build policy for pull-request package-input changes.
## Revisit when
An explicit request for per-PR artifacts can be implemented with bounded platform
selection and a separate runner budget without delaying mandatory validation.
@@ -0,0 +1,77 @@
# Plan the native matrix before requesting runners
## Status
Implemented for review. Preserves the existing ten required check names and
ready-PR platform coverage; server-side rules are not changed.
## Context
The initial draft tier skipped individual steps on Intel Mac and Windows ARM64
jobs. GitHub still assigned runners before skipping those steps. In PR #257's
draft run 35820869856, the Intel test job waited 309 seconds before spending
eight seconds on a job whose test steps were skipped.
## Evidence
`runs-on` belongs to a job, while the old draft conditions belonged to its steps.
Avoiding the platform command does not avoid allocating that job's runner.
The required `lint` check already runs on Ubuntu and can validate both formatting
and the platform plan before expensive jobs are created.
## Decision
`scripts/ci_plan.py` is the authority for platform selection. It reads the event
JSON and produces native-test and release-compile matrices. Draft PRs select the
three existing core platforms and the Apple Silicon release check. Ready PRs,
main pushes, merge groups and release callers retain all five native platforms
and both macOS release checks.
The existing required `lint` job owns planning, after rustfmt. Native and release
jobs depend on that job and consume its validated outputs. Invalid input fails
the required check rather than silently returning an empty matrix. Checkouts
explicitly discard Git credentials; the workflow retains read-only permissions.
Draft-only deferred checks are absent, not reported as successful executions.
A ready PR must still supply every required platform result. This change does
not copy previous runs' check conclusions. Cargo caches accelerate rebuilding;
they are not evidence that the current validation succeeded.
## Rejected alternatives
- Keep step-only skipping: retains the unnecessary runner allocation.
- Move planning into an unrequired job: its failure could be less visible than
a failure in an existing required check, especially on repeated runs.
- Remove release-profile checks with Preview packaging: `cargo check --release`
verifies compilation and does not create installation packages.
- Mirror an earlier green check onto a new run: requires independently verified
source, base, workflow, configuration and provenance rules; cache hits alone
cannot establish those facts.
## Consequences
Heavy jobs begin after the Ubuntu lint/planning job. Formatting errors and bad
event metadata stop before requesting platform runners. Draft PR pages may show
deferred required contexts as missing; drafts are not mergeable, and the ready
event runs the full matrix. Ready transitions still rerun core validation.
This reduces avoidable runner requests without promising a fixed queue time.
The existing Windows diagnostic dispatch continues to use its separate workflow.
## Validation
Planner tests cover draft and ready PRs, all full-validation caller events,
malformed input and real command-line output. Workflow contract tests require the
existing lint check to own planning, both matrices to depend on it, draft step
guards to be absent, and release compilation to remain present. Native PR and
merge-group coverage and explicit failure propagation retain their regressions.
Actual hosted-runner execution remains subject to the PR's required CI results.
## Supersedes
Step-level draft skipping in PR #257's initial native matrix.
## Revisit when
Queue and execution measurements justify a different platform schedule, or a
separately reviewed artifact-provenance design supports build-once test shards.
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Plan the native CI matrices before GitHub creates runner jobs.
The output file is the path normally supplied as ``GITHUB_OUTPUT``. Planning
is deliberately separate from workflow steps: a draft pull request therefore
does not materialize jobs for the scarce platforms.
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Sequence
@dataclass(frozen=True)
class Platform:
runner: str
draft: bool
release_check: bool = False
# One platform catalog owns both draft eligibility and release compilation.
PLATFORMS = (
Platform("ubuntu-24.04", draft=True),
Platform("windows-2022", draft=True),
Platform("macos-26", draft=True, release_check=True),
Platform("windows-11-arm", draft=False),
Platform("macos-26-intel", draft=False, release_check=True),
)
FULL_EVENTS = frozenset(
{"push", "merge_group", "workflow_dispatch", "workflow_call", "schedule"}
)
class PlanError(ValueError):
"""An event cannot be converted into a safe CI plan."""
def _matrices(draft: bool) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
selected = [platform for platform in PLATFORMS if not draft or platform.draft]
return (
[{"os": platform.runner} for platform in selected],
[{"os": platform.runner} for platform in selected if platform.release_check],
)
def _reject_json_constant(value: str) -> None:
raise ValueError(f"non-standard JSON constant {value}")
def _read_event(path: Path) -> dict[str, Any]:
try:
raw = path.read_text(encoding="utf-8")
except (OSError, UnicodeError) as exc:
raise PlanError(f"cannot read event payload {path}: {exc}") from exc
try:
payload = json.loads(raw, parse_constant=_reject_json_constant)
except (json.JSONDecodeError, ValueError) as exc:
raise PlanError(f"invalid event JSON in {path}: {exc}") from exc
if not isinstance(payload, dict):
raise PlanError("event payload must be a JSON object")
return payload
def plan_matrices(event_name: str, payload: dict[str, Any]) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
"""Return native and release matrices for one validated event."""
if event_name == "pull_request":
pull_request = payload.get("pull_request")
if not isinstance(pull_request, dict):
raise PlanError("pull_request event must contain a pull_request object")
draft = pull_request.get("draft")
if not isinstance(draft, bool):
raise PlanError("pull_request.draft must be a boolean")
return _matrices(draft)
if event_name in FULL_EVENTS:
return _matrices(draft=False)
supported = sorted(FULL_EVENTS | {"pull_request"})
raise PlanError(f"unsupported event name {event_name!r}; expected one of {', '.join(supported)}")
def _compact_output(name: str, value: list[dict[str, str]]) -> str:
return f"{name}={json.dumps(value, separators=(',', ':'), ensure_ascii=True)}"
def write_outputs(path: Path, native_matrix: list[dict[str, str]], release_matrix: list[dict[str, str]]) -> None:
"""Append both outputs in one write after planning has fully succeeded."""
content = "\n".join(
(
_compact_output("native_matrix", native_matrix),
_compact_output("release_matrix", release_matrix),
)
) + "\n"
try:
with path.open("a", encoding="utf-8", newline="\n") as output:
output.write(content)
except (OSError, UnicodeError) as exc:
raise PlanError(f"cannot append CI plan to {path}: {exc}") from exc
def _arguments(argv: Sequence[str] | None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--event-name", required=True)
parser.add_argument("--event-path", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
return parser.parse_args(argv)
def main(argv: Sequence[str] | None = None) -> int:
arguments = _arguments(argv)
try:
payload = _read_event(arguments.event_path)
native_matrix, release_matrix = plan_matrices(arguments.event_name, payload)
write_outputs(arguments.output, native_matrix, release_matrix)
except PlanError as exc:
print(f"ci_plan: {exc}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
+48 -1
View File
@@ -11,6 +11,48 @@ from scripts.ci_native_tests import main, native_commands
class NativeSuiteTests(unittest.TestCase):
def test_required_lint_plans_runners_before_native_jobs(self):
root = Path(__file__).resolve().parents[2]
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
lint = workflow.split("\n lint:\n", 1)[1].split("\n native-tests:\n", 1)[0]
self.assertIn("name: lint", lint)
self.assertIn("python scripts/ci_plan.py", lint)
self.assertIn('--event-path "$GITHUB_EVENT_PATH"', lint)
self.assertLess(lint.index("cargo fmt"), lint.index("python scripts/ci_plan.py"))
for job, output in (("native-tests", "native_matrix"),
("macos-release-check", "release_matrix")):
body = workflow.split(f"\n {job}:\n", 1)[1]
body = re.split(r"\n [a-z][a-z-]*:\n", body, maxsplit=1)[0]
self.assertIn("needs: lint", body)
self.assertIn(f"fromJSON(needs.lint.outputs.{output})", body)
self.assertNotIn("pull_request.draft", body)
self.assertNotIn("matrix.tier", body)
self.assertIn("cargo check --locked --workspace --release", workflow)
# Native validation never needs to retain checkout credentials.
checkouts = re.findall(r"- uses: actions/checkout@[^\n]+\n(.*?)(?= - |\Z)",
workflow, re.S)
self.assertTrue(checkouts)
for checkout in checkouts:
self.assertIn("persist-credentials: false", checkout)
def test_packages_run_after_merge_or_manual_dispatch_not_for_prs(self):
root = Path(__file__).resolve().parents[2]
workflow = (root / ".github/workflows/preview-packages.yml").read_text()
events = workflow.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0]
triggers = set(re.findall(r"^ ([a-z_]+):", events, re.M))
self.assertEqual(triggers, {"push", "workflow_dispatch"})
push = re.search(r"^ push:(.*?)(?=^ [a-z_]+:|\Z)", events, re.M | re.S)
self.assertIsNotNone(push)
self.assertIn("branches: [main]", push.group(1))
self.assertIn("paths:", push.group(1))
# Packaging still needs explicit dispatch to create a public release.
self.assertIn("github.event_name == 'workflow_dispatch' && inputs.publish == true", workflow)
stable = (root / ".github/workflows/release.yml").read_text()
stable_events = stable.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0]
self.assertEqual(set(re.findall(r"^ ([a-z_]+):", stable_events, re.M)),
{"push", "workflow_dispatch"})
self.assertIn('tags: ["v*.*.*"]', stable_events)
def test_every_pr_and_merge_group_runs_without_path_exclusions(self):
root = Path(__file__).resolve().parents[2]
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
@@ -20,7 +62,12 @@ class NativeSuiteTests(unittest.TestCase):
self.assertIsNotNone(declaration)
self.assertNotIn("paths", declaration.group(1))
self.assertNotIn("branches", declaration.group(1))
self.assertNotIn("types", declaration.group(1))
if event == "pull_request":
# ready_for_review starts the scarce-runner jobs that draft
# pushes skip; the default activity types must stay listed.
self.assertIn("types: [opened, synchronize, reopened, ready_for_review]", declaration.group(1))
else:
self.assertNotIn("types", declaration.group(1))
self.assertIn("branches: [main]", events)
self.assertNotIn("branches-ignore", events)
self.assertNotIn("pull_request_target", workflow)
+172
View File
@@ -0,0 +1,172 @@
from __future__ import annotations
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
SCRIPT = Path(__file__).resolve().parents[1] / "ci_plan.py"
class CiPlanCliTests(unittest.TestCase):
def run_cli(self, event_name: str, payload: object, output: Path) -> subprocess.CompletedProcess[str]:
event_path = output.parent / "event.json"
event_path.write_text(json.dumps(payload), encoding="utf-8")
return subprocess.run(
[
sys.executable,
str(SCRIPT),
"--event-name",
event_name,
"--event-path",
str(event_path),
"--output",
str(output),
],
text=True,
capture_output=True,
check=False,
)
@staticmethod
def outputs(path: Path) -> dict[str, object]:
lines = path.read_text(encoding="utf-8").splitlines()
return {name: json.loads(value) for name, value in (line.split("=", 1) for line in lines)}
def test_draft_pr_does_not_materialize_scarce_runner_rows(self) -> None:
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "github-output"
result = self.run_cli("pull_request", {"pull_request": {"draft": True}}, output)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
output.read_text(encoding="utf-8"),
'native_matrix=[{"os":"ubuntu-24.04"},{"os":"windows-2022"},{"os":"macos-26"}]\n'
'release_matrix=[{"os":"macos-26"}]\n',
)
plan = self.outputs(output)
self.assertNotIn({"os": "windows-11-arm"}, plan["native_matrix"])
self.assertNotIn({"os": "macos-26-intel"}, plan["native_matrix"])
def test_ready_pr_has_all_native_and_release_checks(self) -> None:
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "github-output"
result = self.run_cli("pull_request", {"pull_request": {"draft": False}}, output)
self.assertEqual(result.returncode, 0, result.stderr)
plan = self.outputs(output)
self.assertEqual(
[row["os"] for row in plan["native_matrix"]],
["ubuntu-24.04", "windows-2022", "macos-26", "windows-11-arm", "macos-26-intel"],
)
self.assertEqual(
[row["os"] for row in plan["release_matrix"]],
["macos-26", "macos-26-intel"],
)
def test_success_appends_outputs_without_replacing_existing_lines(self) -> None:
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "github-output"
output.write_text("existing=preserved\n", encoding="utf-8")
result = self.run_cli("push", {}, output)
self.assertEqual(result.returncode, 0, result.stderr)
lines = output.read_text(encoding="utf-8").splitlines()
self.assertEqual(lines[0], "existing=preserved")
self.assertTrue(lines[1].startswith("native_matrix=[{"))
self.assertTrue(lines[2].startswith("release_matrix=[{"))
def test_merge_group_and_non_pr_events_have_full_coverage(self) -> None:
events = ("merge_group", "push", "workflow_dispatch", "workflow_call", "schedule")
with tempfile.TemporaryDirectory() as directory:
for event_name in events:
with self.subTest(event_name=event_name):
output = Path(directory) / f"{event_name}.out"
result = self.run_cli(event_name, {}, output)
self.assertEqual(result.returncode, 0, result.stderr)
plan = self.outputs(output)
self.assertEqual(
[row["os"] for row in plan["native_matrix"]],
["ubuntu-24.04", "windows-2022", "macos-26", "windows-11-arm", "macos-26-intel"],
)
self.assertEqual(
[row["os"] for row in plan["release_matrix"]],
["macos-26", "macos-26-intel"],
)
def test_invalid_events_fail_without_matrix_outputs(self) -> None:
cases = (
("pull_request", {}, "missing pull request"),
("pull_request", {"pull_request": []}, "wrong pull request type"),
("pull_request", {"pull_request": {}}, "missing draft"),
("pull_request", {"pull_request": {"draft": "false"}}, "wrong draft type"),
("pull_request", {"pull_request": {"draft": 0}}, "integer draft type"),
("unknown", {}, "unknown event"),
("push", [], "non-object payload"),
)
with tempfile.TemporaryDirectory() as directory:
for event_name, payload, label in cases:
with self.subTest(label=label):
output = Path(directory) / f"{label.replace(' ', '-')}.out"
output.write_text("existing=preserved\n", encoding="utf-8")
result = self.run_cli(event_name, payload, output)
self.assertNotEqual(result.returncode, 0)
self.assertIn("ci_plan:", result.stderr)
self.assertEqual(output.read_text(encoding="utf-8"), "existing=preserved\n")
def test_malformed_json_fails_without_creating_output(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
event_path = root / "broken.json"
output = root / "github-output"
event_path.write_text('{"pull_request":', encoding="utf-8")
result = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--event-name",
"pull_request",
"--event-path",
str(event_path),
"--output",
str(output),
],
text=True,
capture_output=True,
check=False,
)
self.assertNotEqual(result.returncode, 0)
self.assertFalse(output.exists())
def test_non_standard_json_constant_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
event_path = root / "non-standard.json"
output = root / "github-output"
event_path.write_text('{"value":NaN}', encoding="utf-8")
result = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--event-name",
"push",
"--event-path",
str(event_path),
"--output",
str(output),
],
text=True,
capture_output=True,
check=False,
)
self.assertNotEqual(result.returncode, 0)
self.assertFalse(output.exists())
if __name__ == "__main__":
unittest.main()
+16 -2
View File
@@ -16,6 +16,7 @@ from scripts.stable_release import (
)
from scripts.preview_release import sha256
from scripts.preview_release import MIN_ASSET_SIZE
from scripts.ci_plan import plan_matrices
VERSION = "1.6.0"
@@ -78,8 +79,21 @@ class StableReleaseTests(unittest.TestCase):
self.assertNotIn(" native-tests:\n", workflow)
self.assertNotIn("uses: ./.github/workflows/linux-lua.yml", workflow)
shared = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
for platform in ("ubuntu-24.04", "windows-2022", "windows-11-arm", "macos-26", "macos-26-intel"):
self.assertIn(platform, shared)
# Platform coverage comes from the event plan, not literal runner names
# in YAML. Verify both the consumer wiring and the full caller matrices.
for output in ("native_matrix", "release_matrix"):
self.assertIn(f"fromJSON(needs.lint.outputs.{output})", shared)
self.assertIn("runs-on: ${{ matrix.os }}", shared)
for event in ("push", "merge_group", "workflow_call", "workflow_dispatch"):
with self.subTest(event=event):
native, release = plan_matrices(event, {})
self.assertCountEqual(
[row["os"] for row in native],
["ubuntu-24.04", "windows-2022", "windows-11-arm", "macos-26", "macos-26-intel"],
)
self.assertCountEqual(
[row["os"] for row in release], ["macos-26", "macos-26-intel"],
)
for trigger in ("pull_request:", "merge_group:", "branches: [main]"):
self.assertIn(trigger, shared)
self.assertIn("run: python scripts/ci_native_tests.py", shared)