Skip to content

feat: add Checks system for YAML workflow execution - #84

Open
Ziinc wants to merge 10 commits into
mainfrom
claude/checks-yaml-workflows-72iq7u
Open

feat: add Checks system for YAML workflow execution#84
Ziinc wants to merge 10 commits into
mainfrom
claude/checks-yaml-workflows-72iq7u

Conversation

@Ziinc

@Ziinc Ziinc commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

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

  • core/checks.rs: business logic for listing and running workflows, with
    bounded-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_runs table for persisting job results, plus a
    repo_trust table backing the execution guard
  • NAPI dispatch: new arms so JS integration tests exercise real Rust code

Security

  • Trusted-repository guard: running any job requires the repo to be
    explicitly trusted first; untrusted repos surface a banner and disabled
    run buttons instead of silently executing repository-defined commands
  • Path containment: workflow filenames reject path separators and ..,
    and resolved paths are canonicalized and confirmed to stay inside
    .treq/workflows/
  • Step working-directory values reject absolute paths and parent
    traversal
  • Invalid YAML files are skipped rather than failing the whole listing

Execution

  • Bounded concurrency (max 4 concurrent jobs) instead of unbounded
    thread-per-job spawning
  • Per-step timeout (60s) that kills the child process on expiry
  • Fail-fast: a failed step stops the remaining steps in that job

Frontend

  • ChecksTab.tsx: per-job and per-workflow Run buttons, step-level
    pass/fail icons, and the trust banner
  • ShowWorkspace.tsx: new Checks tab trigger and content panel
  • api.ts / api-types.ts: TypeScript types and invoke wrappers

Tests

  • Rust unit tests (15), Rust integration tests (7), JS integration tests (6)
  • scripts/screenshot/specs/checks-tab.spec.tsx covering the trust gate,
    the enabled-after-trust state, and per-step pass/fail rendering

Follow-up (not in this PR)

  • Historical run retrieval and display
  • stdout/stderr capture and output truncation
  • Workflow format documentation

@Ziinc
Ziinc force-pushed the claude/checks-yaml-workflows-72iq7u branch from ed8928b to 172326f Compare July 26, 2026 11:25
@Ziinc Ziinc changed the title Add Checks system for YAML workflow execution feat: add Checks system for YAML workflow execution Jul 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📸 App QA screenshots

Re-ran the flow whose spec this PR adds or modifies — 3 captures. Other specs in the library were not run.

⬇️ Download the screenshots

checks-tab
  • checks-tab-01-untrusted.png
    • An amber/yellow warning banner is visible reading "Trust this repository to enable running workflow checks." with a "Trust Repository" button on its right.
    • The workflow card below shows the title "Pull request checks" with the filename "ci.yaml" underneath, and lists "Greet Job" and "Verify Job".
    • All run buttons ("Run All", "Run Greet Job", "Run Verify Job") appear visually disabled/greyed out.
    • Every step name has a grey neutral dot icon beside it — no green checkmarks and no red X icons anywhere.
  • checks-tab-02-trusted.png
    • The amber trust banner is completely gone from the top of the panel.
    • The run buttons ("Run All", "Run Greet Job", "Run Verify Job") now appear enabled — normal contrast, not greyed out.
    • Step names still show grey neutral dot icons, since nothing has been run yet.
  • checks-tab-03-after-run-all.png
    • Under "Greet Job", both "Say hello" and "Say world" have green checkmark icons.
    • Under "Verify Job", "Failing check" has a red X icon.
    • "Never runs" (the step after the failing one) still shows a grey neutral dot, NOT a green check or red X — it was skipped by fail-fast.
    • There are exactly 2 green checkmarks and exactly 1 red X in the whole panel.

Each bullet under a capture is what the spec claims that image should show — open the PNG and check it.


commit 172326f · workflow run — this comment is updated in place on each run.

claude added 8 commits July 29, 2026 19:38
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
Ziinc force-pushed the claude/checks-yaml-workflows-72iq7u branch from 5904b7c to 715a1b2 Compare July 29, 2026 20:17
claude added 2 commits July 30, 2026 03:42
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants