feat: add Checks system for YAML workflow execution - #84
Open
Ziinc wants to merge 10 commits into
Open
Conversation
Ziinc
force-pushed
the
claude/checks-yaml-workflows-72iq7u
branch
from
July 26, 2026 11:25
ed8928b to
172326f
Compare
Contributor
📸 App QA screenshotsRe-ran the flow whose spec this PR adds or modifies — 3 captures. Other specs in the library were not run. checks-tab
Each bullet under a capture is what the spec claims that image should show — open the PNG and check it. commit |
Implements a Checks tab in ShowWorkspace that reads workflow definitions from .treq/workflows/*.yaml and allows manual job execution with per-step pass/fail results. - core/checks.rs: business logic for listing and running workflows, with parallel job execution via std::thread and DB persistence of results - commands/checks.rs: Tauri command wrappers (list_workflows, run_workflow_job, run_workflow) - local_db.rs: workflow_runs table for persisting job results - NAPI dispatch: three new arms so JS integration tests use real Rust code - ChecksTab.tsx: React component with per-job and per-workflow Run buttons, step-level pass/fail icons (data-testid for test queries) - ShowWorkspace.tsx: new Checks tab trigger and content panel - api.ts / api-types.ts: TypeScript types and invoke wrappers - Rust unit tests (9), Rust integration tests (7), JS integration tests (6)
- Add explicit repository trust requirement before running any workflow job; untrusted repos see a UI banner with a "Trust Repository" button - Validate workflow filenames (reject path separators and '..') and working-directory values (reject absolute paths and parent traversal) to prevent path escape from .treq/workflows/ - Canonicalize file paths and confirm containment within the workflows directory before reading or executing any workflow file - Skip invalid YAML files when listing workflows instead of failing the entire listing call - Replace unbounded thread spawning in run_workflow with a bounded executor (MAX_CONCURRENT_JOBS = 4) using a channel semaphore pattern - Add per-step timeout (STEP_TIMEOUT_SECS = 60) with polling loop; kills the child process and returns a timeout error on expiry - Switch step execution from cmd.output() to spawn() + Stdio::null() for safe stdin handling in the NAPI/test context while enabling timeout - Add repo_trust table to local.db; add is_repo_trusted / trust_repo functions in local_db, Tauri commands, and NAPI dispatch - Update JS integration tests to pre-trust repos before running jobs - Update Rust integration tests to call trust_repo before execution tests - Add new unit tests: trust guard, path traversal rejection, invalid YAML skip, filename and working-directory validation
- Apply biome/cargo formatting to satisfy the verify CI job - Hoist userEvent.setup() into a beforeEach hook in the checks integration test, per the local/user-event-setup-in-setup lint rule - Remove unused get_latest_workflow_run and the now-orphaned WorkflowRunRecord struct, flagged by local/no-unused-pub-rust-functions; historical run retrieval is follow-up work with no caller yet - Add scripts/screenshot/specs/checks-tab.spec.tsx covering the trust gate, enabled-after-trust state, and per-step pass/fail rendering
Captures per-step output from workflow checks into newline-delimited JSON,
queries it with bundled DuckDB, and adds a GitHub-Actions-style viewer.
Log collection
- Step execution switches from discarding output to piped stdout/stderr,
drained on separate threads so large output on both pipes cannot deadlock
- Each line is timestamped, ANSI-stripped, and classified info/warning/error
by content (not by stream — cargo and npm write progress to stderr)
- Written to .treq/runs/{run_id}/{job_id}.jsonl; job ids are sanitised to a
safe filename charset
- A timed-out step records an explicit error line before being terminated
Run history
- workflow_runs now holds one row per invocation, with a new
workflow_job_results child table carrying per-job status and log path
- Legacy per-job workflow_runs rows are migrated by dropping the old shape
- "Run All" groups every job under a single run; re-running creates a new
run so older runs stay browsable
- A failing job no longer aborts unrelated in-flight jobs
Querying and export
- core/checks_logs.rs reads the JSONL via DuckDB read_json_auto with
level, step and substring filters plus pagination
- Log paths are resolved and confirmed to stay inside .treq/runs
- Export renders a run's logs to a plain .log file
UI
- LogsBrowser: monospace lines, timestamp column, amber warnings and red
errors, level/step/search filters, Back and Export
- ChecksTab gains a run-history list and opens logs from a job or a step;
results and the open log view reset when the workspace changes
Tests
- 28 Rust unit tests, 11 JS integration tests
- New checks-logs-browser screenshot spec covering history, viewer and filter
…lter Level filter - LogQuery.level becomes levels: Vec<String>, applied as a SQL IN clause; an empty or absent list means no filtering - New LogLevelFilter multi-select built on DropdownMenuCheckboxItem, shared by the run logs browser and the new Logs tab; the menu stays open on select so several levels can be ticked in one pass Repo-wide data source - A DuckDB `logs` view spans .treq/runs/**/*.jsonl, recovering run_id and job_id from each file's path so lines can be grouped across runs - get_repo_logs browses every run's lines with level and search filters SQL explorer - run_logs_sql executes ad-hoc queries against the `logs` view and returns columns plus stringified rows - Only single read-only statements are accepted (SELECT, WITH, DESCRIBE, SHOW, EXPLAIN, SUMMARIZE); writes, multiple statements, and ATTACH/COPY/INSTALL style keywords are rejected before reaching DuckDB - Results are capped by wrapping the user's query in an outer LIMIT UI - LogsTab is offered on the home repo only, with Browse and SQL Explorer views - Browse renders timestamp, run id, job id and message per line, reusing the logs browser's level colouring - LogsSqlExplorer provides a query editor, starter templates, a result grid with NULL rendering, and inline error reporting Tests - 35 Rust unit tests covering multi-level filtering, the cross-run view and SQL validation - 7 new JS integration tests for the Logs tab, level filter and explorer - New logs-tab-sql-explorer screenshot spec; existing specs updated for the multi-select
Log records now follow the OpenTelemetry log data model, and both logs surfaces gain a chart, line selection and a path into an agent session. OpenTelemetry structure - Records carry time_unix_nano, observed_time_unix_nano, severity_number, severity_text, body, trace_id, span_id, resource and scope - A run becomes a trace and each job a span; ids are derived from the run and job so re-reads stay stable rather than random - Step, stream and run/job identity move into attributes, using OTel's own log.iostream convention for the standard stream - The DuckDB view projects the standard field names and lifts attributes into columns, so queries say job_id rather than digging through the map - UI level names are translated to severity text at the query boundary, so stored records stay standard Timeseries chart - New query_log_timeseries buckets records by time and severity - EChart wraps Apache ECharts for React: the instance lives in a ref, only the option object is pushed on update, and the canvas plus its resize observer are disposed on unmount - LogsTimeseriesChart stacks severities over a shared time axis above the feed Logs Explorer - Renamed from SQL Explorer - Templates move from a button row into a dropdown, each item carrying a description, and expand from three to five Selection and send-to-agent - useLineSelection supports drag-select for contiguous runs and a multi-select mode for gathering scattered lines - LogFeed is shared by the run and repo-wide browsers, with a Send to agent action for the selection - The explorer can send a whole result set - All three open a fresh agent session seeded with the content, following the existing create-agent-with-review pattern Tests - 41 Rust unit tests covering the OTel shape, severity mapping, trace/span ids and timeseries bucketing - 195 JS tests including drag-select, multi-select, template listing and the OTel columns - New logs-otel-chart-agent screenshot spec; existing specs updated
Log records
- Replace the earlier ad-hoc OTel-ish shape with exactly the 12 fields of the
OTel log data model, camelCased: timestamp, observedTimestamp, traceId,
spanId, traceFlags, severityText, severityNumber, body, resource,
instrumentationScope, attributes, eventName
- timestamp/observedTimestamp are RFC3339 with nanosecond precision so text
ordering matches chronological ordering
- body is a struct ({ message }) rather than a bare string, since OTel bodies
are typed values
- run_id/job_id/step_index/step_name/stream move under attributes; nothing
treq-specific is added as a top-level field
- The `logs` DuckDB view is now a strict passthrough of those 12 columns —
reading it looks exactly like reading any other OTel log source
- Explorer templates query through attributes.* and body.message accordingly
Bugs from visual QA
- Chart legend could overlap the top bar when it wrapped to two lines in a
narrow panel; grid.top now reserves enough room for the two-line case
- Empty time buckets were dropped, compressing quiet periods; the chart now
fills the full time range so every bucket gets an x-axis tick
- Bucketing used CAST(... AS BIGINT), which rounds in DuckDB rather than
truncating, so a .5s+ timestamp could land in the next second's bucket and
visually split one second's records into two bars — switched to floor()
- Timeline is built in ascending order so the oldest bucket renders left and
the newest right
Tests
- 42 Rust unit tests, including a regression test for the rounding bug and an
assertion that a written record's key set is exactly the 12 OTel fields
- 195 JS tests updated for the new field names and query shapes
- Screenshot spec updated; QA'd against real DuckDB output, not mocked data
Rebased onto main (which had independently gained GitHub PR/issue integration in src-tauri/src/commands/github.rs and elsewhere). Both branches touched lib.rs's command registration list and ShowWorkspace.tsx's imports; kept both sides' additions. The combined import block left two overlapping import statements for several modules in ShowWorkspace.tsx (api.ts, several components, several ui/* primitives) after manual conflict resolution — consolidated into the single already-sorted block, keeping ChecksTab/LogsTab which only existed on this branch. Moving the checks/logs functions (run_workflow*, get_run_logs, get_repo_logs, run_logs_sql, get_log_timeseries, export_run_logs) out of api.ts pushed it over the repo's 500-line cap. Relocated them to api-extra.ts, the file this repo already uses for API overflow and the one place the local eslint rules already special-case (max-params off, command-wrapper coverage), rather than introducing a new file the rules don't know about. dispatch.rs picked up cargo fmt's reflow of code main added; no behavior change.
Ziinc
force-pushed
the
claude/checks-yaml-workflows-72iq7u
branch
from
July 29, 2026 20:17
5904b7c to
715a1b2
Compare
Drops the "bundled" feature from the duckdb crate, which forced every clean build to compile DuckDB's full C++ amalgamation via cc/cmake (10-15+ minutes and several GB of disk each time this session). libduckdb-sys has a built-in DUCKDB_DOWNLOAD_LIB env var that instead fetches the matching official prebuilt libduckdb from DuckDB's own GitHub releases for the build target, caches it under target/duckdb-download/, and links against it dynamically with an rpath baked in. This path only exists when "bundled" is off, so both changes are required together. Wired directly into the npm scripts and CI env blocks that invoke cargo (napi build, tauri dev/build, cargo test/check/fmt in ci.yml) rather than a repo-wide .cargo/config.toml, so it only applies where these builds are actually triggered. release.yml (the macOS app bundle build) is deliberately left on the static bundled build for now: DUCKDB_DOWNLOAD_LIB links dynamically with an rpath pointing at the CI runner's target/duckdb-download path, which is a materially different risk for a shipped binary than for local/CI dev builds and needs separate verification that Tauri's bundler carries the .dylib along correctly. Verified end to end: a clean `cargo build --lib` compiles in ~3.5 minutes (vs 10-15+ with bundled) with zero cc/C++ invocations, and all 42 checks_logs Rust tests plus the full 307-test JS suite pass against the dynamically-linked library.
Replaces the per-script/per-workflow env var wiring (npm scripts, ci.yml) with a single [env] block in a repo-local .cargo/config.toml. Cargo auto-discovers this by walking up from the current directory, so it's picked up by any cargo invocation anywhere in the workspace (napi build, tauri dev/build, cargo test/check/fmt in CI) without needing to set it at each call site individually. Cargo.toml's duckdb dependency already lost the "bundled" feature in the prior commit; DUCKDB_DOWNLOAD_LIB only takes effect once that feature is off, so this only fully applies to the release build (still static-bundled, deliberately left alone pending separate verification of Tauri's macOS resource bundling for a dynamically-linked libduckdb).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements a Checks tab in ShowWorkspace that reads workflow definitions
from
.treq/workflows/*.yamland allows manual job execution with per-steppass/fail results.
Core
core/checks.rs: business logic for listing and running workflows, withbounded-concurrency job execution and DB persistence of results
commands/checks.rs: Tauri command wrappers (list_workflows,run_workflow_job,run_workflow,is_repo_trusted,trust_repo)local_db.rs:workflow_runstable for persisting job results, plus arepo_trusttable backing the execution guardSecurity
explicitly trusted first; untrusted repos surface a banner and disabled
run buttons instead of silently executing repository-defined commands
..,and resolved paths are canonicalized and confirmed to stay inside
.treq/workflows/working-directoryvalues reject absolute paths and parenttraversal
Execution
thread-per-job spawning
Frontend
ChecksTab.tsx: per-job and per-workflow Run buttons, step-levelpass/fail icons, and the trust banner
ShowWorkspace.tsx: new Checks tab trigger and content panelapi.ts/api-types.ts: TypeScript types and invoke wrappersTests
scripts/screenshot/specs/checks-tab.spec.tsxcovering the trust gate,the enabled-after-trust state, and per-step pass/fail rendering
Follow-up (not in this PR)