From 56371733639ea674cb2230acbf1ba4c8cbb5e910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 11 Aug 2026 12:36:09 +0200 Subject: [PATCH 01/16] nixbot: bump --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index c00a78e76..dcc699414 100644 --- a/flake.lock +++ b/flake.lock @@ -787,10 +787,10 @@ ] }, "locked": { - "lastModified": 1785671702, - "narHash": "sha256-wwhItmpu4E7FrdvFQZwBXkTCN1hnm8QuT/y712MF+e8=", + "lastModified": 1786443311, + "narHash": "sha256-e9QA8SLpZyZYVK/K1NJbsMpog1zVwCvvv+0YrC//w8U=", "ref": "main", - "rev": "5eabc926b26f45d014d062ac47b4511c4e0cf197", + "rev": "b631afc8aa6bac2e3aaa459f4ebad171cd7cadfe", "shallow": true, "type": "git", "url": "https://github.com/Mic92/nixbot" From 3d5901f8e84aa27ccc077628c0031ecd82bb0f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 12 Aug 2026 23:10:09 +0200 Subject: [PATCH 02/16] flake: track nixbot main again The per-effect rerun feature landed on main, so the temporary race-condition branch pin is no longer needed. --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index dcc699414..149589c12 100644 --- a/flake.lock +++ b/flake.lock @@ -787,10 +787,10 @@ ] }, "locked": { - "lastModified": 1786443311, - "narHash": "sha256-e9QA8SLpZyZYVK/K1NJbsMpog1zVwCvvv+0YrC//w8U=", + "lastModified": 1786448557, + "narHash": "sha256-TovuKRVqImRcX02gh9a1xKGv1nB3omhjrnP6wSTpUec=", "ref": "main", - "rev": "b631afc8aa6bac2e3aaa459f4ebad171cd7cadfe", + "rev": "6f611e81b579266415fbf4be68b1f6c04ded2465", "shallow": true, "type": "git", "url": "https://github.com/Mic92/nixbot" From 402d6895ed60ff2627e6404fd78f4042bf555889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 13 Aug 2026 14:44:58 +0200 Subject: [PATCH 03/16] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'git+https://github.com/Mic92/nixpkgs?ref=main&rev=f7215b712f4a9fdf123ed271f0f099e2e0d9d6bb&shallow=1' (2026-08-06) → 'git+https://github.com/Mic92/nixpkgs?ref=main&rev=60b17b70d5afcf24ec6fb4f52da4373e8cdc96a8&shallow=1' (2026-08-13) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 149589c12..4e9717469 100644 --- a/flake.lock +++ b/flake.lock @@ -824,10 +824,10 @@ }, "nixpkgs": { "locked": { - "lastModified": 1786009831, - "narHash": "sha256-snb+ERnKgQNvvxyxXiAcImgqen4HWec/Lyo4Dx41lxU=", + "lastModified": 1786625075, + "narHash": "sha256-8RAlfKTt8wDg8Xv7FmU8KWTK0/GWtG3lqHPXKuYnXyY=", "ref": "main", - "rev": "f7215b712f4a9fdf123ed271f0f099e2e0d9d6bb", + "rev": "60b17b70d5afcf24ec6fb4f52da4373e8cdc96a8", "shallow": true, "type": "git", "url": "https://github.com/Mic92/nixpkgs" From 43b725f738c115468813de3106d403d087d88f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 13 Aug 2026 14:56:07 +0200 Subject: [PATCH 04/16] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nix': 'git+https://github.com/Mic92/nix-1?ref=refs/heads/main&rev=cd1729d98a3f247e5f2d28028779ab6c400b2d97&shallow=1' (2026-07-26) → 'git+https://github.com/Mic92/nix-1?ref=refs/heads/main&rev=7c72bf27e73207631ae4d65dac6b836ec6446752&shallow=1' (2026-08-13) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 4e9717469..f795cac9e 100644 --- a/flake.lock +++ b/flake.lock @@ -608,10 +608,10 @@ "nixpkgs-regression": [] }, "locked": { - "lastModified": 1785066688, - "narHash": "sha256-4QykV6vLkLg2SqnAgA9lvpCEFWNy76MlHFIuA3v0wHc=", + "lastModified": 1786625762, + "narHash": "sha256-uJbU5muxrw4LBqMxNLz4E3x30SWNS/f7D/Jv4wOcWYE=", "ref": "refs/heads/main", - "rev": "cd1729d98a3f247e5f2d28028779ab6c400b2d97", + "rev": "7c72bf27e73207631ae4d65dac6b836ec6446752", "shallow": true, "type": "git", "url": "https://github.com/Mic92/nix-1" From 50e556f7af6356cec3828cd27cd5b81eedba2647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 13 Aug 2026 16:17:49 +0200 Subject: [PATCH 05/16] bump gitea-mq --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index f795cac9e..31bce0894 100644 --- a/flake.lock +++ b/flake.lock @@ -350,11 +350,11 @@ ] }, "locked": { - "lastModified": 1785987514, - "narHash": "sha256-kwhLzTSFnBUduhpdDIY153WbLGy7wBzDlLFVYf6QDHM=", + "lastModified": 1786630637, + "narHash": "sha256-avagxcO0xO+h0CsygYY1YZLqDCpD/PNYlIUW8ifc+Pw=", "owner": "Mic92", "repo": "gitea-mq", - "rev": "9f42192515d2cd6c20ab0d058bddff8cb5a98d4a", + "rev": "e89cace8e474f18986ac4c762b922eca5c34553b", "type": "github" }, "original": { From 4355883493506703fffda34ea1304a9890f893fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 13 Aug 2026 16:20:13 +0200 Subject: [PATCH 06/16] bump nixbot --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 31bce0894..858b48c16 100644 --- a/flake.lock +++ b/flake.lock @@ -787,10 +787,10 @@ ] }, "locked": { - "lastModified": 1786448557, - "narHash": "sha256-TovuKRVqImRcX02gh9a1xKGv1nB3omhjrnP6wSTpUec=", + "lastModified": 1786627282, + "narHash": "sha256-Jk/AFmYF/J+5ktxSER4I4HgYaUrx7vgebY03oTp+f2Y=", "ref": "main", - "rev": "6f611e81b579266415fbf4be68b1f6c04ded2465", + "rev": "a5d2de8e747690f13defc573da6b1a8613a4306b", "shallow": true, "type": "git", "url": "https://github.com/Mic92/nixbot" From 6638ee9737d96077e0764ca4bace0cb0d7a46c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 13 Aug 2026 16:23:00 +0200 Subject: [PATCH 07/16] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'git+https://github.com/Mic92/nixpkgs?ref=main&rev=60b17b70d5afcf24ec6fb4f52da4373e8cdc96a8&shallow=1' (2026-08-13) → 'git+https://github.com/Mic92/nixpkgs?ref=main&rev=a5db4ee4c5f6884e4a7766df4922737af755e5e3&shallow=1' (2026-08-13) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 858b48c16..67c58d377 100644 --- a/flake.lock +++ b/flake.lock @@ -824,10 +824,10 @@ }, "nixpkgs": { "locked": { - "lastModified": 1786625075, - "narHash": "sha256-8RAlfKTt8wDg8Xv7FmU8KWTK0/GWtG3lqHPXKuYnXyY=", + "lastModified": 1786630947, + "narHash": "sha256-2uGj3oXJRPnOBmsQriSDRN6oAcahaexZ9LNJAH7uzWg=", "ref": "main", - "rev": "60b17b70d5afcf24ec6fb4f52da4373e8cdc96a8", + "rev": "a5db4ee4c5f6884e4a7766df4922737af755e5e3", "shallow": true, "type": "git", "url": "https://github.com/Mic92/nixpkgs" From c81e603f520fe74e74d380be736b3f72beed8f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 13 Aug 2026 17:23:52 +0200 Subject: [PATCH 08/16] bump nix --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 67c58d377..c7e4070f7 100644 --- a/flake.lock +++ b/flake.lock @@ -608,10 +608,10 @@ "nixpkgs-regression": [] }, "locked": { - "lastModified": 1786625762, - "narHash": "sha256-uJbU5muxrw4LBqMxNLz4E3x30SWNS/f7D/Jv4wOcWYE=", + "lastModified": 1786634418, + "narHash": "sha256-h/kVqwl77NU+P5JtFuPQypPDk/Kr74mm38dYSue7NiI=", "ref": "refs/heads/main", - "rev": "7c72bf27e73207631ae4d65dac6b836ec6446752", + "rev": "8529b7a30195c1f05f5009d16bd11b6577839daa", "shallow": true, "type": "git", "url": "https://github.com/Mic92/nix-1" From 9f6e4756990ad4c332333f24d6c3dea00f5b8c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 13 Aug 2026 21:27:35 +0200 Subject: [PATCH 09/16] update herdr patch --- flake.lock | 6 +++--- home-manager/modules/ai.nix | 21 +++---------------- ...e-ssh-transport-program-configurable.patch | 12 +++++------ 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/flake.lock b/flake.lock index c7e4070f7..be8700f3b 100644 --- a/flake.lock +++ b/flake.lock @@ -509,11 +509,11 @@ ] }, "locked": { - "lastModified": 1785144254, - "narHash": "sha256-Ctdbzhep1w6WphrNGDW7ke8PZa+FBROWhWSTjMAHY8o=", + "lastModified": 1786646823, + "narHash": "sha256-+vQKp9DQ7kKVIopa6U+XHtayQ9OrQvfW+N9xcscpGWo=", "owner": "numtide", "repo": "llm-agents.nix", - "rev": "7687a39e75b295da27b39a2271a7ae663e7c9b4c", + "rev": "49dcc8bd363ae864306896407fa352df4d48473f", "type": "github" }, "original": { diff --git a/home-manager/modules/ai.nix b/home-manager/modules/ai.nix index 0d56c1790..4f0177406 100644 --- a/home-manager/modules/ai.nix +++ b/home-manager/modules/ai.nix @@ -11,32 +11,17 @@ let micsSkillsPkgs = inputs.mics-skills.packages.${pkgs.stdenv.hostPlatform.system}; nixbot-cli = inputs.nixbot.packages.${pkgs.stdenv.hostPlatform.system}.nixbot-cli; - # herdr 0.7.5 drops kitty-protocol printable keys (#1746) and shifted prefix - # keybindings drop out of prefix mode (#1870); build master commit on Linux - # until the next release. + # On Darwin llm-agents ships the prebuilt release binary, so the source + # patch can only be applied on Linux. herdrPackage = if pkgs.stdenv.isDarwin then aiTools.herdr else - aiTools.herdr.overrideAttrs (old: rec { - version = "0.7.5-unstable-2026-07-29"; - src = pkgs.fetchFromGitHub { - owner = "ogulcancelik"; - repo = "herdr"; - rev = "73d92004f50d3f5fafe64e0f9b7fddbcf4d99965"; - hash = "sha256-SeBkJePTxcFhXzFgGiSXMlAY359bgf5aTgmywZJhuoM="; - }; + aiTools.herdr.overrideAttrs (old: { # remote.ssh_command config option; proposed upstream via discussion #1780. patches = (old.patches or [ ]) ++ [ ./herdr/0001-remote-make-ssh-transport-program-configurable.patch ]; - cargoDeps = pkgs.rustPlatform.fetchCargoVendor { - inherit src; - name = "herdr-${version}-vendor"; - hash = "sha256-Ja7fKsLWwCi6oy6zANltlFncbDVK+kgOhpr+bJtZyzg="; - }; - # versionCheckHook compares against the pre-release Cargo version. - doInstallCheck = false; }); in { diff --git a/home-manager/modules/herdr/0001-remote-make-ssh-transport-program-configurable.patch b/home-manager/modules/herdr/0001-remote-make-ssh-transport-program-configurable.patch index 21bed142f..34a249b55 100644 --- a/home-manager/modules/herdr/0001-remote-make-ssh-transport-program-configurable.patch +++ b/home-manager/modules/herdr/0001-remote-make-ssh-transport-program-configurable.patch @@ -1,4 +1,4 @@ -From 6d5109ec08467913a4e53fa3c1d8752705f0504d Mon Sep 17 00:00:00 2001 +From 7f65636f020511fe6f1781f58bb8feab11e7e63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 26 Jul 2026 10:43:42 +0200 Subject: [PATCH] remote: make ssh transport program configurable @@ -28,10 +28,10 @@ Signed-off-by: Jörg Thalheim 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/config/model.rs b/src/config/model.rs -index 9fbca3a..a9a6a26 100644 +index e56ac08..1d471f3 100644 --- a/src/config/model.rs +++ b/src/config/model.rs -@@ -866,12 +866,16 @@ pub struct RemoteConfig { +@@ -878,12 +878,16 @@ pub struct RemoteConfig { /// Add keepalive fallbacks and private connection reuse for `herdr --remote`. /// Set false to run plain ssh unchanged. Default: true. pub manage_ssh_config: bool, @@ -49,10 +49,10 @@ index 9fbca3a..a9a6a26 100644 } } diff --git a/src/main.rs b/src/main.rs -index a73423b..2e6f94c 100644 +index 27c1d92..0227cd2 100644 --- a/src/main.rs +++ b/src/main.rs -@@ -386,6 +386,9 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration +@@ -395,6 +395,9 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration # Set false to run plain ssh against your ssh config unchanged — this does not # force keepalive or multiplexing off, it only stops herdr from adding its own. # manage_ssh_config = true @@ -196,5 +196,5 @@ index 6fe927c..55a0764 100644 fn remote_install_stream_command_avoids_shell_c_wrapper() { let command = remote_install_stream_command("/home/a b/.local/bin/herdr.tmp.123"); -- -2.54.0 +2.55.0 From 1142867fe65a6308186358460d6500d557638622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 14 Aug 2026 08:54:47 +0200 Subject: [PATCH 10/16] merge-when-green: cleanup + switch to nixbot cli --- pkgs/merge-when-green/README.md | 160 +++---------------- pkgs/merge-when-green/merge-when-green.py | 180 ++++++---------------- 2 files changed, 68 insertions(+), 272 deletions(-) diff --git a/pkgs/merge-when-green/README.md b/pkgs/merge-when-green/README.md index e1c4152b0..d0a622101 100644 --- a/pkgs/merge-when-green/README.md +++ b/pkgs/merge-when-green/README.md @@ -1,154 +1,32 @@ # merge-when-green -Automated PR creation and merging for GitHub and Gitea with CI monitoring. - -## Features - -- 🚀 **Automatic PR Creation** - Creates pull requests from your current branch - with commit messages as description -- ♻️ **Smart PR Detection** - Detects and reuses existing PRs instead of failing -- 🔄 **Auto-merge** - Enables auto-merge and waits for CI checks to complete -- 📊 **Real-time CI Monitoring** - Live status updates with colored output - showing passed/failed/pending checks -- 🔍 **Buildbot Integration** - Automatically runs `buildbot-pr-check` when - checks fail (if available) -- 🎯 **Multi-platform** - Supports both GitHub and Gitea -- 🎨 **Code Formatting** - Runs `flake-fmt` and attempts to fix issues with - `git-absorb` -- 🔁 **Auto-rebase** - Rebases your branch after successful merge +Push the current branch, open a PR, enable auto-merge and wait until CI is +green. Works with GitHub (`gh`) and Gitea (`tea`). ## Usage ```bash -# Create PR and wait for CI to pass -merge-when-green - -# Create PR but don't wait for CI -merge-when-green --no-wait - -# Provide custom PR title and body -merge-when-green -m "Fix bug in auth\n\nThis fixes the authentication issue" +merge-when-green # create PR and wait for CI +merge-when-green --no-wait # create PR, don't wait +merge-when-green -m "title" # PR title/body from argument instead of $EDITOR ``` -## Requirements - -### GitHub - -- `gh` - GitHub CLI tool -- Repository with auto-merge enabled - -### Gitea - -- `tea` - Gitea CLI tool -- `GITEA_TOKEN` environment variable (for auto-merge) - -### Optional - -- `flake-fmt` - For code formatting -- `git-absorb` - For automatic commit fixing -- `lazygit` - Interactive git UI (used when formatting fails) -- `buildbot-pr-check` - For detailed CI failure information - ## Workflow -1. **Prepare Repository** - - Pulls latest changes from default branch - - Runs `flake-fmt` to check code formatting - - If formatting fails, attempts to fix with `git-absorb` +1. Rebases onto the default branch and runs `flake-fmt`. Formatting fixes are + folded into your commits with `git-absorb`; if that fails, `lazygit` opens. +2. Pushes the branch (on the default branch, a `merge-when-green-$USER` branch + is created) and creates a PR, or reuses an existing open one. The PR + description is taken from your commit messages, editable via `$EDITOR`. +3. Enables auto-merge and polls check status. On failing checks it runs + `nbo log` (nixbot CLI) to show failure logs, if installed. +4. After the merge, rebases your local branch onto the updated default branch. -2. **Create/Update PR** - - Checks if PR already exists for the branch - - If exists: enables auto-merge and continues - - If new: opens editor with commit messages to create PR description - -3. **Monitor CI** (unless `--no-wait`) - - **GitHub**: Shows real-time check status with colored output - ``` - [19:14:01] Checks - Passed: 5, Failed: 0, Pending: 3 - [19:14:11] Checks - Passed: 8, Failed: 0, Pending: 0 - ✓ PR successfully merged! - ``` - - **Gitea**: Simple polling every 30 seconds - - Automatically runs `buildbot-pr-check` when checks fail - -4. **Finalize** - - Waits for PR to be merged via auto-merge - - Fetches latest changes - - Rebases your branch onto the updated default branch - -## Output Examples - -### Successful Merge - -``` -Detected GitHub -Getting repository information... -Target branch: main - -Preparing changes... -✓ Code formatting check passed - -Pushing changes... - -✓ Using existing pull request -Enabling auto-merge... -✓ Auto-merge enabled - -Waiting for PR 'my-feature' to merge... -[19:14:01] Checks - Passed: 3, Failed: 0, Pending: 5 -[19:14:11] Checks - Passed: 6, Failed: 0, Pending: 2 -[19:14:21] Checks - Passed: 8, Failed: 0, Pending: 0 - -✓ PR successfully merged! -✓ Rebased onto latest changes -``` - -### Failed Checks - -``` -[19:14:01] Checks - Passed: 5, Failed: 2, Pending: 0 - -🔍 Running buildbot-pr-check to get detailed failure information... -[buildbot output here] - -✗ 2 checks failed -``` - -## Platform Detection - -The tool automatically detects whether you're using GitHub or Gitea by checking: - -1. GitHub: `gh repo view` command -2. Gitea: `tea repos list` command -3. Falls back to GitHub if neither works - -## Configuration - -### Gitea Auto-merge - -For Gitea, set the `GITEA_TOKEN` environment variable: - -```bash -export GITEA_TOKEN="your-token-here" -``` - -### Editor - -The tool uses `$EDITOR` (defaults to `vim`) to edit PR descriptions: - -```bash -export EDITOR="nano" -``` - -## Exit Codes - -- `0` - Success -- `1` - Error (formatting failed, no changes, PR closed, etc.) -- `130` - Interrupted by user (Ctrl+C) +## Requirements -## Notes +- GitHub: `gh`, repository with auto-merge enabled +- Gitea: `tea`, `GITEA_TOKEN` set (used to enable auto-merge via API) +- Optional: `flake-fmt`, `git-absorb`, `lazygit`, `nbo` -- If on the default branch, creates a new branch named `merge-when-green-$USER` -- Formatting issues open `lazygit` in interactive mode (if available) -- PR title/body defaults to aggregated commit messages from your branch -- Checks run every 10 seconds for GitHub, 30 seconds for Gitea +The platform is detected by trying `gh repo view` first, then +`tea repos list`. diff --git a/pkgs/merge-when-green/merge-when-green.py b/pkgs/merge-when-green/merge-when-green.py index d9049969f..4ca032e65 100755 --- a/pkgs/merge-when-green/merge-when-green.py +++ b/pkgs/merge-when-green/merge-when-green.py @@ -20,8 +20,6 @@ class Colors: - """ANSI color codes for terminal output.""" - BLUE = "\033[94m" GREEN = "\033[92m" YELLOW = "\033[93m" @@ -32,46 +30,33 @@ class Colors: class Platform(Enum): - """Git hosting platform.""" - GITHUB = "github" GITEA = "gitea" -def print_info(message: str) -> None: - """Print an informational message.""" - print(message) - - def print_success(message: str) -> None: - """Print a success message in green.""" print(f"{Colors.GREEN}{message}{Colors.RESET}") def print_error(message: str) -> None: - """Print an error message in red.""" print(f"{Colors.RED}{message}{Colors.RESET}") def print_warning(message: str) -> None: - """Print a warning message in yellow.""" print(f"{Colors.YELLOW}{message}{Colors.RESET}") def print_header(message: str) -> None: - """Print a header message in bold.""" print(f"\n{Colors.BOLD}{message}{Colors.RESET}") def print_subtle(message: str) -> None: - """Print a subtle message in gray.""" print(f"{Colors.GRAY}{message}{Colors.RESET}") def run( cmd: list[str], check: bool = True, capture: bool = False ) -> subprocess.CompletedProcess[str]: - """Run a command.""" if capture: result = subprocess.run(cmd, check=False, capture_output=True, text=True) if result.returncode != 0 and check: @@ -81,14 +66,11 @@ def run( def detect_platform() -> Platform: - """Detect platform: GitHub or Gitea.""" - # Try GitHub first result = run(["gh", "repo", "view", "--json", "name"], check=False, capture=True) if result.returncode == 0: print_subtle("Detected GitHub") return Platform.GITHUB - # Try Gitea result = run(["tea", "repos", "list", "--limit", "1"], check=False, capture=True) if result.returncode == 0: print_subtle("Detected Gitea") @@ -99,7 +81,6 @@ def detect_platform() -> Platform: def get_default_branch(platform: Platform) -> str: - """Get default branch.""" if platform == Platform.GITHUB: result = run( [ @@ -115,7 +96,6 @@ def get_default_branch(platform: Platform) -> str: ) return result.stdout.strip() - # Gitea: use git symbolic-ref result = run( ["git", "symbolic-ref", "refs/remotes/origin/HEAD"], check=False, capture=True ) @@ -143,7 +123,6 @@ def get_repo_info() -> tuple[str, str, str]: def check_pr_exists(branch: str, platform: Platform) -> bool: - """Check if a PR already exists for this branch.""" if platform == Platform.GITHUB: result = run( ["gh", "pr", "view", branch, "--json", "state"], @@ -158,7 +137,6 @@ def check_pr_exists(branch: str, platform: Platform) -> bool: except json.JSONDecodeError: pass else: - # Gitea result = run( ["tea", "pulls", "list", "--output", "json", "--state", "open"], check=False, @@ -202,9 +180,34 @@ def create_pr_github(branch: str, target: str, title: str, body: str) -> str: return branch +def gitea_enable_automerge(pr_index: str) -> None: + """Gitea has no CLI support for auto-merge, so use its REST API.""" + print_warning("Enabling auto-merge...") + api_url, owner, repo = get_repo_info() + token = os.environ.get("GITEA_TOKEN") + + url = f"{api_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_index}/merge" + headers = {"Content-Type": "application/json"} + if token: + headers["Authorization"] = f"token {token}" + + data = json.dumps( + { + "Do": "merge", + "merge_when_checks_succeed": True, + "delete_branch_after_merge": True, + } + ).encode() + + req = urllib.request.Request(url, data=data, headers=headers, method="POST") # noqa: S310 + try: + urllib.request.urlopen(req, timeout=10) # noqa: S310 + print_success("✓ Auto-merge enabled") + except (urllib.error.HTTPError, urllib.error.URLError) as e: + print_warning(f"Could not enable auto-merge: {e}") + + def create_pr_gitea(branch: str, target: str, title: str, body: str) -> str: - """Create Gitea PR and enable server-side auto-merge.""" - # Create PR result = run( [ "tea", @@ -231,51 +234,10 @@ def create_pr_gitea(branch: str, target: str, title: str, body: str) -> str: print_warning("Could not parse PR number, using branch name") return branch - # Enable auto-merge via API - print_warning("Enabling auto-merge...") - api_url, owner, repo = get_repo_info() - token = os.environ.get("GITEA_TOKEN") - - url = f"{api_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_index}/merge" - headers = {"Content-Type": "application/json"} - if token: - headers["Authorization"] = f"token {token}" - - data = json.dumps( - { - "Do": "merge", - "merge_when_checks_succeed": True, - "delete_branch_after_merge": True, - } - ).encode() - - req = urllib.request.Request(url, data=data, headers=headers, method="POST") # noqa: S310 - try: - urllib.request.urlopen(req, timeout=10) # noqa: S310 - print_success("✓ Auto-merge enabled") - except (urllib.error.HTTPError, urllib.error.URLError) as e: - print_warning(f"Could not enable auto-merge: {e}") - + gitea_enable_automerge(pr_index) return pr_index -def check_github_pr_state(pr_id: str) -> bool | None: - """Check GitHub PR state. Returns True if merged, False if closed, None if open.""" - result = run( - ["gh", "pr", "view", pr_id, "--json", "state"], check=False, capture=True - ) - if result.returncode != 0: - return None - pr_data = json.loads(result.stdout) - state = pr_data.get("state", "") - if state == "MERGED": - return True - if state == "CLOSED": - print_error("PR was closed") - return False - return None - - def check_gitea_pr_state(pr_id: str) -> bool | None: """Check Gitea PR state. Returns True if merged, False if closed, None if open.""" result = run( @@ -303,7 +265,6 @@ def check_gitea_pr_state(pr_id: str) -> bool | None: def count_check_states(checks: list[dict[str, Any]]) -> tuple[int, int, int]: - """Count check states from PR status checks.""" pending = failed = passed = 0 for check in checks: if check.get("__typename") == "CheckRun": @@ -351,11 +312,10 @@ def check_pr_completion( if failed > 0 and pending == 0: return False, f"{failed} checks failed" - return None # Still waiting + return None # still waiting def get_pr_status_github(pr_id: str) -> tuple[dict[str, Any] | None, str]: - """Get PR status from GitHub.""" result = run( [ "gh", @@ -442,28 +402,21 @@ def get_merge_queue_status_github(pr_number: int) -> tuple[bool, str | None]: return in_queue, desc -def run_buildbot_check_if_needed( - pr_data: dict[str, Any], failed: int, pending: int, buildbot_check_done: bool -) -> bool: - """Run buildbot-pr-check if needed.""" - if failed > 0 and pending == 0 and not buildbot_check_done: - pr_url = pr_data.get("url", "") - if pr_url and shutil.which("buildbot-pr-check"): - print_warning( - "\n🔍 Running buildbot-pr-check to get detailed failure information..." - ) - run(["buildbot-pr-check", pr_url], check=False) - print() # Add blank line after buildbot-pr-check output +def run_nixbot_log_if_needed(failed: int, pending: int, nixbot_log_done: bool) -> bool: + """Show CI failure logs via nbo (nixbot's CLI) once all checks finished.""" + if failed > 0 and pending == 0 and not nixbot_log_done: + if shutil.which("nbo"): + print_warning("\nRunning nbo log to get failure details...") + run(["nbo", "log"], check=False) + print() return True - return buildbot_check_done + return nixbot_log_done def wait_for_merge(platform: Platform, pr_id: str) -> bool: - """Wait for PR to be merged.""" print_header(f"Waiting for PR '{pr_id}' to merge...") if platform == Platform.GITEA: - # Gitea: simple polling while True: result = check_gitea_pr_state(pr_id) if result is not None: @@ -471,8 +424,7 @@ def wait_for_merge(platform: Platform, pr_id: str) -> bool: print(f"[{time.strftime('%H:%M:%S')}] Waiting...") time.sleep(30) - # GitHub: detailed check monitoring - buildbot_check_done = False + nixbot_log_done = False while True: pr_data, error = get_pr_status_github(pr_id) if pr_data is None: @@ -486,7 +438,6 @@ def wait_for_merge(platform: Platform, pr_id: str) -> bool: int(pr_data.get("number", 0)) ) - # Print status queue_suffix = ( f" {Colors.BLUE}[merge queue: {queue_desc}]{Colors.RESET}" if in_merge_queue @@ -500,12 +451,8 @@ def wait_for_merge(platform: Platform, pr_id: str) -> bool: f"{queue_suffix}" ) - # Run buildbot-pr-check if we have failing checks - buildbot_check_done = run_buildbot_check_if_needed( - pr_data, failed, pending, buildbot_check_done - ) + nixbot_log_done = run_nixbot_log_if_needed(failed, pending, nixbot_log_done) - # Check for completion completion = check_pr_completion( pr_data, pending, failed, in_merge_queue=in_merge_queue ) @@ -515,12 +462,10 @@ def wait_for_merge(platform: Platform, pr_id: str) -> bool: print_error(f"\n✗ {message}") return success - # Still waiting time.sleep(10) def get_pr_message_from_editor(default_branch: str) -> tuple[str, str]: - """Get PR title/body by opening editor with commit messages.""" remote = ( "upstream" if "upstream" in run(["git", "remote"], capture=True).stdout @@ -553,7 +498,7 @@ def get_pr_message_from_editor(default_branch: str) -> tuple[str, str]: def prepare_repository(default_branch: str) -> int: - """Prepare repository: pull, format check. Returns 0 if ready, 1 on error.""" + """Pull and format-check. Returns 0 if there is something to merge.""" print_header("Preparing changes...") # submodule.recurse=true makes `pull --rebase` return 128 when the current # branch introduces a new submodule that the base branch doesn't have yet — @@ -602,17 +547,14 @@ def prepare_repository(default_branch: str) -> int: def get_pr_message(message_arg: str | None, default_branch: str) -> tuple[str, str]: - """Get PR title and body from args or editor.""" if message_arg: lines = message_arg.split("\n", 1) - title = lines[0] - body = lines[1] if len(lines) > 1 else "" - return title, body + return lines[0], lines[1] if len(lines) > 1 else "" return get_pr_message_from_editor(default_branch) -def push_branch(branch_name: str, default_branch: str) -> str: - """Push branch and return the branch name to use for PR.""" +def push_branch(default_branch: str) -> str: + """Push HEAD and return the branch name to use for the PR.""" current_branch = run( ["git", "branch", "--show-current"], capture=True ).stdout.strip() @@ -628,14 +570,14 @@ def push_branch(branch_name: str, default_branch: str) -> str: def enable_automerge_existing_pr(branch_name: str, platform: Platform) -> str: - """Enable auto-merge on existing PR. Returns PR ID.""" - print_warning("Enabling auto-merge...") + """Enable auto-merge on an existing PR. Returns the PR ID.""" if platform == Platform.GITHUB: + print_warning("Enabling auto-merge...") run(["gh", "pr", "merge", branch_name, "--auto", "--rebase"]) print_success("✓ Auto-merge enabled") return branch_name - # Gitea: need to get the PR number first + # Gitea addresses PRs by number, so look it up from the branch name result = run( ["tea", "pulls", "list", "--output", "json", "--state", "open"], capture=True, @@ -645,28 +587,7 @@ def enable_automerge_existing_pr(branch_name: str, platform: Platform) -> str: for pr in prs: if pr.get("head", {}).get("ref") == branch_name: pr_id = str(pr["index"]) - # Enable auto-merge via API - api_url, owner, repo = get_repo_info() - token = os.environ.get("GITEA_TOKEN") - url = f"{api_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_id}/merge" - headers = {"Content-Type": "application/json"} - if token: - headers["Authorization"] = f"token {token}" - data = json.dumps( - { - "Do": "merge", - "merge_when_checks_succeed": True, - "delete_branch_after_merge": True, - } - ).encode() - req = urllib.request.Request( # noqa: S310 - url, data=data, headers=headers, method="POST" - ) - try: - urllib.request.urlopen(req, timeout=10) # noqa: S310 - print_success("✓ Auto-merge enabled") - except (urllib.error.HTTPError, urllib.error.URLError) as e: - print_warning(f"Could not enable auto-merge: {e}") + gitea_enable_automerge(pr_id) return pr_id except json.JSONDecodeError: print_warning("Could not parse PR list") @@ -674,7 +595,6 @@ def enable_automerge_existing_pr(branch_name: str, platform: Platform) -> str: def finalize_merge(platform: Platform, pr_id: str, default_branch: str) -> int: - """Wait for merge and rebase. Returns exit code.""" if wait_for_merge(platform, pr_id): print_success("\n✓ PR merged!") run(["git", "fetch", "origin", default_branch]) @@ -694,7 +614,6 @@ def chdir_repo_root() -> None: def main() -> int: - """Main entry point.""" parser = argparse.ArgumentParser(description="Create PR and merge when CI passes") parser.add_argument( "--no-wait", action="store_true", help="Don't wait for CI checks to complete" @@ -710,14 +629,13 @@ def main() -> int: print_header("Getting repository information...") default_branch = get_default_branch(platform) - print_info(f"Target branch: {Colors.BLUE}{default_branch}{Colors.RESET}") + print(f"Target branch: {Colors.BLUE}{default_branch}{Colors.RESET}") if prepare_repository(default_branch) != 0: return 1 - branch_name = push_branch("", default_branch) + branch_name = push_branch(default_branch) - # Check if PR already exists if check_pr_exists(branch_name, platform): print_success("✓ Using existing pull request") pr_id = enable_automerge_existing_pr(branch_name, platform) From aaa77e69a94ea71e0762ffc26f8b485fd488d6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 14 Aug 2026 08:54:55 +0200 Subject: [PATCH 11/16] claude.md: reformat --- home/.claude/CLAUDE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/home/.claude/CLAUDE.md b/home/.claude/CLAUDE.md index b53f180d1..74e979c6c 100644 --- a/home/.claude/CLAUDE.md +++ b/home/.claude/CLAUDE.md @@ -62,9 +62,9 @@ ## Running programs -- CRITICAL: ALWAYS use the `queue` skill for ANY command that might take - longer than 10 seconds (nix build, merge-when-green, test runs, make, - ninja, cargo) to avoid tool timeouts. +- CRITICAL: ALWAYS use the `queue` skill for ANY command that might take longer + than 10 seconds (nix build, merge-when-green, test runs, make, ninja, cargo) + to avoid tool timeouts. ## Search From 55033f522b580b7e2ced5686f31e733029e3ffb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 14 Aug 2026 08:54:47 +0200 Subject: [PATCH 12/16] merge-when-green: cleanup + switch to nixbot cli --- pkgs/merge-when-green/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkgs/merge-when-green/README.md b/pkgs/merge-when-green/README.md index d0a622101..92c26a6cb 100644 --- a/pkgs/merge-when-green/README.md +++ b/pkgs/merge-when-green/README.md @@ -28,5 +28,4 @@ merge-when-green -m "title" # PR title/body from argument instead of $EDITOR - Gitea: `tea`, `GITEA_TOKEN` set (used to enable auto-merge via API) - Optional: `flake-fmt`, `git-absorb`, `lazygit`, `nbo` -The platform is detected by trying `gh repo view` first, then -`tea repos list`. +The platform is detected by trying `gh repo view` first, then `tea repos list`. From 9187aae871034beb66cf7d6d182a4aa1fbbc52f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 14 Aug 2026 09:12:54 +0200 Subject: [PATCH 13/16] nix-eval-warnings: fix nix update --- .../src/nix_eval_warnings/__init__.py | 5 +++++ .../tests/test_nix_eval_warnings.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pkgs/nix-eval-warnings/src/nix_eval_warnings/__init__.py b/pkgs/nix-eval-warnings/src/nix_eval_warnings/__init__.py index 22fae6810..0622010bb 100644 --- a/pkgs/nix-eval-warnings/src/nix_eval_warnings/__init__.py +++ b/pkgs/nix-eval-warnings/src/nix_eval_warnings/__init__.py @@ -130,6 +130,11 @@ def extract_source(error_text: str, input_mapping: dict[str, str]) -> str: filepath = resolve_store_path(match.group(1), input_mapping) return f"{filepath}:{match.group(2)}" + # Nix >=2.34 lazy trees print sources as «flakeref»/rel/path.nix:line:col + match = re.search(r"at «[^»]+»/([^:]+\.nix):(\d+):\d+:", error_text) + if match: + return f"{match.group(1)}:{match.group(2)}" + # Fall back to "definitions from" patterns (for NixOS module warnings) matches = re.findall(r"definitions from `([^']+)'", error_text) for path in reversed(matches): diff --git a/pkgs/nix-eval-warnings/tests/test_nix_eval_warnings.py b/pkgs/nix-eval-warnings/tests/test_nix_eval_warnings.py index 88483f82f..d018086b2 100644 --- a/pkgs/nix-eval-warnings/tests/test_nix_eval_warnings.py +++ b/pkgs/nix-eval-warnings/tests/test_nix_eval_warnings.py @@ -212,6 +212,21 @@ def test_builtins_warn_extracts_source_with_line(self) -> None: assert w.source == "flake.nix:25" assert w.option_path == "" + def test_lazy_trees_source_with_flakeref(self) -> None: + """Nix >=2.34 lazy trees print sources as «flakeref»/rel/path.nix:l:c.""" + lines = [ + "evaluation warning: test warning message", + ( + '{"attr":"x86_64-linux.foo","error":"error: evaluation aborted\\n' + " at «git+file:///tmp/flake»/flake.nix:19:9:\\n" + ' 19| builtins.warn \\"m\\" x"}' + ), + ] + + results = [w for w in parse_nix_eval_output(lines, {}) if w] + assert len(results) == 1 + assert results[0].source == "flake.nix:19" + def test_nixos_module_warning_extracts_option_path(self) -> None: """Test parsing NixOS module warning extracts option path.""" error = ( From b1131843183d98fb370e521e90bb1bd7c49f0eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 14 Aug 2026 09:14:10 +0200 Subject: [PATCH 14/16] nix-eval-warnings: handle lazy-trees source locations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nix 2.34 with lazy trees prints stack-trace source locations as «flakeref»/rel/path.nix:line:col instead of a plain store path, so extract_source() matched nothing and warnings lost their source, breaking the package's own test suite in CI (build #395). Match the guillemet form and return the flake-relative path. Also annotate re.findall's result to fix a pre-existing mypy no-any-return error. --- pkgs/nix-eval-warnings/src/nix_eval_warnings/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/nix-eval-warnings/src/nix_eval_warnings/__init__.py b/pkgs/nix-eval-warnings/src/nix_eval_warnings/__init__.py index 0622010bb..ad798ec9f 100644 --- a/pkgs/nix-eval-warnings/src/nix_eval_warnings/__init__.py +++ b/pkgs/nix-eval-warnings/src/nix_eval_warnings/__init__.py @@ -89,7 +89,7 @@ def extract_error_message(error_text: str) -> str: Looks for the main error message after the stack trace. """ # Look for "error: " pattern - get the last one (most specific) - matches = re.findall(r"error:\s*(.+?)(?:\n|$)", error_text) + matches: list[str] = re.findall(r"error:\s*(.+?)(?:\n|$)", error_text) for match in reversed(matches): msg = match.strip() # Skip generic messages like "aborting" or stack trace fragments From 167b394d572c632600053c5c8890ce9f5fb8bdf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 14 Aug 2026 09:36:55 +0200 Subject: [PATCH 15/16] drop moonlight/sunshine for now --- machines/turingmachine/configuration.nix | 6 ---- machines/turingmachine/modules/packages.nix | 1 - machines/turingmachine/modules/sunshine.nix | 39 --------------------- 3 files changed, 46 deletions(-) delete mode 100644 machines/turingmachine/modules/sunshine.nix diff --git a/machines/turingmachine/configuration.nix b/machines/turingmachine/configuration.nix index 264f7126a..b4c9d445f 100644 --- a/machines/turingmachine/configuration.nix +++ b/machines/turingmachine/configuration.nix @@ -1,5 +1,4 @@ { - pkgs, lib, config, self, @@ -24,7 +23,6 @@ ./modules/postgresql.nix ../../nixosModules/tum-vpn ./modules/toggle-keyboard - ./modules/sunshine.nix ./modules/ghaf-hardware.nix ./modules/ghaf-facter.nix @@ -54,10 +52,6 @@ nixpkgs.pkgs = self.inputs.nixpkgs.legacyPackages.x86_64-linux; - environment.systemPackages = with pkgs; [ - sunshine - ]; - services.rustdesk-client.users = [ "joerg" ]; hardware.saleae-logic.enable = true; diff --git a/machines/turingmachine/modules/packages.nix b/machines/turingmachine/modules/packages.nix index fd099ce95..63456e8bb 100644 --- a/machines/turingmachine/modules/packages.nix +++ b/machines/turingmachine/modules/packages.nix @@ -5,6 +5,5 @@ environment.systemPackages = with pkgs; [ cntr ntfs3g - sunshine ]; } diff --git a/machines/turingmachine/modules/sunshine.nix b/machines/turingmachine/modules/sunshine.nix deleted file mode 100644 index 700e84dce..000000000 --- a/machines/turingmachine/modules/sunshine.nix +++ /dev/null @@ -1,39 +0,0 @@ -{ pkgs, ... }: -{ - environment.systemPackages = [ - pkgs.sunshine - pkgs.moonlight-qt # for testing purposes. - ]; - services.udev.packages = [ pkgs.sunshine ]; - networking.firewall = { - enable = true; - allowedTCPPorts = [ - 47984 - 47989 - 47990 - 48010 - ]; - allowedUDPPortRanges = [ - { - from = 47998; - to = 48000; - } - { - from = 8000; - to = 8010; - } - ]; - }; - # Prevents this error: - # Fatal: You must run [sudo setcap cap_sys_admin+p $(readlink -f sunshine)] for KMS display capture to work! - security.wrappers.sunshine = { - owner = "root"; - group = "root"; - capabilities = "cap_sys_admin+p"; - source = "${pkgs.sunshine}/bin/sunshine"; - }; - # Needed for network discovery - services.avahi.enable = true; - services.avahi.publish.enable = true; - services.avahi.publish.userServices = true; -} From 6d24e6261dda9903a104a2f562e984c7c62519f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 14 Aug 2026 09:37:02 +0200 Subject: [PATCH 16/16] drop punchcard for now --- flake.lock | 11 ++++++----- flake.nix | 4 +++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index be8700f3b..1b2f48ed5 100644 --- a/flake.lock +++ b/flake.lock @@ -909,15 +909,16 @@ ] }, "locked": { - "lastModified": 1784461432, - "narHash": "sha256-pLRbRLdMa0WJb1KvA2AFQW+rVOTv3ijh1NRn5Ap572k=", - "owner": "pinpox", + "lastModified": 1786692483, + "narHash": "sha256-CsPDMMHtXrOZ44a9rqm3QLkMLrGfLI1eWjEWOl489zM=", + "owner": "Mic92", "repo": "punchcard", - "rev": "e9c1ebe1429629615951e163166ffe60c22a0bd5", + "rev": "0bfcc08e7a61822e5f7c118994e716bc34009b56", "type": "github" }, "original": { - "owner": "pinpox", + "owner": "Mic92", + "ref": "lazy-oidc", "repo": "punchcard", "type": "github" } diff --git a/flake.nix b/flake.nix index df923b6af..157d8549a 100644 --- a/flake.nix +++ b/flake.nix @@ -104,7 +104,9 @@ forge-triage.url = "github:Mic92/forge-triage"; forge-triage.inputs.nixpkgs.follows = "nixpkgs"; - punchcard.url = "github:pinpox/punchcard"; + # TODO: switch back to pinpox/punchcard once + # https://github.com/pinpox/punchcard/pull/9 is merged + punchcard.url = "github:Mic92/punchcard/lazy-oidc"; punchcard.inputs.nixpkgs.follows = "nixpkgs"; herdr-eternal.url = "github:Mic92/herdr-eternal";