Easy Setup - 2026-11-01¶
Context¶
A team with tests and CI already had to read Getting Started, write qasight.yaml by hand, find the right result-file flag for each runner and write a workflow before seeing anything. qasight demo and the /try page show what QASight does on sample or pasted results; nothing set up the team's own project. Four changes were approved together, each its own pull request:
qasight initdetects the stack, writes the config and the CI workflow, and records a first run.- The GitHub Action works with no config file: it finds result files itself and posts the pull request comment.
- Copy-paste setup for each stack and each CI system (GitHub Actions, GitLab CI, Jenkins, CircleCI).
- A dry run that shows the pull request comment the last results would get.
Decision¶
- One place knows the stacks.
qasight/services/project_setup.pyreads project files only (nothing is run):detect_project()returns aStackper runner with theqasight.yamlstage that runs it so its results are read (native equipment for pytest, Jest, Vitest and Playwright; acommandstage withjunit_xmlfor Go, .NET, Maven, Gradle, RSpec and PHPUnit) and how to install its dependencies.find_result_files()finds results a test run left behind by content, skipping files git tracks (fixtures) and dependency folders.qasight/services/ci_templates.pyturns the same facts into CI files.qasight init, the Action's zero-config mode and the docs snippets all use these, so they can't disagree. qasight initwrites, then records. It writesqasight.yaml,.github/workflows/qasight.ymland missing.gitignorelines, keeps any file that exists unless--force, and records a first run (existing results through the ingest path, else oneqasight run). A failing test still counts as recorded (exit0). The old wizard stays asqasight init --interactive. Reference: Command Reference ยง qasight init.- The Action needs no config.
integrations/github-action/run_testo_action.pypicksqasight ingest --cioverqasight run --ciwhen the workspace has no config or theresultsinput is set, andqasight ingestwith no file arguments usesfind_result_files(), so the Action, a GitLab job and a laptop find results the same way.comment-on-pr: autois on in that mode only, so configured users who never asked for a comment don't start getting one. History is the part a fresh runner lacks: aprepare-historystep pointsDATABASE_URLat a SQLite file under~/.cache/qasightand the composite action restores and saves it withactions/cache(new key per run and step, restored by workflow-and-job prefix), unless the user already set a database. Only the database is cached so the cache doesn't grow with report copies. - Snippets are generated.
ci_templates.pyalso renders GitLab CI, Jenkins and CircleCI files: the project's own test command with a result-file flag (ci_test_command()), thenqasight ingestafter it whatever the outcome (after_script,post { always },when: always), with the history cached per branch where the CI has a cache. Outside GitHub,qasightis installed with uv so a Node, Go or JVM image needs no Python. Setup Snippets is written byscripts/export_setup_snippets.pyfrom those functions for a typical project of each stack, andtests/contract/ci/test_setup_snippets.pyfails when it is stale, so the page shows exactly whatqasight init --ci <system>writes.qasight initnever overwrites an existing pipeline file; it writes its own next to it. - The preview is a real run in a throwaway history.
qasight try(qasight/cli/commands/try_cmd.py) doesn't fake the comment: it runsqasight ingestandqasight report markdownas child processes withDATABASE_URLandTESTO_HOMEpointing at a temporary folder, asqasight demodoes, after copying a local SQLite history in so the "since the last run onmain" part is real. A server or Postgres history isn't copied; the preview is then a first run's comment.--from-githubusesgh run downloadrather than its own API client, so sign-in, GitHub Enterprise hosts and tokens aregh's job. It reuses the/trypage's idea (see what you'd get before setting anything up) for your own repository instead of a pasted file. - A monorepo is scanned, not guessed (
qasight init --scan). Plainqasight initreads the project files at the top of the repository only, soapi/pyproject.toml,web/package.jsonandservices/billing/go.modgave one wrong stage or none.qasight/services/repo_scan.pywalks the folders (skipping dependency and hidden folders, 6 levels deep), runs the samedetect_project()in each folder that has a project file, and writes a stage per runner per folder withtarget_reposet and the folder in its name (api-pytest). When an outer folder's run already picks a folder up (a Maven parent and its modules, Gradle, a root Jest or Vitest, a barepytestorpytest testsabove it) the outer stage is kept and the inner one is listed as left out.dotnet teststarts at a.sln, else in each test project's folder, since it refuses a folder with several projects. CI install and test commands run in their folder in a subshell ((cd api && ...)), because GitLab runs a job's script lines in one shell.--scanprints the stages and asks before it writes; no answer (no terminal) writes nothing,--yesskips the question and--dry-runshows the files. - Proof instead of an AI guess (
--check). An optional AI pass that refines the proposal was considered and dropped: it reads the same files the scanner reads and can't tell whether a command works.qasight/services/stage_check.pyasks each runner to list its tests instead (pytest --collect-only,jest --listTests,vitest list,playwright test --list,go test -list,dotnet test --list-tests,rspec --dry-run,phpunit --list-tests) and counts them. A stage that lists none is left out; one whose runner isn't installed or fails to list is kept and flagged. Listing loads test code (conftest, compilation), so it's opt-in and never runs a test. Node runners run from the project's ownnode_modules/.bin, so nothing is downloaded. Maven and Gradle have no listing mode that doesn't run tests and are not checked. - Generated YAML is text, not a dump. The templates write comments and key order the way a person would, and the tests load the output back (
discover_and_load,yaml.safe_load) so it stays valid.
Consequences¶
- The detection rules are heuristics. A project that installs test dependencies some other way gets a workflow whose install step needs a tweak;
--dry-runshows it before anything is written. - Cypress is detected but not added, since it needs the app running first.
--scanonly looks at a local folder. Scanning a GitHub repository without cloning and running its code (for example from the results its CI already publishes) is a possible later step.- Adding a stack means one
Stackentry inproject_setup.pyand, for CI, its language setup inci_templates.py.