This composite action is a thin wrapper around the CLI contract:
qasight run [--config <path>] [--cycle <name>] --ci [--no-persist], or, with no
qasight.yaml in the workspace (or with results),
qasight ingest [<results>...] [--cycle <name>] --ci
It runs one cycle on the host runner (or records the results an earlier step
wrote), echoes the NDJSON event stream to the job log, and exposes the final
plan_finished event as step outputs.
Keep your test step as it is, make it write results (JUnit XML, Jest/Vitest
--json, Playwright JSON, go test -json, .trx, ...), and add the action
after it. It finds the result files itself (by content; files git tracks are
skipped), records them under a cycle named after the repository, and comments
on the pull request:
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- run: npm ci && npx jest --ci --json --outputFile=test-results/jest.json
- uses: arieleven/qasight@v1
if: ${{ !cancelled() }} # record failing runs tooresults(optional, default empty): result files to record instead of running a cycle (globs, space or newline separated). Empty with no config = find themhistory(optional, defaultauto): keep the run history (SQLite) in the Actions cache between runs, so each run is compared with the ones before it. Skipped whenDATABASE_URLor the config'sdatabase.urlis set;offnever cachesconfig-path(optional, default empty): path toqasight.yaml; empty means discoverycycle(optional, default empty): cycle name; empty runs the only cycle,allruns every cycle and publishes a summary, comment, annotations and alert for eachci-mode(optional, defaulttrue): NDJSON on stdoutpersist(optional, defaulttrue): writeplan_result.jsonand the run history recordpython-version(optional, default3.11)package(optional, defaultqasight): what to pip install, e.g.qasight==1.2.0,qasight[bdd]when the cycle hasbehaveorbehavexstages, or several requirements separated by spaces (qasight pytest-xdist)job-summary(optional, defaulttrue): write the run to the job summarycomment-on-pr(optional, defaultauto: on when recording results with no config, off when running a configured cycle): onpull_requestevents, keep one comment per cycle on the pull request with the same summary (edited on later pushes, not re-posted). Needspermissions: pull-requests: write; with a read-only token (fork PRs) it warns and moves onannotations(optional, defaulttrue): print failing tests as::errorannotations (qasight report annotations), shown on the failing line in the pull request's "Files changed". Needs no permissionslack-webhook,teams-webhook(optional, default empty): incoming-webhook URLs for a chat alert (qasight notify). Pass secrets, e.g.${{ secrets.SLACK_WEBHOOK_URL }}; empty sends nothingnotify-when(optional, defaultfailure):failure(red run or a newly flaky test),change(differs from the previous run) oralwaysflaky-issues(optional, defaultfalse): turn onTESTO_ISSUES=githubfor the run, which opens one issue (labelflaky-test) for each test the run made flaky and closes it once the test is stable (qasight issues sync). Needspermissions: issues: writecoverage(optional): coverage report(s) your tests write, space-separated (Cobertura or JaCoCo XML, LCOV, Go cover profile). Line coverage is stored with the run, and the summary and PR comment show it and how much it moved against the baselineserver-url,server-token(optional): also upload each recorded run to a team server (qasight ingest --server), with the token from a secret (${{ secrets.TESTO_API_TOKEN }}). The summary and comment still come from the Actions-cache history; a failed upload is a warninghistory-artifact(optional, defaulttrue): with the cached history (history: auto), also upload the SQLite file as atesto-history-<workflow>-<job>-...artifact kept 7 days, soqasight ui --from-githubopens the CI history on your machine.falseskips itstatic-site(optional, defaultfalse): after the run, write the history as a read-only site (qasight export-site) and upload it for GitHub Pages, see Results on GitHub Pagesstatic-site-base-path(optional, default:/<repo>/, or/for<owner>.github.io): the path the site is served under;/for a custom domainsetup(optional, defaultfalse): record nothing; set QASight up in this repository instead (qasight init --scan --attach --pr) and open the setup pull request. Needscontents: writeandpull-requests: write; the one-time workflow is in Getting Startedai-summary(optional, defaultfalse): add an AI summary of the failures to a failed run's summary and comment (qasight report markdown --ai). Set the provider in the step'senv:TESTO_AI_PROVIDER(openai,anthropic,openai_compatible),TESTO_AI_MODEL,TESTO_AI_BASE_URL, and the key from a secret (OPENAI_API_KEY,ANTHROPIC_API_KEY, orTESTO_AI_KEY_ENVnaming another variable). A provider that doesn't answer is one line, not a failurecomment-artifact(optional, defaultfalse): onpull_requestevents, upload the comment as atesto-commentartifact instead of posting it (outputcomment_dir), for pull requests from forks, whose token is read-only. A second workflow posts it withpost-comments-frompost-comments-from(optional): in aworkflow_runworkflow, only post the comments acomment-artifactrun left in this folder, on the open pull request whose head is the commit that run tested (never a number from the artifact). Files must start with the<!-- testo-summary:marker;@mentions are defused. Runs nothing else.qasight init --forkswrites both workflows; see Pull requests from forksscorecard(optional, defaultfalse):truescores the repository after the tests withqasight scorecard scan,onlyinstead of them. The scorecard goes to the job summary and one pull request comment; SARIF, an HTML page and a badge go to thescorecard_diroutput. See CI-CD Pipeline Setupscorecard-path(optional, default.): folder or git URL to scorescorecard-min-score(optional): fail the step under this score (0-100)scorecard-no-new(optional, defaultfalse): fail the step when the change brings a high or critical finding the last scored version didn't havescorecard-tools(optional): run only these scorecard tools (comma-separated ids)github-token(optional, default${{ github.token }}): token for the comment and issues
After the run the action calls qasight ci publish, the same step any other CI runs, for the
summary, the comment, the annotations and the alert (with a pinned qasight older than that
command it runs them one by one). The summary is qasight report markdown: pass/fail counts, a per-stage table, failing tests and
the tests that changed outcome since the cycle's previous run (for a pull request, the latest
run on its target branch). That history comes from the Actions cache (history: auto), or from
a database.url / team server shared between jobs.
exit_code(0–4, seedocs/CLI Commands/Troubleshooting and Error Codes.md)status(success,failure,invalid_input,infra_failure,internal_error)summary_json: the finalplan_finished(orerror) NDJSON eventsummary_path: the same JSON saved under$RUNNER_TEMP/testo-summary.jsoncomment_dir: withcomment-artifacton a pull request, the folder with the comment filesscorecard_dir: withscorecard, the folder withscorecard.md,scorecard.sarif,scorecard.htmlandbadge.svgscorecard_sarif: withscorecard, the SARIF file forgithub/codeql-action/upload-sarif
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: arieleven/qasight@v1
with:
cycle: sample-pytests
comment-on-pr: "true"
slack-webhook: ${{ secrets.SLACK_WEBHOOK_URL }}with permissions: { contents: read, pull-requests: write } on the job for the comment
(add issues: write for flaky-issues).
static-site: "true" publishes the run history as a read-only copy of the web UI: the
dashboard, runs, tests, flaky tests and comparisons, at https://<owner>.github.io/<repo>/.
Turn on Settings → Pages → Source: GitHub Actions, then:
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- run: pytest --junitxml=reports/junit.xml || true
- uses: arieleven/qasight@v1
with:
results: reports/*.xml
static-site: "true"
pages:
needs: test
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- id: deploy
uses: actions/deploy-pages@v5Publish from the default branch only: the site shows everything in the cached history. The
same folder comes from qasight export-site anywhere else (GitLab Pages, a bucket).
A fork's pull request gets a read-only GITHUB_TOKEN, so the comment can't be posted from its
run. Leave it as an artifact there and post it from a workflow_run workflow, which runs in
your repository with a token that can write and checks out no code from the fork:
# .github/workflows/qasight.yml (the test workflow)
- uses: arieleven/qasight@v1
with:
cycle: unit
comment-on-pr: "true"
comment-artifact: "true"# .github/workflows/qasight-comment.yml
name: QASight comment
on:
workflow_run:
workflows: [QASight]
types: [completed]
permissions:
actions: read
pull-requests: write
jobs:
comment:
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
continue-on-error: true # no artifact: nothing to comment
with:
name: testo-comment
path: testo-comment
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- uses: arieleven/qasight@v1
with:
post-comments-from: testo-commentqasight init --forks writes both.
The v1.0 inputs ghost-mode, stream-json, runner-image and runner-prebuilt
drove the removed headless/Docker runner and are gone; qasight run --ci already
streams NDJSON and runs stages as host subprocesses. Use Dockerfile.qasight-runner
as the job container if you want a pinned image. The run_id output is gone too:
it read the old summary JSON, which qasight run does not emit.
- Publish immutable tags for every patch release:
v1.0.0,v1.0.1, ... - Keep moving major tag
v1pointing to latest stablev1.x. - For strict supply-chain policies, pin consumers to a commit SHA.
Inputs reach the action's shell steps as environment variables, never pasted into the script,
so an input such as cycle: ${{ github.head_ref }} can't run commands.