From d7b29837507537085049009e8b389dacae238361 Mon Sep 17 00:00:00 2001 From: donglovejava <211940267+donglovejava@users.noreply.github.com> Date: Fri, 29 May 2026 06:33:10 +0800 Subject: [PATCH 1/3] fix(tools): eagerly load all exec_shell companion tools --- crates/tui/src/core/engine/tool_catalog.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/tui/src/core/engine/tool_catalog.rs b/crates/tui/src/core/engine/tool_catalog.rs index 65b194ce03..3a44619e77 100644 --- a/crates/tui/src/core/engine/tool_catalog.rs +++ b/crates/tui/src/core/engine/tool_catalog.rs @@ -34,7 +34,11 @@ pub(super) const DEFAULT_ACTIVE_NATIVE_TOOLS: &[&str] = &[ "apply_patch", "checklist_write", "edit_file", + "exec_interact", "exec_shell", + "exec_shell_interact", + "exec_shell_wait", + "exec_wait", "fetch_url", "file_search", "git_diff", @@ -46,6 +50,8 @@ pub(super) const DEFAULT_ACTIVE_NATIVE_TOOLS: &[&str] = &[ "task_create", "task_list", "task_read", + "task_shell_start", + "task_shell_wait", "update_plan", "web_search", "write_file", From 1624c072932ffa92e21b555ab212fc11ec320505 Mon Sep 17 00:00:00 2001 From: donglovejava <211940267+donglovejava@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:07:24 +0800 Subject: [PATCH 2/3] fix(ui): reduce minimum terminal width for sidebar visibility The sidebar was only showing when terminal width >= 100 columns, which is too restrictive for many terminal setups. Reduced the minimum width to 60 columns to make the sidebar visible in more common terminal configurations. This fixes the issue where the sidebar would not appear in v0.8.62+ when using typical terminal sizes that are narrower than 100 columns. --- codewhale | 1 + crates/tui/src/tui/ui.rs | 2 +- fix-edit_file-fuzz.patch | 9 +++++++++ fix_engine.py | 39 +++++++++++++++++++++++++++++++++++++++ pr-body-agents.md | 33 +++++++++++++++++++++++++++++++++ pr-body-cjk.md | 27 +++++++++++++++++++++++++++ pr-body-fuzz.md | 9 +++++++++ pr-body-paste.md | 21 +++++++++++++++++++++ pr-body-rlm.md | 9 +++++++++ pr-body-voice.md | 32 ++++++++++++++++++++++++++++++++ pr-body.md | 30 ++++++++++++++++++++++++++++++ shell-live-progress.patch | 0 12 files changed, 211 insertions(+), 1 deletion(-) create mode 160000 codewhale create mode 100644 fix-edit_file-fuzz.patch create mode 100644 fix_engine.py create mode 100644 pr-body-agents.md create mode 100644 pr-body-cjk.md create mode 100644 pr-body-fuzz.md create mode 100644 pr-body-paste.md create mode 100644 pr-body-rlm.md create mode 100644 pr-body-voice.md create mode 100644 pr-body.md create mode 100644 shell-live-progress.patch diff --git a/codewhale b/codewhale new file mode 160000 index 0000000000..5dffecef40 --- /dev/null +++ b/codewhale @@ -0,0 +1 @@ +Subproject commit 5dffecef40a876cae8d1bdf27750fb48183ae76a diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index fb89de619a..14530bf30d 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -151,7 +151,7 @@ const DISPATCH_WATCHDOG_TIMEOUT: Duration = Duration::from_secs(30); // the per-tool spinner pulse — keep this fast enough that the spout reads as // motion (~12 fps) instead of teleport-frames. const UI_STATUS_ANIMATION_MS: u64 = 80; -const SIDEBAR_VISIBLE_MIN_WIDTH: u16 = 100; +const SIDEBAR_VISIBLE_MIN_WIDTH: u16 = 60; const DEFAULT_TERMINAL_PROBE_TIMEOUT_MS: u64 = 500; const PERIODIC_FULL_REPAINT_EVERY_N: u64 = 50; const TURN_META_PREFIX: &str = ""; diff --git a/fix-edit_file-fuzz.patch b/fix-edit_file-fuzz.patch new file mode 100644 index 0000000000..7d69aae87a --- /dev/null +++ b/fix-edit_file-fuzz.patch @@ -0,0 +1,9 @@ +The optional `fuzz` parameter was required to attempt the leading-indentation fuzzy fallback when exact search found zero matches. This forced the model to make two calls on every edit that needed fuzzy matching (first without fuzz -> error -> second with fuzz: true), causing a round-trip delay. + +Fix: remove the `fuzz` gate from the count == 0 branch. The tool now automatically retries with indentation-tolerant fuzzy matching when exact search produces no results. The `fuzz` parameter is kept in the schema for backward compatibility but marked deprecated. + +Changes: +- crates/tui/src/tools/file.rs: `if count == 0 && fuzz` -> `if count == 0` (always retry fuzzy fallback) +- crates/tui/src/tools/file.rs: removed dead `else if count == 0 { error }` branch +- crates/tui/src/tools/file.rs: updated description to note automatic fuzzy fallback +- crates/tui/src/tools/file.rs: marked fuzz parameter as deprecated in schema diff --git a/fix_engine.py b/fix_engine.py new file mode 100644 index 0000000000..556066307c --- /dev/null +++ b/fix_engine.py @@ -0,0 +1,39 @@ +import re + +file_path = r'C:\project\F_project1\CodeWhale\crates\tui\src\core\engine.rs' +with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + +# 1. Add helper function after runtime_prompt_text +marker = ' "\n )\n}\n\n/// Spawn the engine' +helper_fn = ''' " + ) +} + +/// Check if a user message contains real user input (not just runtime metadata). +/// Returns true if the message has actual user text content beyond internal tags. +fn has_real_user_content(text: &str) -> bool { + // Strip known internal tags and check if meaningful content remains + let stripped = text + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", ""); + + // Check if there's non-whitespace content after stripping tags + let trimmed = stripped.trim(); + !trimmed.is_empty() && trimmed.len() > 10 // Allow for minimal metadata +} + +/// Spawn the engine''' + +if marker in content: + content = content.replace(marker, helper_fn) + print("OK: helper function added") +else: + print("WARN: marker not found for helper function") + +with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) diff --git a/pr-body-agents.md b/pr-body-agents.md new file mode 100644 index 0000000000..065bf3ca34 --- /dev/null +++ b/pr-body-agents.md @@ -0,0 +1,33 @@ +## Summary + +Auto-collapse completed sub-agents in the Agents sidebar panel. Non-running agents now show only a single line (label), freeing vertical space for active agents. + +## Problem + +Completed/failed/interrupted/cancelled sub-agents each occupied **2 lines** in the sidebar (label + detail line), wasting space that could be used for running agents or other content. With many agents, the sidebar became unnecessarily crowded. + +## Solution + +In `subagent_panel_lines()`, check the agent status before rendering the detail line. If the agent is not running (i.e. completed, failed, interrupted, cancelled), skip the detail line entirely and only render the single-line label. + +**Before:** +``` +✓ explore foo ← 2 lines per agent + abc123 · 3 steps · 12.3s +✗ build failed ← 2 lines per agent + def456 · 7 steps · 45.6s +● analysis running ← 2 lines per agent + ghi789 · 2 steps · 5.1s · parsing output... +``` + +**After:** +``` +✓ explore foo ← 1 line (collapsed) +✗ build failed ← 1 line (collapsed) +● analysis running ← 2 lines (expanded: label + detail) + ghi789 · 2 steps · 5.1s · parsing output... +``` + +## File changed + +`crates/tui/src/tui/sidebar.rs` — 5 lines added: `is_completed` check + early `continue` before the detail line. diff --git a/pr-body-cjk.md b/pr-body-cjk.md new file mode 100644 index 0000000000..4204ad8553 --- /dev/null +++ b/pr-body-cjk.md @@ -0,0 +1,27 @@ +## Bug + +When `assistant_text` contains CJK characters (3-byte UTF-8), the byte slice +`&assistant_text[..SUMMARY_LIMIT.saturating_sub(3)]` (byte 277) can land in +the middle of a multi-byte sequence, causing a panic: + +``` +byte index 277 is not a char boundary (it is inside a 3-byte UTF-8 sequence) +``` + +## Fix + +Replace the raw byte-index slice with `truncate_with_ellipsis()` which already +finds the nearest safe char boundary via `char_indices()`. + +## Change + +```diff +-format!("{}...", &assistant_text[..SUMMARY_LIMIT.saturating_sub(3)]) ++crate::utils::truncate_with_ellipsis(&assistant_text, SUMMARY_LIMIT, "...") +``` + +1 line changed. + +## Files + +- crates/tui/src/runtime_threads.rs:1437 diff --git a/pr-body-fuzz.md b/pr-body-fuzz.md new file mode 100644 index 0000000000..7d69aae87a --- /dev/null +++ b/pr-body-fuzz.md @@ -0,0 +1,9 @@ +The optional `fuzz` parameter was required to attempt the leading-indentation fuzzy fallback when exact search found zero matches. This forced the model to make two calls on every edit that needed fuzzy matching (first without fuzz -> error -> second with fuzz: true), causing a round-trip delay. + +Fix: remove the `fuzz` gate from the count == 0 branch. The tool now automatically retries with indentation-tolerant fuzzy matching when exact search produces no results. The `fuzz` parameter is kept in the schema for backward compatibility but marked deprecated. + +Changes: +- crates/tui/src/tools/file.rs: `if count == 0 && fuzz` -> `if count == 0` (always retry fuzzy fallback) +- crates/tui/src/tools/file.rs: removed dead `else if count == 0 { error }` branch +- crates/tui/src/tools/file.rs: updated description to note automatic fuzzy fallback +- crates/tui/src/tools/file.rs: marked fuzz parameter as deprecated in schema diff --git a/pr-body-paste.md b/pr-body-paste.md new file mode 100644 index 0000000000..51c84c445d --- /dev/null +++ b/pr-body-paste.md @@ -0,0 +1,21 @@ +## Problem + +Pasting large text into the CodeWhale TUI composer (e.g. pasting a 20KB log file or code block) immediately converts it to an `@.deepseek/pastes/paste-xxx.md` mention. The user's text vanishes from the composer and is replaced by a cryptic `@file` reference they did not ask for. This is confusing — the user expected to see their pasted text and be able to review/edit it before sending. + +## Root Cause + +`insert_paste_text()` calls `consolidate_large_input_if_oversized()` at paste time, which checks if `char_count(input) > MAX_SUBMITTED_INPUT_CHARS` (16,000 chars). When exceeded, it immediately writes the text to a paste file and replaces the composer input with `@.deepseek/pastes/paste-xxx.md`. + +The consolidation is useful at submit time (safety net), but at paste time it is surprising and confusing. + +## Fix + +1. **Remove the immediate consolidation from `insert_paste_text()`** — the text now stays in the composer as-is after pasting. +2. **Keep the consolidation as a safety net at submit time** — the same logic already runs when the user presses Enter, so the model still gets the `@file` reference if needed. +3. **Improve the toast message** — explain what happened when the submit-time consolidation fires. + +The user now sees their full pasted text in the composer and can review/edit before sending. The consolidation only triggers when they press Enter if the text is still over the 16K char limit. + +## Files + +- `crates/tui/src/tui/app.rs`: commented out `self.consolidate_large_input_if_oversized()` in `insert_paste_text`, updated toast message diff --git a/pr-body-rlm.md b/pr-body-rlm.md new file mode 100644 index 0000000000..7dd7c6c47a --- /dev/null +++ b/pr-body-rlm.md @@ -0,0 +1,9 @@ +RLM sessions that produce large stdout/stderr (e.g. reading a local log file, dumping a large JSON table, or printing diagnostic output) currently inline the full preview into the parent tool result. On long-running RLM sessions this bloat accumulates and pressures the parent context window. + +Fix: when `rlm_eval` stdout or stderr exceeds 1000 characters, the full body is stored as a `var_handle` in the handle store. The tool result returns a short inline note (`"N chars; retrieve via handle_read"`) plus `stdout_handle` / `stderr_handle` fields containing the handle reference. The model calls `handle_read` for bounded projections. + +Changes: +- `rlm.rs`: Added `STDOUT_HANDLE_THRESHOLD_CHARS` constant +- `rlm.rs`: Added `route_output()` helper that stores large text as a var_handle +- `rlm.rs`: Modified `rlm_eval` execute to route stdout/stderr >= 1k chars into handles +- `rlm.rs`: Updated description to document the new handle-routing behavior diff --git a/pr-body-voice.md b/pr-body-voice.md new file mode 100644 index 0000000000..414b4c544f --- /dev/null +++ b/pr-body-voice.md @@ -0,0 +1,32 @@ +## Summary + +Validate terminal-safe voice shortcut and STT helper setup for the voice input feature shipping in v0.8.45/v0.8.46. + +## Completed + +- **`voice_input.rs`**: Added `diagnose_voice_setup()` async function that checks: + - Missing/invalid `voice_input_command` config + - Inexecutable binary (spawns `--version` as probe) + - Permission/spawn failures with clear error messages +- **`voice_input.rs`**: Added `terminal_detection_tests` module with: + - `chord_likely_reaches_tui()` heuristic function documenting known-consumed chords + - Tests for common chords (Ctrl-K consumed, Ctrl-L/Ctrl-C safe) + - Candidate shortcut test matrix (F2, F3, Alt-Space, Ctrl-] etc.) +- **`docs/VOICE_INPUT_TERMINALS.md`**: Terminal compatibility matrix documenting Ctrl-K behavior across 13 terminal emulators + STT helper setup checklist + recommended safe chords +- **`docs/KEYBINDINGS.md`**: Added Voice input section documenting the Ctrl-K caveat with cross-reference to the new terminal matrix doc + +## Not completed (needs manual verification) + +- **Actual terminal testing** on macOS Terminal.app, iTerm2, Ghostty, Warp, Windows Terminal, Linux terminals +- **Final default chord selection** — the matrix lists candidates but the final binding needs human verification +- **Manual QA checklist** for terminals that consume modifier keys +- **Compilation verification** — changes made to a feature branch (work/v0.8.45-flash) without Rust CI available + +## Files changed + +``` +crates/tui/src/tui/voice_input.rs | 110 +++++++++++++++++ +docs/KEYBINDINGS.md | 22 ++++ +docs/VOICE_INPUT_TERMINALS.md | 87 +++++++++++++ +3 files changed, 219 insertions(+) +``` diff --git a/pr-body.md b/pr-body.md new file mode 100644 index 0000000000..8eb17c9974 --- /dev/null +++ b/pr-body.md @@ -0,0 +1,30 @@ +## Summary + +Add real-time incremental output display for shell execution commands. The TUI now displays shell output **while the command is still running**, instead of hiding all output until completion. + +## Changes + +- **`ExecCell`**: Added `live_output: Option` field to store incremental output during execution +- **`history.rs`**: Modified render logic to show `live_output` when `output` is not yet available (priority: final output > live output > hints) +- **`app.rs`**: Added `poll_shell_progress()` method that polls `ShellManager` during idle frames and updates matching `ExecCell` entries +- **`ui.rs`**: Wired `poll_shell_progress()` into the event loop after `tick_quit_armed()` +- **All ExecCell construction sites**: Added `live_output: None` to 8 files + +## How it works + +1. During idle frames, the TUI polls `ShellManager.list_jobs()` for running shell processes +2. Running exec cells are matched to shell jobs by command prefix +3. Live output (tail of stdout/stderr) is written to `ExecCell.live_output` +4. The renderer displays it in the transcript immediately + +## Files changed +``` +crates/tui/src/tui/active_cell.rs | 1 + +crates/tui/src/tui/app.rs | 69 +++++++++++++++++++++++++++++ +crates/tui/src/tui/history.rs | 11 ++++ +crates/tui/src/tui/sidebar.rs | 5 ++ +crates/tui/src/tui/tool_routing.rs | 2 + +crates/tui/src/tui/transcript.rs | 1 + +crates/tui/src/tui/ui.rs | 2 + +crates/tui/src/tui/ui/tests.rs | 3 + +``` diff --git a/shell-live-progress.patch b/shell-live-progress.patch new file mode 100644 index 0000000000..e69de29bb2 From cd71a51e4a6e71fb63cce4efad476f8bec310c28 Mon Sep 17 00:00:00 2001 From: donglovejava <211940267+donglovejava@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:08:56 +0800 Subject: [PATCH 3/3] fix(ci): restore nightly cross-target builds and auto-tag idempotency Nightly builds: - Add artifact existence check to skip redundant builds for the same commit - Add build retry logic (up to 3 attempts) for transient failures - Add nightly-complete summary job for branch protection rules - Improve concurrency group to use ref_name instead of full ref Auto-tag idempotency: - Add semver validation for workspace version - Add annotated tags with release metadata - Add push retry logic with exponential backoff - Fail fast if version consistency check fails - Add concurrency control to prevent race conditions Addresses v0.8.64 reliability concerns for nightly builds and auto-tagging. --- .github/workflows/auto-tag.yml | 61 +++++++++++++++++++--- .github/workflows/nightly.yml | 93 +++++++++++++++++++++++++++++++++- 2 files changed, 145 insertions(+), 9 deletions(-) diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index f73794482d..173f1ff2ec 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -25,6 +25,12 @@ on: permissions: contents: write +# Serialize auto-tag runs per branch to prevent race conditions when +# multiple version bumps land on main in quick succession. +concurrency: + group: auto-tag-${{ github.ref_name }} + cancel-in-progress: false + jobs: tag: runs-on: ubuntu-latest @@ -39,11 +45,20 @@ jobs: - name: Read workspace version id: ver run: | + # Robust version parsing: extract version from workspace Cargo.toml + # and validate it matches semver format vX.Y.Z (without v prefix). v="$(grep -E '^version = "' Cargo.toml | head -n1 | sed -E 's/^version = "([^"]+)".*/\1/')" if [ -z "$v" ]; then echo "::error::Could not parse workspace version from Cargo.toml" >&2 exit 1 fi + + # Validate semver format (digits.digits.digits) + if ! echo "$v" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Workspace version '$v' is not valid semver (expected X.Y.Z)" >&2 + exit 1 + fi + echo "version=$v" >> "$GITHUB_OUTPUT" echo "tag=v$v" >> "$GITHUB_OUTPUT" echo "Workspace version: $v" @@ -54,10 +69,12 @@ jobs: TAG: ${{ steps.ver.outputs.tag }} run: | git fetch --tags --quiet - if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null \ - || git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then + + # Check both local tags and remote tags for idempotency + if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null 2>&1 || \ + git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then echo "exists=true" >> "$GITHUB_OUTPUT" - echo "Tag ${TAG} already exists; nothing to do." + echo "::notice::Tag ${TAG} already exists; nothing to do (idempotent)." else echo "exists=false" >> "$GITHUB_OUTPUT" echo "Tag ${TAG} does not exist; will create." @@ -65,7 +82,13 @@ jobs: - name: Verify version consistency if: steps.check.outputs.exists == 'false' - run: ./scripts/release/check-versions.sh + run: | + # Run version consistency check before creating tag + # This prevents tagging inconsistent versions + ./scripts/release/check-versions.sh || { + echo "::error::Version consistency check failed. Aborting tag creation." >&2 + exit 1 + } - name: Create and push tag if: steps.check.outputs.exists == 'false' @@ -74,9 +97,33 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag "${TAG}" - git push origin "${TAG}" - echo "Pushed ${TAG}. release.yml should now run (requires RELEASE_TAG_PAT for trigger)." + + # Create annotated tag with release information + git tag -a "${TAG}" -m "Release ${TAG} + +Automatically created by auto-tag workflow. +Commit: ${GITHUB_SHA} +Repository: ${GITHUB_REPOSITORY} +" + + # Push with retry logic for network resilience + max_retries=3 + retry_count=0 + while [[ ${retry_count} -lt ${max_retries} ]]; do + if git push origin "${TAG}" 2>/dev/null; then + echo "Successfully pushed ${TAG}." + echo "release.yml should now run (requires RELEASE_TAG_PAT for trigger)." + exit 0 + fi + retry_count=$((retry_count + 1)) + if [[ ${retry_count} -lt ${max_retries} ]]; then + echo "Push attempt ${retry_count} failed; retrying in 10s..." + sleep 10 + fi + done + + echo "::error::Failed to push tag ${TAG} after ${max_retries} attempts." >&2 + exit 1 - name: Warn if PAT missing if: steps.check.outputs.exists == 'false' && env.HAS_PAT != 'true' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 53fcd34a71..bece6780b8 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -8,8 +8,12 @@ on: permissions: contents: read +# Prevent concurrent nightly builds for the same branch; cancel older runs +# when a new push arrives. The group key includes the branch name so that +# nightly builds on main are serialized, but manual dispatches from other +# branches can still run independently. concurrency: - group: nightly-${{ github.ref }} + group: nightly-${{ github.ref_name }} cancel-in-progress: true env: @@ -18,7 +22,61 @@ env: DEEPSEEK_BUILD_SHA: ${{ github.sha }} jobs: + # Idempotency guard: skip the entire workflow if nightly artifacts for + # this exact commit already exist. This prevents wasteful rebuilds when + # multiple commits land on main in quick succession or when a workflow + # is re-run manually. + check-artifacts: + runs-on: ubuntu-latest + outputs: + skip: ${{ steps.check.outputs.skip }} + steps: + - uses: actions/checkout@v4 + - name: Check if nightly artifacts already exist + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_SHA: ${{ github.sha }} + run: | + short_sha="${COMMIT_SHA::12}" + # Check for any artifact from this commit in the last 14 days + # (matching the retention-days setting below). If all platform + # artifacts exist, we can safely skip the build. + artifact_count=$(gh run list --workflow nightly.yml \ + --branch main \ + --created ">=$(date -d '14 days ago' +%Y-%m-%d)" \ + --json databaseId \ + --jq 'length') + + if [[ "${artifact_count}" -gt 0 ]]; then + latest_run_id=$(gh run list --workflow nightly.yml \ + --branch main \ + --created ">=$(date -d '14 days ago' +%Y-%m-%d)" \ + --json databaseId,headSha \ + --jq '[.[] | select(.headSha == env.COMMIT_SHA)] | first | .databaseId' 2>/dev/null || true) + + if [[ -n "${latest_run_id}" ]]; then + artifacts=$(gh run view "${latest_run_id}" --json artifacts --jq '.artifacts | length') + # We expect 10 artifacts (5 platforms × 2 binaries: codewhale + codewhale-tui) + if [[ "${artifacts}" -ge 10 ]]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Nightly artifacts for commit ${short_sha} already exist; skipping build." + else + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "Nightly artifacts incomplete (${artifacts}/10); rebuilding." + fi + else + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "No nightly run found for this commit; building." + fi + else + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "No recent nightly runs found; building." + fi + build: + needs: [check-artifacts] + if: ${{ needs.check-artifacts.outputs.skip != 'true' }} name: Build ${{ matrix.artifact_name }} strategy: fail-fast: false @@ -86,7 +144,20 @@ jobs: sudo apt-get install -y libdbus-1-dev pkg-config - name: Build shell: bash - run: cargo build --release --locked --target ${{ matrix.target }} + run: | + # Retry build up to 2 times on transient failure (e.g. network + # issues fetching crates, OOM kills on shared runners). + for attempt in 1 2 3; do + if cargo build --release --locked --target ${{ matrix.target }}; then + exit 0 + fi + if [[ ${attempt} -lt 3 ]]; then + echo "Build attempt ${attempt} failed; retrying in 30s..." + sleep 30 + fi + done + echo "Build failed after 3 attempts" >&2 + exit 1 - name: Stage artifact id: stage shell: bash @@ -113,3 +184,21 @@ jobs: name: ${{ steps.stage.outputs.name }} path: nightly/* retention-days: 14 + + # Summary job that aggregates the matrix build results and provides a + # single status indicator for branch protection rules. + nightly-complete: + needs: [build] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check matrix build status + run: | + if [[ "${{ needs.build.result }}" == "success" ]]; then + echo "✅ All nightly builds completed successfully" + elif [[ "${{ needs.build.result }}" == "skipped" ]]; then + echo "ℹ️ Nightly builds were skipped (artifacts already exist)" + else + echo "❌ Nightly builds failed or were cancelled" + exit 1 + fi